pgo-ui 1.1.13 → 1.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,27 +1,26 @@
1
1
  <template>
2
- <Modal
3
- v-model="open"
2
+ <Modal
3
+ v-model="open"
4
4
  persistent
5
5
  padding="p-3"
6
6
  :bg="bg"
7
- :width="width"
8
7
  >
9
8
  <Banner
10
9
  v-if="form?.banner"
11
10
  v-bind="form?.banner"
12
- bannerClass="mb-4"
11
+ bannerClass="mb-4"
13
12
  />
14
- <Form
13
+ <Form
15
14
  v-model="valid"
16
15
  ref="formRef"
17
- >
18
- <input
16
+ >
17
+ <input
19
18
  v-if="props.mode === 'edit' && props.editItemId"
20
19
  v-model="formData.id"
21
20
  type="hidden"
22
21
  />
23
- <!-- Render grouped fields -->
24
-
22
+ <!-- Render grouped fields -->
23
+
25
24
  <!-- A0111904 -->
26
25
  <template v-if="form.groups">
27
26
  <template v-for="(group, id) in form.groups" :key="id">
@@ -30,11 +29,11 @@
30
29
  <div v-if="group.title" :class="['absolute top-0 -translate-y-1/2 bg-inherit px-2 text-sm font-semibold mb-2', lang === 'dv' ? 'right-4' : 'left-4']">
31
30
  {{ group.title ? useLanguageSelected(group.title, lang) : '' }}
32
31
  </div>
33
- <template
34
- v-for="(field, index) in getFieldsByGroup(group.name)"
32
+ <template
33
+ v-for="(field, index) in getFieldsByGroup(group.name)"
35
34
  :key="field?.key || index"
36
35
  >
37
- <div
36
+ <div
38
37
  v-if="field && field.key && shouldShowField(field)"
39
38
  :class="columnSpansMap[field.columnSpan] || columnSpansMap[1]"
40
39
  >
@@ -49,15 +48,15 @@
49
48
  </div>
50
49
  </div>
51
50
  </template>
52
-
51
+
53
52
  <!-- MOVE: Render ungrouped fields ONCE after ALL groups -->
54
53
  <div v-if="getUngroupedFields().length > 0" class="mb-4">
55
- <div :class="['grid', gridColsMap[form.numberOfColumns] || gridColsMap[2], 'gap-x-2 gap-y-4']">
56
- <template
57
- v-for="(field, index) in getUngroupedFields()"
54
+ <div :class="['grid', gridColsMap[form.numberOfColumns] || gridColsMap[2], 'gap-2']">
55
+ <template
56
+ v-for="(field, index) in getUngroupedFields()"
58
57
  :key="field?.key || index"
59
58
  >
60
- <div
59
+ <div
61
60
  v-if="field && field.key && shouldShowField(field)"
62
61
  :class="columnSpansMap[field.columnSpan] || columnSpansMap[1]"
63
62
  >
@@ -72,14 +71,14 @@
72
71
  </div>
73
72
  </div>
74
73
  </template>
75
-
74
+
76
75
  <!-- If no groups, render all fields -->
77
- <div v-else :class="['grid', gridColsMap[form.numberOfColumns] || gridColsMap[2], 'gap-x-2 gap-y-4']">
78
- <template
79
- v-for="(field, index) in form.fields"
76
+ <div v-else :class="['grid', gridColsMap[form.numberOfColumns] || gridColsMap[2], 'gap-2']">
77
+ <template
78
+ v-for="(field, index) in form.fields"
80
79
  :key="field?.key || index"
81
80
  >
82
- <div
81
+ <div
83
82
  v-if="field && field.key && shouldShowField(field)"
84
83
  :class="columnSpansMap[field.columnSpan] || columnSpansMap[1]"
85
84
  >
@@ -94,42 +93,37 @@
94
93
  </div>
95
94
  </Form>
96
95
  <template #footer>
97
- <div class="flex justify-end gap-2">
98
-
99
- <!-- DRAFT: only visible in create mode before any save -->
100
- <Button
101
- v-if="formMode === 'create'"
102
- label="buttons.draft"
103
- color="primary"
104
- :loading="isSubmitting && clickedButton === 'draft'"
105
- :disabled="isSubmitting || !valid"
106
- @click="handleSubmit('draft')"
107
- />
108
-
109
- <!-- UPDATE: visible after draft saved or opened in edit mode -->
110
- <Button
111
- v-if="formMode === 'edit'"
112
- label="buttons.update"
113
- color="warning"
114
- :loading="isSubmitting && clickedButton === 'update'"
115
- :disabled="isSubmitting || !isFormDirty || !valid"
96
+ <div class="flex justify-end gap-2 ">
97
+ <Button v-if="mode == 'edit'"
98
+ label="buttons.update"
99
+ color="primary"
100
+ :loading="isSubmitting"
116
101
  @click="handleSubmit('update')"
117
102
  />
103
+ <template v-else>
104
+ <Button
105
+ v-if="items || draftSubmitted"
106
+ label="buttons.submit"
107
+ color="primary"
108
+ :loading="isSubmitting"
109
+ :disabled="!valid || isSubmitting"
110
+
111
+ @click="handleSubmit('submitted')"
112
+ />
113
+ <Button
114
+ label="buttons.draft"
115
+ color="primary"
116
+ :loading="isSubmitting"
117
+ :disabled="!valid || isSubmitting || draftSubmitted"
118
+
119
+ @click="handleSubmit('draft')"
120
+ />
121
+ <!-- <Button v-if="items" label="buttons.submit" color="primary" @click="handleSubmit('submit')"/> -->
118
122
 
119
- <!-- SUBMIT: visible after draft saved or opened in edit mode -->
120
- <Button
121
- v-if="formMode === 'edit'"
122
- label="buttons.submit"
123
- color="primary"
124
- :loading="isSubmitting && clickedButton === 'submit'"
125
- :disabled="isSubmitting || !valid"
126
- @click="handleSubmit('submit')"
127
- />
128
-
129
- <!-- CLOSE: always visible -->
130
- <Button
131
- label="buttons.close"
132
- color="gray"
123
+ </template>
124
+ <Button
125
+ label="buttons.close"
126
+ color="gray"
133
127
  :disabled="isSubmitting"
134
128
  @click="handleClose"
135
129
  />
@@ -144,31 +138,27 @@
144
138
  import Modal from '../Modal.vue'
145
139
  import { Banner, LabelField } from '../index'
