Enhance Process Builder with New Variables and External Menu Support

- Added new variables for 'namaAsnaf', 'todoStatus', 'asnafScore', 'resultTimestamp', and 'resultSummary' to improve data handling in process definitions.
- Updated process definition JSON to include new output mappings and enhanced script logic for better API response handling.
- Implemented validation for external menu paths in the menu editor, ensuring proper URL formatting and handling for external links.
- Enhanced the user interface in the menu editor to support external URL inputs, improving user experience when adding or editing menu items.
- Updated API endpoints to skip file operations for external menus, streamlining the process of adding and editing external links.
This commit is contained in:
Md Afiq Iskandar 2025-07-16 11:08:38 +08:00
parent 03000b710b
commit 84e8d8e42f
8 changed files with 317 additions and 44 deletions

View File

@ -101,7 +101,8 @@
"inputMappings": [], "inputMappings": [],
"assignmentType": "roles", "assignmentType": "roles",
"outputMappings": [ "outputMappings": [
{ "formField": "kategori_asnaf", "processVariable": "kategoriAsnaf" } { "formField": "kategori_asnaf", "processVariable": "kategoriAsnaf" },
{ "formField": "nama_asnaf", "processVariable": "namaAsnaf" }
], ],
"fieldConditions": [], "fieldConditions": [],
"assignmentVariable": "", "assignmentVariable": "",
@ -121,7 +122,10 @@
{ {
"id": "api-1752550319410", "id": "api-1752550319410",
"data": { "data": {
"body": { "data": "{ \"title\" : \"{todoTitle}\"}", "type": "raw" }, "body": {
"data": "{ \"kategori_asnaf\" : \"{kategoriAsnaf}\", \"nama_asnaf\" : \"{namaAsnaf}\"}",
"type": "raw"
},
"label": "API Call", "label": "API Call",
"shape": "rectangle", "shape": "rectangle",
"apiUrl": "https://jsonplaceholder.typicode.com/posts", "apiUrl": "https://jsonplaceholder.typicode.com/posts",
@ -146,7 +150,7 @@
"id": "script-1752550430989", "id": "script-1752550430989",
"data": { "data": {
"label": "Script Task", "label": "Script Task",
"scriptCode": "// Assign API response title to process variable\nprocessVariables.todoTitle = processVariables.apiResponse?.title || '';\n// You can add more logic here\n", "scriptCode": "// Map API response to process variables\nconst api = processVariables.apiResponse || {};\nprocessVariables.todoTitle = api.kategori_asnaf || '';\nprocessVariables.namaAsnaf = api.nama_asnaf || '';\nprocessVariables.todoStatus = api.id > 100; // true if id > 100, otherwise false\n\n// New logic: Calculate a score\nconst katLen = (api.kategori_asnaf || '').length;\nconst namaLen = (api.nama_asnaf || '').length;\nprocessVariables.asnafScore = katLen * 10 + namaLen * 5;\n\n// New logic: Add a timestamp\nprocessVariables.resultTimestamp = new Date().toISOString();\n\n// New logic: Create a summary string\nprocessVariables.resultSummary = `Asnaf: ${processVariables.todoTitle}, Nama: ${processVariables.namaAsnaf}, Score: ${processVariables.asnafScore}, Time: ${processVariables.resultTimestamp}`;\n",
"description": "Execute JavaScript code", "description": "Execute JavaScript code",
"inputVariables": ["apiResponse"], "inputVariables": ["apiResponse"],
"scriptLanguage": "javascript", "scriptLanguage": "javascript",
@ -155,6 +159,31 @@
"name": "todoTitle", "name": "todoTitle",
"type": "string", "type": "string",
"description": "Title from API response" "description": "Title from API response"
},
{
"name": "namaAsnaf",
"type": "string",
"description": "Nama Asnaf from API response"
},
{
"name": "todoStatus",
"type": "boolean",
"description": "Todo Status from API response"
},
{
"name": "asnafScore",
"type": "number",
"description": "Calculated Asnaf Score"
},
{
"name": "resultTimestamp",
"type": "string",
"description": "Result Timestamp"
},
{
"name": "resultSummary",
"type": "string",
"description": "Result Summary"
} }
] ]
}, },
@ -166,12 +195,16 @@
"id": "html-1752550500000", "id": "html-1752550500000",
"data": { "data": {
"label": "Show Result", "label": "Show Result",
"jsCode": "", "shape": "rectangle",
"cssCode": ".result-card { background: #f9fafb; border: 1px solid #ddd; border-radius: 8px; padding: 16px; max-width: 400px; margin: 24px auto; }", "jsCode": "const completed = \"{{todoStatus}}\" === 'true' || \"{{todoStatus}}\" === true;\ndocument.getElementById('todo-title').innerText = \"{{todoTitle}}\";\ndocument.getElementById('todo-status').innerText = completed ? 'Completed ✅' : 'Not Completed ❌';\ndocument.getElementById('todo-status').className = completed ? 'done' : 'not-done';",
"htmlCode": "<div class='result-card'>\n <h2>API Result</h2>\n <p>Todo Title: <strong>{{ processVariables.todoTitle }}</strong></p>\n</div>", "cssCode": ".result-box {\n background-color: #ffffff;\n border-radius: 12px;\n box-shadow: 0 8px 16px rgba(0,0,0,0.1);\n padding: 30px 40px;\n max-width: 600px;\n text-align: center;\n }\n .result-box h2 {\n color: #333;\n margin-bottom: 20px;\n }\n .result-box p {\n font-size: 1.2em;\n color: #555;\n }\n .done {\n color: green;\n font-weight: bold;\n }\n .not-done {\n color: red;\n font-weight: bold;\n }",
"htmlCode": "<div class=\"result-box\">\n <h2>Asnaf Result</h2>\n <p><strong>Title:</strong> <span id=\"todo-title\">{{todoTitle}}</span></p>\n <p><strong>Status:</strong> <span id=\"todo-status\">{{todoStatus}}</span></p>\n <p><strong>Nama:</strong> {{namaAsnaf}}</p>\n <p><strong>Score:</strong> {{asnafScore}}</p>\n <p><strong>Timestamp:</strong> {{resultTimestamp}}</p>\n <p><strong>Summary:</strong> {{resultSummary}}</p>\n</div>",
"textColor": "#333333",
"autoRefresh": true, "autoRefresh": true,
"borderColor": "#dddddd",
"description": "Display the todo title from API", "description": "Display the todo title from API",
"inputVariables": ["todoTitle"], "inputVariables": ["todoTitle", "todoStatus"],
"backgroundColor": "#ffffff",
"outputVariables": [], "outputVariables": [],
"allowVariableAccess": true "allowVariableAccess": true
}, },
@ -206,8 +239,8 @@
} }
], ],
"viewport": { "viewport": {
"x": -118.4524312896406, "x": -193.044397463002,
"y": 314.4180761099366, "y": 197.8681289640592,
"zoom": 0.6437632135306554 "zoom": 1.049154334038055
} }
} }

