verben-workflow-ui 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm2022/lib/components/designer/models/types.mjs +1 -1
- package/esm2022/lib/components/designer/services/wf-mapper.service.mjs +3 -3
- package/esm2022/lib/components/workflow-designer/conditions-popup/conditions-popup.component.mjs +76 -0
- package/esm2022/lib/components/workflow-designer/decision-popup/decision-popup.component.mjs +65 -0
- package/esm2022/lib/components/workflow-designer/designer-canvas/designer-canvas.component.mjs +245 -17
- package/esm2022/lib/components/workflow-designer/designer-toolbar/designer-toolbar.component.mjs +26 -14
- package/esm2022/lib/components/workflow-designer/services/connection.service.mjs +220 -0
- package/esm2022/lib/components/workflow-designer/services/node-management.service.mjs +288 -0
- package/esm2022/lib/components/workflow-designer/services/popup.service.mjs +235 -0
- package/esm2022/lib/components/workflow-designer/services/swimlane.service.mjs +145 -0
- package/esm2022/lib/components/workflow-designer/services/transformer.service.mjs +260 -0
- package/esm2022/lib/components/workflow-designer/stage-dialog/stage-dialog.component.mjs +67 -11
- package/esm2022/lib/components/workflow-designer/stage-node/stage-node.component.mjs +9 -18
- package/esm2022/lib/components/workflow-designer/swimlane-dialog/swimlane-dialog.component.mjs +30 -24
- package/esm2022/lib/components/workflow-designer/workflow-data.service.mjs +2 -2
- package/esm2022/lib/components/workflow-designer/workflow-designer.component.mjs +138 -96
- package/esm2022/lib/components/workflow-designer/workflow-designer.module.mjs +27 -3
- package/esm2022/lib/components/workflow-designer/workflow-designer.state.mjs +243 -205
- package/esm2022/lib/components/workflow-designer/workflow-designer.types.mjs +1 -1
- package/esm2022/lib/models/SwimLane.mjs +1 -1
- package/esm2022/lib/models/Workflow.mjs +1 -1
- package/esm2022/lib/models/WorkflowStage.mjs +1 -1
- package/esm2022/lib/services/http-web-request.service.mjs +2 -2
- package/fesm2022/verben-workflow-ui.mjs +2644 -1005
- package/fesm2022/verben-workflow-ui.mjs.map +1 -1
- package/lib/components/designer/models/types.d.ts +1 -1
- package/lib/components/workflow-designer/conditions-popup/conditions-popup.component.d.ts +34 -0
- package/lib/components/workflow-designer/decision-popup/decision-popup.component.d.ts +26 -0
- package/lib/components/workflow-designer/designer-canvas/designer-canvas.component.d.ts +35 -1
- package/lib/components/workflow-designer/designer-toolbar/designer-toolbar.component.d.ts +14 -3
- package/lib/components/workflow-designer/services/connection.service.d.ts +85 -0
- package/lib/components/workflow-designer/services/node-management.service.d.ts +52 -0
- package/lib/components/workflow-designer/services/popup.service.d.ts +139 -0
- package/lib/components/workflow-designer/services/swimlane.service.d.ts +51 -0
- package/lib/components/workflow-designer/services/transformer.service.d.ts +36 -0
- package/lib/components/workflow-designer/stage-dialog/stage-dialog.component.d.ts +17 -2
- package/lib/components/workflow-designer/stage-node/stage-node.component.d.ts +2 -2
- package/lib/components/workflow-designer/swimlane-dialog/swimlane-dialog.component.d.ts +7 -1
- package/lib/components/workflow-designer/workflow-designer.component.d.ts +2 -1
- package/lib/components/workflow-designer/workflow-designer.module.d.ts +6 -4
- package/lib/components/workflow-designer/workflow-designer.state.d.ts +17 -1
- package/lib/components/workflow-designer/workflow-designer.types.d.ts +2 -0
- package/lib/models/SwimLane.d.ts +1 -0
- package/lib/models/Workflow.d.ts +1 -0
- package/lib/models/WorkflowStage.d.ts +1 -1
- package/package.json +1 -1
- package/styles/styles.css +23 -0
|
@@ -338,7 +338,7 @@ class HttpWebRequestService {
|
|
|
338
338
|
buildHeaders() {
|
|
339
339
|
return {
|
|
340
340
|
'Content-Type': 'application/json',
|
|
341
|
-
Authorization: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
|
|
341
|
+
Authorization: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJNYWlsQWRkcmVzcyI6InZlcmJlbmFAZ21haWwuY29tIiwiQXBwbGljYXRpb24iOiIiLCJOYW1lIjoiVmVyYmVuYSBMb2dpYyBMaW1pdGVkIiwiUm9sZUlEIjoiUk9MLVpYQVhYVCIsIlRlbmFudElkIjoiUERMVEM2IiwiU2VydmljZU5hbWUiOiJXaGl0ZTM2MCIsIkNvbXBhbnkiOiJDT00tNkxJNjNIIiwibmJmIjoxNzQxMzM0MDg1LCJleHAiOjE3NDE1NTAwODUsImlhdCI6MTc0MTMzNDA4NX0.94flkNSAlkzArxif_0E8MNzbN3-ulXSqQb0u8l8lMqk',
|
|
342
342
|
};
|
|
343
343
|
}
|
|
344
344
|
buildUrl(url, baseUrl) {
|
|
@@ -2585,6 +2585,15 @@ class WorkflowDesignerState {
|
|
|
2585
2585
|
draggingConnectionData = {};
|
|
2586
2586
|
workflowFormId = null;
|
|
2587
2587
|
workflowFormName = null;
|
|
2588
|
+
workflowId = null;
|
|
2589
|
+
workflow = null;
|
|
2590
|
+
swimlaneRecord = {};
|
|
2591
|
+
stageRecord = {};
|
|
2592
|
+
actionRecord = {};
|
|
2593
|
+
setWorkflowId(id) {
|
|
2594
|
+
this.workflowId = id;
|
|
2595
|
+
}
|
|
2596
|
+
laneIdToIndexMap = new Map();
|
|
2588
2597
|
connectionRules = {
|
|
2589
2598
|
stage: ['stage', 'decision', 'subflow', 'form'],
|
|
2590
2599
|
decision: ['stage'], // Decisions can only connect to Stages
|
|
@@ -2606,6 +2615,9 @@ class WorkflowDesignerState {
|
|
|
2606
2615
|
},
|
|
2607
2616
|
]);
|
|
2608
2617
|
}
|
|
2618
|
+
registerLaneMapping(laneId, swimlaneIndex) {
|
|
2619
|
+
this.laneIdToIndexMap.set(laneId, swimlaneIndex);
|
|
2620
|
+
}
|
|
2609
2621
|
// Add this method to generate connection points for a node
|
|
2610
2622
|
generateConnectionPoints(node) {
|
|
2611
2623
|
const points = [];
|
|
@@ -2744,7 +2756,7 @@ class WorkflowDesignerState {
|
|
|
2744
2756
|
}
|
|
2745
2757
|
return points;
|
|
2746
2758
|
}
|
|
2747
|
-
addNode(swimlaneIndex, type, x, y, stageData, workflowData) {
|
|
2759
|
+
addNode(swimlaneIndex, type, x, y, stageData, workflowData, useExistingId = false) {
|
|
2748
2760
|
if (!this.swimlanes[swimlaneIndex] ||
|
|
2749
2761
|
!type ||
|
|
2750
2762
|
(type !== 'stage' &&
|
|
@@ -2755,15 +2767,17 @@ class WorkflowDesignerState {
|
|
|
2755
2767
|
}
|
|
2756
2768
|
// Adjust position relative to swimlane
|
|
2757
2769
|
const adjustedY = y - swimlaneIndex * 263 - 40; // Subtracting header height
|
|
2758
|
-
// Generate a unique ID
|
|
2759
|
-
const id =
|
|
2770
|
+
// Generate a unique ID or use existing
|
|
2771
|
+
const id = useExistingId && stageData?.Id
|
|
2772
|
+
? stageData.Id
|
|
2773
|
+
: `${type}-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
|
2760
2774
|
// Create node with dimensions based on type
|
|
2761
2775
|
const node = {
|
|
2762
2776
|
id,
|
|
2763
2777
|
type,
|
|
2764
2778
|
x,
|
|
2765
2779
|
y: adjustedY,
|
|
2766
|
-
width: type === 'subflow' ?
|
|
2780
|
+
width: type === 'subflow' ? 150 : 169, // Width from SVG
|
|
2767
2781
|
height: 100, // Height from SVG
|
|
2768
2782
|
isStartNode: false, // Default to false
|
|
2769
2783
|
stageData: stageData || {}, // Store stage data
|
|
@@ -2909,119 +2923,6 @@ class WorkflowDesignerState {
|
|
|
2909
2923
|
const sourceNodeType = sourceNodeInfo.node.type;
|
|
2910
2924
|
return this.connectionRules[sourceNodeType] || [];
|
|
2911
2925
|
}
|
|
2912
|
-
// transformToWorkflowModel(): Workflow {
|
|
2913
|
-
// // Create base workflow object
|
|
2914
|
-
// const workflow: Workflow = {
|
|
2915
|
-
// Name: 'New Workflow', // Default name or get it from somewhere
|
|
2916
|
-
// Description: '', // Default description
|
|
2917
|
-
// StageEntryRule: '',
|
|
2918
|
-
// AssignmentType: TaskAssignmentType.AutoRoute, // Default assignment type
|
|
2919
|
-
// Status: Status.Active, // Default status
|
|
2920
|
-
// Actions: [],
|
|
2921
|
-
// Lanes: [],
|
|
2922
|
-
// Stages: [],
|
|
2923
|
-
// };
|
|
2924
|
-
// // Transform swimlanes to SwimLane[]
|
|
2925
|
-
// workflow.Lanes = this.swimlanes.map((swimlane, index) => {
|
|
2926
|
-
// return {
|
|
2927
|
-
// Id: `lane-${index}`,
|
|
2928
|
-
// Workflow: workflow.Id,
|
|
2929
|
-
// Tags: swimlane.tags,
|
|
2930
|
-
// Position: swimlane.order,
|
|
2931
|
-
// Coordinates: { X: 0, Y: swimlane.order * 263 }, // Calculate Y position based on order
|
|
2932
|
-
// Size: { Width: 3000, Height: 263 }, // Default size
|
|
2933
|
-
// // Add other required properties from BaseModel
|
|
2934
|
-
// Code: '',
|
|
2935
|
-
// TenantId: '',
|
|
2936
|
-
// id: `lane-${index}`,
|
|
2937
|
-
// ServiceName: '',
|
|
2938
|
-
// CreatedAt: new Date(),
|
|
2939
|
-
// UpdatedAt: new Date(),
|
|
2940
|
-
// DataState: ObjectState.New,
|
|
2941
|
-
// };
|
|
2942
|
-
// });
|
|
2943
|
-
// // Transform nodes to WorkflowStage[]
|
|
2944
|
-
// const stages: WorkflowStage[] = [];
|
|
2945
|
-
// this.swimlanes.forEach((swimlane, swimlaneIndex) => {
|
|
2946
|
-
// swimlane.nodes?.forEach((node) => {
|
|
2947
|
-
// if (node.type === 'stage') {
|
|
2948
|
-
// // Create a stage from the node
|
|
2949
|
-
// const stage: WorkflowStage = {
|
|
2950
|
-
// Id: node.id,
|
|
2951
|
-
// Workflow: workflow.Id,
|
|
2952
|
-
// Name: node.stageData?.Name || 'Unnamed Stage',
|
|
2953
|
-
// Description: node.stageData?.Description || '',
|
|
2954
|
-
// Duration: node.stageData?.Duration || 0,
|
|
2955
|
-
// PassOnRule: node.stageData?.PassOnRule || '',
|
|
2956
|
-
// ActorRule: node.stageData?.ActorRule || StageActorRule.None,
|
|
2957
|
-
// MinNoOfActor: node.stageData?.MinNoOfActor || 0,
|
|
2958
|
-
// IsParallel: false, // Default value, will be updated below
|
|
2959
|
-
// IsEntryPoint: node.isStartNode,
|
|
2960
|
-
// IsExitPoint: false, // Determine based on connections
|
|
2961
|
-
// Tags: node.stageData?.Tags || [],
|
|
2962
|
-
// Forms: node.stageData?.Forms || [],
|
|
2963
|
-
// AllowMultiSubProcess: false,
|
|
2964
|
-
// AssignmentType:
|
|
2965
|
-
// node.stageData?.AssignmentType || TaskAssignmentType.AutoRoute,
|
|
2966
|
-
// SubWorkFlow: node.stageData?.SubWorkFlow || '',
|
|
2967
|
-
// SwimLane: workflow.Lanes[swimlaneIndex].Id,
|
|
2968
|
-
// Coordinates: { X: node.x, Y: node.y },
|
|
2969
|
-
// IsSubProcess: false,
|
|
2970
|
-
// // Add other required properties from BaseModel
|
|
2971
|
-
// Code: '',
|
|
2972
|
-
// TenantId: '',
|
|
2973
|
-
// id: node.id,
|
|
2974
|
-
// ServiceName: '',
|
|
2975
|
-
// CreatedAt: new Date(),
|
|
2976
|
-
// UpdatedAt: new Date(),
|
|
2977
|
-
// DataState: ObjectState.New,
|
|
2978
|
-
// };
|
|
2979
|
-
// stages.push(stage);
|
|
2980
|
-
// }
|
|
2981
|
-
// });
|
|
2982
|
-
// });
|
|
2983
|
-
// // Now process the connections to set IsParallel on target stages
|
|
2984
|
-
// this.connections.forEach((conn) => {
|
|
2985
|
-
// const sourceNodeInfo = this.findNodeById(conn.sourceNodeId);
|
|
2986
|
-
// if (
|
|
2987
|
-
// sourceNodeInfo &&
|
|
2988
|
-
// sourceNodeInfo.node.type === 'stage' &&
|
|
2989
|
-
// sourceNodeInfo.node.stageData?.hasParallel === true
|
|
2990
|
-
// ) {
|
|
2991
|
-
// // Find the target stage in our stages array
|
|
2992
|
-
// const targetStage = stages.find((s) => s.Id === conn.targetNodeId);
|
|
2993
|
-
// if (targetStage) {
|
|
2994
|
-
// // Set IsParallel to true for this target stage
|
|
2995
|
-
// targetStage.IsParallel = true;
|
|
2996
|
-
// }
|
|
2997
|
-
// }
|
|
2998
|
-
// });
|
|
2999
|
-
// // Transform connections to WorkflowAction[]
|
|
3000
|
-
// workflow.Actions = this.connections.map((conn) => {
|
|
3001
|
-
// const sourceNode = this.findNodeById(conn.sourceNodeId)?.node;
|
|
3002
|
-
// const targetNode = this.findNodeById(conn.targetNodeId)?.node;
|
|
3003
|
-
// return {
|
|
3004
|
-
// Id: conn.id,
|
|
3005
|
-
// Workflow: workflow.Id,
|
|
3006
|
-
// Name: `Action from ${sourceNode?.stageData?.Name || 'Unknown'} to ${
|
|
3007
|
-
// targetNode?.stageData?.Name || 'Unknown'
|
|
3008
|
-
// }`,
|
|
3009
|
-
// FromStage: conn.sourceNodeId,
|
|
3010
|
-
// ToStage: conn.targetNodeId,
|
|
3011
|
-
// IsParallel: targetNode?.stageData?.IsParallel || false,
|
|
3012
|
-
// // Add other required properties from BaseModel
|
|
3013
|
-
// Code: '',
|
|
3014
|
-
// TenantId: '',
|
|
3015
|
-
// id: conn.id,
|
|
3016
|
-
// ServiceName: '',
|
|
3017
|
-
// CreatedAt: new Date(),
|
|
3018
|
-
// UpdatedAt: new Date(),
|
|
3019
|
-
// DataState: ObjectState.New,
|
|
3020
|
-
// };
|
|
3021
|
-
// });
|
|
3022
|
-
// workflow.Stages = stages;
|
|
3023
|
-
// return workflow;
|
|
3024
|
-
// }
|
|
3025
2926
|
updateSwimlane(index, name, tags) {
|
|
3026
2927
|
console.log('State: Updating swimlane at index', index, 'with name', name, 'and tags', tags);
|
|
3027
2928
|
if (index >= 0 && index < this.swimlanes.length) {
|
|
@@ -3037,116 +2938,253 @@ class WorkflowDesignerState {
|
|
|
3037
2938
|
}
|
|
3038
2939
|
}
|
|
3039
2940
|
transformToWorkflowModel() {
|
|
3040
|
-
//
|
|
3041
|
-
const workflow =
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
DataState: ObjectState.New,
|
|
3050
|
-
Name: 'New Workflow', // Default name or get it from somewhere
|
|
3051
|
-
Description: '', // Default description
|
|
3052
|
-
StageEntryRule: '',
|
|
3053
|
-
Form: this.workflowFormId || undefined,
|
|
3054
|
-
AssignmentType: TaskAssignmentType.AutoRoute, // Default assignment type
|
|
3055
|
-
Status: Status.Active, // Default status
|
|
3056
|
-
Actions: [],
|
|
3057
|
-
Lanes: [],
|
|
3058
|
-
Stages: [],
|
|
3059
|
-
};
|
|
3060
|
-
// Transform swimlanes to SwimLane[]
|
|
3061
|
-
workflow.Lanes = this.swimlanes.map((swimlane, index) => {
|
|
3062
|
-
return {
|
|
3063
|
-
Id: `lane-${index}`,
|
|
3064
|
-
Code: '',
|
|
2941
|
+
// Use stored workflow if available, otherwise create a new one
|
|
2942
|
+
const workflow = this.workflow
|
|
2943
|
+
? { ...this.workflow }
|
|
2944
|
+
: {
|
|
2945
|
+
// BaseModel properties
|
|
2946
|
+
Id: this.wasLoadedFromApi(this.workflowId || '')
|
|
2947
|
+
? this.workflowId || ''
|
|
2948
|
+
: '',
|
|
2949
|
+
Code: this.getCodeForObject(this.workflowId || ''),
|
|
3065
2950
|
TenantId: '',
|
|
3066
|
-
id:
|
|
2951
|
+
id: this.wasLoadedFromApi(this.workflowId || '')
|
|
2952
|
+
? this.workflowId || ''
|
|
2953
|
+
: '',
|
|
3067
2954
|
ServiceName: '',
|
|
3068
2955
|
CreatedAt: new Date(),
|
|
3069
2956
|
UpdatedAt: new Date(),
|
|
3070
|
-
DataState:
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
2957
|
+
DataState: this.wasLoadedFromApi(this.workflowId || '')
|
|
2958
|
+
? ObjectState.Changed
|
|
2959
|
+
: ObjectState.New,
|
|
2960
|
+
// Workflow specific properties
|
|
2961
|
+
Name: 'New Workflow',
|
|
2962
|
+
Description: '',
|
|
2963
|
+
StageEntryRule: '',
|
|
2964
|
+
Form: this.workflowFormId || undefined,
|
|
2965
|
+
AssignmentType: TaskAssignmentType.AutoRoute,
|
|
2966
|
+
Operation: '',
|
|
2967
|
+
Status: Status.Active,
|
|
2968
|
+
Actions: [],
|
|
2969
|
+
Lanes: [],
|
|
2970
|
+
Stages: [],
|
|
3076
2971
|
};
|
|
2972
|
+
// Update workflow form if changed
|
|
2973
|
+
if (this.workflowFormId !== this.workflow?.Form) {
|
|
2974
|
+
workflow.Form = this.workflowFormId || workflow.Form;
|
|
2975
|
+
}
|
|
2976
|
+
// Transform swimlanes to SwimLane[]
|
|
2977
|
+
workflow.Lanes = this.swimlanes.map((swimlane) => {
|
|
2978
|
+
// Check if this is an existing swimlane
|
|
2979
|
+
if (swimlane.id && this.swimlaneRecord[swimlane.id]) {
|
|
2980
|
+
// Update existing lane with changed values
|
|
2981
|
+
const existingLane = this.swimlaneRecord[swimlane.id];
|
|
2982
|
+
return {
|
|
2983
|
+
...existingLane,
|
|
2984
|
+
Tags: swimlane.tags,
|
|
2985
|
+
Position: swimlane.order,
|
|
2986
|
+
Coordinates: { X: 0, Y: swimlane.order * 263 },
|
|
2987
|
+
DataState: ObjectState.Changed,
|
|
2988
|
+
UpdatedAt: new Date(),
|
|
2989
|
+
};
|
|
2990
|
+
}
|
|
2991
|
+
else {
|
|
2992
|
+
// Generate a new ID for new swimlanes if not already set
|
|
2993
|
+
const laneId = swimlane.id ||
|
|
2994
|
+
`lane-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
|
2995
|
+
// Create new lane
|
|
2996
|
+
return {
|
|
2997
|
+
Id: '',
|
|
2998
|
+
Code: '',
|
|
2999
|
+
TenantId: '',
|
|
3000
|
+
id: laneId,
|
|
3001
|
+
ServiceName: '',
|
|
3002
|
+
CreatedAt: new Date(),
|
|
3003
|
+
UpdatedAt: new Date(),
|
|
3004
|
+
DataState: ObjectState.New,
|
|
3005
|
+
Workflow: workflow.Id,
|
|
3006
|
+
Tags: swimlane.tags || [],
|
|
3007
|
+
Position: swimlane.order,
|
|
3008
|
+
Coordinates: { X: 0, Y: swimlane.order * 263 },
|
|
3009
|
+
Size: { Width: 3000, Height: 263 },
|
|
3010
|
+
};
|
|
3011
|
+
}
|
|
3077
3012
|
});
|
|
3078
3013
|
// Transform nodes to WorkflowStage[]
|
|
3079
3014
|
const stages = [];
|
|
3080
3015
|
this.swimlanes.forEach((swimlane, swimlaneIndex) => {
|
|
3081
3016
|
swimlane.nodes?.forEach((node) => {
|
|
3082
3017
|
if (node.type === 'stage') {
|
|
3083
|
-
//
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3018
|
+
// Check if this is an existing stage
|
|
3019
|
+
if (this.stageRecord[node.id]) {
|
|
3020
|
+
// Update existing stage with changed values
|
|
3021
|
+
const existingStage = this.stageRecord[node.id];
|
|
3022
|
+
// Create a new object with updated properties
|
|
3023
|
+
const updatedStage = {
|
|
3024
|
+
...existingStage,
|
|
3025
|
+
DataState: ObjectState.Changed,
|
|
3026
|
+
UpdatedAt: new Date(),
|
|
3027
|
+
};
|
|
3028
|
+
// Update properties that may have changed
|
|
3029
|
+
if (node.stageData?.Name !== undefined)
|
|
3030
|
+
updatedStage.Name = node.stageData.Name;
|
|
3031
|
+
if (node.stageData?.Description !== undefined)
|
|
3032
|
+
updatedStage.Description = node.stageData.Description;
|
|
3033
|
+
if (node.stageData?.Duration !== undefined)
|
|
3034
|
+
updatedStage.Duration = node.stageData.Duration;
|
|
3035
|
+
if (node.stageData?.PassOnRule !== undefined)
|
|
3036
|
+
updatedStage.PassOnRule = node.stageData.PassOnRule;
|
|
3037
|
+
if (node.stageData?.ActorRule !== undefined)
|
|
3038
|
+
updatedStage.ActorRule = node.stageData.ActorRule;
|
|
3039
|
+
if (node.stageData?.MinNoOfActor !== undefined)
|
|
3040
|
+
updatedStage.MinNoOfActor = node.stageData.MinNoOfActor;
|
|
3041
|
+
if (node.stageData?.IsParallel !== undefined)
|
|
3042
|
+
updatedStage.IsParallel = node.stageData.IsParallel;
|
|
3043
|
+
if (node.stageData?.Tags)
|
|
3044
|
+
updatedStage.Tags = node.stageData.Tags;
|
|
3045
|
+
// Always update these properties
|
|
3046
|
+
updatedStage.IsEntryPoint = node.isStartNode;
|
|
3047
|
+
updatedStage.Coordinates = { X: node.x, Y: node.y };
|
|
3048
|
+
updatedStage.SwimLane = this.swimlanes[swimlaneIndex].id || '';
|
|
3049
|
+
// Add to stages
|
|
3050
|
+
stages.push(updatedStage);
|
|
3051
|
+
}
|
|
3052
|
+
else {
|
|
3053
|
+
// Create a new stage
|
|
3054
|
+
stages.push({
|
|
3055
|
+
Id: '',
|
|
3056
|
+
Code: '',
|
|
3057
|
+
TenantId: '',
|
|
3058
|
+
id: node.id,
|
|
3059
|
+
ServiceName: '',
|
|
3060
|
+
CreatedAt: new Date(),
|
|
3061
|
+
UpdatedAt: new Date(),
|
|
3062
|
+
DataState: ObjectState.New,
|
|
3063
|
+
Workflow: workflow.Id,
|
|
3064
|
+
Name: node.stageData?.Name || 'Unnamed Stage',
|
|
3065
|
+
Description: node.stageData?.Description || '',
|
|
3066
|
+
Duration: node.stageData?.Duration || 0,
|
|
3067
|
+
PassOnRule: node.stageData?.PassOnRule || '',
|
|
3068
|
+
ActorRule: node.stageData?.ActorRule || StageActorRule.None,
|
|
3069
|
+
MinNoOfActor: node.stageData?.MinNoOfActor || 0,
|
|
3070
|
+
IsParallel: node.stageData?.IsParallel || false,
|
|
3071
|
+
IsEntryPoint: node.isStartNode,
|
|
3072
|
+
IsExitPoint: false, // Will be updated below
|
|
3073
|
+
Tags: node.stageData?.Tags || [],
|
|
3074
|
+
Form: node.stageData?.formId ?? '',
|
|
3075
|
+
AllowMultiSubProcess: false,
|
|
3076
|
+
AssignmentType: TaskAssignmentType.AutoRoute,
|
|
3077
|
+
SubWorkFlow: '',
|
|
3078
|
+
SwimLane: this.swimlanes[swimlaneIndex].id || '',
|
|
3079
|
+
Coordinates: { X: node.x, Y: node.y },
|
|
3080
|
+
IsSubProcess: false,
|
|
3081
|
+
Key: node.stageData?.Key || undefined,
|
|
3082
|
+
});
|
|
3083
|
+
}
|
|
3113
3084
|
}
|
|
3114
3085
|
});
|
|
3115
3086
|
});
|
|
3116
|
-
//
|
|
3117
|
-
|
|
3118
|
-
const
|
|
3119
|
-
if (
|
|
3120
|
-
|
|
3121
|
-
const hasOutgoingConnections = this.connections.some((c) => c.sourceNodeId === targetStage.Id);
|
|
3122
|
-
if (!hasOutgoingConnections) {
|
|
3123
|
-
targetStage.IsExitPoint = true;
|
|
3124
|
-
}
|
|
3087
|
+
// Determine which stages are exit points (no outgoing connections)
|
|
3088
|
+
stages.forEach((stage) => {
|
|
3089
|
+
const hasOutgoingConnections = this.connections.some((conn) => conn.sourceNodeId === stage.Id || conn.sourceNodeId === stage.id);
|
|
3090
|
+
if (!hasOutgoingConnections) {
|
|
3091
|
+
stage.IsExitPoint = true;
|
|
3125
3092
|
}
|
|
3126
3093
|
});
|
|
3127
3094
|
// Transform connections to WorkflowAction[]
|
|
3128
|
-
|
|
3095
|
+
const actions = [];
|
|
3096
|
+
this.connections.forEach((conn) => {
|
|
3129
3097
|
const sourceNode = this.findNodeById(conn.sourceNodeId)?.node;
|
|
3130
3098
|
const targetNode = this.findNodeById(conn.targetNodeId)?.node;
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3099
|
+
// Check if this is an existing action
|
|
3100
|
+
if (this.actionRecord[conn.id]) {
|
|
3101
|
+
// Update existing action with changed values
|
|
3102
|
+
const existingAction = this.actionRecord[conn.id];
|
|
3103
|
+
// Create updated action with changed properties
|
|
3104
|
+
const updatedAction = {
|
|
3105
|
+
...existingAction,
|
|
3106
|
+
DataState: ObjectState.Changed,
|
|
3107
|
+
UpdatedAt: new Date(),
|
|
3108
|
+
};
|
|
3109
|
+
// Update properties that may have changed
|
|
3110
|
+
updatedAction.Name = `Action from ${sourceNode?.stageData?.Name || 'Unknown'} to ${targetNode?.stageData?.Name || 'Unknown'}`;
|
|
3111
|
+
updatedAction.IsParallel = sourceNode?.stageData?.hasParallel || false;
|
|
3112
|
+
updatedAction.PassOnRule = conn.condition || '';
|
|
3113
|
+
actions.push(updatedAction);
|
|
3114
|
+
}
|
|
3115
|
+
else {
|
|
3116
|
+
// Create a new action
|
|
3117
|
+
actions.push({
|
|
3118
|
+
Id: '',
|
|
3119
|
+
Code: '',
|
|
3120
|
+
TenantId: '',
|
|
3121
|
+
id: conn.id,
|
|
3122
|
+
ServiceName: '',
|
|
3123
|
+
CreatedAt: new Date(),
|
|
3124
|
+
UpdatedAt: new Date(),
|
|
3125
|
+
DataState: ObjectState.New,
|
|
3126
|
+
Workflow: workflow.Id,
|
|
3127
|
+
Name: `Action from ${sourceNode?.stageData?.Name || 'Unknown'} to ${targetNode?.stageData?.Name || 'Unknown'}`,
|
|
3128
|
+
FromStage: conn.sourceNodeId,
|
|
3129
|
+
ToStage: conn.targetNodeId,
|
|
3130
|
+
IsParallel: sourceNode?.stageData?.hasParallel || false,
|
|
3131
|
+
PassOnRule: conn.condition || '',
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
});
|
|
3135
|
+
// Add deleted objects with Removed state
|
|
3136
|
+
Object.keys(this.stageRecord).forEach((stageId) => {
|
|
3137
|
+
const exists = stages.some((stage) => stage.Id === stageId || stage.id === stageId);
|
|
3138
|
+
if (!exists) {
|
|
3139
|
+
const stage = this.stageRecord[stageId];
|
|
3140
|
+
stages.push({
|
|
3141
|
+
...stage,
|
|
3142
|
+
DataState: ObjectState.Removed,
|
|
3143
|
+
UpdatedAt: new Date(),
|
|
3144
|
+
});
|
|
3145
|
+
}
|
|
3146
|
+
});
|
|
3147
|
+
Object.keys(this.actionRecord).forEach((actionId) => {
|
|
3148
|
+
const exists = actions.some((action) => action.Id === actionId || action.id === actionId);
|
|
3149
|
+
if (!exists) {
|
|
3150
|
+
const action = this.actionRecord[actionId];
|
|
3151
|
+
actions.push({
|
|
3152
|
+
...action,
|
|
3153
|
+
DataState: ObjectState.Removed,
|
|
3154
|
+
UpdatedAt: new Date(),
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
});
|
|
3158
|
+
// Handle deleted swimlanes
|
|
3159
|
+
Object.keys(this.swimlaneRecord).forEach((laneId) => {
|
|
3160
|
+
const exists = workflow.Lanes.some((lane) => lane.Id === laneId || lane.id === laneId);
|
|
3161
|
+
if (!exists) {
|
|
3162
|
+
const lane = this.swimlaneRecord[laneId];
|
|
3163
|
+
workflow.Lanes.push({
|
|
3164
|
+
...lane,
|
|
3165
|
+
DataState: ObjectState.Removed,
|
|
3166
|
+
UpdatedAt: new Date(),
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3146
3169
|
});
|
|
3147
3170
|
workflow.Stages = stages;
|
|
3171
|
+
workflow.Actions = actions;
|
|
3148
3172
|
return workflow;
|
|
3149
3173
|
}
|
|
3174
|
+
// Add a new property to track loaded objects
|
|
3175
|
+
loadedObjectIds = {}; // Format: { id: code }
|
|
3176
|
+
// Add a method to register loaded objects
|
|
3177
|
+
registerLoadedObject(id, code) {
|
|
3178
|
+
this.loadedObjectIds[id] = code;
|
|
3179
|
+
}
|
|
3180
|
+
// Method to check if an object was loaded from API
|
|
3181
|
+
wasLoadedFromApi(id) {
|
|
3182
|
+
return id in this.loadedObjectIds;
|
|
3183
|
+
}
|
|
3184
|
+
// Method to get the code for a loaded object
|
|
3185
|
+
getCodeForObject(id) {
|
|
3186
|
+
return this.loadedObjectIds[id] || '';
|
|
3187
|
+
}
|
|
3150
3188
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: WorkflowDesignerState, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
3151
3189
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: WorkflowDesignerState, providedIn: 'root' });
|
|
3152
3190
|
}
|
|
@@ -3243,7 +3281,7 @@ class WorkflowDataService {
|
|
|
3243
3281
|
IsEntryPoint: true,
|
|
3244
3282
|
IsExitPoint: false,
|
|
3245
3283
|
Tags: [],
|
|
3246
|
-
|
|
3284
|
+
Form: '',
|
|
3247
3285
|
AllowMultiSubProcess: false,
|
|
3248
3286
|
AssignmentType: TaskAssignmentType.AutoRoute,
|
|
3249
3287
|
SubWorkFlow: '',
|
|
@@ -3265,111 +3303,995 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
|
|
|
3265
3303
|
}]
|
|
3266
3304
|
}], ctorParameters: () => [{ type: HttpWebRequestService }, { type: EnvironmentService }] });
|
|
3267
3305
|
|
|
3268
|
-
class
|
|
3306
|
+
class NodeManagementService {
|
|
3269
3307
|
state;
|
|
3270
|
-
selectedTool = null;
|
|
3271
|
-
isSaving = false;
|
|
3272
|
-
toolSelected = new EventEmitter();
|
|
3273
|
-
saveWorkflow = new EventEmitter();
|
|
3274
|
-
// Simple array of toolbar items
|
|
3275
|
-
toolbarItems = [
|
|
3276
|
-
{ id: 'swimlane', label: 'Swimlane' },
|
|
3277
|
-
{ id: 'stage', label: 'Stage' },
|
|
3278
|
-
{ id: 'action', label: 'Action' },
|
|
3279
|
-
{ id: 'form', label: 'Form' },
|
|
3280
|
-
{ id: 'decision', label: 'Decision' },
|
|
3281
|
-
{ id: 'subflow', label: 'Subflow' },
|
|
3282
|
-
];
|
|
3283
3308
|
constructor(state) {
|
|
3284
3309
|
this.state = state;
|
|
3285
3310
|
}
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
case 'form':
|
|
3298
|
-
case 'action':
|
|
3299
|
-
case 'subflow':
|
|
3300
|
-
// These can be enabled later when you implement connections
|
|
3301
|
-
return this.state.getNodeCount() > 0;
|
|
3302
|
-
default:
|
|
3303
|
-
return false;
|
|
3311
|
+
/**
|
|
3312
|
+
* Adds a new node to the specified swimlane
|
|
3313
|
+
*/
|
|
3314
|
+
addNode(swimlaneIndex, type, x, y, stageData, workflowData) {
|
|
3315
|
+
if (!this.state.swimlanes[swimlaneIndex] ||
|
|
3316
|
+
!type ||
|
|
3317
|
+
(type !== 'stage' &&
|
|
3318
|
+
type !== 'decision' &&
|
|
3319
|
+
type !== 'form' &&
|
|
3320
|
+
type !== 'subflow')) {
|
|
3321
|
+
return null;
|
|
3304
3322
|
}
|
|
3323
|
+
// Adjust position relative to swimlane
|
|
3324
|
+
const adjustedY = y - swimlaneIndex * 263 - 40; // Subtracting header height
|
|
3325
|
+
// Generate a unique ID
|
|
3326
|
+
const id = `${type}-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
|
3327
|
+
// Create node with dimensions based on type
|
|
3328
|
+
const node = {
|
|
3329
|
+
id,
|
|
3330
|
+
type,
|
|
3331
|
+
x,
|
|
3332
|
+
y: adjustedY,
|
|
3333
|
+
width: type === 'subflow' ? 150 : 169, // Width from SVG
|
|
3334
|
+
height: 100, // Height from SVG
|
|
3335
|
+
isStartNode: false, // Default to false
|
|
3336
|
+
stageData: stageData || {}, // Store stage data
|
|
3337
|
+
workflowData: workflowData || null,
|
|
3338
|
+
};
|
|
3339
|
+
// If this is the first node in the entire workflow, mark it as the start node
|
|
3340
|
+
if (this.getNodeCount() === 0) {
|
|
3341
|
+
node.isStartNode = true;
|
|
3342
|
+
this.state.startNodeId = id;
|
|
3343
|
+
}
|
|
3344
|
+
// Generate connection points for the node
|
|
3345
|
+
node.connectionPoints = this.generateConnectionPoints(node);
|
|
3346
|
+
// Add node to the swimlane
|
|
3347
|
+
if (!this.state.swimlanes[swimlaneIndex].nodes) {
|
|
3348
|
+
this.state.swimlanes[swimlaneIndex].nodes = [];
|
|
3349
|
+
}
|
|
3350
|
+
this.state.swimlanes[swimlaneIndex].nodes.push(node);
|
|
3351
|
+
return node;
|
|
3305
3352
|
}
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
}
|
|
3312
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DesignerToolbarComponent, decorators: [{
|
|
3313
|
-
type: Component,
|
|
3314
|
-
args: [{ selector: 'lib-designer-toolbar', template: "<div class=\"designer-toolbar\">\n <div class=\"toolbar-container\">\n <button\n *ngFor=\"let item of toolbarItems\"\n class=\"tool-button\"\n [class.active]=\"selectedTool === item.id\"\n [class.disabled]=\"!isEnabled(item.id)\"\n (click)=\"onToolClick(item.id)\"\n [title]=\"item.label\"\n [disabled]=\"!isEnabled(item.id)\"\n >\n {{ item.label }}\n </button>\n </div>\n\n <button class=\"save-button\" (click)=\"onSaveClick()\" [disabled]=\"isSaving\">\n {{ isSaving ? \"Saving...\" : \"Save Workflow\" }}\n </button>\n</div>\n", styles: [".designer-toolbar{padding:.5rem;background-color:#fff;border-bottom:1px solid #e2e8f0;display:flex;justify-content:space-between;align-items:center}.toolbar-container{display:flex;gap:.5rem;background-color:#fff;border:1px solid #d8b4fe;border-radius:.5rem;padding:.5rem;box-shadow:0 2px 4px #0000001a;max-width:fit-content}.tool-button{padding:.5rem 1rem;background-color:#fff;border:1px solid #e2e8f0;border-radius:.25rem;cursor:pointer;font-size:.875rem;transition:all .2s}.tool-button:hover{background-color:#f9fafb}.tool-button.active{background-color:#f3e8ff;border-color:#d8b4fe;color:#7e22ce}.tool-button.active{opacity:.75}.save-button{padding:.5rem 1rem;background-color:#f3e8ff;border:1px solid #d8b4fe;border-radius:.25rem;color:#7e22ce;font-size:.875rem;cursor:pointer;transition:all .2s}.save-button:hover{background-color:#e9d5ff}.save-button:disabled{opacity:.5;cursor:not-allowed}\n"] }]
|
|
3315
|
-
}], ctorParameters: () => [{ type: WorkflowDesignerState }], propDecorators: { selectedTool: [{
|
|
3316
|
-
type: Input
|
|
3317
|
-
}], isSaving: [{
|
|
3318
|
-
type: Input
|
|
3319
|
-
}], toolSelected: [{
|
|
3320
|
-
type: Output
|
|
3321
|
-
}], saveWorkflow: [{
|
|
3322
|
-
type: Output
|
|
3323
|
-
}] } });
|
|
3324
|
-
|
|
3325
|
-
class StageDialogComponent {
|
|
3326
|
-
fb;
|
|
3327
|
-
dataService;
|
|
3328
|
-
// @Input() visible: boolean = false;
|
|
3329
|
-
// In stage-dialog.component.ts - check input handling
|
|
3330
|
-
visible = input(false);
|
|
3331
|
-
stageData = {}; // For editing existing stages
|
|
3332
|
-
closed = new EventEmitter();
|
|
3333
|
-
saved = new EventEmitter();
|
|
3334
|
-
stageForm;
|
|
3335
|
-
tags = [];
|
|
3336
|
-
actorRules = Object.values(StageActorRule);
|
|
3337
|
-
selectedTagIds = [];
|
|
3338
|
-
constructor(fb, dataService) {
|
|
3339
|
-
this.fb = fb;
|
|
3340
|
-
this.dataService = dataService;
|
|
3341
|
-
this.stageForm = this.fb.group({
|
|
3342
|
-
Name: ['', Validators.required],
|
|
3343
|
-
Description: [''],
|
|
3344
|
-
MinNoOfActor: [0],
|
|
3345
|
-
Duration: [0],
|
|
3346
|
-
PassOnRule: [''],
|
|
3347
|
-
ActorRule: [StageActorRule.None],
|
|
3348
|
-
Tags: [[]],
|
|
3349
|
-
});
|
|
3353
|
+
/**
|
|
3354
|
+
* Gets the total count of nodes across all swimlanes
|
|
3355
|
+
*/
|
|
3356
|
+
getNodeCount() {
|
|
3357
|
+
return this.state.swimlanes.reduce((count, swimlane) => count + (swimlane.nodes?.length || 0), 0);
|
|
3350
3358
|
}
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3359
|
+
/**
|
|
3360
|
+
* Find a node by ID
|
|
3361
|
+
*/
|
|
3362
|
+
findNodeById(id) {
|
|
3363
|
+
for (let i = 0; i < this.state.swimlanes.length; i++) {
|
|
3364
|
+
const swimlane = this.state.swimlanes[i];
|
|
3365
|
+
const node = swimlane.nodes?.find((n) => n.id === id);
|
|
3366
|
+
if (node) {
|
|
3367
|
+
return { node, swimlaneIndex: i };
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
return null;
|
|
3371
|
+
}
|
|
3372
|
+
/**
|
|
3373
|
+
* Generate connection points for a node
|
|
3374
|
+
*/
|
|
3375
|
+
generateConnectionPoints(node) {
|
|
3376
|
+
const points = [];
|
|
3377
|
+
const spacing = 50; // Space between connection points
|
|
3378
|
+
if (node.type === 'stage') {
|
|
3379
|
+
// Rectangular shapes - add points around the rectangle
|
|
3380
|
+
// Top edge points
|
|
3381
|
+
for (let x = spacing; x < node.width; x += spacing) {
|
|
3382
|
+
points.push({
|
|
3383
|
+
id: `${node.id}-top-${x}`,
|
|
3384
|
+
nodeId: node.id,
|
|
3385
|
+
x: x,
|
|
3386
|
+
y: 0,
|
|
3387
|
+
type: 'top',
|
|
3388
|
+
});
|
|
3389
|
+
}
|
|
3390
|
+
// Right edge points
|
|
3391
|
+
for (let y = spacing; y < node.height; y += spacing) {
|
|
3392
|
+
points.push({
|
|
3393
|
+
id: `${node.id}-right-${y}`,
|
|
3394
|
+
nodeId: node.id,
|
|
3395
|
+
x: node.width,
|
|
3396
|
+
y: y,
|
|
3397
|
+
type: 'right',
|
|
3398
|
+
});
|
|
3399
|
+
}
|
|
3400
|
+
// Bottom edge points
|
|
3401
|
+
for (let x = spacing; x < node.width; x += spacing) {
|
|
3402
|
+
points.push({
|
|
3403
|
+
id: `${node.id}-bottom-${x}`,
|
|
3404
|
+
nodeId: node.id,
|
|
3405
|
+
x: x,
|
|
3406
|
+
y: node.height,
|
|
3407
|
+
type: 'bottom',
|
|
3408
|
+
});
|
|
3409
|
+
}
|
|
3410
|
+
// Left edge points
|
|
3411
|
+
for (let y = spacing; y < node.height; y += spacing) {
|
|
3412
|
+
points.push({
|
|
3413
|
+
id: `${node.id}-left-${y}`,
|
|
3414
|
+
nodeId: node.id,
|
|
3415
|
+
x: 0,
|
|
3416
|
+
y: y,
|
|
3417
|
+
type: 'left',
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
else if (node.type === 'decision') {
|
|
3422
|
+
// Diamond shape - add points at the four corners
|
|
3423
|
+
points.push({
|
|
3424
|
+
id: `${node.id}-top-0`,
|
|
3425
|
+
nodeId: node.id,
|
|
3426
|
+
x: node.width / 2,
|
|
3427
|
+
y: 0,
|
|
3428
|
+
type: 'top',
|
|
3429
|
+
});
|
|
3430
|
+
points.push({
|
|
3431
|
+
id: `${node.id}-right-0`,
|
|
3432
|
+
nodeId: node.id,
|
|
3433
|
+
x: node.width,
|
|
3434
|
+
y: node.height / 2,
|
|
3435
|
+
type: 'right',
|
|
3436
|
+
});
|
|
3437
|
+
points.push({
|
|
3438
|
+
id: `${node.id}-bottom-0`,
|
|
3439
|
+
nodeId: node.id,
|
|
3440
|
+
x: node.width / 2,
|
|
3441
|
+
y: node.height,
|
|
3442
|
+
type: 'bottom',
|
|
3443
|
+
});
|
|
3444
|
+
points.push({
|
|
3445
|
+
id: `${node.id}-left-0`,
|
|
3446
|
+
nodeId: node.id,
|
|
3447
|
+
x: 0,
|
|
3448
|
+
y: node.height / 2,
|
|
3449
|
+
type: 'left',
|
|
3450
|
+
});
|
|
3451
|
+
}
|
|
3452
|
+
else if (node.type === 'form') {
|
|
3453
|
+
// Form only has input connection points, no output points
|
|
3454
|
+
// Left edge points (for input connections)
|
|
3455
|
+
for (let y = spacing; y < node.height; y += spacing) {
|
|
3456
|
+
points.push({
|
|
3457
|
+
id: `${node.id}-left-${y}`,
|
|
3458
|
+
nodeId: node.id,
|
|
3459
|
+
x: 0,
|
|
3460
|
+
y: y,
|
|
3461
|
+
type: 'left',
|
|
3462
|
+
});
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
else if (node.type === 'subflow') {
|
|
3466
|
+
// Hexagon shape - add points at the six corners
|
|
3467
|
+
points.push({
|
|
3468
|
+
id: `${node.id}-top-0`,
|
|
3469
|
+
nodeId: node.id,
|
|
3470
|
+
x: node.width / 2,
|
|
3471
|
+
y: 0,
|
|
3472
|
+
type: 'top',
|
|
3473
|
+
});
|
|
3474
|
+
points.push({
|
|
3475
|
+
id: `${node.id}-right-top`,
|
|
3476
|
+
nodeId: node.id,
|
|
3477
|
+
x: node.width,
|
|
3478
|
+
y: node.height / 4,
|
|
3479
|
+
type: 'right',
|
|
3480
|
+
});
|
|
3481
|
+
points.push({
|
|
3482
|
+
id: `${node.id}-right-bottom`,
|
|
3483
|
+
nodeId: node.id,
|
|
3484
|
+
x: node.width,
|
|
3485
|
+
y: (node.height * 3) / 4,
|
|
3486
|
+
type: 'right',
|
|
3487
|
+
});
|
|
3488
|
+
points.push({
|
|
3489
|
+
id: `${node.id}-bottom-0`,
|
|
3490
|
+
nodeId: node.id,
|
|
3491
|
+
x: node.width / 2,
|
|
3492
|
+
y: node.height,
|
|
3493
|
+
type: 'bottom',
|
|
3494
|
+
});
|
|
3495
|
+
points.push({
|
|
3496
|
+
id: `${node.id}-left-bottom`,
|
|
3497
|
+
nodeId: node.id,
|
|
3498
|
+
x: 0,
|
|
3499
|
+
y: (node.height * 3) / 4,
|
|
3500
|
+
type: 'left',
|
|
3501
|
+
});
|
|
3502
|
+
points.push({
|
|
3503
|
+
id: `${node.id}-left-top`,
|
|
3504
|
+
nodeId: node.id,
|
|
3505
|
+
x: 0,
|
|
3506
|
+
y: node.height / 4,
|
|
3507
|
+
type: 'left',
|
|
3508
|
+
});
|
|
3509
|
+
}
|
|
3510
|
+
return points;
|
|
3511
|
+
}
|
|
3512
|
+
/**
|
|
3513
|
+
* Update node properties
|
|
3514
|
+
*/
|
|
3515
|
+
updateNodeProperties(nodeId, properties) {
|
|
3516
|
+
const nodeInfo = this.findNodeById(nodeId);
|
|
3517
|
+
if (!nodeInfo)
|
|
3518
|
+
return false;
|
|
3519
|
+
const { node, swimlaneIndex } = nodeInfo;
|
|
3520
|
+
// Update the node with new properties
|
|
3521
|
+
Object.assign(node, properties);
|
|
3522
|
+
return true;
|
|
3523
|
+
}
|
|
3524
|
+
/**
|
|
3525
|
+
* Update stage data for a node
|
|
3526
|
+
*/
|
|
3527
|
+
updateStageData(nodeId, stageData) {
|
|
3528
|
+
const nodeInfo = this.findNodeById(nodeId);
|
|
3529
|
+
if (!nodeInfo)
|
|
3530
|
+
return false;
|
|
3531
|
+
const { node } = nodeInfo;
|
|
3532
|
+
// Update the stage data
|
|
3533
|
+
node.stageData = {
|
|
3534
|
+
...node.stageData,
|
|
3535
|
+
...stageData,
|
|
3536
|
+
};
|
|
3537
|
+
return true;
|
|
3538
|
+
}
|
|
3539
|
+
/**
|
|
3540
|
+
* Set a node as a start node
|
|
3541
|
+
*/
|
|
3542
|
+
setAsStartNode(nodeId) {
|
|
3543
|
+
// First, clear any existing start nodes
|
|
3544
|
+
this.state.swimlanes.forEach((swimlane) => {
|
|
3545
|
+
swimlane.nodes?.forEach((node) => {
|
|
3546
|
+
if (node.isStartNode) {
|
|
3547
|
+
node.isStartNode = false;
|
|
3548
|
+
}
|
|
3549
|
+
});
|
|
3550
|
+
});
|
|
3551
|
+
// Then set the new start node
|
|
3552
|
+
const nodeInfo = this.findNodeById(nodeId);
|
|
3553
|
+
if (!nodeInfo)
|
|
3554
|
+
return false;
|
|
3555
|
+
nodeInfo.node.isStartNode = true;
|
|
3556
|
+
this.state.startNodeId = nodeId;
|
|
3557
|
+
return true;
|
|
3558
|
+
}
|
|
3559
|
+
/**
|
|
3560
|
+
* Delete a node
|
|
3561
|
+
*/
|
|
3562
|
+
deleteNode(nodeId) {
|
|
3563
|
+
const nodeInfo = this.findNodeById(nodeId);
|
|
3564
|
+
if (!nodeInfo)
|
|
3565
|
+
return false;
|
|
3566
|
+
const { swimlaneIndex } = nodeInfo;
|
|
3567
|
+
// Find the node index in the swimlane
|
|
3568
|
+
const nodeIndex = this.state.swimlanes[swimlaneIndex].nodes?.findIndex((n) => n.id === nodeId);
|
|
3569
|
+
if (nodeIndex === undefined || nodeIndex === -1)
|
|
3570
|
+
return false;
|
|
3571
|
+
// Remove the node
|
|
3572
|
+
this.state.swimlanes[swimlaneIndex].nodes?.splice(nodeIndex, 1);
|
|
3573
|
+
return true;
|
|
3574
|
+
}
|
|
3575
|
+
/**
|
|
3576
|
+
* Check if a node has outgoing connections
|
|
3577
|
+
*/
|
|
3578
|
+
hasOutgoingConnections(nodeId) {
|
|
3579
|
+
return this.state.connections.some((conn) => conn.sourceNodeId === nodeId);
|
|
3580
|
+
}
|
|
3581
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NodeManagementService, deps: [{ token: WorkflowDesignerState }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
3582
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NodeManagementService, providedIn: 'root' });
|
|
3583
|
+
}
|
|
3584
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NodeManagementService, decorators: [{
|
|
3585
|
+
type: Injectable,
|
|
3586
|
+
args: [{
|
|
3587
|
+
providedIn: 'root',
|
|
3588
|
+
}]
|
|
3589
|
+
}], ctorParameters: () => [{ type: WorkflowDesignerState }] });
|
|
3590
|
+
|
|
3591
|
+
class SwimlaneService {
|
|
3592
|
+
state;
|
|
3593
|
+
swimlaneHeight = 263;
|
|
3594
|
+
constructor(state) {
|
|
3595
|
+
this.state = state;
|
|
3596
|
+
}
|
|
3597
|
+
/**
|
|
3598
|
+
* Add a new swimlane
|
|
3599
|
+
*/
|
|
3600
|
+
addSwimlane(name, tags) {
|
|
3601
|
+
const newSwimlane = {
|
|
3602
|
+
order: this.state.swimlanes.length,
|
|
3603
|
+
label: name,
|
|
3604
|
+
tags,
|
|
3605
|
+
nodes: [],
|
|
3606
|
+
};
|
|
3607
|
+
this.state.swimlanes.push(newSwimlane);
|
|
3608
|
+
return newSwimlane;
|
|
3609
|
+
}
|
|
3610
|
+
/**
|
|
3611
|
+
* Update an existing swimlane
|
|
3612
|
+
*/
|
|
3613
|
+
updateSwimlane(index, name, tags) {
|
|
3614
|
+
if (index < 0 || index >= this.state.swimlanes.length) {
|
|
3615
|
+
console.error('Invalid swimlane index:', index);
|
|
3616
|
+
return false;
|
|
3617
|
+
}
|
|
3618
|
+
this.state.swimlanes[index] = {
|
|
3619
|
+
...this.state.swimlanes[index],
|
|
3620
|
+
label: name,
|
|
3621
|
+
tags: tags,
|
|
3622
|
+
};
|
|
3623
|
+
return true;
|
|
3624
|
+
}
|
|
3625
|
+
/**
|
|
3626
|
+
* Reorder swimlanes (move a swimlane up or down in the list)
|
|
3627
|
+
*/
|
|
3628
|
+
reorderSwimlane(fromIndex, toIndex) {
|
|
3629
|
+
if (fromIndex < 0 ||
|
|
3630
|
+
fromIndex >= this.state.swimlanes.length ||
|
|
3631
|
+
toIndex < 0 ||
|
|
3632
|
+
toIndex >= this.state.swimlanes.length) {
|
|
3633
|
+
return false;
|
|
3634
|
+
}
|
|
3635
|
+
// Remove the swimlane from its current position
|
|
3636
|
+
const [swimlane] = this.state.swimlanes.splice(fromIndex, 1);
|
|
3637
|
+
// Insert it at the new position
|
|
3638
|
+
this.state.swimlanes.splice(toIndex, 0, swimlane);
|
|
3639
|
+
// Update order properties for all swimlanes
|
|
3640
|
+
this.state.swimlanes.forEach((lane, idx) => {
|
|
3641
|
+
lane.order = idx;
|
|
3642
|
+
});
|
|
3643
|
+
return true;
|
|
3644
|
+
}
|
|
3645
|
+
/**
|
|
3646
|
+
* Delete a swimlane by index
|
|
3647
|
+
*/
|
|
3648
|
+
deleteSwimlane(index) {
|
|
3649
|
+
if (index < 0 || index >= this.state.swimlanes.length) {
|
|
3650
|
+
return false;
|
|
3651
|
+
}
|
|
3652
|
+
// Check if the swimlane has nodes
|
|
3653
|
+
if (this.state.swimlanes[index].nodes?.length) {
|
|
3654
|
+
console.error('Cannot delete swimlane with nodes');
|
|
3655
|
+
return false;
|
|
3656
|
+
}
|
|
3657
|
+
// Remove the swimlane
|
|
3658
|
+
this.state.swimlanes.splice(index, 1);
|
|
3659
|
+
// Update order properties for all swimlanes
|
|
3660
|
+
this.state.swimlanes.forEach((lane, idx) => {
|
|
3661
|
+
lane.order = idx;
|
|
3662
|
+
});
|
|
3663
|
+
return true;
|
|
3664
|
+
}
|
|
3665
|
+
/**
|
|
3666
|
+
* Get all swimlanes
|
|
3667
|
+
*/
|
|
3668
|
+
getSwimlanes() {
|
|
3669
|
+
return this.state.swimlanes;
|
|
3670
|
+
}
|
|
3671
|
+
/**
|
|
3672
|
+
* Get swimlane by index
|
|
3673
|
+
*/
|
|
3674
|
+
getSwimlane(index) {
|
|
3675
|
+
if (index < 0 || index >= this.state.swimlanes.length) {
|
|
3676
|
+
return null;
|
|
3677
|
+
}
|
|
3678
|
+
return this.state.swimlanes[index];
|
|
3679
|
+
}
|
|
3680
|
+
/**
|
|
3681
|
+
* Calculate total canvas height needed for all swimlanes
|
|
3682
|
+
*/
|
|
3683
|
+
calculateCanvasHeight(minHeight = 2000) {
|
|
3684
|
+
const requiredHeight = (this.state.swimlanes.length + 1) * this.swimlaneHeight;
|
|
3685
|
+
return Math.max(minHeight, requiredHeight);
|
|
3686
|
+
}
|
|
3687
|
+
/**
|
|
3688
|
+
* Check if a Y position is within any swimlane
|
|
3689
|
+
*/
|
|
3690
|
+
isPositionInsideSwimlane(y) {
|
|
3691
|
+
// Check if y position is within any swimlane
|
|
3692
|
+
const swimlaneCount = this.state.swimlanes.length;
|
|
3693
|
+
if (swimlaneCount === 0)
|
|
3694
|
+
return false;
|
|
3695
|
+
// Each swimlane has a height of 263px
|
|
3696
|
+
const totalSwimlaneHeight = swimlaneCount * this.swimlaneHeight;
|
|
3697
|
+
// Check if y is within the total height of all swimlanes
|
|
3698
|
+
return y >= 0 && y < totalSwimlaneHeight;
|
|
3699
|
+
}
|
|
3700
|
+
/**
|
|
3701
|
+
* Calculate swimlane index from Y position
|
|
3702
|
+
*/
|
|
3703
|
+
getSwimlaneIndexFromPosition(y) {
|
|
3704
|
+
if (!this.isPositionInsideSwimlane(y))
|
|
3705
|
+
return -1;
|
|
3706
|
+
return Math.floor(y / this.swimlaneHeight);
|
|
3707
|
+
}
|
|
3708
|
+
/**
|
|
3709
|
+
* Find swimlane index by Lane ID
|
|
3710
|
+
*/
|
|
3711
|
+
findSwimlaneIndexByLaneId(laneId) {
|
|
3712
|
+
// In a real implementation, you would maintain a mapping between API lane IDs and UI swimlane indices
|
|
3713
|
+
// For now we'll just return the index based on position
|
|
3714
|
+
const parts = laneId.split('-');
|
|
3715
|
+
if (parts.length > 1) {
|
|
3716
|
+
const index = parseInt(parts[1]);
|
|
3717
|
+
if (!isNaN(index) && index < this.state.swimlanes.length) {
|
|
3718
|
+
return index;
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
return -1;
|
|
3722
|
+
}
|
|
3723
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: SwimlaneService, deps: [{ token: WorkflowDesignerState }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
3724
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: SwimlaneService, providedIn: 'root' });
|
|
3725
|
+
}
|
|
3726
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: SwimlaneService, decorators: [{
|
|
3727
|
+
type: Injectable,
|
|
3728
|
+
args: [{
|
|
3729
|
+
providedIn: 'root',
|
|
3730
|
+
}]
|
|
3731
|
+
}], ctorParameters: () => [{ type: WorkflowDesignerState }] });
|
|
3732
|
+
|
|
3733
|
+
class DesignerToolbarComponent {
|
|
3734
|
+
nodeService;
|
|
3735
|
+
swimlaneService;
|
|
3736
|
+
selectedTool = null;
|
|
3737
|
+
isSaving = false;
|
|
3738
|
+
toolSelected = new EventEmitter();
|
|
3739
|
+
saveWorkflow = new EventEmitter();
|
|
3740
|
+
// Simple array of toolbar items
|
|
3741
|
+
toolbarItems = [
|
|
3742
|
+
{ id: 'swimlane', label: 'Swimlane' },
|
|
3743
|
+
{ id: 'stage', label: 'Stage' },
|
|
3744
|
+
{ id: 'action', label: 'Action' },
|
|
3745
|
+
{ id: 'form', label: 'Form' },
|
|
3746
|
+
{ id: 'decision', label: 'Decision' },
|
|
3747
|
+
{ id: 'subflow', label: 'Subflow' },
|
|
3748
|
+
];
|
|
3749
|
+
constructor(nodeService, swimlaneService) {
|
|
3750
|
+
this.nodeService = nodeService;
|
|
3751
|
+
this.swimlaneService = swimlaneService;
|
|
3752
|
+
}
|
|
3753
|
+
/**
|
|
3754
|
+
* Handle tool button click
|
|
3755
|
+
*/
|
|
3756
|
+
onToolClick(tool) {
|
|
3757
|
+
this.toolSelected.emit(tool);
|
|
3758
|
+
}
|
|
3759
|
+
/**
|
|
3760
|
+
* Check if a tool should be enabled
|
|
3761
|
+
*/
|
|
3762
|
+
isEnabled(type) {
|
|
3763
|
+
switch (type) {
|
|
3764
|
+
case 'swimlane':
|
|
3765
|
+
return true; // Always enabled
|
|
3766
|
+
case 'stage':
|
|
3767
|
+
case 'decision':
|
|
3768
|
+
// Enabled if there are swimlanes available
|
|
3769
|
+
return this.swimlaneService.getSwimlanes().length > 0;
|
|
3770
|
+
case 'form':
|
|
3771
|
+
case 'action':
|
|
3772
|
+
case 'subflow':
|
|
3773
|
+
// Only enabled if there are nodes in the workflow
|
|
3774
|
+
return this.nodeService.getNodeCount() > 0;
|
|
3775
|
+
default:
|
|
3776
|
+
return false;
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
/**
|
|
3780
|
+
* Handle save button click
|
|
3781
|
+
*/
|
|
3782
|
+
onSaveClick() {
|
|
3783
|
+
this.saveWorkflow.emit();
|
|
3784
|
+
}
|
|
3785
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DesignerToolbarComponent, deps: [{ token: NodeManagementService }, { token: SwimlaneService }], target: i0.ɵɵFactoryTarget.Component });
|
|
3786
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: DesignerToolbarComponent, selector: "lib-designer-toolbar", inputs: { selectedTool: "selectedTool", isSaving: "isSaving" }, outputs: { toolSelected: "toolSelected", saveWorkflow: "saveWorkflow" }, ngImport: i0, template: "<div class=\"designer-toolbar\">\n <div class=\"toolbar-container\">\n <button\n *ngFor=\"let item of toolbarItems\"\n class=\"tool-button\"\n [class.active]=\"selectedTool === item.id\"\n [class.disabled]=\"!isEnabled(item.id)\"\n (click)=\"onToolClick(item.id)\"\n [title]=\"item.label\"\n [disabled]=\"!isEnabled(item.id)\"\n >\n {{ item.label }}\n </button>\n </div>\n\n <button class=\"save-button\" (click)=\"onSaveClick()\" [disabled]=\"isSaving\">\n {{ isSaving ? \"Saving...\" : \"Save Workflow\" }}\n </button>\n</div>\n", styles: [".designer-toolbar{padding:.5rem;background-color:#fff;border-bottom:1px solid #e2e8f0;display:flex;justify-content:space-between;align-items:center}.toolbar-container{display:flex;gap:.5rem;background-color:#fff;border:1px solid #d8b4fe;border-radius:.5rem;padding:.5rem;box-shadow:0 2px 4px #0000001a;max-width:fit-content}.tool-button{padding:.5rem 1rem;background-color:#fff;border:1px solid #e2e8f0;border-radius:.25rem;cursor:pointer;font-size:.875rem;transition:all .2s}.tool-button:hover{background-color:#f9fafb}.tool-button.active{background-color:#f3e8ff;border-color:#d8b4fe;color:#7e22ce}.tool-button.active{opacity:.75}.save-button{padding:.5rem 1rem;background-color:#f3e8ff;border:1px solid #d8b4fe;border-radius:.25rem;color:#7e22ce;font-size:.875rem;cursor:pointer;transition:all .2s}.save-button:hover{background-color:#e9d5ff}.save-button:disabled{opacity:.5;cursor:not-allowed}\n"], dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }] });
|
|
3787
|
+
}
|
|
3788
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DesignerToolbarComponent, decorators: [{
|
|
3789
|
+
type: Component,
|
|
3790
|
+
args: [{ selector: 'lib-designer-toolbar', template: "<div class=\"designer-toolbar\">\n <div class=\"toolbar-container\">\n <button\n *ngFor=\"let item of toolbarItems\"\n class=\"tool-button\"\n [class.active]=\"selectedTool === item.id\"\n [class.disabled]=\"!isEnabled(item.id)\"\n (click)=\"onToolClick(item.id)\"\n [title]=\"item.label\"\n [disabled]=\"!isEnabled(item.id)\"\n >\n {{ item.label }}\n </button>\n </div>\n\n <button class=\"save-button\" (click)=\"onSaveClick()\" [disabled]=\"isSaving\">\n {{ isSaving ? \"Saving...\" : \"Save Workflow\" }}\n </button>\n</div>\n", styles: [".designer-toolbar{padding:.5rem;background-color:#fff;border-bottom:1px solid #e2e8f0;display:flex;justify-content:space-between;align-items:center}.toolbar-container{display:flex;gap:.5rem;background-color:#fff;border:1px solid #d8b4fe;border-radius:.5rem;padding:.5rem;box-shadow:0 2px 4px #0000001a;max-width:fit-content}.tool-button{padding:.5rem 1rem;background-color:#fff;border:1px solid #e2e8f0;border-radius:.25rem;cursor:pointer;font-size:.875rem;transition:all .2s}.tool-button:hover{background-color:#f9fafb}.tool-button.active{background-color:#f3e8ff;border-color:#d8b4fe;color:#7e22ce}.tool-button.active{opacity:.75}.save-button{padding:.5rem 1rem;background-color:#f3e8ff;border:1px solid #d8b4fe;border-radius:.25rem;color:#7e22ce;font-size:.875rem;cursor:pointer;transition:all .2s}.save-button:hover{background-color:#e9d5ff}.save-button:disabled{opacity:.5;cursor:not-allowed}\n"] }]
|
|
3791
|
+
}], ctorParameters: () => [{ type: NodeManagementService }, { type: SwimlaneService }], propDecorators: { selectedTool: [{
|
|
3792
|
+
type: Input
|
|
3793
|
+
}], isSaving: [{
|
|
3794
|
+
type: Input
|
|
3795
|
+
}], toolSelected: [{
|
|
3796
|
+
type: Output
|
|
3797
|
+
}], saveWorkflow: [{
|
|
3798
|
+
type: Output
|
|
3799
|
+
}] } });
|
|
3800
|
+
|
|
3801
|
+
class StageNodeComponent {
|
|
3802
|
+
state;
|
|
3803
|
+
dataService;
|
|
3804
|
+
node;
|
|
3805
|
+
isStartNode = false;
|
|
3806
|
+
stageData = {};
|
|
3807
|
+
stagePropertiesUpdated = new EventEmitter();
|
|
3808
|
+
parallelExecutionToggled = new EventEmitter();
|
|
3809
|
+
showShieldDialog = new EventEmitter();
|
|
3810
|
+
// Properties for icon click events
|
|
3811
|
+
showFormPopup = false;
|
|
3812
|
+
showTimerPopup = false;
|
|
3813
|
+
showActionPopupLeft = false;
|
|
3814
|
+
showActionPopupRight = false;
|
|
3815
|
+
showCodePopup = false;
|
|
3816
|
+
formsList = [];
|
|
3817
|
+
isLoadingForms = false;
|
|
3818
|
+
formPopupX = 0;
|
|
3819
|
+
formPopupY = 0;
|
|
3820
|
+
constructor(state, dataService) {
|
|
3821
|
+
this.state = state;
|
|
3822
|
+
this.dataService = dataService;
|
|
3823
|
+
}
|
|
3824
|
+
ngOnInit() {
|
|
3825
|
+
console.log('Stage node initialized with:', this.node);
|
|
3826
|
+
this.updateConnectedStagesInfo();
|
|
3827
|
+
}
|
|
3828
|
+
// Method to check if this stage has multiple outgoing connections to other stages
|
|
3829
|
+
updateConnectedStagesInfo() {
|
|
3830
|
+
// Count the number of outgoing connections that connect to stages
|
|
3831
|
+
const outgoingConnections = this.state.connections.filter((conn) => conn.sourceNodeId === this.node.id);
|
|
3832
|
+
// Check if the target nodes are stages
|
|
3833
|
+
const connectedStageNodes = outgoingConnections
|
|
3834
|
+
.map((conn) => this.state.findNodeById(conn.targetNodeId))
|
|
3835
|
+
.filter((nodeInfo) => nodeInfo && nodeInfo.node.type === 'stage');
|
|
3836
|
+
this.node.hasMultipleConnectedStages = connectedStageNodes.length > 1;
|
|
3837
|
+
}
|
|
3838
|
+
get hasMultipleConnectedStages() {
|
|
3839
|
+
return this.node.hasMultipleConnectedStages === true;
|
|
3840
|
+
}
|
|
3841
|
+
get isParallelExecution() {
|
|
3842
|
+
return this.node.stageData?.hasParallel === true;
|
|
3843
|
+
}
|
|
3844
|
+
selectForm(form) {
|
|
3845
|
+
// If form is null, we're clearing the selection
|
|
3846
|
+
if (!this.node.stageData) {
|
|
3847
|
+
this.node.stageData = {};
|
|
3848
|
+
}
|
|
3849
|
+
if (form) {
|
|
3850
|
+
this.node.stageData.formId = form.Id;
|
|
3851
|
+
this.node.stageData.formName = form.Name;
|
|
3852
|
+
}
|
|
3853
|
+
else {
|
|
3854
|
+
// Clear form selection
|
|
3855
|
+
delete this.node.stageData.formId;
|
|
3856
|
+
delete this.node.stageData.formName;
|
|
3857
|
+
}
|
|
3858
|
+
// Close the popup
|
|
3859
|
+
this.showFormPopup = false;
|
|
3860
|
+
// Emit an event to update the stage data
|
|
3861
|
+
this.stagePropertiesUpdated.emit({
|
|
3862
|
+
nodeId: this.node.id,
|
|
3863
|
+
stageData: this.node.stageData,
|
|
3864
|
+
});
|
|
3865
|
+
}
|
|
3866
|
+
toggleFormPopup(event) {
|
|
3867
|
+
if (event) {
|
|
3868
|
+
event.preventDefault();
|
|
3869
|
+
event.stopPropagation();
|
|
3870
|
+
// Calculate absolute position for the popup
|
|
3871
|
+
const rect = event.target.getBoundingClientRect();
|
|
3872
|
+
this.formPopupX = rect.left + window.scrollX;
|
|
3873
|
+
this.formPopupY = rect.top + window.scrollY;
|
|
3874
|
+
}
|
|
3875
|
+
// If we're opening the popup, load forms
|
|
3876
|
+
if (!this.showFormPopup) {
|
|
3877
|
+
this.isLoadingForms = true;
|
|
3878
|
+
this.dataService
|
|
3879
|
+
.getForms()
|
|
3880
|
+
.then((response) => {
|
|
3881
|
+
this.formsList = response.Result;
|
|
3882
|
+
this.isLoadingForms = false;
|
|
3883
|
+
})
|
|
3884
|
+
.catch((error) => {
|
|
3885
|
+
console.error('Error loading forms:', error);
|
|
3886
|
+
this.isLoadingForms = false;
|
|
3887
|
+
});
|
|
3888
|
+
}
|
|
3889
|
+
this.showFormPopup = !this.showFormPopup;
|
|
3890
|
+
}
|
|
3891
|
+
toggleShieldPopup(event) {
|
|
3892
|
+
if (event) {
|
|
3893
|
+
event.preventDefault();
|
|
3894
|
+
event.stopPropagation();
|
|
3895
|
+
}
|
|
3896
|
+
console.log('Shield icon clicked for node:', this.node.id);
|
|
3897
|
+
this.showShieldDialog.emit(this.node.id);
|
|
3898
|
+
}
|
|
3899
|
+
toggleTimerPopup(event) {
|
|
3900
|
+
if (event) {
|
|
3901
|
+
event.preventDefault();
|
|
3902
|
+
event.stopPropagation();
|
|
3903
|
+
}
|
|
3904
|
+
this.showTimerPopup = !this.showTimerPopup;
|
|
3905
|
+
}
|
|
3906
|
+
toggleActionPopup(side, event) {
|
|
3907
|
+
if (event) {
|
|
3908
|
+
event.preventDefault();
|
|
3909
|
+
event.stopPropagation();
|
|
3910
|
+
}
|
|
3911
|
+
if (side === 'left') {
|
|
3912
|
+
this.showActionPopupLeft = !this.showActionPopupLeft;
|
|
3913
|
+
}
|
|
3914
|
+
else {
|
|
3915
|
+
this.showActionPopupRight = !this.showActionPopupRight;
|
|
3916
|
+
}
|
|
3917
|
+
}
|
|
3918
|
+
toggleCodePopup(event) {
|
|
3919
|
+
if (event) {
|
|
3920
|
+
event.preventDefault();
|
|
3921
|
+
event.stopPropagation();
|
|
3922
|
+
}
|
|
3923
|
+
// Toggle parallel execution state
|
|
3924
|
+
const newParallelState = !this.isParallelExecution;
|
|
3925
|
+
// Update this node's stage data
|
|
3926
|
+
if (!this.node.stageData) {
|
|
3927
|
+
this.node.stageData = {};
|
|
3928
|
+
}
|
|
3929
|
+
this.node.stageData.hasParallel = newParallelState;
|
|
3930
|
+
console.log(`Parallel execution ${newParallelState ? 'enabled' : 'disabled'} for node:`, this.node.id);
|
|
3931
|
+
}
|
|
3932
|
+
// This method should be called whenever connections change
|
|
3933
|
+
refreshState() {
|
|
3934
|
+
this.updateConnectedStagesInfo();
|
|
3935
|
+
}
|
|
3936
|
+
onStagePropertiesSaved(stageData) {
|
|
3937
|
+
// Emit the event to the parent component with the node id and updated data
|
|
3938
|
+
this.stagePropertiesUpdated.emit({
|
|
3939
|
+
nodeId: this.node.id,
|
|
3940
|
+
stageData: stageData,
|
|
3941
|
+
});
|
|
3942
|
+
console.log('Stage properties updated:', stageData);
|
|
3943
|
+
}
|
|
3944
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: StageNodeComponent, deps: [{ token: WorkflowDesignerState }, { token: WorkflowDataService }], target: i0.ɵɵFactoryTarget.Component });
|
|
3945
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: StageNodeComponent, selector: "svg:g[lib-stage-node]", inputs: { node: "node", isStartNode: "isStartNode", stageData: "stageData" }, outputs: { stagePropertiesUpdated: "stagePropertiesUpdated", parallelExecutionToggled: "parallelExecutionToggled", showShieldDialog: "showShieldDialog" }, ngImport: i0, template: "<svg:g>\n <!-- Stage node -->\n <svg:rect\n x=\"0\"\n y=\"0\"\n [attr.width]=\"node.width\"\n [attr.height]=\"node.height\"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n ></svg:rect>\n\n <!-- Top-left icon: Stage form -->\n <svg:g\n (click)=\"toggleFormPopup($event)\"\n class=\"stage-icon\"\n transform=\"translate(6, 6)\"\n >\n <!-- Transparent rectangle to capture clicks -->\n <svg:rect\n x=\"-2\"\n y=\"-2\"\n width=\"24\"\n height=\"24\"\n fill=\"transparent\"\n style=\"cursor: pointer\"\n />\n\n <svg:path\n d=\"M16.5 20.475V17.475H13.5V16.475H16.5V13.475H17.5V16.475H20.5V17.475H17.5V20.475H16.5ZM3.5 17.5V16.5H4.5V17.5H3.5ZM6.5 17.5V16.5H11.517C11.5057 16.6767 11.5043 16.845 11.513 17.005C11.521 17.165 11.531 17.33 11.543 17.5H6.5ZM3.5 13.5V12.5H4.5V13.5H3.5ZM6.5 13.5V12.5H13.804C13.6127 12.6387 13.4333 12.7913 13.266 12.958C13.0993 13.1247 12.9377 13.3053 12.781 13.5H6.5ZM3.5 9.5V8.5H4.5V9.5H3.5ZM6.5 9.5V8.5H18.5V9.5H6.5ZM3.5 5.5V4.5H4.5V5.5H3.5ZM6.5 5.5V4.5H18.5V5.5H6.5Z\"\n [attr.fill]=\"node.stageData?.formId ? '#D36CFF' : 'black'\"\n />\n </svg:g>\n\n <!-- Top-right icon: Shield -->\n <svg:g\n (click)=\"toggleShieldPopup($event)\"\n class=\"stage-icon\"\n [attr.transform]=\"'translate(' + (node.width - 30) + ', 6)'\"\n >\n <!-- Transparent rectangle to capture clicks -->\n <svg:rect\n x=\"-2\"\n y=\"-2\"\n width=\"24\"\n height=\"24\"\n fill=\"transparent\"\n style=\"cursor: pointer\"\n />\n\n <svg:path\n d=\"M7.5 12H16.5V13.5H7.5V12ZM7.5 7.5H16.5V9H7.5V7.5Z\"\n fill=\"black\"\n />\n <svg:path\n d=\"M12 22.5L7.36801 20.0303C6.0474 19.3279 4.94303 18.2791 4.17348 16.9964C3.40393 15.7138 2.99825 14.2458 3.00001 12.75V3C3.00001 2.60218 3.15804 2.22064 3.43935 1.93934C3.72065 1.65804 4.10218 1.5 4.50001 1.5H19.5C19.8978 1.5 20.2794 1.65804 20.5607 1.93934C20.842 2.22064 21 2.60218 21 3V12.75C21.0018 14.2458 20.5961 15.7138 19.8265 16.9964C19.057 18.2791 17.9526 19.3279 16.632 20.0303L12 22.5ZM4.50001 3V12.75C4.49917 13.9738 4.83141 15.1747 5.46111 16.224C6.09082 17.2733 6.99423 18.1315 8.07451 18.7065L12 20.7997L15.9255 18.7073C17.0059 18.1322 17.9094 17.2739 18.5391 16.2244C19.1688 15.175 19.501 13.9739 19.5 12.75V3H4.50001Z\"\n fill=\"black\"\n />\n </svg:g>\n\n <!-- Left-center icon: Thunderbolt -->\n <svg:g\n (click)=\"toggleActionPopup('left', $event)\"\n class=\"stage-icon\"\n transform=\"translate(6, 38)\"\n >\n <svg:path d=\"M11 15H6L13 1V9H18L11 23V15Z\" fill=\"black\" />\n </svg:g>\n\n <!-- Right-center icon: Thunderbolt -->\n <svg:g\n (click)=\"toggleActionPopup('right', $event)\"\n class=\"stage-icon\"\n [attr.transform]=\"'translate(' + (node.width - 30) + ', 38)'\"\n >\n <svg:path d=\"M11 15H6L13 1V9H18L11 23V15Z\" fill=\"black\" />\n </svg:g>\n\n <!-- Bottom-left icon: Double slash text -->\n <svg:g\n *ngIf=\"hasMultipleConnectedStages\"\n (click)=\"toggleCodePopup($event)\"\n class=\"stage-icon\"\n [attr.transform]=\"'translate(6, ' + (node.height - 16) + ')'\"\n >\n <svg:text\n font-family=\"'Plus Jakarta Sans', sans-serif\"\n font-weight=\"500\"\n font-size=\"16px\"\n dominant-baseline=\"middle\"\n [attr.fill]=\"isParallelExecution ? '#D36CFF' : 'black'\"\n >\n //\n </svg:text>\n </svg:g>\n\n <!-- Bottom-right icon: Timer -->\n <svg:g\n (click)=\"toggleTimerPopup($event)\"\n class=\"stage-icon\"\n [attr.transform]=\"\n 'translate(' + (node.width - 30) + ', ' + (node.height - 30) + ')'\n \"\n >\n <svg:path\n d=\"M11.5 3C14.0196 3 16.4359 4.00089 18.2175 5.78249C19.9991 7.56408 21 9.98044 21 12.5C21 15.0196 19.9991 17.4359 18.2175 19.2175C16.4359 20.9991 14.0196 22 11.5 22C8.98044 22 6.56408 20.9991 4.78249 19.2175C3.00089 17.4359 2 15.0196 2 12.5C2 9.98044 3.00089 7.56408 4.78249 5.78249C6.56408 4.00089 8.98044 3 11.5 3ZM11.5 4C9.24566 4 7.08365 4.89553 5.48959 6.48959C3.89553 8.08365 3 10.2457 3 12.5C3 14.7543 3.89553 16.9163 5.48959 18.5104C7.08365 20.1045 9.24566 21 11.5 21C12.6162 21 13.7215 20.7801 14.7528 20.353C15.7841 19.9258 16.7211 19.2997 17.5104 18.5104C18.2997 17.7211 18.9258 16.7841 19.353 15.7528C19.7801 14.7215 20 13.6162 20 12.5C20 10.2457 19.1045 8.08365 17.5104 6.48959C15.9163 4.89553 13.7543 4 11.5 4ZM11 7H12V12.42L16.7 15.13L16.2 16L11 13V7Z\"\n fill=\"black\"\n />\n </svg:g>\n\n <!-- Label in the center -->\n <svg:text\n [attr.x]=\"node.width / 2\"\n [attr.y]=\"node.height / 2\"\n text-anchor=\"middle\"\n dominant-baseline=\"middle\"\n font-size=\"14\"\n fill=\"#000000\"\n >\n {{ node.stageData?.Name || \"Stage\" }}\n </svg:text>\n</svg:g>\n\n<!-- <lib-stage-dialog\n [visible]=\"showShieldPopup()\"\n [stageData]=\"node.stageData || {}\"\n (closed)=\"showShieldPopup.set(false)\"\n (saved)=\"onStagePropertiesSaved($event)\"\n></lib-stage-dialog> -->\n\n<div\n *ngIf=\"showFormPopup\"\n [style.position]=\"'fixed'\"\n [style.left.px]=\"formPopupX\"\n [style.top.px]=\"formPopupY\"\n [style.background-color]=\"'white'\"\n>\n <verben-pop-Up [dropdownOpen]=\"true\" [customStyles]=\"{ 'z-index': '100' }\">\n <div dropdown-trigger style=\"display: none\">\n <!-- Hidden trigger element -->\n </div>\n <div\n class=\"p-3 bg-white border border-gray-200 rounded shadow-md w-64\"\n dropdown-content\n style=\"background-color: white; z-index: 1000\"\n >\n <h4 class=\"mb-2 font-medium\">Select Form</h4>\n <div *ngIf=\"isLoadingForms\" class=\"text-center py-2\">\n Loading forms...\n </div>\n <div *ngIf=\"!isLoadingForms\" class=\"max-h-48 overflow-y-auto\">\n <div class=\"mb-2\">\n <button\n class=\"w-full px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50 text-left\"\n (click)=\"selectForm(null)\"\n >\n Clear form selection\n </button>\n </div>\n <div *ngFor=\"let form of formsList\" class=\"mb-1\">\n <button\n class=\"w-full px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50 text-left\"\n (click)=\"selectForm(form)\"\n >\n {{ form.Name }}\n </button>\n </div>\n <div\n *ngIf=\"formsList.length === 0\"\n class=\"text-center py-2 text-gray-500\"\n >\n No forms available\n </div>\n </div>\n </div>\n </verben-pop-Up>\n</div>\n", styles: [".stage-icon{cursor:pointer}.stage-icon:hover path{fill:#d36cff}\n"], dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i8.VerbenPopUpComponent, selector: "verben-pop-Up", inputs: ["dropdownOpen", "dropdownWidth", "color", "customStyles", "popUpClass", "border", "borderRadius", "enableMouseLeave"], outputs: ["dropdownOpenChange", "close"] }] });
|
|
3946
|
+
}
|
|
3947
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: StageNodeComponent, decorators: [{
|
|
3948
|
+
type: Component,
|
|
3949
|
+
args: [{ selector: 'svg:g[lib-stage-node]', template: "<svg:g>\n <!-- Stage node -->\n <svg:rect\n x=\"0\"\n y=\"0\"\n [attr.width]=\"node.width\"\n [attr.height]=\"node.height\"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n ></svg:rect>\n\n <!-- Top-left icon: Stage form -->\n <svg:g\n (click)=\"toggleFormPopup($event)\"\n class=\"stage-icon\"\n transform=\"translate(6, 6)\"\n >\n <!-- Transparent rectangle to capture clicks -->\n <svg:rect\n x=\"-2\"\n y=\"-2\"\n width=\"24\"\n height=\"24\"\n fill=\"transparent\"\n style=\"cursor: pointer\"\n />\n\n <svg:path\n d=\"M16.5 20.475V17.475H13.5V16.475H16.5V13.475H17.5V16.475H20.5V17.475H17.5V20.475H16.5ZM3.5 17.5V16.5H4.5V17.5H3.5ZM6.5 17.5V16.5H11.517C11.5057 16.6767 11.5043 16.845 11.513 17.005C11.521 17.165 11.531 17.33 11.543 17.5H6.5ZM3.5 13.5V12.5H4.5V13.5H3.5ZM6.5 13.5V12.5H13.804C13.6127 12.6387 13.4333 12.7913 13.266 12.958C13.0993 13.1247 12.9377 13.3053 12.781 13.5H6.5ZM3.5 9.5V8.5H4.5V9.5H3.5ZM6.5 9.5V8.5H18.5V9.5H6.5ZM3.5 5.5V4.5H4.5V5.5H3.5ZM6.5 5.5V4.5H18.5V5.5H6.5Z\"\n [attr.fill]=\"node.stageData?.formId ? '#D36CFF' : 'black'\"\n />\n </svg:g>\n\n <!-- Top-right icon: Shield -->\n <svg:g\n (click)=\"toggleShieldPopup($event)\"\n class=\"stage-icon\"\n [attr.transform]=\"'translate(' + (node.width - 30) + ', 6)'\"\n >\n <!-- Transparent rectangle to capture clicks -->\n <svg:rect\n x=\"-2\"\n y=\"-2\"\n width=\"24\"\n height=\"24\"\n fill=\"transparent\"\n style=\"cursor: pointer\"\n />\n\n <svg:path\n d=\"M7.5 12H16.5V13.5H7.5V12ZM7.5 7.5H16.5V9H7.5V7.5Z\"\n fill=\"black\"\n />\n <svg:path\n d=\"M12 22.5L7.36801 20.0303C6.0474 19.3279 4.94303 18.2791 4.17348 16.9964C3.40393 15.7138 2.99825 14.2458 3.00001 12.75V3C3.00001 2.60218 3.15804 2.22064 3.43935 1.93934C3.72065 1.65804 4.10218 1.5 4.50001 1.5H19.5C19.8978 1.5 20.2794 1.65804 20.5607 1.93934C20.842 2.22064 21 2.60218 21 3V12.75C21.0018 14.2458 20.5961 15.7138 19.8265 16.9964C19.057 18.2791 17.9526 19.3279 16.632 20.0303L12 22.5ZM4.50001 3V12.75C4.49917 13.9738 4.83141 15.1747 5.46111 16.224C6.09082 17.2733 6.99423 18.1315 8.07451 18.7065L12 20.7997L15.9255 18.7073C17.0059 18.1322 17.9094 17.2739 18.5391 16.2244C19.1688 15.175 19.501 13.9739 19.5 12.75V3H4.50001Z\"\n fill=\"black\"\n />\n </svg:g>\n\n <!-- Left-center icon: Thunderbolt -->\n <svg:g\n (click)=\"toggleActionPopup('left', $event)\"\n class=\"stage-icon\"\n transform=\"translate(6, 38)\"\n >\n <svg:path d=\"M11 15H6L13 1V9H18L11 23V15Z\" fill=\"black\" />\n </svg:g>\n\n <!-- Right-center icon: Thunderbolt -->\n <svg:g\n (click)=\"toggleActionPopup('right', $event)\"\n class=\"stage-icon\"\n [attr.transform]=\"'translate(' + (node.width - 30) + ', 38)'\"\n >\n <svg:path d=\"M11 15H6L13 1V9H18L11 23V15Z\" fill=\"black\" />\n </svg:g>\n\n <!-- Bottom-left icon: Double slash text -->\n <svg:g\n *ngIf=\"hasMultipleConnectedStages\"\n (click)=\"toggleCodePopup($event)\"\n class=\"stage-icon\"\n [attr.transform]=\"'translate(6, ' + (node.height - 16) + ')'\"\n >\n <svg:text\n font-family=\"'Plus Jakarta Sans', sans-serif\"\n font-weight=\"500\"\n font-size=\"16px\"\n dominant-baseline=\"middle\"\n [attr.fill]=\"isParallelExecution ? '#D36CFF' : 'black'\"\n >\n //\n </svg:text>\n </svg:g>\n\n <!-- Bottom-right icon: Timer -->\n <svg:g\n (click)=\"toggleTimerPopup($event)\"\n class=\"stage-icon\"\n [attr.transform]=\"\n 'translate(' + (node.width - 30) + ', ' + (node.height - 30) + ')'\n \"\n >\n <svg:path\n d=\"M11.5 3C14.0196 3 16.4359 4.00089 18.2175 5.78249C19.9991 7.56408 21 9.98044 21 12.5C21 15.0196 19.9991 17.4359 18.2175 19.2175C16.4359 20.9991 14.0196 22 11.5 22C8.98044 22 6.56408 20.9991 4.78249 19.2175C3.00089 17.4359 2 15.0196 2 12.5C2 9.98044 3.00089 7.56408 4.78249 5.78249C6.56408 4.00089 8.98044 3 11.5 3ZM11.5 4C9.24566 4 7.08365 4.89553 5.48959 6.48959C3.89553 8.08365 3 10.2457 3 12.5C3 14.7543 3.89553 16.9163 5.48959 18.5104C7.08365 20.1045 9.24566 21 11.5 21C12.6162 21 13.7215 20.7801 14.7528 20.353C15.7841 19.9258 16.7211 19.2997 17.5104 18.5104C18.2997 17.7211 18.9258 16.7841 19.353 15.7528C19.7801 14.7215 20 13.6162 20 12.5C20 10.2457 19.1045 8.08365 17.5104 6.48959C15.9163 4.89553 13.7543 4 11.5 4ZM11 7H12V12.42L16.7 15.13L16.2 16L11 13V7Z\"\n fill=\"black\"\n />\n </svg:g>\n\n <!-- Label in the center -->\n <svg:text\n [attr.x]=\"node.width / 2\"\n [attr.y]=\"node.height / 2\"\n text-anchor=\"middle\"\n dominant-baseline=\"middle\"\n font-size=\"14\"\n fill=\"#000000\"\n >\n {{ node.stageData?.Name || \"Stage\" }}\n </svg:text>\n</svg:g>\n\n<!-- <lib-stage-dialog\n [visible]=\"showShieldPopup()\"\n [stageData]=\"node.stageData || {}\"\n (closed)=\"showShieldPopup.set(false)\"\n (saved)=\"onStagePropertiesSaved($event)\"\n></lib-stage-dialog> -->\n\n<div\n *ngIf=\"showFormPopup\"\n [style.position]=\"'fixed'\"\n [style.left.px]=\"formPopupX\"\n [style.top.px]=\"formPopupY\"\n [style.background-color]=\"'white'\"\n>\n <verben-pop-Up [dropdownOpen]=\"true\" [customStyles]=\"{ 'z-index': '100' }\">\n <div dropdown-trigger style=\"display: none\">\n <!-- Hidden trigger element -->\n </div>\n <div\n class=\"p-3 bg-white border border-gray-200 rounded shadow-md w-64\"\n dropdown-content\n style=\"background-color: white; z-index: 1000\"\n >\n <h4 class=\"mb-2 font-medium\">Select Form</h4>\n <div *ngIf=\"isLoadingForms\" class=\"text-center py-2\">\n Loading forms...\n </div>\n <div *ngIf=\"!isLoadingForms\" class=\"max-h-48 overflow-y-auto\">\n <div class=\"mb-2\">\n <button\n class=\"w-full px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50 text-left\"\n (click)=\"selectForm(null)\"\n >\n Clear form selection\n </button>\n </div>\n <div *ngFor=\"let form of formsList\" class=\"mb-1\">\n <button\n class=\"w-full px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50 text-left\"\n (click)=\"selectForm(form)\"\n >\n {{ form.Name }}\n </button>\n </div>\n <div\n *ngIf=\"formsList.length === 0\"\n class=\"text-center py-2 text-gray-500\"\n >\n No forms available\n </div>\n </div>\n </div>\n </verben-pop-Up>\n</div>\n", styles: [".stage-icon{cursor:pointer}.stage-icon:hover path{fill:#d36cff}\n"] }]
|
|
3950
|
+
}], ctorParameters: () => [{ type: WorkflowDesignerState }, { type: WorkflowDataService }], propDecorators: { node: [{
|
|
3951
|
+
type: Input
|
|
3952
|
+
}], isStartNode: [{
|
|
3953
|
+
type: Input
|
|
3954
|
+
}], stageData: [{
|
|
3955
|
+
type: Input
|
|
3956
|
+
}], stagePropertiesUpdated: [{
|
|
3957
|
+
type: Output
|
|
3958
|
+
}], parallelExecutionToggled: [{
|
|
3959
|
+
type: Output
|
|
3960
|
+
}], showShieldDialog: [{
|
|
3961
|
+
type: Output
|
|
3962
|
+
}] } });
|
|
3963
|
+
|
|
3964
|
+
/**
|
|
3965
|
+
* Service for managing various popups in the workflow designer
|
|
3966
|
+
*/
|
|
3967
|
+
class PopupService {
|
|
3968
|
+
dataService;
|
|
3969
|
+
// Connection creation popup
|
|
3970
|
+
_connectionPopupVisible = new BehaviorSubject(false);
|
|
3971
|
+
_connectionPopupPosition = new BehaviorSubject({ x: 0, y: 0 });
|
|
3972
|
+
_allowedNodeTypes = new BehaviorSubject([]);
|
|
3973
|
+
// Form selection popup for stages
|
|
3974
|
+
_formPopupVisible = new BehaviorSubject(false);
|
|
3975
|
+
_formPopupPosition = new BehaviorSubject({
|
|
3976
|
+
x: 0,
|
|
3977
|
+
y: 0,
|
|
3978
|
+
});
|
|
3979
|
+
_formsList = new BehaviorSubject([]);
|
|
3980
|
+
_isLoadingForms = new BehaviorSubject(false);
|
|
3981
|
+
_selectedNodeForForm = new BehaviorSubject(null);
|
|
3982
|
+
// Workflow form selection popup for start node
|
|
3983
|
+
_startNodeFormPopupVisible = new BehaviorSubject(false);
|
|
3984
|
+
_startNodeFormPopupPosition = new BehaviorSubject({ x: 0, y: 0 });
|
|
3985
|
+
// Subflow selection popup
|
|
3986
|
+
_subflowPopupVisible = new BehaviorSubject(false);
|
|
3987
|
+
_subflowPopupPosition = new BehaviorSubject({ x: 0, y: 0 });
|
|
3988
|
+
_workflowsList = new BehaviorSubject([]);
|
|
3989
|
+
_isLoadingWorkflows = new BehaviorSubject(false);
|
|
3990
|
+
_pendingSubflowPosition = new BehaviorSubject(null);
|
|
3991
|
+
// Connection data for popups
|
|
3992
|
+
_pendingConnectionSourcePoint = new BehaviorSubject(null);
|
|
3993
|
+
_pendingConnectionSourceSwimlaneIndex = new BehaviorSubject(null);
|
|
3994
|
+
constructor(dataService) {
|
|
3995
|
+
this.dataService = dataService;
|
|
3996
|
+
}
|
|
3997
|
+
// Connection popup observables
|
|
3998
|
+
get connectionPopupVisible() {
|
|
3999
|
+
return this._connectionPopupVisible.asObservable();
|
|
4000
|
+
}
|
|
4001
|
+
get connectionPopupPosition() {
|
|
4002
|
+
return this._connectionPopupPosition.asObservable();
|
|
4003
|
+
}
|
|
4004
|
+
get allowedNodeTypes() {
|
|
4005
|
+
return this._allowedNodeTypes.asObservable();
|
|
4006
|
+
}
|
|
4007
|
+
// Form popup observables
|
|
4008
|
+
get formPopupVisible() {
|
|
4009
|
+
return this._formPopupVisible.asObservable();
|
|
4010
|
+
}
|
|
4011
|
+
get formPopupPosition() {
|
|
4012
|
+
return this._formPopupPosition.asObservable();
|
|
4013
|
+
}
|
|
4014
|
+
get formsList() {
|
|
4015
|
+
return this._formsList.asObservable();
|
|
4016
|
+
}
|
|
4017
|
+
get isLoadingForms() {
|
|
4018
|
+
return this._isLoadingForms.asObservable();
|
|
4019
|
+
}
|
|
4020
|
+
get selectedNodeForForm() {
|
|
4021
|
+
return this._selectedNodeForForm.asObservable();
|
|
4022
|
+
}
|
|
4023
|
+
// Start node form popup observables
|
|
4024
|
+
get startNodeFormPopupVisible() {
|
|
4025
|
+
return this._startNodeFormPopupVisible.asObservable();
|
|
4026
|
+
}
|
|
4027
|
+
get startNodeFormPopupPosition() {
|
|
4028
|
+
return this._startNodeFormPopupPosition.asObservable();
|
|
4029
|
+
}
|
|
4030
|
+
// Subflow popup observables
|
|
4031
|
+
get subflowPopupVisible() {
|
|
4032
|
+
return this._subflowPopupVisible.asObservable();
|
|
4033
|
+
}
|
|
4034
|
+
get subflowPopupPosition() {
|
|
4035
|
+
return this._subflowPopupPosition.asObservable();
|
|
4036
|
+
}
|
|
4037
|
+
get workflowsList() {
|
|
4038
|
+
return this._workflowsList.asObservable();
|
|
4039
|
+
}
|
|
4040
|
+
get isLoadingWorkflows() {
|
|
4041
|
+
return this._isLoadingWorkflows.asObservable();
|
|
4042
|
+
}
|
|
4043
|
+
get pendingSubflowPosition() {
|
|
4044
|
+
return this._pendingSubflowPosition.asObservable();
|
|
4045
|
+
}
|
|
4046
|
+
// Connection data observables
|
|
4047
|
+
get pendingConnectionSourcePoint() {
|
|
4048
|
+
return this._pendingConnectionSourcePoint.asObservable();
|
|
4049
|
+
}
|
|
4050
|
+
get pendingConnectionSourceSwimlaneIndex() {
|
|
4051
|
+
return this._pendingConnectionSourceSwimlaneIndex.asObservable();
|
|
4052
|
+
}
|
|
4053
|
+
/**
|
|
4054
|
+
* Show connection creation popup
|
|
4055
|
+
*/
|
|
4056
|
+
showConnectionPopup(x, y, allowedNodeTypes) {
|
|
4057
|
+
this._connectionPopupPosition.next({ x, y });
|
|
4058
|
+
this._allowedNodeTypes.next(allowedNodeTypes);
|
|
4059
|
+
this._connectionPopupVisible.next(true);
|
|
4060
|
+
}
|
|
4061
|
+
/**
|
|
4062
|
+
* Hide connection creation popup
|
|
4063
|
+
*/
|
|
4064
|
+
hideConnectionPopup() {
|
|
4065
|
+
this._connectionPopupVisible.next(false);
|
|
4066
|
+
}
|
|
4067
|
+
/**
|
|
4068
|
+
* Store connection source data for use in other popups
|
|
4069
|
+
*/
|
|
4070
|
+
storeConnectionSourceData(point, swimlaneIndex) {
|
|
4071
|
+
this._pendingConnectionSourcePoint.next(point);
|
|
4072
|
+
this._pendingConnectionSourceSwimlaneIndex.next(swimlaneIndex);
|
|
4073
|
+
}
|
|
4074
|
+
/**
|
|
4075
|
+
* Clear connection source data
|
|
4076
|
+
*/
|
|
4077
|
+
clearConnectionSourceData() {
|
|
4078
|
+
this._pendingConnectionSourcePoint.next(null);
|
|
4079
|
+
this._pendingConnectionSourceSwimlaneIndex.next(null);
|
|
4080
|
+
}
|
|
4081
|
+
/**
|
|
4082
|
+
* Show form selection popup for a stage
|
|
4083
|
+
*/
|
|
4084
|
+
async showFormPopup(x, y, nodeId) {
|
|
4085
|
+
this._formPopupPosition.next({ x, y });
|
|
4086
|
+
this._selectedNodeForForm.next(nodeId);
|
|
4087
|
+
this._formPopupVisible.next(true);
|
|
4088
|
+
// Load forms
|
|
4089
|
+
this._isLoadingForms.next(true);
|
|
4090
|
+
try {
|
|
4091
|
+
const response = await this.dataService.getForms();
|
|
4092
|
+
this._formsList.next(response.Result);
|
|
4093
|
+
}
|
|
4094
|
+
catch (error) {
|
|
4095
|
+
console.error('Error loading forms:', error);
|
|
4096
|
+
this._formsList.next([]);
|
|
4097
|
+
}
|
|
4098
|
+
finally {
|
|
4099
|
+
this._isLoadingForms.next(false);
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
/**
|
|
4103
|
+
* Hide form selection popup
|
|
4104
|
+
*/
|
|
4105
|
+
hideFormPopup() {
|
|
4106
|
+
this._formPopupVisible.next(false);
|
|
4107
|
+
this._selectedNodeForForm.next(null);
|
|
4108
|
+
}
|
|
4109
|
+
/**
|
|
4110
|
+
* Show workflow form selection popup for start node
|
|
4111
|
+
*/
|
|
4112
|
+
async showStartNodeFormPopup(x, y) {
|
|
4113
|
+
this._startNodeFormPopupPosition.next({ x, y });
|
|
4114
|
+
this._startNodeFormPopupVisible.next(true);
|
|
4115
|
+
// Load forms
|
|
4116
|
+
this._isLoadingForms.next(true);
|
|
4117
|
+
try {
|
|
4118
|
+
const response = await this.dataService.getForms();
|
|
4119
|
+
this._formsList.next(response.Result);
|
|
4120
|
+
}
|
|
4121
|
+
catch (error) {
|
|
4122
|
+
console.error('Error loading forms:', error);
|
|
4123
|
+
this._formsList.next([]);
|
|
4124
|
+
}
|
|
4125
|
+
finally {
|
|
4126
|
+
this._isLoadingForms.next(false);
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
/**
|
|
4130
|
+
* Hide workflow form selection popup
|
|
4131
|
+
*/
|
|
4132
|
+
hideStartNodeFormPopup() {
|
|
4133
|
+
this._startNodeFormPopupVisible.next(false);
|
|
4134
|
+
}
|
|
4135
|
+
/**
|
|
4136
|
+
* Show subflow selection popup
|
|
4137
|
+
*/
|
|
4138
|
+
async showSubflowPopup(x, y, swimlaneIndex) {
|
|
4139
|
+
this._subflowPopupPosition.next({ x, y });
|
|
4140
|
+
this._pendingSubflowPosition.next({ swimlaneIndex, x, y });
|
|
4141
|
+
this._subflowPopupVisible.next(true);
|
|
4142
|
+
// Load workflows
|
|
4143
|
+
this._isLoadingWorkflows.next(true);
|
|
4144
|
+
try {
|
|
4145
|
+
const response = await this.dataService.getWorkflows();
|
|
4146
|
+
this._workflowsList.next(response.Result);
|
|
4147
|
+
}
|
|
4148
|
+
catch (error) {
|
|
4149
|
+
console.error('Error loading workflows:', error);
|
|
4150
|
+
this._workflowsList.next([]);
|
|
4151
|
+
}
|
|
4152
|
+
finally {
|
|
4153
|
+
this._isLoadingWorkflows.next(false);
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
/**
|
|
4157
|
+
* Hide subflow selection popup
|
|
4158
|
+
*/
|
|
4159
|
+
hideSubflowPopup() {
|
|
4160
|
+
this._subflowPopupVisible.next(false);
|
|
4161
|
+
this._pendingSubflowPosition.next(null);
|
|
4162
|
+
}
|
|
4163
|
+
// Current values for internal use
|
|
4164
|
+
getCurrentValues() {
|
|
4165
|
+
return {
|
|
4166
|
+
connectionPopupVisible: this._connectionPopupVisible.getValue(),
|
|
4167
|
+
connectionPopupPosition: this._connectionPopupPosition.getValue(),
|
|
4168
|
+
allowedNodeTypes: this._allowedNodeTypes.getValue(),
|
|
4169
|
+
formPopupVisible: this._formPopupVisible.getValue(),
|
|
4170
|
+
formPopupPosition: this._formPopupPosition.getValue(),
|
|
4171
|
+
formsList: this._formsList.getValue(),
|
|
4172
|
+
isLoadingForms: this._isLoadingForms.getValue(),
|
|
4173
|
+
selectedNodeForForm: this._selectedNodeForForm.getValue(),
|
|
4174
|
+
startNodeFormPopupVisible: this._startNodeFormPopupVisible.getValue(),
|
|
4175
|
+
startNodeFormPopupPosition: this._startNodeFormPopupPosition.getValue(),
|
|
4176
|
+
subflowPopupVisible: this._subflowPopupVisible.getValue(),
|
|
4177
|
+
subflowPopupPosition: this._subflowPopupPosition.getValue(),
|
|
4178
|
+
workflowsList: this._workflowsList.getValue(),
|
|
4179
|
+
isLoadingWorkflows: this._isLoadingWorkflows.getValue(),
|
|
4180
|
+
pendingSubflowPosition: this._pendingSubflowPosition.getValue(),
|
|
4181
|
+
pendingConnectionSourcePoint: this._pendingConnectionSourcePoint.getValue(),
|
|
4182
|
+
pendingConnectionSourceSwimlaneIndex: this._pendingConnectionSourceSwimlaneIndex.getValue(),
|
|
4183
|
+
};
|
|
4184
|
+
}
|
|
4185
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: PopupService, deps: [{ token: WorkflowDataService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4186
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: PopupService, providedIn: 'root' });
|
|
4187
|
+
}
|
|
4188
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: PopupService, decorators: [{
|
|
4189
|
+
type: Injectable,
|
|
4190
|
+
args: [{
|
|
4191
|
+
providedIn: 'root',
|
|
4192
|
+
}]
|
|
4193
|
+
}], ctorParameters: () => [{ type: WorkflowDataService }] });
|
|
4194
|
+
|
|
4195
|
+
class StageDialogComponent {
|
|
4196
|
+
fb;
|
|
4197
|
+
dataService;
|
|
4198
|
+
nodeService;
|
|
4199
|
+
popupService;
|
|
4200
|
+
// Use Angular's signal-based input
|
|
4201
|
+
visible = input(false);
|
|
4202
|
+
stageData = {}; // For editing existing stages
|
|
4203
|
+
closed = new EventEmitter();
|
|
4204
|
+
saved = new EventEmitter();
|
|
4205
|
+
stageForm;
|
|
4206
|
+
tags = [];
|
|
4207
|
+
actorRules = Object.values(StageActorRule);
|
|
4208
|
+
selectedTagIds = [];
|
|
4209
|
+
hasOutgoingConnections = false;
|
|
4210
|
+
constructor(fb, dataService, nodeService, popupService) {
|
|
4211
|
+
this.fb = fb;
|
|
4212
|
+
this.dataService = dataService;
|
|
4213
|
+
this.nodeService = nodeService;
|
|
4214
|
+
this.popupService = popupService;
|
|
4215
|
+
this.stageForm = this.fb.group({
|
|
4216
|
+
Name: ['', Validators.required],
|
|
4217
|
+
Description: [''],
|
|
4218
|
+
MinNoOfActor: [0],
|
|
4219
|
+
Duration: [0],
|
|
4220
|
+
PassOnRule: [''],
|
|
4221
|
+
ActorRule: [StageActorRule.None],
|
|
4222
|
+
IsExitPoint: [false],
|
|
4223
|
+
Tags: [[]],
|
|
4224
|
+
});
|
|
4225
|
+
}
|
|
4226
|
+
ngOnInit() {
|
|
4227
|
+
// Load tags
|
|
4228
|
+
this.loadTags();
|
|
4229
|
+
// If editing an existing stage, populate form
|
|
4230
|
+
this.initFormWithStageData();
|
|
4231
|
+
}
|
|
4232
|
+
ngOnChanges(changes) {
|
|
4233
|
+
// If stageData changes, update the form
|
|
4234
|
+
if (changes['stageData'] && this.stageData) {
|
|
4235
|
+
this.stageForm.patchValue({
|
|
4236
|
+
Name: this.stageData.Name || '',
|
|
4237
|
+
Description: this.stageData.Description || '',
|
|
4238
|
+
MinNoOfActor: this.stageData.MinNoOfActor || 0,
|
|
4239
|
+
Duration: this.stageData.Duration || 0,
|
|
4240
|
+
PassOnRule: this.stageData.PassOnRule || '',
|
|
4241
|
+
ActorRule: this.stageData.ActorRule || StageActorRule.None,
|
|
4242
|
+
IsExitPoint: this.stageData.IsExitPoint || false,
|
|
4243
|
+
});
|
|
4244
|
+
// Update selected tags
|
|
4245
|
+
if (this.stageData.Tags && this.stageData.Tags.length > 0) {
|
|
4246
|
+
this.selectedTagIds = this.stageData.Tags.map((tag) => tag.Id);
|
|
4247
|
+
}
|
|
4248
|
+
else {
|
|
4249
|
+
this.selectedTagIds = [];
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
/**
|
|
4254
|
+
* Load tags from data service
|
|
4255
|
+
*/
|
|
4256
|
+
loadTags() {
|
|
4257
|
+
this.dataService.getTags().then((data) => {
|
|
4258
|
+
this.tags = data.Result;
|
|
4259
|
+
// If we have stage data with tags, update selectedTagIds after tags are loaded
|
|
4260
|
+
if (this.stageData &&
|
|
4261
|
+
this.stageData.Tags &&
|
|
4262
|
+
this.stageData.Tags.length > 0) {
|
|
4263
|
+
this.selectedTagIds = this.stageData.Tags.map((tag) => tag.Id);
|
|
4264
|
+
}
|
|
4265
|
+
});
|
|
4266
|
+
}
|
|
4267
|
+
/**
|
|
4268
|
+
* Initialize form with stage data if provided
|
|
4269
|
+
*/
|
|
4270
|
+
initFormWithStageData() {
|
|
4271
|
+
if (this.stageData) {
|
|
4272
|
+
console.log('Initializing form with stage data:', this.stageData);
|
|
4273
|
+
this.stageForm.patchValue({
|
|
4274
|
+
Name: this.stageData.Name || '',
|
|
4275
|
+
Description: this.stageData.Description || '',
|
|
4276
|
+
MinNoOfActor: this.stageData.MinNoOfActor || 0,
|
|
4277
|
+
Duration: this.stageData.Duration || 0,
|
|
4278
|
+
PassOnRule: this.stageData.PassOnRule || '',
|
|
4279
|
+
ActorRule: this.stageData.ActorRule || StageActorRule.None,
|
|
4280
|
+
IsExitPoint: this.stageData.IsExitPoint || false,
|
|
4281
|
+
});
|
|
4282
|
+
}
|
|
4283
|
+
}
|
|
4284
|
+
checkOutgoingConnections() {
|
|
4285
|
+
if (this.stageData && this.stageData.Id) {
|
|
4286
|
+
// Check if there are outgoing connections from this stage
|
|
4287
|
+
this.hasOutgoingConnections = this.nodeService.hasOutgoingConnections(this.stageData.Id);
|
|
4288
|
+
// If there are outgoing connections, disable IsExitPoint
|
|
4289
|
+
if (this.hasOutgoingConnections) {
|
|
4290
|
+
this.stageForm.get('IsExitPoint')?.disable();
|
|
4291
|
+
}
|
|
4292
|
+
else {
|
|
4293
|
+
this.stageForm.get('IsExitPoint')?.enable();
|
|
3360
4294
|
}
|
|
3361
|
-
});
|
|
3362
|
-
// If editing an existing stage, populate form
|
|
3363
|
-
if (this.stageData) {
|
|
3364
|
-
console.log('Initializing form with stage data:', this.stageData);
|
|
3365
|
-
this.stageForm.patchValue({
|
|
3366
|
-
Name: this.stageData.Name || '',
|
|
3367
|
-
Description: this.stageData.Description || '',
|
|
3368
|
-
MinNoOfActor: this.stageData.MinNoOfActor || 0,
|
|
3369
|
-
Duration: this.stageData.Duration || 0,
|
|
3370
|
-
PassOnRule: this.stageData.PassOnRule || '',
|
|
3371
|
-
ActorRule: this.stageData.ActorRule || StageActorRule.None,
|
|
3372
|
-
});
|
|
3373
4295
|
}
|
|
3374
4296
|
}
|
|
3375
4297
|
onDialogClose(event) {
|
|
@@ -3382,7 +4304,9 @@ class StageDialogComponent {
|
|
|
3382
4304
|
...this.stageForm.value,
|
|
3383
4305
|
Tags: selectedTags,
|
|
3384
4306
|
};
|
|
4307
|
+
// Reset form after saving
|
|
3385
4308
|
this.stageForm.reset();
|
|
4309
|
+
// Emit the saved data to the parent component
|
|
3386
4310
|
this.saved.emit(stageData);
|
|
3387
4311
|
}
|
|
3388
4312
|
}
|
|
@@ -3401,13 +4325,13 @@ class StageDialogComponent {
|
|
|
3401
4325
|
onDialogOpen(event) {
|
|
3402
4326
|
console.log('Dialog opened:', event);
|
|
3403
4327
|
}
|
|
3404
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: StageDialogComponent, deps: [{ token: i1$2.FormBuilder }, { token: WorkflowDataService }], target: i0.ɵɵFactoryTarget.Component });
|
|
3405
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "18.2.13", type: StageDialogComponent, selector: "lib-stage-dialog", inputs: { visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null }, stageData: { classPropertyName: "stageData", publicName: "stageData", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { closed: "closed", saved: "saved" }, ngImport: i0, template: "<verben-dialogue\n [showCloseIcon]=\"true\"\n [dismissOutsideClick]=\"true\"\n [closeOnEscape]=\"true\"\n [size]=\"'medium'\"\n [mode]=\"'drawer'\"\n [disableFooter]=\"false\"\n [isVisible]=\"visible()\"\n [headerTemplate]=\"headerTemplate\"\n [bodyTemplate]=\"bodyTemplate\"\n [footerTemplate]=\"footerTemplate\"\n (openModal)=\"onDialogOpen($event)\"\n (closeModal)=\"onDialogClose($event)\"\n>\n</verben-dialogue>\n\n<ng-template #headerTemplate>\n <div class=\"p-4 border-b border-gray-200\">\n <h2 class=\"text-xl font-medium m-0\">Stage Properties</h2>\n </div>\n</ng-template>\n\n<ng-template #bodyTemplate>\n <div class=\"p-4\">\n <form [formGroup]=\"stageForm\" class=\"space-y-4\">\n <!-- Stage Name -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Stage Name</label>\n <input\n type=\"text\"\n formControlName=\"Name\"\n placeholder=\"Enter Stage Name\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n />\n </div>\n\n <!-- Description -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Description</label>\n <textarea\n formControlName=\"Description\"\n placeholder=\"Enter Description\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n rows=\"3\"\n ></textarea>\n </div>\n\n <!-- Recipient Count -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Recipient Count</label>\n <input\n type=\"number\"\n formControlName=\"MinNoOfActor\"\n placeholder=\"Enter Count\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n />\n </div>\n\n <!-- Pass On Rule -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Pass On Rule</label>\n <input\n type=\"text\"\n formControlName=\"PassOnRule\"\n placeholder=\"Select Rule\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n />\n </div>\n\n <!-- Duration -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Duration</label>\n <input\n type=\"number\"\n formControlName=\"Duration\"\n placeholder=\"Set Duration\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n />\n </div>\n\n <!-- Actor Rule -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Actor Rule</label>\n <select\n formControlName=\"ActorRule\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n >\n <option *ngFor=\"let rule of actorRules\" [value]=\"rule\">\n {{ rule }}\n </option>\n </select>\n </div>\n\n <!-- Tags -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Tags</label>\n <div\n class=\"border border-gray-200 rounded p-2 max-h-40 overflow-y-auto\"\n >\n <div *ngFor=\"let tag of tags\" class=\"py-1\">\n <label class=\"flex items-center\">\n <input\n type=\"checkbox\"\n [checked]=\"isTagSelected(tag.Id)\"\n (change)=\"toggleTagSelection(tag.Id)\"\n class=\"mr-2\"\n />\n <span>{{ tag.Name }}</span>\n </label>\n </div>\n </div>\n </div>\n </form>\n </div>\n</ng-template>\n\n<ng-template #footerTemplate>\n <div class=\"flex justify-end p-4 border-t border-gray-200\">\n <button\n class=\"px-4 py-2 mr-2 bg-white border border-gray-200 rounded text-sm font-medium\"\n (click)=\"onDialogClose($event)\"\n >\n Cancel\n </button>\n <button\n class=\"px-4 py-2 bg-yellow-300 text-black rounded text-sm font-medium\"\n (click)=\"saveStage()\"\n [disabled]=\"stageForm.invalid\"\n >\n Save\n </button>\n </div>\n</ng-template>\n", styles: [""], dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: i8.VerbenDialogueComponent, selector: "verben-dialogue", inputs: ["headerTemplate", "bodyTemplate", "footerTemplate", "showCloseIcon", "dismissOutsideClick", "closeOnEscape", "isVisible", "size", "backdropColor", "customClass", "disableFooter", "margin", "padding", "borderRadius", "dialogueBgColor", "closeIconClass", "boxShadow", "enableTransition", "modalData", "mode", "position", "drawerWidth"], outputs: ["openModal", "closeModal"] }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }] });
|
|
4328
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: StageDialogComponent, deps: [{ token: i1$2.FormBuilder }, { token: WorkflowDataService }, { token: NodeManagementService }, { token: PopupService }], target: i0.ɵɵFactoryTarget.Component });
|
|
4329
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "18.2.13", type: StageDialogComponent, selector: "lib-stage-dialog", inputs: { visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null }, stageData: { classPropertyName: "stageData", publicName: "stageData", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { closed: "closed", saved: "saved" }, usesOnChanges: true, ngImport: i0, template: "<verben-dialogue\n [showCloseIcon]=\"true\"\n [dismissOutsideClick]=\"true\"\n [closeOnEscape]=\"true\"\n [size]=\"'medium'\"\n [mode]=\"'drawer'\"\n [disableFooter]=\"false\"\n [isVisible]=\"visible()\"\n [headerTemplate]=\"headerTemplate\"\n [bodyTemplate]=\"bodyTemplate\"\n [footerTemplate]=\"footerTemplate\"\n (openModal)=\"onDialogOpen($event)\"\n (closeModal)=\"onDialogClose($event)\"\n>\n</verben-dialogue>\n\n<ng-template #headerTemplate>\n <div class=\"p-4 border-b border-gray-200\">\n <h2 class=\"text-xl font-medium m-0\">Stage Properties</h2>\n </div>\n</ng-template>\n\n<ng-template #bodyTemplate>\n <div class=\"p-4\">\n <form [formGroup]=\"stageForm\" class=\"space-y-4\">\n <!-- Stage Name -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Stage Name</label>\n <input\n type=\"text\"\n formControlName=\"Name\"\n placeholder=\"Enter Stage Name\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n />\n </div>\n\n <!-- Description -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Description</label>\n <textarea\n formControlName=\"Description\"\n placeholder=\"Enter Description\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n rows=\"3\"\n ></textarea>\n </div>\n\n <!-- Recipient Count -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Recipient Count</label>\n <input\n type=\"number\"\n formControlName=\"MinNoOfActor\"\n placeholder=\"Enter Count\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n />\n </div>\n\n <!-- Pass On Rule -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Pass On Rule</label>\n <input\n type=\"text\"\n formControlName=\"PassOnRule\"\n placeholder=\"Select Rule\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n />\n </div>\n\n <!-- Duration -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Duration</label>\n <input\n type=\"number\"\n formControlName=\"Duration\"\n placeholder=\"Set Duration\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n />\n </div>\n\n <!-- Actor Rule -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Actor Rule</label>\n <select\n formControlName=\"ActorRule\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n >\n <option *ngFor=\"let rule of actorRules\" [value]=\"rule\">\n {{ rule }}\n </option>\n </select>\n </div>\n\n <!-- Is Exit Point -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Exit Point</label>\n <div class=\"flex items-center\">\n <input\n type=\"checkbox\"\n formControlName=\"IsExitPoint\"\n [disabled]=\"hasOutgoingConnections\"\n class=\"mr-2 h-4 w-4\"\n />\n <span class=\"text-sm text-gray-600\">Mark as workflow end point</span>\n </div>\n </div>\n\n <!-- Tags -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Tags</label>\n <div\n class=\"border border-gray-200 rounded p-2 max-h-40 overflow-y-auto\"\n >\n <div *ngFor=\"let tag of tags\" class=\"py-1\">\n <label class=\"flex items-center\">\n <input\n type=\"checkbox\"\n [checked]=\"isTagSelected(tag.Id)\"\n (change)=\"toggleTagSelection(tag.Id)\"\n class=\"mr-2\"\n />\n <span>{{ tag.Name }}</span>\n </label>\n </div>\n </div>\n </div>\n </form>\n </div>\n</ng-template>\n\n<ng-template #footerTemplate>\n <div class=\"flex justify-end p-4 border-t border-gray-200\">\n <button\n class=\"px-4 py-2 mr-2 bg-white border border-gray-200 rounded text-sm font-medium\"\n (click)=\"onDialogClose($event)\"\n >\n Cancel\n </button>\n <button\n class=\"px-4 py-2 bg-yellow-300 text-black rounded text-sm font-medium\"\n (click)=\"saveStage()\"\n [disabled]=\"stageForm.invalid\"\n >\n Save\n </button>\n </div>\n</ng-template>\n", styles: [""], dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: i8.VerbenDialogueComponent, selector: "verben-dialogue", inputs: ["headerTemplate", "bodyTemplate", "footerTemplate", "showCloseIcon", "dismissOutsideClick", "closeOnEscape", "isVisible", "size", "backdropColor", "customClass", "disableFooter", "margin", "padding", "borderRadius", "dialogueBgColor", "closeIconClass", "boxShadow", "enableTransition", "modalData", "mode", "position", "drawerWidth"], outputs: ["openModal", "closeModal"] }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }] });
|
|
3406
4330
|
}
|
|
3407
4331
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: StageDialogComponent, decorators: [{
|
|
3408
4332
|
type: Component,
|
|
3409
|
-
args: [{ selector: 'lib-stage-dialog', template: "<verben-dialogue\n [showCloseIcon]=\"true\"\n [dismissOutsideClick]=\"true\"\n [closeOnEscape]=\"true\"\n [size]=\"'medium'\"\n [mode]=\"'drawer'\"\n [disableFooter]=\"false\"\n [isVisible]=\"visible()\"\n [headerTemplate]=\"headerTemplate\"\n [bodyTemplate]=\"bodyTemplate\"\n [footerTemplate]=\"footerTemplate\"\n (openModal)=\"onDialogOpen($event)\"\n (closeModal)=\"onDialogClose($event)\"\n>\n</verben-dialogue>\n\n<ng-template #headerTemplate>\n <div class=\"p-4 border-b border-gray-200\">\n <h2 class=\"text-xl font-medium m-0\">Stage Properties</h2>\n </div>\n</ng-template>\n\n<ng-template #bodyTemplate>\n <div class=\"p-4\">\n <form [formGroup]=\"stageForm\" class=\"space-y-4\">\n <!-- Stage Name -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Stage Name</label>\n <input\n type=\"text\"\n formControlName=\"Name\"\n placeholder=\"Enter Stage Name\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n />\n </div>\n\n <!-- Description -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Description</label>\n <textarea\n formControlName=\"Description\"\n placeholder=\"Enter Description\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n rows=\"3\"\n ></textarea>\n </div>\n\n <!-- Recipient Count -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Recipient Count</label>\n <input\n type=\"number\"\n formControlName=\"MinNoOfActor\"\n placeholder=\"Enter Count\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n />\n </div>\n\n <!-- Pass On Rule -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Pass On Rule</label>\n <input\n type=\"text\"\n formControlName=\"PassOnRule\"\n placeholder=\"Select Rule\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n />\n </div>\n\n <!-- Duration -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Duration</label>\n <input\n type=\"number\"\n formControlName=\"Duration\"\n placeholder=\"Set Duration\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n />\n </div>\n\n <!-- Actor Rule -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Actor Rule</label>\n <select\n formControlName=\"ActorRule\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n >\n <option *ngFor=\"let rule of actorRules\" [value]=\"rule\">\n {{ rule }}\n </option>\n </select>\n </div>\n\n <!-- Tags -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Tags</label>\n <div\n class=\"border border-gray-200 rounded p-2 max-h-40 overflow-y-auto\"\n >\n <div *ngFor=\"let tag of tags\" class=\"py-1\">\n <label class=\"flex items-center\">\n <input\n type=\"checkbox\"\n [checked]=\"isTagSelected(tag.Id)\"\n (change)=\"toggleTagSelection(tag.Id)\"\n class=\"mr-2\"\n />\n <span>{{ tag.Name }}</span>\n </label>\n </div>\n </div>\n </div>\n </form>\n </div>\n</ng-template>\n\n<ng-template #footerTemplate>\n <div class=\"flex justify-end p-4 border-t border-gray-200\">\n <button\n class=\"px-4 py-2 mr-2 bg-white border border-gray-200 rounded text-sm font-medium\"\n (click)=\"onDialogClose($event)\"\n >\n Cancel\n </button>\n <button\n class=\"px-4 py-2 bg-yellow-300 text-black rounded text-sm font-medium\"\n (click)=\"saveStage()\"\n [disabled]=\"stageForm.invalid\"\n >\n Save\n </button>\n </div>\n</ng-template>\n" }]
|
|
3410
|
-
}], ctorParameters: () => [{ type: i1$2.FormBuilder }, { type: WorkflowDataService }], propDecorators: { stageData: [{
|
|
4333
|
+
args: [{ selector: 'lib-stage-dialog', template: "<verben-dialogue\n [showCloseIcon]=\"true\"\n [dismissOutsideClick]=\"true\"\n [closeOnEscape]=\"true\"\n [size]=\"'medium'\"\n [mode]=\"'drawer'\"\n [disableFooter]=\"false\"\n [isVisible]=\"visible()\"\n [headerTemplate]=\"headerTemplate\"\n [bodyTemplate]=\"bodyTemplate\"\n [footerTemplate]=\"footerTemplate\"\n (openModal)=\"onDialogOpen($event)\"\n (closeModal)=\"onDialogClose($event)\"\n>\n</verben-dialogue>\n\n<ng-template #headerTemplate>\n <div class=\"p-4 border-b border-gray-200\">\n <h2 class=\"text-xl font-medium m-0\">Stage Properties</h2>\n </div>\n</ng-template>\n\n<ng-template #bodyTemplate>\n <div class=\"p-4\">\n <form [formGroup]=\"stageForm\" class=\"space-y-4\">\n <!-- Stage Name -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Stage Name</label>\n <input\n type=\"text\"\n formControlName=\"Name\"\n placeholder=\"Enter Stage Name\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n />\n </div>\n\n <!-- Description -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Description</label>\n <textarea\n formControlName=\"Description\"\n placeholder=\"Enter Description\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n rows=\"3\"\n ></textarea>\n </div>\n\n <!-- Recipient Count -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Recipient Count</label>\n <input\n type=\"number\"\n formControlName=\"MinNoOfActor\"\n placeholder=\"Enter Count\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n />\n </div>\n\n <!-- Pass On Rule -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Pass On Rule</label>\n <input\n type=\"text\"\n formControlName=\"PassOnRule\"\n placeholder=\"Select Rule\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n />\n </div>\n\n <!-- Duration -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Duration</label>\n <input\n type=\"number\"\n formControlName=\"Duration\"\n placeholder=\"Set Duration\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n />\n </div>\n\n <!-- Actor Rule -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Actor Rule</label>\n <select\n formControlName=\"ActorRule\"\n class=\"w-full px-4 py-2 rounded border border-gray-200\"\n >\n <option *ngFor=\"let rule of actorRules\" [value]=\"rule\">\n {{ rule }}\n </option>\n </select>\n </div>\n\n <!-- Is Exit Point -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Exit Point</label>\n <div class=\"flex items-center\">\n <input\n type=\"checkbox\"\n formControlName=\"IsExitPoint\"\n [disabled]=\"hasOutgoingConnections\"\n class=\"mr-2 h-4 w-4\"\n />\n <span class=\"text-sm text-gray-600\">Mark as workflow end point</span>\n </div>\n </div>\n\n <!-- Tags -->\n <div>\n <label class=\"block text-sm font-medium mb-1\">Tags</label>\n <div\n class=\"border border-gray-200 rounded p-2 max-h-40 overflow-y-auto\"\n >\n <div *ngFor=\"let tag of tags\" class=\"py-1\">\n <label class=\"flex items-center\">\n <input\n type=\"checkbox\"\n [checked]=\"isTagSelected(tag.Id)\"\n (change)=\"toggleTagSelection(tag.Id)\"\n class=\"mr-2\"\n />\n <span>{{ tag.Name }}</span>\n </label>\n </div>\n </div>\n </div>\n </form>\n </div>\n</ng-template>\n\n<ng-template #footerTemplate>\n <div class=\"flex justify-end p-4 border-t border-gray-200\">\n <button\n class=\"px-4 py-2 mr-2 bg-white border border-gray-200 rounded text-sm font-medium\"\n (click)=\"onDialogClose($event)\"\n >\n Cancel\n </button>\n <button\n class=\"px-4 py-2 bg-yellow-300 text-black rounded text-sm font-medium\"\n (click)=\"saveStage()\"\n [disabled]=\"stageForm.invalid\"\n >\n Save\n </button>\n </div>\n</ng-template>\n" }]
|
|
4334
|
+
}], ctorParameters: () => [{ type: i1$2.FormBuilder }, { type: WorkflowDataService }, { type: NodeManagementService }, { type: PopupService }], propDecorators: { stageData: [{
|
|
3411
4335
|
type: Input
|
|
3412
4336
|
}], closed: [{
|
|
3413
4337
|
type: Output
|
|
@@ -3415,174 +4339,73 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
|
|
|
3415
4339
|
type: Output
|
|
3416
4340
|
}] } });
|
|
3417
4341
|
|
|
3418
|
-
class
|
|
4342
|
+
class ConditionsPopupComponent {
|
|
3419
4343
|
state;
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
showActionPopupLeft = false;
|
|
3431
|
-
showActionPopupRight = false;
|
|
3432
|
-
showCodePopup = false;
|
|
3433
|
-
formsList = [];
|
|
3434
|
-
isLoadingForms = false;
|
|
3435
|
-
formPopupX = 0;
|
|
3436
|
-
formPopupY = 0;
|
|
3437
|
-
constructor(state, dataService) {
|
|
4344
|
+
visible = false;
|
|
4345
|
+
decisionNodeId = '';
|
|
4346
|
+
newConnectionId = '';
|
|
4347
|
+
popupX = 0;
|
|
4348
|
+
popupY = 0;
|
|
4349
|
+
closed = new EventEmitter();
|
|
4350
|
+
saved = new EventEmitter();
|
|
4351
|
+
connectionConditions = [];
|
|
4352
|
+
currentCondition = '';
|
|
4353
|
+
constructor(state) {
|
|
3438
4354
|
this.state = state;
|
|
3439
|
-
this.dataService = dataService;
|
|
3440
4355
|
}
|
|
3441
4356
|
ngOnInit() {
|
|
3442
|
-
|
|
3443
|
-
this.updateConnectedStagesInfo();
|
|
3444
|
-
}
|
|
3445
|
-
// Method to check if this stage has multiple outgoing connections to other stages
|
|
3446
|
-
updateConnectedStagesInfo() {
|
|
3447
|
-
// Count the number of outgoing connections that connect to stages
|
|
3448
|
-
const outgoingConnections = this.state.connections.filter((conn) => conn.sourceNodeId === this.node.id);
|
|
3449
|
-
// Check if the target nodes are stages
|
|
3450
|
-
const connectedStageNodes = outgoingConnections
|
|
3451
|
-
.map((conn) => this.state.findNodeById(conn.targetNodeId))
|
|
3452
|
-
.filter((nodeInfo) => nodeInfo && nodeInfo.node.type === 'stage');
|
|
3453
|
-
this.node.hasMultipleConnectedStages = connectedStageNodes.length > 1;
|
|
3454
|
-
}
|
|
3455
|
-
get hasMultipleConnectedStages() {
|
|
3456
|
-
return this.node.hasMultipleConnectedStages === true;
|
|
3457
|
-
}
|
|
3458
|
-
get isParallelExecution() {
|
|
3459
|
-
return this.node.stageData?.hasParallel === true;
|
|
4357
|
+
this.loadConnectionConditions();
|
|
3460
4358
|
}
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
this.
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
this.showFormPopup = false;
|
|
3477
|
-
// Emit an event to update the stage data
|
|
3478
|
-
this.stagePropertiesUpdated.emit({
|
|
3479
|
-
nodeId: this.node.id,
|
|
3480
|
-
stageData: this.node.stageData,
|
|
4359
|
+
loadConnectionConditions() {
|
|
4360
|
+
if (!this.decisionNodeId)
|
|
4361
|
+
return;
|
|
4362
|
+
// Get all connections from this decision node
|
|
4363
|
+
const connections = this.state.connections.filter((conn) => conn.sourceNodeId === this.decisionNodeId);
|
|
4364
|
+
this.connectionConditions = connections.map((conn) => {
|
|
4365
|
+
const targetNode = this.state.findNodeById(conn.targetNodeId)?.node;
|
|
4366
|
+
return {
|
|
4367
|
+
connectionId: conn.id,
|
|
4368
|
+
sourceNodeId: conn.sourceNodeId,
|
|
4369
|
+
targetNodeId: conn.targetNodeId,
|
|
4370
|
+
targetNodeName: targetNode?.stageData?.Name || 'Unknown Stage',
|
|
4371
|
+
condition: conn.condition || '',
|
|
4372
|
+
isActive: conn.id === this.newConnectionId,
|
|
4373
|
+
};
|
|
3481
4374
|
});
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
if (
|
|
3485
|
-
|
|
3486
|
-
event.stopPropagation();
|
|
3487
|
-
// Calculate absolute position for the popup
|
|
3488
|
-
const rect = event.target.getBoundingClientRect();
|
|
3489
|
-
this.formPopupX = rect.left + window.scrollX;
|
|
3490
|
-
this.formPopupY = rect.top + window.scrollY;
|
|
3491
|
-
}
|
|
3492
|
-
// If we're opening the popup, load forms
|
|
3493
|
-
if (!this.showFormPopup) {
|
|
3494
|
-
this.isLoadingForms = true;
|
|
3495
|
-
this.dataService
|
|
3496
|
-
.getForms()
|
|
3497
|
-
.then((response) => {
|
|
3498
|
-
this.formsList = response.Result;
|
|
3499
|
-
this.isLoadingForms = false;
|
|
3500
|
-
})
|
|
3501
|
-
.catch((error) => {
|
|
3502
|
-
console.error('Error loading forms:', error);
|
|
3503
|
-
this.isLoadingForms = false;
|
|
3504
|
-
});
|
|
3505
|
-
}
|
|
3506
|
-
this.showFormPopup = !this.showFormPopup;
|
|
3507
|
-
}
|
|
3508
|
-
toggleShieldPopup(event) {
|
|
3509
|
-
// Prevent the event from propagating to parent elements
|
|
3510
|
-
if (event) {
|
|
3511
|
-
event.preventDefault();
|
|
3512
|
-
event.stopPropagation();
|
|
3513
|
-
}
|
|
3514
|
-
// Toggle the shield popup
|
|
3515
|
-
this.showShieldPopup = !this.showShieldPopup;
|
|
3516
|
-
console.log('Shield popup toggled:', this.showShieldPopup, 'with data:', this.node.stageData);
|
|
3517
|
-
// Force change detection in case Angular isn't detecting the state change
|
|
3518
|
-
setTimeout(() => {
|
|
3519
|
-
if (this.showShieldPopup) {
|
|
3520
|
-
console.log('Shield popup should be visible now');
|
|
3521
|
-
}
|
|
3522
|
-
}, 0);
|
|
3523
|
-
}
|
|
3524
|
-
toggleTimerPopup(event) {
|
|
3525
|
-
if (event) {
|
|
3526
|
-
event.preventDefault();
|
|
3527
|
-
event.stopPropagation();
|
|
3528
|
-
}
|
|
3529
|
-
this.showTimerPopup = !this.showTimerPopup;
|
|
3530
|
-
}
|
|
3531
|
-
toggleActionPopup(side, event) {
|
|
3532
|
-
if (event) {
|
|
3533
|
-
event.preventDefault();
|
|
3534
|
-
event.stopPropagation();
|
|
3535
|
-
}
|
|
3536
|
-
if (side === 'left') {
|
|
3537
|
-
this.showActionPopupLeft = !this.showActionPopupLeft;
|
|
4375
|
+
// Find the current connection being edited
|
|
4376
|
+
const activeCondition = this.connectionConditions.find((c) => c.isActive);
|
|
4377
|
+
if (activeCondition) {
|
|
4378
|
+
this.currentCondition = activeCondition.condition;
|
|
3538
4379
|
}
|
|
3539
|
-
else {
|
|
3540
|
-
this.showActionPopupRight = !this.showActionPopupRight;
|
|
3541
|
-
}
|
|
3542
|
-
}
|
|
3543
|
-
toggleCodePopup(event) {
|
|
3544
|
-
if (event) {
|
|
3545
|
-
event.preventDefault();
|
|
3546
|
-
event.stopPropagation();
|
|
3547
|
-
}
|
|
3548
|
-
// Toggle parallel execution state
|
|
3549
|
-
const newParallelState = !this.isParallelExecution;
|
|
3550
|
-
// Update this node's stage data
|
|
3551
|
-
if (!this.node.stageData) {
|
|
3552
|
-
this.node.stageData = {};
|
|
3553
|
-
}
|
|
3554
|
-
this.node.stageData.hasParallel = newParallelState;
|
|
3555
|
-
console.log(`Parallel execution ${newParallelState ? 'enabled' : 'disabled'} for node:`, this.node.id);
|
|
3556
|
-
}
|
|
3557
|
-
// This method should be called whenever connections change
|
|
3558
|
-
refreshState() {
|
|
3559
|
-
this.updateConnectedStagesInfo();
|
|
3560
4380
|
}
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
this.stagePropertiesUpdated.emit({
|
|
3566
|
-
nodeId: this.node.id,
|
|
3567
|
-
stageData: stageData,
|
|
4381
|
+
saveCondition() {
|
|
4382
|
+
this.saved.emit({
|
|
4383
|
+
connectionId: this.newConnectionId,
|
|
4384
|
+
condition: this.currentCondition,
|
|
3568
4385
|
});
|
|
3569
|
-
console.log('Stage properties updated:', stageData);
|
|
3570
4386
|
}
|
|
3571
|
-
|
|
3572
|
-
|
|
4387
|
+
onClose() {
|
|
4388
|
+
this.closed.emit();
|
|
4389
|
+
}
|
|
4390
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ConditionsPopupComponent, deps: [{ token: WorkflowDesignerState }], target: i0.ɵɵFactoryTarget.Component });
|
|
4391
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: ConditionsPopupComponent, selector: "lib-conditions-popup", inputs: { visible: "visible", decisionNodeId: "decisionNodeId", newConnectionId: "newConnectionId", popupX: "popupX", popupY: "popupY" }, outputs: { closed: "closed", saved: "saved" }, ngImport: i0, template: "<div\n *ngIf=\"visible\"\n [style.position]=\"'fixed'\"\n [style.left.px]=\"popupX\"\n [style.top.px]=\"popupY\"\n>\n <verben-pop-Up [dropdownOpen]=\"true\" [customStyles]=\"{ 'z-index': '100' }\">\n <div dropdown-trigger style=\"display: none\">\n <!-- Hidden trigger element -->\n </div>\n <div\n class=\"p-4 bg-white border border-purple-300 rounded shadow-md w-80\"\n dropdown-content\n >\n <div class=\"flex justify-between items-center mb-3\">\n <h3 class=\"text-lg font-medium\">Decision Conditions</h3>\n <button class=\"text-gray-500 hover:text-gray-700\" (click)=\"onClose()\">\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line>\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>\n </svg>\n </button>\n </div>\n\n <div class=\"mb-3\">\n <p class=\"text-sm text-gray-600\">Define a condition for this branch.</p>\n </div>\n\n <div class=\"mb-4\">\n <label class=\"block text-sm font-medium mb-1\">Condition</label>\n <input\n type=\"text\"\n [(ngModel)]=\"currentCondition\"\n placeholder=\"e.g., order.amount > 500,000\"\n class=\"w-full px-3 py-2 rounded border border-gray-200 text-sm\"\n />\n </div>\n\n <div\n class=\"space-y-3 max-h-60 overflow-y-auto\"\n *ngIf=\"connectionConditions.length > 1\"\n >\n <h4 class=\"text-sm font-medium\">Existing Conditions:</h4>\n @for (conn of connectionConditions; track conn.connectionId) { @if\n (!conn.isActive) {\n <div class=\"border rounded p-3\">\n <div class=\"mb-2\">\n <span class=\"font-medium\">{{ conn.targetNodeName }}</span>\n </div>\n <div>\n <input\n type=\"text\"\n [value]=\"conn.condition\"\n placeholder=\"No condition\"\n class=\"w-full px-3 py-2 rounded border border-gray-200 bg-gray-50 text-sm\"\n readonly\n />\n </div>\n </div>\n } }\n </div>\n\n <div class=\"flex justify-end mt-4 pt-3 border-t border-gray-200\">\n <button\n class=\"px-4 py-2 mr-2 bg-white border border-gray-200 rounded text-sm font-medium\"\n (click)=\"onClose()\"\n >\n Cancel\n </button>\n <button\n class=\"px-4 py-2 bg-yellow-300 text-black rounded text-sm font-medium\"\n (click)=\"saveCondition()\"\n >\n Save\n </button>\n </div>\n </div>\n </verben-pop-Up>\n</div>\n", styles: [".border-purple-400{border-color:#d36cff}.bg-purple-50{background-color:#f9f5ff}\n"], dependencies: [{ kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i8.VerbenPopUpComponent, selector: "verben-pop-Up", inputs: ["dropdownOpen", "dropdownWidth", "color", "customStyles", "popUpClass", "border", "borderRadius", "enableMouseLeave"], outputs: ["dropdownOpenChange", "close"] }] });
|
|
3573
4392
|
}
|
|
3574
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type:
|
|
4393
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ConditionsPopupComponent, decorators: [{
|
|
3575
4394
|
type: Component,
|
|
3576
|
-
args: [{ selector: '
|
|
3577
|
-
}], ctorParameters: () => [{ type: WorkflowDesignerState }
|
|
4395
|
+
args: [{ selector: 'lib-conditions-popup', template: "<div\n *ngIf=\"visible\"\n [style.position]=\"'fixed'\"\n [style.left.px]=\"popupX\"\n [style.top.px]=\"popupY\"\n>\n <verben-pop-Up [dropdownOpen]=\"true\" [customStyles]=\"{ 'z-index': '100' }\">\n <div dropdown-trigger style=\"display: none\">\n <!-- Hidden trigger element -->\n </div>\n <div\n class=\"p-4 bg-white border border-purple-300 rounded shadow-md w-80\"\n dropdown-content\n >\n <div class=\"flex justify-between items-center mb-3\">\n <h3 class=\"text-lg font-medium\">Decision Conditions</h3>\n <button class=\"text-gray-500 hover:text-gray-700\" (click)=\"onClose()\">\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line>\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>\n </svg>\n </button>\n </div>\n\n <div class=\"mb-3\">\n <p class=\"text-sm text-gray-600\">Define a condition for this branch.</p>\n </div>\n\n <div class=\"mb-4\">\n <label class=\"block text-sm font-medium mb-1\">Condition</label>\n <input\n type=\"text\"\n [(ngModel)]=\"currentCondition\"\n placeholder=\"e.g., order.amount > 500,000\"\n class=\"w-full px-3 py-2 rounded border border-gray-200 text-sm\"\n />\n </div>\n\n <div\n class=\"space-y-3 max-h-60 overflow-y-auto\"\n *ngIf=\"connectionConditions.length > 1\"\n >\n <h4 class=\"text-sm font-medium\">Existing Conditions:</h4>\n @for (conn of connectionConditions; track conn.connectionId) { @if\n (!conn.isActive) {\n <div class=\"border rounded p-3\">\n <div class=\"mb-2\">\n <span class=\"font-medium\">{{ conn.targetNodeName }}</span>\n </div>\n <div>\n <input\n type=\"text\"\n [value]=\"conn.condition\"\n placeholder=\"No condition\"\n class=\"w-full px-3 py-2 rounded border border-gray-200 bg-gray-50 text-sm\"\n readonly\n />\n </div>\n </div>\n } }\n </div>\n\n <div class=\"flex justify-end mt-4 pt-3 border-t border-gray-200\">\n <button\n class=\"px-4 py-2 mr-2 bg-white border border-gray-200 rounded text-sm font-medium\"\n (click)=\"onClose()\"\n >\n Cancel\n </button>\n <button\n class=\"px-4 py-2 bg-yellow-300 text-black rounded text-sm font-medium\"\n (click)=\"saveCondition()\"\n >\n Save\n </button>\n </div>\n </div>\n </verben-pop-Up>\n</div>\n", styles: [".border-purple-400{border-color:#d36cff}.bg-purple-50{background-color:#f9f5ff}\n"] }]
|
|
4396
|
+
}], ctorParameters: () => [{ type: WorkflowDesignerState }], propDecorators: { visible: [{
|
|
3578
4397
|
type: Input
|
|
3579
|
-
}],
|
|
4398
|
+
}], decisionNodeId: [{
|
|
3580
4399
|
type: Input
|
|
3581
|
-
}],
|
|
4400
|
+
}], newConnectionId: [{
|
|
3582
4401
|
type: Input
|
|
3583
|
-
}],
|
|
4402
|
+
}], popupX: [{
|
|
4403
|
+
type: Input
|
|
4404
|
+
}], popupY: [{
|
|
4405
|
+
type: Input
|
|
4406
|
+
}], closed: [{
|
|
3584
4407
|
type: Output
|
|
3585
|
-
}],
|
|
4408
|
+
}], saved: [{
|
|
3586
4409
|
type: Output
|
|
3587
4410
|
}] } });
|
|
3588
4411
|
|
|
@@ -3594,6 +4417,7 @@ class DesignerCanvasComponent {
|
|
|
3594
4417
|
subflowSelected = new EventEmitter();
|
|
3595
4418
|
pendingStagePosition = null;
|
|
3596
4419
|
showStageDialog = new EventEmitter();
|
|
4420
|
+
stagePropertiesUpdated = new EventEmitter();
|
|
3597
4421
|
// Reference to the SVG element
|
|
3598
4422
|
canvasRef;
|
|
3599
4423
|
onWindowMouseUp(event) {
|
|
@@ -3628,8 +4452,20 @@ class DesignerCanvasComponent {
|
|
|
3628
4452
|
subflowPopupX = 0;
|
|
3629
4453
|
subflowPopupY = 0;
|
|
3630
4454
|
pendingSubflowPosition = null;
|
|
4455
|
+
pendingDecisionConnection = null;
|
|
3631
4456
|
pendingConnectionSourcePoint = null;
|
|
3632
4457
|
pendingConnectionSourceSwimlaneIndex = null;
|
|
4458
|
+
showDecisionConditionDialog = false;
|
|
4459
|
+
showConditionsDialog = false;
|
|
4460
|
+
activeDecisionNodeId = '';
|
|
4461
|
+
activeConnectionId = '';
|
|
4462
|
+
decisionConditionPopupX = 0;
|
|
4463
|
+
decisionConditionPopupY = 0;
|
|
4464
|
+
isCreatingStageFromDecision = false;
|
|
4465
|
+
pendingDecisionCondition = '';
|
|
4466
|
+
activeStageId = null;
|
|
4467
|
+
showStageDialogLocal = false;
|
|
4468
|
+
activeStageData = {};
|
|
3633
4469
|
constructor(state, dataService) {
|
|
3634
4470
|
this.state = state;
|
|
3635
4471
|
this.dataService = dataService;
|
|
@@ -3656,9 +4492,59 @@ class DesignerCanvasComponent {
|
|
|
3656
4492
|
});
|
|
3657
4493
|
}
|
|
3658
4494
|
}
|
|
4495
|
+
onShowShieldDialog(stageId) {
|
|
4496
|
+
// Find the node
|
|
4497
|
+
const nodeInfo = this.state.findNodeById(stageId);
|
|
4498
|
+
if (nodeInfo) {
|
|
4499
|
+
this.activeStageId = stageId;
|
|
4500
|
+
this.activeStageData = nodeInfo.node.stageData || {};
|
|
4501
|
+
this.showStageDialogLocal = true;
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
// Add this method to handle dialog closure
|
|
4505
|
+
onStageDialogClosed() {
|
|
4506
|
+
this.showStageDialogLocal = false;
|
|
4507
|
+
this.activeStageId = null;
|
|
4508
|
+
this.activeStageData = {};
|
|
4509
|
+
}
|
|
4510
|
+
// Add this method to handle saving stage data
|
|
4511
|
+
onStageDialogSaved(stageData) {
|
|
4512
|
+
if (this.activeStageId) {
|
|
4513
|
+
// Editing an existing stage
|
|
4514
|
+
const nodeInfo = this.state.findNodeById(this.activeStageId);
|
|
4515
|
+
if (nodeInfo) {
|
|
4516
|
+
// Update the stage data
|
|
4517
|
+
nodeInfo.node.stageData = {
|
|
4518
|
+
...nodeInfo.node.stageData,
|
|
4519
|
+
...stageData,
|
|
4520
|
+
};
|
|
4521
|
+
console.log('Updated stage data:', nodeInfo.node.stageData);
|
|
4522
|
+
// Emit event for parent component to know about the update
|
|
4523
|
+
this.stagePropertiesUpdated.emit({
|
|
4524
|
+
nodeId: this.activeStageId,
|
|
4525
|
+
stageData: nodeInfo.node.stageData,
|
|
4526
|
+
});
|
|
4527
|
+
}
|
|
4528
|
+
}
|
|
4529
|
+
// Close the dialog
|
|
4530
|
+
this.onStageDialogClosed();
|
|
4531
|
+
}
|
|
3659
4532
|
updateCanvasSize() {
|
|
3660
4533
|
const requiredHeight = (this.state.swimlanes.length + 1) * this.swimlaneHeight;
|
|
3661
4534
|
this.canvasHeight = Math.max(2000, requiredHeight); // Ensure we have at least our default height
|
|
4535
|
+
// Update all the swimlane clip paths when size changes
|
|
4536
|
+
setTimeout(() => {
|
|
4537
|
+
this.state.swimlanes.forEach((swimlane, index) => {
|
|
4538
|
+
const clipPathId = `swimlane-clip-${index}`;
|
|
4539
|
+
const clipPathElement = document.getElementById(clipPathId);
|
|
4540
|
+
if (clipPathElement) {
|
|
4541
|
+
const rectElement = clipPathElement.querySelector('rect');
|
|
4542
|
+
if (rectElement) {
|
|
4543
|
+
rectElement.setAttribute('width', this.canvasWidth.toString());
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
});
|
|
4547
|
+
}, 0);
|
|
3662
4548
|
}
|
|
3663
4549
|
// Helper method to get the mouse position relative to the SVG canvas
|
|
3664
4550
|
getMousePosition(event) {
|
|
@@ -3704,6 +4590,15 @@ class DesignerCanvasComponent {
|
|
|
3704
4590
|
startConnectionDrag(event, point, swimlaneIndex) {
|
|
3705
4591
|
event.preventDefault();
|
|
3706
4592
|
event.stopPropagation();
|
|
4593
|
+
// Check if this is an exit point node
|
|
4594
|
+
const nodeInfo = this.state.findNodeById(point.nodeId);
|
|
4595
|
+
if (nodeInfo &&
|
|
4596
|
+
nodeInfo.node.type === 'stage' &&
|
|
4597
|
+
nodeInfo.node.stageData?.IsExitPoint) {
|
|
4598
|
+
// Don't allow connections from exit points
|
|
4599
|
+
console.log('Cannot create connections from exit points');
|
|
4600
|
+
return;
|
|
4601
|
+
}
|
|
3707
4602
|
const position = this.getMousePosition(event);
|
|
3708
4603
|
this.state.startConnectionDrag(point, swimlaneIndex, position.x, position.y);
|
|
3709
4604
|
// Add mouse move and mouse up event listeners to the document
|
|
@@ -3713,8 +4608,20 @@ class DesignerCanvasComponent {
|
|
|
3713
4608
|
// Use arrow functions to preserve 'this' context
|
|
3714
4609
|
onMouseMove = (event) => {
|
|
3715
4610
|
const position = this.getMousePosition(event);
|
|
3716
|
-
this.
|
|
4611
|
+
this.updateConnectionDrag(position.x, position.y);
|
|
3717
4612
|
};
|
|
4613
|
+
updateConnectionDrag(currentX, currentY) {
|
|
4614
|
+
if (this.state.draggingConnectionData.sourcePoint) {
|
|
4615
|
+
this.state.draggingConnectionData.currentX = currentX;
|
|
4616
|
+
this.state.draggingConnectionData.currentY = currentY;
|
|
4617
|
+
// Calculate which swimlane the cursor is over
|
|
4618
|
+
const swimlaneIndex = Math.floor(currentY / this.swimlaneHeight);
|
|
4619
|
+
if (swimlaneIndex >= 0 && swimlaneIndex < this.state.swimlanes.length) {
|
|
4620
|
+
// Ensure the target swimlane is fully visible
|
|
4621
|
+
this.ensureSwimlaneInView(swimlaneIndex);
|
|
4622
|
+
}
|
|
4623
|
+
}
|
|
4624
|
+
}
|
|
3718
4625
|
onMouseUp = (event) => {
|
|
3719
4626
|
document.removeEventListener('mousemove', this.onMouseMove);
|
|
3720
4627
|
document.removeEventListener('mouseup', this.onMouseUp);
|
|
@@ -3794,22 +4701,66 @@ class DesignerCanvasComponent {
|
|
|
3794
4701
|
return;
|
|
3795
4702
|
}
|
|
3796
4703
|
const { endX, endY, sourceSwimlaneIndex } = pathData;
|
|
4704
|
+
// Get source node info
|
|
4705
|
+
const sourcePoint = this.state.draggingConnectionData.sourcePoint;
|
|
4706
|
+
if (!sourcePoint) {
|
|
4707
|
+
this.hideConnectionPopup();
|
|
4708
|
+
return;
|
|
4709
|
+
}
|
|
4710
|
+
const sourceNodeInfo = this.state.findNodeById(sourcePoint.nodeId);
|
|
4711
|
+
if (!sourceNodeInfo) {
|
|
4712
|
+
this.hideConnectionPopup();
|
|
4713
|
+
return;
|
|
4714
|
+
}
|
|
4715
|
+
// If connecting from a decision to a stage, show condition popup first
|
|
4716
|
+
if (sourceNodeInfo.node.type === 'decision' && nodeType === 'stage') {
|
|
4717
|
+
// Store information for later stage creation
|
|
4718
|
+
this.pendingDecisionConnection = {
|
|
4719
|
+
decisionNodeId: sourceNodeInfo.node.id,
|
|
4720
|
+
swimlaneIndex: sourceSwimlaneIndex,
|
|
4721
|
+
x: endX,
|
|
4722
|
+
y: endY,
|
|
4723
|
+
};
|
|
4724
|
+
// Calculate popup position - position it near the decision node
|
|
4725
|
+
const swimlaneOffsetY = sourceSwimlaneIndex * 263 + 40;
|
|
4726
|
+
// Center the popup over the connection endpoint
|
|
4727
|
+
this.decisionConditionPopupX = endX;
|
|
4728
|
+
this.decisionConditionPopupY = endY - 150; // Position above the endpoint
|
|
4729
|
+
// Set decision node ID
|
|
4730
|
+
this.activeDecisionNodeId = sourceNodeInfo.node.id;
|
|
4731
|
+
this.showDecisionConditionDialog = true;
|
|
4732
|
+
// End the connection drag
|
|
4733
|
+
this.hideConnectionPopup();
|
|
4734
|
+
return;
|
|
4735
|
+
}
|
|
3797
4736
|
// Calculate swimlane-relative position
|
|
3798
4737
|
const swimlaneOffsetY = sourceSwimlaneIndex * 263 + 40;
|
|
3799
4738
|
const relativeY = endY - swimlaneOffsetY;
|
|
3800
4739
|
// Handle preconditions based on node type
|
|
3801
4740
|
if (nodeType === 'stage') {
|
|
3802
|
-
//
|
|
3803
|
-
this.
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
4741
|
+
// Create the stage directly with default properties
|
|
4742
|
+
const newNode = this.state.addNode(sourceSwimlaneIndex, 'stage', endX, relativeY, { Name: 'New Stage' } // Add default properties
|
|
4743
|
+
);
|
|
4744
|
+
if (newNode &&
|
|
4745
|
+
newNode.connectionPoints &&
|
|
4746
|
+
newNode.connectionPoints.length > 0 &&
|
|
4747
|
+
sourcePoint) {
|
|
4748
|
+
// Handle connection creation
|
|
4749
|
+
const opposingType = {
|
|
4750
|
+
right: 'left',
|
|
4751
|
+
left: 'right',
|
|
4752
|
+
top: 'bottom',
|
|
4753
|
+
bottom: 'top',
|
|
4754
|
+
}[sourcePoint.type];
|
|
4755
|
+
const targetPoint = newNode.connectionPoints.find((p) => p.type === opposingType);
|
|
4756
|
+
if (targetPoint) {
|
|
4757
|
+
// Create the connection
|
|
4758
|
+
const connection = this.state.createConnection(newNode.id, targetPoint.id, sourceSwimlaneIndex);
|
|
4759
|
+
if (connection) {
|
|
4760
|
+
this.onConnectionCreated(connection);
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
4763
|
+
}
|
|
3813
4764
|
this.hideConnectionPopup();
|
|
3814
4765
|
return;
|
|
3815
4766
|
}
|
|
@@ -3826,7 +4777,6 @@ class DesignerCanvasComponent {
|
|
|
3826
4777
|
newNode.connectionPoints &&
|
|
3827
4778
|
newNode.connectionPoints.length > 0) {
|
|
3828
4779
|
// Find the most appropriate connection point (closest to the source)
|
|
3829
|
-
const sourcePoint = this.state.draggingConnectionData.sourcePoint;
|
|
3830
4780
|
if (sourcePoint) {
|
|
3831
4781
|
// For simplicity, use the first connection point of opposing type
|
|
3832
4782
|
// In a real implementation, you'd find the closest or most appropriate point
|
|
@@ -3841,12 +4791,75 @@ class DesignerCanvasComponent {
|
|
|
3841
4791
|
targetPoint = newNode.connectionPoints.find((p) => p.type === opposingType);
|
|
3842
4792
|
if (targetPoint) {
|
|
3843
4793
|
// Create the connection
|
|
3844
|
-
this.state.createConnection(newNode.id, targetPoint.id, sourceSwimlaneIndex);
|
|
4794
|
+
const connection = this.state.createConnection(newNode.id, targetPoint.id, sourceSwimlaneIndex);
|
|
4795
|
+
if (connection) {
|
|
4796
|
+
this.onConnectionCreated(connection);
|
|
4797
|
+
}
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
}
|
|
4801
|
+
if (sourceNodeInfo) {
|
|
4802
|
+
this.updateNodeConnections(sourceNodeInfo.node);
|
|
4803
|
+
}
|
|
4804
|
+
// End the connection drag
|
|
4805
|
+
this.hideConnectionPopup();
|
|
4806
|
+
}
|
|
4807
|
+
// Method to handle saving the condition
|
|
4808
|
+
onDecisionConditionSaved(event) {
|
|
4809
|
+
if (!this.pendingDecisionConnection) {
|
|
4810
|
+
this.showDecisionConditionDialog = false;
|
|
4811
|
+
return;
|
|
4812
|
+
}
|
|
4813
|
+
const { decisionNodeId, swimlaneIndex, x, y } = this.pendingDecisionConnection;
|
|
4814
|
+
// Create the stage directly with default properties
|
|
4815
|
+
const node = this.state.addNode(swimlaneIndex, 'stage', x, y, { Name: 'New Stage' } // Add default properties
|
|
4816
|
+
);
|
|
4817
|
+
if (node) {
|
|
4818
|
+
const decisionNodeInfo = this.state.findNodeById(decisionNodeId);
|
|
4819
|
+
if (decisionNodeInfo &&
|
|
4820
|
+
node.connectionPoints &&
|
|
4821
|
+
node.connectionPoints.length > 0) {
|
|
4822
|
+
// Find a suitable connection point from the decision node
|
|
4823
|
+
const decisionNode = decisionNodeInfo.node;
|
|
4824
|
+
const decisionNodePoints = decisionNode.connectionPoints || [];
|
|
4825
|
+
const sourcePoint = decisionNodePoints.find((p) => p.type === 'right' || p.type === 'bottom');
|
|
4826
|
+
if (sourcePoint) {
|
|
4827
|
+
// Find opposing type connection point on the new stage
|
|
4828
|
+
const opposingType = {
|
|
4829
|
+
right: 'left',
|
|
4830
|
+
bottom: 'top',
|
|
4831
|
+
left: 'right',
|
|
4832
|
+
top: 'bottom',
|
|
4833
|
+
}[sourcePoint.type];
|
|
4834
|
+
const targetPoint = node.connectionPoints.find((p) => p.type === opposingType);
|
|
4835
|
+
if (targetPoint) {
|
|
4836
|
+
// Create connection with condition
|
|
4837
|
+
const connection = {
|
|
4838
|
+
id: `conn-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
|
|
4839
|
+
sourceNodeId: decisionNode.id,
|
|
4840
|
+
targetNodeId: node.id,
|
|
4841
|
+
sourcePointId: sourcePoint.id,
|
|
4842
|
+
targetPointId: targetPoint.id,
|
|
4843
|
+
sourceSwimlaneIndex: decisionNodeInfo.swimlaneIndex,
|
|
4844
|
+
targetSwimlaneIndex: swimlaneIndex,
|
|
4845
|
+
condition: event.condition,
|
|
4846
|
+
};
|
|
4847
|
+
// Add connection to the state
|
|
4848
|
+
this.state.connections.push(connection);
|
|
4849
|
+
console.log('Created decision connection with condition:', connection);
|
|
4850
|
+
}
|
|
3845
4851
|
}
|
|
3846
4852
|
}
|
|
3847
4853
|
}
|
|
3848
|
-
//
|
|
3849
|
-
this.
|
|
4854
|
+
// Clear pending data
|
|
4855
|
+
this.pendingDecisionConnection = null;
|
|
4856
|
+
this.pendingDecisionCondition = '';
|
|
4857
|
+
// Hide the condition popup
|
|
4858
|
+
this.showDecisionConditionDialog = false;
|
|
4859
|
+
}
|
|
4860
|
+
onDecisionConditionCancelled() {
|
|
4861
|
+
this.showDecisionConditionDialog = false;
|
|
4862
|
+
// Don't create the stage or connection
|
|
3850
4863
|
}
|
|
3851
4864
|
getConnectionPath() {
|
|
3852
4865
|
const pathData = this.state.getConnectionPathData();
|
|
@@ -3884,410 +4897,1107 @@ class DesignerCanvasComponent {
|
|
|
3884
4897
|
return '';
|
|
3885
4898
|
}
|
|
3886
4899
|
}
|
|
3887
|
-
getOrthogonalPath(startX, startY, endX, endY, pointType) {
|
|
3888
|
-
// Determine initial direction based on connection point type
|
|
3889
|
-
let path = '';
|
|
3890
|
-
// For side points (left/right), start with horizontal line
|
|
3891
|
-
if (pointType === 'left' || pointType === 'right') {
|
|
3892
|
-
// Calculate horizontal distance
|
|
3893
|
-
const horizontalDist = endX - startX;
|
|
3894
|
-
// If the end point is very close horizontally, use a simple 3-segment path
|
|
3895
|
-
if (Math.abs(horizontalDist) < 30) {
|
|
3896
|
-
const midY = (startY + endY) / 2;
|
|
3897
|
-
path = `M ${startX} ${startY} H ${endX} V ${midY} V ${endY}`;
|
|
3898
|
-
}
|
|
3899
|
-
// Otherwise, create a path with horizontal segment first, then vertical, then horizontal
|
|
3900
|
-
else {
|
|
3901
|
-
path = `M ${startX} ${startY} H ${startX + horizontalDist / 2} V ${endY} H ${endX}`;
|
|
3902
|
-
}
|
|
4900
|
+
getOrthogonalPath(startX, startY, endX, endY, pointType) {
|
|
4901
|
+
// Determine initial direction based on connection point type
|
|
4902
|
+
let path = '';
|
|
4903
|
+
// For side points (left/right), start with horizontal line
|
|
4904
|
+
if (pointType === 'left' || pointType === 'right') {
|
|
4905
|
+
// Calculate horizontal distance
|
|
4906
|
+
const horizontalDist = endX - startX;
|
|
4907
|
+
// If the end point is very close horizontally, use a simple 3-segment path
|
|
4908
|
+
if (Math.abs(horizontalDist) < 30) {
|
|
4909
|
+
const midY = (startY + endY) / 2;
|
|
4910
|
+
path = `M ${startX} ${startY} H ${endX} V ${midY} V ${endY}`;
|
|
4911
|
+
}
|
|
4912
|
+
// Otherwise, create a path with horizontal segment first, then vertical, then horizontal
|
|
4913
|
+
else {
|
|
4914
|
+
path = `M ${startX} ${startY} H ${startX + horizontalDist / 2} V ${endY} H ${endX}`;
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4917
|
+
// For top/bottom points, start with vertical line
|
|
4918
|
+
else if (pointType === 'top' || pointType === 'bottom') {
|
|
4919
|
+
// Calculate vertical distance
|
|
4920
|
+
const verticalDist = endY - startY;
|
|
4921
|
+
// If the end point is very close vertically, use a simple 3-segment path
|
|
4922
|
+
if (Math.abs(verticalDist) < 30) {
|
|
4923
|
+
const midX = (startX + endX) / 2;
|
|
4924
|
+
path = `M ${startX} ${startY} V ${endY} H ${midX} H ${endX}`;
|
|
4925
|
+
}
|
|
4926
|
+
// Otherwise, create a path with vertical segment first, then horizontal, then vertical
|
|
4927
|
+
else {
|
|
4928
|
+
path = `M ${startX} ${startY} V ${startY + verticalDist / 2} H ${endX} V ${endY}`;
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
4931
|
+
return path;
|
|
4932
|
+
}
|
|
4933
|
+
onStagePropertiesUpdated(event) {
|
|
4934
|
+
// Find the node in the swimlanes
|
|
4935
|
+
for (let i = 0; i < this.state.swimlanes.length; i++) {
|
|
4936
|
+
const swimlane = this.state.swimlanes[i];
|
|
4937
|
+
const nodeIndex = swimlane.nodes?.findIndex((n) => n.id === event.nodeId);
|
|
4938
|
+
if (nodeIndex !== undefined && nodeIndex >= 0) {
|
|
4939
|
+
// Update the stage data
|
|
4940
|
+
if (swimlane.nodes) {
|
|
4941
|
+
swimlane.nodes[nodeIndex].stageData = {
|
|
4942
|
+
...swimlane.nodes[nodeIndex].stageData,
|
|
4943
|
+
...event.stageData,
|
|
4944
|
+
};
|
|
4945
|
+
console.log('Updated stage data in swimlane:', i, 'node:', nodeIndex, 'data:', swimlane.nodes[nodeIndex].stageData);
|
|
4946
|
+
}
|
|
4947
|
+
break;
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4951
|
+
onConnectionCreated(connection) {
|
|
4952
|
+
// After a connection is created, refresh the source node to update its state
|
|
4953
|
+
const sourceNodeInfo = this.state.findNodeById(connection.sourceNodeId);
|
|
4954
|
+
if (sourceNodeInfo && sourceNodeInfo.node.type === 'stage') {
|
|
4955
|
+
// Refresh node state
|
|
4956
|
+
this.updateNodeConnections(sourceNodeInfo.node);
|
|
4957
|
+
}
|
|
4958
|
+
}
|
|
4959
|
+
// Helper method to update node's connection status
|
|
4960
|
+
updateNodeConnections(node) {
|
|
4961
|
+
// Count the number of outgoing connections that connect to stages
|
|
4962
|
+
const outgoingConnections = this.state.connections.filter((conn) => conn.sourceNodeId === node.id);
|
|
4963
|
+
// Check if the target nodes are stages
|
|
4964
|
+
const connectedStageNodes = outgoingConnections
|
|
4965
|
+
.map((conn) => this.state.findNodeById(conn.targetNodeId))
|
|
4966
|
+
.filter((nodeInfo) => nodeInfo && nodeInfo.node.type === 'stage');
|
|
4967
|
+
node.hasMultipleConnectedStages = connectedStageNodes.length > 1;
|
|
4968
|
+
}
|
|
4969
|
+
onEditSwimlane(event, swimlane, swimlaneIndex) {
|
|
4970
|
+
event.stopPropagation(); // Prevent the canvas click event from firing
|
|
4971
|
+
event.preventDefault();
|
|
4972
|
+
console.log('Edit swimlane button clicked for swimlane:', swimlaneIndex);
|
|
4973
|
+
// Emit a special event for editing a swimlane
|
|
4974
|
+
this.clickedPosition.emit({
|
|
4975
|
+
x: -1, // Special value to indicate edit mode
|
|
4976
|
+
y: swimlaneIndex,
|
|
4977
|
+
});
|
|
4978
|
+
}
|
|
4979
|
+
// showStartNodeFormPopup(event: MouseEvent): void {
|
|
4980
|
+
// event.preventDefault();
|
|
4981
|
+
// event.stopPropagation();
|
|
4982
|
+
// // Implementation will be similar to the stage form popup
|
|
4983
|
+
// // We'll need to add UI elements for this
|
|
4984
|
+
// console.log('Start node form popup requested');
|
|
4985
|
+
// // For now, this is a placeholder
|
|
4986
|
+
// // The actual implementation would load forms and add them to the workflow
|
|
4987
|
+
// }
|
|
4988
|
+
toggleStartNodeFormPopup(event, nodeX, nodeY, swimlaneIndex) {
|
|
4989
|
+
event.preventDefault();
|
|
4990
|
+
event.stopPropagation();
|
|
4991
|
+
// Set popup position
|
|
4992
|
+
const swimlaneOffset = swimlaneIndex * 263 + 40;
|
|
4993
|
+
this.startNodeFormPopupX = nodeX - 30; // Position near the start node
|
|
4994
|
+
this.startNodeFormPopupY = nodeY + swimlaneOffset;
|
|
4995
|
+
// If we're opening the popup, load forms
|
|
4996
|
+
if (!this.showStartNodeFormPopup) {
|
|
4997
|
+
this.isLoadingStartNodeForms = true;
|
|
4998
|
+
this.dataService
|
|
4999
|
+
.getForms()
|
|
5000
|
+
.then((response) => {
|
|
5001
|
+
this.startNodeFormsList = response.Result;
|
|
5002
|
+
this.isLoadingStartNodeForms = false;
|
|
5003
|
+
})
|
|
5004
|
+
.catch((error) => {
|
|
5005
|
+
console.error('Error loading forms:', error);
|
|
5006
|
+
this.isLoadingStartNodeForms = false;
|
|
5007
|
+
});
|
|
5008
|
+
}
|
|
5009
|
+
this.showStartNodeFormPopup = !this.showStartNodeFormPopup;
|
|
5010
|
+
}
|
|
5011
|
+
selectStartNodeForm(form) {
|
|
5012
|
+
if (form) {
|
|
5013
|
+
this.state.setWorkflowForm(form.Id, form.Name);
|
|
5014
|
+
}
|
|
5015
|
+
else {
|
|
5016
|
+
this.state.setWorkflowForm(null, null);
|
|
5017
|
+
}
|
|
5018
|
+
// Close the popup
|
|
5019
|
+
this.showStartNodeFormPopup = false;
|
|
5020
|
+
}
|
|
5021
|
+
showSubflowSelectionPopup(x, y, swimlaneIndex) {
|
|
5022
|
+
this.subflowPopupX = x;
|
|
5023
|
+
this.subflowPopupY = y;
|
|
5024
|
+
this.pendingSubflowPosition = { swimlaneIndex, x, y };
|
|
5025
|
+
// Store connection data if we're creating from a connection
|
|
5026
|
+
if (this.state.isConnectionDragging()) {
|
|
5027
|
+
// Store the source connection data
|
|
5028
|
+
this.pendingConnectionSourcePoint =
|
|
5029
|
+
this.state.draggingConnectionData.sourcePoint ?? null;
|
|
5030
|
+
this.pendingConnectionSourceSwimlaneIndex =
|
|
5031
|
+
this.state.draggingConnectionData.sourceSwimlaneIndex ?? null;
|
|
5032
|
+
console.log('Stored connection data for subflow:', {
|
|
5033
|
+
sourcePoint: this.pendingConnectionSourcePoint,
|
|
5034
|
+
sourceSwimlaneIndex: this.pendingConnectionSourceSwimlaneIndex,
|
|
5035
|
+
});
|
|
5036
|
+
}
|
|
5037
|
+
// Load workflows
|
|
5038
|
+
this.isLoadingWorkflows = true;
|
|
5039
|
+
this.dataService
|
|
5040
|
+
.getWorkflows()
|
|
5041
|
+
.then((response) => {
|
|
5042
|
+
this.workflowsList = response.Result;
|
|
5043
|
+
this.isLoadingWorkflows = false;
|
|
5044
|
+
})
|
|
5045
|
+
.catch((error) => {
|
|
5046
|
+
console.error('Error loading workflows:', error);
|
|
5047
|
+
this.isLoadingWorkflows = false;
|
|
5048
|
+
});
|
|
5049
|
+
this.showSubflowPopup = true;
|
|
5050
|
+
}
|
|
5051
|
+
selectSubflowWorkflow(workflow) {
|
|
5052
|
+
console.log('Selecting workflow for subflow:', workflow.Name);
|
|
5053
|
+
console.log('Pending connection data:', {
|
|
5054
|
+
sourcePoint: this.pendingConnectionSourcePoint,
|
|
5055
|
+
sourceSwimlaneIndex: this.pendingConnectionSourceSwimlaneIndex,
|
|
5056
|
+
position: this.pendingSubflowPosition,
|
|
5057
|
+
});
|
|
5058
|
+
if (this.pendingSubflowPosition) {
|
|
5059
|
+
const { swimlaneIndex, x, y } = this.pendingSubflowPosition;
|
|
5060
|
+
// Create the subflow node with the selected workflow data
|
|
5061
|
+
const node = this.state.addNode(swimlaneIndex, 'subflow', x, y, undefined, // No stage data
|
|
5062
|
+
{ id: workflow.Id, name: workflow.Name });
|
|
5063
|
+
console.log('Created subflow node:', node);
|
|
5064
|
+
// If this was created from a connection, create the connection
|
|
5065
|
+
if (node && this.pendingConnectionSourcePoint) {
|
|
5066
|
+
console.log('Creating connection to new subflow node');
|
|
5067
|
+
// Find a suitable connection point on the new node
|
|
5068
|
+
if (node.connectionPoints && node.connectionPoints.length > 0) {
|
|
5069
|
+
const sourcePoint = this.pendingConnectionSourcePoint;
|
|
5070
|
+
// Find opposing type connection point
|
|
5071
|
+
const opposingType = {
|
|
5072
|
+
right: 'left',
|
|
5073
|
+
left: 'right',
|
|
5074
|
+
top: 'bottom',
|
|
5075
|
+
bottom: 'top',
|
|
5076
|
+
}[sourcePoint.type];
|
|
5077
|
+
const targetPoint = node.connectionPoints.find((p) => p.type === opposingType);
|
|
5078
|
+
if (targetPoint) {
|
|
5079
|
+
// Create connection
|
|
5080
|
+
const connection = {
|
|
5081
|
+
id: `conn-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
|
|
5082
|
+
sourceNodeId: sourcePoint.nodeId,
|
|
5083
|
+
targetNodeId: node.id,
|
|
5084
|
+
sourcePointId: sourcePoint.id,
|
|
5085
|
+
targetPointId: targetPoint.id,
|
|
5086
|
+
sourceSwimlaneIndex: this.pendingConnectionSourceSwimlaneIndex,
|
|
5087
|
+
targetSwimlaneIndex: swimlaneIndex,
|
|
5088
|
+
};
|
|
5089
|
+
// Add connection directly to the state's connections array
|
|
5090
|
+
this.state.connections.push(connection);
|
|
5091
|
+
console.log('Added connection to state:', connection);
|
|
5092
|
+
console.log('Connections after adding:', this.state.connections);
|
|
5093
|
+
this.onConnectionCreated(connection);
|
|
5094
|
+
}
|
|
5095
|
+
}
|
|
5096
|
+
this.state.endConnectionDrag();
|
|
5097
|
+
}
|
|
5098
|
+
// Reset selection tool in parent component
|
|
5099
|
+
this.subflowSelected.emit();
|
|
5100
|
+
}
|
|
5101
|
+
// Clean up
|
|
5102
|
+
this.showSubflowPopup = false;
|
|
5103
|
+
this.pendingSubflowPosition = null;
|
|
5104
|
+
this.pendingConnectionSourcePoint = null;
|
|
5105
|
+
this.pendingConnectionSourceSwimlaneIndex = null;
|
|
5106
|
+
}
|
|
5107
|
+
showDecisionConditionsPopup(event, node, swimlaneIndex) {
|
|
5108
|
+
event.preventDefault();
|
|
5109
|
+
event.stopPropagation();
|
|
5110
|
+
// Get the position for the popup
|
|
5111
|
+
const position = this.getMousePosition(event);
|
|
5112
|
+
const globalX = position.x;
|
|
5113
|
+
const globalY = position.y;
|
|
5114
|
+
// Find all connections from this decision node
|
|
5115
|
+
const connections = this.state.connections.filter((conn) => conn.sourceNodeId === node.id);
|
|
5116
|
+
if (connections.length === 0) {
|
|
5117
|
+
// No connections to show
|
|
5118
|
+
return;
|
|
5119
|
+
}
|
|
5120
|
+
// Show the decision conditions popup
|
|
5121
|
+
this.activeDecisionNodeId = node.id;
|
|
5122
|
+
this.decisionConditionPopupX = globalX;
|
|
5123
|
+
this.decisionConditionPopupY = globalY;
|
|
5124
|
+
// Set the correct popup visibility flag - this is likely the issue
|
|
5125
|
+
this.showDecisionConditionDialog = true;
|
|
5126
|
+
console.log('Showing decision popup:', {
|
|
5127
|
+
x: globalX,
|
|
5128
|
+
y: globalY,
|
|
5129
|
+
nodeId: node.id,
|
|
5130
|
+
isVisible: this.showDecisionConditionDialog,
|
|
5131
|
+
});
|
|
5132
|
+
}
|
|
5133
|
+
ensureSwimlaneInView(swimlaneIndex) {
|
|
5134
|
+
// Ensure we have enough swimlanes visible
|
|
5135
|
+
const requiredHeight = (swimlaneIndex + 1) * this.swimlaneHeight;
|
|
5136
|
+
if (this.canvasHeight < requiredHeight) {
|
|
5137
|
+
this.canvasHeight = requiredHeight;
|
|
5138
|
+
}
|
|
5139
|
+
}
|
|
5140
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DesignerCanvasComponent, deps: [{ token: WorkflowDesignerState }, { token: WorkflowDataService }], target: i0.ɵɵFactoryTarget.Component });
|
|
5141
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: DesignerCanvasComponent, selector: "lib-designer-canvas", inputs: { selectedTool: "selectedTool" }, outputs: { clickedPosition: "clickedPosition", subflowSelected: "subflowSelected", showStageDialog: "showStageDialog", stagePropertiesUpdated: "stagePropertiesUpdated" }, host: { listeners: { "window:mouseup": "onWindowMouseUp($event)", "document:click": "onDocumentClick($event)" } }, viewQueries: [{ propertyName: "canvasRef", first: true, predicate: ["canvas"], descendants: true, static: true }], ngImport: i0, template: "<div class=\"canvas-container\">\n <svg\n #canvas\n [attr.width]=\"canvasWidth\"\n [attr.height]=\"canvasHeight\"\n class=\"designer-canvas\"\n (click)=\"onCanvasClick($event)\"\n >\n <defs>\n <!-- Grid pattern definition -->\n\n <pattern\n id=\"grid\"\n [attr.width]=\"gridSize\"\n [attr.height]=\"gridSize\"\n patternUnits=\"userSpaceOnUse\"\n >\n <path\n d=\"M 20 0 L 0 0 0 20\"\n fill=\"none\"\n stroke=\"#e2e8f0\"\n stroke-width=\"0.5\"\n />\n </pattern>\n\n <!-- Arrow head marker definition -->\n <marker\n id=\"arrowhead\"\n markerWidth=\"10\"\n markerHeight=\"7\"\n refX=\"9\"\n refY=\"3.5\"\n orient=\"auto\"\n >\n <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"#D36CFF\" />\n </marker>\n\n <!-- Connection point styles -->\n <circle\n id=\"connection-point-template\"\n r=\"5\"\n fill=\"#D36CFF\"\n stroke=\"#FFFFFF\"\n stroke-width=\"1\"\n />\n\n <!-- Dashed line style for connection preview -->\n <pattern\n id=\"dashed-line\"\n width=\"10\"\n height=\"10\"\n patternUnits=\"userSpaceOnUse\"\n >\n <line\n x1=\"0\"\n y1=\"5\"\n x2=\"10\"\n y2=\"5\"\n stroke=\"#D36CFF\"\n stroke-width=\"2\"\n stroke-dasharray=\"5,5\"\n />\n </pattern>\n\n @for (swimlane of state.swimlanes; track swimlane.order) {\n <clipPath [attr.id]=\"'swimlane-clip-' + swimlane.order\">\n <rect x=\"0\" y=\"0\" [attr.width]=\"canvasWidth\" height=\"263\" />\n </clipPath>\n }\n </defs>\n\n <!-- Background grid -->\n <rect width=\"100%\" height=\"100%\" fill=\"url(#grid)\" />\n\n <!-- Display a message when no swimlanes exist -->\n @if (state.swimlanes.length === 0) {\n <text\n x=\"50%\"\n y=\"50%\"\n font-family=\"sans-serif\"\n font-size=\"16\"\n fill=\"#94a3b8\"\n text-anchor=\"middle\"\n >\n Select the Swimlane tool and click on the canvas to add a swimlane\n </text>\n }\n\n <!-- This is where workflow elements will be added later -->\n @for (swimlane of state.swimlanes; track swimlane.order) {\n <g\n [attr.transform]=\"'translate(0,' + swimlane.order * 263 + ')'\"\n [attr.clip-path]=\"'url(#swimlane-clip-' + swimlane.order + ')'\"\n >\n <!-- Swimlane container -->\n <rect\n x=\"0\"\n y=\"0\"\n [attr.width]=\"canvasWidth\"\n [attr.height]=\"263\"\n fill=\"#ffffff\"\n stroke=\"#e2e8f0\"\n stroke-width=\"1\"\n ></rect>\n\n <!-- Swimlane header -->\n <rect\n x=\"0\"\n y=\"0\"\n [attr.width]=\"canvasWidth\"\n height=\"40\"\n fill=\"#f8fafc\"\n stroke=\"#e2e8f0\"\n stroke-width=\"1\"\n ></rect>\n\n <!-- Swimlane label -->\n <text\n x=\"20\"\n y=\"25\"\n font-family=\"sans-serif\"\n font-size=\"14\"\n fill=\"#333333\"\n font-weight=\"bold\"\n >\n {{ swimlane.label }}\n </text>\n\n <!-- Edit button -->\n <g\n class=\"edit-swimlane-button\"\n [attr.transform]=\"'translate(100, 20)'\"\n (click)=\"\n onEditSwimlane($event, swimlane, swimlane.order);\n $event.stopPropagation()\n \"\n >\n <rect\n x=\"-5\"\n y=\"-15\"\n width=\"40\"\n height=\"20\"\n fill=\"#f3e8ff\"\n rx=\"3\"\n ry=\"3\"\n stroke=\"#d8b4fe\"\n stroke-width=\"1\"\n ></rect>\n <text\n x=\"15\"\n y=\"0\"\n font-family=\"sans-serif\"\n font-size=\"12\"\n fill=\"#7e22ce\"\n text-anchor=\"middle\"\n >\n Edit\n </text>\n </g>\n\n <!-- Tag indicators -->\n <g [attr.transform]=\"'translate(200, 20)'\">\n @for (tag of swimlane.tags.slice(0, 3); track tag.Name; let i = $index)\n {\n <text\n [attr.x]=\"i * 100\"\n y=\"5\"\n font-family=\"sans-serif\"\n font-size=\"12\"\n fill=\"#666666\"\n >\n {{ tag.Name }}\n </text>\n } @if (swimlane.tags.length > 3) {\n <text\n [attr.x]=\"3 * 100 + 10\"\n y=\"5\"\n font-family=\"sans-serif\"\n font-size=\"12\"\n fill=\"#666666\"\n >\n +{{ swimlane.tags.length - 3 }} more\n </text>\n }\n </g>\n </g>\n }\n\n <!-- Nodes -->\n @for (swimlane of state.swimlanes; track swimlane.order) {\n <!-- Render nodes in this swimlane -->\n @for (node of swimlane.nodes; track node.id) {\n <g\n class=\"node-element\"\n [attr.transform]=\"'translate(' + node.x + ',' + (node.y + 40) + ')'\"\n (mouseenter)=\"showConnectionPoints(node.id, swimlane.order)\"\n (mouseleave)=\"hideConnectionPoints(node.id)\"\n >\n <!-- Start node indicator (circle and arrow) for the first node -->\n @if (node.isStartNode) {\n <!-- Circle -->\n <circle\n cx=\"-30\"\n cy=\"50\"\n r=\"15\"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n ></circle>\n <!-- Form icon for start node -->\n <svg:g\n (click)=\"\n toggleStartNodeFormPopup($event, node.x, node.y, swimlane.order)\n \"\n class=\"stage-icon\"\n transform=\"translate(-45, 42)\"\n style=\"cursor: pointer; pointer-events: all\"\n >\n <!-- Transparent rectangle to capture clicks -->\n <svg:rect\n x=\"-2\"\n y=\"-2\"\n width=\"24\"\n height=\"24\"\n fill=\"transparent\"\n style=\"cursor: pointer\"\n />\n <svg:path\n d=\"M16.5 20.475V17.475H13.5V16.475H16.5V13.475H17.5V16.475H20.5V17.475H17.5V20.475H16.5ZM3.5 17.5V16.5H4.5V17.5H3.5ZM6.5 17.5V16.5H11.517C11.5057 16.6767 11.5043 16.845 11.513 17.005C11.521 17.165 11.531 17.33 11.543 17.5H6.5ZM3.5 13.5V12.5H4.5V13.5H3.5ZM6.5 13.5V12.5H13.804C13.6127 12.6387 13.4333 12.7913 13.266 12.958C13.0993 13.1247 12.9377 13.3053 12.781 13.5H6.5ZM3.5 9.5V8.5H4.5V9.5H3.5ZM6.5 9.5V8.5H18.5V9.5H6.5ZM3.5 5.5V4.5H4.5V5.5H3.5ZM6.5 5.5V4.5H18.5V5.5H6.5Z\"\n [attr.fill]=\"state.workflowFormId ? '#D36CFF' : 'black'\"\n transform=\"scale(0.7)\"\n />\n </svg:g>\n <!-- Arrow from circle to node -->\n <path\n d=\"M -20 50 L 0 50\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n marker-end=\"url(#arrowhead)\"\n ></path>\n }\n\n <!-- Exit point indicator (circle and arrow) -->\n @if (node.stageData?.IsExitPoint) {\n <!-- Arrow from node to circle -->\n <path\n [attr.d]=\"'M ' + node.width + ' 50 L ' + (node.width + 20) + ' 50'\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n marker-end=\"url(#arrowhead)\"\n ></path>\n <!-- Circle -->\n <circle\n [attr.cx]=\"node.width + 50\"\n cy=\"50\"\n r=\"30\"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-width=\"2\"\n ></circle>\n }\n\n <!-- Stage node -->\n @if (node.type === 'stage') {\n <!-- <rect\n x=\"0\"\n y=\"0\"\n [attr.width]=\"node.width\"\n [attr.height]=\"node.height\"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n ></rect> -->\n <svg:g\n lib-stage-node\n [node]=\"node\"\n [isStartNode]=\"node.isStartNode\"\n (stagePropertiesUpdated)=\"onStagePropertiesUpdated($event)\"\n (showShieldDialog)=\"onShowShieldDialog($event)\"\n ></svg:g>\n }\n\n <!-- Decision node -->\n @if (node.type === 'decision') {\n <path\n [attr.d]=\"\n 'M 0 ' +\n node.height / 2 +\n ' L ' +\n node.width / 2 +\n ' 0' +\n ' L ' +\n node.width +\n ' ' +\n node.height / 2 +\n ' L ' +\n node.width / 2 +\n ' ' +\n node.height +\n ' Z'\n \"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n ></path>\n\n <!-- Decision node condition indicator icon -->\n <svg:g\n (click)=\"showDecisionConditionsPopup($event, node, swimlane.order)\"\n class=\"decision-condition-icon\"\n [attr.transform]=\"\n 'translate(' +\n (node.width / 2 - 10) +\n ',' +\n (node.height / 2 - 10) +\n ')'\n \"\n >\n <svg:rect\n x=\"-5\"\n y=\"-5\"\n width=\"30\"\n height=\"30\"\n fill=\"transparent\"\n style=\"cursor: pointer\"\n />\n <svg:path\n d=\"M10 0H20V2H10V0ZM10 8H20V10H10V8ZM10 4H20V6H10V4ZM0 0H8V2H0V0ZM0 8H8V10H0V8ZM0 4H8V6H0V4Z\"\n fill=\"#D36CFF\"\n />\n </svg:g>\n }\n\n <!-- Form node -->\n @if (node.type === 'form') {\n <path\n d=\"M95.0625 50.5591V95.0625C95.0625 97.9716 93.9069 100.762 91.8498 102.819C89.7928 104.876 87.0028 106.031 84.0938 106.031H32.9062C29.9972 106.031 27.2072 104.876 25.1502 102.819C23.0931 100.762 21.9375 97.9716 21.9375 95.0625V21.9375C21.9375 19.0284 23.0931 16.2385 25.1502 14.1814C27.2072 12.1244 29.9972 10.9688 32.9062 10.9688H55.4722C57.4109 10.969 59.2701 11.7392 60.6412 13.1099L92.9213 45.3901C94.292 46.7611 95.0622 48.6204 95.0625 50.5591Z\"\n transform=\"scale(0.7)\"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-linejoin=\"round\"\n />\n <path\n d=\"M58.5 12.7969V40.2188C58.5 42.1581 59.2704 44.0181 60.6418 45.3895C62.0131 46.7608 63.8731 47.5312 65.8125 47.5312H93.2344\"\n transform=\"scale(0.7)\"\n stroke=\"#D36CFF\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n <text\n x=\"50%\"\n y=\"50%\"\n text-anchor=\"middle\"\n dominant-baseline=\"middle\"\n font-family=\"sans-serif\"\n font-size=\"14\"\n fill=\"#D36CFF\"\n >\n Form\n </text>\n }\n\n <!-- Subflow node -->\n @if (node.type === 'subflow') {\n <path\n [attr.d]=\"\n 'M 1 ' +\n node.height / 4 +\n ' L ' +\n node.width / 2 +\n ' 1 L ' +\n (node.width - 1) +\n ' ' +\n node.height / 4 +\n ' V ' +\n (node.height * 3) / 4 +\n ' L ' +\n node.width / 2 +\n ' ' +\n (node.height - 1) +\n ' L 1 ' +\n (node.height * 3) / 4 +\n ' Z'\n \"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n ></path>\n <text\n [attr.x]=\"node.width / 2\"\n [attr.y]=\"node.height / 2\"\n text-anchor=\"middle\"\n alignment-baseline=\"middle\"\n font-family=\"sans-serif\"\n font-size=\"10\"\n fill=\"#000000\"\n >\n <!-- Limit text length -->\n {{\n (node.workflowData?.name || \"Subflow\").length > 12\n ? (node.workflowData?.name || \"Subflow\").substring(0, 10) + \"...\"\n : node.workflowData?.name || \"Subflow\"\n }}\n </text>\n }\n\n <!-- Connection points for this node -->\n @for (point of node.connectionPoints || []; track point.id) { @if\n (isConnectionPointVisible(node.id)) {\n <use\n [attr.href]=\"'#connection-point-template'\"\n [attr.x]=\"point.x\"\n [attr.y]=\"point.y\"\n [attr.id]=\"point.id\"\n (mousedown)=\"startConnectionDrag($event, point, swimlane.order)\"\n />\n } }\n </g>\n <!-- A transparent hover area for improved hover detection -->\n <rect\n [attr.x]=\"node.x - 10\"\n [attr.y]=\"node.y + 40 - 10\"\n [attr.width]=\"node.width + 20\"\n [attr.height]=\"node.height + 20\"\n fill=\"transparent\"\n (mouseover)=\"showConnectionPoints(node.id, swimlane.order)\"\n (mouseout)=\"hideConnectionPoints(node.id)\"\n style=\"pointer-events: none\"\n />\n } }\n\n <!-- Connections -->\n @for (connection of state.connections; track connection.id) {\n <g class=\"connection-element\">\n <path\n [attr.d]=\"getConnectionPathForSavedConnection(connection)\"\n stroke=\"#D36CFF\"\n stroke-width=\"2\"\n fill=\"none\"\n marker-end=\"url(#arrowhead)\"\n ></path>\n </g>\n }\n\n <!-- Connection preview line -->\n @if (state.isConnectionDragging()) {\n <g>\n <path\n [attr.d]=\"getConnectionPath()\"\n stroke=\"#D36CFF\"\n stroke-width=\"2\"\n stroke-dasharray=\"5,5\"\n fill=\"none\"\n ></path>\n </g>\n }\n </svg>\n\n <div\n *ngIf=\"popupVisible\"\n [style.position]=\"'absolute'\"\n [style.left.px]=\"popupX\"\n [style.top.px]=\"popupY\"\n >\n <verben-pop-Up\n [dropdownOpen]=\"true\"\n [customStyles]=\"{ 'z-index': '99' }\"\n [enableMouseLeave]=\"false\"\n >\n <div dropdown-trigger style=\"display: none\">\n <!-- Hidden trigger element -->\n </div>\n <div\n class=\"p-3 bg-white border border-gray-200 rounded shadow-md\"\n dropdown-content\n >\n <h4 class=\"mb-2 font-medium\">Create Connection</h4>\n <div class=\"flex flex-col gap-2\">\n <ng-container *ngFor=\"let type of allowedNodeTypes\">\n <button\n class=\"px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50\"\n (click)=\"createNodeConnection(type)\"\n >\n Create {{ type | titlecase }}\n </button>\n </ng-container>\n <button\n class=\"px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50\"\n (click)=\"hideConnectionPopup()\"\n >\n Cancel\n </button>\n </div>\n </div>\n </verben-pop-Up>\n </div>\n\n <div\n *ngIf=\"showStartNodeFormPopup\"\n [style.position]=\"'absolute'\"\n [style.left.px]=\"startNodeFormPopupX\"\n [style.top.px]=\"startNodeFormPopupY\"\n >\n <verben-pop-Up [dropdownOpen]=\"true\" [customStyles]=\"{ 'z-index': '100' }\">\n <div dropdown-trigger style=\"display: none\">\n <!-- Hidden trigger element -->\n </div>\n <div\n class=\"p-3 bg-white border border-gray-200 rounded shadow-md w-64\"\n dropdown-content\n style=\"background-color: white; z-index: 1000\"\n >\n <h4 class=\"mb-2 font-medium\">Select Workflow Form</h4>\n <div *ngIf=\"isLoadingStartNodeForms\" class=\"text-center py-2\">\n Loading forms...\n </div>\n <div *ngIf=\"!isLoadingStartNodeForms\" class=\"max-h-48 overflow-y-auto\">\n <div class=\"mb-2\">\n <button\n class=\"w-full px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50 text-left\"\n (click)=\"selectStartNodeForm(null)\"\n >\n Clear form selection\n </button>\n </div>\n <div *ngFor=\"let form of startNodeFormsList\" class=\"mb-1\">\n <button\n class=\"w-full px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50 text-left\"\n (click)=\"selectStartNodeForm(form)\"\n >\n {{ form.Name }}\n </button>\n </div>\n <div\n *ngIf=\"startNodeFormsList.length === 0\"\n class=\"text-center py-2 text-gray-500\"\n >\n No forms available\n </div>\n </div>\n </div>\n </verben-pop-Up>\n </div>\n\n <div\n *ngIf=\"showSubflowPopup\"\n [style.position]=\"'absolute'\"\n [style.left.px]=\"subflowPopupX\"\n [style.top.px]=\"subflowPopupY\"\n >\n <verben-pop-Up [dropdownOpen]=\"true\" [customStyles]=\"{ 'z-index': '100' }\">\n <div dropdown-trigger style=\"display: none\">\n <!-- Hidden trigger element -->\n </div>\n <div\n class=\"p-3 bg-white border border-gray-200 rounded shadow-md w-64\"\n dropdown-content\n style=\"background-color: white; z-index: 1000\"\n >\n <h4 class=\"mb-2 font-medium\">Select Workflow</h4>\n <div *ngIf=\"isLoadingWorkflows\" class=\"text-center py-2\">\n Loading workflows...\n </div>\n <div *ngIf=\"!isLoadingWorkflows\" class=\"max-h-48 overflow-y-auto\">\n <div *ngFor=\"let workflow of workflowsList\" class=\"mb-1\">\n <button\n class=\"w-full px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50 text-left\"\n (click)=\"selectSubflowWorkflow(workflow)\"\n >\n {{ workflow.Name }}\n </button>\n </div>\n <div\n *ngIf=\"workflowsList.length === 0\"\n class=\"text-center py-2 text-gray-500\"\n >\n No workflows available\n </div>\n </div>\n </div>\n </verben-pop-Up>\n </div>\n\n <lib-conditions-popup\n [visible]=\"showDecisionConditionDialog\"\n [decisionNodeId]=\"activeDecisionNodeId\"\n [newConnectionId]=\"activeConnectionId\"\n [popupX]=\"decisionConditionPopupX\"\n [popupY]=\"decisionConditionPopupY\"\n (closed)=\"onDecisionConditionCancelled()\"\n (saved)=\"onDecisionConditionSaved($event)\"\n ></lib-conditions-popup>\n\n <!-- <lib-decision-popup\n [visible]=\"showConditionsDialog\"\n [decisionNodeId]=\"activeDecisionNodeId\"\n [popupX]=\"decisionConditionPopupX\"\n [popupY]=\"decisionConditionPopupY\"\n (closed)=\"onDecisionConditionCancelled()\"\n ></lib-decision-popup> -->\n\n <lib-stage-dialog\n [visible]=\"showStageDialogLocal\"\n [stageData]=\"activeStageData || {}\"\n (closed)=\"onStageDialogClosed()\"\n (saved)=\"onStageDialogSaved($event)\"\n ></lib-stage-dialog>\n</div>\n", styles: [".canvas-container{flex:1;overflow:auto;background-color:#fff}.designer-canvas{min-width:100%;min-height:100%;cursor:default}.designer-canvas:focus{outline:none}.edit-swimlane-button{cursor:pointer}.edit-swimlane-button:hover rect{fill:#ddd6fe}.decision-condition-icon{cursor:pointer}.decision-condition-icon:hover path{fill:#c084fc}.swimlane-label{z-index:10;cursor:default}.plus-icon:hover{fill:#d36cff}.swimlane-container{pointer-events:none}.swimlane-container>*{pointer-events:auto}.node-element{z-index:10}.connection-element{z-index:5}\n"], dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i8.VerbenPopUpComponent, selector: "verben-pop-Up", inputs: ["dropdownOpen", "dropdownWidth", "color", "customStyles", "popUpClass", "border", "borderRadius", "enableMouseLeave"], outputs: ["dropdownOpenChange", "close"] }, { kind: "component", type: StageNodeComponent, selector: "svg:g[lib-stage-node]", inputs: ["node", "isStartNode", "stageData"], outputs: ["stagePropertiesUpdated", "parallelExecutionToggled", "showShieldDialog"] }, { kind: "component", type: StageDialogComponent, selector: "lib-stage-dialog", inputs: ["visible", "stageData"], outputs: ["closed", "saved"] }, { kind: "component", type: ConditionsPopupComponent, selector: "lib-conditions-popup", inputs: ["visible", "decisionNodeId", "newConnectionId", "popupX", "popupY"], outputs: ["closed", "saved"] }, { kind: "pipe", type: i1$1.TitleCasePipe, name: "titlecase" }] });
|
|
5142
|
+
}
|
|
5143
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DesignerCanvasComponent, decorators: [{
|
|
5144
|
+
type: Component,
|
|
5145
|
+
args: [{ selector: 'lib-designer-canvas', template: "<div class=\"canvas-container\">\n <svg\n #canvas\n [attr.width]=\"canvasWidth\"\n [attr.height]=\"canvasHeight\"\n class=\"designer-canvas\"\n (click)=\"onCanvasClick($event)\"\n >\n <defs>\n <!-- Grid pattern definition -->\n\n <pattern\n id=\"grid\"\n [attr.width]=\"gridSize\"\n [attr.height]=\"gridSize\"\n patternUnits=\"userSpaceOnUse\"\n >\n <path\n d=\"M 20 0 L 0 0 0 20\"\n fill=\"none\"\n stroke=\"#e2e8f0\"\n stroke-width=\"0.5\"\n />\n </pattern>\n\n <!-- Arrow head marker definition -->\n <marker\n id=\"arrowhead\"\n markerWidth=\"10\"\n markerHeight=\"7\"\n refX=\"9\"\n refY=\"3.5\"\n orient=\"auto\"\n >\n <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"#D36CFF\" />\n </marker>\n\n <!-- Connection point styles -->\n <circle\n id=\"connection-point-template\"\n r=\"5\"\n fill=\"#D36CFF\"\n stroke=\"#FFFFFF\"\n stroke-width=\"1\"\n />\n\n <!-- Dashed line style for connection preview -->\n <pattern\n id=\"dashed-line\"\n width=\"10\"\n height=\"10\"\n patternUnits=\"userSpaceOnUse\"\n >\n <line\n x1=\"0\"\n y1=\"5\"\n x2=\"10\"\n y2=\"5\"\n stroke=\"#D36CFF\"\n stroke-width=\"2\"\n stroke-dasharray=\"5,5\"\n />\n </pattern>\n\n @for (swimlane of state.swimlanes; track swimlane.order) {\n <clipPath [attr.id]=\"'swimlane-clip-' + swimlane.order\">\n <rect x=\"0\" y=\"0\" [attr.width]=\"canvasWidth\" height=\"263\" />\n </clipPath>\n }\n </defs>\n\n <!-- Background grid -->\n <rect width=\"100%\" height=\"100%\" fill=\"url(#grid)\" />\n\n <!-- Display a message when no swimlanes exist -->\n @if (state.swimlanes.length === 0) {\n <text\n x=\"50%\"\n y=\"50%\"\n font-family=\"sans-serif\"\n font-size=\"16\"\n fill=\"#94a3b8\"\n text-anchor=\"middle\"\n >\n Select the Swimlane tool and click on the canvas to add a swimlane\n </text>\n }\n\n <!-- This is where workflow elements will be added later -->\n @for (swimlane of state.swimlanes; track swimlane.order) {\n <g\n [attr.transform]=\"'translate(0,' + swimlane.order * 263 + ')'\"\n [attr.clip-path]=\"'url(#swimlane-clip-' + swimlane.order + ')'\"\n >\n <!-- Swimlane container -->\n <rect\n x=\"0\"\n y=\"0\"\n [attr.width]=\"canvasWidth\"\n [attr.height]=\"263\"\n fill=\"#ffffff\"\n stroke=\"#e2e8f0\"\n stroke-width=\"1\"\n ></rect>\n\n <!-- Swimlane header -->\n <rect\n x=\"0\"\n y=\"0\"\n [attr.width]=\"canvasWidth\"\n height=\"40\"\n fill=\"#f8fafc\"\n stroke=\"#e2e8f0\"\n stroke-width=\"1\"\n ></rect>\n\n <!-- Swimlane label -->\n <text\n x=\"20\"\n y=\"25\"\n font-family=\"sans-serif\"\n font-size=\"14\"\n fill=\"#333333\"\n font-weight=\"bold\"\n >\n {{ swimlane.label }}\n </text>\n\n <!-- Edit button -->\n <g\n class=\"edit-swimlane-button\"\n [attr.transform]=\"'translate(100, 20)'\"\n (click)=\"\n onEditSwimlane($event, swimlane, swimlane.order);\n $event.stopPropagation()\n \"\n >\n <rect\n x=\"-5\"\n y=\"-15\"\n width=\"40\"\n height=\"20\"\n fill=\"#f3e8ff\"\n rx=\"3\"\n ry=\"3\"\n stroke=\"#d8b4fe\"\n stroke-width=\"1\"\n ></rect>\n <text\n x=\"15\"\n y=\"0\"\n font-family=\"sans-serif\"\n font-size=\"12\"\n fill=\"#7e22ce\"\n text-anchor=\"middle\"\n >\n Edit\n </text>\n </g>\n\n <!-- Tag indicators -->\n <g [attr.transform]=\"'translate(200, 20)'\">\n @for (tag of swimlane.tags.slice(0, 3); track tag.Name; let i = $index)\n {\n <text\n [attr.x]=\"i * 100\"\n y=\"5\"\n font-family=\"sans-serif\"\n font-size=\"12\"\n fill=\"#666666\"\n >\n {{ tag.Name }}\n </text>\n } @if (swimlane.tags.length > 3) {\n <text\n [attr.x]=\"3 * 100 + 10\"\n y=\"5\"\n font-family=\"sans-serif\"\n font-size=\"12\"\n fill=\"#666666\"\n >\n +{{ swimlane.tags.length - 3 }} more\n </text>\n }\n </g>\n </g>\n }\n\n <!-- Nodes -->\n @for (swimlane of state.swimlanes; track swimlane.order) {\n <!-- Render nodes in this swimlane -->\n @for (node of swimlane.nodes; track node.id) {\n <g\n class=\"node-element\"\n [attr.transform]=\"'translate(' + node.x + ',' + (node.y + 40) + ')'\"\n (mouseenter)=\"showConnectionPoints(node.id, swimlane.order)\"\n (mouseleave)=\"hideConnectionPoints(node.id)\"\n >\n <!-- Start node indicator (circle and arrow) for the first node -->\n @if (node.isStartNode) {\n <!-- Circle -->\n <circle\n cx=\"-30\"\n cy=\"50\"\n r=\"15\"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n ></circle>\n <!-- Form icon for start node -->\n <svg:g\n (click)=\"\n toggleStartNodeFormPopup($event, node.x, node.y, swimlane.order)\n \"\n class=\"stage-icon\"\n transform=\"translate(-45, 42)\"\n style=\"cursor: pointer; pointer-events: all\"\n >\n <!-- Transparent rectangle to capture clicks -->\n <svg:rect\n x=\"-2\"\n y=\"-2\"\n width=\"24\"\n height=\"24\"\n fill=\"transparent\"\n style=\"cursor: pointer\"\n />\n <svg:path\n d=\"M16.5 20.475V17.475H13.5V16.475H16.5V13.475H17.5V16.475H20.5V17.475H17.5V20.475H16.5ZM3.5 17.5V16.5H4.5V17.5H3.5ZM6.5 17.5V16.5H11.517C11.5057 16.6767 11.5043 16.845 11.513 17.005C11.521 17.165 11.531 17.33 11.543 17.5H6.5ZM3.5 13.5V12.5H4.5V13.5H3.5ZM6.5 13.5V12.5H13.804C13.6127 12.6387 13.4333 12.7913 13.266 12.958C13.0993 13.1247 12.9377 13.3053 12.781 13.5H6.5ZM3.5 9.5V8.5H4.5V9.5H3.5ZM6.5 9.5V8.5H18.5V9.5H6.5ZM3.5 5.5V4.5H4.5V5.5H3.5ZM6.5 5.5V4.5H18.5V5.5H6.5Z\"\n [attr.fill]=\"state.workflowFormId ? '#D36CFF' : 'black'\"\n transform=\"scale(0.7)\"\n />\n </svg:g>\n <!-- Arrow from circle to node -->\n <path\n d=\"M -20 50 L 0 50\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n marker-end=\"url(#arrowhead)\"\n ></path>\n }\n\n <!-- Exit point indicator (circle and arrow) -->\n @if (node.stageData?.IsExitPoint) {\n <!-- Arrow from node to circle -->\n <path\n [attr.d]=\"'M ' + node.width + ' 50 L ' + (node.width + 20) + ' 50'\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n marker-end=\"url(#arrowhead)\"\n ></path>\n <!-- Circle -->\n <circle\n [attr.cx]=\"node.width + 50\"\n cy=\"50\"\n r=\"30\"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-width=\"2\"\n ></circle>\n }\n\n <!-- Stage node -->\n @if (node.type === 'stage') {\n <!-- <rect\n x=\"0\"\n y=\"0\"\n [attr.width]=\"node.width\"\n [attr.height]=\"node.height\"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n ></rect> -->\n <svg:g\n lib-stage-node\n [node]=\"node\"\n [isStartNode]=\"node.isStartNode\"\n (stagePropertiesUpdated)=\"onStagePropertiesUpdated($event)\"\n (showShieldDialog)=\"onShowShieldDialog($event)\"\n ></svg:g>\n }\n\n <!-- Decision node -->\n @if (node.type === 'decision') {\n <path\n [attr.d]=\"\n 'M 0 ' +\n node.height / 2 +\n ' L ' +\n node.width / 2 +\n ' 0' +\n ' L ' +\n node.width +\n ' ' +\n node.height / 2 +\n ' L ' +\n node.width / 2 +\n ' ' +\n node.height +\n ' Z'\n \"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n ></path>\n\n <!-- Decision node condition indicator icon -->\n <svg:g\n (click)=\"showDecisionConditionsPopup($event, node, swimlane.order)\"\n class=\"decision-condition-icon\"\n [attr.transform]=\"\n 'translate(' +\n (node.width / 2 - 10) +\n ',' +\n (node.height / 2 - 10) +\n ')'\n \"\n >\n <svg:rect\n x=\"-5\"\n y=\"-5\"\n width=\"30\"\n height=\"30\"\n fill=\"transparent\"\n style=\"cursor: pointer\"\n />\n <svg:path\n d=\"M10 0H20V2H10V0ZM10 8H20V10H10V8ZM10 4H20V6H10V4ZM0 0H8V2H0V0ZM0 8H8V10H0V8ZM0 4H8V6H0V4Z\"\n fill=\"#D36CFF\"\n />\n </svg:g>\n }\n\n <!-- Form node -->\n @if (node.type === 'form') {\n <path\n d=\"M95.0625 50.5591V95.0625C95.0625 97.9716 93.9069 100.762 91.8498 102.819C89.7928 104.876 87.0028 106.031 84.0938 106.031H32.9062C29.9972 106.031 27.2072 104.876 25.1502 102.819C23.0931 100.762 21.9375 97.9716 21.9375 95.0625V21.9375C21.9375 19.0284 23.0931 16.2385 25.1502 14.1814C27.2072 12.1244 29.9972 10.9688 32.9062 10.9688H55.4722C57.4109 10.969 59.2701 11.7392 60.6412 13.1099L92.9213 45.3901C94.292 46.7611 95.0622 48.6204 95.0625 50.5591Z\"\n transform=\"scale(0.7)\"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-linejoin=\"round\"\n />\n <path\n d=\"M58.5 12.7969V40.2188C58.5 42.1581 59.2704 44.0181 60.6418 45.3895C62.0131 46.7608 63.8731 47.5312 65.8125 47.5312H93.2344\"\n transform=\"scale(0.7)\"\n stroke=\"#D36CFF\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n <text\n x=\"50%\"\n y=\"50%\"\n text-anchor=\"middle\"\n dominant-baseline=\"middle\"\n font-family=\"sans-serif\"\n font-size=\"14\"\n fill=\"#D36CFF\"\n >\n Form\n </text>\n }\n\n <!-- Subflow node -->\n @if (node.type === 'subflow') {\n <path\n [attr.d]=\"\n 'M 1 ' +\n node.height / 4 +\n ' L ' +\n node.width / 2 +\n ' 1 L ' +\n (node.width - 1) +\n ' ' +\n node.height / 4 +\n ' V ' +\n (node.height * 3) / 4 +\n ' L ' +\n node.width / 2 +\n ' ' +\n (node.height - 1) +\n ' L 1 ' +\n (node.height * 3) / 4 +\n ' Z'\n \"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n ></path>\n <text\n [attr.x]=\"node.width / 2\"\n [attr.y]=\"node.height / 2\"\n text-anchor=\"middle\"\n alignment-baseline=\"middle\"\n font-family=\"sans-serif\"\n font-size=\"10\"\n fill=\"#000000\"\n >\n <!-- Limit text length -->\n {{\n (node.workflowData?.name || \"Subflow\").length > 12\n ? (node.workflowData?.name || \"Subflow\").substring(0, 10) + \"...\"\n : node.workflowData?.name || \"Subflow\"\n }}\n </text>\n }\n\n <!-- Connection points for this node -->\n @for (point of node.connectionPoints || []; track point.id) { @if\n (isConnectionPointVisible(node.id)) {\n <use\n [attr.href]=\"'#connection-point-template'\"\n [attr.x]=\"point.x\"\n [attr.y]=\"point.y\"\n [attr.id]=\"point.id\"\n (mousedown)=\"startConnectionDrag($event, point, swimlane.order)\"\n />\n } }\n </g>\n <!-- A transparent hover area for improved hover detection -->\n <rect\n [attr.x]=\"node.x - 10\"\n [attr.y]=\"node.y + 40 - 10\"\n [attr.width]=\"node.width + 20\"\n [attr.height]=\"node.height + 20\"\n fill=\"transparent\"\n (mouseover)=\"showConnectionPoints(node.id, swimlane.order)\"\n (mouseout)=\"hideConnectionPoints(node.id)\"\n style=\"pointer-events: none\"\n />\n } }\n\n <!-- Connections -->\n @for (connection of state.connections; track connection.id) {\n <g class=\"connection-element\">\n <path\n [attr.d]=\"getConnectionPathForSavedConnection(connection)\"\n stroke=\"#D36CFF\"\n stroke-width=\"2\"\n fill=\"none\"\n marker-end=\"url(#arrowhead)\"\n ></path>\n </g>\n }\n\n <!-- Connection preview line -->\n @if (state.isConnectionDragging()) {\n <g>\n <path\n [attr.d]=\"getConnectionPath()\"\n stroke=\"#D36CFF\"\n stroke-width=\"2\"\n stroke-dasharray=\"5,5\"\n fill=\"none\"\n ></path>\n </g>\n }\n </svg>\n\n <div\n *ngIf=\"popupVisible\"\n [style.position]=\"'absolute'\"\n [style.left.px]=\"popupX\"\n [style.top.px]=\"popupY\"\n >\n <verben-pop-Up\n [dropdownOpen]=\"true\"\n [customStyles]=\"{ 'z-index': '99' }\"\n [enableMouseLeave]=\"false\"\n >\n <div dropdown-trigger style=\"display: none\">\n <!-- Hidden trigger element -->\n </div>\n <div\n class=\"p-3 bg-white border border-gray-200 rounded shadow-md\"\n dropdown-content\n >\n <h4 class=\"mb-2 font-medium\">Create Connection</h4>\n <div class=\"flex flex-col gap-2\">\n <ng-container *ngFor=\"let type of allowedNodeTypes\">\n <button\n class=\"px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50\"\n (click)=\"createNodeConnection(type)\"\n >\n Create {{ type | titlecase }}\n </button>\n </ng-container>\n <button\n class=\"px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50\"\n (click)=\"hideConnectionPopup()\"\n >\n Cancel\n </button>\n </div>\n </div>\n </verben-pop-Up>\n </div>\n\n <div\n *ngIf=\"showStartNodeFormPopup\"\n [style.position]=\"'absolute'\"\n [style.left.px]=\"startNodeFormPopupX\"\n [style.top.px]=\"startNodeFormPopupY\"\n >\n <verben-pop-Up [dropdownOpen]=\"true\" [customStyles]=\"{ 'z-index': '100' }\">\n <div dropdown-trigger style=\"display: none\">\n <!-- Hidden trigger element -->\n </div>\n <div\n class=\"p-3 bg-white border border-gray-200 rounded shadow-md w-64\"\n dropdown-content\n style=\"background-color: white; z-index: 1000\"\n >\n <h4 class=\"mb-2 font-medium\">Select Workflow Form</h4>\n <div *ngIf=\"isLoadingStartNodeForms\" class=\"text-center py-2\">\n Loading forms...\n </div>\n <div *ngIf=\"!isLoadingStartNodeForms\" class=\"max-h-48 overflow-y-auto\">\n <div class=\"mb-2\">\n <button\n class=\"w-full px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50 text-left\"\n (click)=\"selectStartNodeForm(null)\"\n >\n Clear form selection\n </button>\n </div>\n <div *ngFor=\"let form of startNodeFormsList\" class=\"mb-1\">\n <button\n class=\"w-full px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50 text-left\"\n (click)=\"selectStartNodeForm(form)\"\n >\n {{ form.Name }}\n </button>\n </div>\n <div\n *ngIf=\"startNodeFormsList.length === 0\"\n class=\"text-center py-2 text-gray-500\"\n >\n No forms available\n </div>\n </div>\n </div>\n </verben-pop-Up>\n </div>\n\n <div\n *ngIf=\"showSubflowPopup\"\n [style.position]=\"'absolute'\"\n [style.left.px]=\"subflowPopupX\"\n [style.top.px]=\"subflowPopupY\"\n >\n <verben-pop-Up [dropdownOpen]=\"true\" [customStyles]=\"{ 'z-index': '100' }\">\n <div dropdown-trigger style=\"display: none\">\n <!-- Hidden trigger element -->\n </div>\n <div\n class=\"p-3 bg-white border border-gray-200 rounded shadow-md w-64\"\n dropdown-content\n style=\"background-color: white; z-index: 1000\"\n >\n <h4 class=\"mb-2 font-medium\">Select Workflow</h4>\n <div *ngIf=\"isLoadingWorkflows\" class=\"text-center py-2\">\n Loading workflows...\n </div>\n <div *ngIf=\"!isLoadingWorkflows\" class=\"max-h-48 overflow-y-auto\">\n <div *ngFor=\"let workflow of workflowsList\" class=\"mb-1\">\n <button\n class=\"w-full px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50 text-left\"\n (click)=\"selectSubflowWorkflow(workflow)\"\n >\n {{ workflow.Name }}\n </button>\n </div>\n <div\n *ngIf=\"workflowsList.length === 0\"\n class=\"text-center py-2 text-gray-500\"\n >\n No workflows available\n </div>\n </div>\n </div>\n </verben-pop-Up>\n </div>\n\n <lib-conditions-popup\n [visible]=\"showDecisionConditionDialog\"\n [decisionNodeId]=\"activeDecisionNodeId\"\n [newConnectionId]=\"activeConnectionId\"\n [popupX]=\"decisionConditionPopupX\"\n [popupY]=\"decisionConditionPopupY\"\n (closed)=\"onDecisionConditionCancelled()\"\n (saved)=\"onDecisionConditionSaved($event)\"\n ></lib-conditions-popup>\n\n <!-- <lib-decision-popup\n [visible]=\"showConditionsDialog\"\n [decisionNodeId]=\"activeDecisionNodeId\"\n [popupX]=\"decisionConditionPopupX\"\n [popupY]=\"decisionConditionPopupY\"\n (closed)=\"onDecisionConditionCancelled()\"\n ></lib-decision-popup> -->\n\n <lib-stage-dialog\n [visible]=\"showStageDialogLocal\"\n [stageData]=\"activeStageData || {}\"\n (closed)=\"onStageDialogClosed()\"\n (saved)=\"onStageDialogSaved($event)\"\n ></lib-stage-dialog>\n</div>\n", styles: [".canvas-container{flex:1;overflow:auto;background-color:#fff}.designer-canvas{min-width:100%;min-height:100%;cursor:default}.designer-canvas:focus{outline:none}.edit-swimlane-button{cursor:pointer}.edit-swimlane-button:hover rect{fill:#ddd6fe}.decision-condition-icon{cursor:pointer}.decision-condition-icon:hover path{fill:#c084fc}.swimlane-label{z-index:10;cursor:default}.plus-icon:hover{fill:#d36cff}.swimlane-container{pointer-events:none}.swimlane-container>*{pointer-events:auto}.node-element{z-index:10}.connection-element{z-index:5}\n"] }]
|
|
5146
|
+
}], ctorParameters: () => [{ type: WorkflowDesignerState }, { type: WorkflowDataService }], propDecorators: { selectedTool: [{
|
|
5147
|
+
type: Input
|
|
5148
|
+
}], clickedPosition: [{
|
|
5149
|
+
type: Output
|
|
5150
|
+
}], subflowSelected: [{
|
|
5151
|
+
type: Output
|
|
5152
|
+
}], showStageDialog: [{
|
|
5153
|
+
type: Output
|
|
5154
|
+
}], stagePropertiesUpdated: [{
|
|
5155
|
+
type: Output
|
|
5156
|
+
}], canvasRef: [{
|
|
5157
|
+
type: ViewChild,
|
|
5158
|
+
args: ['canvas', { static: true }]
|
|
5159
|
+
}], onWindowMouseUp: [{
|
|
5160
|
+
type: HostListener,
|
|
5161
|
+
args: ['window:mouseup', ['$event']]
|
|
5162
|
+
}], onDocumentClick: [{
|
|
5163
|
+
type: HostListener,
|
|
5164
|
+
args: ['document:click', ['$event']]
|
|
5165
|
+
}] } });
|
|
5166
|
+
|
|
5167
|
+
class SwimlaneDialogComponent {
|
|
5168
|
+
dataService;
|
|
5169
|
+
swimlaneService;
|
|
5170
|
+
visible = false;
|
|
5171
|
+
swimlaneData = null;
|
|
5172
|
+
closed = new EventEmitter();
|
|
5173
|
+
created = new EventEmitter();
|
|
5174
|
+
searchQuery = '';
|
|
5175
|
+
workflowName = '';
|
|
5176
|
+
// Track selected tags in a separate array
|
|
5177
|
+
selectedTagNames = [];
|
|
5178
|
+
// Tags list
|
|
5179
|
+
tags = [];
|
|
5180
|
+
constructor(dataService, swimlaneService) {
|
|
5181
|
+
this.dataService = dataService;
|
|
5182
|
+
this.swimlaneService = swimlaneService;
|
|
5183
|
+
}
|
|
5184
|
+
ngOnInit() {
|
|
5185
|
+
console.log('Swimlane dialog initialized with data:', this.swimlaneData);
|
|
5186
|
+
// Reset form fields when dialog opens
|
|
5187
|
+
this.selectedTagNames = [];
|
|
5188
|
+
this.workflowName = '';
|
|
5189
|
+
this.searchQuery = '';
|
|
5190
|
+
// Load tags from the data service
|
|
5191
|
+
this.loadTags();
|
|
5192
|
+
// If editing an existing swimlane, populate the form
|
|
5193
|
+
if (this.swimlaneData) {
|
|
5194
|
+
this.workflowName = this.swimlaneData.name;
|
|
5195
|
+
// Set selected tags
|
|
5196
|
+
this.selectedTagNames = this.swimlaneData.tags.map((tag) => tag.Name);
|
|
5197
|
+
}
|
|
5198
|
+
}
|
|
5199
|
+
/**
|
|
5200
|
+
* Load tags from the data service
|
|
5201
|
+
*/
|
|
5202
|
+
loadTags() {
|
|
5203
|
+
this.dataService.getTags().then((data) => {
|
|
5204
|
+
this.tags = data.Result;
|
|
5205
|
+
});
|
|
5206
|
+
}
|
|
5207
|
+
get allSelected() {
|
|
5208
|
+
return (this.tags.length > 0 && this.selectedTagNames.length === this.tags.length);
|
|
5209
|
+
}
|
|
5210
|
+
set allSelected(value) {
|
|
5211
|
+
if (value) {
|
|
5212
|
+
this.selectedTagNames = this.tags.map((tag) => tag.Name);
|
|
3903
5213
|
}
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
// Calculate vertical distance
|
|
3907
|
-
const verticalDist = endY - startY;
|
|
3908
|
-
// If the end point is very close vertically, use a simple 3-segment path
|
|
3909
|
-
if (Math.abs(verticalDist) < 30) {
|
|
3910
|
-
const midX = (startX + endX) / 2;
|
|
3911
|
-
path = `M ${startX} ${startY} V ${endY} H ${midX} H ${endX}`;
|
|
3912
|
-
}
|
|
3913
|
-
// Otherwise, create a path with vertical segment first, then horizontal, then vertical
|
|
3914
|
-
else {
|
|
3915
|
-
path = `M ${startX} ${startY} V ${startY + verticalDist / 2} H ${endX} V ${endY}`;
|
|
3916
|
-
}
|
|
5214
|
+
else {
|
|
5215
|
+
this.selectedTagNames = [];
|
|
3917
5216
|
}
|
|
3918
|
-
return path;
|
|
3919
5217
|
}
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
const swimlane = this.state.swimlanes[i];
|
|
3924
|
-
const nodeIndex = swimlane.nodes?.findIndex((n) => n.id === event.nodeId);
|
|
3925
|
-
if (nodeIndex !== undefined && nodeIndex >= 0) {
|
|
3926
|
-
// Update the stage data
|
|
3927
|
-
if (swimlane.nodes) {
|
|
3928
|
-
swimlane.nodes[nodeIndex].stageData = {
|
|
3929
|
-
...swimlane.nodes[nodeIndex].stageData,
|
|
3930
|
-
...event.stageData,
|
|
3931
|
-
};
|
|
3932
|
-
console.log('Updated stage data in swimlane:', i, 'node:', nodeIndex, 'data:', swimlane.nodes[nodeIndex].stageData);
|
|
3933
|
-
}
|
|
3934
|
-
break;
|
|
3935
|
-
}
|
|
5218
|
+
get filteredTags() {
|
|
5219
|
+
if (!this.searchQuery) {
|
|
5220
|
+
return this.tags;
|
|
3936
5221
|
}
|
|
5222
|
+
return this.tags.filter((tag) => tag.Name.toLowerCase().includes(this.searchQuery.toLowerCase()));
|
|
3937
5223
|
}
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
5224
|
+
isTagSelected(tagName) {
|
|
5225
|
+
return this.selectedTagNames.includes(tagName);
|
|
5226
|
+
}
|
|
5227
|
+
toggleTagSelection(tagName) {
|
|
5228
|
+
const index = this.selectedTagNames.indexOf(tagName);
|
|
5229
|
+
if (index > -1) {
|
|
5230
|
+
this.selectedTagNames.splice(index, 1);
|
|
5231
|
+
}
|
|
5232
|
+
else {
|
|
5233
|
+
this.selectedTagNames.push(tagName);
|
|
3944
5234
|
}
|
|
3945
5235
|
}
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
.map((conn) => this.state.findNodeById(conn.targetNodeId))
|
|
3953
|
-
.filter((nodeInfo) => nodeInfo && nodeInfo.node.type === 'stage');
|
|
3954
|
-
node.hasMultipleConnectedStages = connectedStageNodes.length > 1;
|
|
5236
|
+
onDialogClose(eventData) {
|
|
5237
|
+
console.log('Dialog closed, received data:', eventData);
|
|
5238
|
+
this.searchQuery = '';
|
|
5239
|
+
this.workflowName = '';
|
|
5240
|
+
this.selectedTagNames = [];
|
|
5241
|
+
this.closed.emit();
|
|
3955
5242
|
}
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
//
|
|
3961
|
-
this.
|
|
3962
|
-
|
|
3963
|
-
|
|
5243
|
+
onDialogOpen(eventData) {
|
|
5244
|
+
console.log('Dialog opened, received data:', eventData);
|
|
5245
|
+
}
|
|
5246
|
+
applySelection() {
|
|
5247
|
+
// Validate name
|
|
5248
|
+
if (!this.workflowName || this.workflowName.trim() === '') {
|
|
5249
|
+
console.error('Swimlane name cannot be empty');
|
|
5250
|
+
return;
|
|
5251
|
+
}
|
|
5252
|
+
const selectedTags = this.tags.filter((tag) => this.selectedTagNames.includes(tag.Name));
|
|
5253
|
+
console.log('Submitting swimlane data:', {
|
|
5254
|
+
name: this.workflowName,
|
|
5255
|
+
tags: selectedTags,
|
|
5256
|
+
});
|
|
5257
|
+
this.created.emit({
|
|
5258
|
+
tags: selectedTags,
|
|
5259
|
+
name: this.workflowName,
|
|
3964
5260
|
});
|
|
3965
5261
|
}
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
5262
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: SwimlaneDialogComponent, deps: [{ token: WorkflowDataService }, { token: SwimlaneService }], target: i0.ɵɵFactoryTarget.Component });
|
|
5263
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: SwimlaneDialogComponent, selector: "lib-swimlane-dialog", inputs: { visible: "visible", swimlaneData: "swimlaneData" }, outputs: { closed: "closed", created: "created" }, ngImport: i0, template: "<verben-dialogue\n [showCloseIcon]=\"true\"\n [dismissOutsideClick]=\"true\"\n [closeOnEscape]=\"true\"\n [size]=\"'medium'\"\n [mode]=\"'drawer'\"\n [disableFooter]=\"false\"\n [isVisible]=\"visible\"\n [headerTemplate]=\"headerTemplate\"\n [bodyTemplate]=\"bodyTemplate\"\n [footerTemplate]=\"footerTemplate\"\n (openModal)=\"onDialogOpen($event)\"\n (closeModal)=\"onDialogClose($event)\"\n>\n</verben-dialogue>\n\n<ng-template #headerTemplate>\n <div class=\"flex items-center p-4 border-b border-gray-200\">\n <button class=\"mr-4\" type=\"button\">\n <span\n class=\"block w-3 h-3 border-t-2 border-l-2 border-gray-700 transform -rotate-45\"\n ></span>\n </button>\n <div class=\"flex-1\">\n <h2 class=\"text-base font-medium m-0\">Name</h2>\n <span class=\"text-xs text-gray-500\">{{ tags.length }} tags</span>\n </div>\n <span class=\"text-blue-600 font-medium cursor-pointer\">Create</span>\n </div>\n</ng-template>\n\n<ng-template #bodyTemplate>\n <div class=\"p-4\">\n <div class=\"mb-4 space-y-4\">\n <input\n type=\"text\"\n placeholder=\"Enter Name\"\n class=\"w-full px-4 py-2 rounded-full bg-gray-50 border border-gray-200\"\n [(ngModel)]=\"workflowName\"\n />\n\n <input\n type=\"text\"\n placeholder=\"Filter Tags\"\n class=\"w-full px-4 py-2 rounded-full bg-gray-50 border border-gray-200\"\n [(ngModel)]=\"searchQuery\"\n />\n </div>\n\n <div class=\"divide-y divide-gray-100\">\n <div class=\"py-3\">\n <label class=\"flex items-center\">\n <input type=\"checkbox\" class=\"mr-3\" [(ngModel)]=\"allSelected\" />\n <span class=\"text-sm\">Select All</span>\n </label>\n </div>\n\n <div *ngFor=\"let tag of filteredTags\" class=\"py-3\">\n <label class=\"flex items-center\">\n <input\n type=\"checkbox\"\n class=\"mr-3\"\n [checked]=\"isTagSelected(tag.Name)\"\n (change)=\"toggleTagSelection(tag.Name)\"\n />\n <span class=\"text-sm\">{{ tag.Name }}</span>\n <span *ngIf=\"!tag.IsOptional\" class=\"ml-2 text-xs text-red-500\"\n >(Required)</span\n >\n </label>\n </div>\n </div>\n </div>\n</ng-template>\n\n<ng-template #footerTemplate>\n <div class=\"flex justify-end p-4 border-t border-gray-200\">\n <button\n class=\"px-4 py-2 mr-2 bg-white border border-gray-200 rounded text-sm font-medium\"\n (click)=\"onDialogClose($event)\"\n >\n Cancel\n </button>\n <button\n class=\"px-4 py-2 bg-blue-600 text-white rounded text-sm font-medium\"\n (click)=\"applySelection()\"\n >\n Apply\n </button>\n </div>\n</ng-template>\n", styles: [""], dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i8.VerbenDialogueComponent, selector: "verben-dialogue", inputs: ["headerTemplate", "bodyTemplate", "footerTemplate", "showCloseIcon", "dismissOutsideClick", "closeOnEscape", "isVisible", "size", "backdropColor", "customClass", "disableFooter", "margin", "padding", "borderRadius", "dialogueBgColor", "closeIconClass", "boxShadow", "enableTransition", "modalData", "mode", "position", "drawerWidth"], outputs: ["openModal", "closeModal"] }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
|
|
5264
|
+
}
|
|
5265
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: SwimlaneDialogComponent, decorators: [{
|
|
5266
|
+
type: Component,
|
|
5267
|
+
args: [{ selector: 'lib-swimlane-dialog', template: "<verben-dialogue\n [showCloseIcon]=\"true\"\n [dismissOutsideClick]=\"true\"\n [closeOnEscape]=\"true\"\n [size]=\"'medium'\"\n [mode]=\"'drawer'\"\n [disableFooter]=\"false\"\n [isVisible]=\"visible\"\n [headerTemplate]=\"headerTemplate\"\n [bodyTemplate]=\"bodyTemplate\"\n [footerTemplate]=\"footerTemplate\"\n (openModal)=\"onDialogOpen($event)\"\n (closeModal)=\"onDialogClose($event)\"\n>\n</verben-dialogue>\n\n<ng-template #headerTemplate>\n <div class=\"flex items-center p-4 border-b border-gray-200\">\n <button class=\"mr-4\" type=\"button\">\n <span\n class=\"block w-3 h-3 border-t-2 border-l-2 border-gray-700 transform -rotate-45\"\n ></span>\n </button>\n <div class=\"flex-1\">\n <h2 class=\"text-base font-medium m-0\">Name</h2>\n <span class=\"text-xs text-gray-500\">{{ tags.length }} tags</span>\n </div>\n <span class=\"text-blue-600 font-medium cursor-pointer\">Create</span>\n </div>\n</ng-template>\n\n<ng-template #bodyTemplate>\n <div class=\"p-4\">\n <div class=\"mb-4 space-y-4\">\n <input\n type=\"text\"\n placeholder=\"Enter Name\"\n class=\"w-full px-4 py-2 rounded-full bg-gray-50 border border-gray-200\"\n [(ngModel)]=\"workflowName\"\n />\n\n <input\n type=\"text\"\n placeholder=\"Filter Tags\"\n class=\"w-full px-4 py-2 rounded-full bg-gray-50 border border-gray-200\"\n [(ngModel)]=\"searchQuery\"\n />\n </div>\n\n <div class=\"divide-y divide-gray-100\">\n <div class=\"py-3\">\n <label class=\"flex items-center\">\n <input type=\"checkbox\" class=\"mr-3\" [(ngModel)]=\"allSelected\" />\n <span class=\"text-sm\">Select All</span>\n </label>\n </div>\n\n <div *ngFor=\"let tag of filteredTags\" class=\"py-3\">\n <label class=\"flex items-center\">\n <input\n type=\"checkbox\"\n class=\"mr-3\"\n [checked]=\"isTagSelected(tag.Name)\"\n (change)=\"toggleTagSelection(tag.Name)\"\n />\n <span class=\"text-sm\">{{ tag.Name }}</span>\n <span *ngIf=\"!tag.IsOptional\" class=\"ml-2 text-xs text-red-500\"\n >(Required)</span\n >\n </label>\n </div>\n </div>\n </div>\n</ng-template>\n\n<ng-template #footerTemplate>\n <div class=\"flex justify-end p-4 border-t border-gray-200\">\n <button\n class=\"px-4 py-2 mr-2 bg-white border border-gray-200 rounded text-sm font-medium\"\n (click)=\"onDialogClose($event)\"\n >\n Cancel\n </button>\n <button\n class=\"px-4 py-2 bg-blue-600 text-white rounded text-sm font-medium\"\n (click)=\"applySelection()\"\n >\n Apply\n </button>\n </div>\n</ng-template>\n" }]
|
|
5268
|
+
}], ctorParameters: () => [{ type: WorkflowDataService }, { type: SwimlaneService }], propDecorators: { visible: [{
|
|
5269
|
+
type: Input
|
|
5270
|
+
}], swimlaneData: [{
|
|
5271
|
+
type: Input
|
|
5272
|
+
}], closed: [{
|
|
5273
|
+
type: Output
|
|
5274
|
+
}], created: [{
|
|
5275
|
+
type: Output
|
|
5276
|
+
}] } });
|
|
5277
|
+
|
|
5278
|
+
class WorkflowDesignerComponent {
|
|
5279
|
+
state;
|
|
5280
|
+
dataService;
|
|
5281
|
+
canvasRef;
|
|
5282
|
+
workflowCode = null;
|
|
5283
|
+
// Currently selected tool
|
|
5284
|
+
selectedTool = null;
|
|
5285
|
+
showSwimlaneDialog = false;
|
|
5286
|
+
// New properties for stage dialog
|
|
5287
|
+
showStageDialog = false;
|
|
5288
|
+
pendingStagePosition = null;
|
|
5289
|
+
pendingStageData = {};
|
|
5290
|
+
editingSwimlaneIndex = null;
|
|
5291
|
+
isCreatingStageFromConnection = false;
|
|
5292
|
+
pendingConnectionSourcePoint = null;
|
|
5293
|
+
pendingConnectionSourceSwimlaneIndex = null;
|
|
5294
|
+
isLoading = false;
|
|
5295
|
+
isSaving = false;
|
|
5296
|
+
constructor(state, dataService) {
|
|
5297
|
+
this.state = state;
|
|
5298
|
+
this.dataService = dataService;
|
|
5299
|
+
}
|
|
5300
|
+
ngOnInit() {
|
|
5301
|
+
// Load workflow data if code is provided
|
|
5302
|
+
if (this.workflowCode) {
|
|
5303
|
+
this.loadWorkflow(this.workflowCode);
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
saveWorkflow() {
|
|
5307
|
+
if (this.isSaving)
|
|
5308
|
+
return;
|
|
5309
|
+
this.isSaving = true;
|
|
5310
|
+
// Transform the UI model to the API model
|
|
5311
|
+
const workflowModel = this.state.transformToWorkflowModel();
|
|
5312
|
+
if (workflowModel) {
|
|
5313
|
+
// Save to the backend
|
|
3985
5314
|
this.dataService
|
|
3986
|
-
.
|
|
5315
|
+
.saveWorkflows([workflowModel])
|
|
3987
5316
|
.then((response) => {
|
|
3988
|
-
|
|
3989
|
-
|
|
5317
|
+
console.log('Workflow saved successfully:', response);
|
|
5318
|
+
// If the API returns an updated workflow, we might want to reload it
|
|
5319
|
+
if (response?.Result?.Code) {
|
|
5320
|
+
this.workflowCode = response.Result.Code;
|
|
5321
|
+
}
|
|
5322
|
+
this.isSaving = false;
|
|
3990
5323
|
})
|
|
3991
5324
|
.catch((error) => {
|
|
3992
|
-
console.error('Error
|
|
3993
|
-
this.
|
|
5325
|
+
console.error('Error saving workflow:', error);
|
|
5326
|
+
this.isSaving = false;
|
|
5327
|
+
// Handle error - show a notification, etc.
|
|
5328
|
+
});
|
|
5329
|
+
}
|
|
5330
|
+
}
|
|
5331
|
+
loadWorkflow(code) {
|
|
5332
|
+
this.isLoading = true;
|
|
5333
|
+
this.dataService
|
|
5334
|
+
.getWorkflowWithParam(code)
|
|
5335
|
+
.then((response) => {
|
|
5336
|
+
if (response && response.Result) {
|
|
5337
|
+
// Store the workflow ID
|
|
5338
|
+
const wf = response.Result[0];
|
|
5339
|
+
this.state.setWorkflowId(wf.Id);
|
|
5340
|
+
this.parseWorkflowData(wf);
|
|
5341
|
+
}
|
|
5342
|
+
this.isLoading = false;
|
|
5343
|
+
})
|
|
5344
|
+
.catch((error) => {
|
|
5345
|
+
console.error('Error loading workflow:', error);
|
|
5346
|
+
this.isLoading = false;
|
|
5347
|
+
});
|
|
5348
|
+
}
|
|
5349
|
+
// Method to parse workflow data from API and populate the designer
|
|
5350
|
+
parseWorkflowData(workflow) {
|
|
5351
|
+
// Store the complete workflow
|
|
5352
|
+
this.state.workflow = workflow;
|
|
5353
|
+
// Clear existing state
|
|
5354
|
+
this.state.swimlanes = [];
|
|
5355
|
+
this.state.connections = [];
|
|
5356
|
+
this.state.swimlaneRecord = {};
|
|
5357
|
+
this.state.stageRecord = {};
|
|
5358
|
+
this.state.actionRecord = {};
|
|
5359
|
+
// Set workflow form if exists
|
|
5360
|
+
if (workflow.Form) {
|
|
5361
|
+
this.state.setWorkflowForm(workflow.Form, workflow.FormName || 'Workflow Form');
|
|
5362
|
+
}
|
|
5363
|
+
// Store the workflow ID
|
|
5364
|
+
this.state.setWorkflowId(workflow.Id);
|
|
5365
|
+
// Process swimlanes first
|
|
5366
|
+
if (workflow.Lanes && workflow.Lanes.length) {
|
|
5367
|
+
workflow.Lanes.sort((a, b) => a.Position - b.Position).forEach((lane) => {
|
|
5368
|
+
// Store original lane in record
|
|
5369
|
+
this.state.swimlaneRecord[lane.Id] = lane;
|
|
5370
|
+
// Add swimlane to UI model with the original ID
|
|
5371
|
+
const swimlane = {
|
|
5372
|
+
order: lane.Position,
|
|
5373
|
+
label: lane.Name || `Lane ${lane.Position}`,
|
|
5374
|
+
tags: lane.Tags || [],
|
|
5375
|
+
nodes: [],
|
|
5376
|
+
id: lane.Id, // Store the original ID
|
|
5377
|
+
};
|
|
5378
|
+
this.state.swimlanes.push(swimlane);
|
|
5379
|
+
// Register loaded object
|
|
5380
|
+
this.state.registerLoadedObject(lane.Id, lane.Code);
|
|
5381
|
+
});
|
|
5382
|
+
}
|
|
5383
|
+
// Process stages
|
|
5384
|
+
if (workflow.Stages && workflow.Stages.length) {
|
|
5385
|
+
workflow.Stages.forEach((stage) => {
|
|
5386
|
+
// Store original stage in record
|
|
5387
|
+
this.state.stageRecord[stage.Id] = stage;
|
|
5388
|
+
// Find swimlane index by SwimLane ID
|
|
5389
|
+
const swimlaneIndex = this.state.swimlanes.findIndex((lane) => lane.id === stage.SwimLane);
|
|
5390
|
+
if (swimlaneIndex !== -1) {
|
|
5391
|
+
const x = stage.Coordinates?.X || 100;
|
|
5392
|
+
const y = stage.Coordinates?.Y || 50;
|
|
5393
|
+
// Create node with original ID
|
|
5394
|
+
const node = {
|
|
5395
|
+
id: stage.Id, // Use original ID
|
|
5396
|
+
type: 'stage',
|
|
5397
|
+
x: x,
|
|
5398
|
+
y: y - swimlaneIndex * 263 - 40, // Adjust Y for swimlane offset
|
|
5399
|
+
width: 169,
|
|
5400
|
+
height: 100,
|
|
5401
|
+
isStartNode: stage.IsEntryPoint,
|
|
5402
|
+
stageData: {
|
|
5403
|
+
Name: stage.Name,
|
|
5404
|
+
Description: stage.Description,
|
|
5405
|
+
Duration: stage.Duration,
|
|
5406
|
+
PassOnRule: stage.PassOnRule,
|
|
5407
|
+
ActorRule: stage.ActorRule,
|
|
5408
|
+
MinNoOfActor: stage.MinNoOfActor,
|
|
5409
|
+
Tags: stage.Tags || [],
|
|
5410
|
+
IsParallel: stage.IsParallel,
|
|
5411
|
+
IsEntryPoint: stage.IsEntryPoint,
|
|
5412
|
+
IsExitPoint: stage.IsExitPoint,
|
|
5413
|
+
Id: stage.Id, // Keep original ID
|
|
5414
|
+
},
|
|
5415
|
+
connectionPoints: [], // Initialize with empty array
|
|
5416
|
+
};
|
|
5417
|
+
// Generate connection points
|
|
5418
|
+
node.connectionPoints = this.state.generateConnectionPoints(node);
|
|
5419
|
+
// Add node to swimlane
|
|
5420
|
+
this.state.swimlanes[swimlaneIndex].nodes =
|
|
5421
|
+
this.state.swimlanes[swimlaneIndex].nodes || [];
|
|
5422
|
+
this.state.swimlanes[swimlaneIndex].nodes.push(node);
|
|
5423
|
+
// If this is a start node, update state
|
|
5424
|
+
if (stage.IsEntryPoint) {
|
|
5425
|
+
this.state.startNodeId = stage.Id;
|
|
5426
|
+
}
|
|
5427
|
+
// Register loaded stage
|
|
5428
|
+
this.state.registerLoadedObject(stage.Id, stage.Code);
|
|
5429
|
+
}
|
|
5430
|
+
});
|
|
5431
|
+
}
|
|
5432
|
+
// Process connections/actions
|
|
5433
|
+
if (workflow.Actions && workflow.Actions.length) {
|
|
5434
|
+
workflow.Actions.forEach((action) => {
|
|
5435
|
+
// Store original action in record
|
|
5436
|
+
this.state.actionRecord[action.Id] = action;
|
|
5437
|
+
// Find source and target nodes
|
|
5438
|
+
const sourceNodeInfo = this.state.findNodeById(action.FromStage);
|
|
5439
|
+
const targetNodeInfo = this.state.findNodeById(action.ToStage);
|
|
5440
|
+
if (sourceNodeInfo && targetNodeInfo) {
|
|
5441
|
+
// Find suitable connection points
|
|
5442
|
+
const sourceNode = sourceNodeInfo.node;
|
|
5443
|
+
const targetNode = targetNodeInfo.node;
|
|
5444
|
+
const sourcePoint = sourceNode.connectionPoints?.find((p) => p.type === 'right' || p.type === 'bottom');
|
|
5445
|
+
const targetPoint = targetNode.connectionPoints?.find((p) => p.type === 'left' || p.type === 'top');
|
|
5446
|
+
if (sourcePoint && targetPoint) {
|
|
5447
|
+
const connection = {
|
|
5448
|
+
id: action.Id, // Use original ID
|
|
5449
|
+
sourceNodeId: action.FromStage,
|
|
5450
|
+
targetNodeId: action.ToStage,
|
|
5451
|
+
sourcePointId: sourcePoint.id,
|
|
5452
|
+
targetPointId: targetPoint.id,
|
|
5453
|
+
sourceSwimlaneIndex: sourceNodeInfo.swimlaneIndex,
|
|
5454
|
+
targetSwimlaneIndex: targetNodeInfo.swimlaneIndex,
|
|
5455
|
+
condition: action.PassOnRule || '',
|
|
5456
|
+
};
|
|
5457
|
+
this.state.connections.push(connection);
|
|
5458
|
+
// Register loaded action
|
|
5459
|
+
this.state.registerLoadedObject(action.Id, action.Code);
|
|
5460
|
+
}
|
|
5461
|
+
}
|
|
3994
5462
|
});
|
|
3995
5463
|
}
|
|
3996
|
-
this.showStartNodeFormPopup = !this.showStartNodeFormPopup;
|
|
3997
5464
|
}
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
5465
|
+
// Helper method to find swimlane index by Lane ID
|
|
5466
|
+
findSwimlaneIndexByLaneId(laneId) {
|
|
5467
|
+
// First check our mapping
|
|
5468
|
+
if (this.state.laneIdToIndexMap.has(laneId)) {
|
|
5469
|
+
return this.state.laneIdToIndexMap.get(laneId);
|
|
4001
5470
|
}
|
|
4002
|
-
|
|
4003
|
-
|
|
5471
|
+
// Fallback to the original logic (for backward compatibility)
|
|
5472
|
+
const parts = laneId.split('-');
|
|
5473
|
+
if (parts.length > 1) {
|
|
5474
|
+
const index = parseInt(parts[1]);
|
|
5475
|
+
if (!isNaN(index) && index < this.state.swimlanes.length) {
|
|
5476
|
+
return index;
|
|
5477
|
+
}
|
|
4004
5478
|
}
|
|
4005
|
-
|
|
4006
|
-
this.showStartNodeFormPopup = false;
|
|
5479
|
+
return -1;
|
|
4007
5480
|
}
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
this.
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
5481
|
+
// Handle tool selection from toolbar
|
|
5482
|
+
onToolSelected(tool) {
|
|
5483
|
+
// Toggle selection if the same tool is clicked again
|
|
5484
|
+
this.selectedTool = this.selectedTool === tool ? null : tool;
|
|
5485
|
+
console.log('Selected tool:', this.selectedTool);
|
|
5486
|
+
}
|
|
5487
|
+
openSwimlaneDialog() {
|
|
5488
|
+
this.showSwimlaneDialog = true;
|
|
5489
|
+
}
|
|
5490
|
+
onShowStageDialog(event) {
|
|
5491
|
+
console.log('Received showStageDialog event:', event);
|
|
5492
|
+
this.pendingStagePosition = event.position;
|
|
5493
|
+
this.showStageDialog = true;
|
|
5494
|
+
// If this is from a connection creation, store that info
|
|
5495
|
+
this.isCreatingStageFromConnection = event.isFromConnection;
|
|
5496
|
+
// Store the source point data before it gets lost
|
|
5497
|
+
if (event.isFromConnection &&
|
|
5498
|
+
this.state.draggingConnectionData.sourcePoint) {
|
|
4015
5499
|
this.pendingConnectionSourcePoint =
|
|
4016
|
-
this.state.draggingConnectionData.sourcePoint
|
|
5500
|
+
this.state.draggingConnectionData.sourcePoint;
|
|
4017
5501
|
this.pendingConnectionSourceSwimlaneIndex =
|
|
4018
5502
|
this.state.draggingConnectionData.sourceSwimlaneIndex ?? null;
|
|
4019
|
-
console.log('Stored
|
|
4020
|
-
sourcePoint: this.pendingConnectionSourcePoint,
|
|
4021
|
-
sourceSwimlaneIndex: this.pendingConnectionSourceSwimlaneIndex,
|
|
4022
|
-
});
|
|
5503
|
+
console.log('Stored source point for connection:', this.pendingConnectionSourcePoint);
|
|
4023
5504
|
}
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
.getWorkflows()
|
|
4028
|
-
.then((response) => {
|
|
4029
|
-
this.workflowsList = response.Result;
|
|
4030
|
-
this.isLoadingWorkflows = false;
|
|
4031
|
-
})
|
|
4032
|
-
.catch((error) => {
|
|
4033
|
-
console.error('Error loading workflows:', error);
|
|
4034
|
-
this.isLoadingWorkflows = false;
|
|
5505
|
+
console.log('Stage dialog state:', {
|
|
5506
|
+
pendingPosition: this.pendingStagePosition,
|
|
5507
|
+
isVisible: this.showStageDialog,
|
|
4035
5508
|
});
|
|
4036
|
-
this.showSubflowPopup = true;
|
|
4037
5509
|
}
|
|
4038
|
-
|
|
4039
|
-
console.log(
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
console.log('
|
|
4054
|
-
//
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
const connection = {
|
|
4068
|
-
id: `conn-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
|
|
4069
|
-
sourceNodeId: sourcePoint.nodeId,
|
|
4070
|
-
targetNodeId: node.id,
|
|
4071
|
-
sourcePointId: sourcePoint.id,
|
|
4072
|
-
targetPointId: targetPoint.id,
|
|
4073
|
-
sourceSwimlaneIndex: this.pendingConnectionSourceSwimlaneIndex,
|
|
4074
|
-
targetSwimlaneIndex: swimlaneIndex,
|
|
4075
|
-
};
|
|
4076
|
-
// Add connection directly to the state's connections array
|
|
4077
|
-
this.state.connections.push(connection);
|
|
4078
|
-
console.log('Added connection to state:', connection);
|
|
4079
|
-
console.log('Connections after adding:', this.state.connections);
|
|
5510
|
+
handleCanvasPositionClick(event) {
|
|
5511
|
+
console.log(event);
|
|
5512
|
+
// Check if this is an edit swimlane event
|
|
5513
|
+
if (event.x === -1 &&
|
|
5514
|
+
event.y >= 0 &&
|
|
5515
|
+
event.y < this.state.swimlanes.length) {
|
|
5516
|
+
console.log('Editing swimlane', event.y);
|
|
5517
|
+
this.editingSwimlaneIndex = event.y;
|
|
5518
|
+
this.openSwimlaneDialog();
|
|
5519
|
+
return;
|
|
5520
|
+
}
|
|
5521
|
+
// Normal canvas click handling
|
|
5522
|
+
const swimlaneIndex = Math.floor(event.y / 263);
|
|
5523
|
+
switch (this.selectedTool) {
|
|
5524
|
+
case 'swimlane':
|
|
5525
|
+
console.log('Opening swimlane dialog for new swimlane');
|
|
5526
|
+
this.editingSwimlaneIndex = null; // Set to null to indicate creating a new swimlane
|
|
5527
|
+
this.openSwimlaneDialog();
|
|
5528
|
+
this.selectedTool = null;
|
|
5529
|
+
break;
|
|
5530
|
+
case 'stage':
|
|
5531
|
+
if (swimlaneIndex >= 0 && swimlaneIndex < this.state.swimlanes.length) {
|
|
5532
|
+
console.log(`Adding ${this.selectedTool} to swimlane ${swimlaneIndex}`);
|
|
5533
|
+
// Create the stage directly with default properties
|
|
5534
|
+
const node = this.state.addNode(swimlaneIndex, 'stage', event.x, event.y, { Name: 'New Stage' } // Add default properties
|
|
5535
|
+
);
|
|
5536
|
+
if (node) {
|
|
5537
|
+
// Reset the selected tool after placing a node
|
|
5538
|
+
this.selectedTool = null;
|
|
4080
5539
|
}
|
|
4081
5540
|
}
|
|
4082
|
-
|
|
5541
|
+
break;
|
|
5542
|
+
case 'decision':
|
|
5543
|
+
case 'form':
|
|
5544
|
+
if (swimlaneIndex >= 0 && swimlaneIndex < this.state.swimlanes.length) {
|
|
5545
|
+
console.log(`Adding ${this.selectedTool} to swimlane ${swimlaneIndex}`);
|
|
5546
|
+
const node = this.state.addNode(swimlaneIndex, this.selectedTool, event.x, event.y);
|
|
5547
|
+
if (node) {
|
|
5548
|
+
// Reset the selected tool after placing a node
|
|
5549
|
+
this.selectedTool = null;
|
|
5550
|
+
}
|
|
5551
|
+
}
|
|
5552
|
+
break;
|
|
5553
|
+
case 'subflow':
|
|
5554
|
+
if (swimlaneIndex >= 0 && swimlaneIndex < this.state.swimlanes.length) {
|
|
5555
|
+
console.log(`Adding ${this.selectedTool} to swimlane ${swimlaneIndex}`);
|
|
5556
|
+
// Show subflow workflow selection popup
|
|
5557
|
+
this.canvasRef.showSubflowSelectionPopup(event.x, event.y, swimlaneIndex);
|
|
5558
|
+
}
|
|
5559
|
+
break;
|
|
5560
|
+
default:
|
|
5561
|
+
break;
|
|
5562
|
+
}
|
|
5563
|
+
}
|
|
5564
|
+
onSwimlaneDialogFilled(event) {
|
|
5565
|
+
console.log('Swimlane dialog filled:', event, 'Editing index:', this.editingSwimlaneIndex);
|
|
5566
|
+
// Validate that name is not empty
|
|
5567
|
+
if (!event.name || event.name.trim() === '') {
|
|
5568
|
+
console.error('Swimlane name cannot be empty');
|
|
5569
|
+
return;
|
|
5570
|
+
}
|
|
5571
|
+
if (this.editingSwimlaneIndex !== null) {
|
|
5572
|
+
// Update existing swimlane
|
|
5573
|
+
console.log('Updating existing swimlane at index:', this.editingSwimlaneIndex);
|
|
5574
|
+
this.state.updateSwimlane(this.editingSwimlaneIndex, event.name, event.tags);
|
|
5575
|
+
}
|
|
5576
|
+
else {
|
|
5577
|
+
// Create new swimlane
|
|
5578
|
+
console.log('Creating new swimlane');
|
|
5579
|
+
this.state.addSwimlane(event.name, event.tags);
|
|
5580
|
+
}
|
|
5581
|
+
this.showSwimlaneDialog = false;
|
|
5582
|
+
this.editingSwimlaneIndex = null;
|
|
5583
|
+
}
|
|
5584
|
+
onStageDialogSaved(stageData) {
|
|
5585
|
+
if (this.canvasRef.activeStageId) {
|
|
5586
|
+
// Editing an existing stage
|
|
5587
|
+
const nodeInfo = this.state.findNodeById(this.canvasRef.activeStageId);
|
|
5588
|
+
if (nodeInfo) {
|
|
5589
|
+
// Update the stage data
|
|
5590
|
+
nodeInfo.node.stageData = {
|
|
5591
|
+
...nodeInfo.node.stageData,
|
|
5592
|
+
...stageData,
|
|
5593
|
+
};
|
|
5594
|
+
console.log('Updated stage data:', nodeInfo.node.stageData);
|
|
4083
5595
|
}
|
|
4084
|
-
|
|
4085
|
-
|
|
5596
|
+
this.canvasRef.activeStageId = null;
|
|
5597
|
+
}
|
|
5598
|
+
else if (this.pendingStagePosition) {
|
|
5599
|
+
if (this.canvasRef.activeStageId) {
|
|
5600
|
+
// Editing an existing stage
|
|
5601
|
+
const nodeInfo = this.state.findNodeById(this.canvasRef.activeStageId);
|
|
5602
|
+
if (nodeInfo) {
|
|
5603
|
+
// Update the stage data
|
|
5604
|
+
nodeInfo.node.stageData = {
|
|
5605
|
+
...nodeInfo.node.stageData,
|
|
5606
|
+
...stageData,
|
|
5607
|
+
};
|
|
5608
|
+
console.log('Updated stage data:', nodeInfo.node.stageData);
|
|
5609
|
+
}
|
|
5610
|
+
this.canvasRef.activeStageId = null;
|
|
5611
|
+
}
|
|
5612
|
+
else if (this.pendingStagePosition) {
|
|
5613
|
+
// Creating a new stage (keep existing logic for this case)
|
|
5614
|
+
// ...existing code...
|
|
5615
|
+
}
|
|
5616
|
+
this.showStageDialog = false;
|
|
4086
5617
|
}
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
this.
|
|
5618
|
+
this.showStageDialog = false;
|
|
5619
|
+
}
|
|
5620
|
+
onSubflowSelected() {
|
|
5621
|
+
// Reset the selected tool after placing a subflow node
|
|
5622
|
+
this.selectedTool = null;
|
|
5623
|
+
}
|
|
5624
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: WorkflowDesignerComponent, deps: [{ token: WorkflowDesignerState }, { token: WorkflowDataService }], target: i0.ɵɵFactoryTarget.Component });
|
|
5625
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: WorkflowDesignerComponent, selector: "lib-workflow-designer", inputs: { workflowCode: "workflowCode" }, viewQueries: [{ propertyName: "canvasRef", first: true, predicate: ["canvasRef"], descendants: true }], ngImport: i0, template: "<div class=\"workflow-designer\">\n <div class=\"workflow-header\">\n <h1>Workflow Designer</h1>\n </div>\n\n <!-- Toolbar Component -->\n <lib-designer-toolbar\n [selectedTool]=\"selectedTool\"\n [isSaving]=\"isSaving\"\n (toolSelected)=\"onToolSelected($event)\"\n (saveWorkflow)=\"saveWorkflow()\"\n >\n </lib-designer-toolbar>\n\n <!-- Show loading indicator if needed -->\n <div *ngIf=\"isLoading\" class=\"loading-overlay\">\n <div class=\"loading-spinner\"></div>\n <div class=\"loading-text\">Loading workflow...</div>\n </div>\n\n <!-- Canvas Component -->\n <lib-designer-canvas\n [selectedTool]=\"selectedTool\"\n (clickedPosition)=\"handleCanvasPositionClick($event)\"\n (subflowSelected)=\"onSubflowSelected()\"\n (showStageDialog)=\"onShowStageDialog($event)\"\n #canvasRef\n >\n </lib-designer-canvas>\n\n <lib-swimlane-dialog\n [visible]=\"showSwimlaneDialog\"\n [swimlaneData]=\"\n editingSwimlaneIndex !== null\n ? {\n name: state.swimlanes[editingSwimlaneIndex].label,\n tags: state.swimlanes[editingSwimlaneIndex].tags\n }\n : null\n \"\n (created)=\"onSwimlaneDialogFilled($event)\"\n (closed)=\"showSwimlaneDialog = false\"\n ></lib-swimlane-dialog>\n\n <lib-stage-dialog\n [visible]=\"showStageDialog\"\n [stageData]=\"pendingStageData\"\n (closed)=\"showStageDialog = false\"\n (saved)=\"onStageDialogSaved($event)\"\n ></lib-stage-dialog>\n</div>\n", styles: [".workflow-designer{display:flex;flex-direction:column;height:100vh;background-color:#f8fafc}.workflow-header{padding:1rem;background-color:#fff;border-bottom:1px solid #e2e8f0}.workflow-header h1{font-size:1.25rem;font-weight:600;color:#334155;margin:0}.loading-overlay{position:absolute;inset:0;background-color:#ffffffb3;display:flex;flex-direction:column;justify-content:center;align-items:center;z-index:1000}.loading-spinner{border:4px solid #f3f3f3;border-top:4px solid #d8b4fe;border-radius:50%;width:40px;height:40px;animation:spin 1s linear infinite}.loading-text{margin-top:1rem;font-size:1rem;color:#7e22ce}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"], dependencies: [{ kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: DesignerToolbarComponent, selector: "lib-designer-toolbar", inputs: ["selectedTool", "isSaving"], outputs: ["toolSelected", "saveWorkflow"] }, { kind: "component", type: DesignerCanvasComponent, selector: "lib-designer-canvas", inputs: ["selectedTool"], outputs: ["clickedPosition", "subflowSelected", "showStageDialog", "stagePropertiesUpdated"] }, { kind: "component", type: SwimlaneDialogComponent, selector: "lib-swimlane-dialog", inputs: ["visible", "swimlaneData"], outputs: ["closed", "created"] }, { kind: "component", type: StageDialogComponent, selector: "lib-stage-dialog", inputs: ["visible", "stageData"], outputs: ["closed", "saved"] }] });
|
|
5626
|
+
}
|
|
5627
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: WorkflowDesignerComponent, decorators: [{
|
|
5628
|
+
type: Component,
|
|
5629
|
+
args: [{ selector: 'lib-workflow-designer', template: "<div class=\"workflow-designer\">\n <div class=\"workflow-header\">\n <h1>Workflow Designer</h1>\n </div>\n\n <!-- Toolbar Component -->\n <lib-designer-toolbar\n [selectedTool]=\"selectedTool\"\n [isSaving]=\"isSaving\"\n (toolSelected)=\"onToolSelected($event)\"\n (saveWorkflow)=\"saveWorkflow()\"\n >\n </lib-designer-toolbar>\n\n <!-- Show loading indicator if needed -->\n <div *ngIf=\"isLoading\" class=\"loading-overlay\">\n <div class=\"loading-spinner\"></div>\n <div class=\"loading-text\">Loading workflow...</div>\n </div>\n\n <!-- Canvas Component -->\n <lib-designer-canvas\n [selectedTool]=\"selectedTool\"\n (clickedPosition)=\"handleCanvasPositionClick($event)\"\n (subflowSelected)=\"onSubflowSelected()\"\n (showStageDialog)=\"onShowStageDialog($event)\"\n #canvasRef\n >\n </lib-designer-canvas>\n\n <lib-swimlane-dialog\n [visible]=\"showSwimlaneDialog\"\n [swimlaneData]=\"\n editingSwimlaneIndex !== null\n ? {\n name: state.swimlanes[editingSwimlaneIndex].label,\n tags: state.swimlanes[editingSwimlaneIndex].tags\n }\n : null\n \"\n (created)=\"onSwimlaneDialogFilled($event)\"\n (closed)=\"showSwimlaneDialog = false\"\n ></lib-swimlane-dialog>\n\n <lib-stage-dialog\n [visible]=\"showStageDialog\"\n [stageData]=\"pendingStageData\"\n (closed)=\"showStageDialog = false\"\n (saved)=\"onStageDialogSaved($event)\"\n ></lib-stage-dialog>\n</div>\n", styles: [".workflow-designer{display:flex;flex-direction:column;height:100vh;background-color:#f8fafc}.workflow-header{padding:1rem;background-color:#fff;border-bottom:1px solid #e2e8f0}.workflow-header h1{font-size:1.25rem;font-weight:600;color:#334155;margin:0}.loading-overlay{position:absolute;inset:0;background-color:#ffffffb3;display:flex;flex-direction:column;justify-content:center;align-items:center;z-index:1000}.loading-spinner{border:4px solid #f3f3f3;border-top:4px solid #d8b4fe;border-radius:50%;width:40px;height:40px;animation:spin 1s linear infinite}.loading-text{margin-top:1rem;font-size:1rem;color:#7e22ce}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"] }]
|
|
5630
|
+
}], ctorParameters: () => [{ type: WorkflowDesignerState }, { type: WorkflowDataService }], propDecorators: { canvasRef: [{
|
|
5631
|
+
type: ViewChild,
|
|
5632
|
+
args: ['canvasRef']
|
|
5633
|
+
}], workflowCode: [{
|
|
5634
|
+
type: Input
|
|
5635
|
+
}] } });
|
|
5636
|
+
|
|
5637
|
+
let ConnectionService$1 = class ConnectionService {
|
|
5638
|
+
state;
|
|
5639
|
+
nodeService;
|
|
5640
|
+
draggingConnectionData = {};
|
|
5641
|
+
// The rules defining which node types can connect to which other node types
|
|
5642
|
+
connectionRules = {
|
|
5643
|
+
stage: ['stage', 'decision', 'subflow', 'form'],
|
|
5644
|
+
decision: ['stage'], // Decisions can only connect to Stages
|
|
5645
|
+
form: [],
|
|
5646
|
+
subflow: ['stage', 'decision'],
|
|
5647
|
+
};
|
|
5648
|
+
constructor(state, nodeService) {
|
|
5649
|
+
this.state = state;
|
|
5650
|
+
this.nodeService = nodeService;
|
|
5651
|
+
}
|
|
5652
|
+
/**
|
|
5653
|
+
* Start a new connection drag operation
|
|
5654
|
+
*/
|
|
5655
|
+
startConnectionDrag(point, swimlaneIndex, globalX, globalY) {
|
|
5656
|
+
this.draggingConnectionData = {
|
|
5657
|
+
sourcePoint: point,
|
|
5658
|
+
sourceSwimlaneIndex: swimlaneIndex,
|
|
5659
|
+
startX: globalX,
|
|
5660
|
+
startY: globalY,
|
|
5661
|
+
currentX: globalX,
|
|
5662
|
+
currentY: globalY,
|
|
5663
|
+
};
|
|
5664
|
+
}
|
|
5665
|
+
/**
|
|
5666
|
+
* Update the position of the dragging connection
|
|
5667
|
+
*/
|
|
5668
|
+
updateConnectionDrag(currentX, currentY) {
|
|
5669
|
+
if (this.draggingConnectionData.sourcePoint) {
|
|
5670
|
+
this.draggingConnectionData.currentX = currentX;
|
|
5671
|
+
this.draggingConnectionData.currentY = currentY;
|
|
5672
|
+
}
|
|
5673
|
+
}
|
|
5674
|
+
/**
|
|
5675
|
+
* End the current connection drag operation
|
|
5676
|
+
*/
|
|
5677
|
+
endConnectionDrag() {
|
|
5678
|
+
this.draggingConnectionData = {};
|
|
4092
5679
|
}
|
|
4093
|
-
|
|
4094
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: DesignerCanvasComponent, selector: "lib-designer-canvas", inputs: { selectedTool: "selectedTool" }, outputs: { clickedPosition: "clickedPosition", subflowSelected: "subflowSelected", showStageDialog: "showStageDialog" }, host: { listeners: { "window:mouseup": "onWindowMouseUp($event)", "document:click": "onDocumentClick($event)" } }, viewQueries: [{ propertyName: "canvasRef", first: true, predicate: ["canvas"], descendants: true, static: true }], ngImport: i0, template: "<div class=\"canvas-container\">\n <svg\n #canvas\n [attr.width]=\"canvasWidth\"\n [attr.height]=\"canvasHeight\"\n class=\"designer-canvas\"\n (click)=\"onCanvasClick($event)\"\n >\n <defs>\n <!-- Grid pattern definition -->\n\n <pattern\n id=\"grid\"\n [attr.width]=\"gridSize\"\n [attr.height]=\"gridSize\"\n patternUnits=\"userSpaceOnUse\"\n >\n <path\n d=\"M 20 0 L 0 0 0 20\"\n fill=\"none\"\n stroke=\"#e2e8f0\"\n stroke-width=\"0.5\"\n />\n </pattern>\n\n <!-- Arrow head marker definition -->\n <marker\n id=\"arrowhead\"\n markerWidth=\"10\"\n markerHeight=\"7\"\n refX=\"9\"\n refY=\"3.5\"\n orient=\"auto\"\n >\n <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"#D36CFF\" />\n </marker>\n\n <!-- Connection point styles -->\n <circle\n id=\"connection-point-template\"\n r=\"5\"\n fill=\"#D36CFF\"\n stroke=\"#FFFFFF\"\n stroke-width=\"1\"\n />\n\n <!-- Dashed line style for connection preview -->\n <pattern\n id=\"dashed-line\"\n width=\"10\"\n height=\"10\"\n patternUnits=\"userSpaceOnUse\"\n >\n <line\n x1=\"0\"\n y1=\"5\"\n x2=\"10\"\n y2=\"5\"\n stroke=\"#D36CFF\"\n stroke-width=\"2\"\n stroke-dasharray=\"5,5\"\n />\n </pattern>\n </defs>\n\n <!-- Background grid -->\n <rect width=\"100%\" height=\"100%\" fill=\"url(#grid)\" />\n\n <!-- Display a message when no swimlanes exist -->\n @if (state.swimlanes.length === 0) {\n <text\n x=\"50%\"\n y=\"50%\"\n font-family=\"sans-serif\"\n font-size=\"16\"\n fill=\"#94a3b8\"\n text-anchor=\"middle\"\n >\n Select the Swimlane tool and click on the canvas to add a swimlane\n </text>\n }\n\n <!-- This is where workflow elements will be added later -->\n @for (swimlane of state.swimlanes; track swimlane.order) {\n <g [attr.transform]=\"'translate(0,' + swimlane.order * 263 + ')'\">\n <!-- Swimlane container -->\n <rect\n x=\"0\"\n y=\"0\"\n [attr.width]=\"canvasWidth\"\n [attr.height]=\"263\"\n fill=\"#ffffff\"\n stroke=\"#e2e8f0\"\n stroke-width=\"1\"\n ></rect>\n\n <!-- Swimlane header -->\n <rect\n x=\"0\"\n y=\"0\"\n [attr.width]=\"canvasWidth\"\n height=\"40\"\n fill=\"#f8fafc\"\n stroke=\"#e2e8f0\"\n stroke-width=\"1\"\n ></rect>\n\n <!-- Swimlane label -->\n <text\n x=\"20\"\n y=\"25\"\n font-family=\"sans-serif\"\n font-size=\"14\"\n fill=\"#333333\"\n font-weight=\"bold\"\n >\n {{ swimlane.label }}\n </text>\n\n <!-- Edit button -->\n <g\n class=\"edit-swimlane-button\"\n [attr.transform]=\"'translate(200, 20)'\"\n (click)=\"\n onEditSwimlane($event, swimlane, swimlane.order);\n $event.stopPropagation()\n \"\n >\n <rect\n x=\"-5\"\n y=\"-15\"\n width=\"40\"\n height=\"20\"\n fill=\"#f3e8ff\"\n rx=\"3\"\n ry=\"3\"\n stroke=\"#d8b4fe\"\n stroke-width=\"1\"\n ></rect>\n <text\n x=\"15\"\n y=\"0\"\n font-family=\"sans-serif\"\n font-size=\"12\"\n fill=\"#7e22ce\"\n text-anchor=\"middle\"\n >\n Edit\n </text>\n </g>\n\n <!-- Tag indicators -->\n <g [attr.transform]=\"'translate(200, 20)'\">\n @for (tag of swimlane.tags.slice(0, 3); track tag.Name; let i = $index)\n {\n <text\n [attr.x]=\"i * 100\"\n y=\"5\"\n font-family=\"sans-serif\"\n font-size=\"12\"\n fill=\"#666666\"\n >\n {{ tag.Name }}\n </text>\n } @if (swimlane.tags.length > 3) {\n <text\n [attr.x]=\"3 * 100 + 10\"\n y=\"5\"\n font-family=\"sans-serif\"\n font-size=\"12\"\n fill=\"#666666\"\n >\n +{{ swimlane.tags.length - 3 }} more\n </text>\n }\n </g>\n\n <!-- Render nodes in this swimlane -->\n @for (node of swimlane.nodes; track node.id) {\n <g\n [attr.transform]=\"'translate(' + node.x + ',' + (node.y + 40) + ')'\"\n (mouseenter)=\"showConnectionPoints(node.id, swimlane.order)\"\n (mouseleave)=\"hideConnectionPoints(node.id)\"\n >\n <!-- Start node indicator (circle and arrow) for the first node -->\n @if (node.isStartNode) {\n <!-- Circle -->\n <circle\n cx=\"-30\"\n cy=\"50\"\n r=\"15\"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n ></circle>\n <!-- Form icon for start node -->\n <svg:g\n (click)=\"\n toggleStartNodeFormPopup($event, node.x, node.y, swimlane.order)\n \"\n class=\"stage-icon\"\n transform=\"translate(-45, 42)\"\n style=\"cursor: pointer; pointer-events: all\"\n >\n <!-- Transparent rectangle to capture clicks -->\n <svg:rect\n x=\"-2\"\n y=\"-2\"\n width=\"24\"\n height=\"24\"\n fill=\"transparent\"\n style=\"cursor: pointer\"\n />\n <svg:path\n d=\"M16.5 20.475V17.475H13.5V16.475H16.5V13.475H17.5V16.475H20.5V17.475H17.5V20.475H16.5ZM3.5 17.5V16.5H4.5V17.5H3.5ZM6.5 17.5V16.5H11.517C11.5057 16.6767 11.5043 16.845 11.513 17.005C11.521 17.165 11.531 17.33 11.543 17.5H6.5ZM3.5 13.5V12.5H4.5V13.5H3.5ZM6.5 13.5V12.5H13.804C13.6127 12.6387 13.4333 12.7913 13.266 12.958C13.0993 13.1247 12.9377 13.3053 12.781 13.5H6.5ZM3.5 9.5V8.5H4.5V9.5H3.5ZM6.5 9.5V8.5H18.5V9.5H6.5ZM3.5 5.5V4.5H4.5V5.5H3.5ZM6.5 5.5V4.5H18.5V5.5H6.5Z\"\n [attr.fill]=\"state.workflowFormId ? '#D36CFF' : 'black'\"\n transform=\"scale(0.7)\"\n />\n </svg:g>\n <!-- Arrow from circle to node -->\n <path\n d=\"M -20 50 L 0 50\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n marker-end=\"url(#arrowhead)\"\n ></path>\n }\n\n <!-- Stage node -->\n @if (node.type === 'stage') {\n <!-- <rect\n x=\"0\"\n y=\"0\"\n [attr.width]=\"node.width\"\n [attr.height]=\"node.height\"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n ></rect> -->\n <svg:g\n lib-stage-node\n [node]=\"node\"\n [isStartNode]=\"node.isStartNode\"\n (stagePropertiesUpdated)=\"onStagePropertiesUpdated($event)\"\n ></svg:g>\n }\n\n <!-- Decision node -->\n @if (node.type === 'decision') {\n <path\n [attr.d]=\"\n 'M 0 ' +\n node.height / 2 +\n ' L ' +\n node.width / 2 +\n ' 0' +\n ' L ' +\n node.width +\n ' ' +\n node.height / 2 +\n ' L ' +\n node.width / 2 +\n ' ' +\n node.height +\n ' Z'\n \"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n ></path>\n }\n\n <!-- Form node -->\n @if (node.type === 'form') {\n <path\n d=\"M95.0625 50.5591V95.0625C95.0625 97.9716 93.9069 100.762 91.8498 102.819C89.7928 104.876 87.0028 106.031 84.0938 106.031H32.9062C29.9972 106.031 27.2072 104.876 25.1502 102.819C23.0931 100.762 21.9375 97.9716 21.9375 95.0625V21.9375C21.9375 19.0284 23.0931 16.2385 25.1502 14.1814C27.2072 12.1244 29.9972 10.9688 32.9062 10.9688H55.4722C57.4109 10.969 59.2701 11.7392 60.6412 13.1099L92.9213 45.3901C94.292 46.7611 95.0622 48.6204 95.0625 50.5591Z\"\n transform=\"scale(0.7)\"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-linejoin=\"round\"\n />\n <path\n d=\"M58.5 12.7969V40.2188C58.5 42.1581 59.2704 44.0181 60.6418 45.3895C62.0131 46.7608 63.8731 47.5312 65.8125 47.5312H93.2344\"\n transform=\"scale(0.7)\"\n stroke=\"#D36CFF\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n <text\n x=\"50%\"\n y=\"50%\"\n text-anchor=\"middle\"\n dominant-baseline=\"middle\"\n font-family=\"sans-serif\"\n font-size=\"14\"\n fill=\"#D36CFF\"\n >\n Form\n </text>\n }\n\n <!-- Subflow node -->\n @if (node.type === 'subflow') {\n <path\n [attr.d]=\"\n 'M 1 ' +\n node.height / 4 +\n ' L ' +\n node.width / 2 +\n ' 1 L ' +\n (node.width - 1) +\n ' ' +\n node.height / 4 +\n ' V ' +\n (node.height * 3) / 4 +\n ' L ' +\n node.width / 2 +\n ' ' +\n (node.height - 1) +\n ' L 1 ' +\n (node.height * 3) / 4 +\n ' Z'\n \"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n ></path>\n <text\n x=\"50%\"\n y=\"50%\"\n text-anchor=\"middle\"\n dominant-baseline=\"middle\"\n font-family=\"sans-serif\"\n font-size=\"14\"\n fill=\"#000000\"\n >\n {{ node.workflowData?.name || \"Subflow\" }}\n </text>\n }\n\n <!-- Connection points for this node -->\n @for (point of node.connectionPoints || []; track point.id) { @if\n (isConnectionPointVisible(node.id)) {\n <use\n [attr.href]=\"'#connection-point-template'\"\n [attr.x]=\"point.x\"\n [attr.y]=\"point.y\"\n [attr.id]=\"point.id\"\n (mousedown)=\"startConnectionDrag($event, point, swimlane.order)\"\n />\n } }\n </g>\n <!-- A transparent hover area for improved hover detection -->\n <rect\n [attr.x]=\"node.x - 10\"\n [attr.y]=\"node.y + 40 - 10\"\n [attr.width]=\"node.width + 20\"\n [attr.height]=\"node.height + 20\"\n fill=\"transparent\"\n (mouseover)=\"showConnectionPoints(node.id, swimlane.order)\"\n (mouseout)=\"hideConnectionPoints(node.id)\"\n style=\"pointer-events: none\"\n />\n }\n </g>\n } @for (connection of state.connections; track connection.id) {\n <g>\n <path\n [attr.d]=\"getConnectionPathForSavedConnection(connection)\"\n stroke=\"#D36CFF\"\n stroke-width=\"2\"\n fill=\"none\"\n marker-end=\"url(#arrowhead)\"\n ></path>\n </g>\n }\n\n <!-- Connection preview line -->\n @if (state.isConnectionDragging()) {\n <g>\n <path\n [attr.d]=\"getConnectionPath()\"\n stroke=\"#D36CFF\"\n stroke-width=\"2\"\n stroke-dasharray=\"5,5\"\n fill=\"none\"\n ></path>\n </g>\n }\n </svg>\n\n <div\n *ngIf=\"popupVisible\"\n [style.position]=\"'absolute'\"\n [style.left.px]=\"popupX\"\n [style.top.px]=\"popupY\"\n >\n <verben-pop-Up\n [dropdownOpen]=\"true\"\n [customStyles]=\"{ 'z-index': '99' }\"\n [enableMouseLeave]=\"false\"\n >\n <div dropdown-trigger style=\"display: none\">\n <!-- Hidden trigger element -->\n </div>\n <div\n class=\"p-3 bg-white border border-gray-200 rounded shadow-md\"\n dropdown-content\n >\n <h4 class=\"mb-2 font-medium\">Create Connection</h4>\n <div class=\"flex flex-col gap-2\">\n <ng-container *ngFor=\"let type of allowedNodeTypes\">\n <button\n class=\"px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50\"\n (click)=\"createNodeConnection(type)\"\n >\n Create {{ type | titlecase }}\n </button>\n </ng-container>\n <button\n class=\"px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50\"\n (click)=\"hideConnectionPopup()\"\n >\n Cancel\n </button>\n </div>\n </div>\n </verben-pop-Up>\n </div>\n\n <div\n *ngIf=\"showStartNodeFormPopup\"\n [style.position]=\"'absolute'\"\n [style.left.px]=\"startNodeFormPopupX\"\n [style.top.px]=\"startNodeFormPopupY\"\n >\n <verben-pop-Up [dropdownOpen]=\"true\" [customStyles]=\"{ 'z-index': '100' }\">\n <div dropdown-trigger style=\"display: none\">\n <!-- Hidden trigger element -->\n </div>\n <div\n class=\"p-3 bg-white border border-gray-200 rounded shadow-md w-64\"\n dropdown-content\n style=\"background-color: white; z-index: 1000\"\n >\n <h4 class=\"mb-2 font-medium\">Select Workflow Form</h4>\n <div *ngIf=\"isLoadingStartNodeForms\" class=\"text-center py-2\">\n Loading forms...\n </div>\n <div *ngIf=\"!isLoadingStartNodeForms\" class=\"max-h-48 overflow-y-auto\">\n <div class=\"mb-2\">\n <button\n class=\"w-full px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50 text-left\"\n (click)=\"selectStartNodeForm(null)\"\n >\n Clear form selection\n </button>\n </div>\n <div *ngFor=\"let form of startNodeFormsList\" class=\"mb-1\">\n <button\n class=\"w-full px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50 text-left\"\n (click)=\"selectStartNodeForm(form)\"\n >\n {{ form.Name }}\n </button>\n </div>\n <div\n *ngIf=\"startNodeFormsList.length === 0\"\n class=\"text-center py-2 text-gray-500\"\n >\n No forms available\n </div>\n </div>\n </div>\n </verben-pop-Up>\n </div>\n\n <div\n *ngIf=\"showSubflowPopup\"\n [style.position]=\"'fiabsolutexed'\"\n [style.left.px]=\"subflowPopupX\"\n [style.top.px]=\"subflowPopupY\"\n >\n <verben-pop-Up [dropdownOpen]=\"true\" [customStyles]=\"{ 'z-index': '100' }\">\n <div dropdown-trigger style=\"display: none\">\n <!-- Hidden trigger element -->\n </div>\n <div\n class=\"p-3 bg-white border border-gray-200 rounded shadow-md w-64\"\n dropdown-content\n style=\"background-color: white; z-index: 1000\"\n >\n <h4 class=\"mb-2 font-medium\">Select Workflow</h4>\n <div *ngIf=\"isLoadingWorkflows\" class=\"text-center py-2\">\n Loading workflows...\n </div>\n <div *ngIf=\"!isLoadingWorkflows\" class=\"max-h-48 overflow-y-auto\">\n <div *ngFor=\"let workflow of workflowsList\" class=\"mb-1\">\n <button\n class=\"w-full px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50 text-left\"\n (click)=\"selectSubflowWorkflow(workflow)\"\n >\n {{ workflow.Name }}\n </button>\n </div>\n <div\n *ngIf=\"workflowsList.length === 0\"\n class=\"text-center py-2 text-gray-500\"\n >\n No workflows available\n </div>\n </div>\n </div>\n </verben-pop-Up>\n </div>\n</div>\n", styles: [".canvas-container{flex:1;overflow:auto;background-color:#fff}.designer-canvas{min-width:100%;min-height:100%;cursor:default}.designer-canvas:focus{outline:none}.edit-swimlane-button{cursor:pointer}.edit-swimlane-button:hover rect{fill:#ddd6fe}\n"], dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i8.VerbenPopUpComponent, selector: "verben-pop-Up", inputs: ["dropdownOpen", "dropdownWidth", "color", "customStyles", "popUpClass", "border", "borderRadius", "enableMouseLeave"], outputs: ["dropdownOpenChange", "close"] }, { kind: "component", type: StageNodeComponent, selector: "svg:g[lib-stage-node]", inputs: ["node", "isStartNode", "stageData"], outputs: ["stagePropertiesUpdated", "parallelExecutionToggled"] }, { kind: "pipe", type: i1$1.TitleCasePipe, name: "titlecase" }] });
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
args: [{ selector: 'lib-designer-canvas', template: "<div class=\"canvas-container\">\n <svg\n #canvas\n [attr.width]=\"canvasWidth\"\n [attr.height]=\"canvasHeight\"\n class=\"designer-canvas\"\n (click)=\"onCanvasClick($event)\"\n >\n <defs>\n <!-- Grid pattern definition -->\n\n <pattern\n id=\"grid\"\n [attr.width]=\"gridSize\"\n [attr.height]=\"gridSize\"\n patternUnits=\"userSpaceOnUse\"\n >\n <path\n d=\"M 20 0 L 0 0 0 20\"\n fill=\"none\"\n stroke=\"#e2e8f0\"\n stroke-width=\"0.5\"\n />\n </pattern>\n\n <!-- Arrow head marker definition -->\n <marker\n id=\"arrowhead\"\n markerWidth=\"10\"\n markerHeight=\"7\"\n refX=\"9\"\n refY=\"3.5\"\n orient=\"auto\"\n >\n <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"#D36CFF\" />\n </marker>\n\n <!-- Connection point styles -->\n <circle\n id=\"connection-point-template\"\n r=\"5\"\n fill=\"#D36CFF\"\n stroke=\"#FFFFFF\"\n stroke-width=\"1\"\n />\n\n <!-- Dashed line style for connection preview -->\n <pattern\n id=\"dashed-line\"\n width=\"10\"\n height=\"10\"\n patternUnits=\"userSpaceOnUse\"\n >\n <line\n x1=\"0\"\n y1=\"5\"\n x2=\"10\"\n y2=\"5\"\n stroke=\"#D36CFF\"\n stroke-width=\"2\"\n stroke-dasharray=\"5,5\"\n />\n </pattern>\n </defs>\n\n <!-- Background grid -->\n <rect width=\"100%\" height=\"100%\" fill=\"url(#grid)\" />\n\n <!-- Display a message when no swimlanes exist -->\n @if (state.swimlanes.length === 0) {\n <text\n x=\"50%\"\n y=\"50%\"\n font-family=\"sans-serif\"\n font-size=\"16\"\n fill=\"#94a3b8\"\n text-anchor=\"middle\"\n >\n Select the Swimlane tool and click on the canvas to add a swimlane\n </text>\n }\n\n <!-- This is where workflow elements will be added later -->\n @for (swimlane of state.swimlanes; track swimlane.order) {\n <g [attr.transform]=\"'translate(0,' + swimlane.order * 263 + ')'\">\n <!-- Swimlane container -->\n <rect\n x=\"0\"\n y=\"0\"\n [attr.width]=\"canvasWidth\"\n [attr.height]=\"263\"\n fill=\"#ffffff\"\n stroke=\"#e2e8f0\"\n stroke-width=\"1\"\n ></rect>\n\n <!-- Swimlane header -->\n <rect\n x=\"0\"\n y=\"0\"\n [attr.width]=\"canvasWidth\"\n height=\"40\"\n fill=\"#f8fafc\"\n stroke=\"#e2e8f0\"\n stroke-width=\"1\"\n ></rect>\n\n <!-- Swimlane label -->\n <text\n x=\"20\"\n y=\"25\"\n font-family=\"sans-serif\"\n font-size=\"14\"\n fill=\"#333333\"\n font-weight=\"bold\"\n >\n {{ swimlane.label }}\n </text>\n\n <!-- Edit button -->\n <g\n class=\"edit-swimlane-button\"\n [attr.transform]=\"'translate(200, 20)'\"\n (click)=\"\n onEditSwimlane($event, swimlane, swimlane.order);\n $event.stopPropagation()\n \"\n >\n <rect\n x=\"-5\"\n y=\"-15\"\n width=\"40\"\n height=\"20\"\n fill=\"#f3e8ff\"\n rx=\"3\"\n ry=\"3\"\n stroke=\"#d8b4fe\"\n stroke-width=\"1\"\n ></rect>\n <text\n x=\"15\"\n y=\"0\"\n font-family=\"sans-serif\"\n font-size=\"12\"\n fill=\"#7e22ce\"\n text-anchor=\"middle\"\n >\n Edit\n </text>\n </g>\n\n <!-- Tag indicators -->\n <g [attr.transform]=\"'translate(200, 20)'\">\n @for (tag of swimlane.tags.slice(0, 3); track tag.Name; let i = $index)\n {\n <text\n [attr.x]=\"i * 100\"\n y=\"5\"\n font-family=\"sans-serif\"\n font-size=\"12\"\n fill=\"#666666\"\n >\n {{ tag.Name }}\n </text>\n } @if (swimlane.tags.length > 3) {\n <text\n [attr.x]=\"3 * 100 + 10\"\n y=\"5\"\n font-family=\"sans-serif\"\n font-size=\"12\"\n fill=\"#666666\"\n >\n +{{ swimlane.tags.length - 3 }} more\n </text>\n }\n </g>\n\n <!-- Render nodes in this swimlane -->\n @for (node of swimlane.nodes; track node.id) {\n <g\n [attr.transform]=\"'translate(' + node.x + ',' + (node.y + 40) + ')'\"\n (mouseenter)=\"showConnectionPoints(node.id, swimlane.order)\"\n (mouseleave)=\"hideConnectionPoints(node.id)\"\n >\n <!-- Start node indicator (circle and arrow) for the first node -->\n @if (node.isStartNode) {\n <!-- Circle -->\n <circle\n cx=\"-30\"\n cy=\"50\"\n r=\"15\"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n ></circle>\n <!-- Form icon for start node -->\n <svg:g\n (click)=\"\n toggleStartNodeFormPopup($event, node.x, node.y, swimlane.order)\n \"\n class=\"stage-icon\"\n transform=\"translate(-45, 42)\"\n style=\"cursor: pointer; pointer-events: all\"\n >\n <!-- Transparent rectangle to capture clicks -->\n <svg:rect\n x=\"-2\"\n y=\"-2\"\n width=\"24\"\n height=\"24\"\n fill=\"transparent\"\n style=\"cursor: pointer\"\n />\n <svg:path\n d=\"M16.5 20.475V17.475H13.5V16.475H16.5V13.475H17.5V16.475H20.5V17.475H17.5V20.475H16.5ZM3.5 17.5V16.5H4.5V17.5H3.5ZM6.5 17.5V16.5H11.517C11.5057 16.6767 11.5043 16.845 11.513 17.005C11.521 17.165 11.531 17.33 11.543 17.5H6.5ZM3.5 13.5V12.5H4.5V13.5H3.5ZM6.5 13.5V12.5H13.804C13.6127 12.6387 13.4333 12.7913 13.266 12.958C13.0993 13.1247 12.9377 13.3053 12.781 13.5H6.5ZM3.5 9.5V8.5H4.5V9.5H3.5ZM6.5 9.5V8.5H18.5V9.5H6.5ZM3.5 5.5V4.5H4.5V5.5H3.5ZM6.5 5.5V4.5H18.5V5.5H6.5Z\"\n [attr.fill]=\"state.workflowFormId ? '#D36CFF' : 'black'\"\n transform=\"scale(0.7)\"\n />\n </svg:g>\n <!-- Arrow from circle to node -->\n <path\n d=\"M -20 50 L 0 50\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n marker-end=\"url(#arrowhead)\"\n ></path>\n }\n\n <!-- Stage node -->\n @if (node.type === 'stage') {\n <!-- <rect\n x=\"0\"\n y=\"0\"\n [attr.width]=\"node.width\"\n [attr.height]=\"node.height\"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n ></rect> -->\n <svg:g\n lib-stage-node\n [node]=\"node\"\n [isStartNode]=\"node.isStartNode\"\n (stagePropertiesUpdated)=\"onStagePropertiesUpdated($event)\"\n ></svg:g>\n }\n\n <!-- Decision node -->\n @if (node.type === 'decision') {\n <path\n [attr.d]=\"\n 'M 0 ' +\n node.height / 2 +\n ' L ' +\n node.width / 2 +\n ' 0' +\n ' L ' +\n node.width +\n ' ' +\n node.height / 2 +\n ' L ' +\n node.width / 2 +\n ' ' +\n node.height +\n ' Z'\n \"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n ></path>\n }\n\n <!-- Form node -->\n @if (node.type === 'form') {\n <path\n d=\"M95.0625 50.5591V95.0625C95.0625 97.9716 93.9069 100.762 91.8498 102.819C89.7928 104.876 87.0028 106.031 84.0938 106.031H32.9062C29.9972 106.031 27.2072 104.876 25.1502 102.819C23.0931 100.762 21.9375 97.9716 21.9375 95.0625V21.9375C21.9375 19.0284 23.0931 16.2385 25.1502 14.1814C27.2072 12.1244 29.9972 10.9688 32.9062 10.9688H55.4722C57.4109 10.969 59.2701 11.7392 60.6412 13.1099L92.9213 45.3901C94.292 46.7611 95.0622 48.6204 95.0625 50.5591Z\"\n transform=\"scale(0.7)\"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-linejoin=\"round\"\n />\n <path\n d=\"M58.5 12.7969V40.2188C58.5 42.1581 59.2704 44.0181 60.6418 45.3895C62.0131 46.7608 63.8731 47.5312 65.8125 47.5312H93.2344\"\n transform=\"scale(0.7)\"\n stroke=\"#D36CFF\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n <text\n x=\"50%\"\n y=\"50%\"\n text-anchor=\"middle\"\n dominant-baseline=\"middle\"\n font-family=\"sans-serif\"\n font-size=\"14\"\n fill=\"#D36CFF\"\n >\n Form\n </text>\n }\n\n <!-- Subflow node -->\n @if (node.type === 'subflow') {\n <path\n [attr.d]=\"\n 'M 1 ' +\n node.height / 4 +\n ' L ' +\n node.width / 2 +\n ' 1 L ' +\n (node.width - 1) +\n ' ' +\n node.height / 4 +\n ' V ' +\n (node.height * 3) / 4 +\n ' L ' +\n node.width / 2 +\n ' ' +\n (node.height - 1) +\n ' L 1 ' +\n (node.height * 3) / 4 +\n ' Z'\n \"\n fill=\"none\"\n stroke=\"#D36CFF\"\n stroke-width=\"1\"\n ></path>\n <text\n x=\"50%\"\n y=\"50%\"\n text-anchor=\"middle\"\n dominant-baseline=\"middle\"\n font-family=\"sans-serif\"\n font-size=\"14\"\n fill=\"#000000\"\n >\n {{ node.workflowData?.name || \"Subflow\" }}\n </text>\n }\n\n <!-- Connection points for this node -->\n @for (point of node.connectionPoints || []; track point.id) { @if\n (isConnectionPointVisible(node.id)) {\n <use\n [attr.href]=\"'#connection-point-template'\"\n [attr.x]=\"point.x\"\n [attr.y]=\"point.y\"\n [attr.id]=\"point.id\"\n (mousedown)=\"startConnectionDrag($event, point, swimlane.order)\"\n />\n } }\n </g>\n <!-- A transparent hover area for improved hover detection -->\n <rect\n [attr.x]=\"node.x - 10\"\n [attr.y]=\"node.y + 40 - 10\"\n [attr.width]=\"node.width + 20\"\n [attr.height]=\"node.height + 20\"\n fill=\"transparent\"\n (mouseover)=\"showConnectionPoints(node.id, swimlane.order)\"\n (mouseout)=\"hideConnectionPoints(node.id)\"\n style=\"pointer-events: none\"\n />\n }\n </g>\n } @for (connection of state.connections; track connection.id) {\n <g>\n <path\n [attr.d]=\"getConnectionPathForSavedConnection(connection)\"\n stroke=\"#D36CFF\"\n stroke-width=\"2\"\n fill=\"none\"\n marker-end=\"url(#arrowhead)\"\n ></path>\n </g>\n }\n\n <!-- Connection preview line -->\n @if (state.isConnectionDragging()) {\n <g>\n <path\n [attr.d]=\"getConnectionPath()\"\n stroke=\"#D36CFF\"\n stroke-width=\"2\"\n stroke-dasharray=\"5,5\"\n fill=\"none\"\n ></path>\n </g>\n }\n </svg>\n\n <div\n *ngIf=\"popupVisible\"\n [style.position]=\"'absolute'\"\n [style.left.px]=\"popupX\"\n [style.top.px]=\"popupY\"\n >\n <verben-pop-Up\n [dropdownOpen]=\"true\"\n [customStyles]=\"{ 'z-index': '99' }\"\n [enableMouseLeave]=\"false\"\n >\n <div dropdown-trigger style=\"display: none\">\n <!-- Hidden trigger element -->\n </div>\n <div\n class=\"p-3 bg-white border border-gray-200 rounded shadow-md\"\n dropdown-content\n >\n <h4 class=\"mb-2 font-medium\">Create Connection</h4>\n <div class=\"flex flex-col gap-2\">\n <ng-container *ngFor=\"let type of allowedNodeTypes\">\n <button\n class=\"px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50\"\n (click)=\"createNodeConnection(type)\"\n >\n Create {{ type | titlecase }}\n </button>\n </ng-container>\n <button\n class=\"px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50\"\n (click)=\"hideConnectionPopup()\"\n >\n Cancel\n </button>\n </div>\n </div>\n </verben-pop-Up>\n </div>\n\n <div\n *ngIf=\"showStartNodeFormPopup\"\n [style.position]=\"'absolute'\"\n [style.left.px]=\"startNodeFormPopupX\"\n [style.top.px]=\"startNodeFormPopupY\"\n >\n <verben-pop-Up [dropdownOpen]=\"true\" [customStyles]=\"{ 'z-index': '100' }\">\n <div dropdown-trigger style=\"display: none\">\n <!-- Hidden trigger element -->\n </div>\n <div\n class=\"p-3 bg-white border border-gray-200 rounded shadow-md w-64\"\n dropdown-content\n style=\"background-color: white; z-index: 1000\"\n >\n <h4 class=\"mb-2 font-medium\">Select Workflow Form</h4>\n <div *ngIf=\"isLoadingStartNodeForms\" class=\"text-center py-2\">\n Loading forms...\n </div>\n <div *ngIf=\"!isLoadingStartNodeForms\" class=\"max-h-48 overflow-y-auto\">\n <div class=\"mb-2\">\n <button\n class=\"w-full px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50 text-left\"\n (click)=\"selectStartNodeForm(null)\"\n >\n Clear form selection\n </button>\n </div>\n <div *ngFor=\"let form of startNodeFormsList\" class=\"mb-1\">\n <button\n class=\"w-full px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50 text-left\"\n (click)=\"selectStartNodeForm(form)\"\n >\n {{ form.Name }}\n </button>\n </div>\n <div\n *ngIf=\"startNodeFormsList.length === 0\"\n class=\"text-center py-2 text-gray-500\"\n >\n No forms available\n </div>\n </div>\n </div>\n </verben-pop-Up>\n </div>\n\n <div\n *ngIf=\"showSubflowPopup\"\n [style.position]=\"'fiabsolutexed'\"\n [style.left.px]=\"subflowPopupX\"\n [style.top.px]=\"subflowPopupY\"\n >\n <verben-pop-Up [dropdownOpen]=\"true\" [customStyles]=\"{ 'z-index': '100' }\">\n <div dropdown-trigger style=\"display: none\">\n <!-- Hidden trigger element -->\n </div>\n <div\n class=\"p-3 bg-white border border-gray-200 rounded shadow-md w-64\"\n dropdown-content\n style=\"background-color: white; z-index: 1000\"\n >\n <h4 class=\"mb-2 font-medium\">Select Workflow</h4>\n <div *ngIf=\"isLoadingWorkflows\" class=\"text-center py-2\">\n Loading workflows...\n </div>\n <div *ngIf=\"!isLoadingWorkflows\" class=\"max-h-48 overflow-y-auto\">\n <div *ngFor=\"let workflow of workflowsList\" class=\"mb-1\">\n <button\n class=\"w-full px-3 py-2 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50 text-left\"\n (click)=\"selectSubflowWorkflow(workflow)\"\n >\n {{ workflow.Name }}\n </button>\n </div>\n <div\n *ngIf=\"workflowsList.length === 0\"\n class=\"text-center py-2 text-gray-500\"\n >\n No workflows available\n </div>\n </div>\n </div>\n </verben-pop-Up>\n </div>\n</div>\n", styles: [".canvas-container{flex:1;overflow:auto;background-color:#fff}.designer-canvas{min-width:100%;min-height:100%;cursor:default}.designer-canvas:focus{outline:none}.edit-swimlane-button{cursor:pointer}.edit-swimlane-button:hover rect{fill:#ddd6fe}\n"] }]
|
|
4099
|
-
}], ctorParameters: () => [{ type: WorkflowDesignerState }, { type: WorkflowDataService }], propDecorators: { selectedTool: [{
|
|
4100
|
-
type: Input
|
|
4101
|
-
}], clickedPosition: [{
|
|
4102
|
-
type: Output
|
|
4103
|
-
}], subflowSelected: [{
|
|
4104
|
-
type: Output
|
|
4105
|
-
}], showStageDialog: [{
|
|
4106
|
-
type: Output
|
|
4107
|
-
}], canvasRef: [{
|
|
4108
|
-
type: ViewChild,
|
|
4109
|
-
args: ['canvas', { static: true }]
|
|
4110
|
-
}], onWindowMouseUp: [{
|
|
4111
|
-
type: HostListener,
|
|
4112
|
-
args: ['window:mouseup', ['$event']]
|
|
4113
|
-
}], onDocumentClick: [{
|
|
4114
|
-
type: HostListener,
|
|
4115
|
-
args: ['document:click', ['$event']]
|
|
4116
|
-
}] } });
|
|
4117
|
-
|
|
4118
|
-
class SwimlaneDialogComponent {
|
|
4119
|
-
dataService;
|
|
4120
|
-
visible = false;
|
|
4121
|
-
swimlaneData = null;
|
|
4122
|
-
closed = new EventEmitter();
|
|
4123
|
-
created = new EventEmitter();
|
|
4124
|
-
searchQuery = '';
|
|
4125
|
-
workflowName = '';
|
|
4126
|
-
// Track selected tags in a separate array
|
|
4127
|
-
selectedTagNames = [];
|
|
4128
|
-
// Sample tag data - you would likely load this from your service
|
|
4129
|
-
// tags: Tag[] = Array.from({ length: 12 }, (_, i) => ({
|
|
4130
|
-
// Name: `Tags ${i + 1}`,
|
|
4131
|
-
// IsOptional: true,
|
|
4132
|
-
// }));
|
|
4133
|
-
tags = [];
|
|
4134
|
-
constructor(dataService) {
|
|
4135
|
-
this.dataService = dataService;
|
|
5680
|
+
/**
|
|
5681
|
+
* Check if a connection drag is in progress
|
|
5682
|
+
*/
|
|
5683
|
+
isConnectionDragging() {
|
|
5684
|
+
return !!this.draggingConnectionData.sourcePoint;
|
|
4136
5685
|
}
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
this.
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
5686
|
+
/**
|
|
5687
|
+
* Get the current connection path data for rendering
|
|
5688
|
+
*/
|
|
5689
|
+
getConnectionPathData() {
|
|
5690
|
+
if (!this.draggingConnectionData.sourcePoint)
|
|
5691
|
+
return null;
|
|
5692
|
+
const { sourcePoint, sourceSwimlaneIndex, currentX, currentY } = this.draggingConnectionData;
|
|
5693
|
+
const nodeInfo = this.nodeService.findNodeById(sourcePoint.nodeId);
|
|
5694
|
+
if (!nodeInfo || sourceSwimlaneIndex === undefined)
|
|
5695
|
+
return null;
|
|
5696
|
+
const node = nodeInfo.node;
|
|
5697
|
+
// Calculate start point in global coordinates
|
|
5698
|
+
const startX = node.x + sourcePoint.x;
|
|
5699
|
+
const startY = node.y + sourcePoint.y + (sourceSwimlaneIndex * 263 + 40); // Add swimlane offset
|
|
5700
|
+
return {
|
|
5701
|
+
startX,
|
|
5702
|
+
startY,
|
|
5703
|
+
endX: currentX ?? startX,
|
|
5704
|
+
endY: currentY ?? startY,
|
|
5705
|
+
sourceSwimlaneIndex,
|
|
5706
|
+
};
|
|
4155
5707
|
}
|
|
4156
|
-
|
|
4157
|
-
|
|
5708
|
+
/**
|
|
5709
|
+
* Create a new connection between nodes
|
|
5710
|
+
*/
|
|
5711
|
+
createConnection(targetNodeId, targetPointId, targetSwimlaneIndex) {
|
|
5712
|
+
const { sourcePoint, sourceSwimlaneIndex } = this.draggingConnectionData;
|
|
5713
|
+
if (!sourcePoint || sourceSwimlaneIndex === undefined) {
|
|
5714
|
+
console.error('Missing source data for connection');
|
|
5715
|
+
return null;
|
|
5716
|
+
}
|
|
5717
|
+
const connection = {
|
|
5718
|
+
id: `conn-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
|
|
5719
|
+
sourceNodeId: sourcePoint.nodeId,
|
|
5720
|
+
targetNodeId: targetNodeId,
|
|
5721
|
+
sourcePointId: sourcePoint.id,
|
|
5722
|
+
targetPointId: targetPointId,
|
|
5723
|
+
sourceSwimlaneIndex: sourceSwimlaneIndex,
|
|
5724
|
+
targetSwimlaneIndex: targetSwimlaneIndex,
|
|
5725
|
+
};
|
|
5726
|
+
// Add the connection to the state
|
|
5727
|
+
this.state.connections.push(connection);
|
|
5728
|
+
return connection;
|
|
4158
5729
|
}
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
5730
|
+
/**
|
|
5731
|
+
* Get the path data for rendering a saved connection
|
|
5732
|
+
*/
|
|
5733
|
+
getConnectionData(connection) {
|
|
5734
|
+
const sourceNodeInfo = this.nodeService.findNodeById(connection.sourceNodeId);
|
|
5735
|
+
const targetNodeInfo = this.nodeService.findNodeById(connection.targetNodeId);
|
|
5736
|
+
if (!sourceNodeInfo || !targetNodeInfo) {
|
|
5737
|
+
return null;
|
|
4162
5738
|
}
|
|
4163
|
-
|
|
4164
|
-
|
|
5739
|
+
const sourceNode = sourceNodeInfo.node;
|
|
5740
|
+
const targetNode = targetNodeInfo.node;
|
|
5741
|
+
// Find connection points
|
|
5742
|
+
const sourcePoint = sourceNode.connectionPoints?.find((p) => p.id === connection.sourcePointId);
|
|
5743
|
+
const targetPoint = targetNode.connectionPoints?.find((p) => p.id === connection.targetPointId);
|
|
5744
|
+
if (!sourcePoint || !targetPoint) {
|
|
5745
|
+
return null;
|
|
4165
5746
|
}
|
|
5747
|
+
// Calculate global coordinates
|
|
5748
|
+
const startX = sourceNode.x + sourcePoint.x;
|
|
5749
|
+
const startY = sourceNode.y +
|
|
5750
|
+
sourcePoint.y +
|
|
5751
|
+
(connection.sourceSwimlaneIndex * 263 + 40);
|
|
5752
|
+
const endX = targetNode.x + targetPoint.x;
|
|
5753
|
+
const endY = targetNode.y +
|
|
5754
|
+
targetPoint.y +
|
|
5755
|
+
(connection.targetSwimlaneIndex * 263 + 40);
|
|
5756
|
+
return {
|
|
5757
|
+
startX,
|
|
5758
|
+
startY,
|
|
5759
|
+
endX,
|
|
5760
|
+
endY,
|
|
5761
|
+
sourceSwimlaneIndex: connection.sourceSwimlaneIndex,
|
|
5762
|
+
targetSwimlaneIndex: connection.targetSwimlaneIndex,
|
|
5763
|
+
};
|
|
4166
5764
|
}
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
5765
|
+
/**
|
|
5766
|
+
* Calculate a path for orthogonal connection rendering
|
|
5767
|
+
*/
|
|
5768
|
+
getOrthogonalPath(startX, startY, endX, endY, pointType) {
|
|
5769
|
+
// Determine initial direction based on connection point type
|
|
5770
|
+
let path = '';
|
|
5771
|
+
// For side points (left/right), start with horizontal line
|
|
5772
|
+
if (pointType === 'left' || pointType === 'right') {
|
|
5773
|
+
// Calculate horizontal distance
|
|
5774
|
+
const horizontalDist = endX - startX;
|
|
5775
|
+
// If the end point is very close horizontally, use a simple 3-segment path
|
|
5776
|
+
if (Math.abs(horizontalDist) < 30) {
|
|
5777
|
+
const midY = (startY + endY) / 2;
|
|
5778
|
+
path = `M ${startX} ${startY} H ${endX} V ${midY} V ${endY}`;
|
|
5779
|
+
}
|
|
5780
|
+
// Otherwise, create a path with horizontal segment first, then vertical, then horizontal
|
|
5781
|
+
else {
|
|
5782
|
+
path = `M ${startX} ${startY} H ${startX + horizontalDist / 2} V ${endY} H ${endX}`;
|
|
5783
|
+
}
|
|
4170
5784
|
}
|
|
4171
|
-
|
|
5785
|
+
// For top/bottom points, start with vertical line
|
|
5786
|
+
else if (pointType === 'top' || pointType === 'bottom') {
|
|
5787
|
+
// Calculate vertical distance
|
|
5788
|
+
const verticalDist = endY - startY;
|
|
5789
|
+
// If the end point is very close vertically, use a simple 3-segment path
|
|
5790
|
+
if (Math.abs(verticalDist) < 30) {
|
|
5791
|
+
const midX = (startX + endX) / 2;
|
|
5792
|
+
path = `M ${startX} ${startY} V ${endY} H ${midX} H ${endX}`;
|
|
5793
|
+
}
|
|
5794
|
+
// Otherwise, create a path with vertical segment first, then horizontal, then vertical
|
|
5795
|
+
else {
|
|
5796
|
+
path = `M ${startX} ${startY} V ${startY + verticalDist / 2} H ${endX} V ${endY}`;
|
|
5797
|
+
}
|
|
5798
|
+
}
|
|
5799
|
+
return path;
|
|
4172
5800
|
}
|
|
4173
|
-
|
|
4174
|
-
|
|
5801
|
+
/**
|
|
5802
|
+
* Check if a connection is allowed between node types
|
|
5803
|
+
*/
|
|
5804
|
+
canConnect(sourceNodeType, targetNodeType) {
|
|
5805
|
+
return (this.connectionRules[sourceNodeType]?.includes(targetNodeType) || false);
|
|
4175
5806
|
}
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
5807
|
+
/**
|
|
5808
|
+
* Get allowed node types for the current dragging connection
|
|
5809
|
+
*/
|
|
5810
|
+
getAllowedTargetNodeTypes() {
|
|
5811
|
+
if (!this.draggingConnectionData.sourcePoint) {
|
|
5812
|
+
return [];
|
|
4180
5813
|
}
|
|
4181
|
-
|
|
4182
|
-
|
|
5814
|
+
const sourceNodeInfo = this.nodeService.findNodeById(this.draggingConnectionData.sourcePoint.nodeId);
|
|
5815
|
+
if (!sourceNodeInfo) {
|
|
5816
|
+
return [];
|
|
4183
5817
|
}
|
|
5818
|
+
const sourceNodeType = sourceNodeInfo.node.type;
|
|
5819
|
+
return this.connectionRules[sourceNodeType] || [];
|
|
4184
5820
|
}
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
5821
|
+
/**
|
|
5822
|
+
* Delete a connection by ID
|
|
5823
|
+
*/
|
|
5824
|
+
deleteConnection(connectionId) {
|
|
5825
|
+
const index = this.state.connections.findIndex((conn) => conn.id === connectionId);
|
|
5826
|
+
if (index === -1)
|
|
5827
|
+
return false;
|
|
5828
|
+
this.state.connections.splice(index, 1);
|
|
5829
|
+
return true;
|
|
4188
5830
|
}
|
|
4189
|
-
|
|
4190
|
-
|
|
5831
|
+
/**
|
|
5832
|
+
* Delete all connections associated with a node
|
|
5833
|
+
*/
|
|
5834
|
+
deleteConnectionsForNode(nodeId) {
|
|
5835
|
+
this.state.connections = this.state.connections.filter((conn) => conn.sourceNodeId !== nodeId && conn.targetNodeId !== nodeId);
|
|
4191
5836
|
}
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
}
|
|
4198
|
-
const selectedTags = this.tags.filter((tag) => this.selectedTagNames.includes(tag.Name));
|
|
4199
|
-
console.log('Submitting swimlane data:', {
|
|
4200
|
-
name: this.workflowName,
|
|
4201
|
-
tags: selectedTags,
|
|
4202
|
-
});
|
|
4203
|
-
this.created.emit({
|
|
4204
|
-
tags: selectedTags,
|
|
4205
|
-
name: this.workflowName,
|
|
4206
|
-
});
|
|
5837
|
+
/**
|
|
5838
|
+
* Get the current connection drag data
|
|
5839
|
+
*/
|
|
5840
|
+
getConnectionDragData() {
|
|
5841
|
+
return this.draggingConnectionData;
|
|
4207
5842
|
}
|
|
4208
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type:
|
|
4209
|
-
static
|
|
4210
|
-
}
|
|
4211
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type:
|
|
4212
|
-
type:
|
|
4213
|
-
args: [{
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
type: Input
|
|
4218
|
-
}], closed: [{
|
|
4219
|
-
type: Output
|
|
4220
|
-
}], created: [{
|
|
4221
|
-
type: Output
|
|
4222
|
-
}] } });
|
|
5843
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ConnectionService, deps: [{ token: WorkflowDesignerState }, { token: NodeManagementService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
5844
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ConnectionService, providedIn: 'root' });
|
|
5845
|
+
};
|
|
5846
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ConnectionService$1, decorators: [{
|
|
5847
|
+
type: Injectable,
|
|
5848
|
+
args: [{
|
|
5849
|
+
providedIn: 'root',
|
|
5850
|
+
}]
|
|
5851
|
+
}], ctorParameters: () => [{ type: WorkflowDesignerState }, { type: NodeManagementService }] });
|
|
4223
5852
|
|
|
4224
|
-
class
|
|
5853
|
+
class TransformerService {
|
|
4225
5854
|
state;
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
//
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
// New properties for stage dialog
|
|
4233
|
-
showStageDialog = false;
|
|
4234
|
-
pendingStagePosition = null;
|
|
4235
|
-
pendingStageData = {};
|
|
4236
|
-
editingSwimlaneIndex = null;
|
|
4237
|
-
isCreatingStageFromConnection = false;
|
|
4238
|
-
pendingConnectionSourcePoint = null;
|
|
4239
|
-
pendingConnectionSourceSwimlaneIndex = null;
|
|
4240
|
-
isLoading = false;
|
|
4241
|
-
isSaving = false;
|
|
4242
|
-
constructor(state, dataService) {
|
|
5855
|
+
swimlaneService;
|
|
5856
|
+
nodeService;
|
|
5857
|
+
connectionService;
|
|
5858
|
+
// Map to track loaded objects
|
|
5859
|
+
loadedObjectIds = {}; // Format: { id: code }
|
|
5860
|
+
constructor(state, swimlaneService, nodeService, connectionService) {
|
|
4243
5861
|
this.state = state;
|
|
4244
|
-
this.
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
// Load workflow data if code is provided
|
|
4248
|
-
if (this.workflowCode) {
|
|
4249
|
-
this.loadWorkflow(this.workflowCode);
|
|
4250
|
-
}
|
|
5862
|
+
this.swimlaneService = swimlaneService;
|
|
5863
|
+
this.nodeService = nodeService;
|
|
5864
|
+
this.connectionService = connectionService;
|
|
4251
5865
|
}
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
//
|
|
4257
|
-
const
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
this.
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
5866
|
+
/**
|
|
5867
|
+
* Transform the UI model to a Workflow API model
|
|
5868
|
+
*/
|
|
5869
|
+
transformToWorkflowModel() {
|
|
5870
|
+
// Create base workflow object
|
|
5871
|
+
const workflow = {
|
|
5872
|
+
// BaseModel properties
|
|
5873
|
+
Id: this.wasLoadedFromApi(this.state.workflowId || '')
|
|
5874
|
+
? this.state.workflowId || ''
|
|
5875
|
+
: '',
|
|
5876
|
+
Code: this.getCodeForObject(this.state.workflowId || ''),
|
|
5877
|
+
TenantId: '',
|
|
5878
|
+
id: this.wasLoadedFromApi(this.state.workflowId || '')
|
|
5879
|
+
? this.state.workflowId || ''
|
|
5880
|
+
: '',
|
|
5881
|
+
ServiceName: '',
|
|
5882
|
+
CreatedAt: new Date(),
|
|
5883
|
+
UpdatedAt: new Date(),
|
|
5884
|
+
DataState: this.wasLoadedFromApi(this.state.workflowId || '')
|
|
5885
|
+
? ObjectState.Changed
|
|
5886
|
+
: ObjectState.New,
|
|
5887
|
+
// Workflow specific properties
|
|
5888
|
+
Name: 'New Workflow', // Default name
|
|
5889
|
+
Description: '', // Default description
|
|
5890
|
+
StageEntryRule: '',
|
|
5891
|
+
Form: this.state.workflowFormId || undefined,
|
|
5892
|
+
AssignmentType: TaskAssignmentType.AutoRoute,
|
|
5893
|
+
Operation: '',
|
|
5894
|
+
Status: Status.Active,
|
|
5895
|
+
Actions: [],
|
|
5896
|
+
Lanes: [],
|
|
5897
|
+
Stages: [],
|
|
5898
|
+
};
|
|
5899
|
+
// Transform swimlanes to SwimLane[]
|
|
5900
|
+
workflow.Lanes = this.state.swimlanes.map((swimlane, index) => {
|
|
5901
|
+
const laneId = `lane-${index}`;
|
|
5902
|
+
return {
|
|
5903
|
+
// BaseModel properties
|
|
5904
|
+
Id: '',
|
|
5905
|
+
Code: '',
|
|
5906
|
+
TenantId: '',
|
|
5907
|
+
id: laneId,
|
|
5908
|
+
ServiceName: '',
|
|
5909
|
+
CreatedAt: new Date(),
|
|
5910
|
+
UpdatedAt: new Date(),
|
|
5911
|
+
DataState: ObjectState.New,
|
|
5912
|
+
// SwimLane specific properties
|
|
5913
|
+
Workflow: workflow.Id,
|
|
5914
|
+
Tags: swimlane.tags || [],
|
|
5915
|
+
Position: swimlane.order,
|
|
5916
|
+
Coordinates: { X: 0, Y: swimlane.order * 263 },
|
|
5917
|
+
Size: { Width: 3000, Height: 263 },
|
|
5918
|
+
};
|
|
4273
5919
|
});
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
this.
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
5920
|
+
// Transform nodes to WorkflowStage[]
|
|
5921
|
+
const stages = [];
|
|
5922
|
+
this.state.swimlanes.forEach((swimlane, swimlaneIndex) => {
|
|
5923
|
+
swimlane.nodes?.forEach((node) => {
|
|
5924
|
+
if (node.type === 'stage') {
|
|
5925
|
+
// Create a stage from the node
|
|
5926
|
+
const stage = {
|
|
5927
|
+
// BaseModel properties
|
|
5928
|
+
Id: node.id,
|
|
5929
|
+
Code: this.getCodeForObject(node.id),
|
|
5930
|
+
TenantId: '',
|
|
5931
|
+
id: node.id,
|
|
5932
|
+
ServiceName: '',
|
|
5933
|
+
CreatedAt: new Date(),
|
|
5934
|
+
UpdatedAt: new Date(),
|
|
5935
|
+
DataState: this.wasLoadedFromApi(node.id)
|
|
5936
|
+
? ObjectState.Changed
|
|
5937
|
+
: ObjectState.New,
|
|
5938
|
+
// WorkflowStage specific properties
|
|
5939
|
+
Workflow: workflow.Id,
|
|
5940
|
+
Name: node.stageData?.Name || 'Unnamed Stage',
|
|
5941
|
+
Description: node.stageData?.Description || '',
|
|
5942
|
+
Duration: node.stageData?.Duration || 0,
|
|
5943
|
+
PassOnRule: node.stageData?.PassOnRule || '',
|
|
5944
|
+
ActorRule: node.stageData?.ActorRule || StageActorRule.None,
|
|
5945
|
+
MinNoOfActor: node.stageData?.MinNoOfActor || 0,
|
|
5946
|
+
IsParallel: node.stageData?.IsParallel || false,
|
|
5947
|
+
IsEntryPoint: node.isStartNode,
|
|
5948
|
+
IsExitPoint: false, // Will be updated below
|
|
5949
|
+
Tags: node.stageData?.Tags || [],
|
|
5950
|
+
Form: node.stageData?.formId ? node.stageData.formId : '',
|
|
5951
|
+
AllowMultiSubProcess: false,
|
|
5952
|
+
AssignmentType: TaskAssignmentType.AutoRoute,
|
|
5953
|
+
SubWorkFlow: '',
|
|
5954
|
+
SwimLane: workflow.Lanes[swimlaneIndex].Id,
|
|
5955
|
+
Coordinates: { X: node.x, Y: node.y },
|
|
5956
|
+
IsSubProcess: false,
|
|
5957
|
+
Key: node.stageData?.Key || undefined,
|
|
5958
|
+
};
|
|
5959
|
+
stages.push(stage);
|
|
5960
|
+
}
|
|
5961
|
+
});
|
|
5962
|
+
});
|
|
5963
|
+
// Determine which stages are exit points (no outgoing connections)
|
|
5964
|
+
stages.forEach((stage) => {
|
|
5965
|
+
const hasOutgoingConnections = this.state.connections.some((conn) => conn.sourceNodeId === stage.Id);
|
|
5966
|
+
if (!hasOutgoingConnections) {
|
|
5967
|
+
stage.IsExitPoint = true;
|
|
4282
5968
|
}
|
|
4283
|
-
this.isLoading = false;
|
|
4284
|
-
})
|
|
4285
|
-
.catch((error) => {
|
|
4286
|
-
console.error('Error loading workflow:', error);
|
|
4287
|
-
this.isLoading = false;
|
|
4288
5969
|
});
|
|
5970
|
+
// Transform connections to WorkflowAction[]
|
|
5971
|
+
workflow.Actions = this.state.connections.map((conn) => {
|
|
5972
|
+
const sourceNode = this.nodeService.findNodeById(conn.sourceNodeId)?.node;
|
|
5973
|
+
const targetNode = this.nodeService.findNodeById(conn.targetNodeId)?.node;
|
|
5974
|
+
return {
|
|
5975
|
+
// BaseModel properties
|
|
5976
|
+
Id: conn.id,
|
|
5977
|
+
Code: this.getCodeForObject(conn.id),
|
|
5978
|
+
TenantId: '',
|
|
5979
|
+
id: conn.id,
|
|
5980
|
+
ServiceName: '',
|
|
5981
|
+
CreatedAt: new Date(),
|
|
5982
|
+
UpdatedAt: new Date(),
|
|
5983
|
+
DataState: this.wasLoadedFromApi(conn.id)
|
|
5984
|
+
? ObjectState.Changed
|
|
5985
|
+
: ObjectState.New,
|
|
5986
|
+
// WorkflowAction specific properties
|
|
5987
|
+
Workflow: workflow.Id,
|
|
5988
|
+
Name: `Action from ${sourceNode?.stageData?.Name || 'Unknown'} to ${targetNode?.stageData?.Name || 'Unknown'}`,
|
|
5989
|
+
FromStage: conn.sourceNodeId,
|
|
5990
|
+
ToStage: conn.targetNodeId,
|
|
5991
|
+
IsParallel: sourceNode?.stageData?.hasParallel || false,
|
|
5992
|
+
PassOnRule: '', // Optional property
|
|
5993
|
+
};
|
|
5994
|
+
});
|
|
5995
|
+
workflow.Stages = stages;
|
|
5996
|
+
return workflow;
|
|
4289
5997
|
}
|
|
4290
|
-
|
|
5998
|
+
/**
|
|
5999
|
+
* Parse API workflow model and convert to UI model
|
|
6000
|
+
*/
|
|
4291
6001
|
parseWorkflowData(workflow) {
|
|
4292
6002
|
// Clear existing state
|
|
4293
6003
|
this.state.swimlanes = [];
|
|
@@ -4296,21 +6006,27 @@ class WorkflowDesignerComponent {
|
|
|
4296
6006
|
if (workflow.Form) {
|
|
4297
6007
|
this.state.setWorkflowForm(workflow.Form, workflow.FormName || 'Workflow Form');
|
|
4298
6008
|
}
|
|
6009
|
+
// Store the workflow ID
|
|
6010
|
+
this.state.setWorkflowId(workflow.Id);
|
|
6011
|
+
// Register the workflow itself
|
|
6012
|
+
this.registerLoadedObject(workflow.Id, workflow.Code);
|
|
4299
6013
|
// Process swimlanes first
|
|
4300
6014
|
if (workflow.Lanes && workflow.Lanes.length) {
|
|
4301
6015
|
workflow.Lanes.sort((a, b) => a.Position - b.Position).forEach((lane) => {
|
|
4302
|
-
this.
|
|
6016
|
+
this.swimlaneService.addSwimlane(lane.Name || `Lane ${lane.Position}`, lane.Tags || []);
|
|
6017
|
+
// Register loaded lane
|
|
6018
|
+
this.registerLoadedObject(lane.Id, lane.Code);
|
|
4303
6019
|
});
|
|
4304
6020
|
}
|
|
4305
6021
|
// Process stages
|
|
4306
6022
|
if (workflow.Stages && workflow.Stages.length) {
|
|
4307
6023
|
workflow.Stages.forEach((stage) => {
|
|
4308
6024
|
// Find swimlane index
|
|
4309
|
-
const swimlaneIndex = this.findSwimlaneIndexByLaneId(stage.SwimLane);
|
|
6025
|
+
const swimlaneIndex = this.swimlaneService.findSwimlaneIndexByLaneId(stage.SwimLane);
|
|
4310
6026
|
if (swimlaneIndex !== -1) {
|
|
4311
6027
|
const x = stage.Coordinates?.X || 100;
|
|
4312
6028
|
const y = stage.Coordinates?.Y || 50;
|
|
4313
|
-
this.
|
|
6029
|
+
this.nodeService.addNode(swimlaneIndex, 'stage', x, y, {
|
|
4314
6030
|
Name: stage.Name,
|
|
4315
6031
|
Description: stage.Description,
|
|
4316
6032
|
Duration: stage.Duration,
|
|
@@ -4323,6 +6039,8 @@ class WorkflowDesignerComponent {
|
|
|
4323
6039
|
IsExitPoint: stage.IsExitPoint,
|
|
4324
6040
|
Id: stage.Id,
|
|
4325
6041
|
});
|
|
6042
|
+
// Register loaded stage
|
|
6043
|
+
this.registerLoadedObject(stage.Id, stage.Code);
|
|
4326
6044
|
}
|
|
4327
6045
|
});
|
|
4328
6046
|
}
|
|
@@ -4330,8 +6048,8 @@ class WorkflowDesignerComponent {
|
|
|
4330
6048
|
if (workflow.Actions && workflow.Actions.length) {
|
|
4331
6049
|
workflow.Actions.forEach((action) => {
|
|
4332
6050
|
// Find source and target nodes
|
|
4333
|
-
const sourceNodeInfo = this.
|
|
4334
|
-
const targetNodeInfo = this.
|
|
6051
|
+
const sourceNodeInfo = this.nodeService.findNodeById(action.FromStage);
|
|
6052
|
+
const targetNodeInfo = this.nodeService.findNodeById(action.ToStage);
|
|
4335
6053
|
if (sourceNodeInfo && targetNodeInfo) {
|
|
4336
6054
|
// Find suitable connection points
|
|
4337
6055
|
const sourcePoint = sourceNodeInfo.node.connectionPoints?.[0];
|
|
@@ -4347,195 +6065,99 @@ class WorkflowDesignerComponent {
|
|
|
4347
6065
|
targetSwimlaneIndex: targetNodeInfo.swimlaneIndex,
|
|
4348
6066
|
};
|
|
4349
6067
|
this.state.connections.push(connection);
|
|
6068
|
+
// Register loaded action
|
|
6069
|
+
this.registerLoadedObject(action.Id, action.Code);
|
|
4350
6070
|
}
|
|
4351
6071
|
}
|
|
4352
6072
|
});
|
|
4353
6073
|
}
|
|
4354
6074
|
}
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
if (parts.length > 1) {
|
|
4361
|
-
const index = parseInt(parts[1]);
|
|
4362
|
-
if (!isNaN(index) && index < this.state.swimlanes.length) {
|
|
4363
|
-
return index;
|
|
4364
|
-
}
|
|
4365
|
-
}
|
|
4366
|
-
return -1;
|
|
6075
|
+
/**
|
|
6076
|
+
* Register a loaded object from the API
|
|
6077
|
+
*/
|
|
6078
|
+
registerLoadedObject(id, code) {
|
|
6079
|
+
this.loadedObjectIds[id] = code;
|
|
4367
6080
|
}
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
6081
|
+
/**
|
|
6082
|
+
* Check if an object was loaded from API
|
|
6083
|
+
*/
|
|
6084
|
+
wasLoadedFromApi(id) {
|
|
6085
|
+
return id in this.loadedObjectIds;
|
|
4373
6086
|
}
|
|
4374
|
-
|
|
4375
|
-
|
|
6087
|
+
/**
|
|
6088
|
+
* Get the code for a loaded object
|
|
6089
|
+
*/
|
|
6090
|
+
getCodeForObject(id) {
|
|
6091
|
+
return this.loadedObjectIds[id] || '';
|
|
4376
6092
|
}
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
6093
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TransformerService, deps: [{ token: WorkflowDesignerState }, { token: SwimlaneService }, { token: NodeManagementService }, { token: ConnectionService$1 }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
6094
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TransformerService, providedIn: 'root' });
|
|
6095
|
+
}
|
|
6096
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TransformerService, decorators: [{
|
|
6097
|
+
type: Injectable,
|
|
6098
|
+
args: [{
|
|
6099
|
+
providedIn: 'root',
|
|
6100
|
+
}]
|
|
6101
|
+
}], ctorParameters: () => [{ type: WorkflowDesignerState }, { type: SwimlaneService }, { type: NodeManagementService }, { type: ConnectionService$1 }] });
|
|
6102
|
+
|
|
6103
|
+
class DecisionPopupComponent {
|
|
6104
|
+
state;
|
|
6105
|
+
visible = false;
|
|
6106
|
+
decisionNodeId = '';
|
|
6107
|
+
popupX = 0;
|
|
6108
|
+
popupY = 0;
|
|
6109
|
+
closed = new EventEmitter();
|
|
6110
|
+
showDuration = true;
|
|
6111
|
+
constructor(state) {
|
|
6112
|
+
this.state = state;
|
|
4396
6113
|
}
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
const swimlaneIndex = Math.floor(event.y / 263);
|
|
4410
|
-
switch (this.selectedTool) {
|
|
4411
|
-
case 'swimlane':
|
|
4412
|
-
console.log('Opening swimlane dialog for new swimlane');
|
|
4413
|
-
this.editingSwimlaneIndex = null; // Set to null to indicate creating a new swimlane
|
|
4414
|
-
this.openSwimlaneDialog();
|
|
4415
|
-
break;
|
|
4416
|
-
case 'stage':
|
|
4417
|
-
if (swimlaneIndex >= 0 && swimlaneIndex < this.state.swimlanes.length) {
|
|
4418
|
-
console.log(`Adding ${this.selectedTool} to swimlane ${swimlaneIndex}`);
|
|
4419
|
-
// Store position for later node creation
|
|
4420
|
-
this.pendingStagePosition = {
|
|
4421
|
-
swimlaneIndex,
|
|
4422
|
-
x: event.x,
|
|
4423
|
-
y: event.y,
|
|
4424
|
-
};
|
|
4425
|
-
// Show stage properties dialog
|
|
4426
|
-
this.showStageDialog = true;
|
|
4427
|
-
}
|
|
4428
|
-
break;
|
|
4429
|
-
case 'decision':
|
|
4430
|
-
case 'form':
|
|
4431
|
-
if (swimlaneIndex >= 0 && swimlaneIndex < this.state.swimlanes.length) {
|
|
4432
|
-
console.log(`Adding ${this.selectedTool} to swimlane ${swimlaneIndex}`);
|
|
4433
|
-
const node = this.state.addNode(swimlaneIndex, this.selectedTool, event.x, event.y);
|
|
4434
|
-
if (node) {
|
|
4435
|
-
// Reset the selected tool after placing a node
|
|
4436
|
-
this.selectedTool = null;
|
|
4437
|
-
}
|
|
4438
|
-
}
|
|
4439
|
-
break;
|
|
4440
|
-
case 'subflow':
|
|
4441
|
-
if (swimlaneIndex >= 0 && swimlaneIndex < this.state.swimlanes.length) {
|
|
4442
|
-
console.log(`Adding ${this.selectedTool} to swimlane ${swimlaneIndex}`);
|
|
4443
|
-
// Show subflow workflow selection popup
|
|
4444
|
-
this.canvasRef.showSubflowSelectionPopup(event.x, event.y, swimlaneIndex);
|
|
4445
|
-
}
|
|
4446
|
-
break;
|
|
4447
|
-
default:
|
|
4448
|
-
break;
|
|
4449
|
-
}
|
|
6114
|
+
getConnectionConditions() {
|
|
6115
|
+
if (!this.decisionNodeId)
|
|
6116
|
+
return [];
|
|
6117
|
+
// Get all connections from this decision node
|
|
6118
|
+
return this.state.connections
|
|
6119
|
+
.filter((conn) => conn.sourceNodeId === this.decisionNodeId)
|
|
6120
|
+
.map((conn) => ({
|
|
6121
|
+
connectionId: conn.id,
|
|
6122
|
+
sourceNodeId: conn.sourceNodeId,
|
|
6123
|
+
targetNodeId: conn.targetNodeId,
|
|
6124
|
+
condition: conn.condition || 'order.amount > 500,000', // Default or actual condition
|
|
6125
|
+
}));
|
|
4450
6126
|
}
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
console.error('Swimlane name cannot be empty');
|
|
4456
|
-
return;
|
|
4457
|
-
}
|
|
4458
|
-
if (this.editingSwimlaneIndex !== null) {
|
|
4459
|
-
// Update existing swimlane
|
|
4460
|
-
console.log('Updating existing swimlane at index:', this.editingSwimlaneIndex);
|
|
4461
|
-
this.state.updateSwimlane(this.editingSwimlaneIndex, event.name, event.tags);
|
|
4462
|
-
}
|
|
4463
|
-
else {
|
|
4464
|
-
// Create new swimlane
|
|
4465
|
-
console.log('Creating new swimlane');
|
|
4466
|
-
this.state.addSwimlane(event.name, event.tags);
|
|
6127
|
+
getTargetNodeName(nodeId) {
|
|
6128
|
+
const nodeInfo = this.state.findNodeById(nodeId);
|
|
6129
|
+
if (nodeInfo && nodeInfo.node) {
|
|
6130
|
+
return nodeInfo.node.stageData?.Name || 'Stage';
|
|
4467
6131
|
}
|
|
4468
|
-
|
|
4469
|
-
this.editingSwimlaneIndex = null;
|
|
6132
|
+
return 'Unknown Stage';
|
|
4470
6133
|
}
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
console.log('Is creating from connection:', this.isCreatingStageFromConnection);
|
|
4477
|
-
if (node) {
|
|
4478
|
-
// If this was created from a connection, create the connection
|
|
4479
|
-
if (this.isCreatingStageFromConnection &&
|
|
4480
|
-
this.pendingConnectionSourcePoint) {
|
|
4481
|
-
console.log('Using stored source point:', this.pendingConnectionSourcePoint);
|
|
4482
|
-
// Find a suitable connection point on the new node
|
|
4483
|
-
if (node.connectionPoints && node.connectionPoints.length > 0) {
|
|
4484
|
-
const sourcePoint = this.pendingConnectionSourcePoint;
|
|
4485
|
-
// Find opposing type connection point
|
|
4486
|
-
const opposingType = {
|
|
4487
|
-
right: 'left',
|
|
4488
|
-
left: 'right',
|
|
4489
|
-
top: 'bottom',
|
|
4490
|
-
bottom: 'top',
|
|
4491
|
-
}[sourcePoint.type];
|
|
4492
|
-
const targetPoint = node.connectionPoints.find((p) => p.type === opposingType);
|
|
4493
|
-
if (targetPoint) {
|
|
4494
|
-
// Create the connection using our stored source data
|
|
4495
|
-
const connection = {
|
|
4496
|
-
id: `conn-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
|
|
4497
|
-
sourceNodeId: sourcePoint.nodeId,
|
|
4498
|
-
targetNodeId: node.id,
|
|
4499
|
-
sourcePointId: sourcePoint.id,
|
|
4500
|
-
targetPointId: targetPoint.id,
|
|
4501
|
-
sourceSwimlaneIndex: this.pendingConnectionSourceSwimlaneIndex,
|
|
4502
|
-
targetSwimlaneIndex: this.pendingStagePosition.swimlaneIndex,
|
|
4503
|
-
};
|
|
4504
|
-
// Add connection to the state
|
|
4505
|
-
this.state.connections.push(connection);
|
|
4506
|
-
console.log('Created connection:', connection);
|
|
4507
|
-
}
|
|
4508
|
-
}
|
|
4509
|
-
// End the connection drag
|
|
4510
|
-
// this.state.endConnectionDrag();
|
|
4511
|
-
}
|
|
4512
|
-
// Reset the selected tool after placing a node
|
|
4513
|
-
this.selectedTool = null;
|
|
4514
|
-
}
|
|
4515
|
-
// Clear pending data
|
|
4516
|
-
this.pendingStagePosition = null;
|
|
4517
|
-
this.isCreatingStageFromConnection = false;
|
|
4518
|
-
this.pendingConnectionSourcePoint = null;
|
|
4519
|
-
this.pendingConnectionSourceSwimlaneIndex = null;
|
|
6134
|
+
getDuration() {
|
|
6135
|
+
// Get duration from the decision node or default to 120 days
|
|
6136
|
+
const nodeInfo = this.state.findNodeById(this.decisionNodeId);
|
|
6137
|
+
if (nodeInfo && nodeInfo.node && nodeInfo.node.stageData) {
|
|
6138
|
+
return nodeInfo.node.stageData.Duration || 120;
|
|
4520
6139
|
}
|
|
4521
|
-
|
|
4522
|
-
this.showStageDialog = false;
|
|
6140
|
+
return 120;
|
|
4523
6141
|
}
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
this.selectedTool = null;
|
|
6142
|
+
onClose() {
|
|
6143
|
+
this.closed.emit();
|
|
4527
6144
|
}
|
|
4528
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type:
|
|
4529
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "
|
|
6145
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DecisionPopupComponent, deps: [{ token: WorkflowDesignerState }], target: i0.ɵɵFactoryTarget.Component });
|
|
6146
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: DecisionPopupComponent, selector: "lib-decision-popup", inputs: { visible: "visible", decisionNodeId: "decisionNodeId", popupX: "popupX", popupY: "popupY" }, outputs: { closed: "closed" }, ngImport: i0, template: "<div\n *ngIf=\"visible\"\n [style.position]=\"'absolute'\"\n [style.left.px]=\"popupX\"\n [style.top.px]=\"popupY\"\n class=\"decision-conditions-popup\"\n>\n <verben-pop-Up [dropdownOpen]=\"true\" [customStyles]=\"{ 'z-index': '100' }\">\n <div dropdown-trigger style=\"display: none\">\n <!-- Hidden trigger element -->\n </div>\n <div\n class=\"p-4 bg-white border border-purple-300 rounded shadow-md w-80\"\n dropdown-content\n >\n <div class=\"flex justify-between items-center mb-3\">\n <h3 class=\"text-lg font-medium\">Decision Conditions</h3>\n <button class=\"text-gray-500 hover:text-gray-700\" (click)=\"onClose()\">\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line>\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>\n </svg>\n </button>\n </div>\n\n <div class=\"space-y-3\">\n @for (condition of getConnectionConditions(); track\n condition.connectionId) {\n <div class=\"p-3 border rounded\">\n <div class=\"mb-2\">\n <span class=\"font-medium\">Action {{ $index + 1 }}:</span>\n </div>\n <div class=\"flex items-center\">\n <input\n type=\"text\"\n [value]=\"condition.condition\"\n readonly\n class=\"w-full px-3 py-2 bg-gray-50 border rounded\"\n placeholder=\"No condition\"\n />\n <span class=\"ml-2\">></span>\n </div>\n <div class=\"mt-1 text-xs text-gray-500\">\n {{ getTargetNodeName(condition.targetNodeId) }}\n </div>\n </div>\n }\n </div>\n\n @if (getConnectionConditions().length === 0) {\n <div class=\"text-center py-3 text-gray-500\">\n No conditions defined for this decision node\n </div>\n } @if (showDuration) {\n <div class=\"mt-4 pt-3 border-t border-gray-200\">\n <div class=\"flex items-center\">\n <span class=\"font-medium\">Duration:</span>\n <span class=\"ml-2\">{{ getDuration() }} days</span>\n </div>\n </div>\n }\n </div>\n </verben-pop-Up>\n</div>\n", styles: [".decision-conditions-popup{z-index:1000}.border-purple-300{border-color:#d8b4fe}.bg-gray-50{background-color:#f9fafb}\n"], dependencies: [{ kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i8.VerbenPopUpComponent, selector: "verben-pop-Up", inputs: ["dropdownOpen", "dropdownWidth", "color", "customStyles", "popUpClass", "border", "borderRadius", "enableMouseLeave"], outputs: ["dropdownOpenChange", "close"] }] });
|
|
4530
6147
|
}
|
|
4531
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type:
|
|
6148
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DecisionPopupComponent, decorators: [{
|
|
4532
6149
|
type: Component,
|
|
4533
|
-
args: [{ selector: 'lib-
|
|
4534
|
-
}], ctorParameters: () => [{ type: WorkflowDesignerState }
|
|
4535
|
-
type:
|
|
4536
|
-
|
|
4537
|
-
|
|
6150
|
+
args: [{ selector: 'lib-decision-popup', template: "<div\n *ngIf=\"visible\"\n [style.position]=\"'absolute'\"\n [style.left.px]=\"popupX\"\n [style.top.px]=\"popupY\"\n class=\"decision-conditions-popup\"\n>\n <verben-pop-Up [dropdownOpen]=\"true\" [customStyles]=\"{ 'z-index': '100' }\">\n <div dropdown-trigger style=\"display: none\">\n <!-- Hidden trigger element -->\n </div>\n <div\n class=\"p-4 bg-white border border-purple-300 rounded shadow-md w-80\"\n dropdown-content\n >\n <div class=\"flex justify-between items-center mb-3\">\n <h3 class=\"text-lg font-medium\">Decision Conditions</h3>\n <button class=\"text-gray-500 hover:text-gray-700\" (click)=\"onClose()\">\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line>\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>\n </svg>\n </button>\n </div>\n\n <div class=\"space-y-3\">\n @for (condition of getConnectionConditions(); track\n condition.connectionId) {\n <div class=\"p-3 border rounded\">\n <div class=\"mb-2\">\n <span class=\"font-medium\">Action {{ $index + 1 }}:</span>\n </div>\n <div class=\"flex items-center\">\n <input\n type=\"text\"\n [value]=\"condition.condition\"\n readonly\n class=\"w-full px-3 py-2 bg-gray-50 border rounded\"\n placeholder=\"No condition\"\n />\n <span class=\"ml-2\">></span>\n </div>\n <div class=\"mt-1 text-xs text-gray-500\">\n {{ getTargetNodeName(condition.targetNodeId) }}\n </div>\n </div>\n }\n </div>\n\n @if (getConnectionConditions().length === 0) {\n <div class=\"text-center py-3 text-gray-500\">\n No conditions defined for this decision node\n </div>\n } @if (showDuration) {\n <div class=\"mt-4 pt-3 border-t border-gray-200\">\n <div class=\"flex items-center\">\n <span class=\"font-medium\">Duration:</span>\n <span class=\"ml-2\">{{ getDuration() }} days</span>\n </div>\n </div>\n }\n </div>\n </verben-pop-Up>\n</div>\n", styles: [".decision-conditions-popup{z-index:1000}.border-purple-300{border-color:#d8b4fe}.bg-gray-50{background-color:#f9fafb}\n"] }]
|
|
6151
|
+
}], ctorParameters: () => [{ type: WorkflowDesignerState }], propDecorators: { visible: [{
|
|
6152
|
+
type: Input
|
|
6153
|
+
}], decisionNodeId: [{
|
|
6154
|
+
type: Input
|
|
6155
|
+
}], popupX: [{
|
|
4538
6156
|
type: Input
|
|
6157
|
+
}], popupY: [{
|
|
6158
|
+
type: Input
|
|
6159
|
+
}], closed: [{
|
|
6160
|
+
type: Output
|
|
4539
6161
|
}] } });
|
|
4540
6162
|
|
|
4541
6163
|
class WorkflowDesignerModule {
|
|
@@ -4545,12 +6167,20 @@ class WorkflowDesignerModule {
|
|
|
4545
6167
|
DesignerCanvasComponent,
|
|
4546
6168
|
SwimlaneDialogComponent,
|
|
4547
6169
|
StageNodeComponent,
|
|
4548
|
-
StageDialogComponent
|
|
6170
|
+
StageDialogComponent,
|
|
6171
|
+
ConditionsPopupComponent,
|
|
6172
|
+
DecisionPopupComponent], imports: [CommonModule,
|
|
4549
6173
|
VerbenDialogueModule,
|
|
4550
6174
|
FormsModule$1,
|
|
4551
6175
|
ReactiveFormsModule,
|
|
4552
6176
|
VerbenPopUpModule], exports: [WorkflowDesignerComponent] });
|
|
4553
|
-
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: WorkflowDesignerModule,
|
|
6177
|
+
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: WorkflowDesignerModule, providers: [
|
|
6178
|
+
NodeManagementService,
|
|
6179
|
+
ConnectionService$1,
|
|
6180
|
+
SwimlaneService,
|
|
6181
|
+
TransformerService,
|
|
6182
|
+
PopupService,
|
|
6183
|
+
], imports: [CommonModule,
|
|
4554
6184
|
VerbenDialogueModule,
|
|
4555
6185
|
FormsModule$1,
|
|
4556
6186
|
ReactiveFormsModule,
|
|
@@ -4566,6 +6196,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
|
|
|
4566
6196
|
SwimlaneDialogComponent,
|
|
4567
6197
|
StageNodeComponent,
|
|
4568
6198
|
StageDialogComponent,
|
|
6199
|
+
ConditionsPopupComponent,
|
|
6200
|
+
DecisionPopupComponent,
|
|
4569
6201
|
],
|
|
4570
6202
|
imports: [
|
|
4571
6203
|
CommonModule,
|
|
@@ -4574,6 +6206,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
|
|
|
4574
6206
|
ReactiveFormsModule,
|
|
4575
6207
|
VerbenPopUpModule,
|
|
4576
6208
|
],
|
|
6209
|
+
providers: [
|
|
6210
|
+
NodeManagementService,
|
|
6211
|
+
ConnectionService$1,
|
|
6212
|
+
SwimlaneService,
|
|
6213
|
+
TransformerService,
|
|
6214
|
+
PopupService,
|
|
6215
|
+
],
|
|
4577
6216
|
exports: [WorkflowDesignerComponent],
|
|
4578
6217
|
}]
|
|
4579
6218
|
}] });
|
|
@@ -6289,7 +7928,7 @@ class WorkflowMapperService {
|
|
|
6289
7928
|
IsEntryPoint: isEntryPoint,
|
|
6290
7929
|
IsExitPoint: isExitPoint,
|
|
6291
7930
|
Tags: stage.tags || [], // Now correctly maps Tag[] to Tag[]
|
|
6292
|
-
|
|
7931
|
+
Form: stage.form || '',
|
|
6293
7932
|
AllowMultiSubProcess: stage.allowMultiSubProcess || false,
|
|
6294
7933
|
AssignmentType: stage.assignmentType ||
|
|
6295
7934
|
TaskAssignmentType.Queue,
|
|
@@ -6418,7 +8057,7 @@ class WorkflowMapperService {
|
|
|
6418
8057
|
isEntryPoint: backendStage.IsEntryPoint,
|
|
6419
8058
|
isExitPoint: backendStage.IsExitPoint,
|
|
6420
8059
|
tags: backendStage.Tags,
|
|
6421
|
-
forms: backendStage.
|
|
8060
|
+
forms: backendStage.Form,
|
|
6422
8061
|
allowMultiSubProcess: backendStage.AllowMultiSubProcess,
|
|
6423
8062
|
assignmentType: backendStage.AssignmentType,
|
|
6424
8063
|
subWorkflow: backendStage.SubWorkFlow,
|