Implement drag&drop to change connections
This commit is contained in:
parent
6220a49708
commit
dee30999b4
83
src/designer/elsa-workflows-studio/package-lock.json
generated
83
src/designer/elsa-workflows-studio/package-lock.json
generated
|
|
@ -19,6 +19,7 @@
|
|||
"autoprefixer": "10.2.4",
|
||||
"axios": "^0.21.1",
|
||||
"cssnano": "^4.1.10",
|
||||
"dragula": "^3.7.3",
|
||||
"el-transition": "^0.0.7",
|
||||
"eslint": "7.20.0",
|
||||
"js-event-bus": "^1.0.3",
|
||||
|
|
@ -287,6 +288,11 @@
|
|||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/atoa": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/atoa/-/atoa-1.0.0.tgz",
|
||||
"integrity": "sha1-DMDpGkgOc4+SPrwQNnZHF3mzSkk="
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.2.4",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.2.4.tgz",
|
||||
|
|
@ -527,6 +533,15 @@
|
|||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"node_modules/contra": {
|
||||
"version": "1.9.4",
|
||||
"resolved": "https://registry.npmjs.org/contra/-/contra-1.9.4.tgz",
|
||||
"integrity": "sha1-9TveQtfltZhcrk2ZqNYQUm3o8o0=",
|
||||
"dependencies": {
|
||||
"atoa": "1.0.0",
|
||||
"ticky": "1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/copyfiles": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz",
|
||||
|
|
@ -591,6 +606,14 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/crossvent": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/crossvent/-/crossvent-1.5.5.tgz",
|
||||
"integrity": "sha1-rSCHjkkh6b5z2daXb4suzQ9xoLE=",
|
||||
"dependencies": {
|
||||
"custom-event": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-blank-pseudo": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz",
|
||||
|
|
@ -943,6 +966,11 @@
|
|||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
|
||||
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="
|
||||
},
|
||||
"node_modules/custom-event": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz",
|
||||
"integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
||||
|
|
@ -1054,6 +1082,15 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dragula": {
|
||||
"version": "3.7.3",
|
||||
"resolved": "https://registry.npmjs.org/dragula/-/dragula-3.7.3.tgz",
|
||||
"integrity": "sha512-/rRg4zRhcpf81TyDhaHLtXt6sEywdfpv1cRUMeFFy7DuypH2U0WUL0GTdyAQvXegviT4PJK4KuMmOaIDpICseQ==",
|
||||
"dependencies": {
|
||||
"contra": "1.9.4",
|
||||
"crossvent": "1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/el-transition": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/el-transition/-/el-transition-0.0.7.tgz",
|
||||
|
|
@ -4612,6 +4649,11 @@
|
|||
"xtend": "~4.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ticky": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ticky/-/ticky-1.0.1.tgz",
|
||||
"integrity": "sha1-t8+nHnaPHJAAxJe5FRswlHxQ5G0="
|
||||
},
|
||||
"node_modules/timsort": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
|
||||
|
|
@ -5185,6 +5227,11 @@
|
|||
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
|
||||
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="
|
||||
},
|
||||
"atoa": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/atoa/-/atoa-1.0.0.tgz",
|
||||
"integrity": "sha1-DMDpGkgOc4+SPrwQNnZHF3mzSkk="
|
||||
},
|
||||
"autoprefixer": {
|
||||
"version": "10.2.4",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.2.4.tgz",
|
||||
|
|
@ -5385,6 +5432,15 @@
|
|||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"contra": {
|
||||
"version": "1.9.4",
|
||||
"resolved": "https://registry.npmjs.org/contra/-/contra-1.9.4.tgz",
|
||||
"integrity": "sha1-9TveQtfltZhcrk2ZqNYQUm3o8o0=",
|
||||
"requires": {
|
||||
"atoa": "1.0.0",
|
||||
"ticky": "1.0.1"
|
||||
}
|
||||
},
|
||||
"copyfiles": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz",
|
||||
|
|
@ -5435,6 +5491,14 @@
|
|||
"which": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"crossvent": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/crossvent/-/crossvent-1.5.5.tgz",
|
||||
"integrity": "sha1-rSCHjkkh6b5z2daXb4suzQ9xoLE=",
|
||||
"requires": {
|
||||
"custom-event": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"css-blank-pseudo": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz",
|
||||
|
|
@ -5713,6 +5777,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"custom-event": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz",
|
||||
"integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
||||
|
|
@ -5805,6 +5874,15 @@
|
|||
"is-obj": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"dragula": {
|
||||
"version": "3.7.3",
|
||||
"resolved": "https://registry.npmjs.org/dragula/-/dragula-3.7.3.tgz",
|
||||
"integrity": "sha512-/rRg4zRhcpf81TyDhaHLtXt6sEywdfpv1cRUMeFFy7DuypH2U0WUL0GTdyAQvXegviT4PJK4KuMmOaIDpICseQ==",
|
||||
"requires": {
|
||||
"contra": "1.9.4",
|
||||
"crossvent": "1.5.5"
|
||||
}
|
||||
},
|
||||
"el-transition": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/el-transition/-/el-transition-0.0.7.tgz",
|
||||
|
|
@ -8770,6 +8848,11 @@
|
|||
"xtend": "~4.0.1"
|
||||
}
|
||||
},
|
||||
"ticky": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ticky/-/ticky-1.0.1.tgz",
|
||||
"integrity": "sha1-t8+nHnaPHJAAxJe5FRswlHxQ5G0="
|
||||
},
|
||||
"timsort": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
|
||||
|
|
|
|||
|
|
@ -204,119 +204,6 @@ export class ElsaWorkflowDesigner {
|
|||
this.workflowChanged.emit(model);
|
||||
}
|
||||
|
||||
onConnectionCreated(connection: ConnectionModel) {
|
||||
const workflowModel = {...this.workflowModel};
|
||||
workflowModel.connections = [...workflowModel.connections, connection];
|
||||
|
||||
this.updateWorkflowModel(workflowModel);
|
||||
}
|
||||
|
||||
onConnectionDetached(connection: ConnectionModel) {
|
||||
const workflowModel = {...this.workflowModel};
|
||||
workflowModel.connections = workflowModel.connections.filter(x => !(x.sourceId == connection.sourceId && x.targetId == connection.targetId && x.outcome == connection.outcome));
|
||||
|
||||
this.updateWorkflowModel(workflowModel);
|
||||
}
|
||||
|
||||
onAddButtonClick() {
|
||||
this.showActivityPicker();
|
||||
}
|
||||
|
||||
onOutcomeButtonClick(e: Event, outcome?: string, activityId?: string) {
|
||||
e.preventDefault();
|
||||
this.parentActivityId = activityId;
|
||||
this.parentActivityOutcome = outcome;
|
||||
this.showActivityPicker();
|
||||
}
|
||||
|
||||
render() {
|
||||
const renderedActivities = new Set<string>();
|
||||
|
||||
return (
|
||||
<Host class="workflow-canvas flex-1 flex" ref={el => this.el = el}>
|
||||
<div class="flex-1 text-gray-200">
|
||||
<div class="p-10">
|
||||
<div class="canvas select-none" ref={el => this.canvasElement = el}>
|
||||
<div class="tree">
|
||||
<ul>
|
||||
<li>
|
||||
<div class="inline-flex flex flex-col items-center">
|
||||
<button id="start-button"
|
||||
onClick={() => this.onAddButtonClick()}
|
||||
type="button"
|
||||
class="px-6 py-3 border border-transparent text-base leading-6 font-medium rounded-md text-white bg-green-600 hover:bg-green-500 focus:outline-none focus:border-green-700 focus:shadow-outline-green active:bg-green-700 transition ease-in-out duration-150">
|
||||
Start
|
||||
</button>
|
||||
</div>
|
||||
{this.renderTree(this.getRootActivities(), true, renderedActivities)}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
||||
renderTree(activities: Array<ActivityModel>, isRoot: boolean, renderedActivities: Set<string>): any {
|
||||
const list = activities.filter(x => !renderedActivities.has(x.activityId));
|
||||
const cssClass = isRoot ? "root" : undefined;
|
||||
|
||||
for (const activity of list)
|
||||
renderedActivities.add(activity.activityId);
|
||||
|
||||
if (list.length == 0)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<ul class={cssClass}>
|
||||
{activities.map(x => {
|
||||
const activityId = x.activityId;
|
||||
const children = getChildActivities(this.workflowModel, activityId);
|
||||
const displayContext = this.activityDisplayContexts[activityId];
|
||||
|
||||
return <li key={x.activityId}>
|
||||
<div class="inline-flex flex flex-col items-center">
|
||||
{isRoot ? this.renderOutcomeButton(`start-button-plus-${x.activityId}`, '', e => this.onOutcomeButtonClick(e, null, null)) : undefined}
|
||||
{this.renderActivity(displayContext)}
|
||||
{this.renderOutcomeButtons(displayContext)}
|
||||
</div>
|
||||
{children.length > 0 ? this.renderTree(children, false, renderedActivities) : undefined}
|
||||
</li>;
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
renderOutcomeButtons(displayContext: ActivityDesignDisplayContext) {
|
||||
const activityId = displayContext.activityModel.activityId;
|
||||
|
||||
return (
|
||||
<div class="flex flex-row space-x-6">
|
||||
{displayContext.outcomes.map(x => this.renderOutcomeButton(`${activityId}-${x}`, x, e => this.onOutcomeButtonClick(e, x, activityId)))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderOutcomeButton(id: string, outcome: string, clickHandler: (e: MouseEvent) => void) {
|
||||
return (
|
||||
<div class="my-6 flex flex-col items-center">
|
||||
{outcome && outcome.length > 0 ? <div
|
||||
class="mb-4 relative z-10 px-2.5 py-0.5 rounded-full text-xs font-medium leading-4 bg-cool-gray-100 text-cool-gray-800 capitalize">{outcome}</div> : undefined}
|
||||
<a key={id} id={id} href="#" onClick={e => clickHandler(e)}>
|
||||
<svg class="h-8 w-8 text-gray-400 hover:text-blue-500 cursor-pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderActivity(displayContext: ActivityDesignDisplayContext) {
|
||||
return <elsa-designer-tree-activity displayContext={displayContext}/>;
|
||||
}
|
||||
|
||||
getJsPlumbConnections(): Array<any> {
|
||||
const rootActivities = getChildActivities(this.workflowModel, null);
|
||||
|
||||
|
|
@ -395,4 +282,181 @@ export class ElsaWorkflowDesigner {
|
|||
}
|
||||
));
|
||||
}
|
||||
|
||||
onConnectionCreated(connection: ConnectionModel) {
|
||||
const workflowModel = {...this.workflowModel};
|
||||
workflowModel.connections = [...workflowModel.connections, connection];
|
||||
|
||||
this.updateWorkflowModel(workflowModel);
|
||||
}
|
||||
|
||||
onConnectionDetached(connection: ConnectionModel) {
|
||||
const workflowModel = {...this.workflowModel};
|
||||
workflowModel.connections = workflowModel.connections.filter(x => !(x.sourceId == connection.sourceId && x.targetId == connection.targetId && x.outcome == connection.outcome));
|
||||
|
||||
this.updateWorkflowModel(workflowModel);
|
||||
}
|
||||
|
||||
onAddButtonClick() {
|
||||
this.showActivityPicker();
|
||||
}
|
||||
|
||||
onOutcomeButtonClick(e: Event, outcome?: string, activityId?: string) {
|
||||
e.preventDefault();
|
||||
this.parentActivityId = activityId;
|
||||
this.parentActivityOutcome = outcome;
|
||||
this.showActivityPicker();
|
||||
}
|
||||
|
||||
async onDragStartActivity(e: DragEvent, activityDisplayContext: ActivityDesignDisplayContext) {
|
||||
const activityModel = activityDisplayContext.activityModel;
|
||||
e.dataTransfer.setData("text", activityModel.activityId);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
}
|
||||
|
||||
onDragOverOutcome(e: DragEvent, outcome: string, sourceActivityId: string) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
onDropOnOutcome(e: DragEvent, outcome: string, sourceActivityId) {
|
||||
const targetActivityId = e.dataTransfer.getData('text');
|
||||
|
||||
if (sourceActivityId === targetActivityId)
|
||||
return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const workflowModel = {...this.workflowModel};
|
||||
const targetActivityDisplayContext = this.activityDisplayContexts[targetActivityId];
|
||||
let connections = [...workflowModel.connections];
|
||||
|
||||
// Remove current connection where dropped activity is target.
|
||||
connections = connections.filter(x => x.targetId !== targetActivityId)
|
||||
|
||||
// Check if there is already a connection.
|
||||
const existingConnection = connections.find(x => x.sourceId == sourceActivityId && x.outcome == outcome);
|
||||
|
||||
if (existingConnection) {
|
||||
// Try to find a matching outcome on the dropped activity to attach the existing activity to.
|
||||
const newOutcome = targetActivityDisplayContext.outcomes.find(x => x == existingConnection.outcome) || targetActivityDisplayContext.outcomes[0];
|
||||
|
||||
existingConnection.sourceId = targetActivityId;
|
||||
existingConnection.outcome = newOutcome;
|
||||
|
||||
// Create new connection.
|
||||
const newConnection: ConnectionModel = {
|
||||
sourceId: sourceActivityId,
|
||||
targetId: targetActivityId,
|
||||
outcome: outcome
|
||||
};
|
||||
|
||||
workflowModel.connections = [...connections, newConnection];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create new connection.
|
||||
const newConnection: ConnectionModel = {
|
||||
sourceId: sourceActivityId,
|
||||
targetId: targetActivityId,
|
||||
outcome: outcome
|
||||
};
|
||||
|
||||
workflowModel.connections = [...connections, newConnection];
|
||||
}
|
||||
|
||||
this.updateWorkflowModel(workflowModel);
|
||||
}
|
||||
|
||||
render() {
|
||||
const renderedActivities = new Set<string>();
|
||||
|
||||
return (
|
||||
<Host class="workflow-canvas flex-1 flex" ref={el => this.el = el}>
|
||||
<div class="flex-1 text-gray-200">
|
||||
<div class="p-10">
|
||||
<div class="canvas select-none" ref={el => this.canvasElement = el}>
|
||||
<div class="tree">
|
||||
<ul>
|
||||
<li>
|
||||
<div class="inline-flex flex flex-col items-center">
|
||||
<button id="start-button"
|
||||
onClick={() => this.onAddButtonClick()}
|
||||
type="button"
|
||||
class="px-6 py-3 border border-transparent text-base leading-6 font-medium rounded-md text-white bg-green-600 hover:bg-green-500 focus:outline-none focus:border-green-700 focus:shadow-outline-green active:bg-green-700 transition ease-in-out duration-150">
|
||||
Start
|
||||
</button>
|
||||
</div>
|
||||
{this.renderTree(this.getRootActivities(), true, renderedActivities)}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
||||
renderTree(activities: Array<ActivityModel>, isRoot: boolean, renderedActivities: Set<string>): any {
|
||||
const list = activities.filter(x => !renderedActivities.has(x.activityId));
|
||||
const cssClass = isRoot ? "root" : undefined;
|
||||
|
||||
for (const activity of list)
|
||||
renderedActivities.add(activity.activityId);
|
||||
|
||||
if (list.length == 0)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<ul class={cssClass}>
|
||||
{activities.map(x => {
|
||||
const activityId = x.activityId;
|
||||
const children = getChildActivities(this.workflowModel, activityId);
|
||||
const displayContext = this.activityDisplayContexts[activityId];
|
||||
|
||||
return <li key={x.activityId}>
|
||||
<div class="inline-flex flex flex-col items-center">
|
||||
{isRoot ? this.renderOutcomeButton(`start-button-plus-${x.activityId}`, '', activityId, e => this.onOutcomeButtonClick(e, null, null)) : undefined}
|
||||
{this.renderActivity(displayContext)}
|
||||
{this.renderOutcomeButtons(displayContext)}
|
||||
</div>
|
||||
{children.length > 0 ? this.renderTree(children, false, renderedActivities) : undefined}
|
||||
</li>;
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
renderOutcomeButtons(displayContext: ActivityDesignDisplayContext) {
|
||||
const activityId = displayContext.activityModel.activityId;
|
||||
|
||||
return (
|
||||
<div class="flex flex-row space-x-6">
|
||||
{displayContext.outcomes.map(x => this.renderOutcomeButton(`${activityId}-${x}`, x, activityId, e => this.onOutcomeButtonClick(e, x, activityId)))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderOutcomeButton(id: string, outcome: string, activityId: string, clickHandler: (e: MouseEvent) => void) {
|
||||
return (
|
||||
<div class="my-6 flex flex-col items-center">
|
||||
{outcome && outcome.length > 0 ? <div
|
||||
class="mb-4 relative z-10 px-2.5 py-0.5 rounded-full text-xs font-medium leading-4 bg-cool-gray-100 text-cool-gray-800 capitalize">{outcome}</div> : undefined}
|
||||
<a key={id} id={id} href="#" onClick={e => clickHandler(e)}
|
||||
onDragOver={e => this.onDragOverOutcome(e, outcome, activityId)}
|
||||
onDrop={e => this.onDropOnOutcome(e, outcome, activityId)}
|
||||
>
|
||||
<svg class="h-8 w-8 text-gray-400 hover:text-blue-500 cursor-pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderActivity(displayContext: ActivityDesignDisplayContext) {
|
||||
return <elsa-designer-tree-activity displayContext={displayContext} draggable={true}
|
||||
onDragStart={e => this.onDragStartActivity(e, displayContext)}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ namespace Elsa.Server.Api.Endpoints.WorkflowDefinitions
|
|||
from connection in request.Connections
|
||||
let sourceActivity = request.Activities.FirstOrDefault(x => x.ActivityId == connection.SourceActivityId)
|
||||
let targetActivity = request.Activities.FirstOrDefault(x => x.ActivityId == connection.TargetActivityId)
|
||||
where sourceActivity != null && targetActivity != null
|
||||
where sourceActivity != null && targetActivity != null && connection.Outcome is not null and not ""
|
||||
select connection;
|
||||
|
||||
return validConnections;
|
||||
|
|
|
|||
Loading…
Reference in a new issue