@openg2p/registry-widgets 1.1.0-dev.1 → 1.1.0-dev.11
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/components/SectionRenderer.d.ts +8 -1
- package/dist/components/SectionRenderer.d.ts.map +1 -1
- package/dist/components/SectionsContainer.d.ts.map +1 -1
- package/dist/hooks/useBaseWidget.d.ts.map +1 -1
- package/dist/hooks/useGeoWidgetCascade.d.ts.map +1 -1
- package/dist/index.d.ts +44 -4
- package/dist/index.esm.js +1618 -272
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1619 -271
- package/dist/index.js.map +1 -1
- package/dist/registry/defaultWidgets.d.ts.map +1 -1
- package/dist/utils/buildSectionChanges.d.ts.map +1 -1
- package/dist/utils/dataSource.d.ts.map +1 -1
- package/dist/utils/extractTableRecordsFromSnapshot.d.ts +11 -0
- package/dist/utils/extractTableRecordsFromSnapshot.d.ts.map +1 -0
- package/dist/utils/sectionValidate.d.ts +9 -1
- package/dist/utils/sectionValidate.d.ts.map +1 -1
- package/dist/utils/validation.d.ts +6 -2
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/widgets/DialogTableWidget.d.ts +25 -0
- package/dist/widgets/DialogTableWidget.d.ts.map +1 -0
- package/dist/widgets/DisplayWidget.d.ts.map +1 -1
- package/dist/widgets/HeaderSectionWidget.d.ts.map +1 -1
- package/dist/widgets/IdAuthenticationWidget.d.ts +8 -0
- package/dist/widgets/IdAuthenticationWidget.d.ts.map +1 -0
- package/dist/widgets/index.d.ts +2 -0
- package/dist/widgets/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -245,24 +245,27 @@ const getValidationPattern = (validationType) => {
|
|
|
245
245
|
};
|
|
246
246
|
|
|
247
247
|
/**
|
|
248
|
-
* Validate value against validation rules
|
|
248
|
+
* Validate value against validation rules.
|
|
249
|
+
*
|
|
250
|
+
* @param skipRequired - When true, required-field checks are skipped (used by
|
|
251
|
+
* per-section Save/Next buttons so the user can move between sections without
|
|
252
|
+
* filling every mandatory field; only format/range checks still run).
|
|
249
253
|
*/
|
|
250
|
-
const validateWidget = (value, validation, required = false) => {
|
|
254
|
+
const validateWidget = (value, validation, required = false, skipRequired = false) => {
|
|
251
255
|
const errors = [];
|
|
252
256
|
if (!validation && !required) {
|
|
253
257
|
return errors;
|
|
254
258
|
}
|
|
255
|
-
// Check required
|
|
256
|
-
const isRequired = validation?.required ?? required;
|
|
259
|
+
// Check required (skipped when navigating between sections)
|
|
260
|
+
const isRequired = !skipRequired && (validation?.required ?? required);
|
|
257
261
|
// For boolean, false is a valid value, so only check for null/undefined/empty string
|
|
258
262
|
const isEmpty = value === null || value === undefined || value === '';
|
|
259
263
|
if (isRequired && isEmpty) {
|
|
260
264
|
errors.push('This field is required');
|
|
261
265
|
return errors; // Return early if required field is empty
|
|
262
266
|
}
|
|
263
|
-
// Skip
|
|
264
|
-
|
|
265
|
-
if (isEmpty && !isRequired) {
|
|
267
|
+
// Skip format/range validations if value is empty
|
|
268
|
+
if (isEmpty) {
|
|
266
269
|
return errors;
|
|
267
270
|
}
|
|
268
271
|
if (!validation) {
|
|
@@ -1004,6 +1007,19 @@ const getApiDataSource = async (dataSource, allValues, dataSourceRequestHandler,
|
|
|
1004
1007
|
// If not found and doesn't contain dots, try as widget-id
|
|
1005
1008
|
if ((depValue === null || depValue === undefined) && !dataSource.dependsOn.includes('.')) {
|
|
1006
1009
|
depValue = allValues[dataSource.dependsOn];
|
|
1010
|
+
// Smart resolution: If not found at top level, try to find the dependency in the same nested object
|
|
1011
|
+
// by looking for other keys in allValues that might contain the dependency.
|
|
1012
|
+
// We look for objects that contain both the current widget's path (if we can guess it) and the dependency.
|
|
1013
|
+
// But since we don't know the current widget's path here, we search for any object that has this dependency key.
|
|
1014
|
+
if (depValue === null || depValue === undefined || depValue === '') {
|
|
1015
|
+
for (const val of Object.values(allValues)) {
|
|
1016
|
+
if (val && typeof val === 'object' && !Array.isArray(val) && dataSource.dependsOn in val) {
|
|
1017
|
+
depValue = val[dataSource.dependsOn];
|
|
1018
|
+
if (depValue !== null && depValue !== undefined && depValue !== '')
|
|
1019
|
+
break;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1007
1023
|
}
|
|
1008
1024
|
if (depValue === null || depValue === undefined || depValue === '') {
|
|
1009
1025
|
// If dependency is empty, return empty array
|
|
@@ -1047,8 +1063,8 @@ const getApiDataSource = async (dataSource, allValues, dataSourceRequestHandler,
|
|
|
1047
1063
|
}
|
|
1048
1064
|
}
|
|
1049
1065
|
else if (staticParams.level_id) {
|
|
1050
|
-
// First level has no parent
|
|
1051
|
-
requestParams.parent_level_value_id =
|
|
1066
|
+
// First level has no parent, send empty string as many OpenG2P APIs expect it
|
|
1067
|
+
requestParams.parent_level_value_id = "";
|
|
1052
1068
|
}
|
|
1053
1069
|
// Get service mnemonic and endpoint (required)
|
|
1054
1070
|
const service = dataSource.service;
|
|
@@ -1117,10 +1133,12 @@ const transformDataSourceOptions = (data, valueKey, labelKey) => {
|
|
|
1117
1133
|
return { value: item, label: String(item) };
|
|
1118
1134
|
});
|
|
1119
1135
|
}
|
|
1120
|
-
return data.map((item) =>
|
|
1121
|
-
value
|
|
1122
|
-
label
|
|
1123
|
-
|
|
1136
|
+
return data.map((item) => {
|
|
1137
|
+
const value = item[valueKey];
|
|
1138
|
+
// Try multiple common label keys if the primary one is missing
|
|
1139
|
+
const label = item[labelKey] || item.name || item.label || item.mnemonic || item.level_value_mnemonic || String(value);
|
|
1140
|
+
return { value, label };
|
|
1141
|
+
});
|
|
1124
1142
|
};
|
|
1125
1143
|
|
|
1126
1144
|
const WidgetEventBusContext = React.createContext(null);
|
|
@@ -1898,46 +1916,71 @@ const useBaseWidget = (options) => {
|
|
|
1898
1916
|
const userHasSetValueRef = useRef(false);
|
|
1899
1917
|
// Use ref for values to avoid stale closures in handleChange
|
|
1900
1918
|
const valuesRef = useRef(values);
|
|
1919
|
+
const loadingRef = useRef(loading);
|
|
1920
|
+
const dataSourceOptionsRef = useRef(dataSourceOptions);
|
|
1901
1921
|
useEffect(() => {
|
|
1902
1922
|
valuesRef.current = values;
|
|
1903
|
-
|
|
1923
|
+
loadingRef.current = loading;
|
|
1924
|
+
dataSourceOptionsRef.current = dataSourceOptions;
|
|
1925
|
+
}, [values, loading, dataSourceOptions]);
|
|
1904
1926
|
// Track last dispatched value to prevent duplicate dispatches
|
|
1905
1927
|
const lastDispatchedValueRef = useRef(null);
|
|
1906
|
-
//
|
|
1907
|
-
const
|
|
1908
|
-
if (
|
|
1909
|
-
return
|
|
1928
|
+
// Helper to extract displayable value from object (especially geo hierarchy objects)
|
|
1929
|
+
const extractValueFromObject = useCallback((obj) => {
|
|
1930
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
1931
|
+
return obj;
|
|
1910
1932
|
}
|
|
1911
|
-
//
|
|
1912
|
-
const
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
if (
|
|
1919
|
-
return
|
|
1933
|
+
// Check for geo hierarchy structure first
|
|
1934
|
+
const geoConfig = config['widget-geo-config'];
|
|
1935
|
+
if (geoConfig) {
|
|
1936
|
+
// If we have a geo hierarchy object, extract the value for this specific level
|
|
1937
|
+
const hierarchy = obj.hierarchy || obj.geo_code_hierarchy_json?.hierarchy;
|
|
1938
|
+
if (Array.isArray(hierarchy)) {
|
|
1939
|
+
const levelData = hierarchy.find((l) => l.level === geoConfig.level);
|
|
1940
|
+
if (levelData) {
|
|
1941
|
+
return levelData.level_value_id;
|
|
1920
1942
|
}
|
|
1921
|
-
// If it's a geo hierarchy object but no extractable ID, return undefined to avoid rendering object
|
|
1922
|
-
return undefined;
|
|
1923
1943
|
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1944
|
+
}
|
|
1945
|
+
if ('geo_code_hierarchy_json' in obj || 'geo_lowest_level_value_id' in obj || 'hierarchy' in obj) {
|
|
1946
|
+
if ('geo_lowest_level_value_id' in obj) {
|
|
1947
|
+
return obj.geo_lowest_level_value_id;
|
|
1927
1948
|
}
|
|
1928
|
-
if ('
|
|
1929
|
-
return obj.
|
|
1949
|
+
if ('lowest_level_value_id' in obj) {
|
|
1950
|
+
return obj.lowest_level_value_id;
|
|
1930
1951
|
}
|
|
1931
|
-
|
|
1932
|
-
|
|
1952
|
+
// Fallback for nested geo_code_hierarchy_json
|
|
1953
|
+
if (obj.geo_code_hierarchy_json?.lowest_level_value_id) {
|
|
1954
|
+
return obj.geo_code_hierarchy_json.lowest_level_value_id;
|
|
1933
1955
|
}
|
|
1934
|
-
if (
|
|
1935
|
-
return obj.
|
|
1956
|
+
if (obj.geo_code_hierarchy_json?.geo_lowest_level_value_id) {
|
|
1957
|
+
return obj.geo_code_hierarchy_json.geo_lowest_level_value_id;
|
|
1936
1958
|
}
|
|
1937
|
-
// If no extractable
|
|
1938
|
-
// This prevents "Objects are not valid as a React child" errors
|
|
1959
|
+
// If it's a geo hierarchy object but no extractable ID, return undefined to avoid rendering object
|
|
1939
1960
|
return undefined;
|
|
1940
|
-
}
|
|
1961
|
+
}
|
|
1962
|
+
// Try common value fields
|
|
1963
|
+
if ('value' in obj) {
|
|
1964
|
+
return obj.value;
|
|
1965
|
+
}
|
|
1966
|
+
if ('id' in obj) {
|
|
1967
|
+
return obj.id;
|
|
1968
|
+
}
|
|
1969
|
+
if ('label' in obj) {
|
|
1970
|
+
return obj.label;
|
|
1971
|
+
}
|
|
1972
|
+
if ('name' in obj) {
|
|
1973
|
+
return obj.name;
|
|
1974
|
+
}
|
|
1975
|
+
// If no extractable value found, return undefined to avoid rendering object as React child
|
|
1976
|
+
// This prevents "Objects are not valid as a React child" errors
|
|
1977
|
+
return undefined;
|
|
1978
|
+
}, [config]);
|
|
1979
|
+
// Get current value
|
|
1980
|
+
const currentValue = useMemo(() => {
|
|
1981
|
+
if (isLayoutWidget) {
|
|
1982
|
+
return undefined; // Layout widgets don't have values
|
|
1983
|
+
}
|
|
1941
1984
|
// Try to get value from widgetId first (this should have the actual selected value)
|
|
1942
1985
|
// For geo widgets with dataPath, widgetId stores the actual ID, while dataPath stores the hierarchy object
|
|
1943
1986
|
let value = values[widgetId];
|
|
@@ -1978,6 +2021,33 @@ const useBaseWidget = (options) => {
|
|
|
1978
2021
|
}
|
|
1979
2022
|
return value !== undefined ? value : config['widget-data-default'];
|
|
1980
2023
|
}, [values, config, widgetId, isLayoutWidget]);
|
|
2024
|
+
// Track the last value we attempted to mirror to prevent infinite loops
|
|
2025
|
+
const lastMirroredValueRef = useRef(null);
|
|
2026
|
+
// Mirror value from dataPath to widgetId in Redux state if it's not already there.
|
|
2027
|
+
// This is essential for widgets that depend on this widget via 'dependsOn' using its widgetId,
|
|
2028
|
+
// especially when the actual data is stored in a nested path.
|
|
2029
|
+
// CRITICAL: This ensures that dependencies are resolved correctly when entering Edit mode.
|
|
2030
|
+
useEffect(() => {
|
|
2031
|
+
if (isLayoutWidget || !config['widget-data-path']) {
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
const rawValue = getWidgetValue(values, config['widget-data-path'], widgetId);
|
|
2035
|
+
if (rawValue !== undefined && rawValue !== null) {
|
|
2036
|
+
const extractedValue = extractValueFromObject(rawValue);
|
|
2037
|
+
// Only mirror if:
|
|
2038
|
+
// 1. The top-level value is undefined (initial load or entering edit mode)
|
|
2039
|
+
// 2. We haven't already tried to mirror this specific value (prevents loops if dispatch is ignored or delayed)
|
|
2040
|
+
// 3. The extracted value is valid
|
|
2041
|
+
if (values[widgetId] === undefined &&
|
|
2042
|
+
extractedValue !== undefined &&
|
|
2043
|
+
extractedValue !== null &&
|
|
2044
|
+
lastMirroredValueRef.current !== extractedValue) {
|
|
2045
|
+
lastMirroredValueRef.current = extractedValue;
|
|
2046
|
+
dispatch(setValue({ widgetId, value: extractedValue }));
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2050
|
+
}, [values, config['widget-data-path'], widgetId, isLayoutWidget]);
|
|
1981
2051
|
// Initialize default value only once on mount (skip for layout widgets)
|
|
1982
2052
|
useEffect(() => {
|
|
1983
2053
|
if (isLayoutWidget) {
|
|
@@ -2000,6 +2070,20 @@ const useBaseWidget = (options) => {
|
|
|
2000
2070
|
if (currentValue === newValue) {
|
|
2001
2071
|
return;
|
|
2002
2072
|
}
|
|
2073
|
+
// CRITICAL FIX: Ignore auto-clears (empty string or undefined) from UI components
|
|
2074
|
+
// when the widget's data source is currently loading OR if options are empty.
|
|
2075
|
+
// This prevents data disappearance when switching to Edit mode and components
|
|
2076
|
+
// incorrectly clear values before options load or if handler is temporarily missing.
|
|
2077
|
+
if (newValue === '' || newValue === null || newValue === undefined) {
|
|
2078
|
+
if (loadingRef.current) {
|
|
2079
|
+
console.warn(`[useBaseWidget] Ignoring empty value for ${widgetId} because data source is loading`);
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
if (config['widget-data-source']?.type === 'api' && dataSourceOptionsRef.current.length === 0) {
|
|
2083
|
+
console.warn(`[useBaseWidget] Ignoring empty value for ${widgetId} because API options are empty`);
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2003
2087
|
// Mark that user has set a value (unless this is the default initialization)
|
|
2004
2088
|
if (newValue !== config['widget-data-default'] || userHasSetValueRef.current) {
|
|
2005
2089
|
userHasSetValueRef.current = true;
|
|
@@ -2017,6 +2101,13 @@ const useBaseWidget = (options) => {
|
|
|
2017
2101
|
}
|
|
2018
2102
|
else {
|
|
2019
2103
|
// Has dataPath: update both widgetId and dataPath
|
|
2104
|
+
// CRITICAL: For geo widgets, we do NOT want to overwrite the shared hierarchy dataPath
|
|
2105
|
+
// with a primitive value (the selected ID). The hierarchy object is managed by useGeoWidgetCascade.
|
|
2106
|
+
if (config['widget-geo-config']) {
|
|
2107
|
+
dispatch(setValue({ widgetId, value: newValue }));
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
// For non-geo widgets, update both widgetId and dataPath
|
|
2020
2111
|
// CRITICAL: Create updated values object with newValue already set
|
|
2021
2112
|
// This prevents setWidgetValue from reading stale values
|
|
2022
2113
|
const currentValuesWithUpdate = {
|
|
@@ -2111,6 +2202,17 @@ const useBaseWidget = (options) => {
|
|
|
2111
2202
|
const apiService = dataSource?.type === 'api' ? dataSource.service : '';
|
|
2112
2203
|
const apiEndpoint = dataSource?.type === 'api' ? dataSource.endpoint : '';
|
|
2113
2204
|
const configKey = `${widgetId}-${isReadonly}-${dataSource?.type || 'none'}-${apiService}-${apiEndpoint}`;
|
|
2205
|
+
// Extract dependency value using a granular selector to prevent unnecessary re-renders
|
|
2206
|
+
// and infinite loops when other unrelated values in the state change.
|
|
2207
|
+
const dependencyValue = useSelector((state) => {
|
|
2208
|
+
if (dataSource?.type !== 'api' || !dataSource.dependsOn) {
|
|
2209
|
+
return null;
|
|
2210
|
+
}
|
|
2211
|
+
if (dataSource.dependsOn.includes('.')) {
|
|
2212
|
+
return getWidgetValue(state.widget.values, dataSource.dependsOn, '');
|
|
2213
|
+
}
|
|
2214
|
+
return state.widget.values[dataSource.dependsOn];
|
|
2215
|
+
});
|
|
2114
2216
|
// Handle data source loading
|
|
2115
2217
|
useEffect(() => {
|
|
2116
2218
|
if (!dataSource) {
|
|
@@ -2131,6 +2233,17 @@ const useBaseWidget = (options) => {
|
|
|
2131
2233
|
}
|
|
2132
2234
|
else {
|
|
2133
2235
|
depValue = values[dataSource.dependsOn];
|
|
2236
|
+
// Smart resolution: If not found at top level, and current widget has a nested dataPath,
|
|
2237
|
+
// try to find the dependency in the same nested object.
|
|
2238
|
+
if ((depValue === undefined || depValue === null || depValue === '') &&
|
|
2239
|
+
typeof config['widget-data-path'] === 'string' &&
|
|
2240
|
+
config['widget-data-path'].includes('.')) {
|
|
2241
|
+
const pathParts = config['widget-data-path'].split('.');
|
|
2242
|
+
pathParts.pop(); // Remove current field name
|
|
2243
|
+
const prefix = pathParts.join('.');
|
|
2244
|
+
const tryPath = `${prefix}.${dataSource.dependsOn}`;
|
|
2245
|
+
depValue = getWidgetValue(values, tryPath, '');
|
|
2246
|
+
}
|
|
2134
2247
|
}
|
|
2135
2248
|
// If dependency is empty, don't load (will load when dependency has value)
|
|
2136
2249
|
if (depValue === null || depValue === undefined || depValue === '') {
|
|
@@ -2164,7 +2277,7 @@ const useBaseWidget = (options) => {
|
|
|
2164
2277
|
}
|
|
2165
2278
|
// Extract level_id from widget-geo-config.level if available
|
|
2166
2279
|
const levelId = geoConfig?.level;
|
|
2167
|
-
data = await getApiDataSource(dataSource,
|
|
2280
|
+
data = await getApiDataSource(dataSource, valuesRef.current, currentHandler, levelId);
|
|
2168
2281
|
}
|
|
2169
2282
|
else if (dataSource.type === 'schema') {
|
|
2170
2283
|
data = getSchemaDataSource(dataSource, schemaData || {});
|
|
@@ -2199,9 +2312,9 @@ const useBaseWidget = (options) => {
|
|
|
2199
2312
|
}
|
|
2200
2313
|
};
|
|
2201
2314
|
loadDataSource();
|
|
2202
|
-
// Use configKey to ensure effect runs when
|
|
2315
|
+
// Use configKey and dependencyValue to ensure effect runs only when relevant state changes
|
|
2203
2316
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2204
|
-
}, [configKey,
|
|
2317
|
+
}, [configKey, dependencyValue, dataSourceRequestHandler, schemaData, widgetId, dispatch]);
|
|
2205
2318
|
return {
|
|
2206
2319
|
widgetId,
|
|
2207
2320
|
value: currentValue,
|
|
@@ -2402,6 +2515,9 @@ const useGeoWidgetCascade = (options) => {
|
|
|
2402
2515
|
const geoConfig = config['widget-geo-config'];
|
|
2403
2516
|
const dataSource = config['widget-data-source'];
|
|
2404
2517
|
const dataPath = config['widget-data-path'];
|
|
2518
|
+
const groupId = typeof dataPath === 'string' && dataPath.includes('.')
|
|
2519
|
+
? dataPath.split('.').slice(0, -1).join('.')
|
|
2520
|
+
: 'default';
|
|
2405
2521
|
const valuesRef = useRef(values);
|
|
2406
2522
|
const handlerRef = useRef(dataSourceRequestHandler);
|
|
2407
2523
|
// Keep refs updated
|
|
@@ -2411,10 +2527,36 @@ const useGeoWidgetCascade = (options) => {
|
|
|
2411
2527
|
}, [values, dataSourceRequestHandler]);
|
|
2412
2528
|
// Get current value and data source options
|
|
2413
2529
|
const currentValue = useSelector((state) => {
|
|
2414
|
-
|
|
2415
|
-
|
|
2530
|
+
// Try to get value from widgetId first (most recent selection)
|
|
2531
|
+
let value = state.widget.values[widgetId];
|
|
2532
|
+
// If not found in widgetId, try dataPath
|
|
2533
|
+
if (value === undefined && dataPath) {
|
|
2534
|
+
value = getWidgetValue(state.widget.values, dataPath, widgetId);
|
|
2535
|
+
}
|
|
2536
|
+
// Extract value if it's a geo hierarchy object
|
|
2537
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && geoConfig) {
|
|
2538
|
+
const hierarchy = value.hierarchy || value.geo_code_hierarchy_json?.hierarchy;
|
|
2539
|
+
if (Array.isArray(hierarchy)) {
|
|
2540
|
+
const levelData = hierarchy.find((l) => l.level === geoConfig.level);
|
|
2541
|
+
if (levelData) {
|
|
2542
|
+
return levelData.level_value_id;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
// Extended fallbacks (matching useBaseWidget)
|
|
2546
|
+
if ('geo_lowest_level_value_id' in value) {
|
|
2547
|
+
return value.geo_lowest_level_value_id;
|
|
2548
|
+
}
|
|
2549
|
+
if ('lowest_level_value_id' in value) {
|
|
2550
|
+
return value.lowest_level_value_id;
|
|
2551
|
+
}
|
|
2552
|
+
if (value.geo_code_hierarchy_json?.lowest_level_value_id) {
|
|
2553
|
+
return value.geo_code_hierarchy_json.lowest_level_value_id;
|
|
2554
|
+
}
|
|
2555
|
+
if (value.geo_code_hierarchy_json?.geo_lowest_level_value_id) {
|
|
2556
|
+
return value.geo_code_hierarchy_json.geo_lowest_level_value_id;
|
|
2557
|
+
}
|
|
2416
2558
|
}
|
|
2417
|
-
return
|
|
2559
|
+
return value;
|
|
2418
2560
|
});
|
|
2419
2561
|
// Memoize selector to avoid returning new array reference
|
|
2420
2562
|
const dataSourceOptions = useSelector((state) => state.widget.dataSources[widgetId] ?? EMPTY_DATA_SOURCE);
|
|
@@ -2434,11 +2576,17 @@ const useGeoWidgetCascade = (options) => {
|
|
|
2434
2576
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
2435
2577
|
const currentValues = valuesRef.current;
|
|
2436
2578
|
const currentHandler = handlerRef.current;
|
|
2437
|
-
// CRITICAL:
|
|
2438
|
-
|
|
2439
|
-
|
|
2579
|
+
// CRITICAL: Try to get parent value from event first, then from Redux
|
|
2580
|
+
let parentValue = event.value;
|
|
2581
|
+
if (parentValue === undefined || parentValue === null) {
|
|
2582
|
+
parentValue = currentValues[parentWidgetId];
|
|
2583
|
+
// If not found in top-level values, try to find it via dataPath or dependsOn
|
|
2584
|
+
if (parentValue === undefined && dataSource.dependsOn) {
|
|
2585
|
+
parentValue = getWidgetValue(currentValues, dataSource.dependsOn, '');
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2440
2588
|
// Remove this level and all below from hierarchy
|
|
2441
|
-
geoHierarchyBuilder.removeLevelAndBelow(level);
|
|
2589
|
+
geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
|
|
2442
2590
|
// Clear this widget's value
|
|
2443
2591
|
// CRITICAL: Only dispatch setValue for THIS widget, not for parent or other widgets
|
|
2444
2592
|
// setWidgetValue returns the entire updated state, but we only want to update this widget
|
|
@@ -2493,7 +2641,8 @@ const useGeoWidgetCascade = (options) => {
|
|
|
2493
2641
|
}
|
|
2494
2642
|
}
|
|
2495
2643
|
else {
|
|
2496
|
-
// If parent value is cleared, clear the data source
|
|
2644
|
+
// If parent value is cleared, clear the data source and hierarchy
|
|
2645
|
+
geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
|
|
2497
2646
|
dispatch(setDataSource({ widgetId, data: [] }));
|
|
2498
2647
|
}
|
|
2499
2648
|
};
|
|
@@ -2508,25 +2657,49 @@ const useGeoWidgetCascade = (options) => {
|
|
|
2508
2657
|
if (!geoConfig) {
|
|
2509
2658
|
return;
|
|
2510
2659
|
}
|
|
2511
|
-
// Skip if value is
|
|
2512
|
-
if
|
|
2513
|
-
|
|
2660
|
+
// Skip if value is undefined (it might still be loading or rehydrating)
|
|
2661
|
+
// ONLY clear hierarchy if the value is explicitly null or empty string (user action)
|
|
2662
|
+
if (currentValue === null || currentValue === '') {
|
|
2514
2663
|
const { level } = geoConfig;
|
|
2515
|
-
geoHierarchyBuilder.removeLevelAndBelow(level);
|
|
2664
|
+
geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
|
|
2665
|
+
// If we have a dataPath, we need to update Redux with the cleared hierarchy
|
|
2666
|
+
if (dataPath) {
|
|
2667
|
+
const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson(groupId);
|
|
2668
|
+
let finalUpdatedValues = valuesRef.current;
|
|
2669
|
+
// Use logic similar to the build section below to update the dataPath
|
|
2670
|
+
if (typeof dataPath === 'string' && dataPath.endsWith('.geo_code_hierarchy_json')) {
|
|
2671
|
+
const prefix = dataPath.substring(0, dataPath.lastIndexOf('.'));
|
|
2672
|
+
finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson?.geo_code_hierarchy_json);
|
|
2673
|
+
finalUpdatedValues = setWidgetValue(finalUpdatedValues, `${prefix}.geo_lowest_level_value_id`, widgetId, hierarchyJson?.geo_lowest_level_value_id);
|
|
2674
|
+
}
|
|
2675
|
+
else {
|
|
2676
|
+
finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson?.geo_code_hierarchy_json);
|
|
2677
|
+
}
|
|
2678
|
+
dispatch(setValues(finalUpdatedValues));
|
|
2679
|
+
}
|
|
2516
2680
|
return;
|
|
2517
2681
|
}
|
|
2682
|
+
if (currentValue === undefined) {
|
|
2683
|
+
return; // Skip if undefined (still initializing)
|
|
2684
|
+
}
|
|
2518
2685
|
const { level, isLastLevel } = geoConfig;
|
|
2519
|
-
//
|
|
2520
|
-
if (
|
|
2686
|
+
// Check if hierarchy is already built to prevent endless loops
|
|
2687
|
+
if (dataPath) {
|
|
2521
2688
|
const currentHierarchy = getWidgetValue(valuesRef.current, dataPath, widgetId);
|
|
2522
2689
|
// If hierarchy JSON is already set and matches current value, skip rebuilding
|
|
2523
|
-
if (currentHierarchy && typeof currentHierarchy === 'object'
|
|
2524
|
-
// Check if
|
|
2525
|
-
const
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2690
|
+
if (currentHierarchy && typeof currentHierarchy === 'object') {
|
|
2691
|
+
// Check if this specific level's value matches the hierarchy
|
|
2692
|
+
const hierarchyArray = currentHierarchy.hierarchy || currentHierarchy.geo_code_hierarchy_json?.hierarchy;
|
|
2693
|
+
if (Array.isArray(hierarchyArray)) {
|
|
2694
|
+
const currentLevelValue = typeof currentValue === 'object'
|
|
2695
|
+
? (currentValue.level_value_id || currentValue.id || currentValue.value)
|
|
2696
|
+
: currentValue;
|
|
2697
|
+
const levelData = hierarchyArray.find((l) => l.level === geoConfig.level);
|
|
2698
|
+
// If this level is already correctly represented in the hierarchy, skip rebuilding
|
|
2699
|
+
// String conversion ensures comparison works for mixed types
|
|
2700
|
+
if (levelData && String(levelData.level_value_id) === String(currentLevelValue)) {
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2530
2703
|
}
|
|
2531
2704
|
}
|
|
2532
2705
|
}
|
|
@@ -2549,23 +2722,31 @@ const useGeoWidgetCascade = (options) => {
|
|
|
2549
2722
|
return;
|
|
2550
2723
|
}
|
|
2551
2724
|
// When a widget's own value changes, remove this level and all below from hierarchy first
|
|
2552
|
-
|
|
2553
|
-
// The addLevel method already handles removing existing levels, but we explicitly clear to be safe
|
|
2554
|
-
geoHierarchyBuilder.removeLevelAndBelow(level);
|
|
2725
|
+
geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
|
|
2555
2726
|
// Add level to hierarchy
|
|
2556
|
-
geoHierarchyBuilder.addLevel(level, level_value_id, level_value_mnemonic);
|
|
2557
|
-
//
|
|
2558
|
-
if (
|
|
2559
|
-
const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson();
|
|
2727
|
+
geoHierarchyBuilder.addLevel(level, level_value_id, level_value_mnemonic, groupId);
|
|
2728
|
+
// Build and store hierarchy JSON on every change
|
|
2729
|
+
if (dataPath) {
|
|
2730
|
+
const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson(groupId);
|
|
2560
2731
|
if (hierarchyJson) {
|
|
2561
|
-
//
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2732
|
+
// Fix: Avoid double nesting of geo_code_hierarchy_json
|
|
2733
|
+
// If dataPath ends with .geo_code_hierarchy_json, we want to save the content directly to it
|
|
2734
|
+
// and save the lowest level ID as a sibling
|
|
2735
|
+
let finalUpdatedValues = valuesRef.current;
|
|
2736
|
+
if (typeof dataPath === 'string' && dataPath.endsWith('.geo_code_hierarchy_json')) {
|
|
2737
|
+
const prefix = dataPath.substring(0, dataPath.lastIndexOf('.'));
|
|
2738
|
+
// Save hierarchy JSON content directly to dataPath (avoiding double nesting)
|
|
2739
|
+
finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson.geo_code_hierarchy_json);
|
|
2740
|
+
// Save lowest level ID as sibling
|
|
2741
|
+
finalUpdatedValues = setWidgetValue(finalUpdatedValues, `${prefix}.geo_lowest_level_value_id`, widgetId, hierarchyJson.geo_lowest_level_value_id);
|
|
2742
|
+
}
|
|
2743
|
+
else {
|
|
2744
|
+
// Fallback if path doesn't follow the naming convention
|
|
2745
|
+
finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson.geo_code_hierarchy_json);
|
|
2746
|
+
}
|
|
2747
|
+
// CRITICAL: Use setValues for deep merge instead of replacing root keys with setValue
|
|
2748
|
+
// setWidgetValue returns the complete updated state object with all keys preserved
|
|
2749
|
+
dispatch(setValues(finalUpdatedValues));
|
|
2569
2750
|
}
|
|
2570
2751
|
}
|
|
2571
2752
|
}, [geoConfig, currentValue, widgetId, dataPath, dispatch, dataSourceOptions]);
|
|
@@ -3628,7 +3809,15 @@ const collectWidgets = (panels) => {
|
|
|
3628
3809
|
});
|
|
3629
3810
|
return widgets;
|
|
3630
3811
|
};
|
|
3631
|
-
|
|
3812
|
+
/**
|
|
3813
|
+
* Validate all widgets in a section and dispatch errors to the store.
|
|
3814
|
+
*
|
|
3815
|
+
* @param skipRequired - When true, required-field checks (widget-required,
|
|
3816
|
+
* validation.required, document-required) are skipped. Use this for
|
|
3817
|
+
* per-section Save/Next navigation so the user can advance without filling
|
|
3818
|
+
* every mandatory field; only format/range errors are reported.
|
|
3819
|
+
*/
|
|
3820
|
+
const sectionValidate = (section, currentSchemaData, dispatch, skipRequired = false) => {
|
|
3632
3821
|
const allWidgets = collectWidgets(section.panels);
|
|
3633
3822
|
let isValid = true;
|
|
3634
3823
|
for (const widget of allWidgets) {
|
|
@@ -3637,7 +3826,7 @@ const sectionValidate = (section, currentSchemaData, dispatch) => {
|
|
|
3637
3826
|
continue;
|
|
3638
3827
|
const widgetId = widget['widget-id'];
|
|
3639
3828
|
const value = getWidgetValue(currentSchemaData, widget['widget-data-path'], widgetId);
|
|
3640
|
-
const errors = validateWidget(value, widget['widget-data-validation'], widget['widget-required']);
|
|
3829
|
+
const errors = validateWidget(value, widget['widget-data-validation'], widget['widget-required'], skipRequired);
|
|
3641
3830
|
if (errors.length > 0) {
|
|
3642
3831
|
isValid = false;
|
|
3643
3832
|
dispatch(setTouched({ widgetId, touched: true }));
|
|
@@ -3648,10 +3837,10 @@ const sectionValidate = (section, currentSchemaData, dispatch) => {
|
|
|
3648
3837
|
dispatch(setError({ widgetId, errors: [] }));
|
|
3649
3838
|
}
|
|
3650
3839
|
}
|
|
3651
|
-
// Supporting Documents
|
|
3840
|
+
// Supporting Documents — only check required when not skipping required validation
|
|
3652
3841
|
section['section-supporting-documents']?.forEach((doc, index) => {
|
|
3653
3842
|
const widgetId = `supporting-doc-${section['section-id']}-${index}`;
|
|
3654
|
-
if (doc['document-required']) {
|
|
3843
|
+
if (!skipRequired && doc['document-required']) {
|
|
3655
3844
|
const file = getValueByPath(currentSchemaData, doc['document-data-path']);
|
|
3656
3845
|
if (!file) {
|
|
3657
3846
|
isValid = false;
|
|
@@ -3670,6 +3859,85 @@ const sectionValidate = (section, currentSchemaData, dispatch) => {
|
|
|
3670
3859
|
return isValid;
|
|
3671
3860
|
};
|
|
3672
3861
|
|
|
3862
|
+
/** Table-style widgets that bind to an array path in the store / schema. */
|
|
3863
|
+
function isTableLikeWidget(widget) {
|
|
3864
|
+
const w = widget.widget;
|
|
3865
|
+
/** widget-type union in types omits legacy values like simple-table still used at runtime */
|
|
3866
|
+
const t = widget['widget-type'];
|
|
3867
|
+
return (w === 'table' ||
|
|
3868
|
+
w === 'dialog-table' ||
|
|
3869
|
+
w === 'simple-table' ||
|
|
3870
|
+
t === 'table' ||
|
|
3871
|
+
t === 'simple-table');
|
|
3872
|
+
}
|
|
3873
|
+
/**
|
|
3874
|
+
* Resolve `records` for section save payloads.
|
|
3875
|
+
* - Back-compat: path ending in `.records` (e.g. `regId.records`)
|
|
3876
|
+
* - Else: first array snapshot at a string `widget-data-path` on a table-like widget
|
|
3877
|
+
* (e.g. `household.members` for dialog-table)
|
|
3878
|
+
*/
|
|
3879
|
+
function extractTableRecordsFromSnapshot(snapshot, sectionWidgets) {
|
|
3880
|
+
const convention = Object.entries(snapshot).find(([key, value]) => key.endsWith('.records') && Array.isArray(value));
|
|
3881
|
+
if (convention) {
|
|
3882
|
+
return convention[1];
|
|
3883
|
+
}
|
|
3884
|
+
const tablePaths = [];
|
|
3885
|
+
sectionWidgets.forEach((widget) => {
|
|
3886
|
+
if (!isTableLikeWidget(widget))
|
|
3887
|
+
return;
|
|
3888
|
+
const p = widget['widget-data-path'];
|
|
3889
|
+
if (typeof p === 'string' && p.length > 0) {
|
|
3890
|
+
tablePaths.push(p);
|
|
3891
|
+
}
|
|
3892
|
+
else if (p && typeof p === 'object') {
|
|
3893
|
+
Object.values(p).forEach((sub) => {
|
|
3894
|
+
if (typeof sub === 'string' && sub.length > 0)
|
|
3895
|
+
tablePaths.push(sub);
|
|
3896
|
+
});
|
|
3897
|
+
}
|
|
3898
|
+
});
|
|
3899
|
+
for (const path of tablePaths) {
|
|
3900
|
+
const val = snapshot[path];
|
|
3901
|
+
if (Array.isArray(val)) {
|
|
3902
|
+
return val;
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
return [];
|
|
3906
|
+
}
|
|
3907
|
+
|
|
3908
|
+
/** Root class on readonly label/value rows; SectionRenderer scopes overflow/ellipsis rules here. */
|
|
3909
|
+
const READONLY_VALUE_ROW_ROOT_CLASSES = [
|
|
3910
|
+
'TextDisplayWidget',
|
|
3911
|
+
'TextAreaDisplayWidget',
|
|
3912
|
+
'SelectDisplayWidget',
|
|
3913
|
+
'PhoneDisplayWidget',
|
|
3914
|
+
'NumberDisplayWidget',
|
|
3915
|
+
'CurrencyDisplayWidget',
|
|
3916
|
+
'RadioDisplayWidget',
|
|
3917
|
+
'DateDisplayWidget',
|
|
3918
|
+
'DateTimeDisplayWidget',
|
|
3919
|
+
'CheckboxDisplayWidget',
|
|
3920
|
+
'BooleanDisplayWidget',
|
|
3921
|
+
'FileDisplayWidget',
|
|
3922
|
+
'DisplayFieldWidget',
|
|
3923
|
+
];
|
|
3924
|
+
/** Rows whose value is one line in .flex-1 > .text-gray-900 (ellipsis; full string via title on the element). */
|
|
3925
|
+
const READONLY_SINGLE_LINE_VALUE_ROW_CLASSES = [
|
|
3926
|
+
'TextDisplayWidget',
|
|
3927
|
+
'SelectDisplayWidget',
|
|
3928
|
+
'PhoneDisplayWidget',
|
|
3929
|
+
'NumberDisplayWidget',
|
|
3930
|
+
'CurrencyDisplayWidget',
|
|
3931
|
+
'RadioDisplayWidget',
|
|
3932
|
+
'DateDisplayWidget',
|
|
3933
|
+
'DateTimeDisplayWidget',
|
|
3934
|
+
'CheckboxDisplayWidget',
|
|
3935
|
+
'BooleanDisplayWidget',
|
|
3936
|
+
'DisplayFieldWidget',
|
|
3937
|
+
];
|
|
3938
|
+
function scopedClassSelectors(sectionClassId, classNames) {
|
|
3939
|
+
return classNames.map((c) => `.${sectionClassId} .${c}`).join(',\n ');
|
|
3940
|
+
}
|
|
3673
3941
|
/**
|
|
3674
3942
|
* Renders a section with its panels
|
|
3675
3943
|
*
|
|
@@ -3678,7 +3946,7 @@ const sectionValidate = (section, currentSchemaData, dispatch) => {
|
|
|
3678
3946
|
* - Panels wrap when they exceed available width
|
|
3679
3947
|
* - Sections can sit side-by-side if there's space
|
|
3680
3948
|
*/
|
|
3681
|
-
const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequestHandler, schemaData, onValueChange, gridColumnSpan, onSectionSave, hideEditButton = false, mode = 'RegistryView', namespace, changeRequestType, showChangeRequestLabel = true, dbSectionId, sectionRegisterId, onSectionDirtyChange, sectionIndex, sectionCount, expandedSectionIndex, onExpandSection, onSectionSaveSuccess, onPreviousSection, isDraft, onEditModeChange, forceExitEdit, }) => {
|
|
3949
|
+
const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequestHandler, schemaData, onValueChange, gridColumnSpan, onSectionSave, hideEditButton = false, mode = 'RegistryView', namespace, changeRequestType, showChangeRequestLabel = true, dbSectionId, sectionRegisterId, onSectionDirtyChange, sectionIndex, sectionCount, expandedSectionIndex, onExpandSection, onSectionSaveSuccess, onPreviousSection, isDraft, isAccessible = false, onEditModeChange, forceExitEdit, }) => {
|
|
3682
3950
|
const { translateConfig, translate } = useWidgetTranslation();
|
|
3683
3951
|
const resolvedTheme = useWidgetTheme();
|
|
3684
3952
|
const portalCSSVariables = useMemo(() => themeToCSSVariables(resolvedTheme), [resolvedTheme]);
|
|
@@ -3698,41 +3966,22 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
3698
3966
|
}
|
|
3699
3967
|
return section;
|
|
3700
3968
|
}, [section, namespace]);
|
|
3701
|
-
// Create namespaced schemaData if namespace is provided
|
|
3702
|
-
//
|
|
3969
|
+
// Create namespaced schemaData if namespace is provided.
|
|
3970
|
+
// Widgets with namespaced data-paths (e.g. "rv-section-0.a1a4d25a.birth_date")
|
|
3971
|
+
// need a nested object at values[namespace] so getValueByPath can traverse it.
|
|
3703
3972
|
const namespacedSchemaData = useMemo(() => {
|
|
3704
3973
|
if (!namespace || !currentSchemaData) {
|
|
3705
3974
|
return schemaData;
|
|
3706
3975
|
}
|
|
3707
|
-
|
|
3708
|
-
const namespaced = { ...currentSchemaData };
|
|
3709
|
-
// Copy all top-level keys to namespaced paths
|
|
3710
|
-
Object.keys(currentSchemaData).forEach(key => {
|
|
3711
|
-
const namespacedKey = `${namespace}.${key}`;
|
|
3712
|
-
if (!(namespacedKey in namespaced)) {
|
|
3713
|
-
namespaced[namespacedKey] = currentSchemaData[key];
|
|
3714
|
-
}
|
|
3715
|
-
});
|
|
3716
|
-
// Also handle nested objects - copy nested values to namespaced paths
|
|
3717
|
-
const copyNestedValues = (obj, prefix = '') => {
|
|
3718
|
-
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
3719
|
-
Object.keys(obj).forEach(key => {
|
|
3720
|
-
const fullPath = prefix ? `${prefix}.${key}` : key;
|
|
3721
|
-
const namespacedPath = `${namespace}.${fullPath}`;
|
|
3722
|
-
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
|
|
3723
|
-
copyNestedValues(obj[key], fullPath);
|
|
3724
|
-
// Also set the nested object at the namespaced path
|
|
3725
|
-
setValueByPath(namespaced, namespacedPath, obj[key]);
|
|
3726
|
-
}
|
|
3727
|
-
else {
|
|
3728
|
-
setValueByPath(namespaced, namespacedPath, obj[key]);
|
|
3729
|
-
}
|
|
3730
|
-
});
|
|
3731
|
-
}
|
|
3732
|
-
};
|
|
3733
|
-
copyNestedValues(currentSchemaData);
|
|
3734
|
-
return namespaced;
|
|
3976
|
+
return { ...currentSchemaData, [namespace]: currentSchemaData };
|
|
3735
3977
|
}, [namespace, schemaData, currentSchemaData]);
|
|
3978
|
+
// Populate the store with namespaced schema data so that namespaced widgets
|
|
3979
|
+
// can read their initial values via getValueByPath on the namespaced paths.
|
|
3980
|
+
useEffect(() => {
|
|
3981
|
+
if (namespace && namespacedSchemaData) {
|
|
3982
|
+
dispatch(setValues(namespacedSchemaData));
|
|
3983
|
+
}
|
|
3984
|
+
}, [namespace, namespacedSchemaData, dispatch]);
|
|
3736
3985
|
const crViewData = useMemo(() => {
|
|
3737
3986
|
if (mode !== 'CRView')
|
|
3738
3987
|
return null;
|
|
@@ -3755,21 +4004,33 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
3755
4004
|
const sectionId = sectionToRender['section-id'];
|
|
3756
4005
|
const gridId = `section-panels-${sectionId}`;
|
|
3757
4006
|
const sectionClassId = `section-${sectionId}`;
|
|
4007
|
+
const readonlyValueRowRootsCss = useMemo(() => scopedClassSelectors(sectionClassId, READONLY_VALUE_ROW_ROOT_CLASSES), [sectionClassId]);
|
|
4008
|
+
const readonlyValueRowFlex1Css = useMemo(() => READONLY_VALUE_ROW_ROOT_CLASSES.map((c) => `.${sectionClassId} .${c} > .flex-1`).join(',\n '), [sectionClassId]);
|
|
4009
|
+
const readonlySingleLineValueTextCss = useMemo(() => READONLY_SINGLE_LINE_VALUE_ROW_CLASSES.map((c) => `.${sectionClassId} .${c} > .flex-1 > .text-gray-900`).join(',\n '), [sectionClassId]);
|
|
3758
4010
|
// IntakeForm mode: accordion expand/collapse state (supports toggle)
|
|
3759
4011
|
const [standaloneExpanded, setStandaloneExpanded] = useState(true); // For sectionIndex undefined (standalone use)
|
|
3760
4012
|
const isExpandedFromContainer = typeof sectionIndex === 'number' && expandedSectionIndex === sectionIndex;
|
|
3761
4013
|
const isExpandedStandalone = sectionIndex === undefined && standaloneExpanded;
|
|
3762
4014
|
const isExpanded = mode === 'IntakeForm' && (isExpandedFromContainer || isExpandedStandalone);
|
|
4015
|
+
// IntakeForm only: tracks whether the user has clicked Next on this section at least once.
|
|
4016
|
+
// Used to unlock the accordion header so the user can navigate back to a visited section.
|
|
4017
|
+
const [hasBeenSavedByUser, setHasBeenSavedByUser] = useState(false);
|
|
4018
|
+
// Accordion header click behaviour in IntakeForm mode:
|
|
4019
|
+
// - Standalone (no sectionIndex): always toggleable.
|
|
4020
|
+
// - Managed by SectionsContainer: toggleable only when isAccessible is true
|
|
4021
|
+
// (i.e. the section has been visited OR is the immediate next one).
|
|
4022
|
+
// Sections beyond that remain locked.
|
|
3763
4023
|
const handleAccordionToggle = useCallback(() => {
|
|
3764
4024
|
if (mode !== 'IntakeForm')
|
|
3765
4025
|
return;
|
|
3766
|
-
if (
|
|
3767
|
-
onExpandSection(sectionIndex);
|
|
3768
|
-
}
|
|
3769
|
-
else if (sectionIndex === undefined) {
|
|
4026
|
+
if (sectionIndex === undefined) {
|
|
3770
4027
|
setStandaloneExpanded(prev => !prev);
|
|
3771
4028
|
}
|
|
3772
|
-
|
|
4029
|
+
else if (isAccessible && onExpandSection) {
|
|
4030
|
+
onExpandSection(sectionIndex);
|
|
4031
|
+
}
|
|
4032
|
+
// Intentionally no-op for locked sections (isAccessible === false)
|
|
4033
|
+
}, [mode, sectionIndex, isAccessible, onExpandSection]);
|
|
3773
4034
|
// Recursively count all vertical panels, especially those nested inside horizontal panels
|
|
3774
4035
|
// Typically: horizontal panels at first level contain vertical panels at second level
|
|
3775
4036
|
// Accounts for panel-column-span: a panel with column-span 3 counts as 3 columns
|
|
@@ -4018,9 +4279,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4018
4279
|
const widgetPath = widget['widget-data-path'];
|
|
4019
4280
|
if (!widgetPath)
|
|
4020
4281
|
return;
|
|
4021
|
-
if (widget
|
|
4022
|
-
widget['widget-type'] === 'simple-table' ||
|
|
4023
|
-
widget['widget'] === 'table') {
|
|
4282
|
+
if (isTableLikeWidget(widget)) {
|
|
4024
4283
|
hasTable = true;
|
|
4025
4284
|
}
|
|
4026
4285
|
if (typeof widgetPath === 'object') {
|
|
@@ -4055,8 +4314,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4055
4314
|
},
|
|
4056
4315
|
];
|
|
4057
4316
|
}
|
|
4058
|
-
|
|
4059
|
-
return tableEntry ? tableEntry[1] : [];
|
|
4317
|
+
return extractTableRecordsFromSnapshot(snapshot, widgets);
|
|
4060
4318
|
};
|
|
4061
4319
|
// Get original section (without namespace) for building snapshots
|
|
4062
4320
|
// This ensures we use the original data paths when saving
|
|
@@ -4149,18 +4407,68 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4149
4407
|
const intakeFormSectionStatus = useMemo(() => {
|
|
4150
4408
|
if (mode !== 'IntakeForm' || isDraft === false)
|
|
4151
4409
|
return null;
|
|
4152
|
-
const hasValue = (v) => v !== undefined && v !== null && (typeof v !== 'string' || v.trim().length > 0);
|
|
4153
|
-
const currentSnapshot = buildSectionSnapshot(storeValues, namespace);
|
|
4154
|
-
const record = currentSnapshot.records?.[0];
|
|
4155
|
-
const hasData = record &&
|
|
4156
|
-
typeof record === 'object' &&
|
|
4157
|
-
Object.values(record).some((v) => hasValue(v));
|
|
4158
4410
|
if (isDirty)
|
|
4159
4411
|
return 'modified';
|
|
4160
|
-
if (
|
|
4412
|
+
if (hasBeenSavedByUser)
|
|
4161
4413
|
return 'saved';
|
|
4162
4414
|
return null;
|
|
4163
|
-
}, [mode, isDirty,
|
|
4415
|
+
}, [mode, isDirty, hasBeenSavedByUser]);
|
|
4416
|
+
// Revert store values to the original schemaData for this section's widgets.
|
|
4417
|
+
// Used by both handleSave (RegistryView raises a CR, so values should not persist)
|
|
4418
|
+
// and handleCancel.
|
|
4419
|
+
const revertToOriginalValues = useCallback(() => {
|
|
4420
|
+
const sectionWidgets = collectWidgets(originalSection.panels);
|
|
4421
|
+
const oldSchemaData = schemaData || contextSchemaData;
|
|
4422
|
+
const currentStoreValues = store.getState().widget.values;
|
|
4423
|
+
let newStoreValues = currentStoreValues;
|
|
4424
|
+
sectionWidgets.forEach(widget => {
|
|
4425
|
+
const originalWidgetId = widget['widget-id'];
|
|
4426
|
+
const namespacedWidgetId = namespace ? `${namespace}__${originalWidgetId}` : originalWidgetId;
|
|
4427
|
+
const widgetId = namespacedWidgetId;
|
|
4428
|
+
const originalDataPath = widget['widget-data-path'];
|
|
4429
|
+
const storeDataPath = namespace && originalDataPath
|
|
4430
|
+
? (typeof originalDataPath === 'string'
|
|
4431
|
+
? `${namespace}.${originalDataPath}`
|
|
4432
|
+
: Object.fromEntries(Object.entries(originalDataPath).map(([key, path]) => [key, `${namespace}.${path}`])))
|
|
4433
|
+
: originalDataPath;
|
|
4434
|
+
if (widgetId && originalDataPath) {
|
|
4435
|
+
let oldValue;
|
|
4436
|
+
if (typeof originalDataPath === 'object') {
|
|
4437
|
+
oldValue = {};
|
|
4438
|
+
Object.entries(originalDataPath).forEach(([key, path]) => {
|
|
4439
|
+
if (typeof path === 'string') {
|
|
4440
|
+
oldValue[key] = getValueByPath(oldSchemaData, path);
|
|
4441
|
+
}
|
|
4442
|
+
});
|
|
4443
|
+
}
|
|
4444
|
+
else if (typeof originalDataPath === 'string') {
|
|
4445
|
+
oldValue = getValueByPath(oldSchemaData, originalDataPath);
|
|
4446
|
+
}
|
|
4447
|
+
if (oldValue !== undefined) {
|
|
4448
|
+
newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
|
|
4449
|
+
// Also revert the widgetId-based entry — useBaseWidget.handleChange
|
|
4450
|
+
// sets values[widgetId] during editing, and useBaseWidget.currentValue
|
|
4451
|
+
// reads values[widgetId] first before falling through to the dataPath.
|
|
4452
|
+
newStoreValues = { ...newStoreValues, [widgetId]: oldValue };
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
});
|
|
4456
|
+
if (hasSupportingDocuments) {
|
|
4457
|
+
const originalSupportingDocuments = originalSection['section-supporting-documents'] || [];
|
|
4458
|
+
originalSupportingDocuments.forEach((doc, index) => {
|
|
4459
|
+
const widgetId = `supporting-doc-${sectionId}-${index}`;
|
|
4460
|
+
const originalDataPath = doc['document-data-path'];
|
|
4461
|
+
const storeDataPath = namespace && originalDataPath
|
|
4462
|
+
? `${namespace}.${originalDataPath}`
|
|
4463
|
+
: originalDataPath;
|
|
4464
|
+
const oldValue = getValueByPath(oldSchemaData, originalDataPath);
|
|
4465
|
+
newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
|
|
4466
|
+
});
|
|
4467
|
+
}
|
|
4468
|
+
if (newStoreValues !== currentStoreValues) {
|
|
4469
|
+
dispatch(setValues(newStoreValues));
|
|
4470
|
+
}
|
|
4471
|
+
}, [originalSection, schemaData, contextSchemaData, store, namespace, hasSupportingDocuments, sectionId, dispatch]);
|
|
4164
4472
|
// Handle save button click
|
|
4165
4473
|
const handleSave = async () => {
|
|
4166
4474
|
if (!store || !onSectionSave) {
|
|
@@ -4174,7 +4482,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4174
4482
|
const sectionWidgets = collectWidgets(originalSection.panels);
|
|
4175
4483
|
const currentState = store.getState().widget;
|
|
4176
4484
|
const currentSchemaData = currentState.values || {};
|
|
4177
|
-
const isSectionValid = sectionValidate(originalSection, currentSchemaData, dispatch);
|
|
4485
|
+
const isSectionValid = sectionValidate(originalSection, currentSchemaData, dispatch, true);
|
|
4178
4486
|
if (!isSectionValid) {
|
|
4179
4487
|
return;
|
|
4180
4488
|
}
|
|
@@ -4197,12 +4505,24 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4197
4505
|
});
|
|
4198
4506
|
}
|
|
4199
4507
|
if (JSON.stringify(oldSchemaData) !== JSON.stringify(newSchemaData)) {
|
|
4508
|
+
let profileImage = null;
|
|
4509
|
+
for (const record of newSchemaData) {
|
|
4510
|
+
if (typeof record === 'object' && record !== null) {
|
|
4511
|
+
for (const [key, value] of Object.entries(record)) {
|
|
4512
|
+
if (value instanceof File) {
|
|
4513
|
+
profileImage = value;
|
|
4514
|
+
record[key] = '';
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
4518
|
+
}
|
|
4200
4519
|
try {
|
|
4201
4520
|
const sectionchanges = {
|
|
4202
4521
|
section_id: dbSectionId ?? originalSection['section-id'],
|
|
4203
4522
|
section_register_id: sectionRegisterId,
|
|
4204
4523
|
records: [...newSchemaData],
|
|
4205
|
-
files: [...sectionFiles]
|
|
4524
|
+
files: [...sectionFiles],
|
|
4525
|
+
...(profileImage ? { image: profileImage } : {}),
|
|
4206
4526
|
};
|
|
4207
4527
|
await onSectionSave(sectionchanges);
|
|
4208
4528
|
}
|
|
@@ -4210,6 +4530,12 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4210
4530
|
console.error('Section Changes Save failed', error);
|
|
4211
4531
|
}
|
|
4212
4532
|
}
|
|
4533
|
+
// In RegistryView, save raises a CR — the actual data update follows a
|
|
4534
|
+
// separate approval workflow, so revert the displayed values to the
|
|
4535
|
+
// originals so the view doesn't show unapproved edits.
|
|
4536
|
+
if (mode === 'RegistryView') {
|
|
4537
|
+
revertToOriginalValues();
|
|
4538
|
+
}
|
|
4213
4539
|
setIsEditMode(false);
|
|
4214
4540
|
onEditModeChange?.(originalSectionId, false);
|
|
4215
4541
|
};
|
|
@@ -4222,7 +4548,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4222
4548
|
const sectionWidgets = collectWidgets(originalSection.panels);
|
|
4223
4549
|
const currentState = store.getState().widget;
|
|
4224
4550
|
const currentSchemaData = currentState.values || {};
|
|
4225
|
-
const isSectionValid = sectionValidate(originalSection, currentSchemaData, dispatch);
|
|
4551
|
+
const isSectionValid = sectionValidate(originalSection, currentSchemaData, dispatch, true);
|
|
4226
4552
|
if (!isSectionValid)
|
|
4227
4553
|
return;
|
|
4228
4554
|
const oldSchemaData = schemaData || contextSchemaData;
|
|
@@ -4239,12 +4565,24 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4239
4565
|
});
|
|
4240
4566
|
}
|
|
4241
4567
|
if (JSON.stringify(oldSchemaData) !== JSON.stringify(newSchemaData)) {
|
|
4568
|
+
let profileImage = null;
|
|
4569
|
+
for (const record of newSchemaData) {
|
|
4570
|
+
if (typeof record === 'object' && record !== null) {
|
|
4571
|
+
for (const [key, value] of Object.entries(record)) {
|
|
4572
|
+
if (value instanceof File) {
|
|
4573
|
+
profileImage = value;
|
|
4574
|
+
record[key] = '';
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4242
4579
|
try {
|
|
4243
4580
|
await onSectionSave({
|
|
4244
4581
|
section_id: dbSectionId ?? originalSection['section-id'],
|
|
4245
4582
|
section_register_id: sectionRegisterId,
|
|
4246
4583
|
records: [...newSchemaData],
|
|
4247
4584
|
files: [...sectionFiles],
|
|
4585
|
+
...(profileImage ? { image: profileImage } : {}),
|
|
4248
4586
|
});
|
|
4249
4587
|
}
|
|
4250
4588
|
catch (error) {
|
|
@@ -4255,72 +4593,21 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4255
4593
|
if (mode === 'IntakeForm') {
|
|
4256
4594
|
baselineSnapshotRef.current = buildSectionSnapshot(currentSchemaData, namespace);
|
|
4257
4595
|
setIntakeFormBaselineTrigger((prev) => prev + 1);
|
|
4596
|
+
setHasBeenSavedByUser(true);
|
|
4258
4597
|
}
|
|
4259
4598
|
onSectionDirtyChange?.(sectionId, false);
|
|
4260
4599
|
}
|
|
4600
|
+
else if (mode === 'IntakeForm') {
|
|
4601
|
+
// No onSectionSave provided, but still mark section as visited so the
|
|
4602
|
+
// user can navigate back to it by clicking the accordion header.
|
|
4603
|
+
setHasBeenSavedByUser(true);
|
|
4604
|
+
}
|
|
4261
4605
|
// Always navigate to the next section
|
|
4262
4606
|
onSectionSaveSuccess?.(sectionIndex);
|
|
4263
4607
|
}, [store, onSectionSave, onSectionSaveSuccess, sectionIndex, originalSection, schemaData, contextSchemaData, namespace, hasSupportingDocuments, dbSectionId, sectionRegisterId, dispatch, buildSectionSnapshot, sectionId, onSectionDirtyChange, mode, isDraft]);
|
|
4264
4608
|
// Handle cancel button click
|
|
4265
4609
|
const handleCancel = () => {
|
|
4266
|
-
|
|
4267
|
-
// Use original section (without namespace) for collecting widgets
|
|
4268
|
-
const sectionWidgets = collectWidgets(originalSection.panels);
|
|
4269
|
-
const oldSchemaData = schemaData || contextSchemaData;
|
|
4270
|
-
const currentStoreValues = store.getState().widget.values;
|
|
4271
|
-
let newStoreValues = currentStoreValues;
|
|
4272
|
-
sectionWidgets.forEach(widget => {
|
|
4273
|
-
const originalWidgetId = widget['widget-id'];
|
|
4274
|
-
// If namespace was used, we need to use namespaced widget ID and data path
|
|
4275
|
-
const namespacedWidgetId = namespace ? `${namespace}__${originalWidgetId}` : originalWidgetId;
|
|
4276
|
-
const widgetId = namespacedWidgetId;
|
|
4277
|
-
const originalDataPath = widget['widget-data-path'];
|
|
4278
|
-
// If namespace was used, data path in store is namespaced, but we read from original schema using original path
|
|
4279
|
-
const storeDataPath = namespace && originalDataPath
|
|
4280
|
-
? (typeof originalDataPath === 'string'
|
|
4281
|
-
? `${namespace}.${originalDataPath}`
|
|
4282
|
-
: Object.fromEntries(Object.entries(originalDataPath).map(([key, path]) => [key, `${namespace}.${path}`])))
|
|
4283
|
-
: originalDataPath;
|
|
4284
|
-
if (widgetId && originalDataPath) {
|
|
4285
|
-
// Handle multi-path (object) or single path (string)
|
|
4286
|
-
// Read from original schema data using original paths
|
|
4287
|
-
let oldValue;
|
|
4288
|
-
if (typeof originalDataPath === 'object') {
|
|
4289
|
-
// Multi-path: get values for each path
|
|
4290
|
-
oldValue = {};
|
|
4291
|
-
Object.entries(originalDataPath).forEach(([key, path]) => {
|
|
4292
|
-
if (typeof path === 'string') {
|
|
4293
|
-
oldValue[key] = getValueByPath(oldSchemaData, path);
|
|
4294
|
-
}
|
|
4295
|
-
});
|
|
4296
|
-
}
|
|
4297
|
-
else if (typeof originalDataPath === 'string') {
|
|
4298
|
-
oldValue = getValueByPath(oldSchemaData, originalDataPath);
|
|
4299
|
-
}
|
|
4300
|
-
// Set in store using namespaced data path (if namespace was used)
|
|
4301
|
-
if (oldValue !== undefined) {
|
|
4302
|
-
newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
|
|
4303
|
-
}
|
|
4304
|
-
}
|
|
4305
|
-
});
|
|
4306
|
-
// Also revert supporting documents if any
|
|
4307
|
-
if (hasSupportingDocuments) {
|
|
4308
|
-
// Use original section's supporting documents to get original data paths
|
|
4309
|
-
const originalSupportingDocuments = originalSection['section-supporting-documents'] || [];
|
|
4310
|
-
originalSupportingDocuments.forEach((doc, index) => {
|
|
4311
|
-
const widgetId = `supporting-doc-${sectionId}-${index}`;
|
|
4312
|
-
const originalDataPath = doc['document-data-path'];
|
|
4313
|
-
// If namespace was used, data path in store is namespaced
|
|
4314
|
-
const storeDataPath = namespace && originalDataPath
|
|
4315
|
-
? `${namespace}.${originalDataPath}`
|
|
4316
|
-
: originalDataPath;
|
|
4317
|
-
const oldValue = getValueByPath(oldSchemaData, originalDataPath);
|
|
4318
|
-
newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
|
|
4319
|
-
});
|
|
4320
|
-
}
|
|
4321
|
-
if (newStoreValues !== currentStoreValues) {
|
|
4322
|
-
dispatch(setValues(newStoreValues));
|
|
4323
|
-
}
|
|
4610
|
+
revertToOriginalValues();
|
|
4324
4611
|
setIsEditMode(false);
|
|
4325
4612
|
onEditModeChange?.(originalSectionId, false);
|
|
4326
4613
|
};
|
|
@@ -4371,20 +4658,27 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4371
4658
|
white-space: nowrap !important;
|
|
4372
4659
|
}
|
|
4373
4660
|
/* Readonly: prevent flex row from overflowing panel */
|
|
4374
|
-
|
|
4661
|
+
${readonlyValueRowRootsCss} {
|
|
4375
4662
|
min-width: 0 !important;
|
|
4376
4663
|
overflow: hidden !important;
|
|
4377
4664
|
}
|
|
4378
|
-
|
|
4665
|
+
${readonlyValueRowFlex1Css} {
|
|
4379
4666
|
min-width: 0 !important;
|
|
4380
4667
|
overflow: hidden !important;
|
|
4381
4668
|
}
|
|
4382
|
-
/* Readonly value
|
|
4383
|
-
|
|
4669
|
+
/* Readonly value: single-line ellipsis; full value via title on the value node */
|
|
4670
|
+
${readonlySingleLineValueTextCss} {
|
|
4384
4671
|
overflow: hidden;
|
|
4385
4672
|
text-overflow: ellipsis;
|
|
4386
4673
|
white-space: nowrap;
|
|
4387
4674
|
}
|
|
4675
|
+
/* Readonly textarea: break unbroken long tokens; title on pre keeps full text on hover */
|
|
4676
|
+
.${sectionClassId} .TextAreaDisplayWidget > .flex-1 > pre {
|
|
4677
|
+
min-width: 0;
|
|
4678
|
+
max-width: 100%;
|
|
4679
|
+
overflow-wrap: anywhere;
|
|
4680
|
+
word-break: break-word;
|
|
4681
|
+
}
|
|
4388
4682
|
|
|
4389
4683
|
/* Only apply fixed height when in edit mode */
|
|
4390
4684
|
.${sectionClassId}[data-edit-mode="true"] {
|
|
@@ -4508,6 +4802,10 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4508
4802
|
gap: 0.5rem;
|
|
4509
4803
|
}
|
|
4510
4804
|
|
|
4805
|
+
|
|
4806
|
+
|
|
4807
|
+
|
|
4808
|
+
|
|
4511
4809
|
/* IntakeForm accordion */
|
|
4512
4810
|
.${sectionClassId}.intake-form-accordion-item {
|
|
4513
4811
|
border-color: var(--owt-color-border-light, #E4E4E4);
|
|
@@ -4522,13 +4820,17 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4522
4820
|
.${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header h2 {
|
|
4523
4821
|
color: var(--owt-color-primary-dark, #F07B1A);
|
|
4524
4822
|
}
|
|
4525
|
-
|
|
4823
|
+
/* Hover / focus only shown when the header is actually interactive (standalone mode) */
|
|
4824
|
+
.${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header[data-interactive="true"]:hover {
|
|
4526
4825
|
opacity: 0.85;
|
|
4527
4826
|
}
|
|
4528
|
-
.${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header:focus-visible {
|
|
4827
|
+
.${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header[data-interactive="true"]:focus-visible {
|
|
4529
4828
|
outline: 2px solid var(--owt-color-primary, #F5BB1A);
|
|
4530
4829
|
outline-offset: 2px;
|
|
4531
4830
|
}
|
|
4831
|
+
.${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header[data-interactive="false"]:focus-visible {
|
|
4832
|
+
outline: none;
|
|
4833
|
+
}
|
|
4532
4834
|
.${sectionClassId}.intake-form-accordion-item .intake-form-accordion-content {
|
|
4533
4835
|
padding-top: 8px;
|
|
4534
4836
|
padding-bottom: 0px;
|
|
@@ -4571,7 +4873,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4571
4873
|
}),
|
|
4572
4874
|
}, children: mode === 'IntakeForm' ? (
|
|
4573
4875
|
/* IntakeForm: accordion layout - header always visible, content only when expanded */
|
|
4574
|
-
jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsxs("button", { type: "button", id: `intake-form-accordion-header-${sectionId}`, className: "intake-form-accordion-header", onClick: handleAccordionToggle, "aria-expanded": isExpanded, "aria-controls": isExpanded ? `intake-form-accordion-content-${sectionId}` : undefined, style: {
|
|
4876
|
+
jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsxs("button", { type: "button", id: `intake-form-accordion-header-${sectionId}`, className: "intake-form-accordion-header", onClick: handleAccordionToggle, "aria-expanded": isExpanded, "aria-controls": isExpanded ? `intake-form-accordion-content-${sectionId}` : undefined, "data-interactive": sectionIndex === undefined || isAccessible ? 'true' : 'false', style: {
|
|
4575
4877
|
width: '100%',
|
|
4576
4878
|
display: 'flex',
|
|
4577
4879
|
alignItems: 'flex-start',
|
|
@@ -4581,7 +4883,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4581
4883
|
marginBottom: 0,
|
|
4582
4884
|
background: 'none',
|
|
4583
4885
|
border: 'none',
|
|
4584
|
-
cursor: 'pointer',
|
|
4886
|
+
cursor: sectionIndex === undefined || isAccessible ? 'pointer' : 'default',
|
|
4585
4887
|
textAlign: 'left',
|
|
4586
4888
|
fontFamily: 'Roboto, sans-serif',
|
|
4587
4889
|
}, children: [jsxRuntimeExports.jsxs("div", { style: { flex: 1, display: 'flex', alignItems: 'center', gap: '12px', minWidth: 0 }, children: [jsxRuntimeExports.jsx("h2", { className: "text-xl font-semibold", style: { margin: 0 }, children: sectionToRender['section-title']
|
|
@@ -4712,7 +5014,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4712
5014
|
}, children: crViewData.approvedDate }))] })] })] })), mode === 'RegistryView' && !hideEditButton && (jsxRuntimeExports.jsx("hr", { className: "w-full", style: { height: '1px', marginTop: !isEditMode ? '10px' : 0, marginBottom: '14px', border: 'none', backgroundColor: 'var(--owt-color-border, #C4C4C4)' } })), mode === 'RegistryView' && !isEditMode && !hideEditButton && (jsxRuntimeExports.jsx("div", { className: "flex justify-center items-center", style: { marginBottom: '20px' }, children: jsxRuntimeExports.jsxs("button", { onClick: handleEdit, className: "font-normal inline-flex items-center gap-2 bg-transparent border-0 p-0 cursor-pointer hover:opacity-80", style: {
|
|
4713
5015
|
fontFamily: 'Roboto, sans-serif',
|
|
4714
5016
|
fontSize: '16px',
|
|
4715
|
-
color: 'var(--owt-color-text-muted, #727474)'
|
|
5017
|
+
color: 'var(--owt-color-text-muted, #727474)',
|
|
4716
5018
|
}, children: [translate('common.editDetails') || 'Edit Details', jsxRuntimeExports.jsx("img", { src: img$8, alt: "right-arrow", className: "w-3.5 h-3.5 brightness-0 opacity-50" })] }) }))] })] })) })] }));
|
|
4717
5019
|
};
|
|
4718
5020
|
|
|
@@ -4729,10 +5031,7 @@ function buildSectionChanges(section, storeValues, namespace, options) {
|
|
|
4729
5031
|
const widgetPath = widget['widget-data-path'];
|
|
4730
5032
|
if (!widgetPath)
|
|
4731
5033
|
return;
|
|
4732
|
-
|
|
4733
|
-
if (widgetType === 'table' ||
|
|
4734
|
-
widgetType === 'simple-table' ||
|
|
4735
|
-
widget.widget === 'table') {
|
|
5034
|
+
if (isTableLikeWidget(widget)) {
|
|
4736
5035
|
hasTable = true;
|
|
4737
5036
|
}
|
|
4738
5037
|
if (typeof widgetPath === 'object') {
|
|
@@ -4770,8 +5069,7 @@ function buildSectionChanges(section, storeValues, namespace, options) {
|
|
|
4770
5069
|
];
|
|
4771
5070
|
}
|
|
4772
5071
|
else {
|
|
4773
|
-
|
|
4774
|
-
records = tableEntry ? tableEntry[1] : [];
|
|
5072
|
+
records = extractTableRecordsFromSnapshot(snapshot, sectionWidgets);
|
|
4775
5073
|
}
|
|
4776
5074
|
const files = [];
|
|
4777
5075
|
const supportingDocs = section['section-supporting-documents'] || [];
|
|
@@ -4797,7 +5095,9 @@ const hasTableWidget = (panels) => {
|
|
|
4797
5095
|
// Check widgets in this panel
|
|
4798
5096
|
if (panel.widgets) {
|
|
4799
5097
|
for (const widget of panel.widgets) {
|
|
4800
|
-
if (widget.widget === 'table' ||
|
|
5098
|
+
if (widget.widget === 'table' ||
|
|
5099
|
+
widget.widget === 'dialog-table' ||
|
|
5100
|
+
widget['widget-type'] === 'table') {
|
|
4801
5101
|
return true;
|
|
4802
5102
|
}
|
|
4803
5103
|
}
|
|
@@ -4819,7 +5119,9 @@ const getTableWidgetColumnSpan = (panels) => {
|
|
|
4819
5119
|
// Check widgets in this panel
|
|
4820
5120
|
if (panel.widgets) {
|
|
4821
5121
|
for (const widget of panel.widgets) {
|
|
4822
|
-
if (widget.widget === 'table' ||
|
|
5122
|
+
if (widget.widget === 'table' ||
|
|
5123
|
+
widget.widget === 'dialog-table' ||
|
|
5124
|
+
widget['widget-type'] === 'table') {
|
|
4823
5125
|
// Return the widget's column span if specified, otherwise null
|
|
4824
5126
|
return widget['widget-column-span'] || null;
|
|
4825
5127
|
}
|
|
@@ -4879,6 +5181,11 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
|
|
|
4879
5181
|
const dataSourceRequestHandler = propDataSourceRequestHandler || contextDataSourceRequestHandler;
|
|
4880
5182
|
// IntakeForm mode: accordion state - which section is expanded (null = none; first expanded by default)
|
|
4881
5183
|
const [expandedSectionIndex, setExpandedSectionIndex] = useState(0);
|
|
5184
|
+
// IntakeForm mode: high-water mark of the furthest section the user has clicked Next on.
|
|
5185
|
+
// A section at index i is accessible when i <= maxVisitedIndex + 1
|
|
5186
|
+
// (i.e. every visited section plus the one immediately after it).
|
|
5187
|
+
// Starts at -1 so only section 0 is accessible before any Next is clicked.
|
|
5188
|
+
const [maxVisitedIndex, setMaxVisitedIndex] = useState(-1);
|
|
4882
5189
|
// RegistryView: track which section is currently in edit mode (by section-id); null = none
|
|
4883
5190
|
const [editingSectionId, setEditingSectionId] = useState(null);
|
|
4884
5191
|
const handleEditModeChange = useCallback((sectionId, editing) => {
|
|
@@ -4886,6 +5193,13 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
|
|
|
4886
5193
|
}, []);
|
|
4887
5194
|
const safeSections = sections ?? [];
|
|
4888
5195
|
const prevSectionsLengthRef = useRef(safeSections.length);
|
|
5196
|
+
// Stable ref for namespace so formHandle useMemo doesn't depend on the (possibly inline) function identity
|
|
5197
|
+
const namespaceRef = useRef(namespace);
|
|
5198
|
+
namespaceRef.current = namespace;
|
|
5199
|
+
// Stable refs so formHandle closure can access current mode and accordion setter without stale captures
|
|
5200
|
+
const modeRef = useRef(mode);
|
|
5201
|
+
modeRef.current = mode;
|
|
5202
|
+
const setExpandedSectionIndexRef = useRef(setExpandedSectionIndex);
|
|
4889
5203
|
// Track dirty (unsaved changes) per section for form handle validation
|
|
4890
5204
|
const sectionDirtyMapRef = useRef({});
|
|
4891
5205
|
const handleSectionDirtyChange = useCallback((sectionId, isDirty) => {
|
|
@@ -4896,8 +5210,9 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
|
|
|
4896
5210
|
const handleExpandSection = useCallback((index) => {
|
|
4897
5211
|
setExpandedSectionIndex(prev => (prev === index ? null : index));
|
|
4898
5212
|
}, []);
|
|
4899
|
-
// IntakeForm mode: called after section save - collapse current, expand next
|
|
5213
|
+
// IntakeForm mode: called after section save - advance high-water mark, collapse current, expand next
|
|
4900
5214
|
const handleSectionSaveSuccess = useCallback((index) => {
|
|
5215
|
+
setMaxVisitedIndex(prev => Math.max(prev, index));
|
|
4901
5216
|
if (index + 1 < safeSections.length) {
|
|
4902
5217
|
setExpandedSectionIndex(index + 1);
|
|
4903
5218
|
}
|
|
@@ -4931,11 +5246,14 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
|
|
|
4931
5246
|
// Form handle for onFormReady - allows host to validate and get all section data from its own Submit button
|
|
4932
5247
|
const formHandle = useMemo(() => {
|
|
4933
5248
|
const getValues = () => store.getState().widget?.values || {};
|
|
4934
|
-
const getNamespace = (section, index) =>
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
5249
|
+
const getNamespace = (section, index) => {
|
|
5250
|
+
const ns = namespaceRef.current;
|
|
5251
|
+
return ns
|
|
5252
|
+
? typeof ns === 'string'
|
|
5253
|
+
? ns
|
|
5254
|
+
: ns(section['section-id'], index)
|
|
5255
|
+
: undefined;
|
|
5256
|
+
};
|
|
4939
5257
|
const checkNoUnsavedChanges = () => {
|
|
4940
5258
|
const hasDirty = Object.values(sectionDirtyMapRef.current).some(Boolean);
|
|
4941
5259
|
if (hasDirty) {
|
|
@@ -4944,33 +5262,52 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
|
|
|
4944
5262
|
};
|
|
4945
5263
|
return {
|
|
4946
5264
|
validate: async () => {
|
|
4947
|
-
|
|
5265
|
+
if (modeRef.current !== 'IntakeForm')
|
|
5266
|
+
checkNoUnsavedChanges();
|
|
4948
5267
|
const values = getValues();
|
|
4949
5268
|
let allValid = true;
|
|
5269
|
+
let firstInvalidIndex = null;
|
|
4950
5270
|
for (let i = 0; i < safeSections.length; i++) {
|
|
4951
5271
|
const section = safeSections[i];
|
|
4952
5272
|
const ns = getNamespace(section, i);
|
|
4953
5273
|
const sectionToValidate = ns ? namespaceSectionConfig(section, ns) : section;
|
|
4954
5274
|
const valid = sectionValidate(sectionToValidate, values, dispatch);
|
|
4955
|
-
if (!valid)
|
|
5275
|
+
if (!valid) {
|
|
5276
|
+
if (firstInvalidIndex === null)
|
|
5277
|
+
firstInvalidIndex = i;
|
|
4956
5278
|
allValid = false;
|
|
5279
|
+
}
|
|
5280
|
+
}
|
|
5281
|
+
if (!allValid && modeRef.current === 'IntakeForm' && firstInvalidIndex !== null) {
|
|
5282
|
+
setExpandedSectionIndexRef.current(firstInvalidIndex);
|
|
4957
5283
|
}
|
|
4958
5284
|
return allValid;
|
|
4959
5285
|
},
|
|
4960
5286
|
getFormData: () => getValues(),
|
|
4961
5287
|
validateAndGetData: async () => {
|
|
4962
|
-
|
|
5288
|
+
if (modeRef.current !== 'IntakeForm')
|
|
5289
|
+
checkNoUnsavedChanges();
|
|
4963
5290
|
const values = getValues();
|
|
4964
5291
|
const results = [];
|
|
5292
|
+
let firstInvalidIndex = null;
|
|
4965
5293
|
for (let i = 0; i < safeSections.length; i++) {
|
|
4966
5294
|
const section = safeSections[i];
|
|
4967
5295
|
const ns = getNamespace(section, i);
|
|
4968
5296
|
const sectionToValidate = ns ? namespaceSectionConfig(section, ns) : section;
|
|
4969
5297
|
const valid = sectionValidate(sectionToValidate, values, dispatch);
|
|
4970
5298
|
if (!valid) {
|
|
4971
|
-
|
|
5299
|
+
if (firstInvalidIndex === null)
|
|
5300
|
+
firstInvalidIndex = i;
|
|
4972
5301
|
}
|
|
4973
|
-
|
|
5302
|
+
else {
|
|
5303
|
+
results.push(buildSectionChanges(section, values, ns));
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
if (firstInvalidIndex !== null) {
|
|
5307
|
+
if (modeRef.current === 'IntakeForm') {
|
|
5308
|
+
setExpandedSectionIndexRef.current(firstInvalidIndex);
|
|
5309
|
+
}
|
|
5310
|
+
throw new Error('Validation failed. Please fix the errors and try again.');
|
|
4974
5311
|
}
|
|
4975
5312
|
return results;
|
|
4976
5313
|
},
|
|
@@ -4985,7 +5322,7 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
|
|
|
4985
5322
|
return results;
|
|
4986
5323
|
},
|
|
4987
5324
|
};
|
|
4988
|
-
}, [store, dispatch, safeSections
|
|
5325
|
+
}, [store, dispatch, safeSections]);
|
|
4989
5326
|
// Call onFormReady when form is ready (sections loaded)
|
|
4990
5327
|
useEffect(() => {
|
|
4991
5328
|
if (onFormReady && safeSections.length > 0) {
|
|
@@ -5069,6 +5406,8 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
|
|
|
5069
5406
|
onSectionSaveSuccess: handleSectionSaveSuccess,
|
|
5070
5407
|
onPreviousSection: handlePreviousSection,
|
|
5071
5408
|
isDraft,
|
|
5409
|
+
// Accessible = every visited section + the one immediately after
|
|
5410
|
+
isAccessible: index <= maxVisitedIndex + 1,
|
|
5072
5411
|
}
|
|
5073
5412
|
: {};
|
|
5074
5413
|
// RegistryView: single-edit coordination props
|
|
@@ -8269,10 +8608,10 @@ const DisplayWidget = ({ config }) => {
|
|
|
8269
8608
|
const label = translateConfig(widgetConfig['widget-label']);
|
|
8270
8609
|
// If no label, render as paragraph text
|
|
8271
8610
|
if (!label || label.trim() === '') {
|
|
8272
|
-
return (jsxRuntimeExports.jsx("div", { className: "mb-3 text-base text-gray-700", title: String(displayValue ?? ''), children: displayValue }));
|
|
8611
|
+
return (jsxRuntimeExports.jsx("div", { className: "DisplayFieldWidget mb-3 min-w-0 w-full overflow-hidden text-ellipsis whitespace-nowrap text-base text-gray-700", title: String(displayValue ?? ''), children: displayValue }));
|
|
8273
8612
|
}
|
|
8274
|
-
// With label, render as key-value pair
|
|
8275
|
-
return (jsxRuntimeExports.jsxs("div", { className: "mb-[10px] flex flex-col sm:flex-row sm:items-start", children: [jsxRuntimeExports.jsxs("div", { className: "text-base text-gray-600 font-medium md:min-w-[120px] sm:pr-4 mb-1 sm:mb-0", style: { fontFamily: 'Roboto, sans-serif' }, title: label, children: [label, ":"] }), jsxRuntimeExports.jsx("div", { className: "flex-1 text-base text-gray-900 font-medium", title: String(displayValue ?? ''), children: displayValue })] }));
|
|
8613
|
+
// With label, render as key-value pair (structure matches other readonly widgets for SectionRenderer ellipsis)
|
|
8614
|
+
return (jsxRuntimeExports.jsxs("div", { className: "mb-[10px] DisplayFieldWidget flex flex-col sm:flex-row sm:items-start", children: [jsxRuntimeExports.jsxs("div", { className: "text-base text-gray-600 font-medium md:min-w-[120px] sm:pr-4 mb-1 sm:mb-0", style: { fontFamily: 'Roboto, sans-serif' }, title: label, children: [label, ":"] }), jsxRuntimeExports.jsx("div", { className: "flex-1 min-w-0", children: jsxRuntimeExports.jsx("div", { className: "text-base text-gray-900 font-medium", title: String(displayValue ?? ''), children: displayValue }) })] }));
|
|
8276
8615
|
};
|
|
8277
8616
|
|
|
8278
8617
|
const TableCellSelect = ({ config, value, onValueChange }) => {
|
|
@@ -8286,7 +8625,7 @@ const TableCellSelect = ({ config, value, onValueChange }) => {
|
|
|
8286
8625
|
backgroundColor: isReadonly || loading ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
|
|
8287
8626
|
}, children: [jsxRuntimeExports.jsx("option", { value: "", children: translate('common.select') || 'Select' }), dataSourceOptions.map((option) => (jsxRuntimeExports.jsx("option", { value: option.value, children: option.label }, option.value)))] }));
|
|
8288
8627
|
};
|
|
8289
|
-
const SelectDisplayValue = ({ config, value }) => {
|
|
8628
|
+
const SelectDisplayValue$1 = ({ config, value }) => {
|
|
8290
8629
|
const { dataSourceOptions, loading } = useBaseWidget({ config });
|
|
8291
8630
|
if (loading) {
|
|
8292
8631
|
return jsxRuntimeExports.jsx("span", { children: "-" });
|
|
@@ -8780,7 +9119,7 @@ const TableWidget = ({ config }) => {
|
|
|
8780
9119
|
'widget-readonly': true,
|
|
8781
9120
|
'widget-data-path': undefined,
|
|
8782
9121
|
};
|
|
8783
|
-
return (jsxRuntimeExports.jsx("div", { className: "text-sm", style: getCellStyle(), children: jsxRuntimeExports.jsx(SelectDisplayValue, { config: cellConfig, value: cellValue }) }));
|
|
9122
|
+
return (jsxRuntimeExports.jsx("div", { className: "text-sm", style: getCellStyle(), children: jsxRuntimeExports.jsx(SelectDisplayValue$1, { config: cellConfig, value: cellValue }) }));
|
|
8784
9123
|
}
|
|
8785
9124
|
return (jsxRuntimeExports.jsx("div", { className: "text-sm", style: getCellStyle(), children: displayValue }));
|
|
8786
9125
|
}
|
|
@@ -8919,34 +9258,293 @@ const TableWidget = ({ config }) => {
|
|
|
8919
9258
|
}, children: translate('common.cancel') || 'Cancel' })] }) })] }))] })] }) }), touched && error.length > 0 && (jsxRuntimeExports.jsx("p", { className: "text-sm mt-1", style: { color: 'var(--owt-widget-error-color, #B91C1C)' }, children: error[0] }))] })] }));
|
|
8920
9259
|
};
|
|
8921
9260
|
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
const {
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
9261
|
+
// Display select value label in view mode
|
|
9262
|
+
const SelectDisplayValue = ({ config, value }) => {
|
|
9263
|
+
const { dataSourceOptions, loading } = useBaseWidget({ config });
|
|
9264
|
+
if (loading)
|
|
9265
|
+
return jsxRuntimeExports.jsx("span", { children: "-" });
|
|
9266
|
+
if (value === null || value === undefined || value === '')
|
|
9267
|
+
return jsxRuntimeExports.jsx("span", { children: "-" });
|
|
9268
|
+
const selectedOption = dataSourceOptions.find((option) => option.value === value);
|
|
9269
|
+
return jsxRuntimeExports.jsx("span", { children: selectedOption ? selectedOption.label : String(value) });
|
|
9270
|
+
};
|
|
9271
|
+
/**
|
|
9272
|
+
* Dialog table widget:
|
|
9273
|
+
* - Table displays a subset of columns (n out of x)
|
|
9274
|
+
* - Add/Edit happens in a modal dialog that shows ALL columns as a form
|
|
9275
|
+
*
|
|
9276
|
+
* Usage in schema:
|
|
9277
|
+
* {
|
|
9278
|
+
* "widget": "dialog-table",
|
|
9279
|
+
* "widget-type": "table",
|
|
9280
|
+
* "widget-label": "Household Members",
|
|
9281
|
+
* "widget-id": "householdMembers",
|
|
9282
|
+
* "widget-data-path": "household.members",
|
|
9283
|
+
* "widget-data-columns": [ ...all columns... ],
|
|
9284
|
+
* "widget-data-visible-columns": ["firstName", "lastName", "dob"], // optional override; default = by column flag
|
|
9285
|
+
* // Per-column control (recommended): set "column-visible-in-table": false to hide it in the table
|
|
9286
|
+
* "widget-data-operations": { "add": true, "edit": true, "remove": true }
|
|
9287
|
+
* }
|
|
9288
|
+
*/
|
|
9289
|
+
const DialogTableWidget = ({ config }) => {
|
|
9290
|
+
const { value, error, touched, isEnabled, onChange, config: widgetConfig } = useBaseWidget({ config });
|
|
9291
|
+
const { translate, translateConfig } = useWidgetTranslation();
|
|
9292
|
+
const dispatch = useDispatch();
|
|
9293
|
+
const storeValues = useSelector((state) => state.widget?.values ?? {});
|
|
9294
|
+
const rows = Array.isArray(value) ? value : [];
|
|
9295
|
+
const columns = widgetConfig['widget-data-columns'] || [];
|
|
9296
|
+
const operations = widgetConfig['widget-data-operations'] || {};
|
|
9297
|
+
const isReadonly = widgetConfig['widget-readonly'] || false;
|
|
9298
|
+
const visibleColumnKeys = widgetConfig['widget-data-visible-columns'];
|
|
9299
|
+
const visibleColumns = useMemo(() => {
|
|
9300
|
+
// 1) If explicit list provided, it wins
|
|
9301
|
+
if (Array.isArray(visibleColumnKeys) && visibleColumnKeys.length > 0) {
|
|
9302
|
+
const keySet = new Set(visibleColumnKeys);
|
|
9303
|
+
return columns.filter((c) => keySet.has(c['column-key']));
|
|
9304
|
+
}
|
|
9305
|
+
// 2) Otherwise decide per column (default = visible)
|
|
9306
|
+
return columns.filter((c) => c['column-visible-in-table'] !== false);
|
|
9307
|
+
}, [columns, visibleColumnKeys]);
|
|
9308
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
9309
|
+
const [dialogMode, setDialogMode] = useState('add');
|
|
9310
|
+
const [activeRowIndex, setActiveRowIndex] = useState(null);
|
|
9311
|
+
const [formData, setFormData] = useState({});
|
|
9312
|
+
/** Unique per dialog open so Redux widget ids don't reuse stale values across rows/add sessions */
|
|
9313
|
+
const dialogSessionRef = useRef(0);
|
|
9314
|
+
const [dialogSessionId, setDialogSessionId] = useState(0);
|
|
9315
|
+
const addDialogTitle = translateConfig(widgetConfig['widget-data-dialog-title-add']) ||
|
|
9316
|
+
translate('table.addRecordDialog') ||
|
|
9317
|
+
'Add record';
|
|
9318
|
+
const editDialogTitle = translateConfig(widgetConfig['widget-data-dialog-title-edit']) ||
|
|
9319
|
+
translate('table.editRecordDialog') ||
|
|
9320
|
+
'Edit record';
|
|
9321
|
+
const buildEmptyRow = useCallback(() => {
|
|
9322
|
+
const emptyRow = {};
|
|
9323
|
+
columns.forEach((col) => {
|
|
9324
|
+
const key = col['column-key'];
|
|
9325
|
+
emptyRow[key] = col['widget-data-default'] ?? '';
|
|
9326
|
+
});
|
|
9327
|
+
return emptyRow;
|
|
9328
|
+
}, [columns]);
|
|
9329
|
+
const dialogFieldWidgetId = useCallback((columnKey) => `${widgetConfig['widget-id']}-dlg-${dialogSessionId}-${columnKey}`, [widgetConfig, dialogSessionId]);
|
|
9330
|
+
const resetDialogWidgets = useCallback((sessionId) => {
|
|
9331
|
+
if (sessionId <= 0)
|
|
9332
|
+
return;
|
|
9333
|
+
columns.forEach((col) => {
|
|
9334
|
+
const wid = `${widgetConfig['widget-id']}-dlg-${sessionId}-${col['column-key']}`;
|
|
9335
|
+
dispatch(resetWidget(wid));
|
|
9336
|
+
});
|
|
9337
|
+
}, [columns, widgetConfig, dispatch]);
|
|
9338
|
+
const beginDialogSession = useCallback(() => {
|
|
9339
|
+
dialogSessionRef.current += 1;
|
|
9340
|
+
const nextSession = dialogSessionRef.current;
|
|
9341
|
+
setDialogSessionId(nextSession);
|
|
9342
|
+
return nextSession;
|
|
9343
|
+
}, []);
|
|
9344
|
+
const openAddDialog = useCallback(() => {
|
|
9345
|
+
resetDialogWidgets(dialogSessionId);
|
|
9346
|
+
beginDialogSession();
|
|
9347
|
+
setDialogMode('add');
|
|
9348
|
+
setActiveRowIndex(null);
|
|
9349
|
+
setFormData(buildEmptyRow());
|
|
9350
|
+
setDialogOpen(true);
|
|
9351
|
+
}, [buildEmptyRow, beginDialogSession, resetDialogWidgets, dialogSessionId]);
|
|
9352
|
+
const openEditDialog = useCallback((rowIndex) => {
|
|
9353
|
+
resetDialogWidgets(dialogSessionId);
|
|
9354
|
+
beginDialogSession();
|
|
9355
|
+
const row = rows[rowIndex] || {};
|
|
9356
|
+
const nextFormData = buildEmptyRow();
|
|
9357
|
+
columns.forEach((col) => {
|
|
9358
|
+
const key = col['column-key'];
|
|
9359
|
+
if (row[key] !== undefined)
|
|
9360
|
+
nextFormData[key] = row[key];
|
|
9361
|
+
});
|
|
9362
|
+
setDialogMode('edit');
|
|
9363
|
+
setActiveRowIndex(rowIndex);
|
|
9364
|
+
setFormData(nextFormData);
|
|
9365
|
+
setDialogOpen(true);
|
|
9366
|
+
}, [rows, columns, buildEmptyRow, resetDialogWidgets, dialogSessionId, beginDialogSession]);
|
|
9367
|
+
const closeDialog = useCallback(() => {
|
|
9368
|
+
const sessionToClear = dialogSessionId;
|
|
9369
|
+
setDialogOpen(false);
|
|
9370
|
+
setActiveRowIndex(null);
|
|
9371
|
+
setFormData({});
|
|
9372
|
+
resetDialogWidgets(sessionToClear);
|
|
9373
|
+
setDialogSessionId(0);
|
|
9374
|
+
}, [dialogSessionId, resetDialogWidgets]);
|
|
9375
|
+
const updateField = useCallback((columnKey, newValue) => {
|
|
9376
|
+
setFormData((prev) => ({ ...prev, [columnKey]: newValue }));
|
|
9377
|
+
}, []);
|
|
9378
|
+
const collectMergedRowPayload = useCallback(() => {
|
|
9379
|
+
const merged = { ...formData };
|
|
9380
|
+
columns.forEach((col) => {
|
|
9381
|
+
const k = col['column-key'];
|
|
9382
|
+
const wid = dialogFieldWidgetId(k);
|
|
9383
|
+
const fromStore = storeValues[wid];
|
|
9384
|
+
if (fromStore !== undefined)
|
|
9385
|
+
merged[k] = fromStore;
|
|
9386
|
+
});
|
|
9387
|
+
return merged;
|
|
9388
|
+
}, [formData, columns, storeValues, dialogFieldWidgetId]);
|
|
9389
|
+
const saveDialog = useCallback(() => {
|
|
9390
|
+
const payload = collectMergedRowPayload();
|
|
9391
|
+
if (dialogMode === 'add') {
|
|
9392
|
+
const savedRow = { ...payload, edit_action: 'ADD' };
|
|
9393
|
+
onChange([...rows, savedRow]);
|
|
9394
|
+
closeDialog();
|
|
9395
|
+
return;
|
|
9396
|
+
}
|
|
9397
|
+
if (dialogMode === 'edit' && activeRowIndex !== null) {
|
|
9398
|
+
const newRows = [...rows];
|
|
9399
|
+
const currentRow = newRows[activeRowIndex] || {};
|
|
9400
|
+
const wasDeleted = currentRow.edit_action === 'DELETE';
|
|
9401
|
+
const editAction = wasDeleted ? 'UPDATE' : (currentRow.edit_action ?? 'UPDATE');
|
|
9402
|
+
newRows[activeRowIndex] = { ...currentRow, ...payload, edit_action: editAction };
|
|
9403
|
+
onChange(newRows);
|
|
9404
|
+
closeDialog();
|
|
9405
|
+
}
|
|
9406
|
+
}, [collectMergedRowPayload, dialogMode, onChange, rows, closeDialog, activeRowIndex]);
|
|
9407
|
+
const deleteRow = useCallback((rowIndex) => {
|
|
9408
|
+
const newRows = rows.filter((_, i) => i !== rowIndex);
|
|
9409
|
+
onChange(newRows);
|
|
9410
|
+
}, [rows, onChange]);
|
|
9411
|
+
const getDisplayValue = useCallback((rowIndex, column) => {
|
|
9412
|
+
const key = column['column-key'];
|
|
9413
|
+
const cellValue = rows[rowIndex]?.[key];
|
|
9414
|
+
const widgetType = column.widget || 'text';
|
|
9415
|
+
if (cellValue === null || cellValue === undefined || cellValue === '')
|
|
9416
|
+
return '-';
|
|
9417
|
+
if (widgetType === 'select')
|
|
9418
|
+
return null; // handled by SelectDisplayValue
|
|
9419
|
+
if (column['widget-data-format'])
|
|
9420
|
+
return formatValue(cellValue, column['widget-data-format'], column.widget);
|
|
9421
|
+
return String(cellValue);
|
|
9422
|
+
}, [rows]);
|
|
9423
|
+
const tableWidgetId = `dialog-table-widget-${widgetConfig['widget-id']}`;
|
|
9424
|
+
const columnSpan = widgetConfig['widget-column-span'] || 2;
|
|
9425
|
+
const minWidth = columnSpan * 200;
|
|
9426
|
+
return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("style", { children: `
|
|
9427
|
+
.${tableWidgetId} {
|
|
9428
|
+
width: 100%;
|
|
9429
|
+
min-width: ${minWidth}px;
|
|
9430
|
+
}
|
|
9431
|
+
|
|
9432
|
+
.widget-container[data-widget-id="${widgetConfig['widget-id']}"] {
|
|
9433
|
+
min-width: ${minWidth}px;
|
|
9434
|
+
width: 100%;
|
|
9435
|
+
flex: none;
|
|
9436
|
+
}
|
|
9437
|
+
|
|
9438
|
+
.panel-horizontal .widget-container[data-widget-id="${widgetConfig['widget-id']}"],
|
|
9439
|
+
[data-panel-orientation="horizontal"] .widget-container[data-widget-id="${widgetConfig['widget-id']}"] {
|
|
9440
|
+
grid-column: span ${columnSpan};
|
|
9441
|
+
}
|
|
9442
|
+
` }), jsxRuntimeExports.jsxs("div", { className: `table-widget-container ${tableWidgetId}`, children: [operations.add && !isReadonly && isEnabled && (jsxRuntimeExports.jsx("div", { className: "flex justify-end mb-2", children: jsxRuntimeExports.jsx("button", { type: "button", onClick: openAddDialog, className: "px-3 py-1 text-sm disabled:opacity-50 disabled:cursor-not-allowed", style: {
|
|
9443
|
+
borderRadius: 'var(--owt-btn-border-radius, 10px)',
|
|
9444
|
+
border: '1px solid var(--owt-btn-primary-border, #F07B1A)',
|
|
9445
|
+
backgroundColor: 'var(--owt-color-primary, #F5BB1A)',
|
|
9446
|
+
color: 'var(--owt-color-bg, #FFFFFF)',
|
|
9447
|
+
}, children: translate('table.addRecord') || 'Add New Record' }) })), jsxRuntimeExports.jsx("div", { className: "overflow-x-auto border", style: {
|
|
9448
|
+
borderRadius: 'var(--owt-widget-table-border-radius, 15px)',
|
|
9449
|
+
borderColor: 'var(--owt-widget-table-border-color, #C4C4C4)',
|
|
9450
|
+
}, children: jsxRuntimeExports.jsxs("table", { className: "min-w-full", style: { borderCollapse: 'separate', borderSpacing: 0 }, children: [jsxRuntimeExports.jsx("thead", { style: { backgroundColor: 'var(--owt-widget-table-header-bg, #F6F6F6)' }, children: jsxRuntimeExports.jsxs("tr", { style: { borderBottom: '1px solid var(--owt-widget-table-row-divider, #E4E4E4)' }, children: [visibleColumns.map((col) => (jsxRuntimeExports.jsx("th", { className: "px-4 py-3 text-left text-xs font-medium uppercase tracking-wider", style: { color: 'var(--owt-widget-table-header-color, #727474)' }, children: translateConfig(col['widget-label']) }, col['column-key']))), ((operations.edit || operations.remove) && !isReadonly) && (jsxRuntimeExports.jsx("th", { className: "px-4 py-3 text-left text-xs font-medium uppercase tracking-wider", style: { color: 'var(--owt-widget-table-header-color, #727474)' }, children: translate('common.actions') || 'Actions' }))] }) }), jsxRuntimeExports.jsxs("tbody", { style: { backgroundColor: 'var(--owt-widget-table-body-bg, #FFFFFF)' }, children: [rows.length === 0 && (jsxRuntimeExports.jsx("tr", { children: jsxRuntimeExports.jsxs("td", { colSpan: visibleColumns.length + (((operations.edit || operations.remove) && !isReadonly) ? 1 : 0), className: "px-4 py-6 text-center text-sm", style: { color: 'var(--owt-widget-table-empty-color, #727474)' }, children: [translate('table.noData') || 'No records available.', operations.add && !isReadonly && ` ${translate('table.clickToAdd') || 'Click "Add New Record" to add one.'}`] }) })), rows.map((row, rowIndex) => (jsxRuntimeExports.jsxs("tr", { style: {
|
|
9451
|
+
borderBottom: '1px solid var(--owt-widget-table-row-divider, #E4E4E4)',
|
|
9452
|
+
backgroundColor: row?.edit_action === 'DELETE'
|
|
9453
|
+
? 'var(--owt-widget-table-deleted-row-bg, #FEE2E2)'
|
|
9454
|
+
: undefined,
|
|
9455
|
+
}, children: [visibleColumns.map((col) => {
|
|
9456
|
+
const key = col['column-key'];
|
|
9457
|
+
const widgetType = col.widget || 'text';
|
|
9458
|
+
const displayValue = getDisplayValue(rowIndex, col);
|
|
9459
|
+
if (widgetType === 'select' && displayValue === null) {
|
|
9460
|
+
const displayConfig = {
|
|
9461
|
+
...col,
|
|
9462
|
+
'widget-id': `${widgetConfig['widget-id']}-view-row-${rowIndex}-col-${key}`,
|
|
9463
|
+
'widget-label': '',
|
|
9464
|
+
'widget-readonly': true,
|
|
9465
|
+
'widget-data-path': undefined,
|
|
9466
|
+
};
|
|
9467
|
+
return (jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", children: jsxRuntimeExports.jsx("div", { className: "text-sm", children: jsxRuntimeExports.jsx(SelectDisplayValue, { config: displayConfig, value: row?.[key] }) }) }, key));
|
|
9468
|
+
}
|
|
9469
|
+
return (jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", children: jsxRuntimeExports.jsx("div", { className: "text-sm", children: displayValue }) }, key));
|
|
9470
|
+
}), ((operations.edit || operations.remove) && !isReadonly) && (jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", style: { minWidth: '120px' }, children: jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [operations.edit && (jsxRuntimeExports.jsx("button", { type: "button", onClick: () => openEditDialog(rowIndex), disabled: !isEnabled, className: "px-3 py-1 text-xs disabled:opacity-50 disabled:cursor-not-allowed", style: {
|
|
9471
|
+
borderRadius: 'var(--owt-btn-border-radius, 10px)',
|
|
9472
|
+
color: 'var(--owt-color-primary-dark, #F07B1A)',
|
|
9473
|
+
backgroundColor: 'transparent',
|
|
9474
|
+
border: 'none',
|
|
9475
|
+
}, children: translate('common.edit') || 'Edit' })), operations.remove && (jsxRuntimeExports.jsx("button", { type: "button", onClick: () => deleteRow(rowIndex), disabled: !isEnabled, className: "px-3 py-1 text-xs disabled:opacity-50 disabled:cursor-not-allowed", style: {
|
|
9476
|
+
borderRadius: 'var(--owt-btn-border-radius, 10px)',
|
|
9477
|
+
color: 'var(--owt-color-error, #B91C1C)',
|
|
9478
|
+
backgroundColor: 'transparent',
|
|
9479
|
+
border: 'none',
|
|
9480
|
+
}, children: translate('common.remove') || 'Delete' }))] }) }))] }, rowIndex)))] })] }) }), touched && error.length > 0 && (jsxRuntimeExports.jsx("p", { className: "text-sm mt-1", style: { color: 'var(--owt-widget-error-color, #B91C1C)' }, children: error[0] }))] }), dialogOpen && (jsxRuntimeExports.jsx("div", { className: "fixed inset-0 flex items-center justify-center z-50", style: { backgroundColor: 'rgba(0,0,0,0.5)' }, children: jsxRuntimeExports.jsxs("div", { className: "rounded-lg p-6 w-full mx-4", style: {
|
|
9481
|
+
maxWidth: '900px',
|
|
9482
|
+
backgroundColor: 'var(--owt-color-bg, #FFFFFF)',
|
|
9483
|
+
borderRadius: 'var(--owt-widget-card-border-radius, 20px)',
|
|
9484
|
+
}, children: [jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between gap-4 mb-4", children: [jsxRuntimeExports.jsx("h3", { className: "text-lg font-semibold", style: { color: 'var(--owt-color-text, #011627)' }, children: dialogMode === 'add' ? addDialogTitle : editDialogTitle }), jsxRuntimeExports.jsx("button", { type: "button", onClick: closeDialog, style: {
|
|
9485
|
+
border: 'none',
|
|
9486
|
+
background: 'transparent',
|
|
9487
|
+
color: 'var(--owt-color-text-muted, #727474)',
|
|
9488
|
+
cursor: 'pointer',
|
|
9489
|
+
fontSize: '20px',
|
|
9490
|
+
lineHeight: 1,
|
|
9491
|
+
}, "aria-label": "Close", children: "\u00D7" })] }), jsxRuntimeExports.jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4", style: { maxHeight: '70vh', overflow: 'auto' }, children: columns.map((col) => {
|
|
9492
|
+
const key = col['column-key'];
|
|
9493
|
+
const widgetType = col.widget || 'text';
|
|
9494
|
+
const cellWidgetId = dialogFieldWidgetId(key);
|
|
9495
|
+
const initialValue = formData[key] ?? col['widget-data-default'] ?? '';
|
|
9496
|
+
const fieldConfig = {
|
|
9497
|
+
...col,
|
|
9498
|
+
widget: widgetType,
|
|
9499
|
+
'widget-type': col['widget-type'] || 'input',
|
|
9500
|
+
'widget-id': cellWidgetId,
|
|
9501
|
+
'widget-label': col['widget-label'],
|
|
9502
|
+
'widget-readonly': isReadonly || col['widget-readonly'] === true,
|
|
9503
|
+
'widget-data-path': undefined,
|
|
9504
|
+
'widget-data-default': initialValue,
|
|
9505
|
+
};
|
|
9506
|
+
return (jsxRuntimeExports.jsx("div", { className: "min-w-0", children: jsxRuntimeExports.jsx(WidgetRenderer, { config: fieldConfig, schemaData: { [cellWidgetId]: initialValue }, onValueChange: (_widgetId, newValue) => updateField(key, newValue) }) }, `${dialogSessionId}-${key}`));
|
|
9507
|
+
}) }, `dialog-fields-${dialogSessionId}`), jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-3 mt-6", children: [jsxRuntimeExports.jsx("button", { type: "button", onClick: closeDialog, className: "px-4 py-2 text-sm font-medium", style: {
|
|
9508
|
+
borderRadius: 'var(--owt-btn-border-radius, 10px)',
|
|
9509
|
+
border: '1px solid var(--owt-btn-secondary-border, #C4C4C4)',
|
|
9510
|
+
backgroundColor: 'var(--owt-btn-secondary-bg, #FFFFFF)',
|
|
9511
|
+
color: 'var(--owt-btn-secondary-color, #011627)',
|
|
9512
|
+
}, children: translate('common.cancel') || 'Cancel' }), jsxRuntimeExports.jsx("button", { type: "button", onClick: saveDialog, disabled: isReadonly || !isEnabled, className: "px-4 py-2 text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed", style: {
|
|
9513
|
+
borderRadius: 'var(--owt-btn-border-radius, 10px)',
|
|
9514
|
+
border: '1px solid var(--owt-btn-primary-border, #F07B1A)',
|
|
9515
|
+
backgroundColor: 'var(--owt-color-primary, #F5BB1A)',
|
|
9516
|
+
color: 'var(--owt-color-bg, #FFFFFF)',
|
|
9517
|
+
}, children: translate('common.save') || 'Save' })] })] }) }))] }));
|
|
9518
|
+
};
|
|
9519
|
+
|
|
9520
|
+
const ProfileWidget = ({ config }) => {
|
|
9521
|
+
const { value, config: widgetConfig, getFieldValue, } = useBaseWidget({ config });
|
|
9522
|
+
const { translateConfig } = useWidgetTranslation();
|
|
9523
|
+
// Get schemaData from context as fallback
|
|
9524
|
+
const { schemaData } = useWidgetContext();
|
|
9525
|
+
// Get values from Redux store
|
|
9526
|
+
const values = useSelector((state) => state.widget.values);
|
|
9527
|
+
// Support two approaches for data paths:
|
|
9528
|
+
// 1. Multi-path binding via widget-data-path (object) - RECOMMENDED (Approach 2)
|
|
9529
|
+
// 2. Individual path properties (widget-image-path, widget-name-path, widget-id-path) - Fallback
|
|
9530
|
+
let imageUrl = null;
|
|
9531
|
+
let displayName = '';
|
|
9532
|
+
let idValue = '';
|
|
9533
|
+
const dataPath = widgetConfig['widget-data-path'];
|
|
9534
|
+
const imagePath = widgetConfig['widget-image-path'];
|
|
9535
|
+
const namePath = widgetConfig['widget-name-path'];
|
|
9536
|
+
const idPath = widgetConfig['widget-id-path'];
|
|
9537
|
+
// Prioritize multi-path data binding (Approach 2 - Recommended)
|
|
9538
|
+
if (dataPath && typeof dataPath === 'object') {
|
|
9539
|
+
// Multi-path data binding - preferred approach (Approach 2)
|
|
9540
|
+
// Always fetch each path individually using getFieldValue for reliability
|
|
9541
|
+
// The values in the dataPath object are the actual data paths to fetch
|
|
9542
|
+
const imagePathValue = dataPath.image || dataPath.photo || dataPath.avatar;
|
|
9543
|
+
const namePathValue = dataPath.name || dataPath.displayName;
|
|
9544
|
+
const idPathValue = dataPath.id || dataPath.identifier;
|
|
9545
|
+
// Helper function to search for a path within all top-level objects
|
|
9546
|
+
const findValueInNestedObjects = (path, searchIn) => {
|
|
9547
|
+
if (!searchIn)
|
|
8950
9548
|
return undefined;
|
|
8951
9549
|
// First try direct path (in case it's at root level)
|
|
8952
9550
|
let value = getValueByPath(searchIn, path);
|
|
@@ -9310,15 +9908,107 @@ const HeaderSectionWidget = ({ config }) => {
|
|
|
9310
9908
|
result = searchIn(schemaData);
|
|
9311
9909
|
return result;
|
|
9312
9910
|
}, [paths, values, schemaData]);
|
|
9313
|
-
const
|
|
9911
|
+
const imageVal = findValue('image');
|
|
9912
|
+
const imageUrlVal = findValue('imageUrl');
|
|
9913
|
+
const [previewUrl, setPreviewUrl] = useState(null);
|
|
9914
|
+
useEffect(() => {
|
|
9915
|
+
if (imageVal instanceof File) {
|
|
9916
|
+
const url = URL.createObjectURL(imageVal);
|
|
9917
|
+
setPreviewUrl(url);
|
|
9918
|
+
return () => URL.revokeObjectURL(url);
|
|
9919
|
+
}
|
|
9920
|
+
setPreviewUrl(null);
|
|
9921
|
+
}, [imageVal]);
|
|
9922
|
+
const displayImageUrl = previewUrl || (typeof imageUrlVal === 'string' && imageUrlVal ? imageUrlVal : null);
|
|
9314
9923
|
const displayName = findValue('name') || '';
|
|
9315
9924
|
const functionalId = findValue('functionalId') || '';
|
|
9316
9925
|
const statusValue = findValue('status') || '';
|
|
9317
9926
|
const statusReason = findValue('statusReason') || '';
|
|
9927
|
+
const completionScoreRaw = findValue('completionScore');
|
|
9928
|
+
const idealScoreRaw = findValue('idealScore');
|
|
9318
9929
|
const createdBy = findValue('createdBy') || '';
|
|
9319
9930
|
const createdAt = findValue('createdAt') || '';
|
|
9320
9931
|
const lastApprovedBy = findValue('lastApprovedBy') || '';
|
|
9321
9932
|
const lastApprovedAt = findValue('lastApprovedAt') || '';
|
|
9933
|
+
// ── Validation: status change requires reason ──────────────────
|
|
9934
|
+
// Behavior:
|
|
9935
|
+
// - When status changes away from its initial value, clear reason and require it.
|
|
9936
|
+
// - When status returns to initial value (or a parent "Cancel" restores it), restore initial reason.
|
|
9937
|
+
const initialStatusRef = useRef(null);
|
|
9938
|
+
const initialReasonRef = useRef(null);
|
|
9939
|
+
const prevStatusRef = useRef(null);
|
|
9940
|
+
const [showReasonRequired, setShowReasonRequired] = useState(false);
|
|
9941
|
+
useEffect(() => {
|
|
9942
|
+
// Capture initial status once when it becomes available.
|
|
9943
|
+
if (initialStatusRef.current === null) {
|
|
9944
|
+
const v = statusValue === undefined || statusValue === null ? '' : String(statusValue);
|
|
9945
|
+
initialStatusRef.current = v;
|
|
9946
|
+
}
|
|
9947
|
+
}, [statusValue]);
|
|
9948
|
+
useEffect(() => {
|
|
9949
|
+
// Capture initial reason once when it becomes available.
|
|
9950
|
+
if (initialReasonRef.current === null) {
|
|
9951
|
+
const v = statusReason === undefined || statusReason === null ? '' : String(statusReason);
|
|
9952
|
+
initialReasonRef.current = v;
|
|
9953
|
+
}
|
|
9954
|
+
}, [statusReason]);
|
|
9955
|
+
const isStatusChanged = useMemo(() => {
|
|
9956
|
+
const initial = initialStatusRef.current;
|
|
9957
|
+
if (initial === null)
|
|
9958
|
+
return false;
|
|
9959
|
+
return String(statusValue) !== initial;
|
|
9960
|
+
}, [statusValue]);
|
|
9961
|
+
const isReasonMissing = useMemo(() => {
|
|
9962
|
+
if (!isStatusChanged)
|
|
9963
|
+
return false;
|
|
9964
|
+
return String(statusReason || '').trim().length === 0;
|
|
9965
|
+
}, [isStatusChanged, statusReason]);
|
|
9966
|
+
useEffect(() => {
|
|
9967
|
+
// When status changes:
|
|
9968
|
+
// - If moved away from initial → clear reason.
|
|
9969
|
+
// - If returned to initial → restore initial reason.
|
|
9970
|
+
if (isReadonly)
|
|
9971
|
+
return;
|
|
9972
|
+
if (initialStatusRef.current === null)
|
|
9973
|
+
return;
|
|
9974
|
+
const currentStatus = String(statusValue || '');
|
|
9975
|
+
if (prevStatusRef.current === currentStatus)
|
|
9976
|
+
return;
|
|
9977
|
+
prevStatusRef.current = currentStatus;
|
|
9978
|
+
const initialStatus = initialStatusRef.current;
|
|
9979
|
+
const initialReason = initialReasonRef.current ?? '';
|
|
9980
|
+
if (currentStatus === initialStatus) {
|
|
9981
|
+
// Reverted / cancelled back to original
|
|
9982
|
+
if (String(statusReason || '') !== String(initialReason || '')) {
|
|
9983
|
+
updateFieldValue('statusReason', initialReason);
|
|
9984
|
+
}
|
|
9985
|
+
setShowReasonRequired(false);
|
|
9986
|
+
return;
|
|
9987
|
+
}
|
|
9988
|
+
// Status changed to a new value: clear reason (so user must re-enter)
|
|
9989
|
+
if (String(statusReason || '').trim().length > 0) {
|
|
9990
|
+
updateFieldValue('statusReason', '');
|
|
9991
|
+
}
|
|
9992
|
+
setShowReasonRequired(true);
|
|
9993
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
9994
|
+
}, [statusValue, isReadonly]);
|
|
9995
|
+
const score = useMemo(() => {
|
|
9996
|
+
const toNum = (v) => {
|
|
9997
|
+
if (v === null || v === undefined || String(v).trim() === '')
|
|
9998
|
+
return null;
|
|
9999
|
+
const n = typeof v === 'number' ? v : Number(String(v));
|
|
10000
|
+
return Number.isFinite(n) ? n : null;
|
|
10001
|
+
};
|
|
10002
|
+
const completion = toNum(completionScoreRaw);
|
|
10003
|
+
const ideal = toNum(idealScoreRaw);
|
|
10004
|
+
if (completion === null || ideal === null || ideal <= 0)
|
|
10005
|
+
return null;
|
|
10006
|
+
const ratio = completion / ideal;
|
|
10007
|
+
const percent = Math.max(0, Math.min(100, Math.round(ratio * 100)));
|
|
10008
|
+
const completionDisplay = Number.isInteger(completion) ? completion : Math.round(completion);
|
|
10009
|
+
const idealDisplay = Number.isInteger(ideal) ? ideal : Math.round(ideal);
|
|
10010
|
+
return { completion, ideal, completionDisplay, idealDisplay, percent };
|
|
10011
|
+
}, [completionScoreRaw, idealScoreRaw]);
|
|
9322
10012
|
// ── Format options ────────────────────────────────────────────
|
|
9323
10013
|
const format = (widgetConfig['widget-data-format'] || {});
|
|
9324
10014
|
const imageSize = format.imageSize || 120;
|
|
@@ -9343,18 +10033,20 @@ const HeaderSectionWidget = ({ config }) => {
|
|
|
9343
10033
|
return opt ? opt.label : String(statusValue);
|
|
9344
10034
|
}, [statusValue, statusOptions]);
|
|
9345
10035
|
const statusColor = statusColors[String(statusValue).toLowerCase()] || 'var(--owt-color-text-muted, #6B7280)';
|
|
10036
|
+
// ── Image edit helpers ───────────────────────────────────────
|
|
10037
|
+
const fileInputRef = useRef(null);
|
|
10038
|
+
const handleImageUpload = useCallback((e) => {
|
|
10039
|
+
const file = e.target.files?.[0];
|
|
10040
|
+
if (!file)
|
|
10041
|
+
return;
|
|
10042
|
+
updateFieldValue('image', file);
|
|
10043
|
+
e.target.value = '';
|
|
10044
|
+
}, [updateFieldValue]);
|
|
10045
|
+
const handleImageDelete = useCallback(() => {
|
|
10046
|
+
updateFieldValue('image', '');
|
|
10047
|
+
}, [updateFieldValue]);
|
|
9346
10048
|
// ── Scoped class for CSS isolation ────────────────────────────
|
|
9347
10049
|
const cls = `header-section-widget-${widgetConfig['widget-id']}`;
|
|
9348
|
-
// ── Indicator dot component ───────────────────────────────────
|
|
9349
|
-
const Dot = ({ color }) => (jsxRuntimeExports.jsx("span", { style: {
|
|
9350
|
-
display: 'inline-block',
|
|
9351
|
-
width: 8,
|
|
9352
|
-
height: 8,
|
|
9353
|
-
borderRadius: '50%',
|
|
9354
|
-
backgroundColor: color,
|
|
9355
|
-
flexShrink: 0,
|
|
9356
|
-
marginTop: 6,
|
|
9357
|
-
} }));
|
|
9358
10050
|
// ── RENDER ────────────────────────────────────────────────────
|
|
9359
10051
|
return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("style", { children: `
|
|
9360
10052
|
.${cls} {
|
|
@@ -9383,6 +10075,58 @@ const HeaderSectionWidget = ({ config }) => {
|
|
|
9383
10075
|
min-width: 220px;
|
|
9384
10076
|
}
|
|
9385
10077
|
|
|
10078
|
+
.${cls} .hdr-right-top {
|
|
10079
|
+
display: flex;
|
|
10080
|
+
align-items: flex-start;
|
|
10081
|
+
justify-content: space-between;
|
|
10082
|
+
gap: 14px;
|
|
10083
|
+
width: 100%;
|
|
10084
|
+
}
|
|
10085
|
+
|
|
10086
|
+
.${cls} .hdr-meta-col {
|
|
10087
|
+
display: flex;
|
|
10088
|
+
flex-direction: column;
|
|
10089
|
+
gap: 0.5rem;
|
|
10090
|
+
flex: 1 1 auto;
|
|
10091
|
+
min-width: 0;
|
|
10092
|
+
}
|
|
10093
|
+
|
|
10094
|
+
.${cls} .hdr-score-ring {
|
|
10095
|
+
--ring-size: 54px;
|
|
10096
|
+
--ring-thickness: 7px;
|
|
10097
|
+
--ring-color: var(--owt-color-primary-dark, #F07B1A);
|
|
10098
|
+
--ring-track: rgba(2, 6, 23, 0.10);
|
|
10099
|
+
width: var(--ring-size);
|
|
10100
|
+
height: var(--ring-size);
|
|
10101
|
+
border-radius: 50%;
|
|
10102
|
+
background: conic-gradient(
|
|
10103
|
+
var(--ring-color) calc(var(--pct) * 1%),
|
|
10104
|
+
var(--ring-track) 0
|
|
10105
|
+
);
|
|
10106
|
+
position: relative;
|
|
10107
|
+
flex: 0 0 auto;
|
|
10108
|
+
}
|
|
10109
|
+
|
|
10110
|
+
.${cls} .hdr-score-ring::before {
|
|
10111
|
+
content: "";
|
|
10112
|
+
position: absolute;
|
|
10113
|
+
inset: var(--ring-thickness);
|
|
10114
|
+
border-radius: 50%;
|
|
10115
|
+
background: var(--owt-color-bg, #FFFFFF);
|
|
10116
|
+
}
|
|
10117
|
+
|
|
10118
|
+
.${cls} .hdr-score-value {
|
|
10119
|
+
position: absolute;
|
|
10120
|
+
inset: 0;
|
|
10121
|
+
display: flex;
|
|
10122
|
+
align-items: center;
|
|
10123
|
+
justify-content: center;
|
|
10124
|
+
font-size: 20px;
|
|
10125
|
+
font-weight: 700;
|
|
10126
|
+
color: var(--owt-color-text, #011627);
|
|
10127
|
+
font-family: Roboto, sans-serif;
|
|
10128
|
+
}
|
|
10129
|
+
|
|
9386
10130
|
.${cls} .hdr-avatar {
|
|
9387
10131
|
width: ${imageSize}px;
|
|
9388
10132
|
height: ${imageSize}px;
|
|
@@ -9413,6 +10157,56 @@ const HeaderSectionWidget = ({ config }) => {
|
|
|
9413
10157
|
border-radius: 8px;
|
|
9414
10158
|
}
|
|
9415
10159
|
|
|
10160
|
+
.${cls} .hdr-avatar-wrapper {
|
|
10161
|
+
position: relative;
|
|
10162
|
+
width: ${imageSize}px;
|
|
10163
|
+
height: ${imageSize}px;
|
|
10164
|
+
flex-shrink: 0;
|
|
10165
|
+
}
|
|
10166
|
+
|
|
10167
|
+
.${cls} .hdr-avatar-overlay {
|
|
10168
|
+
position: absolute;
|
|
10169
|
+
inset: 0;
|
|
10170
|
+
border-radius: 8px;
|
|
10171
|
+
background: rgba(0, 0, 0, 0.55);
|
|
10172
|
+
display: flex;
|
|
10173
|
+
flex-direction: column;
|
|
10174
|
+
align-items: center;
|
|
10175
|
+
justify-content: center;
|
|
10176
|
+
gap: 6px;
|
|
10177
|
+
opacity: 0;
|
|
10178
|
+
transition: opacity 0.2s;
|
|
10179
|
+
}
|
|
10180
|
+
|
|
10181
|
+
.${cls} .hdr-avatar-wrapper:hover .hdr-avatar-overlay {
|
|
10182
|
+
opacity: 1;
|
|
10183
|
+
}
|
|
10184
|
+
|
|
10185
|
+
.${cls} .hdr-avatar-action {
|
|
10186
|
+
display: flex;
|
|
10187
|
+
align-items: center;
|
|
10188
|
+
gap: 5px;
|
|
10189
|
+
padding: 5px 14px;
|
|
10190
|
+
border: none;
|
|
10191
|
+
border-radius: 4px;
|
|
10192
|
+
background: rgba(255, 255, 255, 0.92);
|
|
10193
|
+
color: #374151;
|
|
10194
|
+
font-size: 0.7rem;
|
|
10195
|
+
font-weight: 500;
|
|
10196
|
+
cursor: pointer;
|
|
10197
|
+
font-family: Roboto, sans-serif;
|
|
10198
|
+
transition: background 0.15s;
|
|
10199
|
+
white-space: nowrap;
|
|
10200
|
+
}
|
|
10201
|
+
|
|
10202
|
+
.${cls} .hdr-avatar-action:hover {
|
|
10203
|
+
background: #fff;
|
|
10204
|
+
}
|
|
10205
|
+
|
|
10206
|
+
.${cls} .hdr-avatar-action--delete {
|
|
10207
|
+
color: #DC2626;
|
|
10208
|
+
}
|
|
10209
|
+
|
|
9416
10210
|
.${cls} .hdr-info {
|
|
9417
10211
|
display: flex;
|
|
9418
10212
|
flex-direction: column;
|
|
@@ -9509,6 +10303,19 @@ const HeaderSectionWidget = ({ config }) => {
|
|
|
9509
10303
|
box-shadow: 0 0 0 2px rgba(237, 124, 34, 0.15);
|
|
9510
10304
|
}
|
|
9511
10305
|
|
|
10306
|
+
.${cls} .hdr-input--error {
|
|
10307
|
+
border-color: var(--owt-color-danger, #DC2626);
|
|
10308
|
+
box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.12);
|
|
10309
|
+
}
|
|
10310
|
+
|
|
10311
|
+
.${cls} .hdr-error-text {
|
|
10312
|
+
margin-left: calc(0px);
|
|
10313
|
+
color: var(--owt-color-danger, #DC2626);
|
|
10314
|
+
font-size: 0.75rem;
|
|
10315
|
+
line-height: 1.2;
|
|
10316
|
+
font-weight: 500;
|
|
10317
|
+
}
|
|
10318
|
+
|
|
9512
10319
|
@media (max-width: 768px) {
|
|
9513
10320
|
.${cls} {
|
|
9514
10321
|
flex-direction: column;
|
|
@@ -9517,13 +10324,21 @@ const HeaderSectionWidget = ({ config }) => {
|
|
|
9517
10324
|
min-width: 0;
|
|
9518
10325
|
}
|
|
9519
10326
|
}
|
|
9520
|
-
` }), jsxRuntimeExports.jsxs("div", { className: cls, children: [jsxRuntimeExports.jsxs("div", { className: "hdr-left", children: [jsxRuntimeExports.jsxs("div", { children: [
|
|
10327
|
+
` }), jsxRuntimeExports.jsxs("div", { className: cls, children: [jsxRuntimeExports.jsxs("div", { className: "hdr-left", children: [jsxRuntimeExports.jsxs("div", { className: "hdr-avatar-wrapper", children: [displayImageUrl ? (jsxRuntimeExports.jsx("img", { src: displayImageUrl, alt: displayName || 'Profile', className: "hdr-avatar", onError: (e) => {
|
|
9521
10328
|
e.target.style.display = 'none';
|
|
9522
10329
|
const placeholder = e.target
|
|
9523
10330
|
.parentElement?.querySelector('.hdr-avatar-placeholder');
|
|
9524
10331
|
if (placeholder)
|
|
9525
10332
|
placeholder.style.display = 'flex';
|
|
9526
|
-
} })) : null, jsxRuntimeExports.jsx("div", { className: "hdr-avatar-placeholder", style: { display:
|
|
10333
|
+
} })) : null, jsxRuntimeExports.jsx("div", { className: "hdr-avatar-placeholder", style: { display: displayImageUrl ? 'none' : 'flex' }, children: jsxRuntimeExports.jsx("img", { src: img$f, alt: "Profile Placeholder" }) }), !isReadonly && (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsxs("div", { className: "hdr-avatar-overlay", children: [jsxRuntimeExports.jsxs("button", { type: "button", className: "hdr-avatar-action", onClick: () => fileInputRef.current?.click(), children: [jsxRuntimeExports.jsxs("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntimeExports.jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }), jsxRuntimeExports.jsx("polyline", { points: "17 8 12 3 7 8" }), jsxRuntimeExports.jsx("line", { x1: "12", y1: "3", x2: "12", y2: "15" })] }), "Upload"] }), jsxRuntimeExports.jsxs("button", { type: "button", className: "hdr-avatar-action hdr-avatar-action--delete", onClick: handleImageDelete, children: [jsxRuntimeExports.jsxs("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntimeExports.jsx("polyline", { points: "3 6 5 6 21 6" }), jsxRuntimeExports.jsx("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" })] }), "Delete"] })] }), jsxRuntimeExports.jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", style: { display: 'none' }, onChange: handleImageUpload })] }))] }), jsxRuntimeExports.jsxs("div", { className: "hdr-info", children: [displayName && jsxRuntimeExports.jsx("div", { className: "hdr-name", children: displayName }), jsxRuntimeExports.jsxs("div", { className: "hdr-field-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-field-label", children: [getLabel('functionalId'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-field-value", children: functionalId || '-' })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-field-row", children: [jsxRuntimeExports.jsx("span", { className: "hdr-field-label", children: getLabel('status') }), isReadonly ? (statusLabel ? (jsxRuntimeExports.jsx("span", { className: "hdr-status-badge", style: { backgroundColor: statusColor }, children: statusLabel })) : (jsxRuntimeExports.jsx("span", { className: "hdr-field-value", children: "-" }))) : (jsxRuntimeExports.jsxs("select", { className: "hdr-select", value: statusValue, onChange: (e) => updateFieldValue('status', e.target.value), children: [jsxRuntimeExports.jsx("option", { value: "", children: getLabel('select') }), statusOptions.map((opt) => (jsxRuntimeExports.jsx("option", { value: opt.value, children: opt.label }, opt.value)))] }))] }), jsxRuntimeExports.jsxs("div", { className: "hdr-field-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-field-label", children: [getLabel('statusReason'), " :"] }), isReadonly ? (jsxRuntimeExports.jsx("span", { className: "hdr-field-value", children: statusReason || '-' })) : (jsxRuntimeExports.jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: [jsxRuntimeExports.jsx("input", { type: "text", className: `hdr-input ${(!isReadonly && (showReasonRequired || isReasonMissing) && isReasonMissing) ? 'hdr-input--error' : ''}`, value: statusReason, placeholder: getLabel('enterReason'), required: isStatusChanged, "aria-required": isStatusChanged, "aria-invalid": !isReadonly && (showReasonRequired || isReasonMissing) && isReasonMissing, onBlur: () => {
|
|
10334
|
+
if (isReasonMissing)
|
|
10335
|
+
setShowReasonRequired(true);
|
|
10336
|
+
}, onChange: (e) => {
|
|
10337
|
+
updateFieldValue('statusReason', e.target.value);
|
|
10338
|
+
if (showReasonRequired && String(e.target.value || '').trim().length > 0) {
|
|
10339
|
+
setShowReasonRequired(false);
|
|
10340
|
+
}
|
|
10341
|
+
} }), !isReadonly && (showReasonRequired || isReasonMissing) && isReasonMissing ? (jsxRuntimeExports.jsx("div", { className: "hdr-error-text", children: getLabel('enterReason') })) : null] }))] })] })] }), jsxRuntimeExports.jsx("div", { className: "hdr-right", children: jsxRuntimeExports.jsxs("div", { className: "hdr-right-top", children: [jsxRuntimeExports.jsxs("div", { className: "hdr-meta-col", children: [jsxRuntimeExports.jsxs("div", { className: "hdr-meta-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-meta-label", children: [getLabel('createdBy'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-meta-value", children: createdBy || '-' })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-meta-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-meta-label", children: [getLabel('createdAt'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-meta-value", children: createdAt || '-' })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-meta-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-meta-label", children: [getLabel('lastApprovedBy'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-meta-value", children: lastApprovedBy || '-' })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-meta-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-meta-label", children: [getLabel('lastApprovedAt'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-meta-value", children: lastApprovedAt || '-' })] })] }), score ? (jsxRuntimeExports.jsx("div", { className: "hdr-score-ring", style: { ['--pct']: score.percent }, "aria-label": `Completion score ${score.completionDisplay} of ${score.idealDisplay} (${score.percent}%)`, title: `${score.completionDisplay} / ${score.idealDisplay} (${score.percent}%)`, children: jsxRuntimeExports.jsx("div", { className: "hdr-score-value", children: String(score.completionDisplay) }) })) : null] }) })] })] }));
|
|
9527
10342
|
};
|
|
9528
10343
|
|
|
9529
10344
|
function getValueByPathOrKey(obj, path) {
|
|
@@ -9533,7 +10348,7 @@ function getValueByPathOrKey(obj, path) {
|
|
|
9533
10348
|
return obj[path];
|
|
9534
10349
|
return getValueByPath(obj, path);
|
|
9535
10350
|
}
|
|
9536
|
-
function tryFormatDateTime(value) {
|
|
10351
|
+
function tryFormatDateTime$1(value) {
|
|
9537
10352
|
if (typeof value !== 'string' || !value)
|
|
9538
10353
|
return value ? String(value) : '-';
|
|
9539
10354
|
const d = new Date(value);
|
|
@@ -9727,12 +10542,539 @@ const ScoresDisplayWidget = ({ config, schemaData: propSchemaData, }) => {
|
|
|
9727
10542
|
String(s.computed_score) !== ''
|
|
9728
10543
|
? String(s.computed_score)
|
|
9729
10544
|
: '-';
|
|
9730
|
-
const computedAt = tryFormatDateTime(s?.computed_at);
|
|
10545
|
+
const computedAt = tryFormatDateTime$1(s?.computed_at);
|
|
9731
10546
|
const key = `${scoreType}-${String(s?.computed_at || '')}-${idx}`;
|
|
9732
10547
|
return (jsxRuntimeExports.jsxs("div", { className: "scores-card", "aria-live": idx === 0 ? 'polite' : undefined, children: [jsxRuntimeExports.jsx("div", { className: "scores-type", children: scoreType }), jsxRuntimeExports.jsx("div", { className: "scores-value", children: scoreValue }), jsxRuntimeExports.jsx("hr", { className: "scores-separator" }), jsxRuntimeExports.jsx("div", { className: "scores-meta", children: jsxRuntimeExports.jsxs("div", { className: "scores-meta-line", children: ["Computed at: ", jsxRuntimeExports.jsx("strong", { children: computedAt })] }) })] }, key));
|
|
9733
10548
|
}) })) })] }));
|
|
9734
10549
|
};
|
|
9735
10550
|
|
|
10551
|
+
function tryFormatDateTime(value) {
|
|
10552
|
+
if (typeof value !== 'string' || !value)
|
|
10553
|
+
return value ? String(value) : '-';
|
|
10554
|
+
const d = new Date(value);
|
|
10555
|
+
if (Number.isNaN(d.getTime()))
|
|
10556
|
+
return value;
|
|
10557
|
+
try {
|
|
10558
|
+
return d.toLocaleString(undefined, {
|
|
10559
|
+
year: 'numeric',
|
|
10560
|
+
month: 'short',
|
|
10561
|
+
day: '2-digit',
|
|
10562
|
+
hour: '2-digit',
|
|
10563
|
+
minute: '2-digit',
|
|
10564
|
+
});
|
|
10565
|
+
}
|
|
10566
|
+
catch {
|
|
10567
|
+
return value;
|
|
10568
|
+
}
|
|
10569
|
+
}
|
|
10570
|
+
function tryFormatDate(value) {
|
|
10571
|
+
if (typeof value !== 'string' || !value)
|
|
10572
|
+
return value ? String(value) : '-';
|
|
10573
|
+
const d = new Date(value);
|
|
10574
|
+
if (Number.isNaN(d.getTime()))
|
|
10575
|
+
return value;
|
|
10576
|
+
try {
|
|
10577
|
+
return d.toLocaleDateString(undefined, {
|
|
10578
|
+
year: 'numeric',
|
|
10579
|
+
month: 'short',
|
|
10580
|
+
day: '2-digit',
|
|
10581
|
+
});
|
|
10582
|
+
}
|
|
10583
|
+
catch {
|
|
10584
|
+
return value;
|
|
10585
|
+
}
|
|
10586
|
+
}
|
|
10587
|
+
function displayText(value) {
|
|
10588
|
+
if (value === null || value === undefined || String(value).trim() === '')
|
|
10589
|
+
return '-';
|
|
10590
|
+
return String(value);
|
|
10591
|
+
}
|
|
10592
|
+
function normalizeStatus(raw) {
|
|
10593
|
+
if (raw === null || raw === undefined || String(raw).trim() === '')
|
|
10594
|
+
return 'unknown';
|
|
10595
|
+
const v = String(raw).trim().toLowerCase();
|
|
10596
|
+
if (v === 'success' || v === 'succeeded' || v === 'ok')
|
|
10597
|
+
return 'success';
|
|
10598
|
+
if (v === 'failure' || v === 'failed' || v === 'error')
|
|
10599
|
+
return 'failure';
|
|
10600
|
+
if (v === 'not done' || v === 'not_done' || v === 'not-done' || v === 'pending')
|
|
10601
|
+
return 'not_done';
|
|
10602
|
+
return 'unknown';
|
|
10603
|
+
}
|
|
10604
|
+
/** Large enough for eSignet / OIDC login; clamped so it always fits the current screen. */
|
|
10605
|
+
function getCenteredPopupFeatures(width, height) {
|
|
10606
|
+
const dualScreenLeft = window.screenLeft ?? window.screenX ?? 0;
|
|
10607
|
+
const dualScreenTop = window.screenTop ?? window.screenY ?? 0;
|
|
10608
|
+
const viewportWidth = window.innerWidth || document.documentElement.clientWidth || (typeof screen !== 'undefined' ? screen.width : width);
|
|
10609
|
+
const viewportHeight = window.innerHeight || document.documentElement.clientHeight || (typeof screen !== 'undefined' ? screen.height : height);
|
|
10610
|
+
const maxW = Math.max(320, Math.floor(viewportWidth * 0.92));
|
|
10611
|
+
const maxH = Math.max(400, Math.floor(viewportHeight * 0.92));
|
|
10612
|
+
const w = Math.max(320, Math.min(width, maxW));
|
|
10613
|
+
const h = Math.max(400, Math.min(height, maxH));
|
|
10614
|
+
const left = Math.max(0, Math.floor(viewportWidth / 2 - w / 2 + dualScreenLeft));
|
|
10615
|
+
const top = Math.max(0, Math.floor(viewportHeight / 2 - h / 2 + dualScreenTop));
|
|
10616
|
+
return [
|
|
10617
|
+
'popup=yes',
|
|
10618
|
+
'noopener=yes',
|
|
10619
|
+
'noreferrer=yes',
|
|
10620
|
+
`width=${w}`,
|
|
10621
|
+
`height=${h}`,
|
|
10622
|
+
`left=${left}`,
|
|
10623
|
+
`top=${top}`,
|
|
10624
|
+
'scrollbars=yes',
|
|
10625
|
+
'resizable=yes',
|
|
10626
|
+
].join(',');
|
|
10627
|
+
}
|
|
10628
|
+
function pickAuthorizationUrl(resp, explicitKey) {
|
|
10629
|
+
if (!resp)
|
|
10630
|
+
return null;
|
|
10631
|
+
const tryKey = (k) => {
|
|
10632
|
+
const v = resp?.[k];
|
|
10633
|
+
if (typeof v === 'string' && v)
|
|
10634
|
+
return v;
|
|
10635
|
+
return null;
|
|
10636
|
+
};
|
|
10637
|
+
if (explicitKey) {
|
|
10638
|
+
const v = tryKey(explicitKey);
|
|
10639
|
+
if (v)
|
|
10640
|
+
return v;
|
|
10641
|
+
}
|
|
10642
|
+
return (tryKey('authentication_url') ||
|
|
10643
|
+
tryKey('authorization_url') ||
|
|
10644
|
+
tryKey('authorizationUrl') ||
|
|
10645
|
+
tryKey('auth_url') ||
|
|
10646
|
+
tryKey('authUrl') ||
|
|
10647
|
+
tryKey('url') ||
|
|
10648
|
+
null);
|
|
10649
|
+
}
|
|
10650
|
+
function resolveValueFromSources(path, values, schemaData) {
|
|
10651
|
+
if (!path)
|
|
10652
|
+
return undefined;
|
|
10653
|
+
const fromValues = getValueByPath(values, path);
|
|
10654
|
+
if (fromValues !== undefined)
|
|
10655
|
+
return fromValues;
|
|
10656
|
+
return getValueByPath(schemaData, path);
|
|
10657
|
+
}
|
|
10658
|
+
function unwrapPayload(response) {
|
|
10659
|
+
if (response && typeof response === 'object') {
|
|
10660
|
+
if (response.response_body?.response_payload !== undefined)
|
|
10661
|
+
return response.response_body.response_payload;
|
|
10662
|
+
if (response.response_payload !== undefined)
|
|
10663
|
+
return response.response_payload;
|
|
10664
|
+
}
|
|
10665
|
+
return response;
|
|
10666
|
+
}
|
|
10667
|
+
const IdAuthenticationWidget = ({ config, schemaData: propSchemaData }) => {
|
|
10668
|
+
const { dataSourceRequestHandler, schemaData: ctxSchemaData } = useWidgetContext();
|
|
10669
|
+
const values = useSelector((state) => state.widget.values);
|
|
10670
|
+
const schemaData = (propSchemaData || ctxSchemaData || {});
|
|
10671
|
+
const widgetId = config['widget-id'];
|
|
10672
|
+
const dataPath = config['widget-data-path'];
|
|
10673
|
+
const paths = useMemo(() => {
|
|
10674
|
+
if (!dataPath || typeof dataPath !== 'object')
|
|
10675
|
+
return {};
|
|
10676
|
+
return dataPath;
|
|
10677
|
+
}, [dataPath]);
|
|
10678
|
+
const authConfig = config['widget-auth-config'];
|
|
10679
|
+
const registerId = authConfig?.registerId ?? undefined;
|
|
10680
|
+
const internalRecordId = resolveValueFromSources(paths.internalRecordId, values, schemaData);
|
|
10681
|
+
const initiatedByStaffId = resolveValueFromSources(paths.initiatedByStaffId, values, schemaData);
|
|
10682
|
+
const providerId = authConfig?.providerId;
|
|
10683
|
+
const providerName = authConfig?.providerName;
|
|
10684
|
+
const foundationalId = resolveValueFromSources(paths.foundationalId, values, schemaData);
|
|
10685
|
+
const lastAuthenticatedOn = resolveValueFromSources(paths.lastAuthenticatedOn, values, schemaData);
|
|
10686
|
+
const lastAuthStatusRaw = resolveValueFromSources(paths.lastAuthenticationStatus, values, schemaData);
|
|
10687
|
+
const expiryDate = resolveValueFromSources(paths.expiryDate, values, schemaData);
|
|
10688
|
+
const psut = resolveValueFromSources(paths.authenticationToken, values, schemaData);
|
|
10689
|
+
const status = useMemo(() => normalizeStatus(lastAuthStatusRaw), [lastAuthStatusRaw]);
|
|
10690
|
+
/** URL from prefetch (or default); used when opening the OIDC / eSignet popup */
|
|
10691
|
+
const [resolvedAuthUrl, setResolvedAuthUrl] = useState(null);
|
|
10692
|
+
const [authActionLoading, setAuthActionLoading] = useState(false);
|
|
10693
|
+
const [authError, setAuthError] = useState(null);
|
|
10694
|
+
const popupRef = useRef(null);
|
|
10695
|
+
const pollTimerRef = useRef(null);
|
|
10696
|
+
const [overlayUrl, setOverlayUrl] = useState(null);
|
|
10697
|
+
const emitHostEvent = useCallback((detail) => {
|
|
10698
|
+
if (typeof window === 'undefined')
|
|
10699
|
+
return;
|
|
10700
|
+
window.dispatchEvent(new CustomEvent('openg2p:id-authentication', {
|
|
10701
|
+
detail: {
|
|
10702
|
+
widgetId,
|
|
10703
|
+
...detail,
|
|
10704
|
+
},
|
|
10705
|
+
}));
|
|
10706
|
+
}, [widgetId]);
|
|
10707
|
+
const cleanupPopup = useCallback(() => {
|
|
10708
|
+
if (pollTimerRef.current) {
|
|
10709
|
+
window.clearInterval(pollTimerRef.current);
|
|
10710
|
+
pollTimerRef.current = null;
|
|
10711
|
+
}
|
|
10712
|
+
popupRef.current = null;
|
|
10713
|
+
}, []);
|
|
10714
|
+
useEffect(() => {
|
|
10715
|
+
return () => {
|
|
10716
|
+
cleanupPopup();
|
|
10717
|
+
try {
|
|
10718
|
+
popupRef.current?.close?.();
|
|
10719
|
+
}
|
|
10720
|
+
catch {
|
|
10721
|
+
// ignore
|
|
10722
|
+
}
|
|
10723
|
+
};
|
|
10724
|
+
}, [cleanupPopup]);
|
|
10725
|
+
// Provider details are supplied by host; clear any previous resolved URL on provider change.
|
|
10726
|
+
useEffect(() => {
|
|
10727
|
+
setResolvedAuthUrl(null);
|
|
10728
|
+
}, [providerId, providerName]);
|
|
10729
|
+
const openAuthPopup = useCallback((authUrl) => {
|
|
10730
|
+
if (authConfig?.useIframeOverlay === true) {
|
|
10731
|
+
setOverlayUrl(authUrl);
|
|
10732
|
+
emitHostEvent({ type: 'overlay_opened' });
|
|
10733
|
+
return;
|
|
10734
|
+
}
|
|
10735
|
+
const pw = authConfig?.popupWidth ?? 1024;
|
|
10736
|
+
const ph = authConfig?.popupHeight ?? 800;
|
|
10737
|
+
const features = getCenteredPopupFeatures(pw, ph);
|
|
10738
|
+
const popup = window.open(authUrl, `${widgetId}-oidc`, features);
|
|
10739
|
+
if (!popup) {
|
|
10740
|
+
setAuthError('Popup blocked. Please allow popups and try again.');
|
|
10741
|
+
return;
|
|
10742
|
+
}
|
|
10743
|
+
popupRef.current = popup;
|
|
10744
|
+
popup.focus?.();
|
|
10745
|
+
setAuthError(null);
|
|
10746
|
+
emitHostEvent({ type: 'popup_opened' });
|
|
10747
|
+
if (pollTimerRef.current) {
|
|
10748
|
+
window.clearInterval(pollTimerRef.current);
|
|
10749
|
+
pollTimerRef.current = null;
|
|
10750
|
+
}
|
|
10751
|
+
pollTimerRef.current = window.setInterval(() => {
|
|
10752
|
+
try {
|
|
10753
|
+
const closed = !popupRef.current || popupRef.current.closed;
|
|
10754
|
+
if (closed) {
|
|
10755
|
+
cleanupPopup();
|
|
10756
|
+
emitHostEvent({ type: 'popup_closed' });
|
|
10757
|
+
}
|
|
10758
|
+
}
|
|
10759
|
+
catch {
|
|
10760
|
+
// ignore
|
|
10761
|
+
}
|
|
10762
|
+
}, 500);
|
|
10763
|
+
}, [authConfig, cleanupPopup, emitHostEvent, widgetId]);
|
|
10764
|
+
const onAuthenticate = useCallback(async () => {
|
|
10765
|
+
setAuthError(null);
|
|
10766
|
+
if (!authConfig) {
|
|
10767
|
+
setAuthError('Missing widget-auth-config.');
|
|
10768
|
+
return;
|
|
10769
|
+
}
|
|
10770
|
+
const canCallAuthApi = Boolean(dataSourceRequestHandler && authConfig.service && authConfig.authenticateEndpoint);
|
|
10771
|
+
let url = resolvedAuthUrl;
|
|
10772
|
+
if (!url && canCallAuthApi) {
|
|
10773
|
+
setAuthActionLoading(true);
|
|
10774
|
+
try {
|
|
10775
|
+
const basePayload = {
|
|
10776
|
+
register_id: registerId,
|
|
10777
|
+
internal_record_id: internalRecordId,
|
|
10778
|
+
provider_id: providerId,
|
|
10779
|
+
initiated_by_staff_id: initiatedByStaffId,
|
|
10780
|
+
};
|
|
10781
|
+
const requestParams = basePayload;
|
|
10782
|
+
// eslint-disable-next-line no-console
|
|
10783
|
+
console.log('[IdAuthenticationWidget] authenticate_registrant params', requestParams);
|
|
10784
|
+
const resp = await dataSourceRequestHandler(authConfig.service, authConfig.authenticateEndpoint, authConfig.authenticateMethod || 'POST', requestParams);
|
|
10785
|
+
const payload = unwrapPayload(resp);
|
|
10786
|
+
const authUrl = pickAuthorizationUrl(payload, authConfig.authorizationUrlKey);
|
|
10787
|
+
url = authUrl || null;
|
|
10788
|
+
if (authUrl)
|
|
10789
|
+
setResolvedAuthUrl(authUrl);
|
|
10790
|
+
}
|
|
10791
|
+
catch (e) {
|
|
10792
|
+
if (!url) {
|
|
10793
|
+
setAuthError(e?.message || 'Could not load provider URL.');
|
|
10794
|
+
return;
|
|
10795
|
+
}
|
|
10796
|
+
}
|
|
10797
|
+
finally {
|
|
10798
|
+
setAuthActionLoading(false);
|
|
10799
|
+
}
|
|
10800
|
+
}
|
|
10801
|
+
if (!url) {
|
|
10802
|
+
setAuthError('No authorization URL returned from authenticate_registrant.');
|
|
10803
|
+
return;
|
|
10804
|
+
}
|
|
10805
|
+
openAuthPopup(url);
|
|
10806
|
+
}, [
|
|
10807
|
+
authConfig,
|
|
10808
|
+
dataSourceRequestHandler,
|
|
10809
|
+
openAuthPopup,
|
|
10810
|
+
registerId,
|
|
10811
|
+
internalRecordId,
|
|
10812
|
+
initiatedByStaffId,
|
|
10813
|
+
resolvedAuthUrl,
|
|
10814
|
+
]);
|
|
10815
|
+
useEffect(() => {
|
|
10816
|
+
const successType = authConfig?.successMessageType || 'openg2p:oidc:success';
|
|
10817
|
+
const handler = (event) => {
|
|
10818
|
+
const data = event?.data;
|
|
10819
|
+
if (!data || typeof data !== 'object')
|
|
10820
|
+
return;
|
|
10821
|
+
if (data.type !== successType)
|
|
10822
|
+
return;
|
|
10823
|
+
if (data.widgetId && data.widgetId !== widgetId)
|
|
10824
|
+
return;
|
|
10825
|
+
emitHostEvent({ type: 'authenticated', payload: data });
|
|
10826
|
+
try {
|
|
10827
|
+
popupRef.current?.close?.();
|
|
10828
|
+
}
|
|
10829
|
+
catch {
|
|
10830
|
+
// ignore
|
|
10831
|
+
}
|
|
10832
|
+
cleanupPopup();
|
|
10833
|
+
if (authConfig?.reloadOnSuccess) {
|
|
10834
|
+
window.location.reload();
|
|
10835
|
+
}
|
|
10836
|
+
};
|
|
10837
|
+
window.addEventListener('message', handler);
|
|
10838
|
+
return () => window.removeEventListener('message', handler);
|
|
10839
|
+
}, [authConfig?.reloadOnSuccess, authConfig?.successMessageType, cleanupPopup, emitHostEvent, widgetId]);
|
|
10840
|
+
const cls = `id-auth-widget-${widgetId}`;
|
|
10841
|
+
const statusLabel = useMemo(() => {
|
|
10842
|
+
if (status === 'success')
|
|
10843
|
+
return 'Success';
|
|
10844
|
+
if (status === 'failure')
|
|
10845
|
+
return 'Failure';
|
|
10846
|
+
if (status === 'not_done')
|
|
10847
|
+
return 'Not done';
|
|
10848
|
+
return 'Unknown';
|
|
10849
|
+
}, [status]);
|
|
10850
|
+
const statusColor = useMemo(() => {
|
|
10851
|
+
if (status === 'success')
|
|
10852
|
+
return 'var(--owt-color-success, #16A34A)';
|
|
10853
|
+
if (status === 'failure')
|
|
10854
|
+
return 'var(--owt-color-danger, #DC2626)';
|
|
10855
|
+
if (status === 'not_done')
|
|
10856
|
+
return 'var(--owt-color-warning, #D97706)';
|
|
10857
|
+
return 'var(--owt-color-text-muted, #6B7280)';
|
|
10858
|
+
}, [status]);
|
|
10859
|
+
const buttonBusy = authActionLoading;
|
|
10860
|
+
const buttonDisabled = !authConfig || buttonBusy;
|
|
10861
|
+
return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("style", { children: `
|
|
10862
|
+
.${cls} {
|
|
10863
|
+
width: 100%;
|
|
10864
|
+
font-family: Roboto, sans-serif;
|
|
10865
|
+
}
|
|
10866
|
+
|
|
10867
|
+
/* Two-column field grid; primary action in a bottom band (matches section save/edit pattern). */
|
|
10868
|
+
.${cls} .auth-content {
|
|
10869
|
+
display: flex;
|
|
10870
|
+
flex-direction: column;
|
|
10871
|
+
gap: 0;
|
|
10872
|
+
min-width: 0;
|
|
10873
|
+
}
|
|
10874
|
+
|
|
10875
|
+
.${cls} .auth-grid {
|
|
10876
|
+
display: grid;
|
|
10877
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
10878
|
+
gap: 16px 24px;
|
|
10879
|
+
min-width: 0;
|
|
10880
|
+
}
|
|
10881
|
+
|
|
10882
|
+
/* Each field: label (left) + value (right), same as DisplayWidget readonly */
|
|
10883
|
+
.${cls} .auth-cell {
|
|
10884
|
+
display: flex;
|
|
10885
|
+
flex-direction: row;
|
|
10886
|
+
align-items: flex-start;
|
|
10887
|
+
gap: 12px 16px;
|
|
10888
|
+
min-width: 0;
|
|
10889
|
+
}
|
|
10890
|
+
|
|
10891
|
+
.${cls} .auth-cell.auth-cell--full {
|
|
10892
|
+
grid-column: 1 / -1;
|
|
10893
|
+
}
|
|
10894
|
+
|
|
10895
|
+
/* Action cell: no left label spacer, align button to column start */
|
|
10896
|
+
.${cls} .auth-cell.auth-cell--action .auth-label {
|
|
10897
|
+
display: none;
|
|
10898
|
+
}
|
|
10899
|
+
.${cls} .auth-cell.auth-cell--action .auth-value {
|
|
10900
|
+
flex: 1 1 auto;
|
|
10901
|
+
}
|
|
10902
|
+
|
|
10903
|
+
.${cls} .auth-label {
|
|
10904
|
+
flex: 0 0 auto;
|
|
10905
|
+
min-width: 200px;
|
|
10906
|
+
max-width: 40%;
|
|
10907
|
+
font-size: 16px;
|
|
10908
|
+
color: rgba(0, 0, 0, 0.6);
|
|
10909
|
+
font-weight: 500;
|
|
10910
|
+
line-height: 1.45;
|
|
10911
|
+
margin: 0;
|
|
10912
|
+
word-break: break-word;
|
|
10913
|
+
}
|
|
10914
|
+
|
|
10915
|
+
.${cls} .auth-value {
|
|
10916
|
+
flex: 1 1 auto;
|
|
10917
|
+
min-width: 0;
|
|
10918
|
+
font-size: 16px;
|
|
10919
|
+
color: var(--owt-color-text, #111827);
|
|
10920
|
+
font-weight: 500;
|
|
10921
|
+
line-height: 1.45;
|
|
10922
|
+
word-break: break-word;
|
|
10923
|
+
}
|
|
10924
|
+
|
|
10925
|
+
.${cls} .auth-value--foundational {
|
|
10926
|
+
font-size: 18px;
|
|
10927
|
+
font-weight: 700;
|
|
10928
|
+
color: var(--owt-color-primary-dark, #F07B1A);
|
|
10929
|
+
letter-spacing: 0.1px;
|
|
10930
|
+
}
|
|
10931
|
+
|
|
10932
|
+
/* Button is placed inside the grid (next to PSUT) */
|
|
10933
|
+
|
|
10934
|
+
.${cls} .auth-status {
|
|
10935
|
+
display: inline-flex;
|
|
10936
|
+
align-items: center;
|
|
10937
|
+
gap: 8px;
|
|
10938
|
+
width: fit-content;
|
|
10939
|
+
padding: 4px 10px;
|
|
10940
|
+
border-radius: 999px;
|
|
10941
|
+
background: rgba(2, 6, 23, 0.04);
|
|
10942
|
+
border: 1px solid rgba(2, 6, 23, 0.08);
|
|
10943
|
+
font-size: 13px;
|
|
10944
|
+
font-weight: 700;
|
|
10945
|
+
color: var(--owt-color-text, #011627);
|
|
10946
|
+
}
|
|
10947
|
+
|
|
10948
|
+
.${cls} .auth-dot {
|
|
10949
|
+
width: 8px;
|
|
10950
|
+
height: 8px;
|
|
10951
|
+
border-radius: 50%;
|
|
10952
|
+
background: ${statusColor};
|
|
10953
|
+
}
|
|
10954
|
+
|
|
10955
|
+
.${cls} .auth-token {
|
|
10956
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
10957
|
+
font-size: 14px;
|
|
10958
|
+
font-weight: 500;
|
|
10959
|
+
color: var(--owt-color-text, #011627);
|
|
10960
|
+
background: transparent;
|
|
10961
|
+
border: none;
|
|
10962
|
+
border-radius: 0;
|
|
10963
|
+
padding: 0;
|
|
10964
|
+
word-break: break-all;
|
|
10965
|
+
}
|
|
10966
|
+
|
|
10967
|
+
.${cls} .auth-button {
|
|
10968
|
+
/* Match SectionRegistryView Save CTA (SectionRenderer) */
|
|
10969
|
+
font-size: 14px;
|
|
10970
|
+
font-weight: 500;
|
|
10971
|
+
padding: 8px 24px;
|
|
10972
|
+
line-height: 1.5;
|
|
10973
|
+
border-radius: var(--owt-btn-border-radius, 10px);
|
|
10974
|
+
border: 1px solid rgb(237, 124, 34);
|
|
10975
|
+
background-color: rgb(237, 124, 34);
|
|
10976
|
+
color: var(--owt-color-bg, #FFFFFF);
|
|
10977
|
+
font-family: Roboto, sans-serif;
|
|
10978
|
+
cursor: pointer;
|
|
10979
|
+
transition: opacity 0.15s ease;
|
|
10980
|
+
}
|
|
10981
|
+
|
|
10982
|
+
.${cls} .auth-button:disabled {
|
|
10983
|
+
opacity: 0.5;
|
|
10984
|
+
cursor: not-allowed;
|
|
10985
|
+
}
|
|
10986
|
+
|
|
10987
|
+
.${cls} .auth-error {
|
|
10988
|
+
font-size: 12px;
|
|
10989
|
+
color: var(--owt-color-danger, #DC2626);
|
|
10990
|
+
font-weight: 700;
|
|
10991
|
+
line-height: 1.3;
|
|
10992
|
+
text-align: left;
|
|
10993
|
+
max-width: 100%;
|
|
10994
|
+
}
|
|
10995
|
+
|
|
10996
|
+
.${cls} .overlay-backdrop {
|
|
10997
|
+
position: fixed;
|
|
10998
|
+
inset: 0;
|
|
10999
|
+
background: rgba(17, 24, 39, 0.55);
|
|
11000
|
+
z-index: 9999;
|
|
11001
|
+
display: flex;
|
|
11002
|
+
align-items: center;
|
|
11003
|
+
justify-content: center;
|
|
11004
|
+
padding: 24px;
|
|
11005
|
+
}
|
|
11006
|
+
|
|
11007
|
+
.${cls} .overlay-panel {
|
|
11008
|
+
width: min(1100px, 92vw);
|
|
11009
|
+
height: min(820px, 92vh);
|
|
11010
|
+
background: var(--owt-color-bg, #FFFFFF);
|
|
11011
|
+
border-radius: 12px;
|
|
11012
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.25);
|
|
11013
|
+
overflow: hidden;
|
|
11014
|
+
display: flex;
|
|
11015
|
+
flex-direction: column;
|
|
11016
|
+
}
|
|
11017
|
+
|
|
11018
|
+
.${cls} .overlay-header {
|
|
11019
|
+
display: flex;
|
|
11020
|
+
align-items: center;
|
|
11021
|
+
justify-content: space-between;
|
|
11022
|
+
padding: 10px 14px;
|
|
11023
|
+
border-bottom: 1px solid var(--owt-color-border-light, #E4E4E4);
|
|
11024
|
+
font-family: Roboto, sans-serif;
|
|
11025
|
+
}
|
|
11026
|
+
|
|
11027
|
+
.${cls} .overlay-title {
|
|
11028
|
+
font-size: 14px;
|
|
11029
|
+
color: var(--owt-color-text, #011627);
|
|
11030
|
+
font-weight: 600;
|
|
11031
|
+
min-width: 0;
|
|
11032
|
+
overflow: hidden;
|
|
11033
|
+
text-overflow: ellipsis;
|
|
11034
|
+
white-space: nowrap;
|
|
11035
|
+
}
|
|
11036
|
+
|
|
11037
|
+
.${cls} .overlay-close {
|
|
11038
|
+
border: 1px solid var(--owt-btn-secondary-border, #C4C4C4);
|
|
11039
|
+
background: var(--owt-btn-secondary-bg, #FFFFFF);
|
|
11040
|
+
color: var(--owt-btn-secondary-color, #011627);
|
|
11041
|
+
border-radius: var(--owt-btn-border-radius, 10px);
|
|
11042
|
+
padding: 6px 10px;
|
|
11043
|
+
font-size: 12px;
|
|
11044
|
+
cursor: pointer;
|
|
11045
|
+
}
|
|
11046
|
+
|
|
11047
|
+
.${cls} .overlay-iframe {
|
|
11048
|
+
flex: 1 1 auto;
|
|
11049
|
+
width: 100%;
|
|
11050
|
+
border: none;
|
|
11051
|
+
}
|
|
11052
|
+
|
|
11053
|
+
@media (max-width: 640px) {
|
|
11054
|
+
.${cls} .auth-grid {
|
|
11055
|
+
grid-template-columns: 1fr;
|
|
11056
|
+
}
|
|
11057
|
+
.${cls} .auth-cell {
|
|
11058
|
+
flex-direction: column;
|
|
11059
|
+
align-items: stretch;
|
|
11060
|
+
gap: 4px 0;
|
|
11061
|
+
}
|
|
11062
|
+
.${cls} .auth-label {
|
|
11063
|
+
min-width: 0;
|
|
11064
|
+
max-width: none;
|
|
11065
|
+
}
|
|
11066
|
+
}
|
|
11067
|
+
` }), jsxRuntimeExports.jsxs("div", { className: cls, children: [overlayUrl ? (jsxRuntimeExports.jsx("div", { className: "overlay-backdrop", role: "dialog", "aria-modal": "true", "aria-label": "Authentication", onClick: (e) => {
|
|
11068
|
+
if (e.target === e.currentTarget) {
|
|
11069
|
+
setOverlayUrl(null);
|
|
11070
|
+
emitHostEvent({ type: 'overlay_closed' });
|
|
11071
|
+
}
|
|
11072
|
+
}, children: jsxRuntimeExports.jsxs("div", { className: "overlay-panel", children: [jsxRuntimeExports.jsxs("div", { className: "overlay-header", children: [jsxRuntimeExports.jsx("div", { className: "overlay-title", children: providerName ? `Authenticate via ${providerName}` : 'Authenticate' }), jsxRuntimeExports.jsx("button", { type: "button", className: "overlay-close", onClick: () => {
|
|
11073
|
+
setOverlayUrl(null);
|
|
11074
|
+
emitHostEvent({ type: 'overlay_closed' });
|
|
11075
|
+
}, children: "Close" })] }), jsxRuntimeExports.jsx("iframe", { className: "overlay-iframe", src: overlayUrl, title: "Authentication" })] }) })) : null, jsxRuntimeExports.jsx("div", { className: "auth-content", children: jsxRuntimeExports.jsxs("div", { className: "auth-grid", children: [jsxRuntimeExports.jsxs("div", { className: "auth-cell", children: [jsxRuntimeExports.jsx("div", { className: "auth-label", children: "Foundational ID:" }), jsxRuntimeExports.jsx("div", { className: "auth-value auth-value--foundational", children: displayText(foundationalId) })] }), jsxRuntimeExports.jsxs("div", { className: "auth-cell", children: [jsxRuntimeExports.jsx("div", { className: "auth-label", children: "Last authenticated on:" }), jsxRuntimeExports.jsx("div", { className: "auth-value", children: tryFormatDateTime(lastAuthenticatedOn) })] }), jsxRuntimeExports.jsxs("div", { className: "auth-cell", children: [jsxRuntimeExports.jsx("div", { className: "auth-label", children: "Expiry date:" }), jsxRuntimeExports.jsx("div", { className: "auth-value", children: tryFormatDate(expiryDate) })] }), jsxRuntimeExports.jsxs("div", { className: "auth-cell", children: [jsxRuntimeExports.jsx("div", { className: "auth-label", children: "Last authentication status:" }), jsxRuntimeExports.jsx("div", { className: "auth-value", children: jsxRuntimeExports.jsxs("div", { className: "auth-status", "aria-label": `Authentication status: ${statusLabel}`, children: [jsxRuntimeExports.jsx("span", { className: "auth-dot" }), jsxRuntimeExports.jsx("span", { children: statusLabel })] }) })] }), jsxRuntimeExports.jsxs("div", { className: "auth-cell", children: [jsxRuntimeExports.jsx("div", { className: "auth-label", children: "Authentication token (PSUT):" }), jsxRuntimeExports.jsx("div", { className: "auth-value", children: jsxRuntimeExports.jsx("div", { className: "auth-token", children: psut ? String(psut) : '-' }) })] }), jsxRuntimeExports.jsxs("div", { className: "auth-cell auth-cell--action", children: [jsxRuntimeExports.jsx("div", { className: "auth-label", "aria-hidden": true }), jsxRuntimeExports.jsxs("div", { className: "auth-value", children: [jsxRuntimeExports.jsx("button", { type: "button", className: "auth-button", onClick: onAuthenticate, disabled: buttonDisabled, children: buttonBusy ? 'Loading…' : 'Authenticate' }), authError ? (jsxRuntimeExports.jsx("div", { className: "auth-error", style: { marginTop: 8 }, children: authError })) : null] })] })] }) })] })] }));
|
|
11076
|
+
};
|
|
11077
|
+
|
|
9736
11078
|
/**
|
|
9737
11079
|
* Register all default/generic widgets
|
|
9738
11080
|
* This is called automatically when the package is imported
|
|
@@ -9762,6 +11104,8 @@ const registerDefaultWidgets = () => {
|
|
|
9762
11104
|
widgetRegistry.register({ widget: 'simple-table', component: SimpleTableWidget });
|
|
9763
11105
|
// Table widget with record-level editing
|
|
9764
11106
|
widgetRegistry.register({ widget: 'table', component: TableWidget });
|
|
11107
|
+
// Table widget with add/edit popup dialog
|
|
11108
|
+
widgetRegistry.register({ widget: 'dialog-table', component: DialogTableWidget });
|
|
9765
11109
|
// Group widgets
|
|
9766
11110
|
widgetRegistry.register({ widget: 'array-widget', component: ArrayWidget });
|
|
9767
11111
|
widgetRegistry.register({ widget: 'iterable-accordion', component: IterableAccordionWidget });
|
|
@@ -9776,6 +11120,8 @@ const registerDefaultWidgets = () => {
|
|
|
9776
11120
|
widgetRegistry.register({ widget: 'header-section', component: HeaderSectionWidget });
|
|
9777
11121
|
// Scores display widget for full-width computed scores display (view-only)
|
|
9778
11122
|
widgetRegistry.register({ widget: 'scores-display', component: ScoresDisplayWidget });
|
|
11123
|
+
// ID Authentication widget for OIDC-based foundational ID authentication (view-only + action)
|
|
11124
|
+
widgetRegistry.register({ widget: 'id-authentication', component: IdAuthenticationWidget });
|
|
9779
11125
|
};
|
|
9780
11126
|
// Auto-register on import
|
|
9781
11127
|
registerDefaultWidgets();
|
|
@@ -10137,5 +11483,5 @@ const translateUISchema = (schema, translate) => {
|
|
|
10137
11483
|
};
|
|
10138
11484
|
};
|
|
10139
11485
|
|
|
10140
|
-
export { ArrayWidget, BooleanWidget, CheckboxWidget, CurrencyInputWidget, DateInputWidget, DateTimeInputWidget, DisplayWidget, FileInputWidget, HeaderSectionWidget, IterableAccordionWidget, JSONEditorPanel, NumberInputWidget, PanelRenderer, PhoneInputWidget, ProfileWidget, PropertyEditor, RadioWidget, ScoresDisplayWidget, SectionBuilder, SectionRenderer, SectionTree, SectionsContainer, SelectWidget, SimpleTableWidget, TableWidget, TextAreaWidget, TextInputWidget, VisualBuilderPanel, WidgetEventBus, WidgetProvider, WidgetRenderer, applyCaseControl, applyDecimalPrecision, applyMask, createWidgetStore, createZodSchema, defaultTheme, evaluateCondition, filterByCharacterType, formatCurrency, formatDate, formatNumber, formatPhone, formatValue, geoHierarchyBuilder, getApiDataSource, getFormattedNumberLength, getSchemaDataSource, getStaticDataSource, getValueByPath, getWidgetValue, initI18n, isAllowedKey, parseDataPath, parseNumber, registerDefaultWidgets, removeMask, resetAll, resetWidget, resolveTheme, setDataSource, setError, setLoading, setTouched, setValue, setValueByPath, setValues, setWidgetValue, shouldEnableWidget, shouldShowWidget, transformDataSourceOptions, translatePanelConfig, translateUISchema, translateWidgetConfig, useBaseWidget, useGeoWidgetCascade, useWidgetCascade, useWidgetContext, useWidgetEventBus, useWidgetTheme, useWidgetTranslation, validateNumericValue, validateWidget, widgetRegistry };
|
|
11486
|
+
export { ArrayWidget, BooleanWidget, CheckboxWidget, CurrencyInputWidget, DateInputWidget, DateTimeInputWidget, DialogTableWidget, DisplayWidget, FileInputWidget, HeaderSectionWidget, IdAuthenticationWidget, IterableAccordionWidget, JSONEditorPanel, NumberInputWidget, PanelRenderer, PhoneInputWidget, ProfileWidget, PropertyEditor, RadioWidget, ScoresDisplayWidget, SectionBuilder, SectionRenderer, SectionTree, SectionsContainer, SelectWidget, SimpleTableWidget, TableWidget, TextAreaWidget, TextInputWidget, VisualBuilderPanel, WidgetEventBus, WidgetProvider, WidgetRenderer, applyCaseControl, applyDecimalPrecision, applyMask, createWidgetStore, createZodSchema, defaultTheme, evaluateCondition, filterByCharacterType, formatCurrency, formatDate, formatNumber, formatPhone, formatValue, geoHierarchyBuilder, getApiDataSource, getFormattedNumberLength, getSchemaDataSource, getStaticDataSource, getValueByPath, getWidgetValue, initI18n, isAllowedKey, parseDataPath, parseNumber, registerDefaultWidgets, removeMask, resetAll, resetWidget, resolveTheme, setDataSource, setError, setLoading, setTouched, setValue, setValueByPath, setValues, setWidgetValue, shouldEnableWidget, shouldShowWidget, transformDataSourceOptions, translatePanelConfig, translateUISchema, translateWidgetConfig, useBaseWidget, useGeoWidgetCascade, useWidgetCascade, useWidgetContext, useWidgetEventBus, useWidgetTheme, useWidgetTranslation, validateNumericValue, validateWidget, widgetRegistry };
|
|
10141
11487
|
//# sourceMappingURL=index.esm.js.map
|