Incremental work on embedding activities

This commit is contained in:
Sipke Schoorstra 2022-06-17 23:16:03 +02:00
parent 8600dc5fcd
commit afb312654f
6 changed files with 85 additions and 62 deletions

View file

@ -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;

View file

@ -350,7 +350,7 @@ export class FlowchartComponent implements ContainerActivityComponent {
}
onNodeMoved = (e: PositionEventArgs<JQuery.ClickEvent>) => {
const node = e.node;
const node = e.node as ActivityNodeShape;
const activity = node.data as Activity;
const nodePosition = node.position({relative: false});

View file

@ -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<HTMLElement> = 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;
}

View file

@ -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<Element> = 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: {

View file

@ -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 (
<div>
<div class={`activity-wrapper border ${borderColor} ${backgroundColor} rounded text-white overflow-hidden drop-shadow-md`}>
<div class={`activity-wrapper border ${borderColor} ${backgroundColor} ${containerCssClass} rounded text-white overflow-hidden`}>
<div class="activity-content-wrapper flex flex-row">
<div class={`flex flex-shrink items-center ${iconBackgroundColor}`}>
{this.renderIcon(icon)}
</div>
<div class="flex items-center">
<div class={cssClass}>
<div class={contentCssClass}>
<span class={textColor}>{displayText}</span>
<div>
{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 '<div class="px-1 py-1"><span></span></div>';
return undefined;
return (
<div class={`${iconCssClass} py-1`}>
@ -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 (
<div class="activity-port" data-port-name={port.name}>
@ -120,18 +134,16 @@ export class DefaultActivityTemplate {
</div>
<div>
{childActivity ? (
<div onDragOver={this.onDragOverPort}
onDrop={this.onDropOnPort}
onMouseDown={this.onPortMouseDown}
<div class={`relative block w-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500`}
onMouseDown={e => 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">
<span class={textColor}>{childActivity.typeName}</span>
>
<elsa-default-activity-template activityType={childActivity.typeName} displayType="embedded" activity={childActivity} selected={isSelected}/>
{/*<span class={textColor}>{childActivity.typeName}</span>*/}
</div>
) : (
<div onDragOver={this.onDragOverPort}
onDrop={this.onDropOnPort}
onMouseDown={this.onPortMouseDown}
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">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
@ -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) => {

View file

@ -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 (`
<elsa-default-activity-template
activity-type="${activityType}"
display-text="${displayText}"
display-type="${displayType}"
activity="${activityJson}"
can-start-workflow="${canStartWorkflow}" />
`);
return (`<elsa-default-activity-template activity-type="${activityType}" activity="${activityJson}" display-type="${displayType}" />`);
}
}