Enhance Documentation for Gateway Decision Logic in Process Builder

- Added detailed documentation for the new Gateway Decision Logic, including UI components for gateway condition configuration and evaluation processes.
- Introduced examples for condition evaluation and decision path selection, improving clarity for users on how to implement and utilize gateway conditions.
- Updated the process definition and variables to support the new gateway logic, ensuring seamless integration within the existing workflow.
- Enhanced the user interface to display decision paths and condition evaluation results, providing better feedback during workflow execution.
This commit is contained in:
Md Afiq Iskandar 2025-07-25 10:05:37 +08:00
parent 03fdbb5d96
commit 6009b1ecbe
6 changed files with 649 additions and 1124 deletions

View File

@ -1,309 +0,0 @@
this.hideField("form_jeniskp_1");
this.hideField("form_jeniskp_2");
this.hideField("form_jeniskp_3");
this.onFieldChange("select_1", (value) => {
this.hideField("form_jeniskp_1");
this.hideField("form_jeniskp_2");
this.hideField("form_jeniskp_3");
if (value && value.trim()) {
if (value == "jeniskp_1") this.showField("form_jeniskp_1");
if (value == "jeniskp_2") this.showField("form_jeniskp_2");
if (value == "jeniskp_3") this.showField("form_jeniskp_3");
}
});
// Conditional Logic Script
// Conditional Logic Script
// Conditional Logic Script
// Conditional Logic Script
// Conditional Logic Script
// Conditional Logic Script
// Conditional logic for field: nyatakan_lain2
onFieldChange("radio_bangsa", function () {
if (getField("radio_bangsa") !== "lain") {
hideField("nyatakan_lain2");
} else {
showField("nyatakan_lain2");
}
});
// Initial evaluation for field: nyatakan_lain2
(function () {
if (getField("radio_bangsa") !== "lain") {
hideField("nyatakan_lain2");
} else {
showField("nyatakan_lain2");
}
})();
// Conditional Logic Script
// Conditional logic for field: nyatakan_lain2
onFieldChange("radio_bangsa", function () {
if (getField("radio_bangsa") !== "lain") {
hideField("nyatakan_lain2");
} else {
showField("nyatakan_lain2");
}
});
// Initial evaluation for field: nyatakan_lain2
(function () {
if (getField("radio_bangsa") !== "lain") {
hideField("nyatakan_lain2");
} else {
showField("nyatakan_lain2");
}
})();
// Conditional Logic Script
// Conditional logic for field: nyatakan_lain2
onFieldChange("radio_bangsa", function () {
if (getField("radio_bangsa") !== "lain") {
hideField("nyatakan_lain2");
} else {
showField("nyatakan_lain2");
}
});
// Initial evaluation for field: nyatakan_lain2
(function () {
if (getField("radio_bangsa") !== "lain") {
hideField("nyatakan_lain2");
} else {
showField("nyatakan_lain2");
}
})();
// Conditional logic for field: text_14
onFieldChange("radio_pendidikan", function () {
if (getField("radio_pendidikan") !== "lain") {
hideField("text_14");
} else {
showField("text_14");
}
});
// Initial evaluation for field: text_14
(function () {
if (getField("radio_pendidikan") !== "lain") {
hideField("text_14");
} else {
showField("text_14");
}
})();
// Conditional Logic Script
// Conditional logic for field: nyatakan_lain2
onFieldChange("radio_bangsa", function () {
if (getField("radio_bangsa") !== "lain") {
hideField("nyatakan_lain2");
} else {
showField("nyatakan_lain2");
}
});
// Initial evaluation for field: nyatakan_lain2
(function () {
if (getField("radio_bangsa") !== "lain") {
hideField("nyatakan_lain2");
} else {
showField("nyatakan_lain2");
}
})();
// Conditional logic for field: text_14
onFieldChange("radio_pendidikan", function () {
if (getField("radio_pendidikan") !== "lain") {
hideField("text_14");
} else {
showField("text_14");
}
});
// Initial evaluation for field: text_14
(function () {
if (getField("radio_pendidikan") !== "lain") {
hideField("text_14");
} else {
showField("text_14");
}
})();
// Conditional Logic Script
// Conditional logic for field: nyatakan_lain2
onFieldChange("radio_bangsa", function () {
if (getField("radio_bangsa") !== "lain") {
hideField("nyatakan_lain2");
} else {
showField("nyatakan_lain2");
}
});
// Initial evaluation for field: nyatakan_lain2
(function () {
if (getField("radio_bangsa") !== "lain") {
hideField("nyatakan_lain2");
} else {
showField("nyatakan_lain2");
}
})();
// Conditional logic for field: text_14
onFieldChange("radio_pendidikan", function () {
if (getField("radio_pendidikan") !== "lain") {
hideField("text_14");
} else {
showField("text_14");
}
});
// Initial evaluation for field: text_14
(function () {
if (getField("radio_pendidikan") !== "lain") {
hideField("text_14");
} else {
showField("text_14");
}
})();
// Conditional Logic Script
// Conditional logic for field: nyatakan_lain2
onFieldChange("radio_bangsa", function () {
if (getField("radio_bangsa") !== "lain") {
hideField("nyatakan_lain2");
} else {
showField("nyatakan_lain2");
}
});
// Initial evaluation for field: nyatakan_lain2
(function () {
if (getField("radio_bangsa") !== "lain") {
hideField("nyatakan_lain2");
} else {
showField("nyatakan_lain2");
}
})();
// Conditional logic for field: text_14
onFieldChange("radio_pendidikan", function () {
if (getField("radio_pendidikan") !== "lain") {
hideField("text_14");
} else {
showField("text_14");
}
});
// Initial evaluation for field: text_14
(function () {
if (getField("radio_pendidikan") !== "lain") {
hideField("text_14");
} else {
showField("text_14");
}
})();
// Conditional Logic Script
// Conditional logic for field: nyatakan_lain2
onFieldChange("radio_bangsa", function () {
if (getField("radio_bangsa") !== "lain") {
hideField("nyatakan_lain2");
} else {
showField("nyatakan_lain2");
}
});
// Initial evaluation for field: nyatakan_lain2
(function () {
if (getField("radio_bangsa") !== "lain") {
hideField("nyatakan_lain2");
} else {
showField("nyatakan_lain2");
}
})();
// Conditional logic for field: text_14
onFieldChange("radio_pendidikan", function () {
if (getField("radio_pendidikan") !== "lain") {
hideField("text_14");
} else {
showField("text_14");
}
});
// Initial evaluation for field: text_14
(function () {
if (getField("radio_pendidikan") !== "lain") {
hideField("text_14");
} else {
showField("text_14");
}
})();
// Hide "Nyatakan Hubungan Lain-lain" initially
this.hideField("hubungan_lain_nyatakan");
// Show/hide relationship specification field
this.onFieldChange("hubungan_keluarga", (value) => {
if (value && value.includes("lain_lain")) {
this.showField("hubungan_lain_nyatakan");
} else {
this.hideField("hubungan_lain_nyatakan");
}
});
// Hide "Sebab Pembayaran Tunai" initially
this.hideField("sebab_tunai");
// Show/hide cash payment reason field
this.onFieldChange("cara_pembayaran", (value) => {
if (value && value.includes("tunai")) {
this.showField("sebab_tunai");
} else {
this.hideField("sebab_tunai");
}
});
// Hide education specification field initially
this.hideField("pendidikan_lain_tanggungan");
// Show/hide education specification field
this.onFieldChange("pendidikan_tertinggi_tanggungan", (value) => {
if (value && value.includes("lain_lain")) {
this.showField("pendidikan_lain_tanggungan");
} else {
this.hideField("pendidikan_lain_tanggungan");
}
});
// Hide school information initially
this.hideField("maklumat_sekolah");
// Show/hide school information based on schooling status
this.onFieldChange("bersekolah_tanggungan", (value) => {
if (value === "ya") {
this.showField("maklumat_sekolah");
} else {
this.hideField("maklumat_sekolah");
}
});
// Handle repeating group conditional logic for each dependent
this.onFieldChange("tanggungan_maklumat", (value) => {
if (value && Array.isArray(value)) {
value.forEach((item, index) => {
// Handle race specification for each dependent
if (item.bangsa_tanggungan !== "lain_lain") {
// Hide the specification field for this item
const fieldName = `tanggungan_maklumat[${index}].bangsa_lain_tanggungan`;
// Note: Repeating group field hiding requires specific handling
}
});
}
});

