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:
parent
03000b710b
commit
84e8d8e42f
@ -101,7 +101,8 @@
|
||||
"inputMappings": [],
|
||||
"assignmentType": "roles",
|
||||
"outputMappings": [
|
||||
{ "formField": "kategori_asnaf", "processVariable": "kategoriAsnaf" }
|
||||
{ "formField": "kategori_asnaf", "processVariable": "kategoriAsnaf" },
|
||||
{ "formField": "nama_asnaf", "processVariable": "namaAsnaf" }
|
||||
],
|
||||
"fieldConditions": [],
|
||||
"assignmentVariable": "",
|
||||
@ -121,7 +122,10 @@
|
||||
{
|
||||
"id": "api-1752550319410",
|
||||
"data": {
|
||||
"body": { "data": "{ \"title\" : \"{todoTitle}\"}", "type": "raw" },
|
||||
"body": {
|
||||
"data": "{ \"kategori_asnaf\" : \"{kategoriAsnaf}\", \"nama_asnaf\" : \"{namaAsnaf}\"}",
|
||||
"type": "raw"
|
||||
},
|
||||
"label": "API Call",
|
||||
"shape": "rectangle",
|
||||
"apiUrl": "https://jsonplaceholder.typicode.com/posts",
|
||||
@ -146,7 +150,7 @@
|
||||
"id": "script-1752550430989",
|
||||
"data": {
|
||||
"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",
|
||||
"inputVariables": ["apiResponse"],
|
||||
"scriptLanguage": "javascript",
|
||||
@ -155,6 +159,31 @@
|
||||
"name": "todoTitle",
|
||||
"type": "string",
|
||||
"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",
|
||||
"data": {
|
||||
"label": "Show Result",
|
||||
"jsCode": "",
|
||||
"cssCode": ".result-card { background: #f9fafb; border: 1px solid #ddd; border-radius: 8px; padding: 16px; max-width: 400px; margin: 24px auto; }",
|
||||
"htmlCode": "<div class='result-card'>\n <h2>API Result</h2>\n <p>Todo Title: <strong>{{ processVariables.todoTitle }}</strong></p>\n</div>",
|
||||
"shape": "rectangle",
|
||||
"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';",
|
||||
"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,
|
||||
"borderColor": "#dddddd",
|
||||
"description": "Display the todo title from API",
|
||||
"inputVariables": ["todoTitle"],
|
||||
"inputVariables": ["todoTitle", "todoStatus"],
|
||||
"backgroundColor": "#ffffff",
|
||||
"outputVariables": [],
|
||||
"allowVariableAccess": true
|
||||
},
|
||||
@ -206,8 +239,8 @@
|
||||
}
|
||||
],
|
||||
"viewport": {
|
||||
"x": -118.4524312896406,
|
||||
"y": 314.4180761099366,
|
||||
"zoom": 0.6437632135306554
|
||||
"x": -193.044397463002,
|
||||
"y": 197.8681289640592,
|
||||
"zoom": 1.049154334038055
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,18 @@
|
||||
"value": null,
|
||||
"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": {
|
||||
"name": "apiResponse",
|
||||
"type": "object",
|
||||
@ -19,10 +31,28 @@
|
||||
"scope": "global",
|
||||
"description": ""
|
||||
},
|
||||
"todoTitle": {
|
||||
"name": "todoTitle",
|
||||
"todoStatus": {
|
||||
"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",
|
||||
"scope": "global",
|
||||
"description": "Title from API response"
|
||||
"description": "Timestamp when script ran"
|
||||
},
|
||||
"resultSummary": {
|
||||
"name": "resultSummary",
|
||||
"type": "string",
|
||||
"scope": "global",
|
||||
"description": "Summary string for result"
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
description: "Create and design your workflows and forms",
|
||||
|
@ -33,6 +33,7 @@ const showModalEditForm = ref({
|
||||
name: "",
|
||||
path: "",
|
||||
guardType: "",
|
||||
external: false,
|
||||
});
|
||||
// const showModalEditEl = ref(null);
|
||||
|
||||
@ -41,6 +42,7 @@ const showModalAddForm = ref({
|
||||
title: "",
|
||||
name: "",
|
||||
path: "",
|
||||
external: false,
|
||||
});
|
||||
|
||||
const systemPages = [
|
||||
@ -120,6 +122,7 @@ const openModalEdit = (menu) => {
|
||||
showModalEditForm.value.path = menu.path;
|
||||
}
|
||||
|
||||
showModalEditForm.value.external = menu.external === true;
|
||||
showModalEditPath.value = menu.path;
|
||||
|
||||
showModalEdit.value = true;
|
||||
@ -140,6 +143,33 @@ const saveEditMenu = async () => {
|
||||
showModalEditForm.value.title = showModalEditForm.value.title.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", {
|
||||
method: "POST",
|
||||
initialCache: false,
|
||||
@ -148,9 +178,9 @@ const saveEditMenu = async () => {
|
||||
formData: {
|
||||
title: showModalEditForm.value.title || "",
|
||||
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.name = "";
|
||||
showModalAddForm.value.path = "";
|
||||
showModalAddForm.value.external = false;
|
||||
|
||||
showModalAdd.value = true;
|
||||
};
|
||||
@ -194,6 +225,33 @@ const saveAddMenu = async () => {
|
||||
showModalAddForm.value.title = showModalAddForm.value.title.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", {
|
||||
method: "POST",
|
||||
initialCache: false,
|
||||
@ -201,9 +259,9 @@ const saveAddMenu = async () => {
|
||||
formData: {
|
||||
title: showModalAddForm.value.title || "",
|
||||
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"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-if="!showModalEditForm.external"
|
||||
type="text"
|
||||
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."
|
||||
@ -712,6 +770,24 @@ watch(
|
||||
</div>
|
||||
</template>
|
||||
</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">
|
||||
<rs-button variant="outline" @click="showModalEdit = false">
|
||||
Cancel
|
||||
@ -742,8 +818,8 @@ watch(
|
||||
}"
|
||||
v-model="showModalAddForm.title"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-if="!showModalAddForm.external"
|
||||
type="text"
|
||||
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."
|
||||
@ -763,6 +839,24 @@ watch(
|
||||
</div>
|
||||
</template>
|
||||
</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">
|
||||
<rs-button variant="outline" @click="showModalAdd = false">
|
||||
Cancel
|
||||
|
@ -2,6 +2,7 @@
|
||||
import { ref, computed, onMounted, watch, onUnmounted } from 'vue';
|
||||
import { useProcessBuilderStore } from '~/stores/processBuilder';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useToast } from '~/composables/useToast';
|
||||
|
||||
// Define page meta
|
||||
definePageMeta({
|
||||
@ -15,6 +16,7 @@ definePageMeta({
|
||||
// Initialize the store and router
|
||||
const processStore = useProcessBuilderStore();
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
|
||||
// State
|
||||
const searchQuery = ref('');
|
||||
@ -334,6 +336,17 @@ onMounted(async () => {
|
||||
onUnmounted(() => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
@ -651,14 +664,15 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 ml-4">
|
||||
<!-- Run Workflow Button -->
|
||||
<!-- Copy Run Link Button (for published processes) -->
|
||||
<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"
|
||||
title="Run Workflow"
|
||||
:disabled="loading || process.status !== 'published'"
|
||||
title="Copy Run Link"
|
||||
:disabled="loading"
|
||||
>
|
||||
<Icon name="material-symbols:play-arrow" class="text-lg" />
|
||||
<Icon name="material-symbols:link" class="text-lg" />
|
||||
</button>
|
||||
|
||||
<!-- Analytics Button -->
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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 FormScriptEngine from '~/components/FormScriptEngine.vue';
|
||||
import ConditionalLogicEngine from '~/components/ConditionalLogicEngine.vue';
|
||||
@ -286,14 +286,33 @@ const handleFormSubmit = async () => {
|
||||
// --- Utility: Substitute variables in a string ---
|
||||
function substituteVariables(str, variables) {
|
||||
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];
|
||||
if (value === undefined || value === null) return '';
|
||||
if (
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
(typeof value === 'object' && value.name && value.type)
|
||||
) return '';
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(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 ---
|
||||
@ -507,10 +526,19 @@ const executeCurrentStep = async () => {
|
||||
}
|
||||
}
|
||||
} else if (currentNode.value?.type === 'script') {
|
||||
console.log(`[Workflow] Executing script node: ${currentNode.value.data?.label || currentNode.value.label}`);
|
||||
// Simulate script execution
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
const scriptCode = currentNode.value.data?.scriptCode;
|
||||
if (scriptCode) {
|
||||
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
|
||||
if (canAutoProgress(currentNode.value)) {
|
||||
moveToNextStep();
|
||||
@ -797,9 +825,10 @@ const totalSteps = computed(() => workflowPath.value.length);
|
||||
// Computed: Interpolated HTML content for HTML nodes
|
||||
const interpolatedHtmlContent = computed(() => {
|
||||
if (currentNode.value?.type !== 'html') return '';
|
||||
|
||||
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
|
||||
@ -810,6 +839,8 @@ const htmlNodeStyles = computed(() => {
|
||||
|
||||
// CSS injection for HTML nodes
|
||||
let currentStyleElement = null;
|
||||
// JS injection for HTML nodes
|
||||
let currentHtmlScriptElement = null;
|
||||
|
||||
// Function to inject CSS
|
||||
const injectHtmlNodeCSS = (cssCode) => {
|
||||
@ -818,27 +849,64 @@ const injectHtmlNodeCSS = (cssCode) => {
|
||||
currentStyleElement.remove();
|
||||
currentStyleElement = null;
|
||||
}
|
||||
|
||||
// Interpolate variables in CSS
|
||||
const processedCss = substituteVariables(cssCode, processVariables.value);
|
||||
// Add new styles if available
|
||||
if (cssCode && cssCode.trim()) {
|
||||
if (processedCss && processedCss.trim()) {
|
||||
currentStyleElement = document.createElement('style');
|
||||
currentStyleElement.textContent = cssCode;
|
||||
currentStyleElement.textContent = processedCss;
|
||||
currentStyleElement.setAttribute('data-workflow-html-node', 'true');
|
||||
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(htmlNodeStyles, (newStyles) => {
|
||||
injectHtmlNodeCSS(newStyles);
|
||||
}, { 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(() => {
|
||||
if (currentStyleElement) {
|
||||
currentStyleElement.remove();
|
||||
currentStyleElement = null;
|
||||
}
|
||||
if (currentHtmlScriptElement) {
|
||||
currentHtmlScriptElement.remove();
|
||||
currentHtmlScriptElement = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 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">
|
||||
<RsButton
|
||||
@click="makeDecision(edge.target)"
|
||||
variant="outline"
|
||||
variant="outline-primary"
|
||||
class="justify-start p-4 h-auto"
|
||||
>
|
||||
<div class="text-left">
|
||||
@ -1235,8 +1303,8 @@ function getNodeLabel(nodeId) {
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<!-- Variable Mapping Debug (only in development) -->
|
||||
<div v-if="currentNode.type === 'form'" class="bg-gray-100 rounded-lg p-4">
|
||||
<!-- Variable Mapping Debug (always visible for any node) -->
|
||||
<div v-if="currentNode" class="bg-gray-100 rounded-lg p-4">
|
||||
<details>
|
||||
<summary class="font-medium text-gray-700 cursor-pointer mb-2">Variable Mapping Debug</summary>
|
||||
<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>
|
||||
<pre class="text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(currentNode.data.inputMappings, null, 2) }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- Output Mappings -->
|
||||
<div v-if="currentNode.data?.outputMappings?.length">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Field Conditions -->
|
||||
<div v-if="currentNode.data?.fieldConditions?.length">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Current Field States -->
|
||||
<div v-if="Object.keys(fieldStates).length">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Form Data -->
|
||||
<div v-if="Object.keys(formData).length">
|
||||
<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>
|
||||
</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>
|
||||
</details>
|
||||
</div>
|
||||
|
@ -5,6 +5,14 @@ export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
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
|
||||
if (body.formData.path.slice(-1) != "/") {
|
||||
body.formData.path = body.formData.path + "/";
|
||||
|
@ -16,6 +16,14 @@ export default defineEventHandler(async (event) => {
|
||||
const oldFilePath = path.join(process.cwd(), "pages", oldPath, "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 {
|
||||
// Create template content
|
||||
const templateContent = buildNuxtTemplate({
|
||||
|
Loading…
x
Reference in New Issue
Block a user