View File

@ -6,6 +6,18 @@
"value": null, "value": null,
"description": "API error from API Call" "description": "API error from API Call"
}, },
"namaAsnaf": {
"name": "namaAsnaf",
"type": "string",
"scope": "global",
"description": ""
},
"todoTitle": {
"name": "todoTitle",
"type": "string",
"scope": "global",
"description": "Title from API response"
},
"apiResponse": { "apiResponse": {
"name": "apiResponse", "name": "apiResponse",
"type": "object", "type": "object",
@ -19,10 +31,28 @@
"scope": "global", "scope": "global",
"description": "" "description": ""
}, },
"todoTitle": { "todoStatus": {
"name": "todoTitle", "name": "todoStatus",
"type": "boolean",
"scope": "global",
"description": "Completion status (true if id > 100)"
},
"asnafScore": {
"name": "asnafScore",
"type": "number",
"scope": "global",
"description": "Calculated score based on input lengths"
},
"resultTimestamp": {
"name": "resultTimestamp",
"type": "string", "type": "string",
"scope": "global", "scope": "global",
"description": "Title from API response" "description": "Timestamp when script ran"
},
"resultSummary": {
"name": "resultSummary",
"type": "string",
"scope": "global",
"description": "Summary string for result"
} }
} }

View File

@ -12,6 +12,23 @@ export default [
}, },
], ],
}, },
// {
// header: "BPM",
// description: "Manage your BPM application",
// child: [
// {
// title: "Form",
// icon: "material-symbols:dynamic-form",
// path: "http://localhost:3000/workflow/7f024ce2-ce5d-43af-a18e-8e10d390e32b",
// external: true,
// },
// ],
// meta: {
// auth: {
// role: ["Developer"],
// },
// },
// },
{ {
header: "Design & Build", header: "Design & Build",
description: "Create and design your workflows and forms", description: "Create and design your workflows and forms",

View File

@ -33,6 +33,7 @@ const showModalEditForm = ref({
name: "", name: "",
path: "", path: "",
guardType: "", guardType: "",
external: false,
}); });
// const showModalEditEl = ref(null); // const showModalEditEl = ref(null);
@ -41,6 +42,7 @@ const showModalAddForm = ref({
title: "", title: "",
name: "", name: "",
path: "", path: "",
external: false,
}); });
const systemPages = [ const systemPages = [
@ -120,6 +122,7 @@ const openModalEdit = (menu) => {
showModalEditForm.value.path = menu.path; showModalEditForm.value.path = menu.path;
} }
showModalEditForm.value.external = menu.external === true;
showModalEditPath.value = menu.path; showModalEditPath.value = menu.path;
showModalEdit.value = true; showModalEdit.value = true;
@ -140,6 +143,33 @@ const saveEditMenu = async () => {
showModalEditForm.value.title = showModalEditForm.value.title.trim(); showModalEditForm.value.title = showModalEditForm.value.title.trim();
showModalEditForm.value.name = showModalEditForm.value.name.trim(); showModalEditForm.value.name = showModalEditForm.value.name.trim();
// Path validation and formatting
let path = showModalEditForm.value.path.trim();
if (showModalEditForm.value.external) {
// Validate as URL
if (!/^https?:\/\//.test(path)) {
nuxtApp.$swal.fire({
title: "Error",
text: "External URL must start with http:// or https://",
icon: "error",
});
return;
}
} else {
// Internal path validation
if (!/^[a-z0-9/-]+$/.test(path)) {
nuxtApp.$swal.fire({
title: "Error",
text: "Path contains invalid characters or spacing before or after. Only letters, numbers, dashes, and underscores are allowed.",
icon: "error",
});
return;
}
if (!path.startsWith("/")) {
path = "/" + path;
}
}
const res = await useFetch("/api/devtool/menu/edit", { const res = await useFetch("/api/devtool/menu/edit", {
method: "POST", method: "POST",
initialCache: false, initialCache: false,
@ -148,9 +178,9 @@ const saveEditMenu = async () => {
formData: { formData: {
title: showModalEditForm.value.title || "", title: showModalEditForm.value.title || "",
name: showModalEditForm.value.name || "", name: showModalEditForm.value.name || "",
path: "/" + showModalEditForm.value.path || "", path: path || "",
external: showModalEditForm.value.external === true,
}, },
// formData: showModalEditForm.value,
}), }),
}); });
@ -175,6 +205,7 @@ const openModalAdd = () => {
showModalAddForm.value.title = ""; showModalAddForm.value.title = "";
showModalAddForm.value.name = ""; showModalAddForm.value.name = "";
showModalAddForm.value.path = ""; showModalAddForm.value.path = "";
showModalAddForm.value.external = false;
showModalAdd.value = true; showModalAdd.value = true;
}; };
@ -194,6 +225,33 @@ const saveAddMenu = async () => {
showModalAddForm.value.title = showModalAddForm.value.title.trim(); showModalAddForm.value.title = showModalAddForm.value.title.trim();
showModalAddForm.value.name = showModalAddForm.value.name.trim(); showModalAddForm.value.name = showModalAddForm.value.name.trim();
// Path validation and formatting
let path = showModalAddForm.value.path.trim();
if (showModalAddForm.value.external) {
// Validate as URL
if (!/^https?:\/\//.test(path)) {
nuxtApp.$swal.fire({
title: "Error",
text: "External URL must start with http:// or https://",
icon: "error",
});
return;
}
} else {
// Internal path validation
if (!/^[a-z0-9/-]+$/.test(path)) {
nuxtApp.$swal.fire({
title: "Error",
text: "Path contains invalid characters or spacing before or after. Only letters, numbers, dashes, and underscores are allowed.",
icon: "error",
});
return;
}
if (!path.startsWith("/")) {
path = "/" + path;
}
}
const res = await useFetch("/api/devtool/menu/add", { const res = await useFetch("/api/devtool/menu/add", {
method: "POST", method: "POST",
initialCache: false, initialCache: false,
@ -201,9 +259,9 @@ const saveAddMenu = async () => {
formData: { formData: {
title: showModalAddForm.value.title || "", title: showModalAddForm.value.title || "",
name: showModalAddForm.value.name || "", name: showModalAddForm.value.name || "",
path: "/" + showModalAddForm.value.path || "", path: path || "",
external: showModalAddForm.value.external === true,
}, },
// formData: showModalAddForm.value
}), }),
}); });
@ -691,8 +749,8 @@ watch(
}" }"
v-model="showModalEditForm.title" v-model="showModalEditForm.title"
/> />
<FormKit <FormKit
v-if="!showModalEditForm.external"
type="text" type="text"
label="Path" label="Path"
help="If the last path name is '/', the name of the file will be from its name property. While if the last path name is not '/', the name of the file will be from its path property." help="If the last path name is '/', the name of the file will be from its name property. While if the last path name is not '/', the name of the file will be from its path property."
@ -712,6 +770,24 @@ watch(
</div> </div>
</template> </template>
</FormKit> </FormKit>
<FormKit
v-else
type="text"
label="External URL"
help="Enter a full URL (e.g., https://example.com)"
:validation="[['required'], ['matches', '/^https?:\/\//']]"
:validation-messages="{
required: 'URL is required',
matches: 'URL must start with http:// or https://',
}"
v-model="showModalEditForm.path"
/>
<FormKit
type="checkbox"
label="Is External URL?"
v-model="showModalEditForm.external"
help="Check if this menu item is an external URL."
/>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<rs-button variant="outline" @click="showModalEdit = false"> <rs-button variant="outline" @click="showModalEdit = false">
Cancel Cancel
@ -742,8 +818,8 @@ watch(
}" }"
v-model="showModalAddForm.title" v-model="showModalAddForm.title"
/> />
<FormKit <FormKit
v-if="!showModalAddForm.external"
type="text" type="text"
label="Path" label="Path"
help="If the last path name is '/', the name of the file will be from its name property. While if the last path name is not '/', the name of the file will be from its path property." help="If the last path name is '/', the name of the file will be from its name property. While if the last path name is not '/', the name of the file will be from its path property."
@ -763,6 +839,24 @@ watch(
</div> </div>
</template> </template>
</FormKit> </FormKit>
<FormKit
v-else
type="text"
label="External URL"
help="Enter a full URL (e.g., https://example.com)"
:validation="[['required'], ['matches', '/^https?:\/\//']]"
:validation-messages="{
required: 'URL is required',
matches: 'URL must start with http:// or https://',
}"
v-model="showModalAddForm.path"
/>
<FormKit
type="checkbox"
label="Is External URL?"
v-model="showModalAddForm.external"
help="Check if this menu item is an external URL."
/>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<rs-button variant="outline" @click="showModalAdd = false"> <rs-button variant="outline" @click="showModalAdd = false">
Cancel Cancel

