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.
Files changed (47) hide show
  1. package/esm2022/lib/components/designer/models/types.mjs +1 -1
  2. package/esm2022/lib/components/designer/services/wf-mapper.service.mjs +3 -3
  3. package/esm2022/lib/components/workflow-designer/conditions-popup/conditions-popup.component.mjs +76 -0
  4. package/esm2022/lib/components/workflow-designer/decision-popup/decision-popup.component.mjs +65 -0
  5. package/esm2022/lib/components/workflow-designer/designer-canvas/designer-canvas.component.mjs +245 -17
  6. package/esm2022/lib/components/workflow-designer/designer-toolbar/designer-toolbar.component.mjs +26 -14
  7. package/esm2022/lib/components/workflow-designer/services/connection.service.mjs +220 -0
  8. package/esm2022/lib/components/workflow-designer/services/node-management.service.mjs +288 -0
  9. package/esm2022/lib/components/workflow-designer/services/popup.service.mjs +235 -0
  10. package/esm2022/lib/components/workflow-designer/services/swimlane.service.mjs +145 -0
  11. package/esm2022/lib/components/workflow-designer/services/transformer.service.mjs +260 -0
  12. package/esm2022/lib/components/workflow-designer/stage-dialog/stage-dialog.component.mjs +67 -11
  13. package/esm2022/lib/components/workflow-designer/stage-node/stage-node.component.mjs +9 -18
  14. package/esm2022/lib/components/workflow-designer/swimlane-dialog/swimlane-dialog.component.mjs +30 -24
  15. package/esm2022/lib/components/workflow-designer/workflow-data.service.mjs +2 -2
  16. package/esm2022/lib/components/workflow-designer/workflow-designer.component.mjs +138 -96
  17. package/esm2022/lib/components/workflow-designer/workflow-designer.module.mjs +27 -3
  18. package/esm2022/lib/components/workflow-designer/workflow-designer.state.mjs +243 -205
  19. package/esm2022/lib/components/workflow-designer/workflow-designer.types.mjs +1 -1
  20. package/esm2022/lib/models/SwimLane.mjs +1 -1
  21. package/esm2022/lib/models/Workflow.mjs +1 -1
  22. package/esm2022/lib/models/WorkflowStage.mjs +1 -1
  23. package/esm2022/lib/services/http-web-request.service.mjs +2 -2
  24. package/fesm2022/verben-workflow-ui.mjs +2644 -1005
  25. package/fesm2022/verben-workflow-ui.mjs.map +1 -1
  26. package/lib/components/designer/models/types.d.ts +1 -1
  27. package/lib/components/workflow-designer/conditions-popup/conditions-popup.component.d.ts +34 -0
  28. package/lib/components/workflow-designer/decision-popup/decision-popup.component.d.ts +26 -0
  29. package/lib/components/workflow-designer/designer-canvas/designer-canvas.component.d.ts +35 -1
  30. package/lib/components/workflow-designer/designer-toolbar/designer-toolbar.component.d.ts +14 -3
  31. package/lib/components/workflow-designer/services/connection.service.d.ts +85 -0
  32. package/lib/components/workflow-designer/services/node-management.service.d.ts +52 -0
  33. package/lib/components/workflow-designer/services/popup.service.d.ts +139 -0
  34. package/lib/components/workflow-designer/services/swimlane.service.d.ts +51 -0
  35. package/lib/components/workflow-designer/services/transformer.service.d.ts +36 -0
  36. package/lib/components/workflow-designer/stage-dialog/stage-dialog.component.d.ts +17 -2
  37. package/lib/components/workflow-designer/stage-node/stage-node.component.d.ts +2 -2
  38. package/lib/components/workflow-designer/swimlane-dialog/swimlane-dialog.component.d.ts +7 -1
  39. package/lib/components/workflow-designer/workflow-designer.component.d.ts +2 -1
  40. package/lib/components/workflow-designer/workflow-designer.module.d.ts +6 -4
  41. package/lib/components/workflow-designer/workflow-designer.state.d.ts +17 -1
  42. package/lib/components/workflow-designer/workflow-designer.types.d.ts +2 -0
  43. package/lib/models/SwimLane.d.ts +1 -0
  44. package/lib/models/Workflow.d.ts +1 -0
  45. package/lib/models/WorkflowStage.d.ts +1 -1
  46. package/package.json +1 -1
  47. 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.eyJNYWlsQWRkcmVzcyI6InZlcmJlbmFAZ21haWwuY29tIiwiQXBwbGljYXRpb24iOiIiLCJOYW1lIjoiVmVyYmVuYSBMb2dpYyBMaW1pdGVkIiwiUm9sZUlEIjoiUk9MLVpYQVhYVCIsIlRlbmFudElkIjoiUERMVEM2IiwiU2VydmljZU5hbWUiOiJXaGl0ZTM2MCIsIm5iZiI6MTc0MDkwODYxMywiZXhwIjoxNzQxMTI0NjEzLCJpYXQiOjE3NDA5MDg2MTN9.3V63j6NICpTItBjowYuHI96sjYLwoPghryCd1u1-kSw',
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 = `${type}-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
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' ? 88 : 169, // Width from SVG
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
- // Create base workflow object
3041
- const workflow = {
3042
- Id: '', // Will be assigned by backend on first save
3043
- Code: '',
3044
- TenantId: '',
3045
- id: '',
3046
- ServiceName: '',
3047
- CreatedAt: new Date(),
3048
- UpdatedAt: new Date(),
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: `lane-${index}`,
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: ObjectState.New,
3071
- Workflow: workflow.Id,
3072
- Tags: swimlane.tags || [],
3073
- Position: swimlane.order,
3074
- Coordinates: { X: 0, Y: swimlane.order * 263 }, // Calculate Y position based on order
3075
- Size: { Width: 3000, Height: 263 }, // Default size
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
- // Create a stage from the node
3084
- const stage = {
3085
- Id: node.id,
3086
- Code: '',
3087
- TenantId: '',
3088
- id: node.id,
3089
- ServiceName: '',
3090
- CreatedAt: new Date(),
3091
- UpdatedAt: new Date(),
3092
- DataState: ObjectState.New,
3093
- Workflow: workflow.Id,
3094
- Name: node.stageData?.Name || 'Unnamed Stage',
3095
- Description: node.stageData?.Description || '',
3096
- Duration: node.stageData?.Duration || 0,
3097
- PassOnRule: node.stageData?.PassOnRule || '',
3098
- ActorRule: node.stageData?.ActorRule || StageActorRule.None,
3099
- MinNoOfActor: node.stageData?.MinNoOfActor || 0,
3100
- IsParallel: node.stageData?.IsParallel || false,
3101
- IsEntryPoint: node.isStartNode,
3102
- IsExitPoint: false, // Determine based on outgoing connections
3103
- Tags: node.stageData?.Tags || [],
3104
- Forms: node.stageData?.formId ? [node.stageData.formId] : [],
3105
- AllowMultiSubProcess: false,
3106
- AssignmentType: TaskAssignmentType.AutoRoute,
3107
- SubWorkFlow: '',
3108
- SwimLane: workflow.Lanes[swimlaneIndex].Id,
3109
- Coordinates: { X: node.x, Y: node.y },
3110
- IsSubProcess: false,
3111
- };
3112
- stages.push(stage);
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
- // Check which stages have no outgoing connections - they are exit points
3117
- this.connections.forEach((conn) => {
3118
- const targetStage = stages.find((s) => s.Id === conn.targetNodeId);
3119
- if (targetStage) {
3120
- // Check if this target has any outgoing connections
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
- workflow.Actions = this.connections.map((conn) => {
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
- return {
3132
- Id: conn.id,
3133
- Code: '',
3134
- TenantId: '',
3135
- id: conn.id,
3136
- ServiceName: '',
3137
- CreatedAt: new Date(),
3138
- UpdatedAt: new Date(),
3139
- DataState: ObjectState.New,
3140
- Workflow: workflow.Id,
3141
- Name: `Action from ${sourceNode?.stageData?.Name || 'Unknown'} to ${targetNode?.stageData?.Name || 'Unknown'}`,
3142
- FromStage: conn.sourceNodeId,
3143
- ToStage: conn.targetNodeId,
3144
- IsParallel: sourceNode?.stageData?.hasParallel || false,
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
- Forms: [],
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 DesignerToolbarComponent {
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
- // Handle tool button click
3287
- onToolClick(tool) {
3288
- this.toolSelected.emit(tool);
3289
- }
3290
- isEnabled(type) {
3291
- switch (type) {
3292
- case 'swimlane':
3293
- return true;
3294
- case 'stage':
3295
- case 'decision':
3296
- return this.state.swimlanes.length > 0;
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
- onSaveClick() {
3307
- this.saveWorkflow.emit();
3308
- }
3309
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DesignerToolbarComponent, deps: [{ token: WorkflowDesignerState }], target: i0.ɵɵFactoryTarget.Component });
3310
- 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"] }] });
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
- ngOnInit() {
3352
- // Load tags
3353
- this.dataService.getTags().then((data) => {
3354
- this.tags = data.Result;
3355
- // If we have stage data with tags, update selectedTagIds after tags are loaded
3356
- if (this.stageData &&
3357
- this.stageData.Tags &&
3358
- this.stageData.Tags.length > 0) {
3359
- this.selectedTagIds = this.stageData.Tags.map((tag) => tag.Id);
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 StageNodeComponent {
4342
+ class ConditionsPopupComponent {
3419
4343
  state;
3420
- dataService;
3421
- node;
3422
- isStartNode = false;
3423
- stageData = {};
3424
- stagePropertiesUpdated = new EventEmitter();
3425
- parallelExecutionToggled = new EventEmitter();
3426
- // Properties for icon click events
3427
- showFormPopup = false;
3428
- showShieldPopup = false;
3429
- showTimerPopup = false;
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
- console.log('Stage node initialized with:', this.node);
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
- selectForm(form) {
3462
- // If form is null, we're clearing the selection
3463
- if (!this.node.stageData) {
3464
- this.node.stageData = {};
3465
- }
3466
- if (form) {
3467
- this.node.stageData.formId = form.Id;
3468
- this.node.stageData.formName = form.Name;
3469
- }
3470
- else {
3471
- // Clear form selection
3472
- delete this.node.stageData.formId;
3473
- delete this.node.stageData.formName;
3474
- }
3475
- // Close the popup
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
- toggleFormPopup(event) {
3484
- if (event) {
3485
- event.preventDefault();
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
- onStagePropertiesSaved(stageData) {
3562
- // Close the dialog
3563
- this.showShieldPopup = false;
3564
- // Emit the event to the parent component with the node id and updated data
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
- 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 });
3572
- 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" }, 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 <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 = 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"] }, { kind: "component", type: StageDialogComponent, selector: "lib-stage-dialog", inputs: ["visible", "stageData"], outputs: ["closed", "saved"] }] });
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: StageNodeComponent, decorators: [{
4393
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ConditionsPopupComponent, decorators: [{
3575
4394
  type: Component,
3576
- 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 <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 = 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"] }]
3577
- }], ctorParameters: () => [{ type: WorkflowDesignerState }, { type: WorkflowDataService }], propDecorators: { node: [{
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
- }], isStartNode: [{
4398
+ }], decisionNodeId: [{
3580
4399
  type: Input
3581
- }], stageData: [{
4400
+ }], newConnectionId: [{
3582
4401
  type: Input
3583
- }], stagePropertiesUpdated: [{
4402
+ }], popupX: [{
4403
+ type: Input
4404
+ }], popupY: [{
4405
+ type: Input
4406
+ }], closed: [{
3584
4407
  type: Output
3585
- }], parallelExecutionToggled: [{
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.state.updateConnectionDrag(position.x, position.y);
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
- // Open stage properties dialog instead of creating node directly
3803
- this.pendingStagePosition = {
3804
- swimlaneIndex: sourceSwimlaneIndex,
3805
- x: endX,
3806
- y: endY,
3807
- };
3808
- console.log('Emitting showStageDialog event with position:', this.pendingStagePosition);
3809
- this.showStageDialog.emit({
3810
- position: this.pendingStagePosition,
3811
- isFromConnection: true,
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
- // End the connection drag
3849
- this.hideConnectionPopup();
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
- // For top/bottom points, start with vertical line
3905
- else if (pointType === 'top' || pointType === 'bottom') {
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
- onStagePropertiesUpdated(event) {
3921
- // Find the node in the swimlanes
3922
- for (let i = 0; i < this.state.swimlanes.length; i++) {
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
- onConnectionCreated(connection) {
3939
- // After a connection is created, refresh the source node to update its state
3940
- const sourceNodeInfo = this.state.findNodeById(connection.sourceNodeId);
3941
- if (sourceNodeInfo && sourceNodeInfo.node.type === 'stage') {
3942
- // Refresh node state
3943
- this.updateNodeConnections(sourceNodeInfo.node);
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
- // Helper method to update node's connection status
3947
- updateNodeConnections(node) {
3948
- // Count the number of outgoing connections that connect to stages
3949
- const outgoingConnections = this.state.connections.filter((conn) => conn.sourceNodeId === node.id);
3950
- // Check if the target nodes are stages
3951
- const connectedStageNodes = outgoingConnections
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
- onEditSwimlane(event, swimlane, swimlaneIndex) {
3957
- event.stopPropagation(); // Prevent the canvas click event from firing
3958
- event.preventDefault();
3959
- console.log('Edit swimlane button clicked for swimlane:', swimlaneIndex);
3960
- // Emit a special event for editing a swimlane
3961
- this.clickedPosition.emit({
3962
- x: -1, // Special value to indicate edit mode
3963
- y: swimlaneIndex,
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
- // showStartNodeFormPopup(event: MouseEvent): void {
3967
- // event.preventDefault();
3968
- // event.stopPropagation();
3969
- // // Implementation will be similar to the stage form popup
3970
- // // We'll need to add UI elements for this
3971
- // console.log('Start node form popup requested');
3972
- // // For now, this is a placeholder
3973
- // // The actual implementation would load forms and add them to the workflow
3974
- // }
3975
- toggleStartNodeFormPopup(event, nodeX, nodeY, swimlaneIndex) {
3976
- event.preventDefault();
3977
- event.stopPropagation();
3978
- // Set popup position
3979
- const swimlaneOffset = swimlaneIndex * 263 + 40;
3980
- this.startNodeFormPopupX = nodeX - 30; // Position near the start node
3981
- this.startNodeFormPopupY = nodeY + swimlaneOffset;
3982
- // If we're opening the popup, load forms
3983
- if (!this.showStartNodeFormPopup) {
3984
- this.isLoadingStartNodeForms = true;
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
- .getForms()
5315
+ .saveWorkflows([workflowModel])
3987
5316
  .then((response) => {
3988
- this.startNodeFormsList = response.Result;
3989
- this.isLoadingStartNodeForms = false;
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 loading forms:', error);
3993
- this.isLoadingStartNodeForms = false;
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
- selectStartNodeForm(form) {
3999
- if (form) {
4000
- this.state.setWorkflowForm(form.Id, form.Name);
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
- else {
4003
- this.state.setWorkflowForm(null, null);
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
- // Close the popup
4006
- this.showStartNodeFormPopup = false;
5479
+ return -1;
4007
5480
  }
4008
- showSubflowSelectionPopup(x, y, swimlaneIndex) {
4009
- this.subflowPopupX = x;
4010
- this.subflowPopupY = y;
4011
- this.pendingSubflowPosition = { swimlaneIndex, x, y };
4012
- // Store connection data if we're creating from a connection
4013
- if (this.state.isConnectionDragging()) {
4014
- // Store the source connection data
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 ?? null;
5500
+ this.state.draggingConnectionData.sourcePoint;
4017
5501
  this.pendingConnectionSourceSwimlaneIndex =
4018
5502
  this.state.draggingConnectionData.sourceSwimlaneIndex ?? null;
4019
- console.log('Stored connection data for subflow:', {
4020
- sourcePoint: this.pendingConnectionSourcePoint,
4021
- sourceSwimlaneIndex: this.pendingConnectionSourceSwimlaneIndex,
4022
- });
5503
+ console.log('Stored source point for connection:', this.pendingConnectionSourcePoint);
4023
5504
  }
4024
- // Load workflows
4025
- this.isLoadingWorkflows = true;
4026
- this.dataService
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
- selectSubflowWorkflow(workflow) {
4039
- console.log('Selecting workflow for subflow:', workflow.Name);
4040
- console.log('Pending connection data:', {
4041
- sourcePoint: this.pendingConnectionSourcePoint,
4042
- sourceSwimlaneIndex: this.pendingConnectionSourceSwimlaneIndex,
4043
- position: this.pendingSubflowPosition,
4044
- });
4045
- if (this.pendingSubflowPosition) {
4046
- const { swimlaneIndex, x, y } = this.pendingSubflowPosition;
4047
- // Create the subflow node with the selected workflow data
4048
- const node = this.state.addNode(swimlaneIndex, 'subflow', x, y, undefined, // No stage data
4049
- { id: workflow.Id, name: workflow.Name });
4050
- console.log('Created subflow node:', node);
4051
- // If this was created from a connection, create the connection
4052
- if (node && this.pendingConnectionSourcePoint) {
4053
- console.log('Creating connection to new subflow node');
4054
- // Find a suitable connection point on the new node
4055
- if (node.connectionPoints && node.connectionPoints.length > 0) {
4056
- const sourcePoint = this.pendingConnectionSourcePoint;
4057
- // Find opposing type connection point
4058
- const opposingType = {
4059
- right: 'left',
4060
- left: 'right',
4061
- top: 'bottom',
4062
- bottom: 'top',
4063
- }[sourcePoint.type];
4064
- const targetPoint = node.connectionPoints.find((p) => p.type === opposingType);
4065
- if (targetPoint) {
4066
- // Create connection
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
- this.state.endConnectionDrag();
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
- // Reset selection tool in parent component
4085
- this.subflowSelected.emit();
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
- // Clean up
4088
- this.showSubflowPopup = false;
4089
- this.pendingSubflowPosition = null;
4090
- this.pendingConnectionSourcePoint = null;
4091
- this.pendingConnectionSourceSwimlaneIndex = null;
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
- 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 });
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
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DesignerCanvasComponent, decorators: [{
4097
- type: Component,
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
- ngOnInit() {
4138
- console.log('Swimlane dialog initialized with data:', this.swimlaneData);
4139
- // Reset form fields when dialog opens
4140
- this.selectedTagNames = [];
4141
- this.workflowName = '';
4142
- this.dataService.getTags().then((data) => {
4143
- this.tags = data.Result;
4144
- // If editing an existing swimlane, populate the form
4145
- if (this.swimlaneData) {
4146
- this.workflowName = this.swimlaneData.name;
4147
- // Set selected tags
4148
- this.selectedTagNames = this.swimlaneData.tags.map((tag) => tag.Name);
4149
- }
4150
- else {
4151
- this.workflowName = '';
4152
- this.selectedTagNames = [];
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
- get allSelected() {
4157
- return (this.tags.length > 0 && this.selectedTagNames.length === this.tags.length);
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
- set allSelected(value) {
4160
- if (value) {
4161
- this.selectedTagNames = this.tags.map((tag) => tag.Name);
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
- else {
4164
- this.selectedTagNames = [];
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
- get filteredTags() {
4168
- if (!this.searchQuery) {
4169
- return this.tags;
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
- return this.tags.filter((tag) => tag.Name.toLowerCase().includes(this.searchQuery.toLowerCase()));
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
- isTagSelected(tagName) {
4174
- return this.selectedTagNames.includes(tagName);
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
- toggleTagSelection(tagName) {
4177
- const index = this.selectedTagNames.indexOf(tagName);
4178
- if (index > -1) {
4179
- this.selectedTagNames.splice(index, 1);
5807
+ /**
5808
+ * Get allowed node types for the current dragging connection
5809
+ */
5810
+ getAllowedTargetNodeTypes() {
5811
+ if (!this.draggingConnectionData.sourcePoint) {
5812
+ return [];
4180
5813
  }
4181
- else {
4182
- this.selectedTagNames.push(tagName);
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
- onDialogClose(eventData) {
4186
- console.log('Dialog closed, received data:', eventData);
4187
- this.closed.emit();
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
- onDialogOpen(eventData) {
4190
- console.log('Dialog opened, received data:', eventData);
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
- applySelection() {
4193
- // Validate name
4194
- if (!this.workflowName || this.workflowName.trim() === '') {
4195
- console.error('Swimlane name cannot be empty');
4196
- return;
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: SwimlaneDialogComponent, deps: [{ token: WorkflowDataService }], target: i0.ɵɵFactoryTarget.Component });
4209
- 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"] }] });
4210
- }
4211
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: SwimlaneDialogComponent, decorators: [{
4212
- type: Component,
4213
- 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" }]
4214
- }], ctorParameters: () => [{ type: WorkflowDataService }], propDecorators: { visible: [{
4215
- type: Input
4216
- }], swimlaneData: [{
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 WorkflowDesignerComponent {
5853
+ class TransformerService {
4225
5854
  state;
4226
- dataService;
4227
- canvasRef;
4228
- workflowCode = null;
4229
- // Currently selected tool
4230
- selectedTool = null;
4231
- showSwimlaneDialog = false;
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.dataService = dataService;
4245
- }
4246
- ngOnInit() {
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
- saveWorkflow() {
4253
- if (this.isSaving)
4254
- return;
4255
- this.isSaving = true;
4256
- // Transform the UI model to the API model
4257
- const workflowModel = this.state.transformToWorkflowModel();
4258
- // Save to the backend
4259
- this.dataService
4260
- .saveWorkflows([workflowModel])
4261
- .then((response) => {
4262
- console.log('Workflow saved successfully:', response);
4263
- // If the API returns an updated workflow, we might want to reload it
4264
- if (response?.Result?.Code) {
4265
- this.workflowCode = response.Result.Code;
4266
- }
4267
- this.isSaving = false;
4268
- })
4269
- .catch((error) => {
4270
- console.error('Error saving workflow:', error);
4271
- this.isSaving = false;
4272
- // Handle error - show a notification, etc.
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
- loadWorkflow(code) {
4276
- this.isLoading = true;
4277
- this.dataService
4278
- .getWorkflowWithParam(code)
4279
- .then((response) => {
4280
- if (response && response.Result) {
4281
- this.parseWorkflowData(response.Result);
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
- // Method to parse workflow data from API and populate the designer
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.state.addSwimlane(lane.Name || `Lane ${lane.Position}`, lane.Tags || []);
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.state.addNode(swimlaneIndex, 'stage', x, y, {
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.state.findNodeById(action.FromStage);
4334
- const targetNodeInfo = this.state.findNodeById(action.ToStage);
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
- // Helper method to find swimlane index by Lane ID
4356
- findSwimlaneIndexByLaneId(laneId) {
4357
- // In a real implementation, you would maintain a mapping between API lane IDs and UI swimlane indices
4358
- // For now we'll just return the index based on position
4359
- const parts = laneId.split('-');
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
- // Handle tool selection from toolbar
4369
- onToolSelected(tool) {
4370
- // Toggle selection if the same tool is clicked again
4371
- this.selectedTool = this.selectedTool === tool ? null : tool;
4372
- console.log('Selected tool:', this.selectedTool);
6081
+ /**
6082
+ * Check if an object was loaded from API
6083
+ */
6084
+ wasLoadedFromApi(id) {
6085
+ return id in this.loadedObjectIds;
4373
6086
  }
4374
- openSwimlaneDialog() {
4375
- this.showSwimlaneDialog = true;
6087
+ /**
6088
+ * Get the code for a loaded object
6089
+ */
6090
+ getCodeForObject(id) {
6091
+ return this.loadedObjectIds[id] || '';
4376
6092
  }
4377
- onShowStageDialog(event) {
4378
- console.log('Received showStageDialog event:', event);
4379
- this.pendingStagePosition = event.position;
4380
- this.showStageDialog = true;
4381
- // If this is from a connection creation, store that info
4382
- this.isCreatingStageFromConnection = event.isFromConnection;
4383
- // Store the source point data before it gets lost
4384
- if (event.isFromConnection &&
4385
- this.state.draggingConnectionData.sourcePoint) {
4386
- this.pendingConnectionSourcePoint =
4387
- this.state.draggingConnectionData.sourcePoint;
4388
- this.pendingConnectionSourceSwimlaneIndex =
4389
- this.state.draggingConnectionData.sourceSwimlaneIndex ?? null;
4390
- console.log('Stored source point for connection:', this.pendingConnectionSourcePoint);
4391
- }
4392
- console.log('Stage dialog state:', {
4393
- pendingPosition: this.pendingStagePosition,
4394
- isVisible: this.showStageDialog,
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
- handleCanvasPositionClick(event) {
4398
- console.log(event);
4399
- // Check if this is an edit swimlane event
4400
- if (event.x === -1 &&
4401
- event.y >= 0 &&
4402
- event.y < this.state.swimlanes.length) {
4403
- console.log('Editing swimlane', event.y);
4404
- this.editingSwimlaneIndex = event.y;
4405
- this.openSwimlaneDialog();
4406
- return;
4407
- }
4408
- // Normal canvas click handling
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
- onSwimlaneDialogFilled(event) {
4452
- console.log('Swimlane dialog filled:', event, 'Editing index:', this.editingSwimlaneIndex);
4453
- // Validate that name is not empty
4454
- if (!event.name || event.name.trim() === '') {
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
- this.showSwimlaneDialog = false;
4469
- this.editingSwimlaneIndex = null;
6132
+ return 'Unknown Stage';
4470
6133
  }
4471
- onStageDialogSaved(stageData) {
4472
- if (this.pendingStagePosition) {
4473
- // Create the stage node
4474
- const node = this.state.addNode(this.pendingStagePosition.swimlaneIndex, 'stage', this.pendingStagePosition.x, this.pendingStagePosition.y, stageData);
4475
- console.log('Source point on dialog save:', this.state.draggingConnectionData.sourcePoint);
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
- console.log('Connections after adding new one:', this.state.connections);
4522
- this.showStageDialog = false;
6140
+ return 120;
4523
6141
  }
4524
- onSubflowSelected() {
4525
- // Reset the selected tool after placing a subflow node
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: WorkflowDesignerComponent, deps: [{ token: WorkflowDesignerState }, { token: WorkflowDataService }], target: i0.ɵɵFactoryTarget.Component });
4529
- 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 (toolSelected)=\"onToolSelected($event)\"\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"] }, { 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"] }] });
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\">&gt;</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: WorkflowDesignerComponent, decorators: [{
6148
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DecisionPopupComponent, decorators: [{
4532
6149
  type: Component,
4533
- 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 (toolSelected)=\"onToolSelected($event)\"\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"] }]
4534
- }], ctorParameters: () => [{ type: WorkflowDesignerState }, { type: WorkflowDataService }], propDecorators: { canvasRef: [{
4535
- type: ViewChild,
4536
- args: ['canvasRef']
4537
- }], workflowCode: [{
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\">&gt;</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], imports: [CommonModule,
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, imports: [CommonModule,
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
- Forms: stage.forms || [],
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.Forms,
8060
+ forms: backendStage.Form,
6422
8061
  allowMultiSubProcess: backendStage.AllowMultiSubProcess,
6423
8062
  assignmentType: backendStage.AssignmentType,
6424
8063
  subWorkflow: backendStage.SubWorkFlow,