@visns-studio/visns-components 5.12.3 → 5.12.4
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/package.json +1 -1
- package/src/components/DataGrid.jsx +92 -50
- package/src/components/generic/GenericDetail.jsx +62 -25
- package/src/components/generic/StagePopupModal.jsx +451 -0
- package/src/components/styles/StagePopupModal.module.scss +234 -0
- package/src/components/utils/ConfirmDialog.js +1 -1
- package/src/index.js +2 -0
package/package.json
CHANGED
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
88
88
|
},
|
|
89
89
|
"name": "@visns-studio/visns-components",
|
|
90
|
-
"version": "5.12.
|
|
90
|
+
"version": "5.12.4",
|
|
91
91
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
92
92
|
"main": "src/index.js",
|
|
93
93
|
"files": [
|
|
@@ -40,9 +40,12 @@ import DateFilter from '@visns-studio/visns-datagrid-enterprise/DateFilter';
|
|
|
40
40
|
import ReactDataGrid from '@visns-studio/visns-datagrid-enterprise';
|
|
41
41
|
import { toast } from 'react-toastify';
|
|
42
42
|
import fetchUtil from '../utils/fetchUtil';
|
|
43
|
-
import { confirmDialog } from '
|
|
43
|
+
import { confirmDialog } from './utils/ConfirmDialog';
|
|
44
44
|
import { DEFAULT_INTELLIGENT_SORTING_CONFIG } from '../utils/relationshipSortingUtils';
|
|
45
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
extractColumnsMetadata,
|
|
47
|
+
isColumnSortable,
|
|
48
|
+
} from '../utils/columnsMetadataUtils';
|
|
46
49
|
import {
|
|
47
50
|
AlarmClock,
|
|
48
51
|
RotateCcw,
|
|
@@ -207,10 +210,10 @@ const loadData = async (
|
|
|
207
210
|
try {
|
|
208
211
|
const response = await fetchUtil.post(url, params);
|
|
209
212
|
const { data, total } = response.data;
|
|
210
|
-
|
|
213
|
+
|
|
211
214
|
// Extract columns metadata if available
|
|
212
215
|
const metadata = extractColumnsMetadata(response);
|
|
213
|
-
|
|
216
|
+
|
|
214
217
|
return { data, count: total, metadata };
|
|
215
218
|
} catch (error) {
|
|
216
219
|
console.error('Error fetching data:', error);
|
|
@@ -391,7 +394,7 @@ const DataGrid = forwardRef(
|
|
|
391
394
|
if (res.metadata) {
|
|
392
395
|
setColumnsMetadata(res.metadata);
|
|
393
396
|
}
|
|
394
|
-
|
|
397
|
+
|
|
395
398
|
// Sort data by grouping field if grouping is enabled
|
|
396
399
|
if (ajaxSetting?.groupBy?.length > 0) {
|
|
397
400
|
const groupField = ajaxSetting.groupBy[0]; // groupBy is array of strings
|
|
@@ -449,9 +452,9 @@ const DataGrid = forwardRef(
|
|
|
449
452
|
}, 10);
|
|
450
453
|
});
|
|
451
454
|
|
|
452
|
-
return sqlResult.then(res => ({
|
|
455
|
+
return sqlResult.then((res) => ({
|
|
453
456
|
data: res.data,
|
|
454
|
-
count: res.count
|
|
457
|
+
count: res.count,
|
|
455
458
|
}));
|
|
456
459
|
},
|
|
457
460
|
[ajaxSetting, dSearch, collapsedGroups]
|
|
@@ -872,15 +875,12 @@ const DataGrid = forwardRef(
|
|
|
872
875
|
case 'file':
|
|
873
876
|
case 'image':
|
|
874
877
|
case 'undo':
|
|
878
|
+
case 'restore':
|
|
875
879
|
// Execute the action directly since confirmation was already handled
|
|
876
|
-
CustomFetch(
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
(result) => {
|
|
881
|
-
toast.success(result.message);
|
|
882
|
-
}
|
|
883
|
-
);
|
|
880
|
+
CustomFetch(s.url, 'POST', { id: d[s.key] }, (result) => {
|
|
881
|
+
toast.success(result.message);
|
|
882
|
+
handleReload(); // Add reload to refresh data after action
|
|
883
|
+
});
|
|
884
884
|
break;
|
|
885
885
|
case 'clone':
|
|
886
886
|
if (s.url && s.url !== '') {
|
|
@@ -1346,17 +1346,19 @@ const DataGrid = forwardRef(
|
|
|
1346
1346
|
if (iconConfig.formModal) {
|
|
1347
1347
|
// Get all rows in the group for bulk editing
|
|
1348
1348
|
const groupRows = getGroupRows(groupValue);
|
|
1349
|
-
const groupRowIds = groupRows.map(
|
|
1350
|
-
|
|
1349
|
+
const groupRowIds = groupRows.map(
|
|
1350
|
+
(row) => row[form?.primaryKey || 'id']
|
|
1351
|
+
);
|
|
1352
|
+
|
|
1351
1353
|
// Set bulk edit data
|
|
1352
1354
|
setBulkEditMode(true);
|
|
1353
1355
|
setBulkEditGroupData({
|
|
1354
1356
|
groupValue: groupValue,
|
|
1355
1357
|
groupRows: groupRows,
|
|
1356
1358
|
groupRowIds: groupRowIds,
|
|
1357
|
-
iconConfig: iconConfig
|
|
1359
|
+
iconConfig: iconConfig,
|
|
1358
1360
|
});
|
|
1359
|
-
|
|
1361
|
+
|
|
1360
1362
|
// Open modal for bulk editing
|
|
1361
1363
|
modalOpen('update', null);
|
|
1362
1364
|
} else {
|
|
@@ -2374,8 +2376,11 @@ const DataGrid = forwardRef(
|
|
|
2374
2376
|
|
|
2375
2377
|
// Check if column is sortable based on metadata
|
|
2376
2378
|
const columnKey = column.id;
|
|
2377
|
-
const sortable = isColumnSortable(
|
|
2378
|
-
|
|
2379
|
+
const sortable = isColumnSortable(
|
|
2380
|
+
columnKey,
|
|
2381
|
+
columnsMetadata
|
|
2382
|
+
);
|
|
2383
|
+
|
|
2379
2384
|
const commonProps = {
|
|
2380
2385
|
header: column.label,
|
|
2381
2386
|
defaultFlex: 1,
|
|
@@ -2481,20 +2486,26 @@ const DataGrid = forwardRef(
|
|
|
2481
2486
|
? column.filter.placeholder
|
|
2482
2487
|
: 'All Options',
|
|
2483
2488
|
dataSource: selectedDataSource,
|
|
2484
|
-
searchable:
|
|
2489
|
+
searchable:
|
|
2490
|
+
column.filter.searchable !== false,
|
|
2485
2491
|
// Enable clear functionality even when searchable is false
|
|
2486
|
-
allowClearWhenNotSearchable:
|
|
2492
|
+
allowClearWhenNotSearchable:
|
|
2493
|
+
column.filter.searchable === false,
|
|
2487
2494
|
// Explicitly enable clear icon
|
|
2488
2495
|
clearIcon: true,
|
|
2489
2496
|
};
|
|
2490
|
-
|
|
2497
|
+
|
|
2491
2498
|
// Debug: Check if fix is applied
|
|
2492
2499
|
if (column.filter.searchable === false) {
|
|
2493
|
-
console.log(
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2500
|
+
console.log(
|
|
2501
|
+
'Filter Debug - searchable=false:',
|
|
2502
|
+
{
|
|
2503
|
+
columnName:
|
|
2504
|
+
column.name || 'unknown',
|
|
2505
|
+
props: filterEditorProps,
|
|
2506
|
+
visnsDataGridVersion: 'v1.0.18',
|
|
2507
|
+
}
|
|
2508
|
+
);
|
|
2498
2509
|
}
|
|
2499
2510
|
break;
|
|
2500
2511
|
default:
|
|
@@ -2638,7 +2649,9 @@ const DataGrid = forwardRef(
|
|
|
2638
2649
|
filterEditor,
|
|
2639
2650
|
filterEditorProps,
|
|
2640
2651
|
relationName,
|
|
2641
|
-
intelligentSortingConfig:
|
|
2652
|
+
intelligentSortingConfig:
|
|
2653
|
+
tableSetting?.intelligentSorting ||
|
|
2654
|
+
DEFAULT_INTELLIGENT_SORTING_CONFIG,
|
|
2642
2655
|
});
|
|
2643
2656
|
case 'relationArray':
|
|
2644
2657
|
return renderRelationArrayColumn({
|
|
@@ -2647,7 +2660,9 @@ const DataGrid = forwardRef(
|
|
|
2647
2660
|
filterEditor,
|
|
2648
2661
|
filterEditorProps,
|
|
2649
2662
|
relationName,
|
|
2650
|
-
intelligentSortingConfig:
|
|
2663
|
+
intelligentSortingConfig:
|
|
2664
|
+
tableSetting?.intelligentSorting ||
|
|
2665
|
+
DEFAULT_INTELLIGENT_SORTING_CONFIG,
|
|
2651
2666
|
});
|
|
2652
2667
|
case 'richtext':
|
|
2653
2668
|
return renderRichTextColumn({
|
|
@@ -2697,22 +2712,37 @@ const DataGrid = forwardRef(
|
|
|
2697
2712
|
: column.id;
|
|
2698
2713
|
|
|
2699
2714
|
// Auto-detect columns that should have word wrap enabled
|
|
2700
|
-
const shouldAutoEnableWordWrap =
|
|
2715
|
+
const shouldAutoEnableWordWrap =
|
|
2701
2716
|
column.wordWrap !== false && // Allow explicit disable with wordWrap: false
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2717
|
+
// Text-heavy column patterns
|
|
2718
|
+
(/^(task|description|notes?|comments?|content|message|details?|summary|reason|remarks?)$/i.test(
|
|
2719
|
+
columnId
|
|
2720
|
+
) ||
|
|
2721
|
+
/.*_(task|description|notes?|comments?|content|message|details?|summary|reason|remarks?)$/i.test(
|
|
2722
|
+
columnId
|
|
2723
|
+
) ||
|
|
2724
|
+
/(task|description|notes?|comments?|content|message|details?|summary|reason|remarks?)_.*$/i.test(
|
|
2725
|
+
columnId
|
|
2726
|
+
) ||
|
|
2707
2727
|
// Check for long labels that might indicate text content
|
|
2708
|
-
(column.label &&
|
|
2709
|
-
|
|
2728
|
+
(column.label &&
|
|
2729
|
+
column.label.length > 15 &&
|
|
2730
|
+
/description|notes|comments|details|content/i.test(
|
|
2731
|
+
column.label
|
|
2732
|
+
)));
|
|
2710
2733
|
|
|
2711
|
-
const finalWordWrap =
|
|
2734
|
+
const finalWordWrap =
|
|
2735
|
+
column.wordWrap === true ||
|
|
2736
|
+
shouldAutoEnableWordWrap;
|
|
2712
2737
|
|
|
2713
2738
|
// Debug logging for word wrap detection
|
|
2714
|
-
if (
|
|
2715
|
-
|
|
2739
|
+
if (
|
|
2740
|
+
shouldAutoEnableWordWrap &&
|
|
2741
|
+
column.wordWrap !== true
|
|
2742
|
+
) {
|
|
2743
|
+
console.log(
|
|
2744
|
+
`DataGrid: Auto-enabled wordWrap for column "${columnId}" (${column.label})`
|
|
2745
|
+
);
|
|
2716
2746
|
}
|
|
2717
2747
|
|
|
2718
2748
|
return {
|
|
@@ -2720,7 +2750,9 @@ const DataGrid = forwardRef(
|
|
|
2720
2750
|
name: columnId,
|
|
2721
2751
|
filterEditor: filterEditor,
|
|
2722
2752
|
filterEditorProps: filterEditorProps,
|
|
2723
|
-
className: finalWordWrap
|
|
2753
|
+
className: finalWordWrap
|
|
2754
|
+
? 'cell-word-wrap'
|
|
2755
|
+
: '',
|
|
2724
2756
|
render: ({ data }) => {
|
|
2725
2757
|
if (
|
|
2726
2758
|
data &&
|
|
@@ -2751,7 +2783,10 @@ const DataGrid = forwardRef(
|
|
|
2751
2783
|
}
|
|
2752
2784
|
|
|
2753
2785
|
// If removeFormat or stripHtml is true, strip HTML tags
|
|
2754
|
-
if (
|
|
2786
|
+
if (
|
|
2787
|
+
column.removeFormat ||
|
|
2788
|
+
column.stripHtml
|
|
2789
|
+
) {
|
|
2755
2790
|
content = content.replace(
|
|
2756
2791
|
/<\/?[^>]+(>|$)/g,
|
|
2757
2792
|
''
|
|
@@ -3093,15 +3128,22 @@ const DataGrid = forwardRef(
|
|
|
3093
3128
|
{/* Top container with search, action buttons, and auto-refresh */}
|
|
3094
3129
|
{/* Only show top container if there are controls to display */}
|
|
3095
3130
|
{(() => {
|
|
3096
|
-
const hasSearchOrCreate =
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3131
|
+
const hasSearchOrCreate =
|
|
3132
|
+
!form.searchDisable || !form.createDisable;
|
|
3133
|
+
const hasAutoRefresh =
|
|
3134
|
+
!shouldRelocateAutoRefresh() &&
|
|
3135
|
+
ajaxSetting &&
|
|
3136
|
+
ajaxSetting.autoRefresh !== undefined;
|
|
3137
|
+
|
|
3138
|
+
// If DataGridSearch would return null (both search and create disabled),
|
|
3100
3139
|
// only show container if there are other controls besides auto-refresh
|
|
3101
|
-
if (
|
|
3140
|
+
if (
|
|
3141
|
+
form.searchDisable === true &&
|
|
3142
|
+
form.createDisable === true
|
|
3143
|
+
) {
|
|
3102
3144
|
return false; // Hide AutoRefreshControls when it would be the only control
|
|
3103
3145
|
}
|
|
3104
|
-
|
|
3146
|
+
|
|
3105
3147
|
return hasSearchOrCreate || hasAutoRefresh;
|
|
3106
3148
|
})() && (
|
|
3107
3149
|
<div className={styles.dataGridTopContainer}>
|
|
@@ -71,6 +71,7 @@ import GenericEditableTable from './GenericEditableTable';
|
|
|
71
71
|
import MergeEntity from '../MergeEntity';
|
|
72
72
|
import MultiSelect from '../MultiSelect';
|
|
73
73
|
import QrCode from '../QrCode';
|
|
74
|
+
import StagePopupModal from './StagePopupModal';
|
|
74
75
|
import Table from '../DataGrid';
|
|
75
76
|
import TableFilter from '../TableFilter';
|
|
76
77
|
|
|
@@ -157,6 +158,10 @@ function GenericDetail({
|
|
|
157
158
|
const [total, setTotal] = useState(0);
|
|
158
159
|
const [windowHeight, setWindowHeight] = useState(window.innerHeight);
|
|
159
160
|
|
|
161
|
+
/** Stage Popup States */
|
|
162
|
+
const [showStagePopup, setShowStagePopup] = useState(false);
|
|
163
|
+
const [stagePopupData, setStagePopupData] = useState(null);
|
|
164
|
+
|
|
160
165
|
/** Ocr States */
|
|
161
166
|
const [ocrRowsSelected, setOcrRowsSelected] = useState('');
|
|
162
167
|
|
|
@@ -297,6 +302,45 @@ function GenericDetail({
|
|
|
297
302
|
};
|
|
298
303
|
|
|
299
304
|
const handleStageUpdate = async (stage, type) => {
|
|
305
|
+
try {
|
|
306
|
+
// Check if stage has popup configuration
|
|
307
|
+
if (stage.popup?.enabled) {
|
|
308
|
+
setStagePopupData({ stage, type });
|
|
309
|
+
setShowStagePopup(true);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// If no popup, proceed with normal stage update
|
|
314
|
+
await performStageUpdate(stage, type);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
console.error(error);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const handleStagePopupConfirm = async (popupFormData) => {
|
|
321
|
+
try {
|
|
322
|
+
setShowStagePopup(false);
|
|
323
|
+
|
|
324
|
+
if (!stagePopupData) return;
|
|
325
|
+
|
|
326
|
+
const { stage, type } = stagePopupData;
|
|
327
|
+
|
|
328
|
+
// Proceed with normal stage update, but include popup form data
|
|
329
|
+
await performStageUpdate(stage, type, popupFormData);
|
|
330
|
+
|
|
331
|
+
setStagePopupData(null);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
console.error('Stage popup confirm error:', error);
|
|
334
|
+
toast.error('Failed to update stage');
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const handleStagePopupCancel = () => {
|
|
339
|
+
setShowStagePopup(false);
|
|
340
|
+
setStagePopupData(null);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const performStageUpdate = async (stage, type, additionalData = {}) => {
|
|
300
344
|
try {
|
|
301
345
|
const activeTab = subnav.find((s) => s.show === true);
|
|
302
346
|
|
|
@@ -307,7 +351,7 @@ function GenericDetail({
|
|
|
307
351
|
setActiveStage(stage.id);
|
|
308
352
|
} else if (type === 'stageCounter') {
|
|
309
353
|
// Handle new stageCounter functionality
|
|
310
|
-
console.log('
|
|
354
|
+
console.log('performStageUpdate called with stageCounter type:', { stage, data, stages });
|
|
311
355
|
|
|
312
356
|
const stageConfig = stages.stageConfig || activeTabConfig.stages?.stageConfig;
|
|
313
357
|
if (!stageConfig?.toggleable || !stageConfig?.toggleConfig) {
|
|
@@ -319,13 +363,6 @@ function GenericDetail({
|
|
|
319
363
|
const stageDataArray = data[stageConfig.key] || [];
|
|
320
364
|
const currentStage = stageDataArray.find(s => s.id === stage.id);
|
|
321
365
|
|
|
322
|
-
console.log('Stage data lookup:', {
|
|
323
|
-
stageDataKey: stageConfig.key,
|
|
324
|
-
stageDataArray,
|
|
325
|
-
searchingForId: stage.id,
|
|
326
|
-
currentStage
|
|
327
|
-
});
|
|
328
|
-
|
|
329
366
|
if (!currentStage) {
|
|
330
367
|
console.warn('Stage not found in data');
|
|
331
368
|
return;
|
|
@@ -337,26 +374,19 @@ function GenericDetail({
|
|
|
337
374
|
const targetStatus = isActive
|
|
338
375
|
? stageConfig.toggleConfig.inactiveStatusValue
|
|
339
376
|
: stageConfig.toggleConfig.activeStatusValue;
|
|
340
|
-
|
|
341
|
-
console.log('Status toggle logic:', {
|
|
342
|
-
currentStatus,
|
|
343
|
-
activeStatusValue: stageConfig.toggleConfig.activeStatusValue,
|
|
344
|
-
inactiveStatusValue: stageConfig.toggleConfig.inactiveStatusValue,
|
|
345
|
-
isActive,
|
|
346
|
-
targetStatus
|
|
347
|
-
});
|
|
348
377
|
|
|
349
378
|
// Build API URL
|
|
350
379
|
let url = stageConfig.toggleConfig.updateUrl;
|
|
351
380
|
url = url.replace('{id}', routeParams[stages.urlParam]);
|
|
352
381
|
url = url.replace('{stage_id}', stage.id);
|
|
353
382
|
|
|
354
|
-
// Make API call
|
|
383
|
+
// Make API call with additional data from popup
|
|
355
384
|
const res = await CustomFetch(
|
|
356
385
|
url,
|
|
357
386
|
stageConfig.toggleConfig.updateMethod || 'PUT',
|
|
358
387
|
{
|
|
359
388
|
[stageConfig.toggleConfig.updateKey]: targetStatus,
|
|
389
|
+
...additionalData
|
|
360
390
|
}
|
|
361
391
|
);
|
|
362
392
|
|
|
@@ -369,7 +399,7 @@ function GenericDetail({
|
|
|
369
399
|
}
|
|
370
400
|
} else if (type === 'legacy_toggle') {
|
|
371
401
|
// Handle legacy toggle functionality - toggle between current stage and previous
|
|
372
|
-
console.log('
|
|
402
|
+
console.log('performStageUpdate called with legacy_toggle type:', { stage, data, stages });
|
|
373
403
|
|
|
374
404
|
if (
|
|
375
405
|
stages &&
|
|
@@ -392,21 +422,16 @@ function GenericDetail({
|
|
|
392
422
|
// Stage is not completed, move to this stage
|
|
393
423
|
targetStatus = stage.id;
|
|
394
424
|
}
|
|
395
|
-
|
|
396
|
-
console.log('Legacy toggle logic:', {
|
|
397
|
-
currentStatus,
|
|
398
|
-
clickedStageId: stage.id,
|
|
399
|
-
targetStatus,
|
|
400
|
-
isToggleBack: currentStatus >= stage.id
|
|
401
|
-
});
|
|
402
425
|
|
|
403
426
|
const url = `${stages.url}/${routeParams[stages.urlParam]}`;
|
|
404
427
|
|
|
428
|
+
// Include additional data from popup
|
|
405
429
|
const res = await CustomFetch(
|
|
406
430
|
url,
|
|
407
431
|
stages.method || 'PUT',
|
|
408
432
|
{
|
|
409
433
|
[stages.key]: targetStatus,
|
|
434
|
+
...additionalData
|
|
410
435
|
}
|
|
411
436
|
);
|
|
412
437
|
|
|
@@ -433,6 +458,7 @@ function GenericDetail({
|
|
|
433
458
|
|
|
434
459
|
const res = await CustomFetch(url, 'POST', {
|
|
435
460
|
[activeTabConfig.stages.key]: stage.id,
|
|
461
|
+
...additionalData
|
|
436
462
|
});
|
|
437
463
|
|
|
438
464
|
if (res.data.error === '') {
|
|
@@ -457,6 +483,7 @@ function GenericDetail({
|
|
|
457
483
|
stages.method || 'POST',
|
|
458
484
|
{
|
|
459
485
|
[stages.key]: stage.id,
|
|
486
|
+
...additionalData
|
|
460
487
|
}
|
|
461
488
|
);
|
|
462
489
|
|
|
@@ -3130,6 +3157,16 @@ function GenericDetail({
|
|
|
3130
3157
|
</div>
|
|
3131
3158
|
</div>
|
|
3132
3159
|
</Popup>
|
|
3160
|
+
|
|
3161
|
+
<StagePopupModal
|
|
3162
|
+
show={showStagePopup}
|
|
3163
|
+
stage={stagePopupData?.stage}
|
|
3164
|
+
popupConfig={stagePopupData?.stage?.popup}
|
|
3165
|
+
onConfirm={handleStagePopupConfirm}
|
|
3166
|
+
onCancel={handleStagePopupCancel}
|
|
3167
|
+
entityData={data}
|
|
3168
|
+
routeParams={routeParams}
|
|
3169
|
+
/>
|
|
3133
3170
|
</>
|
|
3134
3171
|
);
|
|
3135
3172
|
}
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import Swal from 'sweetalert2';
|
|
3
|
+
import CustomFetch from '../Fetch';
|
|
4
|
+
import styles from '../styles/StagePopupModal.module.scss';
|
|
5
|
+
|
|
6
|
+
const StagePopupModal = ({
|
|
7
|
+
show,
|
|
8
|
+
stage,
|
|
9
|
+
popupConfig,
|
|
10
|
+
onConfirm,
|
|
11
|
+
onCancel,
|
|
12
|
+
entityData = {},
|
|
13
|
+
routeParams = {}
|
|
14
|
+
}) => {
|
|
15
|
+
const [formData, setFormData] = useState({});
|
|
16
|
+
const [fieldOptions, setFieldOptions] = useState({});
|
|
17
|
+
const [loading, setLoading] = useState(false);
|
|
18
|
+
const [validationErrors, setValidationErrors] = useState({});
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (show && popupConfig?.fields) {
|
|
22
|
+
initializeForm();
|
|
23
|
+
}
|
|
24
|
+
}, [show, popupConfig]);
|
|
25
|
+
|
|
26
|
+
// Re-render form fields when fieldOptions change
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
console.log('[StagePopupModal] useEffect triggered:', {
|
|
29
|
+
show,
|
|
30
|
+
loading,
|
|
31
|
+
fieldOptionsKeys: Object.keys(fieldOptions),
|
|
32
|
+
fieldOptions
|
|
33
|
+
});
|
|
34
|
+
if (show && !loading && Object.keys(fieldOptions).length > 0) {
|
|
35
|
+
console.log('[StagePopupModal] Re-rendering form fields after fieldOptions update');
|
|
36
|
+
renderFormFields();
|
|
37
|
+
}
|
|
38
|
+
}, [fieldOptions, loading, formData, validationErrors, show]);
|
|
39
|
+
|
|
40
|
+
const initializeForm = async () => {
|
|
41
|
+
const initialData = {};
|
|
42
|
+
const options = {};
|
|
43
|
+
|
|
44
|
+
setLoading(true);
|
|
45
|
+
|
|
46
|
+
for (const field of popupConfig.fields) {
|
|
47
|
+
// Set default values
|
|
48
|
+
if (field.value !== undefined) {
|
|
49
|
+
initialData[field.id] = field.value;
|
|
50
|
+
} else if (field.urlParam && routeParams[field.urlParam]) {
|
|
51
|
+
initialData[field.id] = routeParams[field.urlParam];
|
|
52
|
+
} else if (entityData[field.id] !== undefined) {
|
|
53
|
+
initialData[field.id] = entityData[field.id];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Load dropdown options
|
|
57
|
+
if (field.type === 'select' && field.url) {
|
|
58
|
+
try {
|
|
59
|
+
let url = field.url;
|
|
60
|
+
|
|
61
|
+
// Replace URL parameters
|
|
62
|
+
Object.keys(routeParams).forEach(key => {
|
|
63
|
+
url = url.replace(`{${key}}`, routeParams[key]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const response = await CustomFetch(url, 'POST');
|
|
67
|
+
console.log(`[StagePopupModal] Response for ${field.id}:`, response);
|
|
68
|
+
console.log(`[StagePopupModal] Response.data:`, response.data);
|
|
69
|
+
|
|
70
|
+
if (response.data && !response.data.error) {
|
|
71
|
+
// Handle both nested (response.data.data) and direct (response.data) array formats
|
|
72
|
+
const optionsData = Array.isArray(response.data.data) ? response.data.data :
|
|
73
|
+
Array.isArray(response.data) ? response.data : [];
|
|
74
|
+
console.log(`[StagePopupModal] Processed options for ${field.id}:`, optionsData);
|
|
75
|
+
options[field.id] = optionsData;
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error(`Failed to load options for ${field.id}:`, error);
|
|
79
|
+
options[field.id] = [];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(`[StagePopupModal] Final options object:`, options);
|
|
85
|
+
console.log(`[StagePopupModal] Setting fieldOptions state:`, options);
|
|
86
|
+
|
|
87
|
+
setFormData(initialData);
|
|
88
|
+
setFieldOptions(options);
|
|
89
|
+
setLoading(false);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const handleFieldChange = (fieldId, value) => {
|
|
93
|
+
setFormData(prev => ({
|
|
94
|
+
...prev,
|
|
95
|
+
[fieldId]: value
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
// Clear validation error for this field
|
|
99
|
+
if (validationErrors[fieldId]) {
|
|
100
|
+
setValidationErrors(prev => ({
|
|
101
|
+
...prev,
|
|
102
|
+
[fieldId]: null
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const validateForm = () => {
|
|
108
|
+
const errors = {};
|
|
109
|
+
|
|
110
|
+
popupConfig.fields?.forEach(field => {
|
|
111
|
+
if (field.required && (!formData[field.id] || formData[field.id] === '')) {
|
|
112
|
+
errors[field.id] = `${field.label} is required`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (field.validation) {
|
|
116
|
+
const value = formData[field.id];
|
|
117
|
+
|
|
118
|
+
if (field.validation.minLength && value && value.length < field.validation.minLength) {
|
|
119
|
+
errors[field.id] = `${field.label} must be at least ${field.validation.minLength} characters`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (field.validation.maxLength && value && value.length > field.validation.maxLength) {
|
|
123
|
+
errors[field.id] = `${field.label} must not exceed ${field.validation.maxLength} characters`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
setValidationErrors(errors);
|
|
129
|
+
return Object.keys(errors).length === 0;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const handleConfirm = () => {
|
|
133
|
+
if (!validateForm()) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const dataToSend = {};
|
|
138
|
+
|
|
139
|
+
// Only include fields specified in saveFields, or all fields if not specified
|
|
140
|
+
const fieldsToSave = popupConfig.saveFields || popupConfig.fields?.map(f => f.id) || [];
|
|
141
|
+
|
|
142
|
+
fieldsToSave.forEach(fieldId => {
|
|
143
|
+
if (formData[fieldId] !== undefined) {
|
|
144
|
+
dataToSend[fieldId] = formData[fieldId];
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
onConfirm(dataToSend);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const renderField = (field) => {
|
|
152
|
+
const value = formData[field.id] || '';
|
|
153
|
+
const hasError = validationErrors[field.id];
|
|
154
|
+
const fieldClass = `${styles.field} ${hasError ? styles.fieldError : ''}`;
|
|
155
|
+
|
|
156
|
+
switch (field.type) {
|
|
157
|
+
case 'select':
|
|
158
|
+
return (
|
|
159
|
+
<div key={field.id} className={fieldClass}>
|
|
160
|
+
<label className={styles.label}>
|
|
161
|
+
{field.label}
|
|
162
|
+
{field.required && <span className={styles.required}>*</span>}
|
|
163
|
+
</label>
|
|
164
|
+
<select
|
|
165
|
+
className={styles.select}
|
|
166
|
+
value={value}
|
|
167
|
+
onChange={(e) => handleFieldChange(field.id, e.target.value)}
|
|
168
|
+
disabled={loading}
|
|
169
|
+
>
|
|
170
|
+
<option value="">-- Select {field.label} --</option>
|
|
171
|
+
{(() => {
|
|
172
|
+
console.log(`[StagePopupModal] Rendering JSX options for ${field.id}:`, fieldOptions[field.id]);
|
|
173
|
+
return (fieldOptions[field.id] || []).map(option => (
|
|
174
|
+
<option key={option.id || option.value} value={option.id || option.value}>
|
|
175
|
+
{option.label || option.name || option.text}
|
|
176
|
+
</option>
|
|
177
|
+
));
|
|
178
|
+
})()}
|
|
179
|
+
</select>
|
|
180
|
+
{hasError && <span className={styles.errorText}>{validationErrors[field.id]}</span>}
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
case 'textarea':
|
|
185
|
+
return (
|
|
186
|
+
<div key={field.id} className={fieldClass}>
|
|
187
|
+
<label className={styles.label}>
|
|
188
|
+
{field.label}
|
|
189
|
+
{field.required && <span className={styles.required}>*</span>}
|
|
190
|
+
</label>
|
|
191
|
+
<textarea
|
|
192
|
+
className={styles.textarea}
|
|
193
|
+
value={value}
|
|
194
|
+
onChange={(e) => handleFieldChange(field.id, e.target.value)}
|
|
195
|
+
placeholder={field.placeholder}
|
|
196
|
+
rows={field.rows || 3}
|
|
197
|
+
disabled={loading}
|
|
198
|
+
/>
|
|
199
|
+
{hasError && <span className={styles.errorText}>{validationErrors[field.id]}</span>}
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
case 'date':
|
|
204
|
+
return (
|
|
205
|
+
<div key={field.id} className={fieldClass}>
|
|
206
|
+
<label className={styles.label}>
|
|
207
|
+
{field.label}
|
|
208
|
+
{field.required && <span className={styles.required}>*</span>}
|
|
209
|
+
</label>
|
|
210
|
+
<input
|
|
211
|
+
type="date"
|
|
212
|
+
className={styles.input}
|
|
213
|
+
value={value}
|
|
214
|
+
onChange={(e) => handleFieldChange(field.id, e.target.value)}
|
|
215
|
+
disabled={loading}
|
|
216
|
+
/>
|
|
217
|
+
{hasError && <span className={styles.errorText}>{validationErrors[field.id]}</span>}
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
case 'datetime':
|
|
222
|
+
return (
|
|
223
|
+
<div key={field.id} className={fieldClass}>
|
|
224
|
+
<label className={styles.label}>
|
|
225
|
+
{field.label}
|
|
226
|
+
{field.required && <span className={styles.required}>*</span>}
|
|
227
|
+
</label>
|
|
228
|
+
<input
|
|
229
|
+
type="datetime-local"
|
|
230
|
+
className={styles.input}
|
|
231
|
+
value={value}
|
|
232
|
+
onChange={(e) => handleFieldChange(field.id, e.target.value)}
|
|
233
|
+
disabled={loading}
|
|
234
|
+
/>
|
|
235
|
+
{hasError && <span className={styles.errorText}>{validationErrors[field.id]}</span>}
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
case 'number':
|
|
240
|
+
return (
|
|
241
|
+
<div key={field.id} className={fieldClass}>
|
|
242
|
+
<label className={styles.label}>
|
|
243
|
+
{field.label}
|
|
244
|
+
{field.required && <span className={styles.required}>*</span>}
|
|
245
|
+
</label>
|
|
246
|
+
<input
|
|
247
|
+
type="number"
|
|
248
|
+
className={styles.input}
|
|
249
|
+
value={value}
|
|
250
|
+
onChange={(e) => handleFieldChange(field.id, e.target.value)}
|
|
251
|
+
placeholder={field.placeholder}
|
|
252
|
+
min={field.min}
|
|
253
|
+
max={field.max}
|
|
254
|
+
step={field.step}
|
|
255
|
+
disabled={loading}
|
|
256
|
+
/>
|
|
257
|
+
{hasError && <span className={styles.errorText}>{validationErrors[field.id]}</span>}
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
case 'text':
|
|
262
|
+
default:
|
|
263
|
+
return (
|
|
264
|
+
<div key={field.id} className={fieldClass}>
|
|
265
|
+
<label className={styles.label}>
|
|
266
|
+
{field.label}
|
|
267
|
+
{field.required && <span className={styles.required}>*</span>}
|
|
268
|
+
</label>
|
|
269
|
+
<input
|
|
270
|
+
type="text"
|
|
271
|
+
className={styles.input}
|
|
272
|
+
value={value}
|
|
273
|
+
onChange={(e) => handleFieldChange(field.id, e.target.value)}
|
|
274
|
+
placeholder={field.placeholder}
|
|
275
|
+
disabled={loading}
|
|
276
|
+
/>
|
|
277
|
+
{hasError && <span className={styles.errorText}>{validationErrors[field.id]}</span>}
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
if (show && popupConfig) {
|
|
285
|
+
if (popupConfig.type === 'confirm') {
|
|
286
|
+
// Simple confirmation dialog
|
|
287
|
+
Swal.fire({
|
|
288
|
+
title: popupConfig.title || 'Confirm Action',
|
|
289
|
+
text: popupConfig.message || 'Are you sure you want to proceed?',
|
|
290
|
+
icon: 'question',
|
|
291
|
+
showCancelButton: true,
|
|
292
|
+
confirmButtonText: popupConfig.confirmText || 'Yes, proceed',
|
|
293
|
+
cancelButtonText: popupConfig.cancelText || 'Cancel',
|
|
294
|
+
customClass: {
|
|
295
|
+
popup: styles.swalPopup,
|
|
296
|
+
confirmButton: styles.swalConfirm,
|
|
297
|
+
cancelButton: styles.swalCancel
|
|
298
|
+
}
|
|
299
|
+
}).then((result) => {
|
|
300
|
+
if (result.isConfirmed) {
|
|
301
|
+
onConfirm({});
|
|
302
|
+
} else {
|
|
303
|
+
onCancel();
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
} else if (popupConfig.type === 'form') {
|
|
307
|
+
// Custom form dialog
|
|
308
|
+
const formHTML = `
|
|
309
|
+
<div class="${styles.stagePopupForm}">
|
|
310
|
+
${popupConfig.message ? `<p class="${styles.message}">${popupConfig.message}</p>` : ''}
|
|
311
|
+
<div id="stage-popup-fields" class="${styles.fields}">
|
|
312
|
+
${loading ? '<div class="' + styles.loading + '">Loading...</div>' : ''}
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
`;
|
|
316
|
+
|
|
317
|
+
Swal.fire({
|
|
318
|
+
title: popupConfig.title || 'Additional Information Required',
|
|
319
|
+
html: formHTML,
|
|
320
|
+
showCancelButton: true,
|
|
321
|
+
confirmButtonText: popupConfig.confirmText || 'Complete Stage',
|
|
322
|
+
cancelButtonText: popupConfig.cancelText || 'Cancel',
|
|
323
|
+
customClass: {
|
|
324
|
+
popup: styles.swalPopup,
|
|
325
|
+
confirmButton: styles.swalConfirm,
|
|
326
|
+
cancelButton: styles.swalCancel,
|
|
327
|
+
htmlContainer: styles.swalHtmlContainer
|
|
328
|
+
},
|
|
329
|
+
didOpen: () => {
|
|
330
|
+
renderFormFields();
|
|
331
|
+
},
|
|
332
|
+
preConfirm: () => {
|
|
333
|
+
if (!validateForm()) {
|
|
334
|
+
Swal.showValidationMessage('Please correct the errors below');
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
}).then((result) => {
|
|
340
|
+
if (result.isConfirmed) {
|
|
341
|
+
handleConfirm();
|
|
342
|
+
} else {
|
|
343
|
+
onCancel();
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}, [show, popupConfig, loading, formData, fieldOptions]);
|
|
349
|
+
|
|
350
|
+
const renderFormFields = () => {
|
|
351
|
+
console.log('[StagePopupModal] renderFormFields called');
|
|
352
|
+
const fieldsContainer = document.getElementById('stage-popup-fields');
|
|
353
|
+
console.log('[StagePopupModal] fieldsContainer:', fieldsContainer);
|
|
354
|
+
console.log('[StagePopupModal] loading:', loading);
|
|
355
|
+
|
|
356
|
+
if (!fieldsContainer || loading) {
|
|
357
|
+
console.log('[StagePopupModal] Early return - no container or loading');
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
console.log('[StagePopupModal] Clearing container and rendering fields');
|
|
362
|
+
fieldsContainer.innerHTML = '';
|
|
363
|
+
|
|
364
|
+
console.log('[StagePopupModal] popupConfig.fields:', popupConfig.fields);
|
|
365
|
+
popupConfig.fields?.forEach(field => {
|
|
366
|
+
console.log(`[StagePopupModal] Processing field: ${field.id}, type: ${field.type}`);
|
|
367
|
+
const fieldElement = document.createElement('div');
|
|
368
|
+
fieldElement.className = styles.fieldWrapper;
|
|
369
|
+
|
|
370
|
+
const fieldHTML = renderFieldHTML(field);
|
|
371
|
+
console.log(`[StagePopupModal] Generated HTML for ${field.id}:`, fieldHTML);
|
|
372
|
+
fieldElement.innerHTML = fieldHTML;
|
|
373
|
+
|
|
374
|
+
fieldsContainer.appendChild(fieldElement);
|
|
375
|
+
|
|
376
|
+
// Add event listeners
|
|
377
|
+
const input = fieldElement.querySelector('input, select, textarea');
|
|
378
|
+
if (input) {
|
|
379
|
+
input.addEventListener('change', (e) => {
|
|
380
|
+
handleFieldChange(field.id, e.target.value);
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const renderFieldHTML = (field) => {
|
|
387
|
+
const value = formData[field.id] || '';
|
|
388
|
+
const hasError = validationErrors[field.id];
|
|
389
|
+
const requiredStar = field.required ? '<span class="' + styles.required + '">*</span>' : '';
|
|
390
|
+
|
|
391
|
+
switch (field.type) {
|
|
392
|
+
case 'select':
|
|
393
|
+
console.log(`[StagePopupModal] Rendering HTML options for ${field.id}:`, fieldOptions[field.id]);
|
|
394
|
+
const options = (fieldOptions[field.id] || []).map(option =>
|
|
395
|
+
`<option value="${option.id || option.value}" ${value === (option.id || option.value) ? 'selected' : ''}>
|
|
396
|
+
${option.label || option.name || option.text}
|
|
397
|
+
</option>`
|
|
398
|
+
).join('');
|
|
399
|
+
|
|
400
|
+
return `
|
|
401
|
+
<label class="${styles.label}">${field.label}${requiredStar}</label>
|
|
402
|
+
<select class="${styles.select}" data-field-id="${field.id}">
|
|
403
|
+
<option value="">-- Select ${field.label} --</option>
|
|
404
|
+
${options}
|
|
405
|
+
</select>
|
|
406
|
+
${hasError ? `<span class="${styles.errorText}">${validationErrors[field.id]}</span>` : ''}
|
|
407
|
+
`;
|
|
408
|
+
|
|
409
|
+
case 'textarea':
|
|
410
|
+
return `
|
|
411
|
+
<label class="${styles.label}">${field.label}${requiredStar}</label>
|
|
412
|
+
<textarea class="${styles.textarea}" data-field-id="${field.id}" placeholder="${field.placeholder || ''}" rows="${field.rows || 3}">${value}</textarea>
|
|
413
|
+
${hasError ? `<span class="${styles.errorText}">${validationErrors[field.id]}</span>` : ''}
|
|
414
|
+
`;
|
|
415
|
+
|
|
416
|
+
case 'date':
|
|
417
|
+
return `
|
|
418
|
+
<label class="${styles.label}">${field.label}${requiredStar}</label>
|
|
419
|
+
<input type="date" class="${styles.input}" data-field-id="${field.id}" value="${value}">
|
|
420
|
+
${hasError ? `<span class="${styles.errorText}">${validationErrors[field.id]}</span>` : ''}
|
|
421
|
+
`;
|
|
422
|
+
|
|
423
|
+
case 'datetime':
|
|
424
|
+
return `
|
|
425
|
+
<label class="${styles.label}">${field.label}${requiredStar}</label>
|
|
426
|
+
<input type="datetime-local" class="${styles.input}" data-field-id="${field.id}" value="${value}">
|
|
427
|
+
${hasError ? `<span class="${styles.errorText}">${validationErrors[field.id]}</span>` : ''}
|
|
428
|
+
`;
|
|
429
|
+
|
|
430
|
+
case 'number':
|
|
431
|
+
return `
|
|
432
|
+
<label class="${styles.label}">${field.label}${requiredStar}</label>
|
|
433
|
+
<input type="number" class="${styles.input}" data-field-id="${field.id}" value="${value}" placeholder="${field.placeholder || ''}" min="${field.min || ''}" max="${field.max || ''}" step="${field.step || ''}">
|
|
434
|
+
${hasError ? `<span class="${styles.errorText}">${validationErrors[field.id]}</span>` : ''}
|
|
435
|
+
`;
|
|
436
|
+
|
|
437
|
+
case 'text':
|
|
438
|
+
default:
|
|
439
|
+
return `
|
|
440
|
+
<label class="${styles.label}">${field.label}${requiredStar}</label>
|
|
441
|
+
<input type="text" class="${styles.input}" data-field-id="${field.id}" value="${value}" placeholder="${field.placeholder || ''}">
|
|
442
|
+
${hasError ? `<span class="${styles.errorText}">${validationErrors[field.id]}</span>` : ''}
|
|
443
|
+
`;
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// This component doesn't render directly - it uses SweetAlert2 for the modal
|
|
448
|
+
return null;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
export default StagePopupModal;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
.stagePopupForm {
|
|
2
|
+
text-align: left;
|
|
3
|
+
min-width: 400px;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.message {
|
|
7
|
+
margin-bottom: 20px;
|
|
8
|
+
color: #666;
|
|
9
|
+
font-size: 14px;
|
|
10
|
+
line-height: 1.4;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.fields {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
gap: 16px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.fieldWrapper {
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
gap: 6px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.field {
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
gap: 6px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.fieldError {
|
|
32
|
+
.input,
|
|
33
|
+
.select,
|
|
34
|
+
.textarea {
|
|
35
|
+
border-color: #e74c3c;
|
|
36
|
+
box-shadow: 0 0 0 2px rgba(231, 76, 60, 0.1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.label {
|
|
41
|
+
font-weight: 500;
|
|
42
|
+
color: #333;
|
|
43
|
+
font-size: 14px;
|
|
44
|
+
margin-bottom: 4px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.required {
|
|
48
|
+
color: #e74c3c;
|
|
49
|
+
margin-left: 2px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.input,
|
|
53
|
+
.select,
|
|
54
|
+
.textarea {
|
|
55
|
+
padding: 8px 12px;
|
|
56
|
+
border: 1px solid #ddd;
|
|
57
|
+
border-radius: 4px;
|
|
58
|
+
font-size: 14px;
|
|
59
|
+
font-family: inherit;
|
|
60
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
61
|
+
|
|
62
|
+
&:focus {
|
|
63
|
+
outline: none;
|
|
64
|
+
border-color: #3498db;
|
|
65
|
+
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
&:disabled {
|
|
69
|
+
background-color: #f8f9fa;
|
|
70
|
+
color: #6c757d;
|
|
71
|
+
cursor: not-allowed;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.select {
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
|
|
78
|
+
&:disabled {
|
|
79
|
+
cursor: not-allowed;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.textarea {
|
|
84
|
+
resize: vertical;
|
|
85
|
+
min-height: 80px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.errorText {
|
|
89
|
+
color: #e74c3c;
|
|
90
|
+
font-size: 12px;
|
|
91
|
+
margin-top: 4px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.loading {
|
|
95
|
+
text-align: center;
|
|
96
|
+
padding: 20px;
|
|
97
|
+
color: #666;
|
|
98
|
+
font-style: italic;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// SweetAlert2 customizations
|
|
102
|
+
.swalPopup {
|
|
103
|
+
border-radius: 8px;
|
|
104
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.swalHtmlContainer {
|
|
108
|
+
padding: 0 !important;
|
|
109
|
+
margin: 16px 0 !important;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.swalConfirm {
|
|
113
|
+
background-color: #3498db !important;
|
|
114
|
+
border: none !important;
|
|
115
|
+
border-radius: 4px !important;
|
|
116
|
+
font-weight: 500 !important;
|
|
117
|
+
padding: 10px 20px !important;
|
|
118
|
+
|
|
119
|
+
&:hover {
|
|
120
|
+
background-color: #2980b9 !important;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
&:focus {
|
|
124
|
+
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.3) !important;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.swalCancel {
|
|
129
|
+
background-color: #95a5a6 !important;
|
|
130
|
+
border: none !important;
|
|
131
|
+
border-radius: 4px !important;
|
|
132
|
+
font-weight: 500 !important;
|
|
133
|
+
padding: 10px 20px !important;
|
|
134
|
+
|
|
135
|
+
&:hover {
|
|
136
|
+
background-color: #7f8c8d !important;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
&:focus {
|
|
140
|
+
box-shadow: 0 0 0 2px rgba(149, 165, 166, 0.3) !important;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Responsive adjustments
|
|
145
|
+
@media (max-width: 768px) {
|
|
146
|
+
.stagePopupForm {
|
|
147
|
+
min-width: 300px;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.fields {
|
|
151
|
+
gap: 12px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.input,
|
|
155
|
+
.select,
|
|
156
|
+
.textarea {
|
|
157
|
+
font-size: 16px; // Prevents zoom on iOS
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Theme variations
|
|
162
|
+
.stagePopupForm {
|
|
163
|
+
&.theme-dark {
|
|
164
|
+
.label {
|
|
165
|
+
color: #f8f9fa;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.message {
|
|
169
|
+
color: #adb5bd;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.input,
|
|
173
|
+
.select,
|
|
174
|
+
.textarea {
|
|
175
|
+
background-color: #343a40;
|
|
176
|
+
border-color: #495057;
|
|
177
|
+
color: #f8f9fa;
|
|
178
|
+
|
|
179
|
+
&:focus {
|
|
180
|
+
border-color: #3498db;
|
|
181
|
+
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
&:disabled {
|
|
185
|
+
background-color: #495057;
|
|
186
|
+
color: #6c757d;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.select option {
|
|
191
|
+
background-color: #343a40;
|
|
192
|
+
color: #f8f9fa;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Animation for field validation
|
|
198
|
+
.field {
|
|
199
|
+
transition: all 0.2s ease;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.fieldError {
|
|
203
|
+
animation: shake 0.3s ease-in-out;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@keyframes shake {
|
|
207
|
+
0%, 100% { transform: translateX(0); }
|
|
208
|
+
25% { transform: translateX(-2px); }
|
|
209
|
+
75% { transform: translateX(2px); }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Loading state
|
|
213
|
+
.loading {
|
|
214
|
+
position: relative;
|
|
215
|
+
|
|
216
|
+
&::after {
|
|
217
|
+
content: '';
|
|
218
|
+
position: absolute;
|
|
219
|
+
top: 50%;
|
|
220
|
+
right: 20px;
|
|
221
|
+
transform: translateY(-50%);
|
|
222
|
+
width: 16px;
|
|
223
|
+
height: 16px;
|
|
224
|
+
border: 2px solid #ddd;
|
|
225
|
+
border-top: 2px solid #3498db;
|
|
226
|
+
border-radius: 50%;
|
|
227
|
+
animation: spin 1s linear infinite;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@keyframes spin {
|
|
232
|
+
0% { transform: translateY(-50%) rotate(0deg); }
|
|
233
|
+
100% { transform: translateY(-50%) rotate(360deg); }
|
|
234
|
+
}
|
package/src/index.js
CHANGED
|
@@ -63,6 +63,7 @@ import GenericQuote from './components/generic/GenericQuote';
|
|
|
63
63
|
import GenericReport from './components/generic/GenericReport';
|
|
64
64
|
import GenericSort from './components/generic/GenericSort';
|
|
65
65
|
import NotificationList from './components/generic/NotificationList';
|
|
66
|
+
import StagePopupModal from './components/generic/StagePopupModal';
|
|
66
67
|
|
|
67
68
|
/** Proposal Components */
|
|
68
69
|
import ProposalTemplateSectionManager from './components/proposal/ProposalTemplateSectionManager';
|
|
@@ -129,6 +130,7 @@ export {
|
|
|
129
130
|
SelectList,
|
|
130
131
|
Select,
|
|
131
132
|
SortableList,
|
|
133
|
+
StagePopupModal,
|
|
132
134
|
Table,
|
|
133
135
|
TableFilter,
|
|
134
136
|
VariableInserter,
|