146
140
  import { gridColsMap, columnSpansMap, useLanguageSelected, initializeFunctions } from '@/pgo-components/lib/componentConfig';
147
-
141
+
148
142
  const rules = inject('validationRules')
149
143
  // const baseUrl = inject('baseUrl')
150
144
  const { language } = inject('i18n')
151
145
 
152
146
  const valid = ref(false);
153
147
  const formId = ref(null);
154
- const relationId = ref(null);
155
148
  const open = ref(true);
156
149
  const formRef = ref(null);
157
150
  const isSubmitting = ref(false);
158
151
  const draftSubmitted = ref(false);
159
152
  const clickedButton = ref('');
160
- const isPreFilled = ref(false)
161
- const originalFormData = ref({})
162
-
163
153
  const emit = defineEmits([
164
- 'update:modelValue',
165
- 'submit:success',
166
- 'submit:error',
154
+ 'update:modelValue',
155
+ 'submit:success',
156
+ 'submit:error',
167
157
  'close',
168
158
  'field:event',
169
159
  'view-pdf'
170
160
  ]);
171
-
161
+
172
162
  const props = defineProps({
173
163
  modelValue: { type: Boolean, default: false },
174
164
  form: { type: Object, required: true },
@@ -177,19 +167,17 @@
177
167
  crudLink:{ type: String },
178
168
  editItemId: { type: [String, Number], default: null },
179
169
  mode: { type: String, default: 'create', enum: ['create', 'edit'] },
180
- width: { type: String, default: '' },
170
+
181
171
  bg: { type: String, default: 'bg-background-color' },
182
172
  });
183
173
 
184
174
  const api = inject('api');
185
175
  const snackbar = inject('snackbar');
186
176
 
187
- // Initialize form data from fields
177
+ // Initialize form data from fields
188
178
  const formData = reactive({});
189
179
  const FormDataList = ref({});
190
180
 
191
- const formMode = ref(props.mode)
192
-
193
181
  // Initialize variables as reactive refs
194
182
  const formVariables = reactive({});
195
183
 
@@ -199,18 +187,15 @@
199
187
 
200
188
  // Enhanced evaluation context that includes props and mode
201
189
  const createEvaluationContext = () => {
202
- return {
203
- formData,
204
- formVariables,
205
- variables: formVariables,
206
- props,
207
- mode: formMode.value,
208
- formType: formType.value,
209
- isEdit: formMode.value === 'edit',
210
- isCreate: formMode.value === 'create',
211
- ...formVariables,
190
+ return {
212
191
  ...formData,
213
- }
192
+ ...formVariables,
193
+ mode: props.mode,
194
+ formType: formType.value,
195
+ props: props,
196
+ isEdit: props.mode === 'edit',
197
+ isCreate: props.mode === 'create'
198
+ };
214
199
  };
215
200
 
216
201
  // const fileList = ref([
@@ -250,84 +235,56 @@
250
235
  return formData[fieldKey];
251
236
  };
252
237
 
253
- // Helper function to set field value (uses flat dot-notation keys matching field.key)
238
+ // Helper function to set field value
254
239
  const setFieldValue = (fieldKey, value) => {
255
- formData[fieldKey] = value;
240
+ if (formData.hasOwnProperty(fieldKey)) {
241
+ formData[fieldKey] = value;
242
+ }
256
243
  };
257
244
 
258
- const isFormDirty = computed(() => {
259
- if (!isPreFilled.value) return false
260
- return Object.keys(originalFormData.value).some(key => {
261
- const original = originalFormData.value[key]
262
- const current = formData[key]
263
- // Handle null/undefined equality
264
- if (original === null || original === undefined) {
265
- return current !== null && current !== undefined && current !== ''
266
- }
267
- // Handle objects/arrays
268
- if (typeof original === 'object') {
269
- return JSON.stringify(original) !== JSON.stringify(current)
270
- }
271
- return original !== current
272
- })
273
- })
274
-
275
245
  // Initialize form data with default values or items data
