Enhance Process Builder with Script Task Configuration
- Introduced a new ScriptNodeConfiguration component for configuring JavaScript tasks within the process builder. - Added ScriptNodeConfigurationModal for user-friendly script task setup, including input and output variable management. - Updated process management logic to handle script variables directly within the process store, improving variable management and accessibility. - Enhanced existing components to support the new script task feature, ensuring seamless integration with the process flow. - Improved overall user experience with intuitive UI elements and clear documentation for the new functionality.
This commit is contained in:
parent
edaae77744
commit
5501c00c7c
@ -3,7 +3,7 @@
|
||||
}
|
||||
|
||||
.formkit-fieldset-box {
|
||||
@apply max-w-md border border-[rgb(var(--fk-border-color))] rounded-lg px-4 py-2;
|
||||
@apply border-0 rounded-lg py-2;
|
||||
}
|
||||
|
||||
.formkit-legend-box {
|
||||
|
@ -24,6 +24,20 @@ const emit = defineEmits(['script-generated'])
|
||||
const generatedScript = ref('')
|
||||
const isInitialized = ref(false)
|
||||
|
||||
// Helper function to map operators to valid JavaScript
|
||||
const getJavaScriptOperator = (operator) => {
|
||||
switch (operator) {
|
||||
case 'and':
|
||||
case 'AND':
|
||||
return '&&'
|
||||
case 'or':
|
||||
case 'OR':
|
||||
return '||'
|
||||
default:
|
||||
return '&&' // Default to AND if unknown operator
|
||||
}
|
||||
}
|
||||
|
||||
// Generate conditional logic script from form components
|
||||
const generateConditionalLogicScript = () => {
|
||||
const scriptsArray = []
|
||||
@ -65,7 +79,7 @@ const generateConditionalLogicScript = () => {
|
||||
default:
|
||||
return `getField('${field}') === '${value}'`
|
||||
}
|
||||
}).join(` ${operator} `)
|
||||
}).join(` ${getJavaScriptOperator(operator)} `)
|
||||
|
||||
// Generate action functions
|
||||
const actionCode = action === 'show' ? `showField('${fieldName}')` :
|
||||
|
@ -659,7 +659,9 @@ const availableComponents = [
|
||||
description: 'Section heading text',
|
||||
defaultProps: {
|
||||
value: 'Section Heading',
|
||||
level: 2
|
||||
level: 2,
|
||||
width: '100%',
|
||||
gridColumn: 'span 12'
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -669,7 +671,9 @@ const availableComponents = [
|
||||
icon: 'material-symbols:text-snippet-outline',
|
||||
description: 'Paragraph of text',
|
||||
defaultProps: {
|
||||
value: 'Enter some descriptive text here.'
|
||||
value: 'Enter some descriptive text here.',
|
||||
width: '100%',
|
||||
gridColumn: 'span 12'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -58,65 +58,166 @@
|
||||
</div>
|
||||
|
||||
<div class="section-content">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Field Type Changer -->
|
||||
<div v-if="availableTypeChanges.length > 0" class="type-changer-section">
|
||||
<FormKit
|
||||
v-if="showField('label') && component.type !== 'button'"
|
||||
type="text"
|
||||
label="Field Label"
|
||||
name="label"
|
||||
v-model="configModel.label"
|
||||
help="What users will see above this field"
|
||||
type="select"
|
||||
label="Field Type"
|
||||
name="fieldType"
|
||||
:modelValue="component.type"
|
||||
@input="handleTypeChange"
|
||||
:options="[
|
||||
{ label: getComponentTypeName(component.type), value: component.type, disabled: true },
|
||||
...availableTypeChanges.map(type => ({
|
||||
label: `Change to ${getComponentTypeName(type)}`,
|
||||
value: type
|
||||
}))
|
||||
]"
|
||||
help="Change this field to a similar compatible type"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="e.g., Full Name, Email Address"
|
||||
/>
|
||||
|
||||
<!-- Button Text (instead of label for buttons) -->
|
||||
<FormKit
|
||||
v-if="component.type === 'button'"
|
||||
type="text"
|
||||
label="Button Text"
|
||||
name="label"
|
||||
v-model="configModel.label"
|
||||
help="Text displayed on the button"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="e.g., Submit, Save, Continue"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-if="showField('name')"
|
||||
type="text"
|
||||
label="Field Name (Internal)"
|
||||
name="name"
|
||||
v-model="configModel.name"
|
||||
help="Used internally to identify this field"
|
||||
validation="required|alpha_numeric"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="e.g., full_name, email_address"
|
||||
/>
|
||||
<!-- Change Preview -->
|
||||
<div v-if="showTypeChangePreview" class="type-change-preview">
|
||||
<div class="preview-header">
|
||||
<Icon name="heroicons:arrow-right" class="w-4 h-4 text-blue-600" />
|
||||
<span class="text-sm font-medium text-gray-700">Preview Change</span>
|
||||
</div>
|
||||
<div class="preview-content">
|
||||
<div class="preview-item">
|
||||
<span class="preview-label">From:</span>
|
||||
<span class="preview-value">{{ getComponentTypeName(component.type) }}</span>
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<span class="preview-label">To:</span>
|
||||
<span class="preview-value">{{ getComponentTypeName(pendingTypeChange) }}</span>
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<span class="preview-label">Preserved:</span>
|
||||
<span class="preview-value">{{ getPreservedProperties(component.type, pendingTypeChange).join(', ') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-actions">
|
||||
<RsButton @click="confirmTypeChange" variant="primary" size="sm">
|
||||
<Icon name="heroicons:check" class="w-4 h-4 mr-1" />
|
||||
Confirm Change
|
||||
</RsButton>
|
||||
<RsButton @click="cancelTypeChange" variant="secondary" size="sm">
|
||||
<Icon name="heroicons:x-mark" class="w-4 h-4 mr-1" />
|
||||
Cancel
|
||||
</RsButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormKit
|
||||
v-if="showField('placeholder')"
|
||||
type="text"
|
||||
label="Placeholder Text"
|
||||
name="placeholder"
|
||||
v-model="configModel.placeholder"
|
||||
help="Hint text shown inside the empty field"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="e.g., Enter your email address..."
|
||||
/>
|
||||
<!-- Heading-specific Essential Settings -->
|
||||
<template v-if="component.type === 'heading'">
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Heading Text"
|
||||
name="value"
|
||||
v-model="configModel.value"
|
||||
help="The heading text that will be displayed"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="e.g., Personal Information, Contact Details"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Heading Level"
|
||||
name="level"
|
||||
v-model="configModel.level"
|
||||
:options="[
|
||||
{ label: 'Heading 1 (Largest)', value: 1 },
|
||||
{ label: 'Heading 2 (Large)', value: 2 },
|
||||
{ label: 'Heading 3 (Medium)', value: 3 },
|
||||
{ label: 'Heading 4 (Small)', value: 4 },
|
||||
{ label: 'Heading 5 (Smaller)', value: 5 },
|
||||
{ label: 'Heading 6 (Smallest)', value: 6 }
|
||||
]"
|
||||
help="Choose the size and importance of this heading"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormKit
|
||||
v-if="showField('help')"
|
||||
type="textarea"
|
||||
label="Help Text"
|
||||
name="help"
|
||||
v-model="configModel.help"
|
||||
help="Additional instructions or guidance for users"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="e.g., We'll use this to send you updates about your order"
|
||||
rows="2"
|
||||
/>
|
||||
<!-- Paragraph-specific Essential Settings -->
|
||||
<template v-else-if="component.type === 'paragraph'">
|
||||
<FormKit
|
||||
type="textarea"
|
||||
label="Paragraph Text"
|
||||
name="value"
|
||||
v-model="configModel.value"
|
||||
help="The text content that will be displayed"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="Enter your paragraph text here..."
|
||||
validation="required"
|
||||
rows="4"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Standard form field Essential Settings -->
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormKit
|
||||
v-if="showField('label') && component.type !== 'button'"
|
||||
type="text"
|
||||
label="Field Label"
|
||||
name="label"
|
||||
v-model="configModel.label"
|
||||
help="What users will see above this field"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="e.g., Full Name, Email Address"
|
||||
/>
|
||||
|
||||
<!-- Button Text (instead of label for buttons) -->
|
||||
<FormKit
|
||||
v-if="component.type === 'button'"
|
||||
type="text"
|
||||
label="Button Text"
|
||||
name="label"
|
||||
v-model="configModel.label"
|
||||
help="Text displayed on the button"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="e.g., Submit, Save, Continue"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-if="showField('name')"
|
||||
type="text"
|
||||
label="Field Name (Internal)"
|
||||
name="name"
|
||||
v-model="configModel.name"
|
||||
help="Used internally to identify this field"
|
||||
validation="required|alpha_numeric"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="e.g., full_name, email_address"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormKit
|
||||
v-if="showField('placeholder')"
|
||||
type="text"
|
||||
label="Placeholder Text"
|
||||
name="placeholder"
|
||||
v-model="configModel.placeholder"
|
||||
help="Hint text shown inside the empty field"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="e.g., Enter your email address..."
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-if="showField('help')"
|
||||
type="textarea"
|
||||
label="Help Text"
|
||||
name="help"
|
||||
v-model="configModel.help"
|
||||
help="Additional instructions or guidance for users"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="e.g., We'll use this to send you updates about your order"
|
||||
rows="2"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -350,23 +451,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Heading Level -->
|
||||
<template v-if="component.type === 'heading'">
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Heading Size"
|
||||
name="level"
|
||||
v-model="configModel.level"
|
||||
:options="[
|
||||
{ label: '🔵 Heading 1 (Largest)', value: 1 },
|
||||
{ label: '🟡 Heading 2 (Large)', value: 2 },
|
||||
{ label: '🟠 Heading 3 (Medium)', value: 3 },
|
||||
{ label: '🔴 Heading 4 (Small)', value: 4 }
|
||||
]"
|
||||
help="Choose the size and importance of this heading"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
||||
<!-- Button Configuration -->
|
||||
<template v-if="component.type === 'button'">
|
||||
@ -1338,6 +1423,10 @@ const configModel = ref({
|
||||
}
|
||||
})
|
||||
|
||||
// Type changing state
|
||||
const pendingTypeChange = ref(null)
|
||||
const showTypeChangePreview = ref(false)
|
||||
|
||||
// Component info helpers
|
||||
const modalTitle = computed(() => {
|
||||
if (!props.component) return 'Field Settings'
|
||||
@ -1485,7 +1574,7 @@ const showField = (fieldName) => {
|
||||
const hasOptions = computed(() => showField('options'))
|
||||
const hasSpecificSettings = computed(() => {
|
||||
if (!props.component) return false
|
||||
const specificTypes = ['mask', 'otp', 'dropzone', 'range', 'heading', 'paragraph', 'button', 'form-section', 'dynamic-list', 'repeating-table']
|
||||
const specificTypes = ['mask', 'otp', 'dropzone', 'range', 'button', 'form-section', 'dynamic-list', 'repeating-table']
|
||||
return specificTypes.includes(props.component.type)
|
||||
})
|
||||
|
||||
@ -1517,6 +1606,79 @@ const isTextBasedField = computed(() => {
|
||||
return ['text', 'textarea', 'email', 'password', 'url', 'tel'].includes(props.component?.type)
|
||||
})
|
||||
|
||||
// Type changing functionality
|
||||
const compatibilityGroups = {
|
||||
// Text-based inputs (can switch between each other)
|
||||
textInputs: ['text', 'textarea', 'email', 'password', 'url', 'tel', 'mask'],
|
||||
|
||||
// Selection inputs (can switch between each other)
|
||||
selectionInputs: ['select', 'radio', 'checkbox'],
|
||||
|
||||
// Date/time inputs (can switch between each other)
|
||||
dateTimeInputs: ['date', 'time', 'datetime-local'],
|
||||
|
||||
// Number-based inputs
|
||||
numberInputs: ['number', 'range'],
|
||||
|
||||
// File inputs
|
||||
fileInputs: ['file', 'dropzone'],
|
||||
|
||||
// Layout elements
|
||||
layoutElements: ['heading', 'paragraph'],
|
||||
|
||||
// Advanced inputs (each unique but can have some compatibility)
|
||||
advancedInputs: ['switch', 'color', 'otp', 'hidden']
|
||||
}
|
||||
|
||||
const availableTypeChanges = computed(() => {
|
||||
if (!props.component) return []
|
||||
|
||||
const currentType = props.component.type
|
||||
|
||||
// Find which group the current type belongs to
|
||||
for (const [groupName, types] of Object.entries(compatibilityGroups)) {
|
||||
if (types.includes(currentType)) {
|
||||
// Return other types in the same group
|
||||
return types.filter(type => type !== currentType)
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
})
|
||||
|
||||
// Get preserved properties when changing types
|
||||
const getPreservedProperties = (fromType, toType) => {
|
||||
const commonProperties = ['label', 'name', 'help', 'validation', 'width', 'gridColumn']
|
||||
const preserved = [...commonProperties]
|
||||
|
||||
// Check for specific preserved properties based on compatibility groups
|
||||
if (compatibilityGroups.textInputs.includes(fromType) && compatibilityGroups.textInputs.includes(toType)) {
|
||||
preserved.push('placeholder')
|
||||
}
|
||||
|
||||
if (compatibilityGroups.selectionInputs.includes(fromType) && compatibilityGroups.selectionInputs.includes(toType)) {
|
||||
preserved.push('options')
|
||||
}
|
||||
|
||||
if (compatibilityGroups.dateTimeInputs.includes(fromType) && compatibilityGroups.dateTimeInputs.includes(toType)) {
|
||||
preserved.push('min', 'max')
|
||||
}
|
||||
|
||||
if (compatibilityGroups.numberInputs.includes(fromType) && compatibilityGroups.numberInputs.includes(toType)) {
|
||||
preserved.push('min', 'max', 'step')
|
||||
}
|
||||
|
||||
if (compatibilityGroups.fileInputs.includes(fromType) && compatibilityGroups.fileInputs.includes(toType)) {
|
||||
preserved.push('accept', 'multiple')
|
||||
}
|
||||
|
||||
if (compatibilityGroups.layoutElements.includes(fromType) && compatibilityGroups.layoutElements.includes(toType)) {
|
||||
preserved.push('value')
|
||||
}
|
||||
|
||||
return preserved.filter((prop, index, self) => self.indexOf(prop) === index) // Remove duplicates
|
||||
}
|
||||
|
||||
// File size helper
|
||||
const configModelMaxSizeMB = computed({
|
||||
get: () => configModel.value.maxSize ? Math.round(configModel.value.maxSize / (1024 * 1024)) : 5,
|
||||
@ -1868,6 +2030,201 @@ const removeNestedComponent = (index) => {
|
||||
configModel.value.children.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Type changing methods
|
||||
const handleTypeChange = (newType) => {
|
||||
if (newType === props.component.type) return
|
||||
|
||||
pendingTypeChange.value = newType
|
||||
showTypeChangePreview.value = true
|
||||
}
|
||||
|
||||
const confirmTypeChange = () => {
|
||||
if (!pendingTypeChange.value) return
|
||||
|
||||
const newType = pendingTypeChange.value
|
||||
const preservedProps = getPreservedProperties(props.component.type, newType)
|
||||
|
||||
// Create new component configuration with preserved properties
|
||||
const newProps = {}
|
||||
|
||||
// Preserve common properties
|
||||
preservedProps.forEach(prop => {
|
||||
if (configModel.value[prop] !== undefined) {
|
||||
newProps[prop] = configModel.value[prop]
|
||||
}
|
||||
})
|
||||
|
||||
// Add default properties for the new type that aren't being preserved
|
||||
const newTypeDefaults = getDefaultPropsForType(newType)
|
||||
Object.keys(newTypeDefaults).forEach(prop => {
|
||||
if (newProps[prop] === undefined) {
|
||||
newProps[prop] = newTypeDefaults[prop]
|
||||
}
|
||||
})
|
||||
|
||||
// Update the component type and properties
|
||||
const updatedComponent = {
|
||||
...props.component,
|
||||
type: newType,
|
||||
props: newProps
|
||||
}
|
||||
|
||||
// Emit the updated component
|
||||
emit('update-component', updatedComponent)
|
||||
|
||||
// Reset state
|
||||
pendingTypeChange.value = null
|
||||
showTypeChangePreview.value = false
|
||||
|
||||
// Close modal after change
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const cancelTypeChange = () => {
|
||||
pendingTypeChange.value = null
|
||||
showTypeChangePreview.value = false
|
||||
}
|
||||
|
||||
// Get default properties for a specific component type
|
||||
const getDefaultPropsForType = (type) => {
|
||||
const defaults = {
|
||||
text: {
|
||||
type: 'text',
|
||||
placeholder: 'Enter text...',
|
||||
validation: ''
|
||||
},
|
||||
textarea: {
|
||||
type: 'textarea',
|
||||
placeholder: 'Enter text...',
|
||||
validation: '',
|
||||
rows: 3
|
||||
},
|
||||
email: {
|
||||
type: 'email',
|
||||
placeholder: 'email@example.com',
|
||||
validation: 'email'
|
||||
},
|
||||
password: {
|
||||
type: 'password',
|
||||
placeholder: 'Enter password...',
|
||||
validation: ''
|
||||
},
|
||||
url: {
|
||||
type: 'url',
|
||||
placeholder: 'https://example.com',
|
||||
validation: 'url'
|
||||
},
|
||||
tel: {
|
||||
type: 'tel',
|
||||
placeholder: '+1 (555) 123-4567',
|
||||
validation: ''
|
||||
},
|
||||
mask: {
|
||||
type: 'mask',
|
||||
placeholder: 'Enter value...',
|
||||
mask: '###-###-####',
|
||||
validation: ''
|
||||
},
|
||||
select: {
|
||||
type: 'select',
|
||||
placeholder: 'Select an option',
|
||||
options: [
|
||||
{ label: 'Option 1', value: 'option_1' },
|
||||
{ label: 'Option 2', value: 'option_2' }
|
||||
],
|
||||
validation: ''
|
||||
},
|
||||
radio: {
|
||||
type: 'radio',
|
||||
options: [
|
||||
{ label: 'Option 1', value: 'option_1' },
|
||||
{ label: 'Option 2', value: 'option_2' }
|
||||
],
|
||||
validation: ''
|
||||
},
|
||||
checkbox: {
|
||||
type: 'checkbox',
|
||||
options: [
|
||||
{ label: 'Option 1', value: 'option_1' },
|
||||
{ label: 'Option 2', value: 'option_2' }
|
||||
],
|
||||
validation: ''
|
||||
},
|
||||
date: {
|
||||
type: 'date',
|
||||
validation: ''
|
||||
},
|
||||
time: {
|
||||
type: 'time',
|
||||
validation: ''
|
||||
},
|
||||
'datetime-local': {
|
||||
type: 'datetime-local',
|
||||
validation: ''
|
||||
},
|
||||
number: {
|
||||
type: 'number',
|
||||
placeholder: '0',
|
||||
validation: ''
|
||||
},
|
||||
range: {
|
||||
type: 'range',
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
value: 50,
|
||||
validation: ''
|
||||
},
|
||||
file: {
|
||||
type: 'file',
|
||||
accept: '*/*',
|
||||
validation: ''
|
||||
},
|
||||
dropzone: {
|
||||
type: 'dropzone',
|
||||
accept: 'image/*,.pdf,.doc,.docx',
|
||||
multiple: true,
|
||||
maxSize: 5242880,
|
||||
maxFiles: 5,
|
||||
validation: ''
|
||||
},
|
||||
switch: {
|
||||
type: 'switch',
|
||||
value: false,
|
||||
validation: ''
|
||||
},
|
||||
color: {
|
||||
type: 'color',
|
||||
value: '#3b82f6',
|
||||
validation: ''
|
||||
},
|
||||
otp: {
|
||||
type: 'otp',
|
||||
digits: 6,
|
||||
validation: 'required'
|
||||
},
|
||||
hidden: {
|
||||
type: 'hidden',
|
||||
value: ''
|
||||
},
|
||||
heading: {
|
||||
value: 'Section Heading',
|
||||
level: 2
|
||||
},
|
||||
paragraph: {
|
||||
value: 'Enter some descriptive text here.'
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure width and gridColumn are included for all types
|
||||
const baseDefaults = {
|
||||
width: '100%',
|
||||
gridColumn: 'span 12'
|
||||
}
|
||||
|
||||
return { ...baseDefaults, ...(defaults[type] || {}) }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -2145,6 +2502,39 @@ const removeNestedComponent = (index) => {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
/* Type Changer Styles */
|
||||
.type-changer-section {
|
||||
@apply mb-6 p-4 border-2 border-dashed border-blue-200 rounded-lg bg-blue-50;
|
||||
}
|
||||
|
||||
.type-change-preview {
|
||||
@apply mt-4 p-4 bg-white border border-blue-300 rounded-lg;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
@apply flex items-center space-x-2 mb-3 pb-2 border-b border-gray-200;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
@apply space-y-2 mb-4;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
@apply flex items-center justify-between text-sm;
|
||||
}
|
||||
|
||||
.preview-label {
|
||||
@apply font-medium text-gray-600;
|
||||
}
|
||||
|
||||
.preview-value {
|
||||
@apply text-gray-800 bg-gray-100 px-2 py-1 rounded text-xs;
|
||||
}
|
||||
|
||||
.preview-actions {
|
||||
@apply flex items-center space-x-2 pt-3 border-t border-gray-200;
|
||||
}
|
||||
|
||||
/* Mask Examples */
|
||||
.mask-examples {
|
||||
@apply mt-3 p-3 bg-gray-50 rounded-lg;
|
||||
|
@ -3,6 +3,7 @@ import { useThemeStore } from "~/stores/theme";
|
||||
|
||||
import { vue } from "@codemirror/lang-vue";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
import { css } from "@codemirror/lang-css";
|
||||
|
||||
import { oneDark } from "@codemirror/theme-one-dark";
|
||||
import { amy, ayuLight, barf, clouds, cobalt, dracula } from "thememirror";
|
||||
@ -24,9 +25,9 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
mode: {
|
||||
language: {
|
||||
type: String,
|
||||
default: "vue",
|
||||
default: "javascript",
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
@ -88,7 +89,7 @@ const dropdownThemes = ref([
|
||||
|
||||
const value = ref(props.modelValue);
|
||||
const extensions = ref([]);
|
||||
if (props.mode == "vue") {
|
||||
if (props.language == "vue") {
|
||||
extensions.value = [
|
||||
vue(),
|
||||
oneDark,
|
||||
@ -96,7 +97,7 @@ if (props.mode == "vue") {
|
||||
indentUnit.of(" "),
|
||||
indentOnInput(),
|
||||
];
|
||||
} else {
|
||||
} else if (props.language == "javascript") {
|
||||
extensions.value = [
|
||||
javascript(),
|
||||
oneDark,
|
||||
@ -104,6 +105,12 @@ if (props.mode == "vue") {
|
||||
indentUnit.of(" "),
|
||||
indentOnInput(),
|
||||
];
|
||||
} else if (props.language == "css") {
|
||||
extensions.value = [
|
||||
css(),
|
||||
oneDark,
|
||||
autocompletion(),
|
||||
];
|
||||
}
|
||||
|
||||
const totalLines = ref(0);
|
||||
@ -135,7 +142,7 @@ watch(
|
||||
? dracula
|
||||
: clouds;
|
||||
|
||||
if (props.mode == "vue") {
|
||||
if (props.language == "vue") {
|
||||
extensions.value = [
|
||||
vue(),
|
||||
themeExtension,
|
||||
|
@ -55,7 +55,7 @@ const props = defineProps({
|
||||
},
|
||||
overlayClose: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
default: false,
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
|
@ -325,7 +325,6 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick } from 'vue';
|
||||
import { useProcessBuilderStore } from '@/stores/processBuilder';
|
||||
import { useVariableStore } from '@/stores/variableStore';
|
||||
|
||||
const props = defineProps({
|
||||
nodeData: {
|
||||
@ -340,8 +339,8 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
// Get the variable store for creating variables
|
||||
const variableStore = useVariableStore();
|
||||
// Get the process store for creating variables
|
||||
const processStore = useProcessBuilderStore();
|
||||
|
||||
// Local state for node data - create a deep copy to avoid mutation issues
|
||||
const localNodeData = ref({
|
||||
@ -366,9 +365,7 @@ const availableVariables = computed(() => {
|
||||
// Only use global variables, matching VariableManager implementation
|
||||
const globalVars = props.availableVariables?.map(v => ({
|
||||
name: v.name || 'unnamed',
|
||||
label: v?.description
|
||||
? `${v.name} (${v.description})`
|
||||
: v.name,
|
||||
label: `${v.name} (${v.type || 'string'})`,
|
||||
type: v.type || 'string',
|
||||
value: v.value
|
||||
})) || [];
|
||||
@ -406,7 +403,7 @@ function createGlobalVariable(name, description = '') {
|
||||
};
|
||||
|
||||
// Add the variable using the store's addVariable method
|
||||
variableStore.addVariable(newVariable);
|
||||
processStore.addProcessVariable(newVariable);
|
||||
|
||||
// Force a refresh of the component
|
||||
nextTick(() => {
|
||||
@ -421,23 +418,29 @@ function saveChanges() {
|
||||
|
||||
// Ensure variables exist before saving
|
||||
if (nodeDataCopy.outputVariable) {
|
||||
variableStore.addVariableIfNotExists({
|
||||
name: nodeDataCopy.outputVariable,
|
||||
type: 'object',
|
||||
scope: 'global',
|
||||
value: null,
|
||||
description: `API response from ${nodeDataCopy.label}`
|
||||
});
|
||||
const existingVar = processStore.getProcessVariable(nodeDataCopy.outputVariable);
|
||||
if (!existingVar) {
|
||||
processStore.addProcessVariable({
|
||||
name: nodeDataCopy.outputVariable,
|
||||
type: 'object',
|
||||
scope: 'global',
|
||||
value: null,
|
||||
description: `API response from ${nodeDataCopy.label}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeDataCopy.errorVariable) {
|
||||
variableStore.addVariableIfNotExists({
|
||||
name: nodeDataCopy.errorVariable,
|
||||
type: 'object',
|
||||
scope: 'global',
|
||||
value: null,
|
||||
description: `API error from ${nodeDataCopy.label}`
|
||||
});
|
||||
const existingVar = processStore.getProcessVariable(nodeDataCopy.errorVariable);
|
||||
if (!existingVar) {
|
||||
processStore.addProcessVariable({
|
||||
name: nodeDataCopy.errorVariable,
|
||||
type: 'object',
|
||||
scope: 'global',
|
||||
value: null,
|
||||
description: `API error from ${nodeDataCopy.label}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Emit the updated data to parent
|
||||
@ -500,16 +503,14 @@ async function testApiCall() {
|
||||
|
||||
// Update variables in the store
|
||||
if (testResult.value.success && localNodeData.value.outputVariable) {
|
||||
variableStore.updateVariable(
|
||||
processStore.updateProcessVariable(
|
||||
localNodeData.value.outputVariable,
|
||||
{ value: result.data },
|
||||
'global'
|
||||
{ value: result.data }
|
||||
);
|
||||
} else if (!testResult.value.success && localNodeData.value.errorVariable) {
|
||||
variableStore.updateVariable(
|
||||
processStore.updateProcessVariable(
|
||||
localNodeData.value.errorVariable,
|
||||
{ value: testResult.value.error },
|
||||
'global'
|
||||
{ value: testResult.value.error }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
@ -524,10 +525,9 @@ async function testApiCall() {
|
||||
|
||||
// Store error in variable
|
||||
if (localNodeData.value.errorVariable) {
|
||||
variableStore.updateVariable(
|
||||
processStore.updateProcessVariable(
|
||||
localNodeData.value.errorVariable,
|
||||
{ value: testResult.value.error },
|
||||
'global'
|
||||
{ value: testResult.value.error }
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@ -586,9 +586,14 @@ function getPreviewWithValues(field) {
|
||||
text = text.replace(variablePattern, (match, varName) => {
|
||||
const variable = availableVariables.value.find(v => v.name === varName);
|
||||
if (variable) {
|
||||
return JSON.stringify(variable.value) || '(undefined)';
|
||||
// Handle different value types
|
||||
if (variable.value === null || variable.value === undefined) {
|
||||
return `"<${varName}>"`; // Show variable name as placeholder
|
||||
}
|
||||
// Return the actual value or stringify complex objects
|
||||
return typeof variable.value === 'string' ? `"${variable.value}"` : JSON.stringify(variable.value);
|
||||
}
|
||||
return '(undefined)';
|
||||
return `"<${varName}>"`; // Show variable name as placeholder for missing variables
|
||||
});
|
||||
|
||||
// Try to format as JSON if possible
|
||||
@ -599,7 +604,8 @@ function getPreviewWithValues(field) {
|
||||
return text;
|
||||
}
|
||||
} catch (error) {
|
||||
return '(Invalid format)';
|
||||
console.warn(`Preview error for ${field}:`, error);
|
||||
return localNodeData.value[field] || '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -492,7 +492,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useVariableStore } from '~/stores/variableStore';
|
||||
import { useProcessBuilderStore } from '~/stores/processBuilder';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Icon } from '#components';
|
||||
|
||||
@ -514,7 +514,7 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update', 'close']);
|
||||
|
||||
// Get the variable store for variables
|
||||
const variableStore = useVariableStore();
|
||||
const processStore = useProcessBuilderStore();
|
||||
|
||||
// Create a local copy of the node data for editing
|
||||
const localNodeData = ref({
|
||||
@ -527,25 +527,17 @@ const localNodeData = ref({
|
||||
|
||||
// Get available variables for conditions and actions
|
||||
const availableVariables = computed(() => {
|
||||
const processVars = variableStore.getAllVariables.process.map(v => ({
|
||||
name: v.name || 'unnamed',
|
||||
label: v?.description
|
||||
? `${v.description} (${v.name || 'unnamed'}, process)`
|
||||
: `${v.name || 'unnamed'} (process)`,
|
||||
type: v.type || 'string',
|
||||
scope: 'process'
|
||||
const processVariables = processStore.getProcessVariables();
|
||||
if (!processVariables || typeof processVariables !== 'object') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(processVariables).map(([name, variable]) => ({
|
||||
name: name,
|
||||
label: `${name} (${variable.type || 'string'})`,
|
||||
type: variable.type || 'string',
|
||||
scope: variable.scope || 'process'
|
||||
}));
|
||||
|
||||
const globalVars = variableStore.getAllVariables.global.map(v => ({
|
||||
name: v.name || 'unnamed',
|
||||
label: v?.description
|
||||
? `${v.description} (${v.name || 'unnamed'}, global)`
|
||||
: `${v.name || 'unnamed'} (global)`,
|
||||
type: v.type || 'string',
|
||||
scope: 'global'
|
||||
}));
|
||||
|
||||
return [...processVars, ...globalVars];
|
||||
});
|
||||
|
||||
// Initialize with default values if needed
|
||||
|
@ -607,7 +607,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick } from 'vue';
|
||||
import { useVariableStore } from '@/stores/variableStore';
|
||||
import { useProcessBuilderStore } from '@/stores/processBuilder';
|
||||
import FormSelector from './FormSelector.vue';
|
||||
import { Icon } from '#components';
|
||||
|
||||
@ -633,7 +633,7 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
// Get the variable store for creating variables
|
||||
const variableStore = useVariableStore();
|
||||
const processStore = useProcessBuilderStore();
|
||||
|
||||
// State for form fields
|
||||
const formFields = ref([]);
|
||||
@ -836,7 +836,7 @@ function createVariableFromMapping(mapping) {
|
||||
};
|
||||
|
||||
// Add the variable
|
||||
variableStore.addVariable(newVariable);
|
||||
processStore.addProcessVariable(newVariable);
|
||||
|
||||
// Update the mapping to use the new variable name (string value, not object)
|
||||
mapping.processVariable = variableName;
|
||||
@ -886,7 +886,7 @@ const formFieldOptions = computed(() => {
|
||||
// Computed property for process variable options (for FormKit select)
|
||||
const processVariableOptions = computed(() => {
|
||||
return props.availableVariables.map(variable => ({
|
||||
label: variable.label || `${variable.name} (${variable.scope || 'unknown'})`,
|
||||
label: `${variable.name} (${variable.type || 'string'})`,
|
||||
value: variable.name,
|
||||
description: variable.description || `Type: ${variable.type || 'unknown'}`
|
||||
}));
|
||||
|
@ -612,7 +612,7 @@ const getPathSummary = (group) => {
|
||||
:key="variable.name"
|
||||
:value="variable.name"
|
||||
>
|
||||
{{ variable.label || variable.name }} ({{ variable.type }})
|
||||
{{ variable.name }} ({{ variable.type }})
|
||||
</option>
|
||||
</select>
|
||||
<div v-if="condition.variable" class="mt-1 flex items-center">
|
||||
|
@ -458,7 +458,6 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { useVariableStore } from '~/stores/variableStore';
|
||||
|
||||
const props = defineProps({
|
||||
nodeData: {
|
||||
|
@ -169,6 +169,25 @@ const availableComponents = [
|
||||
priority: 'medium'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'script',
|
||||
name: 'Script Task',
|
||||
category: 'Core',
|
||||
icon: 'material-symbols:code',
|
||||
description: 'Execute JavaScript to transform data',
|
||||
defaultProps: {
|
||||
label: 'Script Task',
|
||||
data: {
|
||||
description: 'Data transformation script',
|
||||
scriptCode: '// Transform API response or process variables\n// Example:\n// processVariables.newVariable = processVariables.apiResponse.data.field;\n',
|
||||
scriptLanguage: 'javascript',
|
||||
inputVariables: [],
|
||||
outputVariables: [],
|
||||
continueOnError: false,
|
||||
errorVariable: 'scriptError'
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -305,7 +305,6 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useProcessBuilderStore } from '~/stores/processBuilder'
|
||||
import { useVariableStore } from '~/stores/variableStore'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@ -317,7 +316,6 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const processStore = useProcessBuilderStore()
|
||||
const variableStore = useVariableStore()
|
||||
|
||||
// Modal visibility
|
||||
const showModal = computed({
|
||||
@ -370,8 +368,11 @@ const edgeCount = computed(() => {
|
||||
})
|
||||
|
||||
const variableCount = computed(() => {
|
||||
const allVars = variableStore.getAllVariables
|
||||
return (allVars.global?.length || 0) + (allVars.process?.length || 0)
|
||||
const processVariables = processStore.getProcessVariables()
|
||||
if (!processVariables || typeof processVariables !== 'object') {
|
||||
return 0
|
||||
}
|
||||
return Object.keys(processVariables).length
|
||||
})
|
||||
|
||||
// JSON export functionality
|
||||
@ -410,7 +411,7 @@ const formattedJson = computed(() => {
|
||||
nodes: processStore.currentProcess?.nodes || [],
|
||||
edges: processStore.currentProcess?.edges || []
|
||||
},
|
||||
variables: variableStore.getAllVariables,
|
||||
variables: processStore.getProcessVariables(),
|
||||
metadata: {
|
||||
nodeCount: nodeCount.value,
|
||||
edgeCount: edgeCount.value,
|
||||
|
453
components/process-flow/ScriptNodeConfiguration.vue
Normal file
453
components/process-flow/ScriptNodeConfiguration.vue
Normal file
@ -0,0 +1,453 @@
|
||||
<template>
|
||||
<div class="script-node-config">
|
||||
<div class="config-content">
|
||||
<!-- Header -->
|
||||
<div class="config-header">
|
||||
<h3 class="text-lg font-semibold text-gray-800">Script Task Configuration</h3>
|
||||
<p class="text-sm text-gray-600">Configure JavaScript code to transform data and variables</p>
|
||||
</div>
|
||||
|
||||
<!-- Basic Info Section -->
|
||||
<div class="config-section">
|
||||
<h4 class="section-title">Basic Information</h4>
|
||||
<div class="section-content">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Script Name"
|
||||
name="label"
|
||||
v-model="localNodeData.label"
|
||||
help="Display name for this script task"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="e.g., Process API Response, Transform Data"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="textarea"
|
||||
label="Description"
|
||||
name="description"
|
||||
v-model="localNodeData.description"
|
||||
help="Describe what this script does"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="e.g., Transforms API response data into process variables"
|
||||
rows="2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Script Code Section -->
|
||||
<div class="config-section">
|
||||
<h4 class="section-title">Script Code</h4>
|
||||
<div class="section-content">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
JavaScript Code
|
||||
</label>
|
||||
<div class="script-editor-container">
|
||||
<RsCodeMirror
|
||||
v-model="localNodeData.scriptCode"
|
||||
:options="{
|
||||
mode: 'javascript',
|
||||
theme: 'default',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
indentUnit: 2,
|
||||
tabSize: 2
|
||||
}"
|
||||
class="script-editor"
|
||||
placeholder="// Available objects:
|
||||
// - processVariables: object containing all process variables
|
||||
// - console: for debugging (console.log)
|
||||
//
|
||||
// Example - Transform API response:
|
||||
// const apiData = processVariables.apiResponse;
|
||||
// processVariables.applicantStatus = apiData.data.status;
|
||||
// processVariables.approvalRequired = apiData.data.score < 70;
|
||||
//
|
||||
// Example - Extract specific fields:
|
||||
// processVariables.firstName = processVariables.text3?.split(' ')[0];
|
||||
// processVariables.documentCount = processVariables.apiResponse?.data?.documents?.length || 0;"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
💡 Use <code>processVariables</code> to access and modify process data.
|
||||
Available variables: {{ availableVariableNames.join(', ') || 'None' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Error Handling -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
label="Continue on Error"
|
||||
name="continueOnError"
|
||||
v-model="localNodeData.continueOnError"
|
||||
help="If enabled, process continues even if script fails"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-if="!localNodeData.continueOnError"
|
||||
type="text"
|
||||
label="Error Variable"
|
||||
name="errorVariable"
|
||||
v-model="localNodeData.errorVariable"
|
||||
help="Variable to store error information"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="e.g., scriptError"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Variables Section -->
|
||||
<div class="config-section">
|
||||
<h4 class="section-title">Variable Management</h4>
|
||||
<div class="section-content">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Input Variables -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Input Variables
|
||||
</label>
|
||||
<div class="variable-list">
|
||||
<div
|
||||
v-for="variable in availableVariables"
|
||||
:key="variable.name"
|
||||
class="variable-item"
|
||||
:class="{ 'selected': isInputVariable(variable.name) }"
|
||||
@click="toggleInputVariable(variable.name)"
|
||||
>
|
||||
<div class="variable-info">
|
||||
<span class="variable-name">{{ variable.name }}</span>
|
||||
<span class="variable-type">{{ variable.type }}</span>
|
||||
</div>
|
||||
<span class="variable-description">{{ variable.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
Click to select variables this script will read from
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Output Variables -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Output Variables
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="(output, index) in localNodeData.outputVariables"
|
||||
:key="index"
|
||||
class="flex items-center space-x-2"
|
||||
>
|
||||
<FormKit
|
||||
type="text"
|
||||
v-model="output.name"
|
||||
placeholder="Variable name"
|
||||
:classes="{ outer: 'flex-1' }"
|
||||
/>
|
||||
<FormKit
|
||||
type="select"
|
||||
v-model="output.type"
|
||||
:options="[
|
||||
{ label: 'String', value: 'string' },
|
||||
{ label: 'Number', value: 'number' },
|
||||
{ label: 'Boolean', value: 'boolean' },
|
||||
{ label: 'Object', value: 'object' },
|
||||
{ label: 'Array', value: 'array' }
|
||||
]"
|
||||
:classes="{ outer: 'flex-1' }"
|
||||
/>
|
||||
<button
|
||||
@click="removeOutputVariable(index)"
|
||||
class="p-2 text-red-600 hover:bg-red-50 rounded"
|
||||
>
|
||||
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="addOutputVariable"
|
||||
class="flex items-center space-x-2 px-3 py-2 text-blue-600 hover:bg-blue-50 rounded border border-dashed border-blue-300"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="w-4 h-4" />
|
||||
<span class="text-sm">Add Output Variable</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
Define variables this script will create or modify
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Script Testing Section -->
|
||||
<div class="config-section">
|
||||
<h4 class="section-title">Script Testing</h4>
|
||||
<div class="section-content">
|
||||
<div class="test-container p-4 bg-gray-50 rounded-lg">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h5 class="font-medium text-gray-700">Test Script Execution</h5>
|
||||
<button
|
||||
@click="testScript"
|
||||
:disabled="!localNodeData.scriptCode.trim()"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||
>
|
||||
Test Script
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="testResult" class="test-result">
|
||||
<div class="flex items-center space-x-2 mb-2">
|
||||
<Icon
|
||||
:name="testResult.success ? 'material-symbols:check-circle' : 'material-symbols:error'"
|
||||
:class="testResult.success ? 'text-green-600' : 'text-red-600'"
|
||||
class="w-5 h-5"
|
||||
/>
|
||||
<span :class="testResult.success ? 'text-green-700' : 'text-red-700'" class="font-medium">
|
||||
{{ testResult.success ? 'Script executed successfully' : 'Script execution failed' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="testResult.error" class="error-details p-3 bg-red-50 border border-red-200 rounded text-sm text-red-700">
|
||||
{{ testResult.error }}
|
||||
</div>
|
||||
|
||||
<div v-if="testResult.result" class="result-details">
|
||||
<h6 class="font-medium text-gray-700 mb-2">Modified Variables:</h6>
|
||||
<pre class="text-xs bg-white p-3 border border-gray-200 rounded overflow-auto max-h-32">{{ JSON.stringify(testResult.result, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
|
||||
// Props and emits
|
||||
const props = defineProps({
|
||||
nodeData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
availableVariables: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update'])
|
||||
|
||||
// Local state
|
||||
const localNodeData = ref({
|
||||
label: '',
|
||||
description: '',
|
||||
scriptCode: '// Transform API response or process variables\n// Example:\n// processVariables.newVariable = processVariables.apiResponse.data.field;\n',
|
||||
scriptLanguage: 'javascript',
|
||||
inputVariables: [],
|
||||
outputVariables: [],
|
||||
continueOnError: false,
|
||||
errorVariable: 'scriptError'
|
||||
})
|
||||
|
||||
const testResult = ref(null)
|
||||
|
||||
// Computed properties
|
||||
const availableVariableNames = computed(() => {
|
||||
return props.availableVariables.map(v => v.name)
|
||||
})
|
||||
|
||||
// Methods
|
||||
const isInputVariable = (variableName) => {
|
||||
return localNodeData.value.inputVariables.includes(variableName)
|
||||
}
|
||||
|
||||
const toggleInputVariable = (variableName) => {
|
||||
const index = localNodeData.value.inputVariables.indexOf(variableName)
|
||||
if (index > -1) {
|
||||
localNodeData.value.inputVariables.splice(index, 1)
|
||||
} else {
|
||||
localNodeData.value.inputVariables.push(variableName)
|
||||
}
|
||||
emitUpdate()
|
||||
}
|
||||
|
||||
const addOutputVariable = () => {
|
||||
localNodeData.value.outputVariables.push({
|
||||
name: '',
|
||||
type: 'string',
|
||||
description: ''
|
||||
})
|
||||
emitUpdate()
|
||||
}
|
||||
|
||||
const removeOutputVariable = (index) => {
|
||||
localNodeData.value.outputVariables.splice(index, 1)
|
||||
emitUpdate()
|
||||
}
|
||||
|
||||
const testScript = () => {
|
||||
try {
|
||||
// Create a test environment with mock process variables
|
||||
const mockProcessVariables = {}
|
||||
|
||||
// Add mock values for available variables
|
||||
props.availableVariables.forEach(variable => {
|
||||
mockProcessVariables[variable.name] = variable.value || getMockValueForType(variable.type)
|
||||
})
|
||||
|
||||
// Create a safe execution context
|
||||
const scriptFunction = new Function('processVariables', 'console', localNodeData.value.scriptCode)
|
||||
|
||||
// Mock console for testing
|
||||
const mockConsole = {
|
||||
log: (...args) => console.log('[Script Test]', ...args),
|
||||
error: (...args) => console.error('[Script Test]', ...args),
|
||||
warn: (...args) => console.warn('[Script Test]', ...args)
|
||||
}
|
||||
|
||||
// Execute the script
|
||||
scriptFunction(mockProcessVariables, mockConsole)
|
||||
|
||||
testResult.value = {
|
||||
success: true,
|
||||
result: mockProcessVariables,
|
||||
error: null
|
||||
}
|
||||
} catch (error) {
|
||||
testResult.value = {
|
||||
success: false,
|
||||
result: null,
|
||||
error: error.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getMockValueForType = (type) => {
|
||||
switch (type) {
|
||||
case 'string': return 'mock_value'
|
||||
case 'number': return 42
|
||||
case 'boolean': return true
|
||||
case 'object': return { data: { field: 'value' } }
|
||||
case 'array': return ['item1', 'item2']
|
||||
default: return 'mock_value'
|
||||
}
|
||||
}
|
||||
|
||||
const emitUpdate = () => {
|
||||
emit('update', localNodeData.value)
|
||||
}
|
||||
|
||||
// Watch for prop changes
|
||||
watch(
|
||||
() => props.nodeData,
|
||||
(newData) => {
|
||||
if (newData && Object.keys(newData).length > 0) {
|
||||
localNodeData.value = {
|
||||
label: newData.label || 'Script Task',
|
||||
description: newData.description || '',
|
||||
scriptCode: newData.scriptCode || '// Transform API response or process variables\n// Example:\n// processVariables.newVariable = processVariables.apiResponse.data.field;\n',
|
||||
scriptLanguage: newData.scriptLanguage || 'javascript',
|
||||
inputVariables: newData.inputVariables || [],
|
||||
outputVariables: newData.outputVariables || [],
|
||||
continueOnError: newData.continueOnError || false,
|
||||
errorVariable: newData.errorVariable || 'scriptError'
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
// Watch for changes in localNodeData and emit updates
|
||||
watch(
|
||||
localNodeData,
|
||||
() => {
|
||||
emitUpdate()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// Reset test result when script changes
|
||||
watch(
|
||||
() => localNodeData.value.scriptCode,
|
||||
() => {
|
||||
testResult.value = null
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.script-node-config {
|
||||
@apply max-w-6xl mx-auto bg-white;
|
||||
}
|
||||
|
||||
.config-content {
|
||||
@apply p-6 space-y-8;
|
||||
}
|
||||
|
||||
.config-header {
|
||||
@apply border-b border-gray-200 pb-4;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
@apply space-y-4;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@apply text-base font-semibold text-gray-800 mb-3;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
@apply space-y-4;
|
||||
}
|
||||
|
||||
.script-editor-container {
|
||||
@apply border border-gray-300 rounded-lg overflow-hidden;
|
||||
}
|
||||
|
||||
.variable-list {
|
||||
@apply space-y-1 max-h-48 overflow-y-auto border border-gray-200 rounded-lg p-2;
|
||||
}
|
||||
|
||||
.variable-item {
|
||||
@apply p-2 rounded cursor-pointer hover:bg-gray-50 border border-transparent;
|
||||
}
|
||||
|
||||
.variable-item.selected {
|
||||
@apply bg-blue-50 border-blue-200;
|
||||
}
|
||||
|
||||
.variable-info {
|
||||
@apply flex items-center justify-between;
|
||||
}
|
||||
|
||||
.variable-name {
|
||||
@apply font-medium text-gray-800;
|
||||
}
|
||||
|
||||
.variable-type {
|
||||
@apply text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded;
|
||||
}
|
||||
|
||||
.variable-description {
|
||||
@apply text-xs text-gray-600 mt-1;
|
||||
}
|
||||
|
||||
.test-container {
|
||||
@apply border border-gray-200;
|
||||
}
|
||||
|
||||
.field-wrapper {
|
||||
@apply mb-0;
|
||||
}
|
||||
</style>
|
113
components/process-flow/ScriptNodeConfigurationModal.vue
Normal file
113
components/process-flow/ScriptNodeConfigurationModal.vue
Normal file
@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<RsModal
|
||||
v-model="showModal"
|
||||
title="Script Task Configuration"
|
||||
size="xl"
|
||||
position="center"
|
||||
:okCallback="saveAndClose"
|
||||
okTitle="Save"
|
||||
:cancelCallback="closeModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="mb-6">
|
||||
<div class="flex items-start">
|
||||
<div class="mr-4 text-purple-500 flex-shrink-0 mt-1">
|
||||
<Icon name="material-symbols:code" class="text-2xl" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-1">Configure Script Task</h3>
|
||||
<p class="text-sm text-gray-600">
|
||||
Execute JavaScript code to transform data, process variables, and apply business logic.
|
||||
Script tasks are useful for data manipulation, calculations, and preparing data for subsequent steps.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main configuration area -->
|
||||
<ScriptNodeConfiguration
|
||||
:nodeData="localNodeData"
|
||||
:availableVariables="availableVariables"
|
||||
@update="handleUpdate"
|
||||
/>
|
||||
|
||||
<!-- Quick Reference Guide -->
|
||||
<div class="mt-6 bg-purple-50 p-4 rounded-md border border-purple-100">
|
||||
<h4 class="font-medium text-purple-700 mb-2 flex items-center">
|
||||
<Icon name="material-symbols:info-outline" class="mr-1" />
|
||||
Quick Reference Guide
|
||||
</h4>
|
||||
<div class="text-sm text-purple-700">
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li>Use <code class="bg-purple-100 px-1">processVariables</code> to access and modify process data</li>
|
||||
<li>Available functions: <code class="bg-purple-100 px-1">console.log()</code> for debugging</li>
|
||||
<li>Transform API responses into usable variables for subsequent steps</li>
|
||||
<li>Apply business rules and calculations to your data</li>
|
||||
<li>Test your script before saving to ensure it works correctly</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</RsModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import ScriptNodeConfiguration from './ScriptNodeConfiguration.vue';
|
||||
import { Icon } from '#components';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
nodeData: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
availableVariables: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'update']);
|
||||
|
||||
const showModal = ref(props.modelValue);
|
||||
const localNodeData = ref({ ...props.nodeData });
|
||||
|
||||
// Watch for changes to modelValue prop to sync modal visibility
|
||||
watch(() => props.modelValue, (value) => {
|
||||
showModal.value = value;
|
||||
});
|
||||
|
||||
// Watch for changes to showModal to emit update:modelValue
|
||||
watch(() => showModal.value, (value) => {
|
||||
emit('update:modelValue', value);
|
||||
});
|
||||
|
||||
// Watch for changes to nodeData prop
|
||||
watch(() => props.nodeData, (value) => {
|
||||
localNodeData.value = { ...value };
|
||||
}, { deep: true });
|
||||
|
||||
function handleUpdate(updatedData) {
|
||||
localNodeData.value = { ...updatedData };
|
||||
}
|
||||
|
||||
function saveAndClose() {
|
||||
emit('update', localNodeData.value);
|
||||
showModal.value = false;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
showModal.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
code {
|
||||
font-family: monospace;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
</style>
|
@ -141,15 +141,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Default Value Display -->
|
||||
<div v-if="variable.defaultValue !== undefined && variable.defaultValue !== ''" class="mt-3">
|
||||
<!-- Current Value Display -->
|
||||
<div v-if="variable.value !== undefined && variable.value !== ''" class="mt-3">
|
||||
<div class="bg-amber-50 rounded-md p-2 border border-amber-100">
|
||||
<div class="flex items-center gap-1.5 mb-1">
|
||||
<Icon name="material-symbols:settings" class="w-3.5 h-3.5 text-amber-600" />
|
||||
<span class="text-xs font-medium text-amber-700 uppercase tracking-wide">Default Value</span>
|
||||
<span class="text-xs font-medium text-amber-700 uppercase tracking-wide">Current Value</span>
|
||||
</div>
|
||||
<div class="font-mono text-xs text-amber-800 break-all">
|
||||
{{ formatValue(variable.defaultValue, variable.type) }}
|
||||
{{ formatValue(variable.value, variable.type) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -263,9 +263,9 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { useVariableStore } from "~/stores/variableStore";
|
||||
import { useProcessBuilderStore } from "~/stores/processBuilder";
|
||||
|
||||
const variableStore = useVariableStore();
|
||||
const processStore = useProcessBuilderStore();
|
||||
|
||||
// State
|
||||
const showAddVariable = ref(false);
|
||||
@ -353,9 +353,8 @@ const getHelpTextForType = (type) => {
|
||||
|
||||
// Computed
|
||||
const variables = computed(() => {
|
||||
// Return all variables (treating everything as global)
|
||||
const allVars = variableStore.getAllVariables;
|
||||
return [...allVars.global, ...allVars.process];
|
||||
// Get variables from the current process
|
||||
return processStore.getProcessVariables();
|
||||
});
|
||||
|
||||
// Filtered variables based on search query
|
||||
@ -375,15 +374,14 @@ const editVariable = (variable) => {
|
||||
editingVariable.value = variable;
|
||||
variableForm.value = {
|
||||
...variable,
|
||||
defaultValue: variable.defaultValue || ""
|
||||
defaultValue: variable.value || ""
|
||||
};
|
||||
showAddVariable.value = true;
|
||||
};
|
||||
|
||||
const deleteVariable = (variable) => {
|
||||
if (confirm(`Are you sure you want to delete the variable "${variable.name}"? This might affect parts of your process that use this variable.`)) {
|
||||
const scope = variable.scope || 'global';
|
||||
variableStore.deleteVariable(variable.name, scope);
|
||||
processStore.deleteProcessVariable(variable.name);
|
||||
}
|
||||
};
|
||||
|
||||
@ -427,19 +425,15 @@ const saveVariable = async (formData) => {
|
||||
type: formData.type,
|
||||
scope: "global",
|
||||
description: formData.description,
|
||||
defaultValue: processedDefaultValue
|
||||
value: processedDefaultValue
|
||||
};
|
||||
|
||||
if (editingVariable.value) {
|
||||
// Update existing variable
|
||||
variableStore.updateVariable(
|
||||
editingVariable.value.name,
|
||||
newVariable,
|
||||
'global'
|
||||
);
|
||||
processStore.updateProcessVariable(editingVariable.value.name, newVariable);
|
||||
} else {
|
||||
// Add new variable
|
||||
variableStore.addVariable(newVariable);
|
||||
processStore.addProcessVariable(newVariable);
|
||||
}
|
||||
|
||||
// Close modal and reset form
|
||||
|
@ -7,7 +7,7 @@ export const useToast = () => {
|
||||
success: (message, options = {}) => {
|
||||
toast.success(message, {
|
||||
timeout: 3000,
|
||||
position: 'top-right',
|
||||
position: 'bottom-right',
|
||||
...options
|
||||
});
|
||||
},
|
||||
@ -15,7 +15,7 @@ export const useToast = () => {
|
||||
error: (message, options = {}) => {
|
||||
toast.error(message, {
|
||||
timeout: 5000,
|
||||
position: 'top-right',
|
||||
position: 'bottom-right',
|
||||
...options
|
||||
});
|
||||
},
|
||||
@ -23,7 +23,7 @@ export const useToast = () => {
|
||||
warning: (message, options = {}) => {
|
||||
toast.warning(message, {
|
||||
timeout: 4000,
|
||||
position: 'top-right',
|
||||
position: 'bottom-right',
|
||||
...options
|
||||
});
|
||||
},
|
||||
@ -31,7 +31,7 @@ export const useToast = () => {
|
||||
info: (message, options = {}) => {
|
||||
toast.info(message, {
|
||||
timeout: 3000,
|
||||
position: 'top-right',
|
||||
position: 'bottom-right',
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
@ -16,8 +16,11 @@ const props = defineProps({
|
||||
// console.log("props", props);
|
||||
|
||||
const isDynamicRouteError = computed(() => {
|
||||
const path = window?.location?.pathname || '';
|
||||
return path.includes('/[') || path.includes(']');
|
||||
if(process.client) {
|
||||
const path = window?.location?.pathname || '';
|
||||
return path.includes('/[') || path.includes(']');
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const handleError = () => {
|
||||
|
@ -1,479 +1,472 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useProcessBuilderStore } from '~/stores/processBuilder'
|
||||
|
||||
definePageMeta({
|
||||
title: "Dashboard",
|
||||
title: "Dashboard - Corrad BPM",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
});
|
||||
|
||||
const data1 = ref([]);
|
||||
const data2 = ref([]);
|
||||
const data3 = ref([]);
|
||||
const data4 = ref([]);
|
||||
var sparkline1Data = [47, 45, 54, 38, 56, 24, 65];
|
||||
var sparkline2Data = [61, 35, 66, 41, 59, 25, 32];
|
||||
var sparkline3Data = [25, 18, 36, 41, 43, 35, 14];
|
||||
var sparkline4Data = [8, 16, 22, 41, 43, 35, 14];
|
||||
const router = useRouter()
|
||||
const processStore = useProcessBuilderStore()
|
||||
|
||||
const changeKey = ref(0);
|
||||
// Dashboard stats
|
||||
const stats = ref({
|
||||
activeProcesses: 0,
|
||||
totalForms: 0,
|
||||
pendingTasks: 0,
|
||||
completedInstances: 0
|
||||
})
|
||||
|
||||
const customers = [
|
||||
// Recent activity data
|
||||
const recentProcesses = ref([])
|
||||
const recentTasks = ref([])
|
||||
|
||||
// System features data
|
||||
const systemFeatures = [
|
||||
{
|
||||
name: "Iqmal",
|
||||
age: "25",
|
||||
city: "Kuala Lumpur",
|
||||
country: "Malaysia",
|
||||
totalPurchase: 1524,
|
||||
purchase: 23,
|
||||
id: 'process-builder',
|
||||
title: 'Process Builder',
|
||||
description: 'Design business workflows with drag-and-drop visual editor',
|
||||
icon: 'material-symbols:account-tree',
|
||||
color: 'bg-blue-500',
|
||||
colorLight: 'bg-blue-100',
|
||||
textColor: 'text-blue-600',
|
||||
route: '/process-builder',
|
||||
features: ['Visual Workflow Designer', 'BPMN 2.0 Support', 'Real-time Collaboration']
|
||||
},
|
||||
{
|
||||
name: "Adi",
|
||||
age: "45",
|
||||
city: "Pulau Pinang",
|
||||
country: "Malaysia",
|
||||
totalPurchase: 643,
|
||||
purchase: 14,
|
||||
id: 'form-builder',
|
||||
title: 'Form Builder',
|
||||
description: 'Create dynamic forms with advanced components and logic',
|
||||
icon: 'material-symbols:description',
|
||||
color: 'bg-purple-500',
|
||||
colorLight: 'bg-purple-100',
|
||||
textColor: 'text-purple-600',
|
||||
route: '/form-builder',
|
||||
features: ['Drag & Drop Form Builder', 'Conditional Logic', 'FormKit Integration']
|
||||
},
|
||||
{
|
||||
name: "Raziq",
|
||||
age: "21",
|
||||
city: "Kelantan",
|
||||
country: "Malaysia",
|
||||
totalPurchase: 543,
|
||||
purchase: 12,
|
||||
id: 'execution-engine',
|
||||
title: 'Execution Engine',
|
||||
description: 'Run and manage process instances with monitoring',
|
||||
icon: 'material-symbols:play-circle',
|
||||
color: 'bg-green-500',
|
||||
colorLight: 'bg-green-100',
|
||||
textColor: 'text-green-600',
|
||||
route: '/execution',
|
||||
features: ['Process Execution', 'Task Management', 'Instance Monitoring']
|
||||
},
|
||||
{
|
||||
name: "Haqeem",
|
||||
age: "19",
|
||||
city: "Negeri Sembilan",
|
||||
country: "Malaysia",
|
||||
totalPurchase: 258,
|
||||
purchase: 6,
|
||||
},
|
||||
];
|
||||
|
||||
const randomizeArray = function (arg) {
|
||||
var array = arg.slice();
|
||||
var currentIndex = array.length,
|
||||
temporaryValue,
|
||||
randomIndex;
|
||||
|
||||
while (0 !== currentIndex) {
|
||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex -= 1;
|
||||
|
||||
temporaryValue = array[currentIndex];
|
||||
array[currentIndex] = array[randomIndex];
|
||||
array[randomIndex] = temporaryValue;
|
||||
id: 'api-integrations',
|
||||
title: 'API Integrations',
|
||||
description: 'Connect with external systems and services',
|
||||
icon: 'material-symbols:api',
|
||||
color: 'bg-orange-500',
|
||||
colorLight: 'bg-orange-100',
|
||||
textColor: 'text-orange-600',
|
||||
route: '/devtool/api-editor',
|
||||
features: ['REST API Calls', 'Webhook Support', 'Data Transformation']
|
||||
}
|
||||
]
|
||||
|
||||
return array;
|
||||
};
|
||||
|
||||
data1.value.push({
|
||||
name: "Revenues",
|
||||
data: randomizeArray(sparkline1Data),
|
||||
});
|
||||
|
||||
data2.value.push({
|
||||
name: "Users",
|
||||
data: randomizeArray(sparkline2Data),
|
||||
});
|
||||
|
||||
data3.value.push({
|
||||
name: "Products",
|
||||
data: randomizeArray(sparkline3Data),
|
||||
});
|
||||
|
||||
data4.value.push({
|
||||
name: "Viewers",
|
||||
data: randomizeArray(sparkline4Data),
|
||||
});
|
||||
|
||||
const chartOptions = computed(() => ({
|
||||
chart: {
|
||||
type: "area",
|
||||
sparkline: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
curve: "smooth",
|
||||
},
|
||||
fill: {
|
||||
opacity: 1,
|
||||
},
|
||||
labels: [...Array(7).keys()].map((n) => `2022-06-0${n + 1}`),
|
||||
xaxis: {
|
||||
type: "datetime",
|
||||
},
|
||||
}));
|
||||
|
||||
// Radial Chart
|
||||
|
||||
const radialData = ref([44, 55, 67, 83]);
|
||||
|
||||
const chartOptionsRadial = computed(() => ({
|
||||
chart: {
|
||||
height: 350,
|
||||
type: "radialBar",
|
||||
},
|
||||
plotOptions: {
|
||||
radialBar: {
|
||||
dataLabels: {
|
||||
style: {
|
||||
colors: "#9CA3AF",
|
||||
},
|
||||
name: {
|
||||
offsetY: 30,
|
||||
fontSize: "18px",
|
||||
},
|
||||
value: {
|
||||
offsetY: -15,
|
||||
fontSize: "30px",
|
||||
},
|
||||
total: {
|
||||
show: true,
|
||||
label: "Total",
|
||||
formatter: function (w) {
|
||||
// By default this function returns the average of all series. The below is just an example to show the use of custom formatter function
|
||||
return 249;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
labels: ["Product A", "Product B", "Product C", "Product D"],
|
||||
stroke: {
|
||||
lineCap: "round",
|
||||
},
|
||||
}));
|
||||
|
||||
// Transaction Graph
|
||||
const transactionData = ref([
|
||||
// Quick actions
|
||||
const quickActions = [
|
||||
{
|
||||
name: "Bill A",
|
||||
data: [...Array(12).keys()].map((n) => Math.round(Math.random() * 100)),
|
||||
title: 'Create New Process',
|
||||
description: 'Start building a new business process',
|
||||
icon: 'material-symbols:add-circle',
|
||||
color: 'bg-emerald-500',
|
||||
action: () => router.push('/process-builder')
|
||||
},
|
||||
{
|
||||
name: "Bill B",
|
||||
data: [...Array(12).keys()].map((n) => Math.round(Math.random() * 100)),
|
||||
title: 'Design Form',
|
||||
description: 'Build interactive forms',
|
||||
icon: 'material-symbols:edit-document',
|
||||
color: 'bg-indigo-500',
|
||||
action: () => router.push('/form-builder')
|
||||
},
|
||||
]);
|
||||
{
|
||||
title: 'View Inbox',
|
||||
description: 'Check pending tasks',
|
||||
icon: 'material-symbols:inbox',
|
||||
color: 'bg-rose-500',
|
||||
action: () => router.push('/execution/inbox')
|
||||
},
|
||||
{
|
||||
title: 'Manage Processes',
|
||||
description: 'View all processes',
|
||||
icon: 'material-symbols:folder-managed',
|
||||
color: 'bg-amber-500',
|
||||
action: () => router.push('/process-builder/manage')
|
||||
}
|
||||
]
|
||||
|
||||
const chartOptionsTransaction = computed(() => ({
|
||||
chart: {
|
||||
height: 350,
|
||||
type: "area",
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
// Development roadmap
|
||||
const developmentRoadmap = [
|
||||
{
|
||||
phase: 'Phase 1 - Foundation',
|
||||
status: 'completed',
|
||||
items: [
|
||||
'Process Builder with Visual Designer',
|
||||
'Form Builder with Dynamic Components',
|
||||
'Basic Process Execution Engine',
|
||||
'User Authentication & Authorization'
|
||||
]
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
{
|
||||
phase: 'Phase 2 - Integration',
|
||||
status: 'in-progress',
|
||||
items: [
|
||||
'API Node Integration',
|
||||
'Email & SMS Notifications',
|
||||
'Business Rules Engine',
|
||||
'Process Templates & Versioning'
|
||||
]
|
||||
},
|
||||
stroke: {
|
||||
curve: "smooth",
|
||||
},
|
||||
colors: ["#6366F1", "#F97316"],
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
colors: "#9CA3AF",
|
||||
fontSize: "12px",
|
||||
},
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
type: "datetime",
|
||||
categories: [
|
||||
"2022-01-01",
|
||||
"2022-02-01",
|
||||
"2022-03-01",
|
||||
"2022-04-01",
|
||||
"2022-05-01",
|
||||
"2022-06-01",
|
||||
"2022-07-01",
|
||||
"2022-08-01",
|
||||
"2022-09-01",
|
||||
"2022-10-01",
|
||||
"2022-11-01",
|
||||
"2022-12-01",
|
||||
],
|
||||
labels: {
|
||||
style: {
|
||||
colors: "#9CA3AF",
|
||||
fontSize: "14px",
|
||||
fontWeight: 400,
|
||||
},
|
||||
datetimeFormatter: {
|
||||
month: "MMM",
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
position: "top",
|
||||
horizontalAlign: "left",
|
||||
labels: {
|
||||
colors: "#9CA3AF",
|
||||
useSeriesColors: false,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
x: {
|
||||
format: "MMMM",
|
||||
},
|
||||
},
|
||||
}));
|
||||
{
|
||||
phase: 'Phase 3 - Advanced Features',
|
||||
status: 'planned',
|
||||
items: [
|
||||
'Advanced Analytics & Reporting',
|
||||
'Process Mining & Optimization',
|
||||
'Mobile App Support',
|
||||
'Enterprise Connectors'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// Load dashboard data
|
||||
const loadDashboardData = async () => {
|
||||
try {
|
||||
// Load recent processes
|
||||
await processStore.fetchProcesses({ limit: 5, sortBy: 'processModifiedDate', sortOrder: 'desc' })
|
||||
recentProcesses.value = processStore.processes.slice(0, 5)
|
||||
|
||||
// Update stats
|
||||
stats.value = {
|
||||
activeProcesses: processStore.processes.filter(p => p.status === 'published').length,
|
||||
totalForms: processStore.processes.length,
|
||||
pendingTasks: Math.floor(Math.random() * 25) + 5, // Mock data
|
||||
completedInstances: Math.floor(Math.random() * 150) + 50 // Mock data
|
||||
}
|
||||
|
||||
// Mock recent tasks
|
||||
recentTasks.value = [
|
||||
{ id: 1, title: 'Review Profile Registration', process: 'Pendaftaran Profil', assignee: 'You', dueDate: '2024-01-15' },
|
||||
{ id: 2, title: 'Approve Leave Request', process: 'Leave Management', assignee: 'HR Team', dueDate: '2024-01-16' },
|
||||
{ id: 3, title: 'Verify Documents', process: 'Document Verification', assignee: 'Admin', dueDate: '2024-01-17' }
|
||||
]
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboard data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation helpers
|
||||
const navigateToFeature = (route) => {
|
||||
router.push(route)
|
||||
}
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'completed': return 'text-green-600 bg-green-100'
|
||||
case 'in-progress': return 'text-blue-600 bg-blue-100'
|
||||
case 'planned': return 'text-gray-600 bg-gray-100'
|
||||
default: return 'text-gray-600 bg-gray-100'
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
changeKey.value++;
|
||||
}, 500);
|
||||
});
|
||||
loadDashboardData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="space-y-6">
|
||||
<LayoutsBreadcrumb />
|
||||
<!-- First Row -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-x-6">
|
||||
<!-- Summary Card #1 -->
|
||||
<rs-card>
|
||||
<div class="summary-1 pt-5 pb-3 px-5 flex items-center gap-4">
|
||||
<div
|
||||
class="p-5 flex justify-center items-center bg-primary/20 rounded-2xl"
|
||||
>
|
||||
<Icon class="text-primary" name="ic:outline-attach-money"></Icon>
|
||||
|
||||
<!-- Welcome Header -->
|
||||
<div class="bg-gradient-to-r from-blue-600 to-purple-600 rounded-xl p-8 text-white">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold mb-2">Welcome to Corrad BPM</h1>
|
||||
<p class="text-blue-100 text-lg">Business Process Management & Workflow Automation Platform</p>
|
||||
</div>
|
||||
<div class="hidden md:block">
|
||||
<Icon name="material-symbols:float-landscape" class="w-20 h-20 text-blue-200" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Statistics -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
|
||||
<rs-card class="hover:shadow-lg transition-shadow">
|
||||
<div class="p-6 flex items-center">
|
||||
<div class="p-4 bg-blue-100 rounded-xl mr-4">
|
||||
<Icon name="material-symbols:account-tree" class="w-8 h-8 text-blue-600" />
|
||||
</div>
|
||||
<div class="flex-1 truncate">
|
||||
<span class="block font-semibold text-xl leading-tight">
|
||||
RM 100,000</span
|
||||
>
|
||||
<span class="text-base font-semibold text-gray-500"
|
||||
>Total Revenues</span
|
||||
>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-gray-900">{{ stats.activeProcesses }}</div>
|
||||
<div class="text-sm text-gray-500">Active Processes</div>
|
||||
</div>
|
||||
</div>
|
||||
<client-only>
|
||||
<VueApexCharts
|
||||
:key="changeKey"
|
||||
width="100%"
|
||||
height="53"
|
||||
:options="{
|
||||
...chartOptions,
|
||||
colors: ['#F43F5E'],
|
||||
yaxis: {
|
||||
min: 0,
|
||||
max: Math.max(...data1[0].data) + 10,
|
||||
},
|
||||
}"
|
||||
:series="data1"
|
||||
></VueApexCharts>
|
||||
</client-only>
|
||||
</rs-card>
|
||||
<!-- Summary Card #2 -->
|
||||
<rs-card>
|
||||
<div class="summary-2 pt-5 pb-3 px-5 flex items-center gap-4">
|
||||
<div
|
||||
class="p-5 flex justify-center items-center bg-indigo-100 rounded-2xl"
|
||||
>
|
||||
<Icon
|
||||
class="text-indigo-500"
|
||||
name="ic:outline-account-circle"
|
||||
></Icon>
|
||||
|
||||
<rs-card class="hover:shadow-lg transition-shadow">
|
||||
<div class="p-6 flex items-center">
|
||||
<div class="p-4 bg-purple-100 rounded-xl mr-4">
|
||||
<Icon name="material-symbols:description" class="w-8 h-8 text-purple-600" />
|
||||
</div>
|
||||
<div class="flex-1 truncate">
|
||||
<span class="block font-semibold text-xl leading-tight"> 512</span>
|
||||
<span class="text-base font-semibold text-gray-500"
|
||||
>Total Users</span
|
||||
>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-gray-900">{{ stats.totalForms }}</div>
|
||||
<div class="text-sm text-gray-500">Total Forms</div>
|
||||
</div>
|
||||
</div>
|
||||
<client-only>
|
||||
<VueApexCharts
|
||||
:key="changeKey"
|
||||
width="100%"
|
||||
height="53"
|
||||
:options="{
|
||||
...chartOptions,
|
||||
colors: ['#6366F1'],
|
||||
yaxis: {
|
||||
min: 0,
|
||||
max: Math.max(...data2[0].data) + 10,
|
||||
},
|
||||
}"
|
||||
:series="data2"
|
||||
></VueApexCharts>
|
||||
</client-only>
|
||||
</rs-card>
|
||||
<!-- Summary Card #3 -->
|
||||
<rs-card>
|
||||
<div class="summary-3 pt-5 pb-3 px-5 flex items-center gap-4">
|
||||
<div
|
||||
class="p-5 flex justify-center items-center bg-orange-100 rounded-2xl"
|
||||
>
|
||||
<Icon class="text-orange-500" name="ic:outline-shopping-bag"></Icon>
|
||||
|
||||
<rs-card class="hover:shadow-lg transition-shadow">
|
||||
<div class="p-6 flex items-center">
|
||||
<div class="p-4 bg-orange-100 rounded-xl mr-4">
|
||||
<Icon name="material-symbols:pending-actions" class="w-8 h-8 text-orange-600" />
|
||||
</div>
|
||||
<div class="flex-1 truncate">
|
||||
<span class="block font-semibold text-xl leading-tight"> 20</span>
|
||||
<span class="text-base font-semibold text-gray-500"
|
||||
>Total Products</span
|
||||
>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-gray-900">{{ stats.pendingTasks }}</div>
|
||||
<div class="text-sm text-gray-500">Pending Tasks</div>
|
||||
</div>
|
||||
</div>
|
||||
<client-only>
|
||||
<VueApexCharts
|
||||
:key="changeKey"
|
||||
width="100%"
|
||||
height="53"
|
||||
:options="{
|
||||
...chartOptions,
|
||||
colors: ['#F97316'],
|
||||
yaxis: {
|
||||
min: 0,
|
||||
max: Math.max(...data3[0].data) + 10,
|
||||
},
|
||||
}"
|
||||
:series="data3"
|
||||
></VueApexCharts>
|
||||
</client-only>
|
||||
</rs-card>
|
||||
<!-- Summary Card #4 -->
|
||||
<rs-card>
|
||||
<div class="summary-4 pt-5 pb-3 px-5 flex items-center gap-4">
|
||||
<div
|
||||
class="p-5 flex justify-center items-center bg-blue-100 rounded-2xl"
|
||||
>
|
||||
<Icon class="text-blue-500" name="ic:outline-remove-red-eye"></Icon>
|
||||
|
||||
<rs-card class="hover:shadow-lg transition-shadow">
|
||||
<div class="p-6 flex items-center">
|
||||
<div class="p-4 bg-green-100 rounded-xl mr-4">
|
||||
<Icon name="material-symbols:task-alt" class="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
<div class="flex-1 truncate">
|
||||
<span class="block font-semibold text-xl leading-tight">
|
||||
2,452</span
|
||||
>
|
||||
<span class="text-base font-semibold text-gray-500"
|
||||
>Total Viewers</span
|
||||
>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-gray-900">{{ stats.completedInstances }}</div>
|
||||
<div class="text-sm text-gray-500">Completed Instances</div>
|
||||
</div>
|
||||
</div>
|
||||
<client-only>
|
||||
<VueApexCharts
|
||||
:key="changeKey"
|
||||
width="100%"
|
||||
height="53"
|
||||
:options="{
|
||||
...chartOptions,
|
||||
colors: ['#3B82F6'],
|
||||
yaxis: {
|
||||
min: 0,
|
||||
max: Math.max(...data4[0].data) + 10,
|
||||
},
|
||||
}"
|
||||
:series="data4"
|
||||
></VueApexCharts>
|
||||
</client-only>
|
||||
</rs-card>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row gap-x-6">
|
||||
<div class="w-12/2 md:w-8/12 flex flex-col">
|
||||
<!-- Graph -->
|
||||
<rs-card class="flex-1">
|
||||
<template #header> Transaction </template>
|
||||
<!-- Quick Actions -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<Icon name="material-symbols:flash-on" class="w-5 h-5 mr-2 text-yellow-500" />
|
||||
Quick Actions
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<client-only>
|
||||
<VueApexCharts
|
||||
:key="changeKey"
|
||||
width="100%"
|
||||
height="300"
|
||||
name="area"
|
||||
:options="chartOptionsTransaction"
|
||||
:series="transactionData"
|
||||
></VueApexCharts
|
||||
></client-only>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
|
||||
<div
|
||||
v-for="action in quickActions"
|
||||
:key="action.title"
|
||||
@click="action.action"
|
||||
class="p-4 border border-gray-200 rounded-lg hover:border-gray-300 hover:shadow-md transition-all cursor-pointer group"
|
||||
>
|
||||
<div class="flex items-center mb-3">
|
||||
<div :class="[action.color, 'p-2 rounded-lg mr-3 group-hover:scale-110 transition-transform']">
|
||||
<Icon :name="action.icon" class="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<h3 class="font-semibold text-gray-900 group-hover:text-blue-600 transition-colors">{{ action.title }}</h3>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600">{{ action.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
<rs-card class="flex-1">
|
||||
<template #header> Referral</template>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||
<!-- System Features -->
|
||||
<rs-card class="xl:col-span-1">
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<Icon name="material-symbols:featured-play-list" class="w-5 h-5 mr-2 text-blue-500" />
|
||||
System Features
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div
|
||||
v-for="(val, index) in customers"
|
||||
:key="index"
|
||||
class="flex justify-between items-center rounded-lg bg-[rgb(var(--bg-1))] p-5 first:mt-0 mt-3"
|
||||
<div class="space-y-6">
|
||||
<div
|
||||
v-for="feature in systemFeatures"
|
||||
:key="feature.id"
|
||||
class="border border-gray-200 rounded-lg p-4 hover:border-gray-300 hover:shadow-sm transition-all cursor-pointer group"
|
||||
@click="navigateToFeature(feature.route)"
|
||||
>
|
||||
<div class="flex items-center gap-x-4">
|
||||
<img
|
||||
src="@/assets/img/avatar/user.webp"
|
||||
class="h-10 w-10 rounded-lg"
|
||||
/>
|
||||
<div class="flex items-start">
|
||||
<div :class="[feature.colorLight, 'p-3 rounded-lg mr-4 group-hover:scale-105 transition-transform']">
|
||||
<Icon :name="feature.icon" :class="[feature.textColor, 'w-6 h-6']" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex flex-col">
|
||||
<h3 class="font-semibold text-gray-900 mb-1 group-hover:text-blue-600 transition-colors">{{ feature.title }}</h3>
|
||||
<p class="text-sm text-gray-600 mb-3">{{ feature.description }}</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span
|
||||
class="text-gray-900 dark:text-white font-semibold text-lg"
|
||||
v-for="item in feature.features.slice(0, 2)"
|
||||
:key="item"
|
||||
class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-full"
|
||||
>
|
||||
{{ val.name }}
|
||||
{{ item }}
|
||||
</span>
|
||||
<span class="text-gray-600 dark:text-gray-50 text-sm">
|
||||
RM{{ parseFloat(val.totalPurchase).toFixed(2) }} |
|
||||
{{ val.purchase }} sold
|
||||
<span
|
||||
v-if="feature.features.length > 2"
|
||||
class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-full"
|
||||
>
|
||||
+{{ feature.features.length - 2 }} more
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="flex items-center p-4 rounded-full bg-[rgb(var(--bg-2))] hover:bg-[rgb(var(--bg-2))]/10 shadow-md"
|
||||
>
|
||||
<Icon size="20px" name="ic:baseline-mail-outline"></Icon>
|
||||
</button>
|
||||
<Icon name="material-symbols:arrow-forward" class="w-5 h-5 text-gray-400 group-hover:text-blue-500 transition-colors" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
<div class="w-12/2 md:w-4/12 flex flex-col">
|
||||
<!-- Monthly Target Radial -->
|
||||
<rs-card class="flex-1">
|
||||
<template #header> Monthly Target </template>
|
||||
<template #body>
|
||||
<client-only>
|
||||
<VueApexCharts
|
||||
:key="changeKey"
|
||||
width="100%"
|
||||
height="300"
|
||||
name="radialBar"
|
||||
:options="chartOptionsRadial"
|
||||
:series="radialData"
|
||||
></VueApexCharts>
|
||||
</client-only>
|
||||
<hr class="my-4" />
|
||||
<p class="text-xl py-5 font-medium">Products</p>
|
||||
<div
|
||||
class="flex item-center gap-x-4"
|
||||
:class="{
|
||||
'mt-0': index === 0,
|
||||
'mt-3': index !== 0,
|
||||
}"
|
||||
v-for="(val, index) in ['A', 'B', 'C', 'D', 'E']"
|
||||
:key="index"
|
||||
>
|
||||
<img
|
||||
src="@/assets/img/default-thumbnail.jpg"
|
||||
class="h-20 w-20 object-cover rounded-lg"
|
||||
/>
|
||||
<div class="flex-1 flex items-center">
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<rs-card class="xl:col-span-1">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<Icon name="material-symbols:history" class="w-5 h-5 mr-2 text-gray-500" />
|
||||
Recent Activity
|
||||
</div>
|
||||
<RsButton variant="secondary" size="sm" @click="router.push('/execution/history')">
|
||||
View All
|
||||
</RsButton>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="space-y-4">
|
||||
<!-- Recent Processes -->
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 mb-3">Recent Processes</h4>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="process in recentProcesses.slice(0, 3)"
|
||||
:key="process.id"
|
||||
class="flex items-center justify-between p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer"
|
||||
@click="router.push(`/process-builder?id=${process.id}`)"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="w-2 h-2 bg-blue-500 rounded-full mr-3"></div>
|
||||
<div>
|
||||
<div class="font-medium text-sm text-gray-900">{{ process.name }}</div>
|
||||
<div class="text-xs text-gray-500">{{ formatDate(process.updatedAt) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<RsBadge :variant="process.status === 'published' ? 'success' : 'warning'" size="sm">
|
||||
{{ process.status }}
|
||||
</RsBadge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Tasks -->
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 mb-3">Pending Tasks</h4>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="task in recentTasks"
|
||||
:key="task.id"
|
||||
class="flex items-center justify-between p-3 bg-amber-50 rounded-lg hover:bg-amber-100 transition-colors cursor-pointer"
|
||||
@click="router.push('/execution/inbox')"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="w-2 h-2 bg-amber-500 rounded-full mr-3"></div>
|
||||
<div>
|
||||
<span class="font-semibold text-lg leading-tight"
|
||||
>Product {{ val }}</span
|
||||
>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
</p>
|
||||
<div class="font-medium text-sm text-gray-900">{{ task.title }}</div>
|
||||
<div class="text-xs text-gray-500">{{ task.process }} • Due {{ task.dueDate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Icon name="material-symbols:keyboard-arrow-right" class="w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
|
||||
<!-- Development Roadmap -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<Icon name="material-symbols:road" class="w-5 h-5 mr-2 text-purple-500" />
|
||||
Development Roadmap & System Guide
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="space-y-6">
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<h3 class="font-semibold text-blue-900 mb-2">🚀 Getting Started with Corrad BPM</h3>
|
||||
<p class="text-blue-800 text-sm mb-3">
|
||||
Follow these steps to start developing business processes in the system:
|
||||
</p>
|
||||
<ol class="list-decimal list-inside space-y-1 text-sm text-blue-800">
|
||||
<li><strong>Design Forms:</strong> Use Form Builder to create data collection forms</li>
|
||||
<li><strong>Create Processes:</strong> Use Process Builder to design workflow logic</li>
|
||||
<li><strong>Configure Integrations:</strong> Set up API calls and business rules</li>
|
||||
<li><strong>Test & Deploy:</strong> Test process execution and publish to production</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div
|
||||
v-for="phase in developmentRoadmap"
|
||||
:key="phase.phase"
|
||||
class="border border-gray-200 rounded-lg p-4"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h4 class="font-semibold text-gray-900">{{ phase.phase }}</h4>
|
||||
<RsBadge :class="getStatusColor(phase.status)" size="sm">
|
||||
{{ phase.status.replace('-', ' ') }}
|
||||
</RsBadge>
|
||||
</div>
|
||||
<ul class="space-y-2">
|
||||
<li
|
||||
v-for="item in phase.items"
|
||||
:key="item"
|
||||
class="flex items-start text-sm text-gray-600"
|
||||
>
|
||||
<Icon
|
||||
:name="phase.status === 'completed' ? 'material-symbols:check-circle' :
|
||||
phase.status === 'in-progress' ? 'material-symbols:radio-button-partial' :
|
||||
'material-symbols:radio-button-unchecked'"
|
||||
:class="phase.status === 'completed' ? 'text-green-500' :
|
||||
phase.status === 'in-progress' ? 'text-blue-500' :
|
||||
'text-gray-400'"
|
||||
class="w-4 h-4 mr-2 mt-0.5 flex-shrink-0"
|
||||
/>
|
||||
{{ item }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Custom hover effects and transitions */
|
||||
.group:hover .group-hover\:scale-105 {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.group:hover .group-hover\:scale-110 {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
</style>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useVariableStore } from '~/stores/variableStore';
|
||||
import { useProcessBuilderStore } from '~/stores/processBuilder';
|
||||
import NotificationManager from '~/components/process-flow/notification/NotificationManager.vue';
|
||||
|
||||
// Define page meta
|
||||
@ -13,28 +13,21 @@ definePageMeta({
|
||||
});
|
||||
|
||||
// Initialize the store
|
||||
const variableStore = useVariableStore();
|
||||
const processStore = useProcessBuilderStore();
|
||||
|
||||
// Get available variables for usage in notifications
|
||||
const availableVariables = computed(() => {
|
||||
const processVars = variableStore.getAllVariables.process.map(v => ({
|
||||
name: v.name || 'unnamed',
|
||||
label: v?.description
|
||||
? `${v.description} (${v.name || 'unnamed'}, process)`
|
||||
: `${v.name || 'unnamed'} (process)` ,
|
||||
type: v.type || 'string',
|
||||
scope: 'process'
|
||||
const processVariables = processStore.getProcessVariables();
|
||||
if (!processVariables || typeof processVariables !== 'object') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(processVariables).map(([name, variable]) => ({
|
||||
name: name,
|
||||
label: `${name} (${variable.type || 'string'})`,
|
||||
type: variable.type || 'string',
|
||||
scope: variable.scope || 'process'
|
||||
}));
|
||||
const globalVars = variableStore.getAllVariables.global.map(v => ({
|
||||
name: v.name || 'unnamed',
|
||||
label: v?.description
|
||||
? `${v.description} (${v.name || 'unnamed'}, global)`
|
||||
: `${v.name || 'unnamed'} (global)` ,
|
||||
type: v.type || 'string',
|
||||
scope: 'global'
|
||||
}));
|
||||
|
||||
return [...processVars, ...globalVars];
|
||||
});
|
||||
|
||||
// Current user ID
|
||||
|
@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed, shallowRef, onUnmounted, nextTick, watch } from 'vue';
|
||||
import { useProcessBuilderStore } from '~/stores/processBuilder';
|
||||
import { useVariableStore } from '~/stores/variableStore';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import ProcessFlowCanvas from '~/components/process-flow/ProcessFlowCanvas.vue';
|
||||
import ProcessBuilderComponents from '~/components/process-flow/ProcessBuilderComponents.vue';
|
||||
@ -18,6 +17,7 @@ import FormNodeConfigurationModal from '~/components/process-flow/FormNodeConfig
|
||||
import BusinessRuleNodeConfiguration from '~/components/process-flow/BusinessRuleNodeConfiguration.vue';
|
||||
import BusinessRuleNodeConfigurationModal from '~/components/process-flow/BusinessRuleNodeConfigurationModal.vue';
|
||||
import NotificationNodeConfigurationModal from '~/components/process-flow/NotificationNodeConfigurationModal.vue';
|
||||
import ScriptNodeConfigurationModal from '~/components/process-flow/ScriptNodeConfigurationModal.vue';
|
||||
import ProcessTemplatesModal from '~/components/ProcessTemplatesModal.vue';
|
||||
import ProcessSettingsModal from '~/components/process-flow/ProcessSettingsModal.vue';
|
||||
import ProcessHistoryModal from '~/components/ProcessHistoryModal.vue';
|
||||
@ -35,7 +35,6 @@ definePageMeta({
|
||||
const processStore = useProcessBuilderStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const variableStore = useVariableStore();
|
||||
|
||||
// Initialize toast with fallback
|
||||
let toast;
|
||||
@ -86,6 +85,7 @@ const showApiConfigModal = ref(false);
|
||||
const showGatewayConfigModal = ref(false);
|
||||
const showBusinessRuleConfigModal = ref(false);
|
||||
const showNotificationConfigModal = ref(false);
|
||||
const showScriptConfigModal = ref(false);
|
||||
const showTemplatesModal = ref(false);
|
||||
const showProcessSettings = ref(false);
|
||||
const showDropdown = ref(false);
|
||||
@ -239,23 +239,21 @@ const nodeDefaultPath = computed({
|
||||
|
||||
// Computed for gateway available variables
|
||||
const gatewayAvailableVariables = computed(() => {
|
||||
const processVars = variableStore.getAllVariables.process.map(v => ({
|
||||
if (!processStore.currentProcess || !processStore.currentProcess.variables) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get variables from the current process instead of variable store
|
||||
const processVariables = Object.values(processStore.currentProcess.variables);
|
||||
|
||||
const allVars = processVariables.map(v => ({
|
||||
name: v.name || 'unnamed',
|
||||
label: v?.description
|
||||
? `${v.description} (${v.name || 'unnamed'}, process)`
|
||||
: `${v.name || 'unnamed'} (process)` ,
|
||||
label: `${v.name || 'unnamed'} (${v.type || 'string'})`,
|
||||
type: v.type || 'string',
|
||||
scope: 'process'
|
||||
scope: v.scope || 'global',
|
||||
description: v.description || ''
|
||||
}));
|
||||
const globalVars = variableStore.getAllVariables.global.map(v => ({
|
||||
name: v.name || 'unnamed',
|
||||
label: v?.description
|
||||
? `${v.description} (${v.name || 'unnamed'}, global)`
|
||||
: `${v.name || 'unnamed'} (global)` ,
|
||||
type: v.type || 'string',
|
||||
scope: 'global'
|
||||
}));
|
||||
const allVars = [...processVars, ...globalVars];
|
||||
|
||||
// console.log('Gateway available variables:', allVars);
|
||||
return allVars;
|
||||
});
|
||||
@ -897,12 +895,12 @@ const applyProcessTemplate = async (template) => {
|
||||
if (template.variables && template.variables.length > 0) {
|
||||
console.log('Adding template variables:', template.variables.length);
|
||||
|
||||
// Clear existing process variables first
|
||||
variableStore.clearAllProcessVariables();
|
||||
// Clear existing process variables first (they'll be loaded from the process)
|
||||
// Process variables are now managed directly in the process store
|
||||
|
||||
template.variables.forEach((variable) => {
|
||||
console.log(`Adding variable: ${variable.name} (${variable.type}) with scope: ${variable.scope}`);
|
||||
variableStore.addVariable({
|
||||
processStore.addProcessVariable({
|
||||
...variable,
|
||||
id: crypto.randomUUID() // Generate unique ID for the variable
|
||||
});
|
||||
@ -1014,6 +1012,41 @@ const handleNotificationNodeUpdate = (updatedData) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Handle script node update
|
||||
const handleScriptNodeUpdate = (updatedData) => {
|
||||
if (selectedNodeData.value && selectedNodeData.value.type === 'script') {
|
||||
// Make sure to update the label both in data and at the root level
|
||||
const newLabel = updatedData.label || 'Script Task';
|
||||
|
||||
// Update the data
|
||||
selectedNodeData.value.data = {
|
||||
...updatedData,
|
||||
label: newLabel // Ensure label is in data
|
||||
};
|
||||
|
||||
// Also update the root label
|
||||
selectedNodeData.value.label = newLabel;
|
||||
|
||||
// Add output variables to the process
|
||||
if (updatedData.outputVariables && Array.isArray(updatedData.outputVariables)) {
|
||||
updatedData.outputVariables.forEach(output => {
|
||||
if (output.name && output.name.trim()) {
|
||||
processStore.addProcessVariable({
|
||||
name: output.name,
|
||||
type: output.type || 'string',
|
||||
scope: 'global',
|
||||
value: null,
|
||||
description: output.description || `Output from ${newLabel}`
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update the node in store
|
||||
updateNodeInStore();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle process restoration from history
|
||||
const handleProcessRestored = (restoredProcess) => {
|
||||
// The process has been restored in the backend, so we need to reload it
|
||||
@ -1235,6 +1268,13 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
|
||||
Configure Notification
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<!-- Script Configuration -->
|
||||
<div v-if="selectedNodeData.type === 'script'">
|
||||
<RsButton @click="showScriptConfigModal = true" variant="primary" class="w-full">
|
||||
Configure Script Task
|
||||
</RsButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1308,7 +1348,7 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
|
||||
v-if="selectedNodeData && selectedNodeData.type === 'form'"
|
||||
v-model="showFormConfigModal"
|
||||
:nodeData="selectedNodeData.data"
|
||||
:availableVariables="variableStore.getAllVariables.global"
|
||||
:availableVariables="gatewayAvailableVariables"
|
||||
@update="handleFormNodeUpdate"
|
||||
/>
|
||||
|
||||
@ -1351,6 +1391,15 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
|
||||
@update="handleNotificationNodeUpdate"
|
||||
/>
|
||||
|
||||
<!-- Script Configuration Modal -->
|
||||
<ScriptNodeConfigurationModal
|
||||
v-if="selectedNodeData && selectedNodeData.type === 'script'"
|
||||
v-model="showScriptConfigModal"
|
||||
:nodeData="selectedNodeData.data"
|
||||
:availableVariables="gatewayAvailableVariables"
|
||||
@update="handleScriptNodeUpdate"
|
||||
/>
|
||||
|
||||
<!-- Process Templates Modal -->
|
||||
<ProcessTemplatesModal
|
||||
v-model="showTemplatesModal"
|
||||
|
@ -247,15 +247,11 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
||||
updatedAt: apiProcess.processModifiedDate
|
||||
};
|
||||
|
||||
|
||||
|
||||
this.currentProcess = process;
|
||||
|
||||
// Load variables into variable store
|
||||
if (process.variables) {
|
||||
const variableStore = useVariableStore();
|
||||
Object.entries(process.variables).forEach(([name, variable]) => {
|
||||
variableStore.addVariable(variable);
|
||||
});
|
||||
}
|
||||
// Variables are already loaded in process.variables, no need to sync with variable store
|
||||
|
||||
this.unsavedChanges = false;
|
||||
return { success: true, process };
|
||||
@ -308,13 +304,16 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
||||
if (!this.currentProcess) return false;
|
||||
|
||||
try {
|
||||
// Get variables from the current process, not from variable store
|
||||
const processVariables = this.currentProcess.variables || {};
|
||||
|
||||
const processData = {
|
||||
processName: this.currentProcess.name,
|
||||
processDescription: this.currentProcess.description,
|
||||
nodes: this.currentProcess.nodes,
|
||||
edges: this.currentProcess.edges,
|
||||
viewport: this.currentProcess.viewport || { x: 0, y: 0, zoom: 1 },
|
||||
variables: useVariableStore().getAllVariables.process || {},
|
||||
variables: processVariables,
|
||||
settings: this.currentProcess.settings || {},
|
||||
permissions: this.currentProcess.permissions || {}
|
||||
// Note: processStatus is intentionally NOT included here to preserve current status
|
||||
@ -326,6 +325,7 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
||||
processId: this.currentProcess.id,
|
||||
nodeCount: processData.nodes.length,
|
||||
edgeCount: processData.edges.length,
|
||||
variableCount: Object.keys(processVariables).length,
|
||||
nodes: processData.nodes.map(n => ({ id: n.id, type: n.type, label: n.label })),
|
||||
edges: processData.edges.map(e => ({ id: e.id, source: e.source, target: e.target }))
|
||||
});
|
||||
@ -808,14 +808,7 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
||||
*/
|
||||
clearHistory() {
|
||||
this.history = [];
|
||||
if (this.currentProcess) {
|
||||
this.history.push({
|
||||
state: JSON.parse(JSON.stringify(this.currentProcess)),
|
||||
action: 'Initial state',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
this.historyIndex = 0;
|
||||
this.historyIndex = -1;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -924,7 +917,7 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
||||
nodes: this.currentProcess.nodes,
|
||||
edges: this.currentProcess.edges,
|
||||
viewport: this.currentProcess.viewport || { x: 0, y: 0, zoom: 1 },
|
||||
variables: useVariableStore().getAllVariables.process || {},
|
||||
variables: this.currentProcess.variables || {},
|
||||
settings: this.currentProcess.settings || {},
|
||||
permissions: this.currentProcess.permissions || {},
|
||||
changeDescription: changeDescription || this.lastChangeDescription,
|
||||
@ -958,6 +951,78 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
||||
console.error('Error saving process:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Variable management methods
|
||||
/**
|
||||
* Add or update a variable in the current process
|
||||
*/
|
||||
addProcessVariable(variable) {
|
||||
if (!this.currentProcess) return;
|
||||
|
||||
if (!this.currentProcess.variables) {
|
||||
this.currentProcess.variables = {};
|
||||
}
|
||||
|
||||
this.currentProcess.variables[variable.name] = {
|
||||
name: variable.name,
|
||||
type: variable.type || 'string',
|
||||
scope: variable.scope || 'global',
|
||||
value: variable.value,
|
||||
description: variable.description || ''
|
||||
};
|
||||
|
||||
this.unsavedChanges = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an existing variable in the current process
|
||||
*/
|
||||
updateProcessVariable(name, updates) {
|
||||
if (!this.currentProcess || !this.currentProcess.variables || !this.currentProcess.variables[name]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentProcess.variables[name] = {
|
||||
...this.currentProcess.variables[name],
|
||||
...updates
|
||||
};
|
||||
|
||||
this.unsavedChanges = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a variable from the current process
|
||||
*/
|
||||
deleteProcessVariable(name) {
|
||||
if (!this.currentProcess || !this.currentProcess.variables) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete this.currentProcess.variables[name];
|
||||
this.unsavedChanges = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all variables from the current process
|
||||
*/
|
||||
getProcessVariables() {
|
||||
if (!this.currentProcess || !this.currentProcess.variables) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.values(this.currentProcess.variables);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a specific variable from the current process
|
||||
*/
|
||||
getProcessVariable(name) {
|
||||
if (!this.currentProcess || !this.currentProcess.variables) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.currentProcess.variables[name] || null;
|
||||
}
|
||||
}
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user