From afb312654f4dfce93c417c191f91d73d1daa6bcd Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 17 Jun 2022 23:16:03 +0200 Subject: [PATCH] Incremental work on embedding activities --- .../src/components.d.ts | 8 +- .../activities/flowchart/flowchart.tsx | 2 +- .../flowchart/flowchart/assign-parent.ts | 8 +- .../components/activities/flowchart/shapes.ts | 31 +++++-- .../default-activity-template.tsx | 81 ++++++++++++------- .../activity/default-activity-driver.tsx | 17 +--- 6 files changed, 85 insertions(+), 62 deletions(-) diff --git a/src/designer/elsa-workflows-designer/src/components.d.ts b/src/designer/elsa-workflows-designer/src/components.d.ts index 4bfa788d1..34dc26b63 100644 --- a/src/designer/elsa-workflows-designer/src/components.d.ts +++ b/src/designer/elsa-workflows-designer/src/components.d.ts @@ -66,11 +66,11 @@ export namespace Components { "value": string; } interface ElsaDefaultActivityTemplate { + "activity": Activity; "activityJson": string; "activityType": string; - "canStartWorkflow": boolean; - "displayText": string; "displayType": string; + "selected": boolean; } interface ElsaDropdownButton { "icon"?: any; @@ -651,11 +651,11 @@ declare namespace LocalJSX { "value"?: string; } interface ElsaDefaultActivityTemplate { + "activity"?: Activity; "activityJson"?: string; "activityType"?: string; - "canStartWorkflow"?: boolean; - "displayText"?: string; "displayType"?: string; + "selected"?: boolean; } interface ElsaDropdownButton { "icon"?: any; diff --git a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/flowchart.tsx b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/flowchart.tsx index 8a0598a17..df92a78dc 100644 --- a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/flowchart.tsx +++ b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/flowchart.tsx @@ -350,7 +350,7 @@ export class FlowchartComponent implements ContainerActivityComponent { } onNodeMoved = (e: PositionEventArgs) => { - const node = e.node; + const node = e.node as ActivityNodeShape; const activity = node.data as Activity; const nodePosition = node.position({relative: false}); diff --git a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/flowchart/assign-parent.ts b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/flowchart/assign-parent.ts index 41152e17c..074e0ae11 100644 --- a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/flowchart/assign-parent.ts +++ b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/flowchart/assign-parent.ts @@ -1,12 +1,13 @@ import {Graph, Rectangle, Node} from "@antv/x6"; import {camelCase} from 'lodash'; import {Activity} from "../../../../models"; +import {ActivityNode as ActivityNodeShape} from "../shapes"; -export function assignParent(graph: Graph, node: Node){ +export function assignParent(graph: Graph, node: ActivityNodeShape) { const underlyingNodes = graph.getNodesUnderNode(node); if (underlyingNodes.length > 0) { - const underlyingNode = underlyingNodes[0]; + const underlyingNode = underlyingNodes[0] as ActivityNodeShape; const underlyingActivity = underlyingNode.data as Activity; const underlyingView: any = graph.findView(underlyingNode); const portElements: Array = underlyingView.selectors.foContent.getElementsByClassName('activity-port'); @@ -22,9 +23,10 @@ export function assignParent(graph: Graph, node: Node){ const localPortRect = Rectangle.create(graph.pageToLocal(portRect)); if (movedNodeViewRect.intersectsWithRect(localPortRect)) { - const portName = camelCase(portElement.dataset.portName); + const portName = camelCase(portElement.dataset.portName); const childActivity = node.data as Activity; underlyingActivity[portName] = childActivity; + underlyingNode.activity = {...underlyingActivity}; graph.removeCell(node); break; } diff --git a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/shapes.ts b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/shapes.ts index f9d2fd7c7..5b7928910 100644 --- a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/shapes.ts +++ b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/shapes.ts @@ -64,21 +64,37 @@ export class ActivityNode extends Shape.HTML { document.body.append(wrapper); // Wait for activity element to be completely rendered. - const tryUpdate = () => { + // When using custom elements, they are rendered after they are mounted. Before then, they have a 0 width and height. + const tryUpdateSize = () => { + // TODO: Figure out if there's a way to select *any* custom element. + // Worst case, we need to look for a registered set if tags, or introduce a convention where custom elements need to specify a class name for example. + const activityElements: Array = Array.from(wrapper.getElementsByTagName('elsa-default-activity-template')); + + for (const activityElement of activityElements) { + const activityElementRect = activityElement.getBoundingClientRect(); + + // If the custom element has no width or height yet, it means it has not yet rendered. + if (activityElementRect.width == 0 || activityElementRect.height == 0) { + + // Request an animation frame and call ourselves back immediately after. + window.requestAnimationFrame(tryUpdateSize); + return; + } + } + const rect = wrapper.firstElementChild.getBoundingClientRect(); const width = rect.width; const height = rect.height; - if (height == 0 || width == 0) { - window.requestAnimationFrame(tryUpdate); - return; - } - + // Update size of the activity node. this.prop({size: {width, height}}); + + // Remove the temporary element (used only to calculate its size). wrapper.remove(); }; - tryUpdate(); + // Begin try to get our element size. + tryUpdateSize(); } createHtml() { @@ -99,7 +115,6 @@ export class ActivityNode extends Shape.HTML { } ActivityNode.config({ - //portMarkup: [Markup.getForeignObjectMarkup()], ports: { groups: { in: { diff --git a/src/designer/elsa-workflows-designer/src/components/designer/activity-templates/default-activity-template.tsx b/src/designer/elsa-workflows-designer/src/components/designer/activity-templates/default-activity-template.tsx index f09adeba7..30c1523b4 100644 --- a/src/designer/elsa-workflows-designer/src/components/designer/activity-templates/default-activity-template.tsx +++ b/src/designer/elsa-workflows-designer/src/components/designer/activity-templates/default-activity-template.tsx @@ -13,7 +13,7 @@ import {isNullOrWhitespace} from "../../../utils"; export class DefaultActivityTemplate { private readonly iconRegistry: ActivityIconRegistry; private activityDescriptor: ActivityDescriptor; - private activity: Activity; + private parsedActivity: Activity; private icon: ActivityIcon; constructor() { @@ -21,47 +21,61 @@ export class DefaultActivityTemplate { } @Prop({attribute: 'activity-type'}) activityType: string; - @Prop({attribute: 'activity'}) activityJson: string; - @Prop({attribute: 'display-text'}) displayText: string; @Prop({attribute: 'display-type'}) displayType: string; - @Prop({attribute: 'can-start-workflow'}) canStartWorkflow: boolean; + @Prop({attribute: 'activity'}) activityJson: string; + @Prop() selected: boolean; + @Prop() activity: Activity; + @State() private selectedPortName: string; componentWillLoad() { - const activityType = this.activityType; const iconRegistry = this.iconRegistry; - const encodedActivityJson = this.activityJson; - this.activityDescriptor = descriptorsStore.activityDescriptors.find(x => x.activityType == activityType); - if (!isNullOrWhitespace(encodedActivityJson)) { - const decodedActivityJson = decodeURI(encodedActivityJson); - this.activity = JSON.parse(decodedActivityJson); + if (!!this.activity) { + { + this.parsedActivity = this.activity; + } + } else { + const encodedActivityJson = this.activityJson; + + if (!isNullOrWhitespace(encodedActivityJson)) { + const decodedActivityJson = decodeURI(encodedActivityJson); + this.parsedActivity = JSON.parse(decodedActivityJson); + } } + this.activityDescriptor = descriptorsStore.activityDescriptors.find(x => x.activityType == this.activityType); + const activityType = this.activityType; this.icon = iconRegistry.has(activityType) ? iconRegistry.get(activityType) : null; } render() { const activityDescriptor = this.activityDescriptor; - const canStartWorkflow = this.canStartWorkflow; + const activity = this.parsedActivity; + const canStartWorkflow = activity?.canStartWorkflow; const icon = this.icon; const textColor = canStartWorkflow ? 'text-white' : 'text-gray-700'; const isTrigger = activityDescriptor?.kind == ActivityKind.Trigger; const backgroundColor = canStartWorkflow ? isTrigger ? 'bg-green-400' : 'bg-blue-400' : 'bg-white'; const iconBackgroundColor = isTrigger ? 'bg-green-500' : 'bg-blue-500'; - const borderColor = canStartWorkflow ? isTrigger ? 'border-green-600' : 'border-blue-600' : 'border-gray-300'; + const borderColor = this.selected ? 'border-blue-600' : canStartWorkflow ? isTrigger ? 'border-green-600' : 'border-blue-600' : 'border-gray-300'; const displayTypeIsPicker = this.displayTypeIsPicker; - const cssClass = displayTypeIsPicker ? 'px-2 py-2' : 'px-4 py-4'; - let displayText = this.displayText; + const displayTypeIsEmbedded = this.displayTypeIsEmbedded; + const containerCssClass = displayTypeIsEmbedded ? '' : 'drop-shadow-md'; + const contentCssClass = displayTypeIsPicker ? 'px-2 py-2' : 'px-4 py-4'; + let displayText = activity?.metadata?.displayText; + + if (isNullOrWhitespace(displayText)) + displayText = activityDescriptor?.displayName; return (
-
+
{this.renderIcon(icon)}
-
+
{displayText}
{this.renderPorts()} @@ -75,11 +89,10 @@ export class DefaultActivityTemplate { } private renderIcon = (icon?: ActivityIcon): string => { - const displayTypeIsPicker = this.displayTypeIsPicker; - const iconCssClass = displayTypeIsPicker ? 'px-2' : 'px-4'; + const iconCssClass = this.displayTypeIsPicker ? 'px-2' : 'px-4'; if (!icon) - return undefined; //return '
'; + return undefined; return (
@@ -90,7 +103,7 @@ export class DefaultActivityTemplate { private renderPorts = () => { - if (this?.displayType != 'designer') + if (this.displayTypeIsPicker) return undefined; const activityDescriptor = this.activityDescriptor; @@ -107,11 +120,12 @@ export class DefaultActivityTemplate { }; private renderPort = (port: Port) => { - const canStartWorkflow = this.canStartWorkflow; + const canStartWorkflow = this.parsedActivity?.canStartWorkflow; const textColor = canStartWorkflow ? 'text-white' : 'text-gray-700'; const portName = camelCase(port.name); - const activity = this.activity; + const activity = this.parsedActivity; const childActivity: Activity = activity ? activity[portName] : null; + const isSelected = port.name == this.selectedPortName; return (
@@ -120,18 +134,16 @@ export class DefaultActivityTemplate {
{childActivity ? ( -
this.onPortMouseDown(e, port)} onMouseUp={this.onPortMouseUp} - class="relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-6 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"> - {childActivity.typeName} + > + + {/*{childActivity.typeName}*/}
) : (
@@ -146,8 +158,17 @@ export class DefaultActivityTemplate { return this.displayType == "picker"; } - private onPortMouseDown = (e: MouseEvent) => { + private get displayTypeIsEmbedded(): boolean { + return this.displayType == "embedded"; + } + + private onPortMouseDown = (e: MouseEvent, port: Port) => { e.stopPropagation(); + + if (this.selectedPortName != port.name) + this.selectedPortName = port.name; + else + this.selectedPortName = null; }; private onPortMouseUp = (e: MouseEvent) => { diff --git a/src/designer/elsa-workflows-designer/src/drivers/activity/default-activity-driver.tsx b/src/designer/elsa-workflows-designer/src/drivers/activity/default-activity-driver.tsx index 115b53f23..04ae4c955 100644 --- a/src/designer/elsa-workflows-designer/src/drivers/activity/default-activity-driver.tsx +++ b/src/designer/elsa-workflows-designer/src/drivers/activity/default-activity-driver.tsx @@ -11,24 +11,9 @@ export class DefaultActivityDriver implements ActivityDriver { const activityDescriptor = context.activityDescriptor; const activityType = activityDescriptor.activityType; const activity = context.activity; - const canStartWorkflow = activity?.canStartWorkflow == true; - const text = activityDescriptor?.displayName; - let displayText = activity?.metadata?.displayText; - - if (isNullOrWhitespace(displayText)) - displayText = text; - const displayType = context.displayType; const activityJson = displayType == 'designer' ? encodeURI(JSON.stringify(activity)) : ''; - return (` - - `); + return (``); } - }