276
- const initializeFormData = () => {
246
+ const initializeFormData = () => {
247
+ // Add safety check for form.fields
277
248
  if (!props.form.fields || !Array.isArray(props.form.fields)) {
278
249
  return;
279
250
  }
280
-
251
+
281
252
  props.form.fields.forEach(field => {
282
253
  if (!field || !field.key) return;
283
254
 
284
- const fieldKey = field.dataKey ?? field.key
285
-
286
- // 1. Try to get value from props.items (supports dot notation)
287
- if (props.items) {
288
- const val = fieldKey.includes('.')
289
- ? getNestedValue(props.items, fieldKey)
290
- : props.items[fieldKey]
291
-
292
- if (val !== undefined) {
293
- formData[field.key] = val
294
- return
255
+ if (props.items && props.items[field.key] !== undefined) {
256
+ formData[field.key] = props.items[field.key];
257
+ } else if (field.default !== undefined) {
258
+ formData[field.key] = field.default;
259
+ } else if (field.value !== undefined) {
260
+ formData[field.key] = field.value;
261
+ } else {
262
+ // Set default value based on type
263
+ switch (field.type) {
264
+ case 'number':
265
+ case 'integer':
266
+ formData[field.key] = null;
267
+ break;
268
+ case 'boolean':
269
+ formData[field.key] = false;
270
+ break;
271
+ case 'array':
272
+ formData[field.key] = null;
273
+ break;
274
+ case 'object':
275
+ formData[field.key] = null;
276
+ break;
277
+ case 'string':
278
+ default:
279
+ if (field.inputType === 'checkbox') {
280
+ formData[field.key] = false; // Default checkboxes to false
281
+ } else {
282
+ formData[field.key] = null;
283
+ }
284
+ break;
295
285
  }
296
286
  }
297
-
298
-
299
- // 2. Use field default value
300
- if (field.default !== undefined) {
301
- formData[field.key] = field.default
302
- return
303
- }
304
-
305
- // 3. Use field value
306
- if (field.value !== undefined) {
307
- formData[field.key] = field.value
308
- return
309
- }
310
-
311
- // 4. Set type-based default (fallback)
312
- switch (field.type) {
313
- case 'number':
314
- case 'integer':
315
- formData[field.key] = null
316
- break
317
- case 'boolean':
318
- formData[field.key] = false
319
- break
320
- case 'array':
321
- case 'object':
322
- formData[field.key] = null
323
- break
324
- case 'string':
325
- default:
326
- formData[field.key] = field.inputType === 'checkbox' ? false : null
327
- break
328
- }
329
287
  });
330
-
331
288
  // console.log('Initialized form data:', formData);
332
289
  };
333
290
 
@@ -342,35 +299,23 @@ const initializeFormData = () => {
342
299
 
343
300
  const initializeAllFunctions = () => {
344
301
  if (props.form.functions) {
345
- Object.entries(props.form.functions).forEach(([name, functionBody]) => {
346
- try {
347
- compiledFunctions.value[name] = (...args) => {
348
- const context = createEvaluationContext()
349
-
350
- // Add common function-specific arguments
351
- if (args[0] !== undefined) context.response = args[0]
352
- if (args[1] !== undefined) context.item = args[1]
353
- if (args[2] !== undefined) context.event = args[2]
354
-
355
- // Add all positional args
356
- args.forEach((arg, index) => {
357
- context[`arg${index}`] = arg
358
- })
359
-
360
- try {
361
- // Pass function body directly — NOT wrapped with return()
362
- // Function bodies can have multiple statements and returns
363
- return safeEval(functionBody, context)
364
- } catch (error) {
365
- console.warn(`Error executing function "${name}":`, error.message)
366
- console.warn('Function body was:', functionBody)
367
- return null
368
- }
369
- }
370
- } catch (error) {
371
- console.warn(`Error compiling function "${name}":`, error.message)
302
+ // Create context that the functions will have access to
303
+ const functionContext = {
304
+ formData,
305
+ formVariables,
306
+ snackbar,
307
+ api,
308
+ emit,
309
+ props,
310
+ console,
311
+ variables: formVariables,
312
+ this: {
313
+ variables: formVariables
372
314
  }
373
- })
315
+ };
316
+
317
+ // Use the global initializeFunctions helper
318
+ compiledFunctions.value = initializeFunctions(props.form.functions, functionContext);
374
319
  }
375
320
  };
376
321
 
@@ -378,18 +323,273 @@ const initializeFormData = () => {
378
323
  const executeFunction = (functionName, ...args) => {
379
324
  if (compiledFunctions.value[functionName]) {
380
325
  try {
381
- return compiledFunctions.value[functionName](...args)
326
+ return compiledFunctions.value[functionName](...args);
382
327
  } catch (error) {
383
- console.warn(`Error executing function "${functionName}":`, error.message)
384
- return null
328
+ console.error(`Error executing function ${functionName}:`, error);
329
+ snackbar?.show({
330
+ message: `Function ${functionName} execution failed`,
331
+ variant: 'error'
332
+ });
385
333
  }
386
334
  } else {
387
- console.warn(`Function "${functionName}" not found in compiled functions`)
388
- return null
335
+ console.warn(`Function ${functionName} not found`);
336
+ }
337
+ };
338
+
339
+ // Function to get fields by group name
340
+ const getFieldsByGroup = (groupName) => {
341
+ if (!props.form.fields || !Array.isArray(props.form.fields)) {
342
+ return [];
343
+ }
344
+ return props.form.fields.filter(field => field && field.group === groupName);
345
+ };
346
+
347
+ const shouldShowGroup = (group) => {
348
+ // Add null check for group
349
+ if (!group) {
350
+ return false;
351
+ }
352
+
353
+ // If no condition, always show the group
354
+ if (!group.condition) {
355
+ return true;
356
+ }
357
+
358
+ try {
359
+ // Normalize the condition string (trim spaces)
360
+ let expression = group.condition.trim();
361
+
362
+ // Create enhanced evaluation context
363
+ const context = createEvaluationContext();
364
+
365
+ // Helper function to safely evaluate expressions
366
+ const evaluateCondition = (expr) => {
367
+ // Handle various condition patterns:
368
+
369
+ // Pattern 1: Simple variable check "variableName" -> check if variable is truthy
370
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(expr)) {
371
+ return !!context[expr];
372
+ }
373
+
374
+ // Pattern 2: Negation "!variableName"
375
+ if (/^!/.test(expr)) {
376
+ const variableName = expr.substring(1).trim();
377
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(variableName)) {
378
+ return !context[variableName];
379
+ }
380
+ }
381
+
382
+ // Pattern 3: Complex expression with &&, ||, ===, !==, >, <, etc.
383
+ // Create variable declarations from context
384
+ const varDeclarations = Object.keys(context)
385
+ .map(key => {
386
+ const value = context[key];
387
+ // Properly stringify different value types
388
+ if (value === null || value === undefined) {
389
+ return `const ${key} = null;`;
390
+ } else if (typeof value === 'string') {
391
+ return `const ${key} = "${value.replace(/"/g, '\\"')}";`;
392
+ } else if (typeof value === 'boolean') {
393
+ return `const ${key} = ${value};`;
394
+ } else if (typeof value === 'number') {
395
+ return `const ${key} = ${value};`;
396
+ } else {
397
+ return `const ${key} = ${JSON.stringify(value)};`;
398
+ }
399
+ })
400
+ .join('\n');
401
+
402
+ // Create and execute the function with the context
403
+ const fn = new Function(varDeclarations + `\nreturn (${expr});`);
404
+ return fn();
405
+ };
406
+
407
+ // Evaluate the condition expression
408
+ return !!evaluateCondition(expression);
409
+ } catch (error) {
410
+ console.warn(`Error evaluating group condition "${group.condition}":`, error.message);
411
+ // Fallback: check if the condition is a simple variable reference
412
+ const simpleVariable = group.condition.trim().replace(/^!/, '');
413
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(simpleVariable)) {
414
+ const context = createEvaluationContext();
415
+ const value = context[simpleVariable];
416
+ return group.condition.trim().startsWith('!') ? !value : !!value;
417
+ }
418
+ return true; // Show group by default if evaluation fails
419
+ }
420
+ };
421
+
422
+ // Function to get ungrouped fields
423
+ const getUngroupedFields = () => {
424
+ if (!props.form.fields || !Array.isArray(props.form.fields)) {
425
+ return [];
426
+ }
427
+ return props.form.fields.filter(field => field && !field.group);
428
+ };
429
+
430
+ // Function to check if field should be shown based on condition
431
+ const shouldShowField = (field) => {
432
+ // Add null check for field
433
+ if (!field) {
434
+ return false;
435
+ }
436
+
437
+ if (field.hidden === true) {
438
+ return false;
439
+ }
440
+
441
+ if (!field.condition) {
442
+ return true;
443
+ }
444
+
445
+ try {
446
+ // Normalize the condition string (trim spaces)
447
+ let expression = field.condition.trim();
448
+
449
+ // Create enhanced evaluation context
450
+ const context = createEvaluationContext();
451
+
452
+ // Helper function to safely evaluate expressions
453
+ const evaluateCondition = (expr) => {
454
+ // Handle various condition patterns:
455
+
456
+ // Pattern 1: Simple field check "fieldName" -> check if field is truthy
457
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(expr)) {
458
+ return !!context[expr];
459
+ }
460
+
461
+ // Pattern 2: Negation "!fieldName"
462
+ if (/^!/.test(expr)) {
463
+ const fieldName = expr.substring(1).trim();
464
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(fieldName)) {
465
+ return !context[fieldName];
466
+ }
467
+ }
468
+
469
+ // Pattern 3: Complex expression with &&, ||, ===, !==, >, <, etc.
470
+ // Create variable declarations from context
471
+ const varDeclarations = Object.keys(context)
472
+ .map(key => {
473
+ const value = context[key];
474
+ // Properly stringify different value types
475
+ if (value === null || value === undefined) {
476
+ return `const ${key} = null;`;
477
+ } else if (typeof value === 'string') {
478
+ return `const ${key} = "${value.replace(/"/g, '\\"')}";`;
479
+ } else if (typeof value === 'boolean') {
480
+ return `const ${key} = ${value};`;
481
+ } else if (typeof value === 'number') {
482
+ return `const ${key} = ${value};`;
483
+ } else {
484
+ return `const ${key} = ${JSON.stringify(value)};`;
485
+ }
486
+ })
487
+ .join('\n');
488
+
489
+ // Create and execute the function with the context
490
+ const fn = new Function(varDeclarations + `\nreturn (${expr});`);
491
+ return fn();
492
+ };
493
+
494
+ // Evaluate the condition expression
495
+ return !!evaluateCondition(expression);
496
+ } catch (error) {
497
+ console.warn(`Error evaluating condition "${field.condition}":`, error.message);
498
+ // Fallback: check if the condition is a simple field reference
499
+ const simpleField = field.condition.trim().replace(/^!/, '');
500
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(simpleField)) {
501
+ const context = createEvaluationContext();
502
+ const value = context[simpleField];
503
+ return field.condition.trim().startsWith('!') ? !value : !!value;
504
+ }
505
+ return true; // Show field by default if evaluation fails
506
+ }
507
+ };
508
+
509
+ const componentMap = computed(() => {
510
+ const map = {}
511
+ const usedTypes = new Set(props.form.fields.map(f => f.inputType?.toLowerCase()))
512
+
513
+ if (usedTypes.has('search') || usedTypes.has('inputsearch')) {
514
+ map['search'] = defineAsyncComponent(() => import('../SearchBox.vue'))
515
+ map['searchbox'] = defineAsyncComponent(() => import('../SearchBox.vue'))
516
+ }
517
+
518
+ if (usedTypes.has('select')) {
519
+ map['select'] = defineAsyncComponent(() => import('../inputs/Select.vue'))
520
+ }
521
+ if (usedTypes.has('textarea')) {
522
+ map['textarea'] = defineAsyncComponent(() => import('../inputs/Textarea.vue'))
523
+ }
524
+
525
+ if (usedTypes.has('textfield') || usedTypes.has('text') || usedTypes.has('string')) {
526
+ map['textfield'] = defineAsyncComponent(() => import('../inputs/TextField.vue'))
527
+ map['text'] = defineAsyncComponent(() => import('../inputs/TextField.vue'))
528
+ map['string'] = defineAsyncComponent(() => import('../inputs/TextField.vue'))
529
+ }
530
+
531
+ if (usedTypes.has('datepicker') || usedTypes.has('date')) {
532
+ map['datepicker'] = defineAsyncComponent(() => import('../inputs/DatePicker.vue'))
533
+ map['date'] = defineAsyncComponent(() => import('../inputs/DatePicker.vue'))
534
+ }
535
+
536
+ if (usedTypes.has('chipgroup')) {
537
+ map['chipgroup'] = defineAsyncComponent(() => import('../buttons/ChipGroup.vue'))
538
+ }
539
+
540
+ if (usedTypes.has('checkbox')) {
541
+ map['checkbox'] = defineAsyncComponent(() => import('../inputs/Checkbox.vue'))
542
+ }
543
+
544
+ if (usedTypes.has('radio')) {
545
+ map['radio'] = defineAsyncComponent(() => import('../inputs/Radio.vue'))
546
+ }
547
+
548
+ if (usedTypes.has('numberfield') || usedTypes.has('number')) {
549
+ map['numberfield'] = defineAsyncComponent(() => import('../inputs/NumberField.vue'))
550
+ map['number'] = defineAsyncComponent(() => import('../inputs/NumberField.vue'))
551
+ }
552
+ if (usedTypes.has('file') || usedTypes.has('filefield')) {
553
+ map['file'] = defineAsyncComponent(() => import('../inputs/FileUpload.vue'))
554
+ map['filefield'] = defineAsyncComponent(() => import('../inputs/FileUpload.vue'))
555
+ }
556
+ if (usedTypes.has('label') || usedTypes.has('labelfield')) {
557
+ map['label'] = defineAsyncComponent(() => import('../inputs/LabelField.vue'))
558
+ map['labelfield'] = defineAsyncComponent(() => import('../inputs/LabelField.vue'))
559
+ }
560
+
561
+ return map
562
+ })
563
+
564
+ const getFieldComponent = (type) => {
565
+ const normalizedType = type?.toLowerCase() || 'textfield'
566
+ return componentMap.value[normalizedType] || componentMap.value['textfield']
567
+ }
568
+
569
+ // Helper function to evaluate JavaScript expressions with context
570
+ const evaluateExpression = (expression, context) => {
571
+ if (typeof expression !== 'string') {
572
+ return expression; // Return as-is if not a string
573
+ }
574
+
575
+ // If it's a simple variable name, return the context value
576
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(expression)) {
577
+ return context[expression];
578
+ }
579
+
580
+ try {
581
+ // Create function with context variables as parameters
582
+ const contextKeys = Object.keys(context);
583
+ const contextValues = Object.values(context);
584
+ const func = new Function(...contextKeys, `return ${expression}`);
585
+ return func(...contextValues);
586
+ } catch (error) {
587
+ console.warn('Error evaluating expression:', expression, error);
588
+ return false;
389
589
  }