View File

@ -2,9 +2,9 @@
{
"type": "heading",
"props": {
"name": "heading_2",
"name": "heading_kategori_asnaf",
"level": 2,
"value": "SEKSYEN A",
"value": "PILIHAN KATEGORI ASNAF",
"width": "100%",
"gridColumn": "span 12",
"conditionalLogic": {
@ -16,11 +16,10 @@
}
},
{
"type": "heading",
"type": "paragraph",
"props": {
"name": "heading_1",
"level": 3,
"value": "1. MAKLUMAT PENDAFTARAN ASNAF (KETUA KELUARGA)",
"name": "paragraph_kategori_asnaf",
"value": "Sila pilih kategori asnaf yang bersesuaian.",
"width": "100%",
"gridColumn": "span 12",
"conditionalLogic": {
@ -31,139 +30,19 @@
}
}
},
{
"type": "text",
"props": {
"help": "Untuk Mualaf, nama mengikut kad pengenalan",
"name": "text_3",
"type": "text",
"label": "Nama",
"width": "100%",
"gridColumn": "span 12",
"validation": "required",
"placeholder": "",
"conditionalLogic": {
"action": "show",
"enabled": false,
"operator": "and",
"conditions": []
}
}
},
{
"type": "select",
"props": {
"help": "",
"name": "select_1",
"type": "select",
"label": "Jenis Kad Pengenalan",
"width": "100%",
"options": [
{ "label": "Sila Pilih Jenis Kad Pengenalan", "value": "jeniskp" },
{ "label": "MyKad/MyKid", "value": "jeniskp_1" },
{ "label": "No. K/P/Polis/Tentera/No. Pasport", "value": "jeniskp_2" },
{ "label": "No. Sijil Beranak", "value": "jeniskp_3" }
],
"gridColumn": "span 12",
"validation": "required",
"placeholder": "",
"conditionalLogic": {
"action": "show",
"enabled": false,
"operator": "and",
"conditions": []
}
}
},
{
"type": "text",
"props": {
"help": "",
"name": "form_jeniskp_1",
"type": "text",
"label": "MyKad/MyKid",
"width": "100%",
"gridColumn": "span 12",
"validation": "required",
"placeholder": "",
"conditionalLogic": {
"action": "show",
"enabled": false,
"operator": "and",
"conditions": []
}
}
},
{
"type": "text",
"props": {
"help": "",
"name": "form_jeniskp_2",
"type": "text",
"label": "No. K/P/Polis/Tentera/No. Pasport",
"width": "100%",
"gridColumn": "span 12",
"validation": "required",
"placeholder": "",
"conditionalLogic": {
"action": "show",
"enabled": false,
"operator": "and",
"conditions": []
}
}
},
{
"type": "text",
"props": {
"help": "",
"name": "form_jeniskp_3",
"type": "text",
"label": "No. Sijil Beranak",
"width": "100%",
"gridColumn": "span 12",
"validation": "required",
"placeholder": "",
"conditionalLogic": {
"action": "show",
"enabled": false,
"operator": "and",
"conditions": []
}
}
},
{
"type": "text",
"props": {
"help": "Isi Jika Berkenaan",
"name": "text_warganegara",
"type": "text",
"label": "Warganegara",
"width": "100%",
"gridColumn": "span 12",
"validation": "",
"placeholder": "",
"conditionalLogic": {
"action": "show",
"enabled": false,
"operator": "and",
"conditions": []
}
}
},
{
"type": "radio",
"props": {
"help": "",
"name": "radio_jantina",
"help": "Pilih kategori asnaf",
"name": "kategori_asnaf",
"type": "radio",
"label": "Jantina",
"width": "50%",
"label": "Kategori Asnaf",
"width": "100%",
"options": [
{ "label": "Lelaki", "value": "lelaki" },
{ "label": "Perempuan", "value": "perempuan" }
{ "label": "Fakir Miskin", "value": "fakir_miskin" },
{ "label": "Bukan Fakir Miskin", "value": "bukan_fakir_miskin" }
],
"gridColumn": "span 6",
"gridColumn": "span 12",
"validation": "required",
"conditionalLogic": {
"action": "show",
@ -173,60 +52,17 @@
}
}
},
{
"type": "select",
"props": {
"help": "",
"name": "radio_bangsa",
"type": "select",
"label": "Bangsa",
"width": "50%",
"options": [
{ "label": "Melayu", "value": "melayu" },
{ "label": "Cina", "value": "cina" },
{ "label": "India", "value": "india" },
{ "label": "Lain-lain", "value": "lain" }
],
"gridColumn": "span 6",
"validation": "required",
"placeholder": "Select an option"
}
},
{
"type": "text",
"props": {
"help": "",
"name": "nyatakan_lain2",
"name": "nama_asnaf",
"type": "text",
"label": "Nyatakan (Bangsa Lain-Lain)",
"label": "Nama Asnaf",
"width": "100%",
"gridColumn": "span 12",
"validation": "",
"validation": "required",
"placeholder": "Enter text...",
"conditionalLogic": {
"action": "hide",
"enabled": true,
"operator": "and",
"conditions": [
{ "field": "radio_bangsa", "value": "lain", "operator": "not_equals" }
]
}
}
},
{
"type": "radio",
"props": {
"help": "",
"name": "radio_9_copy",
"type": "radio",
"label": "Bersekolah",
"width": "50%",
"options": [
{ "label": "Ya", "value": "ya" },
{ "label": "Tidak", "value": "tidak" }
],
"gridColumn": "span 6",
"validation": "",
"conditionalLogic": {
"action": "show",
"enabled": false,
@ -234,569 +70,5 @@
"conditions": []
}
}
},
{
"type": "select",
"props": {
"help": "",
"name": "radio_pendidikan",
"type": "select",
"label": "Pendidikan Tertinggi",
"width": "50%",
"options": [
{ "label": "Peringkat Rendah", "value": "rendah" },
{ "label": "SRP/PMR", "value": "srp" },
{ "label": "SPM", "value": "spm" },
{ "label": "Sijil", "value": "sijil" },
{ "label": "Diploma", "value": "diploma" },
{ "label": "STPM", "value": "stpm" },
{ "label": "Ijazah", "value": "ijazah" },
{ "label": "Lain-Lain", "value": "lain" }
],
"gridColumn": "span 6",
"validation": "required",
"placeholder": "Select an option"
}
},
{
"type": "text",
"props": {
"help": "",
"name": "text_14",
"type": "text",
"label": "Nyatakan (Pendidikan Tertinggi Lain-Lain)",
"width": "100%",
"gridColumn": "span 12",
"validation": "",
"placeholder": "Enter text...",
"conditionalLogic": {
"action": "hide",
"enabled": true,
"operator": "and",
"conditions": [
{
"field": "radio_pendidikan",
"value": "lain",
"operator": "not_equals"
}
]
}
}
},
{
"type": "date",
"props": {
"help": "Isi Jika Berkenaan",
"name": "date_masukislam",
"type": "date",
"label": "Tarikh Masuk Islam",
"width": "50%",
"gridColumn": "span 6",
"validation": "",
"conditionalLogic": {
"action": "show",
"enabled": false,
"operator": "and",
"conditions": []
}
}
},
{
"type": "date",
"props": {
"help": "Isi Jika Berkenaan",
"name": "date_masukislam_copy",
"type": "date",
"label": "Tarikh Mula Kelas fardu Ain Muallaf (KFAM)",
"width": "50%",
"gridColumn": "span 6",
"validation": "",
"conditionalLogic": {
"action": "show",
"enabled": false,
"operator": "and",
"conditions": []
}
}
},
{
"type": "select",
"props": {
"help": "",
"name": "select_statusperkahwinan",
"type": "select",
"label": "Status Perkahwinan",
"width": "100%",
"options": [
{ "label": "Berkahwin", "value": "berkahwin" },
{ "label": "Ibu Tinggal/Bapa Tinggal", "value": "ibubapatunggal" },
{ "label": "Bujang", "value": "bujang" },
{ "label": "Duda", "value": "duda" },
{ "label": "Janda", "value": "janda" },
{ "label": "Balu", "value": "balu" }
],
"gridColumn": "span 12",
"validation": "required",
"placeholder": "Select an option",
"conditionalLogic": {
"action": "show",
"enabled": false,
"operator": "and",
"conditions": []
}
}
},
{
"type": "heading",
"props": {
"name": "heading_seksyen_b",
"level": 2,
"value": "SEKSYEN B",
"width": "100%",
"gridColumn": "span 12",
"conditionalLogic": {
"action": "show",
"enabled": false,
"operator": "and",
"conditions": []
}
}
},
{
"type": "heading",
"props": {
"name": "heading_tanggungan",
"level": 3,
"value": "1. MAKLUMAT PERIBADI TANGGUNGAN (Ruangan WAJIB diisi)",
"width": "100%",
"gridColumn": "span 12",
"conditionalLogic": {
"action": "show",
"enabled": false,
"operator": "and",
"conditions": []
}
}
},
{
"type": "checkbox",
"props": {
"help": "Pilih semua yang berkenaan",
"name": "hubungan_keluarga",
"type": "checkbox",
"label": "Hubungan dengan Pemohon/Asnaf",
"width": "100%",
"options": [
{ "label": "Pasangan Pemohon", "value": "pasangan" },
{ "label": "Isteri Kedua", "value": "isteri_kedua" },
{ "label": "Isteri Ketiga", "value": "isteri_ketiga" },
{ "label": "Isteri Keempat", "value": "isteri_keempat" },
{ "label": "Ipar", "value": "ipar" },
{ "label": "Abang", "value": "abang" },
{ "label": "Bapa", "value": "bapa" },
{ "label": "Ibu", "value": "ibu" },
{ "label": "Kakak", "value": "kakak" },
{ "label": "Adik", "value": "adik" },
{ "label": "Anak", "value": "anak" },
{ "label": "Cucu", "value": "cucu" },
{ "label": "Bapa Mertua", "value": "bapa_mertua" },
{ "label": "Ibu Mertua", "value": "ibu_mertua" },
{ "label": "Lain-lain (Sila Nyatakan)", "value": "lain_lain" }
],
"gridColumn": "span 12",
"validation": "required"
}
},
{
"type": "text",
"props": {
"name": "hubungan_lain_nyatakan",
"type": "text",
"label": "Nyatakan Hubungan Lain-lain",
"width": "100%",
"gridColumn": "span 12",
"validation": "",
"placeholder": "Sila nyatakan hubungan lain-lain",
"conditionalLogic": {
"action": "hide",
"enabled": true,
"operator": "and",
"conditions": [
{
"field": "hubungan_keluarga",
"value": "lain_lain",
"operator": "not_contains"
}
]
}
}
},
{
"type": "repeating-group",
"props": {
"help": "Tambah maklumat untuk setiap tanggungan",
"name": "tanggungan_maklumat",
"label": "Maklumat Tanggungan",
"width": "100%",
"fields": [
{
"name": "nama_tanggungan",
"type": "text",
"label": "Nama (Untuk Mualaf, nama mengikut kad pengenalan)",
"validation": "required",
"placeholder": "Masukkan nama lengkap"
},
{
"name": "jenis_kad_tanggungan",
"type": "select",
"label": "Jenis Pengenalan",
"options": [
{ "label": "MyKad/MyKid", "value": "mykad" },
{
"label": "No. K/P/Polis/Tentera/No. Pasport",
"value": "kp_polis"
},
{ "label": "No. Sijil Beranak", "value": "sijil_beranak" }
],
"validation": "required"
},
{
"name": "no_pengenalan_tanggungan",
"type": "text",
"label": "No. Pengenalan",
"validation": "required",
"placeholder": "Masukkan nombor pengenalan"
},
{
"name": "jantina_tanggungan",
"type": "radio",
"label": "Jantina",
"options": [
{ "label": "Lelaki", "value": "lelaki" },
{ "label": "Perempuan", "value": "perempuan" }
],
"validation": "required"
},
{
"name": "tarikh_lahir_tanggungan",
"type": "date",
"label": "Tarikh Lahir",
"validation": "required"
},
{
"name": "tempat_lahir_tanggungan",
"type": "text",
"label": "Tempat Lahir",
"placeholder": "Masukkan tempat lahir"
},
{
"name": "bangsa_tanggungan",
"type": "select",
"label": "Bangsa",
"options": [
{ "label": "Melayu", "value": "melayu" },
{ "label": "Cina", "value": "cina" },
{ "label": "India", "value": "india" },
{ "label": "Lain-lain (Sila Nyatakan)", "value": "lain_lain" }
],
"validation": "required"
},
{
"name": "bangsa_lain_tanggungan",
"type": "text",
"label": "Nyatakan Bangsa Lain-lain",
"placeholder": "Sila nyatakan"
},
{
"name": "status_kahwin_tanggungan",
"type": "select",
"label": "Status Perkahwinan",
"options": [
{ "label": "Berkahwin", "value": "berkahwin" },
{
"label": "Ibu Tinggal/Bapa Tinggal",
"value": "ibu_bapa_tinggal"
},
{ "label": "Bujang", "value": "bujang" },
{ "label": "Duda", "value": "duda" },
{ "label": "Janda", "value": "janda" },
{ "label": "Balu", "value": "balu" }
]
},
{
"help": "Jika Berkenaan",
"name": "tarikh_masuk_islam_tanggungan",
"type": "date",
"label": "Tarikh Masuk Islam"
},
{
"help": "Jika Berkenaan",
"name": "tarikh_mula_kfam_tanggungan",
"type": "date",
"label": "Tarikh Mula KFAM"
},
{
"help": "Jika Berkenaan",
"name": "warganegara_tanggungan",
"type": "text",
"label": "Warganegara",
"placeholder": "Masukkan warganegara"
},
{
"name": "tempat_menetap_tanggungan",
"type": "text",
"label": "Tempoh Menetap Di Selangor",
"placeholder": "Contoh: 5 Tahun"
},
{
"name": "no_telefon_tanggungan",
"type": "text",
"label": "No. Telefon/Telefon Bimbit",
"placeholder": "Contoh: 03-12345678"
}
],
"maxItems": 10,
"minItems": 1,
"buttonText": "Tambah Tanggungan",
"gridColumn": "span 12",
"removeText": "Buang"
}
},
{
"type": "heading",
"props": {
"name": "heading_maklumat_perbankan",
"level": 3,
"value": "MAKLUMAT PERBANKAN (Jika Berkenaan)",
"width": "100%",
"gridColumn": "span 12"
}
},
{
"type": "text",
"props": {
"name": "nama_pemegang_akaun",
"type": "text",
"label": "Nama Pemegang Akaun",
"width": "100%",
"gridColumn": "span 12",
"placeholder": "Masukkan nama pemegang akaun"
}
},
{
"type": "text",
"props": {
"name": "nama_bank",
"type": "text",
"label": "Bank",
"width": "50%",
"gridColumn": "span 6",
"placeholder": "Masukkan nama bank"
}
},
{
"type": "text",
"props": {
"name": "no_akaun_bank",
"type": "text",
"label": "No. Akaun Bank",
"width": "50%",
"gridColumn": "span 6",
"placeholder": "Masukkan nombor akaun"
}
},
{
"type": "checkbox",
"props": {
"name": "cara_pembayaran",
"type": "checkbox",
"label": "Cara Pembayaran",
"width": "100%",
"options": [
{ "label": "Akaun", "value": "akaun" },
{ "label": "Cek", "value": "cek" },
{ "label": "Tunai, Nyatakan Sebab", "value": "tunai" },
{ "label": "Uzur/Sakit", "value": "uzur_sakit" },
{ "label": "Muflis", "value": "muflis" },
{ "label": "Disenarai Hitam", "value": "disenarai_hitam" }
],
"gridColumn": "span 12"
}
},
{
"type": "text",
"props": {
"name": "sebab_tunai",
"type": "text",
"label": "Sebab Pembayaran Tunai",
"width": "100%",
"gridColumn": "span 12",
"placeholder": "Sila nyatakan sebab",
"conditionalLogic": {
"action": "hide",
"enabled": true,
"operator": "and",
"conditions": [
{
"field": "cara_pembayaran",
"value": "tunai",
"operator": "not_contains"
}
]
}
}
},
{
"type": "heading",
"props": {
"name": "heading_pendidikan",
"level": 3,
"value": "2. PENDIDIKAN",
"width": "100%",
"gridColumn": "span 12"
}
},
{
"type": "radio",
"props": {
"name": "bersekolah_tanggungan",
"type": "radio",
"label": "Bersekolah",
"width": "100%",
"options": [
{ "label": "Ya", "value": "ya" },
{ "label": "Tidak", "value": "tidak" }
],
"gridColumn": "span 12",
"validation": "required"
}
},
{
"type": "checkbox",
"props": {
"name": "pendidikan_tertinggi_tanggungan",
"type": "checkbox",
"label": "Pendidikan Tertinggi",
"width": "100%",
"options": [
{ "label": "Peringkat Rendah", "value": "peringkat_rendah" },
{ "label": "SRP/PMR", "value": "srp_pmr" },
{ "label": "SPM", "value": "spm" },
{ "label": "STPM", "value": "stpm" },
{ "label": "Pra Sekolah", "value": "pra_sekolah" },
{ "label": "Sekolah Rendah Kebangsaan", "value": "srk" },
{ "label": "Sekolah Menengah Kebangsaan", "value": "smk" },
{ "label": "Sekolah Menengah Agama", "value": "sma" },
{ "label": "Sijil", "value": "sijil" },
{ "label": "Diploma", "value": "diploma" },
{ "label": "Ijazah", "value": "ijazah" },
{ "label": "Lain-Lain (Sila Nyatakan)", "value": "lain_lain" },
{
"label": "Sekolah Rendah Kebangsaan dan Agama",
"value": "srk_agama"
},
{ "label": "IPTA/IPTS", "value": "ipta_ipts" },
{ "label": "Maahad Tahfiz", "value": "maahad_tahfiz" }
],
"gridColumn": "span 12"
}
},
{
"type": "text",
"props": {
"name": "pendidikan_lain_tanggungan",
"type": "text",
"label": "Nyatakan Pendidikan Lain-lain",
"width": "100%",
"gridColumn": "span 12",
"placeholder": "Sila nyatakan",
"conditionalLogic": {
"action": "hide",
"enabled": true,
"operator": "and",
"conditions": [
{
"field": "pendidikan_tertinggi_tanggungan",
"value": "lain_lain",
"operator": "not_contains"
}
]
}
}
},
{
"type": "repeating-group",
"props": {
"help": "Tambah maklumat sekolah untuk setiap tanggungan yang bersekolah",
"name": "maklumat_sekolah",
"label": "Nama dan Alamat Sekolah/Institusi",
"width": "100%",
"fields": [
{
"name": "nama_sekolah",
"type": "text",
"label": "Nama Sekolah",
"validation": "required",
"placeholder": "Masukkan nama sekolah/institusi"
},
{
"name": "alamat_sekolah",
"type": "textarea",
"label": "Alamat Sekolah",
"validation": "required",
"placeholder": "Masukkan alamat lengkap sekolah/institusi"
},
{
"name": "daerah_sekolah",
"type": "text",
"label": "Daerah",
"placeholder": "Masukkan daerah"
},
{
"name": "negeri_sekolah",
"type": "text",
"label": "Negeri",
"placeholder": "Masukkan negeri"
},
{
"name": "poskod_sekolah",
"type": "text",
"label": "Poskod",
"placeholder": "Masukkan poskod"
}
],
"maxItems": 10,
"minItems": 0,
"buttonText": "Tambah Sekolah",
"gridColumn": "span 12",
"removeText": "Buang",
"conditionalLogic": {
"action": "hide",
"enabled": true,
"operator": "and",
"conditions": [
{
"field": "bersekolah_tanggungan",
"value": "ya",
"operator": "not_equals"
}
]
}
}
},
{
"type": "radio",
"props": {
"name": "tinggal_bersama_keluarga",
"type": "radio",
"label": "Tinggal Bersama Keluarga",
"width": "100%",
"options": [
{ "label": "Ya", "value": "ya" },
{ "label": "Tidak", "value": "tidak" },
{ "label": "Asrama", "value": "asrama" }
],
"gridColumn": "span 12"
}
}
]