View File

@ -2,6 +2,7 @@
import { ref, computed, onMounted, watch, onUnmounted } from 'vue'; import { ref, computed, onMounted, watch, onUnmounted } from 'vue';
import { useProcessBuilderStore } from '~/stores/processBuilder'; import { useProcessBuilderStore } from '~/stores/processBuilder';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useToast } from '~/composables/useToast';
// Define page meta // Define page meta
definePageMeta({ definePageMeta({
@ -15,6 +16,7 @@ definePageMeta({
// Initialize the store and router // Initialize the store and router
const processStore = useProcessBuilderStore(); const processStore = useProcessBuilderStore();
const router = useRouter(); const router = useRouter();
const toast = useToast();
// State // State
const searchQuery = ref(''); const searchQuery = ref('');
@ -334,6 +336,17 @@ onMounted(async () => {
onUnmounted(() => { onUnmounted(() => {
clearTimeout(searchTimeout); clearTimeout(searchTimeout);
}); });
// Copy workflow run link to clipboard
const copyWorkflowLink = async (processId) => {
try {
const link = `${window.location.origin}/workflow/${processId}`;
await navigator.clipboard.writeText(link);
toast.success('Run link copied to clipboard!');
} catch (err) {
toast.error('Failed to copy link');
}
};
</script> </script>
<template> <template>
@ -651,14 +664,15 @@ onUnmounted(() => {
</div> </div>
<div class="flex items-center gap-2 ml-4"> <div class="flex items-center gap-2 ml-4">
<!-- Run Workflow Button --> <!-- Copy Run Link Button (for published processes) -->
<button <button
@click="viewProcessWorkflow(process.id)" v-if="process.status === 'published'"
@click="copyWorkflowLink(process.id)"
class="p-2 text-green-600 hover:text-green-800 hover:bg-green-50 rounded-lg transition-colors" class="p-2 text-green-600 hover:text-green-800 hover:bg-green-50 rounded-lg transition-colors"
title="Run Workflow" title="Copy Run Link"
:disabled="loading || process.status !== 'published'" :disabled="loading"
> >
<Icon name="material-symbols:play-arrow" class="text-lg" /> <Icon name="material-symbols:link" class="text-lg" />
</button> </button>
<!-- Analytics Button --> <!-- Analytics Button -->

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'; import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import FormScriptEngine from '~/components/FormScriptEngine.vue'; import FormScriptEngine from '~/components/FormScriptEngine.vue';
import ConditionalLogicEngine from '~/components/ConditionalLogicEngine.vue'; import ConditionalLogicEngine from '~/components/ConditionalLogicEngine.vue';
@ -286,14 +286,33 @@ const handleFormSubmit = async () => {
// --- Utility: Substitute variables in a string --- // --- Utility: Substitute variables in a string ---
function substituteVariables(str, variables) { function substituteVariables(str, variables) {
if (typeof str !== 'string') return str; if (typeof str !== 'string') return str;
return str.replace(/\{([a-zA-Z0-9_]+)\}/g, (match, varName) => { // Replace {{variable}} first
str = str.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (match, varName) => {
const value = variables[varName]; const value = variables[varName];
if (value === undefined || value === null) return ''; if (
value === undefined ||
value === null ||
(typeof value === 'object' && value.name && value.type)
) return '';
if (typeof value === 'object') { if (typeof value === 'object') {
return JSON.stringify(value); return JSON.stringify(value);
} }
return String(value); return String(value);
}); });
// Then replace {variable}
str = str.replace(/\{([a-zA-Z0-9_]+)\}/g, (match, varName) => {
const value = variables[varName];
if (
value === undefined ||
value === null ||
(typeof value === 'object' && value.name && value.type)
) return '';
if (typeof value === 'object') {
return JSON.stringify(value);
}
return String(value);
});
return str;
} }
// --- Variable Mapping Functions --- // --- Variable Mapping Functions ---
@ -507,10 +526,19 @@ const executeCurrentStep = async () => {
} }
} }
} else if (currentNode.value?.type === 'script') { } else if (currentNode.value?.type === 'script') {
console.log(`[Workflow] Executing script node: ${currentNode.value.data?.label || currentNode.value.label}`); const scriptCode = currentNode.value.data?.scriptCode;
// Simulate script execution if (scriptCode) {
await new Promise(resolve => setTimeout(resolve, 500)); try {
// Expose processVariables to the script
window.processVariables = processVariables.value;
// Run the script code with processVariables in scope
// eslint-disable-next-line no-new-func
new Function('processVariables', scriptCode)(processVariables.value);
} catch (err) {
console.error('[Workflow] Error executing script node:', err);
error.value = 'Script execution failed: ' + (err.message || err);
}
}
// Only auto-progress if there's a single outgoing edge // Only auto-progress if there's a single outgoing edge
if (canAutoProgress(currentNode.value)) { if (canAutoProgress(currentNode.value)) {
moveToNextStep(); moveToNextStep();
@ -797,9 +825,10 @@ const totalSteps = computed(() => workflowPath.value.length);
// Computed: Interpolated HTML content for HTML nodes // Computed: Interpolated HTML content for HTML nodes
const interpolatedHtmlContent = computed(() => { const interpolatedHtmlContent = computed(() => {
if (currentNode.value?.type !== 'html') return ''; if (currentNode.value?.type !== 'html') return '';
const htmlContent = currentNode.value?.data?.htmlCode || currentNode.value?.data?.htmlContent || ''; const htmlContent = currentNode.value?.data?.htmlCode || currentNode.value?.data?.htmlContent || '';
return interpolateHtmlContent(htmlContent); // Interpolate variables in HTML
const interpolated = substituteVariables(htmlContent, processVariables.value);
return interpolateHtmlContent(interpolated);
}); });
// Computed: CSS styles for HTML nodes // Computed: CSS styles for HTML nodes
@ -810,6 +839,8 @@ const htmlNodeStyles = computed(() => {
// CSS injection for HTML nodes // CSS injection for HTML nodes
let currentStyleElement = null; let currentStyleElement = null;
// JS injection for HTML nodes
let currentHtmlScriptElement = null;
// Function to inject CSS // Function to inject CSS
const injectHtmlNodeCSS = (cssCode) => { const injectHtmlNodeCSS = (cssCode) => {
@ -818,27 +849,64 @@ const injectHtmlNodeCSS = (cssCode) => {
currentStyleElement.remove(); currentStyleElement.remove();
currentStyleElement = null; currentStyleElement = null;
} }
// Interpolate variables in CSS
const processedCss = substituteVariables(cssCode, processVariables.value);
// Add new styles if available // Add new styles if available
if (cssCode && cssCode.trim()) { if (processedCss && processedCss.trim()) {
currentStyleElement = document.createElement('style'); currentStyleElement = document.createElement('style');
currentStyleElement.textContent = cssCode; currentStyleElement.textContent = processedCss;
currentStyleElement.setAttribute('data-workflow-html-node', 'true'); currentStyleElement.setAttribute('data-workflow-html-node', 'true');
document.head.appendChild(currentStyleElement); document.head.appendChild(currentStyleElement);
} }
}; };
// Function to inject JS
const injectHtmlNodeJS = (jsCode) => {
// Remove previous script if exists
if (currentHtmlScriptElement) {
currentHtmlScriptElement.remove();
currentHtmlScriptElement = null;
}
// Expose processVariables to global scope
window.processVariables = processVariables.value;
// Interpolate variables in JS code
const processedJsCode = substituteVariables(jsCode, processVariables.value);
if (processedJsCode && processedJsCode.trim()) {
const script = document.createElement('script');
script.type = 'text/javascript';
script.textContent = processedJsCode;
script.setAttribute('data-workflow-html-node', 'true');
document.body.appendChild(script);
currentHtmlScriptElement = script;
}
};
// Watch for HTML node CSS changes and inject styles // Watch for HTML node CSS changes and inject styles
watch(htmlNodeStyles, (newStyles) => { watch(htmlNodeStyles, (newStyles) => {
injectHtmlNodeCSS(newStyles); injectHtmlNodeCSS(newStyles);
}, { immediate: true }); }, { immediate: true });
// Cleanup styles on unmount // Watch for HTML node JS changes and inject script
watch(
() => currentNode.value?.type === 'html' ? currentNode.value?.data?.jsCode : '',
(newJsCode) => {
nextTick(() => {
injectHtmlNodeJS(newJsCode);
});
},
{ immediate: true }
);
// Cleanup styles and JS on unmount
onUnmounted(() => { onUnmounted(() => {
if (currentStyleElement) { if (currentStyleElement) {
currentStyleElement.remove(); currentStyleElement.remove();
currentStyleElement = null; currentStyleElement = null;
} }
if (currentHtmlScriptElement) {
currentHtmlScriptElement.remove();
currentHtmlScriptElement = null;
}
}); });
// Helper: Get next node object for single-path nodes // Helper: Get next node object for single-path nodes
@ -1189,7 +1257,7 @@ function getNodeLabel(nodeId) {
<template v-for="edge in workflowData.edges.filter(e => e.source === currentNode.id)" :key="edge.id"> <template v-for="edge in workflowData.edges.filter(e => e.source === currentNode.id)" :key="edge.id">
<RsButton <RsButton
@click="makeDecision(edge.target)" @click="makeDecision(edge.target)"
variant="outline" variant="outline-primary"
class="justify-start p-4 h-auto" class="justify-start p-4 h-auto"
> >
<div class="text-left"> <div class="text-left">
@ -1235,8 +1303,8 @@ function getNodeLabel(nodeId) {
</RsButton> </RsButton>
</div> </div>
<!-- Variable Mapping Debug (only in development) --> <!-- Variable Mapping Debug (always visible for any node) -->
<div v-if="currentNode.type === 'form'" class="bg-gray-100 rounded-lg p-4"> <div v-if="currentNode" class="bg-gray-100 rounded-lg p-4">
<details> <details>
<summary class="font-medium text-gray-700 cursor-pointer mb-2">Variable Mapping Debug</summary> <summary class="font-medium text-gray-700 cursor-pointer mb-2">Variable Mapping Debug</summary>
<div class="space-y-3 text-xs"> <div class="space-y-3 text-xs">
@ -1245,30 +1313,31 @@ function getNodeLabel(nodeId) {
<p class="font-medium text-gray-700">Input Mappings (Process Form):</p> <p class="font-medium text-gray-700">Input Mappings (Process Form):</p>
<pre class="text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(currentNode.data.inputMappings, null, 2) }}</pre> <pre class="text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(currentNode.data.inputMappings, null, 2) }}</pre>
</div> </div>
<!-- Output Mappings --> <!-- Output Mappings -->
<div v-if="currentNode.data?.outputMappings?.length"> <div v-if="currentNode.data?.outputMappings?.length">
<p class="font-medium text-gray-700">Output Mappings (Form Process):</p> <p class="font-medium text-gray-700">Output Mappings (Form Process):</p>
<pre class="text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(currentNode.data.outputMappings, null, 2) }}</pre> <pre class="text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(currentNode.data.outputMappings, null, 2) }}</pre>
</div> </div>
<!-- Field Conditions --> <!-- Field Conditions -->
<div v-if="currentNode.data?.fieldConditions?.length"> <div v-if="currentNode.data?.fieldConditions?.length">
<p class="font-medium text-gray-700">Field Conditions:</p> <p class="font-medium text-gray-700">Field Conditions:</p>
<pre class="text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(currentNode.data.fieldConditions, null, 2) }}</pre> <pre class="text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(currentNode.data.fieldConditions, null, 2) }}</pre>
</div> </div>
<!-- Current Field States --> <!-- Current Field States -->
<div v-if="Object.keys(fieldStates).length"> <div v-if="Object.keys(fieldStates).length">
<p class="font-medium text-gray-700">Active Field States:</p> <p class="font-medium text-gray-700">Active Field States:</p>
<pre class="text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(fieldStates, null, 2) }}</pre> <pre class="text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(fieldStates, null, 2) }}</pre>
</div> </div>
<!-- Form Data --> <!-- Form Data -->
<div v-if="Object.keys(formData).length"> <div v-if="Object.keys(formData).length">
<p class="font-medium text-gray-700">Current Form Data:</p> <p class="font-medium text-gray-700">Current Form Data:</p>
<pre class="text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(formData, null, 2) }}</pre> <pre class="text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(formData, null, 2) }}</pre>
</div> </div>
<!-- Node Data (for non-form nodes) -->
<div v-if="currentNode.data && !currentNode.data.inputMappings && !currentNode.data.outputMappings && !currentNode.data.fieldConditions">
<p class="font-medium text-gray-700">Node Data:</p>
<pre class="text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(currentNode.data, null, 2) }}</pre>
</div>
</div> </div>
</details> </details>
</div> </div>

View File

@ -5,6 +5,14 @@ export default defineEventHandler(async (event) => {
const body = await readBody(event); const body = await readBody(event);
try { try {
// If external, skip file operations
if (body.formData.external === true) {
return {
statusCode: 200,
message: "External menu successfully added!",
};
}
// Check if last character is not slash // Check if last character is not slash
if (body.formData.path.slice(-1) != "/") { if (body.formData.path.slice(-1) != "/") {
body.formData.path = body.formData.path + "/"; body.formData.path = body.formData.path + "/";

View File

@ -16,6 +16,14 @@ export default defineEventHandler(async (event) => {
const oldFilePath = path.join(process.cwd(), "pages", oldPath, "index.vue"); const oldFilePath = path.join(process.cwd(), "pages", oldPath, "index.vue");
const newFilePath = path.join(process.cwd(), "pages", newPath, "index.vue"); const newFilePath = path.join(process.cwd(), "pages", newPath, "index.vue");
// If external, skip file operations
if (body.formData.external === true) {
return {
statusCode: 200,
message: "External menu successfully updated!",
};
}
try { try {
// Create template content // Create template content
const templateContent = buildNuxtTemplate({ const templateContent = buildNuxtTemplate({