390
590
  };
391
591
 
392
- // Simplified getFieldProps function - evaluate disabled expressions
592
+ // Simplified getFieldProps function
393
593
  const getFieldProps = (field) => {
394
594
  if (!field) {
395
595
  return {};
@@ -397,158 +597,269 @@ const initializeFormData = () => {
397
597
 
398
598
  // Create evaluation context
399
599
  const context = createEvaluationContext();
400
-
401
- const {
402
- type,
403
- key,
404
- inputType,
405
- columnSpan,
406
- rules,
407
- condition,
408
- group,
409
- events,
600
+
601
+ const {
602
+ type,
603
+ key,
604
+ inputType,
605
+ columnSpan,
606
+ rules,
607
+ condition,
608
+ group,
609
+ events,
410
610
  hidden,
411
- default: defaultValue,
412
- ...restProps
611
+ disabled,
612
+ reaonly,
613
+ value,
614
+ default: defaultValue,
615
+ ...restProps
413
616
  } = field;
414
-
415
- const cleanProps = { ...restProps }
416
-
417
- // Evaluate disabled if it's a string expression
418
- if (typeof field.disabled === 'string') {
419
- try {
420
- cleanProps.disabled = !!safeEval(`return (${field.disabled});`, context)
421
- } catch (error) {
422
- console.warn(`Error evaluating disabled for field "${field.key}":`, error.message)
423
- cleanProps.disabled = false
424
- }
425
- } else if (typeof field.disabled === 'boolean') {
426
- cleanProps.disabled = field.disabled
617
+
618
+ // Skip hidden fields
619
+ if (hidden) {
620
+ return {};
427
621
  }
428
622
 
623
+ // Start with fieldKey
624
+ const cleanProps = {
625
+ fieldKey: key
626
+ };
627
+
429
628
  // Add relationId for file upload components
430
629
  if (field.inputType === 'file' || field.inputType === 'filefield') {
431
- cleanProps.relationId = relationId.value;
432
- cleanProps['onUploadSuccess'] = () => handleUploadSuccess();
433
- cleanProps['onView-pdf'] = (pdfData) => emit('view-pdf', pdfData);
434
- cleanProps.fileList = FormDataList.value?.[field.dataKey ?? field.key];
630
+ cleanProps.relationId = formId.value || props.editItemId;
631
+ // cleanProps.onuploadSuccess = fetchData();
632
+ // cleanProps.onviewPdf = emit('view-pdf');
633
+ // cleanProps.fileList = FormDataList.value?.[ field.dataKey ?? field.key ];
634
+
635
+ // cleanProps.relationId = formId.value || props.editItemId;
636
+ cleanProps.onUploadSuccess = () => handleUploadSuccess();
637
+ cleanProps['onView-pdf'] = (pdfData) => emit('view-pdf', pdfData);
638
+ cleanProps.fileList = FormDataList.value?.[ field.dataKey ?? field.key ];
639
+ // cleanProps.fileList = fileList.value;
435
640
  }
436
641
 
437
- return cleanProps
438
- };
642
+ // Add all other props directly (assuming they're clean from backend)
643
+ // Add all other props and evaluate expressions
644
+ Object.entries(restProps).forEach(([propKey, value]) => {
645
+ // Skip empty/null/undefined values
646
+ if (value === '' || value === null || value === undefined) {
647
+ return;
648
+ }
649
+
650
+ // Filter out any event-like properties that are strings
651
+ if (propKey.startsWith('on') && typeof value === 'string') {
652
+ console.warn(`Filtering out string event handler: ${propKey}`);
653
+ return;
654
+ }
655
+
656
+ // Evaluate expressions for specific props
657
+ if (typeof value === 'string' && (
658
+ propKey === 'disabled' ||
659
+ propKey === 'readonly' ||
660
+ propKey === 'required' ||
661
+ propKey === 'visible' ||
662
+ propKey === 'hidden'
663
+ )) {
664
+ // Check if it looks like a JavaScript expression
665
+ if (value.includes('===') || value.includes('!==') ||
666
+ value.includes('==') || value.includes('!=') ||
667
+ value.includes('&&') || value.includes('||') ||
668
+ value.includes('?') || value.includes(':')) {
669
+ cleanProps[propKey] = evaluateExpression(value, context);
670
+ } else if (context.hasOwnProperty(value)) {
671
+ // Simple variable reference
672
+ cleanProps[propKey] = context[value];
673
+ } else {
674
+ cleanProps[propKey] = value;
675
+ }
676
+ } else {
677
+ cleanProps[propKey] = value;
678
+ }
679
+ });
439
680
 
681
+ if (disabled !== undefined) {
682
+ if (typeof disabled === 'string') {
683
+ cleanProps.disabled = evaluateExpression(disabled, context);
684
+ } else {
685
+ cleanProps.disabled = disabled;
686
+ }
687
+ }
688
+
689
+ // Dynamically handle events configuration
690
+ if (events && typeof events === 'object') {
691
+ Object.keys(events).forEach(eventName => {
692
+ const functionName = events[eventName];
693
+
694
+ // Convert event name to Vue prop format (onEventName)
695
+ let vueEventName;
696
+
697
+ if (eventName.includes(':')) {
698
+ vueEventName = 'on' + eventName
699
+ .split(':')
700
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
701
+ .join('');
702
+ } else if (eventName.includes('-')) {
703
+ vueEventName = 'on' + eventName
704
+ .split('-')
705
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
706
+ .join('');
707
+ } else {
708
+ vueEventName = 'on' + eventName.charAt(0).toUpperCase() + eventName.slice(1);
709
+ }
710
+
711
+ // Create the event handler
712
+ cleanProps[vueEventName] = (data) => {
713
+ executeFunction(functionName, data);
714
+ };
715
+ });
716
+ }
717
+
718
+ // Handle item-text, item-value, item-title transformations
719
+ if (cleanProps.itemText) {
720
+ cleanProps['item-text'] = cleanProps.itemText;
721
+ delete cleanProps.itemText;
722
+ }
723
+ if (cleanProps.itemValue) {
724
+ cleanProps['item-value'] = cleanProps.itemValue;
725
+ delete cleanProps.itemValue;
726
+ }
727
+ if (cleanProps.itemTitle) {
728
+ cleanProps['item-title'] = cleanProps.itemTitle;
729
+ delete cleanProps.itemTitle;
730
+ }
731
+
732
+ return cleanProps;
733
+ };
734
+
440
735
  const parseRules = (rulesString) => {
441
736
  if (!rulesString || !rules) return [];
737
+
442
738
  if (Array.isArray(rulesString)) return rulesString;
443
-
739
+
444
740
  if (typeof rulesString === 'string') {
445
741
  try {
446
- // Wrap in array brackets if not already wrapped
447
- const normalized = rulesString.trim().startsWith('[')
448
- ? rulesString
449
- : `[${rulesString}]`
450
-
451
- // Evaluate with full context - rules and formData available
452
- const result = new Function('rules', 'formData', 'formVariables', `return ${normalized}`)(
453
- rules,
454
- formData,
455
- formVariables
456
- )
457
-
458
- return Array.isArray(result) ? result.filter(Boolean) : [result].filter(Boolean)
742
+ const cleanString = rulesString.replace(/[\[\]]/g, '').trim();
743
+ const ruleNames = cleanString.split(',').map(r => r.trim());
744
+
745
+ return ruleNames.map(ruleName => {
746
+ const match = ruleName.match(/rules\.(\w+)(\(.*\))?/);
747
+ if (match) {
748
+ const [, funcName, args] = match;
749
+
750
+ if (rules[funcName]) {
751
+ if (args) {
752
+ const argValues = args.slice(1, -1).split(',').map(arg => {
753
+ arg = arg.trim();
754
+ return isNaN(arg) ? arg.replace(/['"]/g, '') : Number(arg);
755
+ });
756
+ return rules[funcName](...argValues);
757
+ }
758
+ return rules[funcName];
759
+ }
760
+ }
761
+ return null;
762
+ }).filter(Boolean);
459
763
  } catch (e) {
460
764
  console.error('Failed to parse rules:', rulesString, e);
461
765
  return [];
462
766
  }
463
767
  }
464
-
768
+
465
769
  return [];
466
770
  };
467
771
 
468
772
  const handleSubmit = async (buttonType) => {
469
- clickedButton.value = buttonType
470
-
471
- const { valid: isValid } = await formRef.value?.validate()
773
+ clickedButton.value = buttonType;
774
+ const { valid: isValid } = await formRef.value?.validate();
472
775
  if (!isValid) {
473
- snackbar?.show({ message: 'Please fix validation errors', variant: 'error' })
474
- return
776
+ snackbar?.show({ message: 'Please fix validation errors', variant: 'error' });
777
+ return;
475
778
  }
476
779
 
477
780
  if (!api) {
478
- snackbar?.show({ message: 'API not configured', variant: 'error' })
479
- return
781
+ console.error('API not available');
782
+ snackbar?.show({ message: 'API not configured', variant: 'error' });
783
+ return;
480
784
  }
481
785
 
482
786
  if (!props.form?.crudLink) {
483
- snackbar?.show({ message: 'Form configuration error', variant: 'error' })
484
- return
787
+ console.error('No crudLink defined in form config');
788
+ snackbar?.show({ message: 'Form configuration error', variant: 'error' });
789
+ return;
485
790
  }
486
791
 
487
- isSubmitting.value = true
792
+ isSubmitting.value = true;
488
793
 
489
794
  try {
490
- const submitData = getDbFieldsOnly(formData)
491
- const currentId = formId.value || props.editItemId
492
-
493
- let response
494
-
495
- if (buttonType === 'draft' && !currentId) {
496
- // ── First save: POST with status=draft ────────────────────────
497
- submitData.status = 'draft'
498
- const createLink = props.form?.createLink || props.form?.crudLink
499
- response = await api.post(createLink, submitData)
500
-
501
- // Transition to edit mode
502
- formId.value = (response?.data?.uuid || response?.uuid) ?? (response?.data?.id || response?.id)
503
- formData.id = response?.data?.id || response?.id
504
- relationId.value = response?.data?.id || response?.id
505
- formMode.value = 'edit' // ← trigger button swap
506
-
507
- await fetchData() // load saved data back
508
-
509
- // snackbar?.show({ message: response?.data?.message || 'Draft saved', variant: 'success' })
510
- // Do NOT close form
511
- emit('submit:success', response?.message)
512
-
513
- } else if (buttonType === 'update') {
514
- // ── Update: PUT, do not close ─────────────────────────────────
515
- submitData.status = 'draft'
516
- //submitData.id = currentId
517
- const updateLink = props.form?.updateLink || props.form?.crudLink
518
- response = await api.put(`${updateLink}/${currentId}`, submitData)
519
-
520
- await fetchData()
521
-
522
- // snackbar?.show({ message: response?.data?.message || 'Updated successfully', variant: 'success' })
523
- // Do NOT close form
524
- emit('submit:success', response?.message)
525
-
526
- } else if (buttonType === 'submit') {
527
- // ── Submit: PUT with status=submitted, then close ─────────────
528
- submitData.status = 'submitted'
529
- //submitData.id = currentId
530
- const updateLink = props.form?.updateLink || props.form?.crudLink
531
- response = await api.put(`${updateLink}/${currentId}`, submitData)
532
-
533
- // snackbar?.show({ message: response?.data?.message || 'Submitted successfully', variant: 'success' })
534
- emit('submit:success', response?.message)
535
- handleClose()
536
- formRef.value?.reset()
795
+ // Filter out non-database fields before submission
796
+ const submitData = getDbFieldsOnly(formData);
797
+ if (clickedButton.value === 'draft') {
798
+ submitData.status = 'draft';
799
+ }else {
800
+ submitData.status = 'submitted';
537
801
  }
538
-
802
+ if (formId.value) {
803
+ submitData.id = formId.value; // Ensure ID is included for updates
804
+ }
805
+ // const isFullUrl = /^https?:\/\//i.test(props.form?.crudLink)
806
+ // let url = isFullUrl ? props.form?.crudLink : baseUrl + '/' + props.form?.crudLink
807
+ let completeUrl = ''
808
+
809
+ if ((props.mode === 'edit' && props.editItemId) || clickedButton.value === 'submitted') {
810
+ const editLink = props.form?.updateLink || props.form?.crudLink;
811
+ // const isFullUrl = /^https?:\/\//i.test(editLink)
812
+ // let url = isFullUrl ? editLink : baseUrl + '/' + editLink
813
+ let url = editLink
814
+ url += `/${submitData.id}`;
815
+ completeUrl = api.put(url, submitData); // Use filtered data
816
+ } else {
817
+
818
+ const createLink = props.form?.createLink || props.form?.crudLink;
819
+ // const isFullUrl = /^https?:\/\//i.test(createLink)
820
+ // let url = isFullUrl ? createLink : baseUrl + '/' + createLink
821
+ let url = createLink
822
+ completeUrl = api.post(url, submitData); // Use filtered data
823
+ }
824
+ const response = await completeUrl;
825
+
826
+ const successMessage = response.data?.message || response.message || 'Created successfully';
827
+ snackbar?.show({ message: successMessage, variant: 'success' });
828
+
829
+ formId.value = response?.data?.id || formData.id || props.editItemId || null; // Update formId with response ID if available
830
+ // submitData.id = formId.value; // Ensure submitData has the correct ID for any subsequent operations
831
+ console.log('Form submission response:', response);
832
+
833
+ if (clickedButton.value === 'draft') {
834
+ draftSubmitted.value = true;
835
+ }
836
+ if (clickedButton.value !== 'draft') {
837
+ emit('submit:success', response.message);
838
+ handleClose();
839
+ formRef.value?.reset();
840
+ }
841
+
539
842
  } catch (error) {
540
- console.error('Form submission error:', error)
541
- // const errorMessage = error.response?.data?.message || 'Form submission failed'
542
- // snackbar?.show({ message: errorMessage, variant: 'error' })
543
-
843
+ console.error('Form submission error:', error);
844
+ const errorMessage = error.response?.data?.message || 'Form submission failed';
845
+ snackbar?.show({ message: errorMessage, variant: 'error' });
544
846
  if (error.response?.status === 422 || error.response?.data?.errors) {
545
- formRef.value?.setErrors(error.response.data.errors)
546
- emit('submit:error', error)
847
+ const backendErrors = error.response.data.errors;
848
+ snackbar?.show({ message: backendErrors, variant: 'error' });
849
+
850
+ // if (formRef.value?.setErrors) {
851
+ formRef.value.setErrors(backendErrors);
852
+ // } else {
853
+ // console.error('formRef.value.setErrors not available')
854
+ // }
855
+
856
+ const errorMessage = error.response.data.message || 'Validation failed';
857
+ emit('submit:error', error);
547
858
  }
548
859
  } finally {
549
- isSubmitting.value = false
860
+ isSubmitting.value = false;
550
861
  }
551
- }
862
+ };
552
863
 
553
864
  // Helper function to filter out non-database fields
554
865
  const getDbFieldsOnly = (data) => {
@@ -561,7 +872,7 @@ const initializeFormData = () => {
561
872
  .filter(field => field && field.NotDbField === true)
562
873
  .map(field => field.key);
563
874
 
564
- // console.log('Non-DB fields to exclude:', nonDbFields);
875
+ console.log('Non-DB fields to exclude:', nonDbFields);
565
876
 
566
877
  // Create new object with only database fields
567
878
  const dbOnlyData = {};
@@ -571,112 +882,45 @@ const initializeFormData = () => {
571
882
  }
572
883
  });
573
884
 
574
- // console.log('Original form data:', data);
575
- // console.log('Filtered DB-only data:', dbOnlyData);
885
+ console.log('Original form data:', data);
886
+ console.log('Filtered DB-only data:', dbOnlyData);
576
887
 
577
888
  return dbOnlyData;
578
889
  };
579
- // Helper to get nested value by dot notation key e.g. "submissionDetails.form_ref_number"
580
- const getNestedValue = (obj, path) => {
581
- if (!path || !obj) return undefined
582
- return path.split('.').reduce((acc, key) => {
583
- if (acc === null || acc === undefined) return undefined
584
- return acc[key]
585
- }, obj)
586
- }
587
-
588
890
  // import dataList from '../../../pgo-components/services/data.json'
589
891
  const fetchData = async () => {
590
892
  // FormDataList.value = dataList
591
893
  // props.form.fields.forEach(field => {
592
- // if (!field || !field.key) return
593
-
594
- // const fieldKey = field.dataKey ?? field.key // support dataKey override
595
-
596
- // // Try dot notation first (e.g. "submissionDetails.form_ref_number")
597
- // if (fieldKey.includes('.')) {
598
- // const nestedVal = getNestedValue(FormDataList.value, fieldKey)
599
- // if (nestedVal !== undefined) {
600
- // formData[field.key] = nestedVal
601
- // return
602
- // }
603
- // }
604
-
605
- // // Fallback: flat key lookup
606
- // if (FormDataList.value[fieldKey] !== undefined) {
607
- // formData[field.key] = FormDataList.value[fieldKey]
608
- // }
894
+ // if (dataList[field.key] !== undefined) {
895
+ // formData[field.key] = dataList[field.key]
896
+ // }
609
897
  // })
610
-
611
- // await nextTick()
612
- // if (formRef.value?.validate) {
613
- // const result = await formRef.value.validate()
614
- // valid.value = result?.valid ?? false
615
- // }
616
- // originalFormData.value = JSON.parse(JSON.stringify({ ...formData }))
617
- // isPreFilled.value = true // mark as pre-filled after fetchData
618
898
  // console.log('Form Fetched data:', FormDataList.value);
619
899
  // return;
620
900
 
621
- const showId = formId.value || props.editItemId
622
- // if (!id) return
901
+ if (props.mode !== 'edit' || !props.editItemId || clickedButton.value !== 'draft') {
902
+ return;
903
+ }
623
904
 
624
905
  try {
625
- const showLink = props.form?.showLink || props.form?.crudLink
626
- let url = `${showLink}/${showId}`
627
- if (props.form?.showLinkParams) {
628
- const rawParams = props.form.showLinkParams
629
- if (typeof rawParams === 'string') {
630
- url = `${url}?${rawParams}`
631
- } else {
632
- const flattened = {}
633
- for (const [key, value] of Object.entries(rawParams)) {
634
- flattened[key] = (typeof value === 'object' && value !== null)
635
- ? JSON.stringify(value)
636
- : value
637
- }
638
- // url = `${url}?${new URLSearchParams(flattened).toString()}`
639
- url = `${url}?payload=${encodeURIComponent(JSON.stringify(rawParams))}`
640
- }
641
- }
642
-
643
- const response = await api.get(`${url}`)
644
- // const response = await api.get(`${url}/${props.editItemId}`)
645
-
646
- // console.log('Fetched data from API:', response);
647
-
906
+ // console.log('form:', props.form);
907
+ const showLink = props.form?.showLink || props.form?.crudLink;
908
+ // const isFullUrl = /^https?:\/\//i.test(showLink)
909
+ // let url = isFullUrl ? showLink : baseUrl + '/' + showLink
910
+ let url = showLink;
911
+
912
+ const response = await api.get(`${url}/${props.editItemId}`)
913
+
648
914
  if (response) {
649
915
  const fetchedData = response
650
- FormDataList.value = response || response.data || {}
651
- formData.id = response?.data?.id || response?.id
652
- relationId.value = response?.data?.id || response?.id
653
-
916
+ FormDataList.value = response || response.data || {};
917
+ formData.id = props.editItemId
918
+
654
919
  props.form.fields.forEach(field => {
655
- if (!field || !field.key) return
656
-
657
- const fieldKey = field.key
658
-
659
- if (fieldKey.includes('.')) {
660
- const nestedVal = getNestedValue(fetchedData, fieldKey)
661
- if (nestedVal !== undefined) {
662
- formData[field.key] = nestedVal
663
- return
664
- }
665
- }
666
-
667
- if (fetchedData[fieldKey] !== undefined) {
668
- formData[field.key] = fetchedData[fieldKey]
920
+ if (fetchedData[field.key] !== undefined) {
921
+ formData[field.key] = fetchedData[field.key]
669
922
  }
670
923
  })
671
- // console.log('Form data after fetch:', formData);
672
- await nextTick()
673
- if (formRef.value?.validate) {
674
- const result = await formRef.value.validate()
675
- valid.value = result?.valid ?? false
676
- }
677
- originalFormData.value = JSON.parse(JSON.stringify({ ...formData }))
678
- isPreFilled.value = true // mark as pre-filled after fetchData
679
-
680
924
  }
681
925
  } catch (error) {
682
926
  snackbar?.show({ message: 'Error fetching data', variant: 'error' })
@@ -684,20 +928,17 @@ const initializeFormData = () => {
684
928
  }
685
929
  }
686
930
 
687
-
688
931
  const handleClose = () => {
689
- open.value = false
690
- isSubmitting.value = false
691
- formRef.value?.reset()
692
- valid.value = false
693
- isPreFilled.value = false
694
- formMode.value = props.mode // ← reset on close
695
- originalFormData.value = {}
696
- formId.value = null
697
- emit('close', false)
932
+ open.value = false;
933
+ isSubmitting.value = false;
934
+ formRef.value?.reset();
935
+ valid.value = false;
936
+ emit('close', false);
698
937
  };
699
938
 
700
939
  const handleUploadSuccess = () => {
940
+ // After a successful file upload, refetch the item data to get the latest file list
941
+ // showUploads.value = true
701
942
  fetchData();
702
943
  };
703
944
  // Initialize on mount
@@ -717,7 +958,6 @@ const initializeFormData = () => {
717
958
  // Watch for form changes
718
959
  watch(() => props.form, (newForm) => {
719
960
  if (newForm) {
720
- initializeFormData();
721
961
  if (newForm.variables) {
722
962
  initializeVariables();
723
963
  }
@@ -725,10 +965,9 @@ const initializeFormData = () => {
725
965
  initializeAllFunctions();
726
966
  }
727
967
  // Only fetch data if we're in edit mode and have the necessary data
728
- if (props.mode === 'edit' && props.editItemId && newForm.crudLink) {
729
- formId.value = props.editItemId
730
- fetchData();
731
- }
968
+ if (props.mode === 'edit' && props.editItemId && newForm.crudLink) {
969
+ fetchData();
970
+ }
732
971
  }
733
972
  }, { deep: true });
734
973
 
@@ -743,9 +982,7 @@ const initializeFormData = () => {
743
982
 
744
983
  // Also watch for mode changes
745
984
  watch(() => props.mode, (newMode) => {
746
- formMode.value = newMode
747
985
  if (newMode === 'edit' && props.editItemId) {
748
- formId.value = props.editItemId
749
986
  fetchData()
750
987
  } else if (newMode === 'create') {
751
988
  initializeFormData()
@@ -753,14 +990,13 @@ const initializeFormData = () => {
753
990
  })
754
991
 
755
992
  // Initialize on mount
756
- // onMounted(async () => {
757
- // await nextTick()
758
- // // Re-validate to ensure form state reflects defaults
759
- // if (formRef.value?.validate) {
760
- // const result = await formRef.value.validate()
761
- // valid.value = result?.valid ?? false
762
- // }
763
- // })
993
+ onMounted(() => {
994
+ // if (props.mode === 'edit' && props.editItemId) {
995
+ // // fetchData()
996
+ // } else {
997
+ // initializeFormData()
998
+ // }
999
+ })
764
1000
 
765
1001
  watch(() => props.modelValue, (newVal) => {
766
1002
  // open.value = newVal;
@@ -773,43 +1009,7 @@ const initializeFormData = () => {
773
1009
  if (!newVal) {
774
1010
  formRef.value?.reset();
775
1011
  valid.value = false; // Reset valid state when modal closes
776
- // console.log('form open', formRef.value.isValid, valid.value);
1012
+ console.log('form open', formRef.value.isValid, valid.value);
777
1013
  }
778
1014
  });
779
- </script>
780
-
781
- <script>
782
- const safeEval = (expression, context) => {
783
- const functionKeys = []
784
- const functionValues = []
785
- const varDeclarations = []
786
-
787
- Object.keys(context)
788
- .filter(key => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key))
789
- .forEach(key => {
790
- const value = context[key]
791
-
792
- if (typeof value === 'function') {
793
- functionKeys.push(key)
794
- functionValues.push(value)
795
- } else if (value === null || value === undefined) {
796
- varDeclarations.push(`var ${key} = null;`)
797
- } else if (typeof value === 'object') {
798
- try {
799
- varDeclarations.push(`var ${key} = ${JSON.stringify(value)};`)
800
- } catch {
801
- varDeclarations.push(`var ${key} = null;`)
802
- }
803
- } else if (typeof value === 'string') {
804
- varDeclarations.push(`var ${key} = ${JSON.stringify(value)};`)
805
- } else {
806
- varDeclarations.push(`var ${key} = ${value};`)
807
- }
808
- })
809
-
810
- // expression is used as-is — caller decides whether to add "return (...)"
811
- const body = varDeclarations.join('\n') + `\n${expression}`
812
- const fn = new Function(...functionKeys, body)
813
- return fn(...functionValues)
814
- }
815
- </script>
1015
+ </script>