View File

@ -33,17 +33,6 @@
"sourceHandle": "api-1752550319410-right",
"targetHandle": "script-1752550430989-left"
},
{
"id": "script-1752550430989-html-1752550500000",
"data": {},
"type": "custom",
"label": "",
"source": "script-1752550430989",
"target": "html-1752550500000",
"animated": true,
"sourceHandle": "script-1752550430989-right",
"targetHandle": "html-1752550500000-left"
},
{
"id": "html-1752550500000-end-1752546716111-1752550899936",
"data": {},
@ -76,6 +65,39 @@
"animated": true,
"sourceHandle": "notification-1752621850786-right",
"targetHandle": "end-1752546716111-left"
},
{
"id": "script-1752550430989-gateway-1752550505000",
"data": {},
"type": "custom",
"label": "",
"source": "script-1752550430989",
"target": "gateway-1752550505000",
"animated": true,
"sourceHandle": "script-1752550430989-right",
"targetHandle": "gateway-1752550505000-left"
},
{
"id": "gateway-1752550505000-html-1752550500000",
"data": { "condition": "todoStatus === true" },
"type": "custom",
"label": "Completed",
"source": "gateway-1752550505000",
"target": "html-1752550500000",
"animated": true,
"sourceHandle": "gateway-1752550505000-right",
"targetHandle": "html-1752550500000-left"
},
{
"id": "gateway-1752550505000-notification-1752621850786",
"data": { "condition": "todoStatus === false" },
"type": "custom",
"label": "Not Completed",
"source": "gateway-1752550505000",
"target": "notification-1752621850786",
"animated": true,
"sourceHandle": "gateway-1752550505000-bottom",
"targetHandle": "notification-1752621850786-left"
}
],
"nodes": [
@ -84,15 +106,18 @@
"data": { "label": "Start", "description": "Process start point" },
"type": "start",
"label": "Start",
"position": { "x": 270, "y": 345 }
"position": { "x": 300, "y": 135 }
},
{
"id": "form-1752546702226",
"data": {
"label": "Pilihan Kategori Asnaf",
"shape": "rectangle",
"formId": 7,
"formName": "Pilihan Kategori Asnaf",
"formUuid": "d3612e05-b31a-46dc-b5e5-67e6c5bd3e78",
"textColor": "#6b21a8",
"borderColor": "#9333ea",
"description": "Form: Pilihan Kategori Asnaf",
"assignedRoles": [
{ "label": "Pemohon", "value": "2", "description": "" }
@ -104,20 +129,21 @@
{ "formField": "kategori_asnaf", "processVariable": "kategoriAsnaf" },
{ "formField": "nama_asnaf", "processVariable": "namaAsnaf" }
],
"backgroundColor": "#faf5ff",
"fieldConditions": [],
"assignmentVariable": "",
"assignmentVariableType": "user_id"
},
"type": "form",
"label": "Pilihan Kategori Asnaf",
"position": { "x": 540, "y": 330 }
"position": { "x": 510, "y": 105 }
},
{
"id": "end-1752546716111",
"data": { "label": "End", "description": "Process end point" },
"type": "end",
"label": "End",
"position": { "x": 1890, "y": 135 }
"position": { "x": 2070, "y": 300 }
},
{
"id": "api-1752550319410",
@ -144,7 +170,7 @@
},
"type": "api",
"label": "API Call",
"position": { "x": 855, "y": 345 }
"position": { "x": 795, "y": 105 }
},
{
"id": "script-1752550430989",
@ -189,7 +215,62 @@
},
"type": "script",
"label": "Script Task",
"position": { "x": 1185, "y": 330 }
"position": { "x": 1050, "y": 120 }
},
{
"id": "gateway-1752550505000",
"data": {
"label": "Decision: Todo Status",
"shape": "diamond",
"textColor": "#c2410c",
"conditions": [
{
"id": "condition-group-1",
"output": "Completed",
"conditions": [
{
"id": "condition-1",
"value": true,
"operator": "eq",
"variable": "todoStatus",
"valueType": "boolean",
"logicalOperator": "and"
},
{
"id": "condition-1753408402567",
"value": "afiq",
"maxValue": "",
"minValue": "",
"operator": "contains",
"variable": "namaAsnaf",
"valueType": "string",
"logicalOperator": "and"
}
]
},
{
"id": "condition-group-2",
"output": "Not Completed",
"conditions": [
{
"id": "condition-2",
"value": false,
"operator": "eq",
"variable": "todoStatus",
"valueType": "boolean",
"logicalOperator": "and"
}
]
}
],
"borderColor": "#f97316",
"defaultPath": "Default",
"description": "Branch based on todoStatus",
"backgroundColor": "#fff7ed"
},
"type": "gateway",
"label": "Decision: Todo Status",
"position": { "x": 1365, "y": 120 }
},
{
"id": "html-1752550500000",
@ -210,7 +291,7 @@
},
"type": "html",
"label": "Show Result",
"position": { "x": 1425, "y": 75 }
"position": { "x": 1590, "y": 150 }
},
{
"id": "notification-1752621850786",
@ -239,8 +320,8 @@
}
],
"viewport": {
"x": -193.044397463002,
"y": 197.8681289640592,
"zoom": 1.049154334038055
"x": -271.3433493533669,
"y": 163.8456958483416,
"zoom": 0.6965142034495484
}
}

