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