diff --git a/README.md b/README.md index 25cf987..a808eec 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The platform consists of two primary modules: #### 1. Process Builder A visual workflow designer that allows you to create and manage business processes with a drag-and-drop interface. -- Create process flows with start/end events, tasks, gateways, and more +- Create process flows with start/end points, tasks, decision points, and more - Define conditions for workflow branching - Connect tasks to forms for data collection - Manage process variables and data flow diff --git a/components/process-flow/GatewayConditionManager.vue b/components/process-flow/GatewayConditionManager.vue index 9e9c717..9893c4e 100644 --- a/components/process-flow/GatewayConditionManager.vue +++ b/components/process-flow/GatewayConditionManager.vue @@ -17,9 +17,81 @@ const emit = defineEmits(['update:conditions', 'add-condition', 'remove-conditio // Local copy of conditions const localConditions = ref([...(props.conditions || [])]); +// Track which condition groups are collapsed +const collapsedGroups = ref({}); + +// Toggle collapse state of a group +const toggleGroupCollapse = (groupId) => { + collapsedGroups.value[groupId] = !collapsedGroups.value[groupId]; +}; + +// Check if a group is collapsed +const isGroupCollapsed = (groupId) => { + return !!collapsedGroups.value[groupId]; +}; + // Watch for external changes watch(() => props.conditions, (newConditions) => { - localConditions.value = [...(newConditions || [])]; + // If we receive conditions in the old format, migrate them to the new format + if (Array.isArray(newConditions) && newConditions.length > 0) { + // Check if this is the old flat format with 'output' property + if (newConditions[0].output !== undefined && !newConditions[0].conditions) { + // Old format had individual conditions with 'output' property + const conditionsByOutput = {}; + + // Group conditions by their output path + newConditions.forEach(condition => { + const output = condition.output || 'Default Path'; + if (!conditionsByOutput[output]) { + conditionsByOutput[output] = []; + } + // Create a copy without the output property + const { output: _, ...conditionWithoutOutput } = condition; + + // Add logicalOperator to each condition (will only be used for 2nd+ conditions) + conditionWithoutOutput.logicalOperator = 'and'; + + conditionsByOutput[output].push(conditionWithoutOutput); + }); + + // Convert to new format + const groupedConditions = Object.entries(conditionsByOutput).map(([output, conditions]) => ({ + id: `condition-group-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, + output, + conditions + })); + + localConditions.value = groupedConditions; + } + // Check if this is the old group format with logicalOperator at the group level + else if (newConditions[0].conditions && newConditions[0].logicalOperator) { + // Migrate from group-level logicalOperator to condition-level logicalOperator + const migratedConditions = newConditions.map(group => { + const groupCopy = { ...group }; + delete groupCopy.logicalOperator; // Remove group-level operator + + // Add logicalOperator to each condition + if (groupCopy.conditions && groupCopy.conditions.length > 0) { + groupCopy.conditions = groupCopy.conditions.map((condition, index) => { + // First condition doesn't need an operator as it's the starting point + if (index === 0) return { ...condition, logicalOperator: 'and' }; + // Apply the group's original operator to each subsequent condition + return { ...condition, logicalOperator: group.logicalOperator || 'and' }; + }); + } + + return groupCopy; + }); + + localConditions.value = migratedConditions; + } else { + // It's already in the new format or is just empty + localConditions.value = [...(newConditions || [])]; + } + } else { + // Empty array or undefined + localConditions.value = []; + } }, { deep: true }); // Operators based on variable type @@ -74,8 +146,8 @@ const getInputTypeForVarType = (type) => { } }; -// Add new condition -const addCondition = () => { +// Add new condition group (represents one outgoing path with potentially multiple conditions) +const addConditionGroup = () => { if (!props.availableVariables || !props.availableVariables.length) { alert('No variables available. Please add a variable before creating a condition.'); return; @@ -87,44 +159,102 @@ const addCondition = () => { return; } + const newGroupId = `condition-group-${Date.now()}`; + const newConditionGroup = { + id: newGroupId, + output: '', // Output path label (e.g., "Yes" or "Approved") + conditions: [ + { + id: `condition-${Date.now()}`, + variable: defaultVar.name, + operator: getOperatorsForType(defaultVar.type || 'string')[0].value, + value: '', + valueType: defaultVar.type || 'string', + logicalOperator: 'and' // This won't be used for the first condition but included for consistency + } + ] + }; + + localConditions.value.push(newConditionGroup); + // Make sure the new group is expanded + collapsedGroups.value[newGroupId] = false; + + emit('update:conditions', [...localConditions.value]); + emit('add-condition', newConditionGroup); +}; + +// Remove condition group +const removeConditionGroup = (groupIndex) => { + // Get the ID before removing + const groupId = localConditions.value[groupIndex].id; + + // Remove from conditions array + localConditions.value.splice(groupIndex, 1); + + // Remove from collapsed state tracker + delete collapsedGroups.value[groupId]; + + emit('update:conditions', [...localConditions.value]); + emit('remove-condition', groupIndex); +}; + +// Add a single condition to a group +const addConditionToGroup = (groupIndex) => { + if (!props.availableVariables || !props.availableVariables.length) { + alert('No variables available. Please add a variable before creating a condition.'); + return; + } + + const defaultVar = props.availableVariables[0]; const newCondition = { id: `condition-${Date.now()}`, variable: defaultVar.name, operator: getOperatorsForType(defaultVar.type || 'string')[0].value, value: '', valueType: defaultVar.type || 'string', - output: '', // Output path label (e.g., "Yes" or "No") + logicalOperator: 'and' // Default operator for this condition }; - localConditions.value.push(newCondition); + localConditions.value[groupIndex].conditions.push(newCondition); emit('update:conditions', [...localConditions.value]); - emit('add-condition', newCondition); }; -// Remove condition -const removeCondition = (index) => { - localConditions.value.splice(index, 1); - emit('update:conditions', [...localConditions.value]); - emit('remove-condition', index); +// Remove a single condition from a group +const removeConditionFromGroup = (groupIndex, conditionIndex) => { + // Ensure we always have at least one condition in a group + if (localConditions.value[groupIndex].conditions.length > 1) { + localConditions.value[groupIndex].conditions.splice(conditionIndex, 1); + emit('update:conditions', [...localConditions.value]); + } else { + alert('A condition group must have at least one condition. Remove the entire group instead.'); + } }; // Update condition -const updateCondition = (index, field, value) => { +const updateCondition = (groupIndex, conditionIndex, field, value) => { if (field === 'variable') { const selectedVar = props.availableVariables.find(v => v.name === value); - localConditions.value[index].variable = value; - localConditions.value[index].valueType = selectedVar.type; + if (!selectedVar) return; + + localConditions.value[groupIndex].conditions[conditionIndex].variable = value; + localConditions.value[groupIndex].conditions[conditionIndex].valueType = selectedVar.type; // Reset operator to a valid one for this type - localConditions.value[index].operator = getOperatorsForType(selectedVar.type)[0].value; + localConditions.value[groupIndex].conditions[conditionIndex].operator = getOperatorsForType(selectedVar.type)[0].value; // Reset value - localConditions.value[index].value = ''; + localConditions.value[groupIndex].conditions[conditionIndex].value = ''; } else { - localConditions.value[index][field] = value; + localConditions.value[groupIndex].conditions[conditionIndex][field] = value; } emit('update:conditions', [...localConditions.value]); }; +// Update condition group properties +const updateConditionGroup = (groupIndex, field, value) => { + localConditions.value[groupIndex][field] = value; + emit('update:conditions', [...localConditions.value]); +}; + // Generate human-readable condition text const conditionText = (condition) => { if (!condition.variable || !condition.operator) return ''; @@ -137,18 +267,58 @@ const conditionText = (condition) => { return `${variableName} ${operatorText} ${condition.value}`; }; + +// Default path value +const defaultPath = ref('Default'); + +// Update default path +const updateDefaultPath = (value) => { + defaultPath.value = value; + // Also emit the change to persist in parent component + emit('update:conditions', [...localConditions.value]); +}; + +// Save when inputs lose focus +const saveChanges = () => { + emit('update:conditions', [...localConditions.value]); +}; + +// Add this new computed property below other computed properties but before methods +const groupedConditionText = (group) => { + if (!group.conditions || group.conditions.length === 0) return ''; + + if (group.conditions.length === 1) { + return conditionText(group.conditions[0]); + } + + const conditions = group.conditions.map((c, i) => { + const text = conditionText(c); + return { + index: i, + text: text, + operator: c.logicalOperator || 'and' // Individual condition operator (for conditions after first) + }; + }); + + // Build readable text with individual operators + return conditions.map((c, i) => { + if (i === 0) return c.text; + return `${c.operator.toUpperCase()} ${c.text}`; + }).join(' '); +};