View File

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

View File

@ -45,6 +45,8 @@ components/process-flow/
│ ├── NotificationManager.vue
│ ├── NotificationQueue.vue
│ └── NotificationLogs.vue
├── GatewayConditionManager.vue # Gateway condition configuration UI
├── GatewayConditionManagerModal.vue # Modal for gateway setup
└── [25+ other process flow files]
```
@ -454,6 +456,153 @@ const customProperties = computed(() => props.data?.customProperties || {})
</template>
```
### **4. Gateway Decision Logic**
#### **Gateway Configuration UI Components**
```vue
<!-- GatewayConditionManager.vue - Main condition builder -->
<template>
<div class="gateway-condition-manager">
<!-- Empty state when no paths defined -->
<div v-if="localConditions.length === 0" class="text-center p-6">
<h3>No Decision Paths Defined</h3>
<p>Your process will always follow the default path</p>
<button @click="addConditionGroup">Add Your First Path</button>
</div>
<!-- Decision paths list -->
<div v-else class="conditions-list">
<div v-for="(group, groupIndex) in localConditions" :key="group.id">
<!-- Path header with collapse/expand -->
<div class="path-header" @click="toggleGroupCollapse(group.id)">
<span>Path {{ groupIndex + 1 }}</span>
<span v-if="group.output">{{ group.output }}</span>
</div>
<!-- Path details when expanded -->
<div v-if="!isGroupCollapsed(group.id)" class="path-details">
<!-- Path name input -->
<input v-model="group.output" placeholder="Path label (e.g. 'Yes', 'Approved')" />
<!-- Conditions list -->
<div class="conditions-list">
<div v-for="(condition, conditionIndex) in group.conditions" :key="condition.id">
<!-- Variable selection -->
<VariableBrowser v-model="condition.variable" :availableVariables="availableVariables" />
<!-- Operator selection -->
<select v-model="condition.operator">
<option v-for="op in getOperatorsForType(condition.valueType)" :key="op.value" :value="op.value">
{{ op.label }}
</option>
</select>
<!-- Value input -->
<input v-model="condition.value" :placeholder="getValuePlaceholder(condition)" />
<!-- Logical operator for multiple conditions -->
<div v-if="conditionIndex > 0">
<label>Combine with: AND/OR</label>
<input type="radio" v-model="condition.logicalOperator" value="and" /> AND
<input type="radio" v-model="condition.logicalOperator" value="or" /> OR
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
```
#### **Condition Evaluation Process**
```javascript
// Workflow execution evaluates gateway conditions
function evaluateConditionGroup(conditionGroup, variables) {
if (!conditionGroup.conditions || conditionGroup.conditions.length === 0) {
return false;
}
// If only one condition, evaluate it directly
if (conditionGroup.conditions.length === 1) {
return evaluateCondition(conditionGroup.conditions[0], variables);
}
// For multiple conditions, evaluate based on logical operators
let result = evaluateCondition(conditionGroup.conditions[0], variables);
for (let i = 1; i < conditionGroup.conditions.length; i++) {
const condition = conditionGroup.conditions[i];
const conditionResult = evaluateCondition(condition, variables);
const operator = condition.logicalOperator || 'and';
if (operator === 'and') {
result = result && conditionResult;
} else if (operator === 'or') {
result = result || conditionResult;
}
}
return result;
}
// Single condition evaluation
function evaluateCondition(condition, variables) {
const { variable, operator, value, valueType } = condition;
const variableValue = variables[variable];
// Handle boolean type conversions
let compareValue = value;
if (valueType === 'boolean') {
if (typeof value === 'string') {
compareValue = value.toLowerCase() === 'true';
} else {
compareValue = Boolean(value);
}
}
// Evaluate based on operator
switch (operator) {
case 'eq':
return variableValue == compareValue;
case 'gt':
return Number(variableValue) > Number(compareValue);
case 'is_true':
return Boolean(variableValue) === true;
// ... other operators
}
}
```
#### **Decision Path Selection**
```javascript
// Determine which path to follow based on conditions
function getNextNodeIdForDecision(currentNodeId) {
const currentNodeObj = workflowData.nodes.find(n => n.id === currentNodeId);
const outgoingEdges = getOutgoingEdges(currentNodeId);
if (!currentNodeObj || !outgoingEdges.length) return null;
const { conditions = [] } = currentNodeObj.data || {};
// Evaluate condition groups (each group represents a path)
for (const conditionGroup of conditions) {
if (evaluateConditionGroup(conditionGroup, variables)) {
// Find the edge that matches this condition group's output label
const edge = outgoingEdges.find(e => e.label === conditionGroup.output);
if (edge) return edge.target;
}
}
// If no conditions match, use default path
const defaultEdge = outgoingEdges.find(e => e.data?.isDefault);
if (defaultEdge) return defaultEdge.target;
// Fallback to first edge
return outgoingEdges[0]?.target || null;
}
```
---
## 🎨 Styling System
@ -577,7 +726,7 @@ export default defineNuxtPlugin(() => {
formId: 'form-uuid', // For form nodes
apiEndpoint: 'url', // For API nodes
scriptCode: 'javascript', // For script nodes
conditions: [], // For gateway nodes
conditions: [], // For gateway nodes (see Gateway Conditions section)
// Custom properties
customProperties: {}
@ -585,6 +734,73 @@ export default defineNuxtPlugin(() => {
}
```
### **Gateway Conditions Structure**
```javascript
// Gateway node conditions format (group-based)
{
id: 'gateway-node-id',
type: 'gateway',
data: {
label: 'Decision Point',
conditions: [
{
id: 'condition-group-1',
output: 'Path Label', // Label for this decision path
conditions: [ // Array of conditions for this path
{
id: 'condition-1',
variable: 'processVariable', // Process variable to evaluate
operator: 'eq', // Comparison operator
value: 'expectedValue', // Value to compare against
valueType: 'string', // Data type (string, boolean, number, etc.)
logicalOperator: 'and' // How to combine with next condition (and/or)
}
// ... more conditions for this path
]
}
// ... more condition groups (paths)
],
defaultPath: 'Default' // Default path if no conditions match
}
}
```
### **Supported Condition Operators**
```javascript
// String operators
'eq', 'equals', '==' // Equal to
'neq', 'not_equals', '!=' // Not equal to
'contains' // Contains substring
'not_contains' // Does not contain
'starts_with' // Starts with
'ends_with' // Ends with
'empty' // Is empty/null
'not_empty' // Is not empty/null
'regex' // Matches regex pattern
// Numeric operators
'gt', 'greater_than', '>' // Greater than
'lt', 'less_than', '<' // Less than
'gte', 'greater_than_or_equal', '>=' // Greater than or equal
'lte', 'less_than_or_equal', '<=' // Less than or equal
'between' // Between two values
'not_between' // Not between two values
// Boolean operators
'is_true' // Is true
'is_false' // Is false
// Date operators
'today' // Is today
'this_week' // Is this week
'this_month' // Is this month
'this_year' // Is this year
// Object operators
'has_property' // Has specific property
'property_equals' // Property equals value
```
---
## 🐛 Common Issues & Solutions
@ -700,4 +916,46 @@ const nodeTypes = {
**🔄 Version**: 1.0 - Comprehensive system documentation
**👥 For New Team Members**: This document contains everything needed to understand and work with the Vue Flow Process Builder system. Start with the "File Structure" section and follow the implementation patterns for any new features.
**👥 For New Team Members**: This document contains everything needed to understand and work with the Vue Flow Process Builder system. Start with the "File Structure" section and follow the implementation patterns for any new features.
---
## 🧪 Testing Gateway Decisions
### **Form Field Integration**
To test gateway decisions, add form fields that map to process variables:
```json
// Form component with gateway test field
{
"type": "checkbox",
"props": {
"name": "todoStatus",
"label": "Test Gateway Decision",
"help": "Toggle to test gateway paths"
}
}
```
### **Process Variable Mapping**
Map form fields to process variables for gateway testing:
```json
// Form node output mappings
{
"outputMappings": [
{ "formField": "todoStatus", "processVariable": "todoStatus" }
]
}
```
### **Gateway Testing Scenarios**
1. **"Completed" Path**: Check the test field → `todoStatus = true` → Follow "Completed" path
2. **"Not Completed" Path**: Leave test field unchecked → `todoStatus = false` → Follow "Not Completed" path
### **Workflow Execution**
The workflow execution engine (`pages/workflow/[id].vue`) automatically:
- Evaluates gateway conditions based on current process variables
- Shows visual feedback of which conditions are true/false
- Follows the first matching path
- Falls back to default path if no conditions match

View File

@ -155,63 +155,204 @@ function getNextNodeIdForDecision(currentNodeId) {
if (!currentNodeObj || !outgoingEdges.length) return null;
const { conditions = [], defaultPath } = currentNodeObj.data || {};
const { conditions = [] } = currentNodeObj.data || {};
// Evaluate conditions
for (const condition of conditions) {
if (evaluateCondition(condition, processVariables.value)) {
// Find the edge that matches this condition's target
const edge = outgoingEdges.find(e => e.data?.conditionId === condition.id || e.label === condition.label);
// Evaluate condition groups (each group represents a path)
for (const conditionGroup of conditions) {
if (evaluateConditionGroup(conditionGroup, processVariables.value)) {
// Find the edge that matches this condition group's output label
const edge = outgoingEdges.find(e => e.label === conditionGroup.output || e.data?.condition === conditionGroup.output);
if (edge) return edge.target;
}
}
// If no conditions match, use default path
if (defaultPath) {
const defaultEdge = outgoingEdges.find(e => e.data?.isDefault || e.label?.toLowerCase().includes('default'));
if (defaultEdge) return defaultEdge.target;
}
// If no conditions match, look for default path
const defaultEdge = outgoingEdges.find(e => e.data?.isDefault || e.label?.toLowerCase().includes('default'));
if (defaultEdge) return defaultEdge.target;
// Fallback to first edge
return outgoingEdges[0]?.target || null;
}
// Helper: Evaluate a condition group (multiple conditions with AND/OR logic)
function evaluateConditionGroup(conditionGroup, variables) {
if (!conditionGroup.conditions || conditionGroup.conditions.length === 0) {
return false;
}
// If only one condition, evaluate it directly
if (conditionGroup.conditions.length === 1) {
const singleResult = evaluateCondition(conditionGroup.conditions[0], variables);
console.log(`[Gateway Debug] Group '${conditionGroup.output}': single condition result=`, singleResult);
return singleResult;
}
// For multiple conditions, evaluate based on logical operators
let result = evaluateCondition(conditionGroup.conditions[0], variables);
console.log(`[Gateway Debug] Group '${conditionGroup.output}': condition 0 result=`, result);
for (let i = 1; i < conditionGroup.conditions.length; i++) {
const condition = conditionGroup.conditions[i];
const conditionResult = evaluateCondition(condition, variables);
const operator = condition.logicalOperator || 'and';
if (operator === 'and') {
result = result && conditionResult;
} else if (operator === 'or') {
result = result || conditionResult;
}
console.log(`[Gateway Debug] Group '${conditionGroup.output}': after condition ${i}, operator='${operator}', conditionResult=`, conditionResult, ', groupResult=', result);
}
return result;
}
// Helper: Evaluate a single condition
function evaluateCondition(condition, variables) {
const { variable, operator, value } = condition;
const { variable, operator, value, valueType, minValue, maxValue } = condition;
const variableValue = variables[variable];
// Handle boolean type conversions
let compareValue = value;
if (valueType === 'boolean') {
if (typeof value === 'string') {
compareValue = value.toLowerCase() === 'true';
} else {
compareValue = Boolean(value);
}
}
let result;
switch (operator) {
case 'eq':
case 'equals':
case '==':
return variableValue == value;
result = variableValue == compareValue;
break;
case 'neq':
case 'not_equals':
case '!=':
return variableValue != value;
result = variableValue != compareValue;
break;
case 'gt':
case 'greater_than':
case '>':
return Number(variableValue) > Number(value);
result = Number(variableValue) > Number(compareValue);
break;
case 'lt':
case 'less_than':
case '<':
return Number(variableValue) < Number(value);
result = Number(variableValue) < Number(compareValue);
break;
case 'gte':
case 'greater_than_or_equal':
case '>=':
return Number(variableValue) >= Number(value);
result = Number(variableValue) >= Number(compareValue);
break;
case 'lte':
case 'less_than_or_equal':
case '<=':
return Number(variableValue) <= Number(value);
result = Number(variableValue) <= Number(compareValue);
break;
case 'between':
result = (
Number(variableValue) >= Number(minValue) &&
Number(variableValue) <= Number(maxValue)
);
break;
case 'not_between':
result = (
Number(variableValue) < Number(minValue) ||
Number(variableValue) > Number(maxValue)
);
break;
case 'contains':
return String(variableValue).includes(String(value));
result = String(variableValue).includes(String(compareValue));
break;
case 'not_contains':
return !String(variableValue).includes(String(value));
result = !String(variableValue).includes(String(compareValue));
break;
case 'starts_with':
result = String(variableValue).startsWith(String(compareValue));
break;
case 'ends_with':
result = String(variableValue).endsWith(String(compareValue));
break;
case 'regex':
try {
result = new RegExp(compareValue).test(variableValue);
} catch (e) {
result = false;
}
break;
case 'has_property':
result = variableValue && typeof variableValue === 'object' && value in variableValue;
break;
case 'property_equals': {
if (!variableValue || typeof variableValue !== 'object' || typeof compareValue !== 'string') { result = false; break; }
const [prop, val] = compareValue.split(':');
result = variableValue[prop] == val;
break;
}
case 'empty':
case 'is_empty':
return !variableValue || variableValue === '' || variableValue === null || variableValue === undefined;
result = !variableValue || variableValue === '' || variableValue === null || variableValue === undefined;
break;
case 'not_empty':
case 'is_not_empty':
return variableValue && variableValue !== '' && variableValue !== null && variableValue !== undefined;
result = variableValue && variableValue !== '' && variableValue !== null && variableValue !== undefined;
break;
case 'is_true':
result = Boolean(variableValue) === true;
break;
case 'is_false':
result = Boolean(variableValue) === false;
break;
// Date operators
case 'today': {
if (!variableValue) { result = false; break; }
const date = new Date(variableValue);
const now = new Date();
result = (
date.getFullYear() === now.getFullYear() &&
date.getMonth() === now.getMonth() &&
date.getDate() === now.getDate()
);
break;
}
case 'this_week': {
if (!variableValue) { result = false; break; }
const date = new Date(variableValue);
const now = new Date();
const startOfWeek = new Date(now);
startOfWeek.setDate(now.getDate() - now.getDay());
const endOfWeek = new Date(startOfWeek);
endOfWeek.setDate(startOfWeek.getDate() + 6);
result = date >= startOfWeek && date <= endOfWeek;
break;
}
case 'this_month': {
if (!variableValue) { result = false; break; }
const date = new Date(variableValue);
const now = new Date();
result = (
date.getFullYear() === now.getFullYear() &&
date.getMonth() === now.getMonth()
);
break;
}
case 'this_year': {
if (!variableValue) { result = false; break; }
const date = new Date(variableValue);
const now = new Date();
result = date.getFullYear() === now.getFullYear();
break;
}
default:
console.warn('[Workflow] Unknown condition operator:', operator);
return false;
result = false;
}
console.log(`[Gateway Debug] Condition: variable='${variable}', operator='${operator}', value=`, value, ', minValue=', minValue, ', maxValue=', maxValue, '| variableValue=', variableValue, '| result=', result);
return result;
}
// Move to a specific node by its ID
@ -744,7 +885,10 @@ const executeDecisionNode = async () => {
stepLoading.value = true;
const currentNodeObj = workflowData.value.nodes[currentStep.value];
const { executionType = 'automatic' } = currentNodeObj.data || {};
// Debug: Log process variables before evaluating gateway
console.log('[Gateway Debug] Current processVariables:', JSON.stringify(processVariables.value, null, 2));
if (executionType === 'automatic') {
// Automatic decision based on conditions
const nextNodeId = getNextNodeIdForDecision(currentNodeObj.id);
@ -837,6 +981,16 @@ const htmlNodeStyles = computed(() => {
return currentNode.value?.data?.cssCode || '';
});
// Computed: Condition evaluation results for gateway nodes
const conditionEvaluationResults = computed(() => {
if (!['decision', 'gateway'].includes(currentNode.value?.type)) return [];
if (!currentNode.value?.data?.conditions) return [];
return currentNode.value.data.conditions.map(conditionGroup =>
getConditionGroupResult(conditionGroup, processVariables.value)
);
});
// CSS injection for HTML nodes
let currentStyleElement = null;
// JS injection for HTML nodes
@ -923,6 +1077,51 @@ function getNodeLabel(nodeId) {
const node = workflowData.value.nodes.find(n => n.id === nodeId);
return node?.data?.label || node?.label || 'Next Step';
}
// Helper: Get human-readable condition description
function getConditionDescription(condition, variables) {
const { variable, operator, value, valueType } = condition;
const variableValue = variables[variable];
let operatorText = operator;
switch (operator) {
case 'eq': operatorText = '='; break;
case 'neq': operatorText = '≠'; break;
case 'gt': operatorText = '>'; break;
case 'lt': operatorText = '<'; break;
case 'gte': operatorText = '≥'; break;
case 'lte': operatorText = '≤'; break;
case 'contains': operatorText = 'contains'; break;
case 'not_contains': operatorText = 'does not contain'; break;
case 'starts_with': operatorText = 'starts with'; break;
case 'ends_with': operatorText = 'ends with'; break;
case 'empty': operatorText = 'is empty'; break;
case 'not_empty': operatorText = 'is not empty'; break;
case 'is_true': operatorText = 'is true'; break;
case 'is_false': operatorText = 'is false'; break;
}
if (['empty', 'not_empty', 'is_true', 'is_false'].includes(operator)) {
return `${variable} ${operatorText}`;
}
return `${variable} ${operatorText} ${value}`;
}
// Helper: Get condition group evaluation result
function getConditionGroupResult(conditionGroup, variables) {
const results = conditionGroup.conditions.map(condition => {
const result = evaluateCondition(condition, variables);
const description = getConditionDescription(condition, variables);
return { result, description };
});
return {
group: conditionGroup,
results,
finalResult: evaluateConditionGroup(conditionGroup, variables)
};
}
</script>
<template>
@ -1280,12 +1479,36 @@ function getNodeLabel(nodeId) {
<!-- Show conditions being evaluated -->
<div v-if="currentNode.data?.conditions?.length" class="mt-6 text-left">
<h5 class="font-medium text-gray-700 mb-3">Conditions:</h5>
<div class="space-y-2">
<div v-for="(condition, index) in currentNode.data.conditions" :key="index"
class="flex items-center gap-2 text-sm text-gray-600 bg-gray-50 p-2 rounded">
<Icon name="material-symbols:rule" class="w-4 h-4" />
<span>{{ condition.variable }} {{ condition.operator }} {{ condition.value }}</span>
<h5 class="font-medium text-gray-700 mb-3">Decision Paths:</h5>
<div class="space-y-3">
<div v-for="(evaluationResult, groupIndex) in conditionEvaluationResults" :key="groupIndex"
class="border rounded-lg p-3"
:class="evaluationResult.finalResult ? 'border-green-300 bg-green-50' : 'border-gray-200 bg-gray-50'">
<div class="flex items-center gap-2 mb-2">
<Icon name="material-symbols:alt-route" class="w-4 h-4"
:class="evaluationResult.finalResult ? 'text-green-500' : 'text-orange-500'" />
<span class="font-medium text-sm">{{ evaluationResult.group.output || `Path ${groupIndex + 1}` }}</span>
<span v-if="evaluationResult.finalResult"
class="px-2 py-0.5 text-xs bg-green-100 text-green-700 rounded-full">
MATCH
</span>
</div>
<div class="space-y-1">
<div v-for="(conditionResult, conditionIndex) in evaluationResult.results" :key="conditionIndex"
class="flex items-center gap-2 text-xs">
<Icon name="material-symbols:rule" class="w-3 h-3"
:class="conditionResult.result ? 'text-green-500' : 'text-red-500'" />
<span v-if="conditionIndex > 0" class="text-orange-600 font-medium">
{{ evaluationResult.group.conditions[conditionIndex].logicalOperator?.toUpperCase() || 'AND' }}
</span>
<span :class="conditionResult.result ? 'text-green-700' : 'text-red-700'">
{{ conditionResult.description }}
</span>
<span class="text-gray-500">
({{ conditionResult.result ? '✓' : '✗' }})
</span>
</div>
</div>
</div>
</div>
</div>
@ -1324,12 +1547,12 @@ function getNodeLabel(nodeId) {
<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">
<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">
<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>
@ -1343,7 +1566,7 @@ function getNodeLabel(nodeId) {
</div>
<!-- Process Variables Debug (only in development) -->
<div v-if="Object.keys(processVariables).length > 0" class="bg-gray-100 rounded-lg p-4 mt-4">
<div v-if="Object.keys(processVariables || {}).length > 0" class="bg-gray-100 rounded-lg p-4 mt-4">
<details>
<summary class="font-medium text-gray-700 cursor-pointer mb-2">Process Variables (Debug)</summary>
<pre class="text-xs text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(processVariables, null, 2) }}</pre>