df-ae-forms-package 1.1.4 → 1.1.6
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.
- package/dist/index.esm.js +348 -76
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +348 -76
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -818,7 +818,6 @@ const DfFormInput = ({ id, properties, validationErrors = {}, formValue = '', in
|
|
|
818
818
|
// Update ref if ID changes (shouldn't happen, but safety check)
|
|
819
819
|
useEffect(() => {
|
|
820
820
|
if (id !== componentIdRef.current) {
|
|
821
|
-
console.warn(`[DfFormInput] Component ID changed from ${componentIdRef.current} to ${id}`);
|
|
822
821
|
componentIdRef.current = id;
|
|
823
822
|
}
|
|
824
823
|
}, [id]);
|
|
@@ -2402,6 +2401,7 @@ const DfFormFileUpload = ({ id, properties, validationErrors = {}, formValue = n
|
|
|
2402
2401
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
2403
2402
|
const [isTouched, setIsTouched] = useState(false);
|
|
2404
2403
|
const fileInputRef = useRef(null);
|
|
2404
|
+
const lastFormValueRef = useRef(null); // Track the last formValue to prevent unnecessary resets
|
|
2405
2405
|
// Convert FileList or File[] to IFilePreview[]
|
|
2406
2406
|
const convertToFilePreviews = useCallback((fileList, startIndex = 0) => {
|
|
2407
2407
|
if (!fileList)
|
|
@@ -2494,12 +2494,20 @@ const DfFormFileUpload = ({ id, properties, validationErrors = {}, formValue = n
|
|
|
2494
2494
|
if (fileObject.url || fileObject.path) {
|
|
2495
2495
|
preview = fileObject.url || fileObject.path;
|
|
2496
2496
|
}
|
|
2497
|
-
else if (
|
|
2498
|
-
// Handle base64 data
|
|
2497
|
+
else if (fileObject.data) {
|
|
2498
|
+
// Handle base64 data - works for all file types with data
|
|
2499
|
+
const resolvedType = fileType || 'application/octet-stream';
|
|
2499
2500
|
const fileData = fileObject.data;
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2501
|
+
if (typeof fileData === 'string') {
|
|
2502
|
+
if (fileData.startsWith('data:')) {
|
|
2503
|
+
// Already a full data URI
|
|
2504
|
+
preview = fileData;
|
|
2505
|
+
}
|
|
2506
|
+
else {
|
|
2507
|
+
// Raw base64 - construct data URI
|
|
2508
|
+
preview = `data:${resolvedType};base64,${fileData}`;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2503
2511
|
}
|
|
2504
2512
|
// Use the original object to preserve data
|
|
2505
2513
|
fileObj = fileObject;
|
|
@@ -2684,10 +2692,35 @@ const DfFormFileUpload = ({ id, properties, validationErrors = {}, formValue = n
|
|
|
2684
2692
|
}
|
|
2685
2693
|
}, [mode]);
|
|
2686
2694
|
// Update value when formValue prop changes or on mount
|
|
2695
|
+
// CRITICAL: Use a stable reference check to prevent unnecessary resets
|
|
2687
2696
|
useEffect(() => {
|
|
2688
|
-
//
|
|
2697
|
+
// Skip if formValue is null/undefined (no data)
|
|
2698
|
+
if (formValue === null || formValue === undefined || (typeof formValue === 'string' && formValue === '')) {
|
|
2699
|
+
// Only clear files if we previously had files from formValue
|
|
2700
|
+
// Don't clear files that were added by user interaction
|
|
2701
|
+
if (lastFormValueRef.current !== null && lastFormValueRef.current !== undefined && lastFormValueRef.current !== '') {
|
|
2702
|
+
setFiles([]);
|
|
2703
|
+
lastFormValueRef.current = null;
|
|
2704
|
+
}
|
|
2705
|
+
return;
|
|
2706
|
+
}
|
|
2707
|
+
// Check if formValue actually changed (deep comparison for arrays)
|
|
2708
|
+
try {
|
|
2709
|
+
const currentStr = JSON.stringify(formValue);
|
|
2710
|
+
const prevStr = JSON.stringify(lastFormValueRef.current);
|
|
2711
|
+
if (currentStr === prevStr) {
|
|
2712
|
+
return; // No change, skip
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
catch {
|
|
2716
|
+
// If stringify fails, proceed with update
|
|
2717
|
+
}
|
|
2718
|
+
// Convert and set new files
|
|
2689
2719
|
const newFiles = convertToFilePreviews(formValue);
|
|
2690
|
-
|
|
2720
|
+
if (newFiles.length > 0) {
|
|
2721
|
+
setFiles(newFiles);
|
|
2722
|
+
lastFormValueRef.current = formValue;
|
|
2723
|
+
}
|
|
2691
2724
|
}, [formValue, convertToFilePreviews]);
|
|
2692
2725
|
// Mark as touched when form is submitted
|
|
2693
2726
|
useEffect(() => {
|
|
@@ -2962,8 +2995,6 @@ const DfFormLocation = ({ id, properties, validationErrors = {}, formValue = nul
|
|
|
2962
2995
|
}
|
|
2963
2996
|
}
|
|
2964
2997
|
catch (error) {
|
|
2965
|
-
// Capacitor not properly initialized, fall through to web geolocation
|
|
2966
|
-
console.warn('Capacitor Geolocation not available, falling back to web geolocation');
|
|
2967
2998
|
}
|
|
2968
2999
|
}
|
|
2969
3000
|
// Fallback to standard web geolocation API (for browsers)
|
|
@@ -3677,7 +3708,6 @@ const AttachmentThumbnails = ({ attachments, onRemove }) => {
|
|
|
3677
3708
|
newUrls.set(index, url);
|
|
3678
3709
|
}
|
|
3679
3710
|
catch (e) {
|
|
3680
|
-
console.warn('Failed to create object URL for attachment:', e);
|
|
3681
3711
|
}
|
|
3682
3712
|
}
|
|
3683
3713
|
else if (file && file.url && file.type && file.type.startsWith('image/')) {
|
|
@@ -4588,7 +4618,6 @@ formTemplateId, onThresholdActionCompletion, onThresholdIssueRaised, onNotesChan
|
|
|
4588
4618
|
zIndex: 5
|
|
4589
4619
|
}, children: jsxs("button", { onClick: (e) => {
|
|
4590
4620
|
e.stopPropagation();
|
|
4591
|
-
console.log('[TableView] Add Entry button clicked (grid view)', 'entries:', dataEntries.length, 'max:', maxEntries);
|
|
4592
4621
|
onAddEntry();
|
|
4593
4622
|
}, disabled: dataEntries.length >= maxEntries, style: {
|
|
4594
4623
|
padding: '8px 16px',
|
|
@@ -4714,7 +4743,6 @@ formTemplateId, onThresholdActionCompletion, onThresholdIssueRaised, onNotesChan
|
|
|
4714
4743
|
justifyContent: 'center'
|
|
4715
4744
|
}, children: jsxs("button", { onClick: (e) => {
|
|
4716
4745
|
e.stopPropagation();
|
|
4717
|
-
console.log('[TableView] Add Entry button clicked (list view)', 'entries:', dataEntries.length, 'max:', maxEntries);
|
|
4718
4746
|
onAddEntry();
|
|
4719
4747
|
}, disabled: dataEntries.length >= maxEntries, style: {
|
|
4720
4748
|
padding: '8px 16px',
|
|
@@ -4967,12 +4995,8 @@ const DfFormDataGrid = ({ id, properties, mode = 'edit', formData = {}, onValueC
|
|
|
4967
4995
|
// In edit/preview modes, don't handle value changes as components are read-only
|
|
4968
4996
|
}, [mode, onValueChange]);
|
|
4969
4997
|
const handleAddEntry = useCallback(() => {
|
|
4970
|
-
console.log('[DfFormDataGrid] handleAddEntry called - Component ID:', id);
|
|
4971
4998
|
// Safety check: ensure we have entries array
|
|
4972
4999
|
const currentEntries = Array.isArray(properties.entries) ? properties.entries : [];
|
|
4973
|
-
console.log('[DfFormDataGrid] gridComponents:', gridComponents.length);
|
|
4974
|
-
console.log('[DfFormDataGrid] current entries count:', currentEntries.length);
|
|
4975
|
-
console.log('[DfFormDataGrid] onValueChange exists:', !!onValueChange);
|
|
4976
5000
|
// Use timestamp and random string to ensure uniqueness even if entries are deleted
|
|
4977
5001
|
const uniqueSuffix = `${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
|
|
4978
5002
|
const newEntryId = `entry-${uniqueSuffix}`;
|
|
@@ -4994,11 +5018,8 @@ const DfFormDataGrid = ({ id, properties, mode = 'edit', formData = {}, onValueC
|
|
|
4994
5018
|
}),
|
|
4995
5019
|
styles: {}
|
|
4996
5020
|
};
|
|
4997
|
-
console.log('[DfFormDataGrid] newEntry created:', newEntry.id, 'with', newEntry.components.length, 'components');
|
|
4998
5021
|
const updatedEntries = [...currentEntries, newEntry];
|
|
4999
|
-
console.log('[DfFormDataGrid] updatedEntries count:', updatedEntries.length);
|
|
5000
5022
|
if (onValueChange) {
|
|
5001
|
-
console.log('[DfFormDataGrid] calling onValueChange with updated datagrid structure - Entries:', updatedEntries.length);
|
|
5002
5023
|
onValueChange({
|
|
5003
5024
|
id: gridId, // Use sanitized gridId
|
|
5004
5025
|
value: {
|
|
@@ -5008,13 +5029,9 @@ const DfFormDataGrid = ({ id, properties, mode = 'edit', formData = {}, onValueC
|
|
|
5008
5029
|
});
|
|
5009
5030
|
// Notify parent if callback provided
|
|
5010
5031
|
if (onEntryAdd) {
|
|
5011
|
-
console.log('[DfFormDataGrid] calling onEntryAdd callback');
|
|
5012
5032
|
onEntryAdd();
|
|
5013
5033
|
}
|
|
5014
5034
|
}
|
|
5015
|
-
else {
|
|
5016
|
-
console.warn('[DfFormDataGrid] Cannot add entry: onValueChange is missing');
|
|
5017
|
-
}
|
|
5018
5035
|
}, [onValueChange, properties, gridId, gridComponents, onEntryAdd]);
|
|
5019
5036
|
const handleRemoveEntry = useCallback((entryIndex) => {
|
|
5020
5037
|
// Safety check: ensure we have entries array
|
|
@@ -5023,7 +5040,6 @@ const DfFormDataGrid = ({ id, properties, mode = 'edit', formData = {}, onValueC
|
|
|
5023
5040
|
.filter((_, index) => index !== entryIndex)
|
|
5024
5041
|
.map((entry, index) => ({ ...entry, index })); // Only update index, preserve unique ID
|
|
5025
5042
|
if (onValueChange) {
|
|
5026
|
-
console.log('[DfFormDataGrid] Removing entry at index:', entryIndex, 'New count:', updatedEntries.length);
|
|
5027
5043
|
onValueChange({
|
|
5028
5044
|
id: gridId, // Use sanitized gridId
|
|
5029
5045
|
value: { ...properties, entries: updatedEntries }
|
|
@@ -5411,8 +5427,8 @@ onComponentSelect, onComponentDelete, onComponentEdit, onComponentUpdate, select
|
|
|
5411
5427
|
const [validationErrors, setValidationErrors] = useState({});
|
|
5412
5428
|
const [formSubmitted, setFormSubmitted] = useState(false);
|
|
5413
5429
|
const [touchedFields, setTouchedFields] = useState({});
|
|
5414
|
-
//
|
|
5415
|
-
|
|
5430
|
+
// Component visibility state - driven by conditional logic evaluation
|
|
5431
|
+
const [componentVisibility, setComponentVisibility] = useState({});
|
|
5416
5432
|
// Track raised issues for threshold conditions (Set of condition IDs)
|
|
5417
5433
|
const [raisedThresholdIssues, setRaisedThresholdIssues] = useState(new Set());
|
|
5418
5434
|
// Track threshold action completions: Map<conditionId, { notesCompleted, attachmentsCompleted, emailSent }>
|
|
@@ -5431,7 +5447,38 @@ onComponentSelect, onComponentDelete, onComponentEdit, onComponentUpdate, select
|
|
|
5431
5447
|
const initializeComponentValues = (components, values) => {
|
|
5432
5448
|
components.forEach(component => {
|
|
5433
5449
|
const componentId = ensureStringId$1(component.id);
|
|
5434
|
-
if (componentId)
|
|
5450
|
+
if (!componentId)
|
|
5451
|
+
return; // Skip components without valid IDs
|
|
5452
|
+
// CRITICAL: Handle file component FIRST — file data is an array, not a simple string
|
|
5453
|
+
// The general handler below would misinterpret an empty file array
|
|
5454
|
+
if (component.name === 'file' && component.basic) {
|
|
5455
|
+
// Don't overwrite if already initialized
|
|
5456
|
+
if (values[componentId] !== undefined && values[componentId] !== '' && values[componentId] !== null) ;
|
|
5457
|
+
else {
|
|
5458
|
+
// Check all possible locations where file data could be stored
|
|
5459
|
+
const fileData = component.basic.value ||
|
|
5460
|
+
component.basic.files ||
|
|
5461
|
+
component.basic.attachments;
|
|
5462
|
+
if (fileData && (Array.isArray(fileData) ? fileData.length > 0 : true)) {
|
|
5463
|
+
values[componentId] = fileData;
|
|
5464
|
+
}
|
|
5465
|
+
else {
|
|
5466
|
+
values[componentId] = null; // Use null, not empty string, for file components
|
|
5467
|
+
}
|
|
5468
|
+
}
|
|
5469
|
+
}
|
|
5470
|
+
// Handle instructions component
|
|
5471
|
+
else if (component.name === 'instructions' && component.basic) {
|
|
5472
|
+
if (!component.basic.instructions) {
|
|
5473
|
+
component.basic.instructions = [];
|
|
5474
|
+
}
|
|
5475
|
+
const instructionValue = component.basic.value || component.basic.instructions;
|
|
5476
|
+
if (instructionValue) {
|
|
5477
|
+
values[componentId] = instructionValue;
|
|
5478
|
+
}
|
|
5479
|
+
}
|
|
5480
|
+
// General handler for all other components
|
|
5481
|
+
else {
|
|
5435
5482
|
// ALWAYS prioritize existing form state (values param) if it exists
|
|
5436
5483
|
if (values[componentId] !== undefined) ;
|
|
5437
5484
|
// Then use captured value in basic.value
|
|
@@ -5446,10 +5493,10 @@ onComponentSelect, onComponentDelete, onComponentEdit, onComponentUpdate, select
|
|
|
5446
5493
|
else {
|
|
5447
5494
|
// For checkbox and multi-select, empty array
|
|
5448
5495
|
if (component.name === 'checkbox' || component.name === 'select') {
|
|
5449
|
-
values[
|
|
5496
|
+
values[componentId] = [];
|
|
5450
5497
|
}
|
|
5451
5498
|
else {
|
|
5452
|
-
values[
|
|
5499
|
+
values[componentId] = '';
|
|
5453
5500
|
}
|
|
5454
5501
|
}
|
|
5455
5502
|
}
|
|
@@ -5471,35 +5518,19 @@ onComponentSelect, onComponentDelete, onComponentEdit, onComponentUpdate, select
|
|
|
5471
5518
|
}
|
|
5472
5519
|
});
|
|
5473
5520
|
}
|
|
5474
|
-
// Handle file component - initialize with file data if present
|
|
5475
|
-
if (component.name === 'file' && component.basic) {
|
|
5476
|
-
const fileData = component.basic.files || component.basic.attachments || component.basic.value;
|
|
5477
|
-
if (fileData) {
|
|
5478
|
-
values[component.id] = fileData;
|
|
5479
|
-
}
|
|
5480
|
-
}
|
|
5481
5521
|
// Initialize notes and attachments
|
|
5482
|
-
if (
|
|
5522
|
+
if (componentId && component.basic) {
|
|
5483
5523
|
if (component.basic.notes) ;
|
|
5484
5524
|
}
|
|
5485
|
-
// Handle instructions component - ensure instructions array exists and initialize formValue
|
|
5486
|
-
if (component.name === 'instructions' && component.basic) {
|
|
5487
|
-
if (!component.basic.instructions) {
|
|
5488
|
-
// Initialize empty instructions array if not present
|
|
5489
|
-
component.basic.instructions = [];
|
|
5490
|
-
}
|
|
5491
|
-
// CRITICAL: Initialize formValue for instructions from API data
|
|
5492
|
-
// Check if component has value from API (could be in basic.value, basic.instructions, or formData)
|
|
5493
|
-
const instructionValue = component.basic.value || component.basic.instructions;
|
|
5494
|
-
if (instructionValue) {
|
|
5495
|
-
// Store instruction data in formValues so it can be passed to DfFormInstruction
|
|
5496
|
-
values[component.id] = instructionValue;
|
|
5497
|
-
}
|
|
5498
|
-
}
|
|
5499
5525
|
// Handle nested components in section children
|
|
5500
5526
|
if (component.children && Array.isArray(component.children)) {
|
|
5501
5527
|
initializeComponentValues(component.children, values);
|
|
5502
5528
|
}
|
|
5529
|
+
// CRITICAL for conditional logic: Also store value under the label key
|
|
5530
|
+
// so condition lookups by label (e.g. "Temperature") work during init
|
|
5531
|
+
if (component.basic?.label && componentId && values[componentId] !== undefined) {
|
|
5532
|
+
values[component.basic.label] = values[componentId];
|
|
5533
|
+
}
|
|
5503
5534
|
});
|
|
5504
5535
|
};
|
|
5505
5536
|
const initializeFormState = useCallback(() => {
|
|
@@ -5571,15 +5602,12 @@ onComponentSelect, onComponentDelete, onComponentEdit, onComponentUpdate, select
|
|
|
5571
5602
|
const labelPart = safeLabel ? `-${safeLabel}` : '';
|
|
5572
5603
|
validatedComponent.id = `${name}${labelPart}-${index}`;
|
|
5573
5604
|
}
|
|
5574
|
-
console.warn(`[DfFormPreview] Fixed missing/invalid ID: ${validatedComponent.id}`);
|
|
5575
5605
|
}
|
|
5576
5606
|
else {
|
|
5577
5607
|
// ID is a valid string, check for duplicates
|
|
5578
5608
|
if (seenIds.has(validatedComponent.id)) {
|
|
5579
|
-
console.error(`[DfFormPreview] Duplicate component ID detected: ${validatedComponent.id}`);
|
|
5580
5609
|
// Generate a unique ID for duplicate - using index to keep it somewhat stable
|
|
5581
5610
|
validatedComponent.id = `${validatedComponent.id}-dup-${index}`;
|
|
5582
|
-
console.warn(`[DfFormPreview] Generated new unique ID: ${validatedComponent.id}`);
|
|
5583
5611
|
}
|
|
5584
5612
|
}
|
|
5585
5613
|
seenIds.add(validatedComponent.id);
|
|
@@ -5634,7 +5662,6 @@ onComponentSelect, onComponentDelete, onComponentEdit, onComponentUpdate, select
|
|
|
5634
5662
|
validatedFormComponents = getValidatedComponents(localFormComponents);
|
|
5635
5663
|
// Synchronize local state if components were mutated (IDs added/fixed)
|
|
5636
5664
|
if (JSON.stringify(validatedFormComponents) !== JSON.stringify(localFormComponents)) {
|
|
5637
|
-
console.log('[DfFormPreview] Synchronizing local components after ID validation');
|
|
5638
5665
|
setLocalFormComponents(validatedFormComponents);
|
|
5639
5666
|
onFormDataChange?.(validatedFormComponents);
|
|
5640
5667
|
}
|
|
@@ -5655,19 +5682,261 @@ onComponentSelect, onComponentDelete, onComponentEdit, onComponentUpdate, select
|
|
|
5655
5682
|
}
|
|
5656
5683
|
// Set the state
|
|
5657
5684
|
setFormValues(prev => {
|
|
5658
|
-
//
|
|
5659
|
-
|
|
5685
|
+
// Smart merge: initial values should win over empty/stale prev values
|
|
5686
|
+
// but user-entered values in prev should win over initial defaults
|
|
5687
|
+
const merged = { ...initialValues };
|
|
5688
|
+
Object.keys(prev).forEach(key => {
|
|
5689
|
+
const prevVal = prev[key];
|
|
5690
|
+
const initVal = initialValues[key];
|
|
5691
|
+
// If prev has a meaningful value (not empty/null/undefined), keep it
|
|
5692
|
+
if (prevVal !== undefined && prevVal !== null && prevVal !== '') {
|
|
5693
|
+
// But if initVal is an array (like file data) and prevVal is a simple empty value,
|
|
5694
|
+
// prefer the initialValue
|
|
5695
|
+
if (Array.isArray(initVal) && initVal.length > 0 && !Array.isArray(prevVal)) {
|
|
5696
|
+
// initialValue is a populated array but prev is not — keep initial
|
|
5697
|
+
merged[key] = initVal;
|
|
5698
|
+
}
|
|
5699
|
+
else {
|
|
5700
|
+
merged[key] = prevVal;
|
|
5701
|
+
}
|
|
5702
|
+
}
|
|
5703
|
+
// If prev has empty string but initial has actual data, use initial
|
|
5704
|
+
else if (initVal !== undefined && initVal !== null && initVal !== '') {
|
|
5705
|
+
merged[key] = initVal;
|
|
5706
|
+
}
|
|
5707
|
+
// Otherwise keep prev (even if empty — preserves user clearing a field)
|
|
5708
|
+
else {
|
|
5709
|
+
merged[key] = prevVal;
|
|
5710
|
+
}
|
|
5711
|
+
});
|
|
5712
|
+
return merged;
|
|
5660
5713
|
});
|
|
5661
5714
|
// Initialize notes and attachments state
|
|
5662
5715
|
setComponentNotes(prev => ({ ...initialNotes, ...prev }));
|
|
5663
5716
|
setComponentAttachments(prev => ({ ...initialAttachments, ...prev }));
|
|
5664
|
-
//
|
|
5665
|
-
|
|
5717
|
+
// Evaluate conditional logic on initialization
|
|
5718
|
+
evaluateConditionalLogic(initialValues);
|
|
5666
5719
|
}, [initialFormData, localFormComponents]);
|
|
5667
|
-
//
|
|
5668
|
-
|
|
5669
|
-
|
|
5720
|
+
// ======================== CONDITIONAL LOGIC ENGINE ========================
|
|
5721
|
+
// This is a pure visibility layer. It NEVER modifies component data.
|
|
5722
|
+
// It computes a { [componentId]: boolean } map based on each component's
|
|
5723
|
+
// `logic` property and the current form values.
|
|
5724
|
+
// Helper: Flatten all components (including nested) into a flat list
|
|
5725
|
+
const flattenAllComponents = useCallback((components) => {
|
|
5726
|
+
const flat = [];
|
|
5727
|
+
const traverse = (items) => {
|
|
5728
|
+
if (!items || !Array.isArray(items))
|
|
5729
|
+
return;
|
|
5730
|
+
for (const item of items) {
|
|
5731
|
+
flat.push(item);
|
|
5732
|
+
if (item.children && Array.isArray(item.children))
|
|
5733
|
+
traverse(item.children);
|
|
5734
|
+
if (item.cells && Array.isArray(item.cells)) {
|
|
5735
|
+
for (const row of item.cells) {
|
|
5736
|
+
const normalizedRow = normalizeTableRow(row);
|
|
5737
|
+
for (const cell of normalizedRow) {
|
|
5738
|
+
if (cell.components)
|
|
5739
|
+
traverse(cell.components);
|
|
5740
|
+
}
|
|
5741
|
+
}
|
|
5742
|
+
}
|
|
5743
|
+
if (item.entries && Array.isArray(item.entries)) {
|
|
5744
|
+
for (const entry of item.entries) {
|
|
5745
|
+
if (entry.components)
|
|
5746
|
+
traverse(entry.components);
|
|
5747
|
+
}
|
|
5748
|
+
}
|
|
5749
|
+
if (item.templateComponents && Array.isArray(item.templateComponents)) {
|
|
5750
|
+
traverse(item.templateComponents);
|
|
5751
|
+
}
|
|
5752
|
+
}
|
|
5753
|
+
};
|
|
5754
|
+
traverse(components);
|
|
5755
|
+
return flat;
|
|
5756
|
+
}, []);
|
|
5757
|
+
// Helper: Get the value of a component referenced by a condition's `when` field.
|
|
5758
|
+
// The `when` field is typically a component LABEL (e.g. "Temperature"), not an ID.
|
|
5759
|
+
const getConditionComponentValue = useCallback((conditionWhen, allComponents, currentFormValues) => {
|
|
5760
|
+
// Try to find the component by label, id, or _id
|
|
5761
|
+
const comp = allComponents.find(c => c.basic?.label === conditionWhen ||
|
|
5762
|
+
ensureStringId$1(c.id) === conditionWhen ||
|
|
5763
|
+
ensureStringId$1(c._id) === conditionWhen);
|
|
5764
|
+
if (!comp)
|
|
5765
|
+
return undefined;
|
|
5766
|
+
const compId = ensureStringId$1(comp.id || comp._id);
|
|
5767
|
+
// Check formValues by ID first
|
|
5768
|
+
if (compId && currentFormValues[compId] !== undefined) {
|
|
5769
|
+
return currentFormValues[compId];
|
|
5770
|
+
}
|
|
5771
|
+
// Also check by label (main website stores values by label too)
|
|
5772
|
+
if (comp.basic?.label && currentFormValues[comp.basic.label] !== undefined) {
|
|
5773
|
+
return currentFormValues[comp.basic.label];
|
|
5774
|
+
}
|
|
5775
|
+
// Fall back to component's basic.value or defaultValue
|
|
5776
|
+
if (comp.basic?.value !== undefined && comp.basic?.value !== null && comp.basic?.value !== '') {
|
|
5777
|
+
return comp.basic.value;
|
|
5778
|
+
}
|
|
5779
|
+
if (comp.basic?.defaultValue !== undefined && comp.basic?.defaultValue !== null && comp.basic?.defaultValue !== '') {
|
|
5780
|
+
return comp.basic.defaultValue;
|
|
5781
|
+
}
|
|
5782
|
+
return undefined;
|
|
5783
|
+
}, []);
|
|
5784
|
+
// Helper: Evaluate a single condition
|
|
5785
|
+
const evaluateSingleCondition = useCallback((condition, componentValue) => {
|
|
5786
|
+
const { operator, value } = condition;
|
|
5787
|
+
// isEmpty / isNotEmpty
|
|
5788
|
+
if (operator === 'isEmpty') {
|
|
5789
|
+
if (componentValue == null)
|
|
5790
|
+
return true;
|
|
5791
|
+
if (typeof componentValue === 'string')
|
|
5792
|
+
return componentValue.trim() === '';
|
|
5793
|
+
if (Array.isArray(componentValue))
|
|
5794
|
+
return componentValue.length === 0;
|
|
5795
|
+
return false;
|
|
5796
|
+
}
|
|
5797
|
+
if (operator === 'isNotEmpty') {
|
|
5798
|
+
if (componentValue == null)
|
|
5799
|
+
return false;
|
|
5800
|
+
if (typeof componentValue === 'string')
|
|
5801
|
+
return componentValue.trim() !== '';
|
|
5802
|
+
if (Array.isArray(componentValue))
|
|
5803
|
+
return componentValue.length > 0;
|
|
5804
|
+
return true;
|
|
5805
|
+
}
|
|
5806
|
+
// checked / notChecked (for checkboxes)
|
|
5807
|
+
if (operator === 'checked' || operator === 'notChecked') {
|
|
5808
|
+
let isChecked = false;
|
|
5809
|
+
if (typeof componentValue === 'boolean')
|
|
5810
|
+
isChecked = componentValue;
|
|
5811
|
+
else if (typeof componentValue === 'string')
|
|
5812
|
+
isChecked = componentValue.toLowerCase() === 'true' || componentValue === '1' || componentValue.length > 0;
|
|
5813
|
+
else if (Array.isArray(componentValue))
|
|
5814
|
+
isChecked = componentValue.length > 0;
|
|
5815
|
+
else if (typeof componentValue === 'number')
|
|
5816
|
+
isChecked = componentValue > 0;
|
|
5817
|
+
return operator === 'checked' ? isChecked : !isChecked;
|
|
5818
|
+
}
|
|
5819
|
+
// equals / notEquals
|
|
5820
|
+
if (operator === 'equals' || operator === 'notEquals') {
|
|
5821
|
+
let isEqual = false;
|
|
5822
|
+
// Loose comparison
|
|
5823
|
+
if (componentValue == value)
|
|
5824
|
+
isEqual = true;
|
|
5825
|
+
// String comparison (case-insensitive, trimmed)
|
|
5826
|
+
if (!isEqual && componentValue != null && value != null) {
|
|
5827
|
+
const strA = String(componentValue).trim();
|
|
5828
|
+
const strB = String(value).trim();
|
|
5829
|
+
// Numeric comparison within strings
|
|
5830
|
+
if (strA !== '' && strB !== '' && !isNaN(Number(strA)) && !isNaN(Number(strB))) {
|
|
5831
|
+
isEqual = Number(strA) === Number(strB);
|
|
5832
|
+
}
|
|
5833
|
+
if (!isEqual) {
|
|
5834
|
+
isEqual = strA.toLowerCase() === strB.toLowerCase();
|
|
5835
|
+
}
|
|
5836
|
+
}
|
|
5837
|
+
// Array contains check (for multi-select)
|
|
5838
|
+
if (!isEqual && Array.isArray(componentValue)) {
|
|
5839
|
+
isEqual = componentValue.some(v => String(v).trim().toLowerCase() === String(value).trim().toLowerCase());
|
|
5840
|
+
}
|
|
5841
|
+
return operator === 'equals' ? isEqual : !isEqual;
|
|
5842
|
+
}
|
|
5843
|
+
// contains / notContains
|
|
5844
|
+
if (operator === 'contains' || operator === 'notContains') {
|
|
5845
|
+
let doesContain = false;
|
|
5846
|
+
if (typeof componentValue === 'string' && value != null) {
|
|
5847
|
+
doesContain = componentValue.trim().toLowerCase().includes(String(value).trim().toLowerCase());
|
|
5848
|
+
}
|
|
5849
|
+
else if (Array.isArray(componentValue) && value != null) {
|
|
5850
|
+
doesContain = componentValue.some(v => String(v).trim().toLowerCase() === String(value).trim().toLowerCase());
|
|
5851
|
+
}
|
|
5852
|
+
return operator === 'contains' ? doesContain : !doesContain;
|
|
5853
|
+
}
|
|
5854
|
+
// Numeric comparisons
|
|
5855
|
+
if (['greaterThan', 'lessThan', 'greaterThanOrEqual', 'lessThanOrEqual'].includes(operator)) {
|
|
5856
|
+
const numA = typeof componentValue === 'number' ? componentValue : parseFloat(String(componentValue));
|
|
5857
|
+
const numB = typeof value === 'number' ? value : parseFloat(String(value));
|
|
5858
|
+
if (isNaN(numA) || isNaN(numB))
|
|
5859
|
+
return false;
|
|
5860
|
+
switch (operator) {
|
|
5861
|
+
case 'greaterThan': return numA > numB;
|
|
5862
|
+
case 'lessThan': return numA < numB;
|
|
5863
|
+
case 'greaterThanOrEqual': return numA >= numB;
|
|
5864
|
+
case 'lessThanOrEqual': return numA <= numB;
|
|
5865
|
+
default: return false;
|
|
5866
|
+
}
|
|
5867
|
+
}
|
|
5868
|
+
return false;
|
|
5670
5869
|
}, []);
|
|
5870
|
+
// Main conditional logic evaluation function
|
|
5871
|
+
const evaluateConditionalLogic = useCallback((explicitValues) => {
|
|
5872
|
+
const currentComponents = localFormComponents;
|
|
5873
|
+
const currentValues = explicitValues || formValues;
|
|
5874
|
+
if (!currentComponents || currentComponents.length === 0)
|
|
5875
|
+
return;
|
|
5876
|
+
const allComponents = flattenAllComponents(currentComponents);
|
|
5877
|
+
const visibility = {};
|
|
5878
|
+
allComponents.forEach(component => {
|
|
5879
|
+
const compId = ensureStringId$1(component.id || component._id);
|
|
5880
|
+
if (!compId)
|
|
5881
|
+
return;
|
|
5882
|
+
// Container components are ALWAYS visible — their children handle their own logic
|
|
5883
|
+
if (['table', 'datagrid', 'section', 'heading', 'instructions'].includes(component.name)) {
|
|
5884
|
+
visibility[compId] = true;
|
|
5885
|
+
return;
|
|
5886
|
+
}
|
|
5887
|
+
// Get the logic property (API sends it as `logic`, builder uses `conditional`)
|
|
5888
|
+
const logic = component.logic || component.conditional;
|
|
5889
|
+
// No logic or action is 'always' → always show
|
|
5890
|
+
if (!logic || logic.action === 'always' || !logic.conditions || logic.conditions.length === 0) {
|
|
5891
|
+
visibility[compId] = true;
|
|
5892
|
+
return;
|
|
5893
|
+
}
|
|
5894
|
+
// Evaluate each condition
|
|
5895
|
+
const conditionResults = logic.conditions.map((condition) => {
|
|
5896
|
+
const componentValue = getConditionComponentValue(condition.when, allComponents, currentValues);
|
|
5897
|
+
return evaluateSingleCondition(condition, componentValue);
|
|
5898
|
+
});
|
|
5899
|
+
// Determine if conditions are met based on 'when' (all/any)
|
|
5900
|
+
let conditionsMet;
|
|
5901
|
+
if (logic.when === 'any') {
|
|
5902
|
+
conditionsMet = conditionResults.some((r) => r);
|
|
5903
|
+
}
|
|
5904
|
+
else {
|
|
5905
|
+
// Default to 'all'
|
|
5906
|
+
conditionsMet = conditionResults.every((r) => r);
|
|
5907
|
+
}
|
|
5908
|
+
// Apply action
|
|
5909
|
+
if (logic.action === 'show') {
|
|
5910
|
+
visibility[compId] = conditionsMet;
|
|
5911
|
+
}
|
|
5912
|
+
else if (logic.action === 'hide') {
|
|
5913
|
+
visibility[compId] = !conditionsMet;
|
|
5914
|
+
}
|
|
5915
|
+
else {
|
|
5916
|
+
visibility[compId] = true;
|
|
5917
|
+
}
|
|
5918
|
+
});
|
|
5919
|
+
// Only update state if visibility actually changed (prevent re-render loops)
|
|
5920
|
+
setComponentVisibility(prev => {
|
|
5921
|
+
const prevStr = JSON.stringify(prev);
|
|
5922
|
+
const newStr = JSON.stringify(visibility);
|
|
5923
|
+
if (prevStr === newStr)
|
|
5924
|
+
return prev;
|
|
5925
|
+
return visibility;
|
|
5926
|
+
});
|
|
5927
|
+
}, [localFormComponents, formValues, flattenAllComponents, getConditionComponentValue, evaluateSingleCondition]);
|
|
5928
|
+
// Re-evaluate conditional logic whenever form values change
|
|
5929
|
+
useEffect(() => {
|
|
5930
|
+
evaluateConditionalLogic();
|
|
5931
|
+
}, [formValues, evaluateConditionalLogic]);
|
|
5932
|
+
// Check if a component should be visible
|
|
5933
|
+
const shouldShowComponent = useCallback((componentId) => {
|
|
5934
|
+
// In preview mode (read-only submission view), always show everything
|
|
5935
|
+
if (isPreviewMode)
|
|
5936
|
+
return true;
|
|
5937
|
+
// Default to visible if not explicitly set to false
|
|
5938
|
+
return componentVisibility[componentId] !== false;
|
|
5939
|
+
}, [componentVisibility, isPreviewMode]);
|
|
5671
5940
|
// Handle form value changes and re-evaluate conditional logic
|
|
5672
5941
|
const onFormValueChange = useCallback((change) => {
|
|
5673
5942
|
// CRITICAL: Validate that change.id is valid and unique
|
|
@@ -5719,6 +5988,13 @@ onComponentSelect, onComponentDelete, onComponentEdit, onComponentUpdate, select
|
|
|
5719
5988
|
// Only update formValues for actual value changes, not structure updates
|
|
5720
5989
|
if (!isStructureUpdate) {
|
|
5721
5990
|
newFormValues[change.id] = change.value;
|
|
5991
|
+
// CRITICAL for conditional logic: Also store value under the component's label.
|
|
5992
|
+
// Conditions reference components by label (e.g. "Temperature"), not by ID.
|
|
5993
|
+
const allComponents = flattenAllComponents(localFormComponents);
|
|
5994
|
+
const targetComp = allComponents.find(c => ensureStringId$1(c.id || c._id) === change.id);
|
|
5995
|
+
if (targetComp?.basic?.label) {
|
|
5996
|
+
newFormValues[targetComp.basic.label] = change.value;
|
|
5997
|
+
}
|
|
5722
5998
|
setFormValues(newFormValues);
|
|
5723
5999
|
}
|
|
5724
6000
|
// Clear raised issues for this component when value changes
|
|
@@ -6459,7 +6735,6 @@ onComponentSelect, onComponentDelete, onComponentEdit, onComponentUpdate, select
|
|
|
6459
6735
|
componentCounterRef.current += 1;
|
|
6460
6736
|
cachedId = `generated-${name}-${componentCounterRef.current}-${Math.random().toString(36).substr(2, 9)}`;
|
|
6461
6737
|
componentIdCacheRef.current.set(cacheKey, cachedId);
|
|
6462
|
-
console.warn('[DfFormPreview] Generated and cached ID for component:', cachedId, 'key:', cacheKey);
|
|
6463
6738
|
}
|
|
6464
6739
|
finalComponentId = cachedId;
|
|
6465
6740
|
// Create a new component object with the cached ID (don't mutate original)
|
|
@@ -6475,9 +6750,7 @@ onComponentSelect, onComponentDelete, onComponentEdit, onComponentUpdate, select
|
|
|
6475
6750
|
if (formValue !== undefined) {
|
|
6476
6751
|
// Check if this value is being used by multiple components
|
|
6477
6752
|
const componentsWithSameValue = localFormComponents.filter(comp => comp.id !== componentId && formValues[comp.id] === formValue);
|
|
6478
|
-
if (componentsWithSameValue.length > 0)
|
|
6479
|
-
console.warn(`[DfFormPreview] Component ${componentId} shares form value with other components:`, componentsWithSameValue.map(c => c.id));
|
|
6480
|
-
}
|
|
6753
|
+
if (componentsWithSameValue.length > 0) ;
|
|
6481
6754
|
}
|
|
6482
6755
|
const commonProps = {
|
|
6483
6756
|
id: componentId, // Use the validated component ID
|
|
@@ -6690,10 +6963,8 @@ onComponentSelect, onComponentDelete, onComponentEdit, onComponentUpdate, select
|
|
|
6690
6963
|
}, onValueChange: (change) => {
|
|
6691
6964
|
const changeId = ensureStringId$1(change.id);
|
|
6692
6965
|
const componentId = ensureStringId$1(component.id);
|
|
6693
|
-
console.log(`[DfFormPreview] datagrid onValueChange - Target: ${changeId}, Component: ${componentId}`);
|
|
6694
6966
|
// Handle datagrid value changes (entries updates)
|
|
6695
6967
|
if (changeId === componentId && change.value && typeof change.value === 'object' && 'entries' in change.value) {
|
|
6696
|
-
console.log('[DfFormPreview] datagrid entries update - entries count:', change.value.entries?.length);
|
|
6697
6968
|
// Update localFormComponents with new entries structure
|
|
6698
6969
|
const updatedComponents = localFormComponents.map(comp => {
|
|
6699
6970
|
const currentCompId = ensureStringId$1(comp.id);
|
|
@@ -6736,7 +7007,6 @@ onComponentSelect, onComponentDelete, onComponentEdit, onComponentUpdate, select
|
|
|
6736
7007
|
}
|
|
6737
7008
|
});
|
|
6738
7009
|
if (valuesChanged) {
|
|
6739
|
-
console.log('[DfFormPreview] Initializing form values for new datagrid entries');
|
|
6740
7010
|
setFormValues(newValues);
|
|
6741
7011
|
}
|
|
6742
7012
|
}
|
|
@@ -6815,12 +7085,14 @@ onComponentSelect, onComponentDelete, onComponentEdit, onComponentUpdate, select
|
|
|
6815
7085
|
} }));
|
|
6816
7086
|
case 'file':
|
|
6817
7087
|
// Get file value from formValues, or from component basic properties
|
|
6818
|
-
// CRITICAL:
|
|
6819
|
-
const
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
component.basic?.
|
|
6823
|
-
|
|
7088
|
+
// CRITICAL: formValue could be '' (empty string) which is falsy — check explicitly
|
|
7089
|
+
const hasFormValue = formValue !== undefined && formValue !== null && formValue !== '';
|
|
7090
|
+
const fileFormValue = hasFormValue
|
|
7091
|
+
? formValue
|
|
7092
|
+
: (component.basic?.value ||
|
|
7093
|
+
component.basic?.files ||
|
|
7094
|
+
component.basic?.attachments ||
|
|
7095
|
+
null);
|
|
6824
7096
|
return (jsx(DfFormFileUpload, { ...commonProps, properties: component, formValue: fileFormValue }));
|
|
6825
7097
|
default:
|
|
6826
7098
|
return (jsx("div", { className: "form-group", children: jsxs("div", { className: "form-group-label", children: ["Unsupported Component: ", component.name] }) }));
|