diff --git a/src/designer/elsa-workflows-studio/src/components.d.ts b/src/designer/elsa-workflows-studio/src/components.d.ts index cf2f48803..86b3c462e 100644 --- a/src/designer/elsa-workflows-studio/src/components.d.ts +++ b/src/designer/elsa-workflows-studio/src/components.d.ts @@ -34,6 +34,7 @@ export namespace Components { "menuItems": Array; } interface ElsaDesignerTree { + "editMode": boolean; "model": WorkflowModel; "selectedActivityIds": Array; } @@ -487,6 +488,7 @@ declare namespace LocalJSX { "menuItems"?: Array; } interface ElsaDesignerTree { + "editMode"?: boolean; "model"?: WorkflowModel; "onActivityDeselected"?: (event: CustomEvent) => void; "onActivitySelected"?: (event: CustomEvent) => void; diff --git a/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx b/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx index 9490532fe..6bf491080 100644 --- a/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx +++ b/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx @@ -1,5 +1,5 @@ -import { Component, Host, h, Prop, State, Event, EventEmitter, Watch } from '@stencil/core'; -import { Map, addConnection, findActivity, getChildActivities, getInboundConnections, getOutboundConnections, removeActivity, removeConnection } from '../../../../utils/utils'; +import {Component, Host, h, Prop, State, Event, EventEmitter, Watch} from '@stencil/core'; +import {Map, addConnection, findActivity, getChildActivities, getInboundConnections, getOutboundConnections, removeActivity, removeConnection} from '../../../../utils/utils'; import { ActivityDescriptor, ActivityDesignDisplayContext, @@ -10,11 +10,11 @@ import { WorkflowModel, WorkflowPersistenceBehavior, } from '../../../../models'; -import { eventBus } from '../../../../services/event-bus'; +import {eventBus} from '../../../../services/event-bus'; import jsPlumb from 'jsplumb'; import * as d3 from 'd3'; import dagreD3 from 'dagre-d3'; -import { registerClickOutside } from 'stencil-click-outside'; +import {registerClickOutside} from 'stencil-click-outside'; import state from '../../../../utils/store'; import uuid = jsPlumb.jsPlumbUtil.uuid; import {ActivityIcon} from '../../../icons/activity-icon'; @@ -30,6 +30,7 @@ import css = languages.css; export class ElsaWorkflowDesigner { @Prop() model: WorkflowModel = {activities: [], connections: [], persistenceBehavior: WorkflowPersistenceBehavior.WorkflowBurst}; @Prop() selectedActivityIds: Array = []; + @Prop() editMode: boolean = true; @Event({eventName: 'workflow-changed', bubbles: true, composed: true, cancelable: true}) workflowChanged: EventEmitter; @Event() activitySelected: EventEmitter; @Event() activityDeselected: EventEmitter; @@ -40,7 +41,7 @@ export class ElsaWorkflowDesigner { inner: SVGElement; svgD3Selected: d3.Selection; innerD3Selected: d3.Selection; - zoomParams: { x: number; y: number; scale: number } = { x: 0, y: 0, scale: 1 }; + zoomParams: { x: number; y: number; scale: number } = {x: 0, y: 0, scale: 1}; dagreD3Renderer: dagreD3.Render = new dagreD3.render(); graph: dagreD3.graphlib.Graph = new dagreD3.graphlib.Graph().setGraph({}); @@ -109,7 +110,7 @@ export class ElsaWorkflowDesigner { eventBus.off(EventTypes.UpdateActivity, this.onUpdateActivity); d3.selectAll('.node').on('click', null); d3.selectAll('.edgePath').on('contextmenu', null); - + } onActivityPicked = async args => { @@ -144,7 +145,7 @@ export class ElsaWorkflowDesigner { }; } - const workflowModel = { ...this.workflowModel, activities: [...this.workflowModel.activities, activity] }; + const workflowModel = {...this.workflowModel, activities: [...this.workflowModel.activities, activity]}; if (targetActivityId) { const existingConnection = workflowModel.connections.find(x => x.targetId == targetActivityId && x.outcome == outcome); @@ -159,7 +160,7 @@ export class ElsaWorkflowDesigner { workflowModel.connections.push(replacementConnection); } else { - workflowModel.connections.push({ sourceId: activity.activityId, targetId: targetActivityId, outcome: outcome }); + workflowModel.connections.push({sourceId: activity.activityId, targetId: targetActivityId, outcome: outcome}); } } @@ -175,10 +176,10 @@ export class ElsaWorkflowDesigner { }; workflowModel.connections.push(replacementConnection); - const connection: ConnectionModel = { sourceId: activity.activityId, targetId: existingConnection.targetId, outcome }; + const connection: ConnectionModel = {sourceId: activity.activityId, targetId: existingConnection.targetId, outcome}; workflowModel.connections.push(connection); } else { - const connection: ConnectionModel = { sourceId: sourceActivityId, targetId: activity.activityId, outcome: outcome }; + const connection: ConnectionModel = {sourceId: sourceActivityId, targetId: activity.activityId, outcome: outcome}; workflowModel.connections.push(connection); } } @@ -194,8 +195,8 @@ export class ElsaWorkflowDesigner { } addConnection(sourceActivityId: string, targetActivityId: string, outcome: string) { - const workflowModel = { ...this.workflowModel }; - const connection: ConnectionModel = { sourceId: sourceActivityId, targetId: targetActivityId, outcome: outcome }; + const workflowModel = {...this.workflowModel}; + const connection: ConnectionModel = {sourceId: sourceActivityId, targetId: targetActivityId, outcome: outcome}; workflowModel.connections.push(connection); this.updateWorkflowModel(workflowModel); this.parentActivityId = null; @@ -203,11 +204,11 @@ export class ElsaWorkflowDesigner { } updateActivity(activity: ActivityModel) { - let workflowModel = { ...this.workflowModel }; + let workflowModel = {...this.workflowModel}; const activities = [...workflowModel.activities]; const index = activities.findIndex(x => x.activityId === activity.activityId); activities[index] = activity; - this.updateWorkflowModel({ ...workflowModel, activities: activities }); + this.updateWorkflowModel({...workflowModel, activities: activities}); } componentWillLoad() { @@ -236,7 +237,7 @@ export class ElsaWorkflowDesigner { const displayContext: ActivityDesignDisplayContext = { activityModel: model, - activityIcon: , + activityIcon: , bodyDisplay: bodyDisplay, outcomes: [...model.outcomes], }; @@ -248,7 +249,8 @@ export class ElsaWorkflowDesigner { this.activityDisplayContexts = displayContexts; } - componentDidRender() {} + componentDidRender() { + } showActivityPicker() { eventBus.emit(EventTypes.ShowActivityPicker); @@ -259,7 +261,7 @@ export class ElsaWorkflowDesigner { } removeActivity(activity: ActivityModel) { - let workflowModel = { ...this.workflowModel }; + let workflowModel = {...this.workflowModel}; const incomingConnections = getInboundConnections(workflowModel, activity.activityId); const outgoingConnections = getOutboundConnections(workflowModel, activity.activityId); @@ -277,14 +279,14 @@ export class ElsaWorkflowDesigner { } removeConnection(sourceId: string, outcome: string) { - let workflowModel = { ...this.workflowModel }; + let workflowModel = {...this.workflowModel}; workflowModel = removeConnection(workflowModel, sourceId, outcome); this.updateWorkflowModel(workflowModel); } applyZoom() { this.zoom = d3.zoom().on('zoom', event => { - const { transform } = event; + const {transform} = event; this.innerD3Selected.attr('transform', transform); this.zoomParams = { x: transform.x, @@ -300,32 +302,37 @@ export class ElsaWorkflowDesigner { const rootActivities = this.getRootActivities(); + // Start node. this.graph.setNode('start', { shape: 'rect', - label: ``, + label: this.editMode + ? `` + : ``, rx: 5, ry: 5, labelType: 'html', class: 'start', }); + // Connections between Start and root activities. rootActivities.forEach(activity => { this.graph.setEdge('start', `${activity.activityId}/start`, { arrowhead: 'undirected', }); - this.graph.setNode(`${activity.activityId}/start`, { shape: 'rect', activity, label: this.renderOutcomeButton(), labelType: 'html', class: 'add' }); - this.graph.setEdge(`${activity.activityId}/start`, activity.activityId, { arrowhead: 'undirected' }); + this.graph.setNode(`${activity.activityId}/start`, {shape: 'rect', activity, label: this.renderOutcomeButton(), labelType: 'html', class: 'add'}); + this.graph.setEdge(`${activity.activityId}/start`, activity.activityId, {arrowhead: 'undirected'}); }); + // Connections between activities and their outcomes. this.workflowModel.activities.forEach(activity => { this.graph.setNode(activity.activityId, this.createActivityOptions(activity)); const displayContext = this.activityDisplayContexts[activity.activityId] || undefined; const outcomes = !!displayContext ? displayContext.outcomes : activity.outcomes || []; outcomes.forEach(outcome => { - this.graph.setNode(`${activity.activityId}/${outcome}`, { shape: 'rect', outcome, activity, label: this.renderOutcomeButton(), labelType: 'html', class: 'add' }); + this.graph.setNode(`${activity.activityId}/${outcome}`, {shape: 'rect', outcome, activity, label: this.renderOutcomeButton(), labelType: 'html', class: 'add'}); this.graph.setEdge(activity.activityId, `${activity.activityId}/${outcome}`, { - label: `

${outcome}

`, + label: `

${outcome}

`, labelpos: 'c', labelType: 'html', arrowhead: 'undirected', @@ -333,8 +340,8 @@ export class ElsaWorkflowDesigner { }); }); - this.workflowModel.connections.forEach(({ sourceId, targetId, outcome }) => { - this.graph.setEdge(`${sourceId}/${outcome}`, targetId, { arrowhead: 'undirected' }); + this.workflowModel.connections.forEach(({sourceId, targetId, outcome}) => { + this.graph.setEdge(`${sourceId}/${outcome}`, targetId, {arrowhead: 'undirected'}); }); } @@ -358,27 +365,52 @@ export class ElsaWorkflowDesigner { this.svgD3Selected.call(this.zoom.scaleTo, scaleAfter); this.innerD3Selected.attr('transform', prevTransform); - d3.selectAll('.node.add').each((n: any) => { - const node = this.graph.node(n) as any; - d3.select(node.elem).on('click', e => { - e.preventDefault(); - d3.selectAll('.node.add svg').classed('text-green-400', false).classed('text-gray-400', true).classed('hover:text-blue-500', true); - this.parentActivityId = node.activity.activityId; - this.parentActivityOutcome = node.outcome; - if (e.shiftKey) { - d3.select(node.elem).select('svg').classed('text-green-400', true).classed('text-gray-400', false).classed('hover:text-blue-500', false); - return; - } - this.showActivityPicker(); - }); - }); + if (this.editMode) { + d3.selectAll('.node.add').each((n: any) => { + const node = this.graph.node(n) as any; + d3.select(node.elem).on('click', e => { + e.preventDefault(); + d3.selectAll('.node.add svg').classed('text-green-400', false).classed('text-gray-400', true).classed('hover:text-blue-500', true); + this.parentActivityId = node.activity.activityId; + this.parentActivityOutcome = node.outcome; - d3.selectAll('.node.start').each((n: any) => { - const node = this.graph.node(n) as any; - d3.select(node.elem).on('click', e => { - this.showActivityPicker(); + if (this.editMode && e.shiftKey) { + d3.select(node.elem).select('svg').classed('text-green-400', true).classed('text-gray-400', false).classed('hover:text-blue-500', false); + return; + } + + this.showActivityPicker(); + }); }); - }); + + d3.selectAll('.node.start').each((n: any) => { + const node = this.graph.node(n) as any; + d3.select(node.elem).on('click', e => { + this.showActivityPicker(); + }); + }); + + d3.selectAll('.edgePath').append(appendClickableEl).attr('class', 'label-clickable'); + + function appendClickableEl() { + const originalD = this.querySelector('.path').getAttribute('d'); + const newPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + newPath.setAttribute('d', originalD); + return this.appendChild(newPath); + } + + d3.selectAll('.edgePath').each((edg: any) => { + const edge = this.graph.edge(edg) as any; + d3.select(edge.elem).on('contextmenu', e => { + e.preventDefault(); + const from = edg.v.split('/'); + const to = edg.w.split('/'); + const fromActivityId = from[0]; + const outcome = from[1] || to[1]; + this.removeConnection(fromActivityId, outcome); + }); + }); + } d3.selectAll('.node.activity').each((n: any) => { const node = this.graph.node(n) as any; @@ -387,7 +419,7 @@ export class ElsaWorkflowDesigner { d3.select(node.elem).on('click', () => { // If a parent activity was selected to connect to: - if (this.parentActivityId && this.parentActivityOutcome) { + if (this.editMode && this.parentActivityId && this.parentActivityOutcome) { this.addConnection(this.parentActivityId, activityId, this.parentActivityOutcome); } else { // When clicking an activity: @@ -405,32 +437,15 @@ export class ElsaWorkflowDesigner { this.rerenderTree(); } }); - d3.select(node.elem) + + if (this.editMode) { + d3.select(node.elem) .select('button') .on('click', evt => { evt.stopPropagation(); this.handleContextMenuChange(evt.clientX, evt.clientY, true, node.activity); }); - }); - - d3.selectAll('.edgePath').append(appendClickableEl).attr('class', 'label-clickable'); - function appendClickableEl() { - const originalD = this.querySelector('.path').getAttribute('d'); - const newPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - newPath.setAttribute('d', originalD); - return this.appendChild(newPath); - } - - d3.selectAll('.edgePath').each((edg: any) => { - const edge = this.graph.edge(edg) as any; - d3.select(edge.elem).on('contextmenu', e => { - e.preventDefault(); - const from = edg.v.split('/'); - const to = edg.w.split('/'); - const fromActivityId = from[0]; - const outcome = from[1] || to[1]; - this.removeConnection(fromActivityId, outcome); - }); + } }); } @@ -453,11 +468,12 @@ export class ElsaWorkflowDesigner { } createOutcomeActivityOptions() { - return { shape: 'circle', label: this.renderOutcomeButton(), labelType: 'html', class: 'add', width: 32, height: 32 }; + return {shape: 'circle', label: this.renderOutcomeButton(), labelType: 'html', class: 'add', width: 32, height: 32}; } renderOutcomeButton() { - return ` + const cssClass = this.editMode ? 'hover:text-blue-500 cursor-pointer' : 'cursor-default'; + return ` `; } @@ -465,17 +481,9 @@ export class ElsaWorkflowDesigner { renderActivity(activity: ActivityModel) { const displayContext = this.activityDisplayContexts[activity.activityId] || undefined; const cssClass = !!this.selectedActivities[activity.activityId] ? 'border-blue-600' : 'border-white hover:border-blue-600' - return `
-
-
-
- ${displayContext?.activityIcon || ''} -
-
-

${activity.displayName}

-
-
+ + const contextButton = this.editMode + ? `
+
` + : ''; + + return `
+
+
+
+ ${displayContext?.activityIcon || ''}
+
+

${activity.displayName}

+
+ ${contextButton}
${this.renderActivityBody(activity.description)} @@ -542,7 +563,7 @@ export class ElsaWorkflowDesigner { Edit
-
+
this.onDeleteActivityClick(e)} diff --git a/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-workflow-definition-editor-screen/elsa-workflow-definition-editor-screen.tsx b/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-workflow-definition-editor-screen/elsa-workflow-definition-editor-screen.tsx index 0e5cd2f05..0569cbd23 100644 --- a/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-workflow-definition-editor-screen/elsa-workflow-definition-editor-screen.tsx +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-workflow-definition-editor-screen/elsa-workflow-definition-editor-screen.tsx @@ -329,7 +329,7 @@ export class ElsaWorkflowDefinitionEditorScreen { renderCanvas() { return (
- this.designer = el}/> + this.designer = el}/> {this.renderWorkflowSettingsButton()} diff --git a/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-viewer-screen/elsa-workflow-instance-journal.tsx b/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-viewer-screen/elsa-workflow-instance-journal.tsx index e5ff8850e..163fa4483 100644 --- a/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-viewer-screen/elsa-workflow-instance-journal.tsx +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-viewer-screen/elsa-workflow-instance-journal.tsx @@ -246,8 +246,19 @@ export class ElsaWorkflowInstanceJournal { if (!value) continue; + + let valueText = null; - filteredRecordData[key] = value; + if (typeof value == 'string') + valueText = value; + else if (typeof value == 'object') + valueText = JSON.stringify(value); + else if (typeof value == 'undefined') + valueText = null; + else + valueText = value.toString(); + + filteredRecordData[key] = valueText; } const deltaTimeText = !!deltaTime ? deltaTime.asHours() > 1 @@ -331,12 +342,12 @@ export class ElsaWorkflowInstanceJournal { }; renderActivityStateTab = () => { - + const activityModel = !!this.workflowModel && this.selectedActivityId ? this.workflowModel.activities.find(x => x.activityId === this.selectedActivityId) : null; - - if(!activityModel) + + if (!activityModel) return

No activity selected

; - + return (
{JSON.stringify(activityModel, null, 2)}
diff --git a/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-viewer-screen/elsa-workflow-instance-viewer-screen.tsx b/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-viewer-screen/elsa-workflow-instance-viewer-screen.tsx index 2f24ca679..bc3c9eb00 100644 --- a/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-viewer-screen/elsa-workflow-instance-viewer-screen.tsx +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-viewer-screen/elsa-workflow-instance-viewer-screen.tsx @@ -196,6 +196,7 @@ export class ElsaWorkflowInstanceViewerScreen {
this.designer = el} + editMode={false} selectedActivityIds={[this.selectedActivityId]} onActivitySelected={e => this.onActivitySelected(e)} onActivityDeselected={e => this.onActivityDeselected(e)}