@openg2p/registry-widgets 1.1.0-dev.0 → 1.1.0-dev.10
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/README.md +15 -25
- package/dist/components/SectionRenderer.d.ts +1 -0
- 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 +40 -19
- package/dist/index.esm.js +1721 -435
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1722 -434
- 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/ScoresDisplayWidget.d.ts +11 -17
- package/dist/widgets/ScoresDisplayWidget.d.ts.map +1 -1
- 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.js
CHANGED
|
@@ -246,24 +246,27 @@ const getValidationPattern = (validationType) => {
|
|
|
246
246
|
};
|
|
247
247
|
|
|
248
248
|
/**
|
|
249
|
-
* Validate value against validation rules
|
|
249
|
+
* Validate value against validation rules.
|
|
250
|
+
*
|
|
251
|
+
* @param skipRequired - When true, required-field checks are skipped (used by
|
|
252
|
+
* per-section Save/Next buttons so the user can move between sections without
|
|
253
|
+
* filling every mandatory field; only format/range checks still run).
|
|
250
254
|
*/
|
|
251
|
-
const validateWidget = (value, validation, required = false) => {
|
|
255
|
+
const validateWidget = (value, validation, required = false, skipRequired = false) => {
|
|
252
256
|
const errors = [];
|
|
253
257
|
if (!validation && !required) {
|
|
254
258
|
return errors;
|
|
255
259
|
}
|
|
256
|
-
// Check required
|
|
257
|
-
const isRequired = validation?.required ?? required;
|
|
260
|
+
// Check required (skipped when navigating between sections)
|
|
261
|
+
const isRequired = !skipRequired && (validation?.required ?? required);
|
|
258
262
|
// For boolean, false is a valid value, so only check for null/undefined/empty string
|
|
259
263
|
const isEmpty = value === null || value === undefined || value === '';
|
|
260
264
|
if (isRequired && isEmpty) {
|
|
261
265
|
errors.push('This field is required');
|
|
262
266
|
return errors; // Return early if required field is empty
|
|
263
267
|
}
|
|
264
|
-
// Skip
|
|
265
|
-
|
|
266
|
-
if (isEmpty && !isRequired) {
|
|
268
|
+
// Skip format/range validations if value is empty
|
|
269
|
+
if (isEmpty) {
|
|
267
270
|
return errors;
|
|
268
271
|
}
|
|
269
272
|
if (!validation) {
|
|
@@ -1005,6 +1008,19 @@ const getApiDataSource = async (dataSource, allValues, dataSourceRequestHandler,
|
|
|
1005
1008
|
// If not found and doesn't contain dots, try as widget-id
|
|
1006
1009
|
if ((depValue === null || depValue === undefined) && !dataSource.dependsOn.includes('.')) {
|
|
1007
1010
|
depValue = allValues[dataSource.dependsOn];
|
|
1011
|
+
// Smart resolution: If not found at top level, try to find the dependency in the same nested object
|
|
1012
|
+
// by looking for other keys in allValues that might contain the dependency.
|
|
1013
|
+
// We look for objects that contain both the current widget's path (if we can guess it) and the dependency.
|
|
1014
|
+
// But since we don't know the current widget's path here, we search for any object that has this dependency key.
|
|
1015
|
+
if (depValue === null || depValue === undefined || depValue === '') {
|
|
1016
|
+
for (const val of Object.values(allValues)) {
|
|
1017
|
+
if (val && typeof val === 'object' && !Array.isArray(val) && dataSource.dependsOn in val) {
|
|
1018
|
+
depValue = val[dataSource.dependsOn];
|
|
1019
|
+
if (depValue !== null && depValue !== undefined && depValue !== '')
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1008
1024
|
}
|
|
1009
1025
|
if (depValue === null || depValue === undefined || depValue === '') {
|
|
1010
1026
|
// If dependency is empty, return empty array
|
|
@@ -1048,8 +1064,8 @@ const getApiDataSource = async (dataSource, allValues, dataSourceRequestHandler,
|
|
|
1048
1064
|
}
|
|
1049
1065
|
}
|
|
1050
1066
|
else if (staticParams.level_id) {
|
|
1051
|
-
// First level has no parent
|
|
1052
|
-
requestParams.parent_level_value_id =
|
|
1067
|
+
// First level has no parent, send empty string as many OpenG2P APIs expect it
|
|
1068
|
+
requestParams.parent_level_value_id = "";
|
|
1053
1069
|
}
|
|
1054
1070
|
// Get service mnemonic and endpoint (required)
|
|
1055
1071
|
const service = dataSource.service;
|
|
@@ -1118,10 +1134,12 @@ const transformDataSourceOptions = (data, valueKey, labelKey) => {
|
|
|
1118
1134
|
return { value: item, label: String(item) };
|
|
1119
1135
|
});
|
|
1120
1136
|
}
|
|
1121
|
-
return data.map((item) =>
|
|
1122
|
-
value
|
|
1123
|
-
label
|
|
1124
|
-
|
|
1137
|
+
return data.map((item) => {
|
|
1138
|
+
const value = item[valueKey];
|
|
1139
|
+
// Try multiple common label keys if the primary one is missing
|
|
1140
|
+
const label = item[labelKey] || item.name || item.label || item.mnemonic || item.level_value_mnemonic || String(value);
|
|
1141
|
+
return { value, label };
|
|
1142
|
+
});
|
|
1125
1143
|
};
|
|
1126
1144
|
|
|
1127
1145
|
const WidgetEventBusContext = React.createContext(null);
|
|
@@ -1899,46 +1917,71 @@ const useBaseWidget = (options) => {
|
|
|
1899
1917
|
const userHasSetValueRef = React.useRef(false);
|
|
1900
1918
|
// Use ref for values to avoid stale closures in handleChange
|
|
1901
1919
|
const valuesRef = React.useRef(values);
|
|
1920
|
+
const loadingRef = React.useRef(loading);
|
|
1921
|
+
const dataSourceOptionsRef = React.useRef(dataSourceOptions);
|
|
1902
1922
|
React.useEffect(() => {
|
|
1903
1923
|
valuesRef.current = values;
|
|
1904
|
-
|
|
1924
|
+
loadingRef.current = loading;
|
|
1925
|
+
dataSourceOptionsRef.current = dataSourceOptions;
|
|
1926
|
+
}, [values, loading, dataSourceOptions]);
|
|
1905
1927
|
// Track last dispatched value to prevent duplicate dispatches
|
|
1906
1928
|
const lastDispatchedValueRef = React.useRef(null);
|
|
1907
|
-
//
|
|
1908
|
-
const
|
|
1909
|
-
if (
|
|
1910
|
-
return
|
|
1929
|
+
// Helper to extract displayable value from object (especially geo hierarchy objects)
|
|
1930
|
+
const extractValueFromObject = React.useCallback((obj) => {
|
|
1931
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
1932
|
+
return obj;
|
|
1911
1933
|
}
|
|
1912
|
-
//
|
|
1913
|
-
const
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
if (
|
|
1920
|
-
return
|
|
1934
|
+
// Check for geo hierarchy structure first
|
|
1935
|
+
const geoConfig = config['widget-geo-config'];
|
|
1936
|
+
if (geoConfig) {
|
|
1937
|
+
// If we have a geo hierarchy object, extract the value for this specific level
|
|
1938
|
+
const hierarchy = obj.hierarchy || obj.geo_code_hierarchy_json?.hierarchy;
|
|
1939
|
+
if (Array.isArray(hierarchy)) {
|
|
1940
|
+
const levelData = hierarchy.find((l) => l.level === geoConfig.level);
|
|
1941
|
+
if (levelData) {
|
|
1942
|
+
return levelData.level_value_id;
|
|
1921
1943
|
}
|
|
1922
|
-
// If it's a geo hierarchy object but no extractable ID, return undefined to avoid rendering object
|
|
1923
|
-
return undefined;
|
|
1924
1944
|
}
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1945
|
+
}
|
|
1946
|
+
if ('geo_code_hierarchy_json' in obj || 'geo_lowest_level_value_id' in obj || 'hierarchy' in obj) {
|
|
1947
|
+
if ('geo_lowest_level_value_id' in obj) {
|
|
1948
|
+
return obj.geo_lowest_level_value_id;
|
|
1928
1949
|
}
|
|
1929
|
-
if ('
|
|
1930
|
-
return obj.
|
|
1950
|
+
if ('lowest_level_value_id' in obj) {
|
|
1951
|
+
return obj.lowest_level_value_id;
|
|
1931
1952
|
}
|
|
1932
|
-
|
|
1933
|
-
|
|
1953
|
+
// Fallback for nested geo_code_hierarchy_json
|
|
1954
|
+
if (obj.geo_code_hierarchy_json?.lowest_level_value_id) {
|
|
1955
|
+
return obj.geo_code_hierarchy_json.lowest_level_value_id;
|
|
1934
1956
|
}
|
|
1935
|
-
if (
|
|
1936
|
-
return obj.
|
|
1957
|
+
if (obj.geo_code_hierarchy_json?.geo_lowest_level_value_id) {
|
|
1958
|
+
return obj.geo_code_hierarchy_json.geo_lowest_level_value_id;
|
|
1937
1959
|
}
|
|
1938
|
-
// If no extractable
|
|
1939
|
-
// This prevents "Objects are not valid as a React child" errors
|
|
1960
|
+
// If it's a geo hierarchy object but no extractable ID, return undefined to avoid rendering object
|
|
1940
1961
|
return undefined;
|
|
1941
|
-
}
|
|
1962
|
+
}
|
|
1963
|
+
// Try common value fields
|
|
1964
|
+
if ('value' in obj) {
|
|
1965
|
+
return obj.value;
|
|
1966
|
+
}
|
|
1967
|
+
if ('id' in obj) {
|
|
1968
|
+
return obj.id;
|
|
1969
|
+
}
|
|
1970
|
+
if ('label' in obj) {
|
|
1971
|
+
return obj.label;
|
|
1972
|
+
}
|
|
1973
|
+
if ('name' in obj) {
|
|
1974
|
+
return obj.name;
|
|
1975
|
+
}
|
|
1976
|
+
// If no extractable value found, return undefined to avoid rendering object as React child
|
|
1977
|
+
// This prevents "Objects are not valid as a React child" errors
|
|
1978
|
+
return undefined;
|
|
1979
|
+
}, [config]);
|
|
1980
|
+
// Get current value
|
|
1981
|
+
const currentValue = React.useMemo(() => {
|
|
1982
|
+
if (isLayoutWidget) {
|
|
1983
|
+
return undefined; // Layout widgets don't have values
|
|
1984
|
+
}
|
|
1942
1985
|
// Try to get value from widgetId first (this should have the actual selected value)
|
|
1943
1986
|
// For geo widgets with dataPath, widgetId stores the actual ID, while dataPath stores the hierarchy object
|
|
1944
1987
|
let value = values[widgetId];
|
|
@@ -1979,6 +2022,33 @@ const useBaseWidget = (options) => {
|
|
|
1979
2022
|
}
|
|
1980
2023
|
return value !== undefined ? value : config['widget-data-default'];
|
|
1981
2024
|
}, [values, config, widgetId, isLayoutWidget]);
|
|
2025
|
+
// Track the last value we attempted to mirror to prevent infinite loops
|
|
2026
|
+
const lastMirroredValueRef = React.useRef(null);
|
|
2027
|
+
// Mirror value from dataPath to widgetId in Redux state if it's not already there.
|
|
2028
|
+
// This is essential for widgets that depend on this widget via 'dependsOn' using its widgetId,
|
|
2029
|
+
// especially when the actual data is stored in a nested path.
|
|
2030
|
+
// CRITICAL: This ensures that dependencies are resolved correctly when entering Edit mode.
|
|
2031
|
+
React.useEffect(() => {
|
|
2032
|
+
if (isLayoutWidget || !config['widget-data-path']) {
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
const rawValue = getWidgetValue(values, config['widget-data-path'], widgetId);
|
|
2036
|
+
if (rawValue !== undefined && rawValue !== null) {
|
|
2037
|
+
const extractedValue = extractValueFromObject(rawValue);
|
|
2038
|
+
// Only mirror if:
|
|
2039
|
+
// 1. The top-level value is undefined (initial load or entering edit mode)
|
|
2040
|
+
// 2. We haven't already tried to mirror this specific value (prevents loops if dispatch is ignored or delayed)
|
|
2041
|
+
// 3. The extracted value is valid
|
|
2042
|
+
if (values[widgetId] === undefined &&
|
|
2043
|
+
extractedValue !== undefined &&
|
|
2044
|
+
extractedValue !== null &&
|
|
2045
|
+
lastMirroredValueRef.current !== extractedValue) {
|
|
2046
|
+
lastMirroredValueRef.current = extractedValue;
|
|
2047
|
+
dispatch(setValue({ widgetId, value: extractedValue }));
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2051
|
+
}, [values, config['widget-data-path'], widgetId, isLayoutWidget]);
|
|
1982
2052
|
// Initialize default value only once on mount (skip for layout widgets)
|
|
1983
2053
|
React.useEffect(() => {
|
|
1984
2054
|
if (isLayoutWidget) {
|
|
@@ -2001,6 +2071,20 @@ const useBaseWidget = (options) => {
|
|
|
2001
2071
|
if (currentValue === newValue) {
|
|
2002
2072
|
return;
|
|
2003
2073
|
}
|
|
2074
|
+
// CRITICAL FIX: Ignore auto-clears (empty string or undefined) from UI components
|
|
2075
|
+
// when the widget's data source is currently loading OR if options are empty.
|
|
2076
|
+
// This prevents data disappearance when switching to Edit mode and components
|
|
2077
|
+
// incorrectly clear values before options load or if handler is temporarily missing.
|
|
2078
|
+
if (newValue === '' || newValue === null || newValue === undefined) {
|
|
2079
|
+
if (loadingRef.current) {
|
|
2080
|
+
console.warn(`[useBaseWidget] Ignoring empty value for ${widgetId} because data source is loading`);
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
if (config['widget-data-source']?.type === 'api' && dataSourceOptionsRef.current.length === 0) {
|
|
2084
|
+
console.warn(`[useBaseWidget] Ignoring empty value for ${widgetId} because API options are empty`);
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2004
2088
|
// Mark that user has set a value (unless this is the default initialization)
|
|
2005
2089
|
if (newValue !== config['widget-data-default'] || userHasSetValueRef.current) {
|
|
2006
2090
|
userHasSetValueRef.current = true;
|
|
@@ -2018,6 +2102,13 @@ const useBaseWidget = (options) => {
|
|
|
2018
2102
|
}
|
|
2019
2103
|
else {
|
|
2020
2104
|
// Has dataPath: update both widgetId and dataPath
|
|
2105
|
+
// CRITICAL: For geo widgets, we do NOT want to overwrite the shared hierarchy dataPath
|
|
2106
|
+
// with a primitive value (the selected ID). The hierarchy object is managed by useGeoWidgetCascade.
|
|
2107
|
+
if (config['widget-geo-config']) {
|
|
2108
|
+
dispatch(setValue({ widgetId, value: newValue }));
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
// For non-geo widgets, update both widgetId and dataPath
|
|
2021
2112
|
// CRITICAL: Create updated values object with newValue already set
|
|
2022
2113
|
// This prevents setWidgetValue from reading stale values
|
|
2023
2114
|
const currentValuesWithUpdate = {
|
|
@@ -2112,6 +2203,17 @@ const useBaseWidget = (options) => {
|
|
|
2112
2203
|
const apiService = dataSource?.type === 'api' ? dataSource.service : '';
|
|
2113
2204
|
const apiEndpoint = dataSource?.type === 'api' ? dataSource.endpoint : '';
|
|
2114
2205
|
const configKey = `${widgetId}-${isReadonly}-${dataSource?.type || 'none'}-${apiService}-${apiEndpoint}`;
|
|
2206
|
+
// Extract dependency value using a granular selector to prevent unnecessary re-renders
|
|
2207
|
+
// and infinite loops when other unrelated values in the state change.
|
|
2208
|
+
const dependencyValue = reactRedux.useSelector((state) => {
|
|
2209
|
+
if (dataSource?.type !== 'api' || !dataSource.dependsOn) {
|
|
2210
|
+
return null;
|
|
2211
|
+
}
|
|
2212
|
+
if (dataSource.dependsOn.includes('.')) {
|
|
2213
|
+
return getWidgetValue(state.widget.values, dataSource.dependsOn, '');
|
|
2214
|
+
}
|
|
2215
|
+
return state.widget.values[dataSource.dependsOn];
|
|
2216
|
+
});
|
|
2115
2217
|
// Handle data source loading
|
|
2116
2218
|
React.useEffect(() => {
|
|
2117
2219
|
if (!dataSource) {
|
|
@@ -2132,6 +2234,17 @@ const useBaseWidget = (options) => {
|
|
|
2132
2234
|
}
|
|
2133
2235
|
else {
|
|
2134
2236
|
depValue = values[dataSource.dependsOn];
|
|
2237
|
+
// Smart resolution: If not found at top level, and current widget has a nested dataPath,
|
|
2238
|
+
// try to find the dependency in the same nested object.
|
|
2239
|
+
if ((depValue === undefined || depValue === null || depValue === '') &&
|
|
2240
|
+
typeof config['widget-data-path'] === 'string' &&
|
|
2241
|
+
config['widget-data-path'].includes('.')) {
|
|
2242
|
+
const pathParts = config['widget-data-path'].split('.');
|
|
2243
|
+
pathParts.pop(); // Remove current field name
|
|
2244
|
+
const prefix = pathParts.join('.');
|
|
2245
|
+
const tryPath = `${prefix}.${dataSource.dependsOn}`;
|
|
2246
|
+
depValue = getWidgetValue(values, tryPath, '');
|
|
2247
|
+
}
|
|
2135
2248
|
}
|
|
2136
2249
|
// If dependency is empty, don't load (will load when dependency has value)
|
|
2137
2250
|
if (depValue === null || depValue === undefined || depValue === '') {
|
|
@@ -2165,7 +2278,7 @@ const useBaseWidget = (options) => {
|
|
|
2165
2278
|
}
|
|
2166
2279
|
// Extract level_id from widget-geo-config.level if available
|
|
2167
2280
|
const levelId = geoConfig?.level;
|
|
2168
|
-
data = await getApiDataSource(dataSource,
|
|
2281
|
+
data = await getApiDataSource(dataSource, valuesRef.current, currentHandler, levelId);
|
|
2169
2282
|
}
|
|
2170
2283
|
else if (dataSource.type === 'schema') {
|
|
2171
2284
|
data = getSchemaDataSource(dataSource, schemaData || {});
|
|
@@ -2200,9 +2313,9 @@ const useBaseWidget = (options) => {
|
|
|
2200
2313
|
}
|
|
2201
2314
|
};
|
|
2202
2315
|
loadDataSource();
|
|
2203
|
-
// Use configKey to ensure effect runs when
|
|
2316
|
+
// Use configKey and dependencyValue to ensure effect runs only when relevant state changes
|
|
2204
2317
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2205
|
-
}, [configKey,
|
|
2318
|
+
}, [configKey, dependencyValue, dataSourceRequestHandler, schemaData, widgetId, dispatch]);
|
|
2206
2319
|
return {
|
|
2207
2320
|
widgetId,
|
|
2208
2321
|
value: currentValue,
|
|
@@ -2403,6 +2516,9 @@ const useGeoWidgetCascade = (options) => {
|
|
|
2403
2516
|
const geoConfig = config['widget-geo-config'];
|
|
2404
2517
|
const dataSource = config['widget-data-source'];
|
|
2405
2518
|
const dataPath = config['widget-data-path'];
|
|
2519
|
+
const groupId = typeof dataPath === 'string' && dataPath.includes('.')
|
|
2520
|
+
? dataPath.split('.').slice(0, -1).join('.')
|
|
2521
|
+
: 'default';
|
|
2406
2522
|
const valuesRef = React.useRef(values);
|
|
2407
2523
|
const handlerRef = React.useRef(dataSourceRequestHandler);
|
|
2408
2524
|
// Keep refs updated
|
|
@@ -2412,10 +2528,36 @@ const useGeoWidgetCascade = (options) => {
|
|
|
2412
2528
|
}, [values, dataSourceRequestHandler]);
|
|
2413
2529
|
// Get current value and data source options
|
|
2414
2530
|
const currentValue = reactRedux.useSelector((state) => {
|
|
2415
|
-
|
|
2416
|
-
|
|
2531
|
+
// Try to get value from widgetId first (most recent selection)
|
|
2532
|
+
let value = state.widget.values[widgetId];
|
|
2533
|
+
// If not found in widgetId, try dataPath
|
|
2534
|
+
if (value === undefined && dataPath) {
|
|
2535
|
+
value = getWidgetValue(state.widget.values, dataPath, widgetId);
|
|
2536
|
+
}
|
|
2537
|
+
// Extract value if it's a geo hierarchy object
|
|
2538
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && geoConfig) {
|
|
2539
|
+
const hierarchy = value.hierarchy || value.geo_code_hierarchy_json?.hierarchy;
|
|
2540
|
+
if (Array.isArray(hierarchy)) {
|
|
2541
|
+
const levelData = hierarchy.find((l) => l.level === geoConfig.level);
|
|
2542
|
+
if (levelData) {
|
|
2543
|
+
return levelData.level_value_id;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
// Extended fallbacks (matching useBaseWidget)
|
|
2547
|
+
if ('geo_lowest_level_value_id' in value) {
|
|
2548
|
+
return value.geo_lowest_level_value_id;
|
|
2549
|
+
}
|
|
2550
|
+
if ('lowest_level_value_id' in value) {
|
|
2551
|
+
return value.lowest_level_value_id;
|
|
2552
|
+
}
|
|
2553
|
+
if (value.geo_code_hierarchy_json?.lowest_level_value_id) {
|
|
2554
|
+
return value.geo_code_hierarchy_json.lowest_level_value_id;
|
|
2555
|
+
}
|
|
2556
|
+
if (value.geo_code_hierarchy_json?.geo_lowest_level_value_id) {
|
|
2557
|
+
return value.geo_code_hierarchy_json.geo_lowest_level_value_id;
|
|
2558
|
+
}
|
|
2417
2559
|
}
|
|
2418
|
-
return
|
|
2560
|
+
return value;
|
|
2419
2561
|
});
|
|
2420
2562
|
// Memoize selector to avoid returning new array reference
|
|
2421
2563
|
const dataSourceOptions = reactRedux.useSelector((state) => state.widget.dataSources[widgetId] ?? EMPTY_DATA_SOURCE);
|
|
@@ -2435,11 +2577,17 @@ const useGeoWidgetCascade = (options) => {
|
|
|
2435
2577
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
2436
2578
|
const currentValues = valuesRef.current;
|
|
2437
2579
|
const currentHandler = handlerRef.current;
|
|
2438
|
-
// CRITICAL:
|
|
2439
|
-
|
|
2440
|
-
|
|
2580
|
+
// CRITICAL: Try to get parent value from event first, then from Redux
|
|
2581
|
+
let parentValue = event.value;
|
|
2582
|
+
if (parentValue === undefined || parentValue === null) {
|
|
2583
|
+
parentValue = currentValues[parentWidgetId];
|
|
2584
|
+
// If not found in top-level values, try to find it via dataPath or dependsOn
|
|
2585
|
+
if (parentValue === undefined && dataSource.dependsOn) {
|
|
2586
|
+
parentValue = getWidgetValue(currentValues, dataSource.dependsOn, '');
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2441
2589
|
// Remove this level and all below from hierarchy
|
|
2442
|
-
geoHierarchyBuilder.removeLevelAndBelow(level);
|
|
2590
|
+
geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
|
|
2443
2591
|
// Clear this widget's value
|
|
2444
2592
|
// CRITICAL: Only dispatch setValue for THIS widget, not for parent or other widgets
|
|
2445
2593
|
// setWidgetValue returns the entire updated state, but we only want to update this widget
|
|
@@ -2494,7 +2642,8 @@ const useGeoWidgetCascade = (options) => {
|
|
|
2494
2642
|
}
|
|
2495
2643
|
}
|
|
2496
2644
|
else {
|
|
2497
|
-
// If parent value is cleared, clear the data source
|
|
2645
|
+
// If parent value is cleared, clear the data source and hierarchy
|
|
2646
|
+
geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
|
|
2498
2647
|
dispatch(setDataSource({ widgetId, data: [] }));
|
|
2499
2648
|
}
|
|
2500
2649
|
};
|
|
@@ -2509,25 +2658,49 @@ const useGeoWidgetCascade = (options) => {
|
|
|
2509
2658
|
if (!geoConfig) {
|
|
2510
2659
|
return;
|
|
2511
2660
|
}
|
|
2512
|
-
// Skip if value is
|
|
2513
|
-
if
|
|
2514
|
-
|
|
2661
|
+
// Skip if value is undefined (it might still be loading or rehydrating)
|
|
2662
|
+
// ONLY clear hierarchy if the value is explicitly null or empty string (user action)
|
|
2663
|
+
if (currentValue === null || currentValue === '') {
|
|
2515
2664
|
const { level } = geoConfig;
|
|
2516
|
-
geoHierarchyBuilder.removeLevelAndBelow(level);
|
|
2665
|
+
geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
|
|
2666
|
+
// If we have a dataPath, we need to update Redux with the cleared hierarchy
|
|
2667
|
+
if (dataPath) {
|
|
2668
|
+
const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson(groupId);
|
|
2669
|
+
let finalUpdatedValues = valuesRef.current;
|
|
2670
|
+
// Use logic similar to the build section below to update the dataPath
|
|
2671
|
+
if (typeof dataPath === 'string' && dataPath.endsWith('.geo_code_hierarchy_json')) {
|
|
2672
|
+
const prefix = dataPath.substring(0, dataPath.lastIndexOf('.'));
|
|
2673
|
+
finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson?.geo_code_hierarchy_json);
|
|
2674
|
+
finalUpdatedValues = setWidgetValue(finalUpdatedValues, `${prefix}.geo_lowest_level_value_id`, widgetId, hierarchyJson?.geo_lowest_level_value_id);
|
|
2675
|
+
}
|
|
2676
|
+
else {
|
|
2677
|
+
finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson?.geo_code_hierarchy_json);
|
|
2678
|
+
}
|
|
2679
|
+
dispatch(setValues(finalUpdatedValues));
|
|
2680
|
+
}
|
|
2517
2681
|
return;
|
|
2518
2682
|
}
|
|
2683
|
+
if (currentValue === undefined) {
|
|
2684
|
+
return; // Skip if undefined (still initializing)
|
|
2685
|
+
}
|
|
2519
2686
|
const { level, isLastLevel } = geoConfig;
|
|
2520
|
-
//
|
|
2521
|
-
if (
|
|
2687
|
+
// Check if hierarchy is already built to prevent endless loops
|
|
2688
|
+
if (dataPath) {
|
|
2522
2689
|
const currentHierarchy = getWidgetValue(valuesRef.current, dataPath, widgetId);
|
|
2523
2690
|
// If hierarchy JSON is already set and matches current value, skip rebuilding
|
|
2524
|
-
if (currentHierarchy && typeof currentHierarchy === 'object'
|
|
2525
|
-
// Check if
|
|
2526
|
-
const
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2691
|
+
if (currentHierarchy && typeof currentHierarchy === 'object') {
|
|
2692
|
+
// Check if this specific level's value matches the hierarchy
|
|
2693
|
+
const hierarchyArray = currentHierarchy.hierarchy || currentHierarchy.geo_code_hierarchy_json?.hierarchy;
|
|
2694
|
+
if (Array.isArray(hierarchyArray)) {
|
|
2695
|
+
const currentLevelValue = typeof currentValue === 'object'
|
|
2696
|
+
? (currentValue.level_value_id || currentValue.id || currentValue.value)
|
|
2697
|
+
: currentValue;
|
|
2698
|
+
const levelData = hierarchyArray.find((l) => l.level === geoConfig.level);
|
|
2699
|
+
// If this level is already correctly represented in the hierarchy, skip rebuilding
|
|
2700
|
+
// String conversion ensures comparison works for mixed types
|
|
2701
|
+
if (levelData && String(levelData.level_value_id) === String(currentLevelValue)) {
|
|
2702
|
+
return;
|
|
2703
|
+
}
|
|
2531
2704
|
}
|
|
2532
2705
|
}
|
|
2533
2706
|
}
|
|
@@ -2550,23 +2723,31 @@ const useGeoWidgetCascade = (options) => {
|
|
|
2550
2723
|
return;
|
|
2551
2724
|
}
|
|
2552
2725
|
// When a widget's own value changes, remove this level and all below from hierarchy first
|
|
2553
|
-
|
|
2554
|
-
// The addLevel method already handles removing existing levels, but we explicitly clear to be safe
|
|
2555
|
-
geoHierarchyBuilder.removeLevelAndBelow(level);
|
|
2726
|
+
geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
|
|
2556
2727
|
// Add level to hierarchy
|
|
2557
|
-
geoHierarchyBuilder.addLevel(level, level_value_id, level_value_mnemonic);
|
|
2558
|
-
//
|
|
2559
|
-
if (
|
|
2560
|
-
const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson();
|
|
2728
|
+
geoHierarchyBuilder.addLevel(level, level_value_id, level_value_mnemonic, groupId);
|
|
2729
|
+
// Build and store hierarchy JSON on every change
|
|
2730
|
+
if (dataPath) {
|
|
2731
|
+
const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson(groupId);
|
|
2561
2732
|
if (hierarchyJson) {
|
|
2562
|
-
//
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2733
|
+
// Fix: Avoid double nesting of geo_code_hierarchy_json
|
|
2734
|
+
// If dataPath ends with .geo_code_hierarchy_json, we want to save the content directly to it
|
|
2735
|
+
// and save the lowest level ID as a sibling
|
|
2736
|
+
let finalUpdatedValues = valuesRef.current;
|
|
2737
|
+
if (typeof dataPath === 'string' && dataPath.endsWith('.geo_code_hierarchy_json')) {
|
|
2738
|
+
const prefix = dataPath.substring(0, dataPath.lastIndexOf('.'));
|
|
2739
|
+
// Save hierarchy JSON content directly to dataPath (avoiding double nesting)
|
|
2740
|
+
finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson.geo_code_hierarchy_json);
|
|
2741
|
+
// Save lowest level ID as sibling
|
|
2742
|
+
finalUpdatedValues = setWidgetValue(finalUpdatedValues, `${prefix}.geo_lowest_level_value_id`, widgetId, hierarchyJson.geo_lowest_level_value_id);
|
|
2743
|
+
}
|
|
2744
|
+
else {
|
|
2745
|
+
// Fallback if path doesn't follow the naming convention
|
|
2746
|
+
finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson.geo_code_hierarchy_json);
|
|
2747
|
+
}
|
|
2748
|
+
// CRITICAL: Use setValues for deep merge instead of replacing root keys with setValue
|
|
2749
|
+
// setWidgetValue returns the complete updated state object with all keys preserved
|
|
2750
|
+
dispatch(setValues(finalUpdatedValues));
|
|
2570
2751
|
}
|
|
2571
2752
|
}
|
|
2572
2753
|
}, [geoConfig, currentValue, widgetId, dataPath, dispatch, dataSourceOptions]);
|
|
@@ -3629,7 +3810,15 @@ const collectWidgets = (panels) => {
|
|
|
3629
3810
|
});
|
|
3630
3811
|
return widgets;
|
|
3631
3812
|
};
|
|
3632
|
-
|
|
3813
|
+
/**
|
|
3814
|
+
* Validate all widgets in a section and dispatch errors to the store.
|
|
3815
|
+
*
|
|
3816
|
+
* @param skipRequired - When true, required-field checks (widget-required,
|
|
3817
|
+
* validation.required, document-required) are skipped. Use this for
|
|
3818
|
+
* per-section Save/Next navigation so the user can advance without filling
|
|
3819
|
+
* every mandatory field; only format/range errors are reported.
|
|
3820
|
+
*/
|
|
3821
|
+
const sectionValidate = (section, currentSchemaData, dispatch, skipRequired = false) => {
|
|
3633
3822
|
const allWidgets = collectWidgets(section.panels);
|
|
3634
3823
|
let isValid = true;
|
|
3635
3824
|
for (const widget of allWidgets) {
|
|
@@ -3638,7 +3827,7 @@ const sectionValidate = (section, currentSchemaData, dispatch) => {
|
|
|
3638
3827
|
continue;
|
|
3639
3828
|
const widgetId = widget['widget-id'];
|
|
3640
3829
|
const value = getWidgetValue(currentSchemaData, widget['widget-data-path'], widgetId);
|
|
3641
|
-
const errors = validateWidget(value, widget['widget-data-validation'], widget['widget-required']);
|
|
3830
|
+
const errors = validateWidget(value, widget['widget-data-validation'], widget['widget-required'], skipRequired);
|
|
3642
3831
|
if (errors.length > 0) {
|
|
3643
3832
|
isValid = false;
|
|
3644
3833
|
dispatch(setTouched({ widgetId, touched: true }));
|
|
@@ -3649,10 +3838,10 @@ const sectionValidate = (section, currentSchemaData, dispatch) => {
|
|
|
3649
3838
|
dispatch(setError({ widgetId, errors: [] }));
|
|
3650
3839
|
}
|
|
3651
3840
|
}
|
|
3652
|
-
// Supporting Documents
|
|
3841
|
+
// Supporting Documents — only check required when not skipping required validation
|
|
3653
3842
|
section['section-supporting-documents']?.forEach((doc, index) => {
|
|
3654
3843
|
const widgetId = `supporting-doc-${section['section-id']}-${index}`;
|
|
3655
|
-
if (doc['document-required']) {
|
|
3844
|
+
if (!skipRequired && doc['document-required']) {
|
|
3656
3845
|
const file = getValueByPath(currentSchemaData, doc['document-data-path']);
|
|
3657
3846
|
if (!file) {
|
|
3658
3847
|
isValid = false;
|
|
@@ -3671,6 +3860,85 @@ const sectionValidate = (section, currentSchemaData, dispatch) => {
|
|
|
3671
3860
|
return isValid;
|
|
3672
3861
|
};
|
|
3673
3862
|
|
|
3863
|
+
/** Table-style widgets that bind to an array path in the store / schema. */
|
|
3864
|
+
function isTableLikeWidget(widget) {
|
|
3865
|
+
const w = widget.widget;
|
|
3866
|
+
/** widget-type union in types omits legacy values like simple-table still used at runtime */
|
|
3867
|
+
const t = widget['widget-type'];
|
|
3868
|
+
return (w === 'table' ||
|
|
3869
|
+
w === 'dialog-table' ||
|
|
3870
|
+
w === 'simple-table' ||
|
|
3871
|
+
t === 'table' ||
|
|
3872
|
+
t === 'simple-table');
|
|
3873
|
+
}
|
|
3874
|
+
/**
|
|
3875
|
+
* Resolve `records` for section save payloads.
|
|
3876
|
+
* - Back-compat: path ending in `.records` (e.g. `regId.records`)
|
|
3877
|
+
* - Else: first array snapshot at a string `widget-data-path` on a table-like widget
|
|
3878
|
+
* (e.g. `household.members` for dialog-table)
|
|
3879
|
+
*/
|
|
3880
|
+
function extractTableRecordsFromSnapshot(snapshot, sectionWidgets) {
|
|
3881
|
+
const convention = Object.entries(snapshot).find(([key, value]) => key.endsWith('.records') && Array.isArray(value));
|
|
3882
|
+
if (convention) {
|
|
3883
|
+
return convention[1];
|
|
3884
|
+
}
|
|
3885
|
+
const tablePaths = [];
|
|
3886
|
+
sectionWidgets.forEach((widget) => {
|
|
3887
|
+
if (!isTableLikeWidget(widget))
|
|
3888
|
+
return;
|
|
3889
|
+
const p = widget['widget-data-path'];
|
|
3890
|
+
if (typeof p === 'string' && p.length > 0) {
|
|
3891
|
+
tablePaths.push(p);
|
|
3892
|
+
}
|
|
3893
|
+
else if (p && typeof p === 'object') {
|
|
3894
|
+
Object.values(p).forEach((sub) => {
|
|
3895
|
+
if (typeof sub === 'string' && sub.length > 0)
|
|
3896
|
+
tablePaths.push(sub);
|
|
3897
|
+
});
|
|
3898
|
+
}
|
|
3899
|
+
});
|
|
3900
|
+
for (const path of tablePaths) {
|
|
3901
|
+
const val = snapshot[path];
|
|
3902
|
+
if (Array.isArray(val)) {
|
|
3903
|
+
return val;
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
return [];
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3909
|
+
/** Root class on readonly label/value rows; SectionRenderer scopes overflow/ellipsis rules here. */
|
|
3910
|
+
const READONLY_VALUE_ROW_ROOT_CLASSES = [
|
|
3911
|
+
'TextDisplayWidget',
|
|
3912
|
+
'TextAreaDisplayWidget',
|
|
3913
|
+
'SelectDisplayWidget',
|
|
3914
|
+
'PhoneDisplayWidget',
|
|
3915
|
+
'NumberDisplayWidget',
|
|
3916
|
+
'CurrencyDisplayWidget',
|
|
3917
|
+
'RadioDisplayWidget',
|
|
3918
|
+
'DateDisplayWidget',
|
|
3919
|
+
'DateTimeDisplayWidget',
|
|
3920
|
+
'CheckboxDisplayWidget',
|
|
3921
|
+
'BooleanDisplayWidget',
|
|
3922
|
+
'FileDisplayWidget',
|
|
3923
|
+
'DisplayFieldWidget',
|
|
3924
|
+
];
|
|
3925
|
+
/** Rows whose value is one line in .flex-1 > .text-gray-900 (ellipsis; full string via title on the element). */
|
|
3926
|
+
const READONLY_SINGLE_LINE_VALUE_ROW_CLASSES = [
|
|
3927
|
+
'TextDisplayWidget',
|
|
3928
|
+
'SelectDisplayWidget',
|
|
3929
|
+
'PhoneDisplayWidget',
|
|
3930
|
+
'NumberDisplayWidget',
|
|
3931
|
+
'CurrencyDisplayWidget',
|
|
3932
|
+
'RadioDisplayWidget',
|
|
3933
|
+
'DateDisplayWidget',
|
|
3934
|
+
'DateTimeDisplayWidget',
|
|
3935
|
+
'CheckboxDisplayWidget',
|
|
3936
|
+
'BooleanDisplayWidget',
|
|
3937
|
+
'DisplayFieldWidget',
|
|
3938
|
+
];
|
|
3939
|
+
function scopedClassSelectors(sectionClassId, classNames) {
|
|
3940
|
+
return classNames.map((c) => `.${sectionClassId} .${c}`).join(',\n ');
|
|
3941
|
+
}
|
|
3674
3942
|
/**
|
|
3675
3943
|
* Renders a section with its panels
|
|
3676
3944
|
*
|
|
@@ -3699,41 +3967,22 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
3699
3967
|
}
|
|
3700
3968
|
return section;
|
|
3701
3969
|
}, [section, namespace]);
|
|
3702
|
-
// Create namespaced schemaData if namespace is provided
|
|
3703
|
-
//
|
|
3970
|
+
// Create namespaced schemaData if namespace is provided.
|
|
3971
|
+
// Widgets with namespaced data-paths (e.g. "rv-section-0.a1a4d25a.birth_date")
|
|
3972
|
+
// need a nested object at values[namespace] so getValueByPath can traverse it.
|
|
3704
3973
|
const namespacedSchemaData = React.useMemo(() => {
|
|
3705
3974
|
if (!namespace || !currentSchemaData) {
|
|
3706
3975
|
return schemaData;
|
|
3707
3976
|
}
|
|
3708
|
-
|
|
3709
|
-
const namespaced = { ...currentSchemaData };
|
|
3710
|
-
// Copy all top-level keys to namespaced paths
|
|
3711
|
-
Object.keys(currentSchemaData).forEach(key => {
|
|
3712
|
-
const namespacedKey = `${namespace}.${key}`;
|
|
3713
|
-
if (!(namespacedKey in namespaced)) {
|
|
3714
|
-
namespaced[namespacedKey] = currentSchemaData[key];
|
|
3715
|
-
}
|
|
3716
|
-
});
|
|
3717
|
-
// Also handle nested objects - copy nested values to namespaced paths
|
|
3718
|
-
const copyNestedValues = (obj, prefix = '') => {
|
|
3719
|
-
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
3720
|
-
Object.keys(obj).forEach(key => {
|
|
3721
|
-
const fullPath = prefix ? `${prefix}.${key}` : key;
|
|
3722
|
-
const namespacedPath = `${namespace}.${fullPath}`;
|
|
3723
|
-
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
|
|
3724
|
-
copyNestedValues(obj[key], fullPath);
|
|
3725
|
-
// Also set the nested object at the namespaced path
|
|
3726
|
-
setValueByPath(namespaced, namespacedPath, obj[key]);
|
|
3727
|
-
}
|
|
3728
|
-
else {
|
|
3729
|
-
setValueByPath(namespaced, namespacedPath, obj[key]);
|
|
3730
|
-
}
|
|
3731
|
-
});
|
|
3732
|
-
}
|
|
3733
|
-
};
|
|
3734
|
-
copyNestedValues(currentSchemaData);
|
|
3735
|
-
return namespaced;
|
|
3977
|
+
return { ...currentSchemaData, [namespace]: currentSchemaData };
|
|
3736
3978
|
}, [namespace, schemaData, currentSchemaData]);
|
|
3979
|
+
// Populate the store with namespaced schema data so that namespaced widgets
|
|
3980
|
+
// can read their initial values via getValueByPath on the namespaced paths.
|
|
3981
|
+
React.useEffect(() => {
|
|
3982
|
+
if (namespace && namespacedSchemaData) {
|
|
3983
|
+
dispatch(setValues(namespacedSchemaData));
|
|
3984
|
+
}
|
|
3985
|
+
}, [namespace, namespacedSchemaData, dispatch]);
|
|
3737
3986
|
const crViewData = React.useMemo(() => {
|
|
3738
3987
|
if (mode !== 'CRView')
|
|
3739
3988
|
return null;
|
|
@@ -3756,21 +4005,25 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
3756
4005
|
const sectionId = sectionToRender['section-id'];
|
|
3757
4006
|
const gridId = `section-panels-${sectionId}`;
|
|
3758
4007
|
const sectionClassId = `section-${sectionId}`;
|
|
4008
|
+
const readonlyValueRowRootsCss = React.useMemo(() => scopedClassSelectors(sectionClassId, READONLY_VALUE_ROW_ROOT_CLASSES), [sectionClassId]);
|
|
4009
|
+
const readonlyValueRowFlex1Css = React.useMemo(() => READONLY_VALUE_ROW_ROOT_CLASSES.map((c) => `.${sectionClassId} .${c} > .flex-1`).join(',\n '), [sectionClassId]);
|
|
4010
|
+
const readonlySingleLineValueTextCss = React.useMemo(() => READONLY_SINGLE_LINE_VALUE_ROW_CLASSES.map((c) => `.${sectionClassId} .${c} > .flex-1 > .text-gray-900`).join(',\n '), [sectionClassId]);
|
|
3759
4011
|
// IntakeForm mode: accordion expand/collapse state (supports toggle)
|
|
3760
4012
|
const [standaloneExpanded, setStandaloneExpanded] = React.useState(true); // For sectionIndex undefined (standalone use)
|
|
3761
4013
|
const isExpandedFromContainer = typeof sectionIndex === 'number' && expandedSectionIndex === sectionIndex;
|
|
3762
4014
|
const isExpandedStandalone = sectionIndex === undefined && standaloneExpanded;
|
|
3763
4015
|
const isExpanded = mode === 'IntakeForm' && (isExpandedFromContainer || isExpandedStandalone);
|
|
4016
|
+
// When the section is managed by SectionsContainer (sectionIndex is set), header clicks are
|
|
4017
|
+
// disabled — navigation is only allowed via the Next / Previous buttons.
|
|
4018
|
+
// Standalone usage (sectionIndex undefined) keeps the classic toggle behaviour.
|
|
3764
4019
|
const handleAccordionToggle = React.useCallback(() => {
|
|
3765
4020
|
if (mode !== 'IntakeForm')
|
|
3766
4021
|
return;
|
|
3767
|
-
if (
|
|
3768
|
-
onExpandSection(sectionIndex);
|
|
3769
|
-
}
|
|
3770
|
-
else if (sectionIndex === undefined) {
|
|
4022
|
+
if (sectionIndex === undefined) {
|
|
3771
4023
|
setStandaloneExpanded(prev => !prev);
|
|
3772
4024
|
}
|
|
3773
|
-
|
|
4025
|
+
// Intentionally no-op when sectionIndex is set (managed by SectionsContainer)
|
|
4026
|
+
}, [mode, sectionIndex]);
|
|
3774
4027
|
// Recursively count all vertical panels, especially those nested inside horizontal panels
|
|
3775
4028
|
// Typically: horizontal panels at first level contain vertical panels at second level
|
|
3776
4029
|
// Accounts for panel-column-span: a panel with column-span 3 counts as 3 columns
|
|
@@ -4019,9 +4272,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4019
4272
|
const widgetPath = widget['widget-data-path'];
|
|
4020
4273
|
if (!widgetPath)
|
|
4021
4274
|
return;
|
|
4022
|
-
if (widget
|
|
4023
|
-
widget['widget-type'] === 'simple-table' ||
|
|
4024
|
-
widget['widget'] === 'table') {
|
|
4275
|
+
if (isTableLikeWidget(widget)) {
|
|
4025
4276
|
hasTable = true;
|
|
4026
4277
|
}
|
|
4027
4278
|
if (typeof widgetPath === 'object') {
|
|
@@ -4056,8 +4307,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4056
4307
|
},
|
|
4057
4308
|
];
|
|
4058
4309
|
}
|
|
4059
|
-
|
|
4060
|
-
return tableEntry ? tableEntry[1] : [];
|
|
4310
|
+
return extractTableRecordsFromSnapshot(snapshot, widgets);
|
|
4061
4311
|
};
|
|
4062
4312
|
// Get original section (without namespace) for building snapshots
|
|
4063
4313
|
// This ensures we use the original data paths when saving
|
|
@@ -4108,6 +4358,8 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4108
4358
|
const baselineSnapshotRef = React.useRef(null);
|
|
4109
4359
|
// IntakeForm only: increment when baseline is updated after save - forces badge to update (refs don't trigger re-renders)
|
|
4110
4360
|
const [intakeFormBaselineTrigger, setIntakeFormBaselineTrigger] = React.useState(0);
|
|
4361
|
+
// IntakeForm only: tracks whether the user has actually saved this section (prevents "Saved" badge on initial load)
|
|
4362
|
+
const [hasBeenSavedByUser, setHasBeenSavedByUser] = React.useState(false);
|
|
4111
4363
|
// IntakeForm: treat as edit mode for dirty tracking when isDraft. RegistryView: use isEditMode.
|
|
4112
4364
|
const effectiveEditModeForDirty = mode === 'IntakeForm' ? (isDraft !== false) : isEditMode;
|
|
4113
4365
|
// Compute isDirty: compare current store state to baseline (only when in edit mode)
|
|
@@ -4150,18 +4402,68 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4150
4402
|
const intakeFormSectionStatus = React.useMemo(() => {
|
|
4151
4403
|
if (mode !== 'IntakeForm' || isDraft === false)
|
|
4152
4404
|
return null;
|
|
4153
|
-
const hasValue = (v) => v !== undefined && v !== null && (typeof v !== 'string' || v.trim().length > 0);
|
|
4154
|
-
const currentSnapshot = buildSectionSnapshot(storeValues, namespace);
|
|
4155
|
-
const record = currentSnapshot.records?.[0];
|
|
4156
|
-
const hasData = record &&
|
|
4157
|
-
typeof record === 'object' &&
|
|
4158
|
-
Object.values(record).some((v) => hasValue(v));
|
|
4159
4405
|
if (isDirty)
|
|
4160
4406
|
return 'modified';
|
|
4161
|
-
if (
|
|
4407
|
+
if (hasBeenSavedByUser)
|
|
4162
4408
|
return 'saved';
|
|
4163
4409
|
return null;
|
|
4164
|
-
}, [mode, isDirty,
|
|
4410
|
+
}, [mode, isDirty, hasBeenSavedByUser]);
|
|
4411
|
+
// Revert store values to the original schemaData for this section's widgets.
|
|
4412
|
+
// Used by both handleSave (RegistryView raises a CR, so values should not persist)
|
|
4413
|
+
// and handleCancel.
|
|
4414
|
+
const revertToOriginalValues = React.useCallback(() => {
|
|
4415
|
+
const sectionWidgets = collectWidgets(originalSection.panels);
|
|
4416
|
+
const oldSchemaData = schemaData || contextSchemaData;
|
|
4417
|
+
const currentStoreValues = store.getState().widget.values;
|
|
4418
|
+
let newStoreValues = currentStoreValues;
|
|
4419
|
+
sectionWidgets.forEach(widget => {
|
|
4420
|
+
const originalWidgetId = widget['widget-id'];
|
|
4421
|
+
const namespacedWidgetId = namespace ? `${namespace}__${originalWidgetId}` : originalWidgetId;
|
|
4422
|
+
const widgetId = namespacedWidgetId;
|
|
4423
|
+
const originalDataPath = widget['widget-data-path'];
|
|
4424
|
+
const storeDataPath = namespace && originalDataPath
|
|
4425
|
+
? (typeof originalDataPath === 'string'
|
|
4426
|
+
? `${namespace}.${originalDataPath}`
|
|
4427
|
+
: Object.fromEntries(Object.entries(originalDataPath).map(([key, path]) => [key, `${namespace}.${path}`])))
|
|
4428
|
+
: originalDataPath;
|
|
4429
|
+
if (widgetId && originalDataPath) {
|
|
4430
|
+
let oldValue;
|
|
4431
|
+
if (typeof originalDataPath === 'object') {
|
|
4432
|
+
oldValue = {};
|
|
4433
|
+
Object.entries(originalDataPath).forEach(([key, path]) => {
|
|
4434
|
+
if (typeof path === 'string') {
|
|
4435
|
+
oldValue[key] = getValueByPath(oldSchemaData, path);
|
|
4436
|
+
}
|
|
4437
|
+
});
|
|
4438
|
+
}
|
|
4439
|
+
else if (typeof originalDataPath === 'string') {
|
|
4440
|
+
oldValue = getValueByPath(oldSchemaData, originalDataPath);
|
|
4441
|
+
}
|
|
4442
|
+
if (oldValue !== undefined) {
|
|
4443
|
+
newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
|
|
4444
|
+
// Also revert the widgetId-based entry — useBaseWidget.handleChange
|
|
4445
|
+
// sets values[widgetId] during editing, and useBaseWidget.currentValue
|
|
4446
|
+
// reads values[widgetId] first before falling through to the dataPath.
|
|
4447
|
+
newStoreValues = { ...newStoreValues, [widgetId]: oldValue };
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
});
|
|
4451
|
+
if (hasSupportingDocuments) {
|
|
4452
|
+
const originalSupportingDocuments = originalSection['section-supporting-documents'] || [];
|
|
4453
|
+
originalSupportingDocuments.forEach((doc, index) => {
|
|
4454
|
+
const widgetId = `supporting-doc-${sectionId}-${index}`;
|
|
4455
|
+
const originalDataPath = doc['document-data-path'];
|
|
4456
|
+
const storeDataPath = namespace && originalDataPath
|
|
4457
|
+
? `${namespace}.${originalDataPath}`
|
|
4458
|
+
: originalDataPath;
|
|
4459
|
+
const oldValue = getValueByPath(oldSchemaData, originalDataPath);
|
|
4460
|
+
newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
|
|
4461
|
+
});
|
|
4462
|
+
}
|
|
4463
|
+
if (newStoreValues !== currentStoreValues) {
|
|
4464
|
+
dispatch(setValues(newStoreValues));
|
|
4465
|
+
}
|
|
4466
|
+
}, [originalSection, schemaData, contextSchemaData, store, namespace, hasSupportingDocuments, sectionId, dispatch]);
|
|
4165
4467
|
// Handle save button click
|
|
4166
4468
|
const handleSave = async () => {
|
|
4167
4469
|
if (!store || !onSectionSave) {
|
|
@@ -4175,7 +4477,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4175
4477
|
const sectionWidgets = collectWidgets(originalSection.panels);
|
|
4176
4478
|
const currentState = store.getState().widget;
|
|
4177
4479
|
const currentSchemaData = currentState.values || {};
|
|
4178
|
-
const isSectionValid = sectionValidate(originalSection, currentSchemaData, dispatch);
|
|
4480
|
+
const isSectionValid = sectionValidate(originalSection, currentSchemaData, dispatch, true);
|
|
4179
4481
|
if (!isSectionValid) {
|
|
4180
4482
|
return;
|
|
4181
4483
|
}
|
|
@@ -4198,12 +4500,24 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4198
4500
|
});
|
|
4199
4501
|
}
|
|
4200
4502
|
if (JSON.stringify(oldSchemaData) !== JSON.stringify(newSchemaData)) {
|
|
4503
|
+
let profileImage = null;
|
|
4504
|
+
for (const record of newSchemaData) {
|
|
4505
|
+
if (typeof record === 'object' && record !== null) {
|
|
4506
|
+
for (const [key, value] of Object.entries(record)) {
|
|
4507
|
+
if (value instanceof File) {
|
|
4508
|
+
profileImage = value;
|
|
4509
|
+
record[key] = '';
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4201
4514
|
try {
|
|
4202
4515
|
const sectionchanges = {
|
|
4203
4516
|
section_id: dbSectionId ?? originalSection['section-id'],
|
|
4204
4517
|
section_register_id: sectionRegisterId,
|
|
4205
4518
|
records: [...newSchemaData],
|
|
4206
|
-
files: [...sectionFiles]
|
|
4519
|
+
files: [...sectionFiles],
|
|
4520
|
+
...(profileImage ? { image: profileImage } : {}),
|
|
4207
4521
|
};
|
|
4208
4522
|
await onSectionSave(sectionchanges);
|
|
4209
4523
|
}
|
|
@@ -4211,6 +4525,12 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4211
4525
|
console.error('Section Changes Save failed', error);
|
|
4212
4526
|
}
|
|
4213
4527
|
}
|
|
4528
|
+
// In RegistryView, save raises a CR — the actual data update follows a
|
|
4529
|
+
// separate approval workflow, so revert the displayed values to the
|
|
4530
|
+
// originals so the view doesn't show unapproved edits.
|
|
4531
|
+
if (mode === 'RegistryView') {
|
|
4532
|
+
revertToOriginalValues();
|
|
4533
|
+
}
|
|
4214
4534
|
setIsEditMode(false);
|
|
4215
4535
|
onEditModeChange?.(originalSectionId, false);
|
|
4216
4536
|
};
|
|
@@ -4223,7 +4543,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4223
4543
|
const sectionWidgets = collectWidgets(originalSection.panels);
|
|
4224
4544
|
const currentState = store.getState().widget;
|
|
4225
4545
|
const currentSchemaData = currentState.values || {};
|
|
4226
|
-
const isSectionValid = sectionValidate(originalSection, currentSchemaData, dispatch);
|
|
4546
|
+
const isSectionValid = sectionValidate(originalSection, currentSchemaData, dispatch, true);
|
|
4227
4547
|
if (!isSectionValid)
|
|
4228
4548
|
return;
|
|
4229
4549
|
const oldSchemaData = schemaData || contextSchemaData;
|
|
@@ -4240,12 +4560,24 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4240
4560
|
});
|
|
4241
4561
|
}
|
|
4242
4562
|
if (JSON.stringify(oldSchemaData) !== JSON.stringify(newSchemaData)) {
|
|
4563
|
+
let profileImage = null;
|
|
4564
|
+
for (const record of newSchemaData) {
|
|
4565
|
+
if (typeof record === 'object' && record !== null) {
|
|
4566
|
+
for (const [key, value] of Object.entries(record)) {
|
|
4567
|
+
if (value instanceof File) {
|
|
4568
|
+
profileImage = value;
|
|
4569
|
+
record[key] = '';
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4243
4574
|
try {
|
|
4244
4575
|
await onSectionSave({
|
|
4245
4576
|
section_id: dbSectionId ?? originalSection['section-id'],
|
|
4246
4577
|
section_register_id: sectionRegisterId,
|
|
4247
4578
|
records: [...newSchemaData],
|
|
4248
4579
|
files: [...sectionFiles],
|
|
4580
|
+
...(profileImage ? { image: profileImage } : {}),
|
|
4249
4581
|
});
|
|
4250
4582
|
}
|
|
4251
4583
|
catch (error) {
|
|
@@ -4256,6 +4588,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4256
4588
|
if (mode === 'IntakeForm') {
|
|
4257
4589
|
baselineSnapshotRef.current = buildSectionSnapshot(currentSchemaData, namespace);
|
|
4258
4590
|
setIntakeFormBaselineTrigger((prev) => prev + 1);
|
|
4591
|
+
setHasBeenSavedByUser(true);
|
|
4259
4592
|
}
|
|
4260
4593
|
onSectionDirtyChange?.(sectionId, false);
|
|
4261
4594
|
}
|
|
@@ -4264,64 +4597,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4264
4597
|
}, [store, onSectionSave, onSectionSaveSuccess, sectionIndex, originalSection, schemaData, contextSchemaData, namespace, hasSupportingDocuments, dbSectionId, sectionRegisterId, dispatch, buildSectionSnapshot, sectionId, onSectionDirtyChange, mode, isDraft]);
|
|
4265
4598
|
// Handle cancel button click
|
|
4266
4599
|
const handleCancel = () => {
|
|
4267
|
-
|
|
4268
|
-
// Use original section (without namespace) for collecting widgets
|
|
4269
|
-
const sectionWidgets = collectWidgets(originalSection.panels);
|
|
4270
|
-
const oldSchemaData = schemaData || contextSchemaData;
|
|
4271
|
-
const currentStoreValues = store.getState().widget.values;
|
|
4272
|
-
let newStoreValues = currentStoreValues;
|
|
4273
|
-
sectionWidgets.forEach(widget => {
|
|
4274
|
-
const originalWidgetId = widget['widget-id'];
|
|
4275
|
-
// If namespace was used, we need to use namespaced widget ID and data path
|
|
4276
|
-
const namespacedWidgetId = namespace ? `${namespace}__${originalWidgetId}` : originalWidgetId;
|
|
4277
|
-
const widgetId = namespacedWidgetId;
|
|
4278
|
-
const originalDataPath = widget['widget-data-path'];
|
|
4279
|
-
// If namespace was used, data path in store is namespaced, but we read from original schema using original path
|
|
4280
|
-
const storeDataPath = namespace && originalDataPath
|
|
4281
|
-
? (typeof originalDataPath === 'string'
|
|
4282
|
-
? `${namespace}.${originalDataPath}`
|
|
4283
|
-
: Object.fromEntries(Object.entries(originalDataPath).map(([key, path]) => [key, `${namespace}.${path}`])))
|
|
4284
|
-
: originalDataPath;
|
|
4285
|
-
if (widgetId && originalDataPath) {
|
|
4286
|
-
// Handle multi-path (object) or single path (string)
|
|
4287
|
-
// Read from original schema data using original paths
|
|
4288
|
-
let oldValue;
|
|
4289
|
-
if (typeof originalDataPath === 'object') {
|
|
4290
|
-
// Multi-path: get values for each path
|
|
4291
|
-
oldValue = {};
|
|
4292
|
-
Object.entries(originalDataPath).forEach(([key, path]) => {
|
|
4293
|
-
if (typeof path === 'string') {
|
|
4294
|
-
oldValue[key] = getValueByPath(oldSchemaData, path);
|
|
4295
|
-
}
|
|
4296
|
-
});
|
|
4297
|
-
}
|
|
4298
|
-
else if (typeof originalDataPath === 'string') {
|
|
4299
|
-
oldValue = getValueByPath(oldSchemaData, originalDataPath);
|
|
4300
|
-
}
|
|
4301
|
-
// Set in store using namespaced data path (if namespace was used)
|
|
4302
|
-
if (oldValue !== undefined) {
|
|
4303
|
-
newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
|
|
4304
|
-
}
|
|
4305
|
-
}
|
|
4306
|
-
});
|
|
4307
|
-
// Also revert supporting documents if any
|
|
4308
|
-
if (hasSupportingDocuments) {
|
|
4309
|
-
// Use original section's supporting documents to get original data paths
|
|
4310
|
-
const originalSupportingDocuments = originalSection['section-supporting-documents'] || [];
|
|
4311
|
-
originalSupportingDocuments.forEach((doc, index) => {
|
|
4312
|
-
const widgetId = `supporting-doc-${sectionId}-${index}`;
|
|
4313
|
-
const originalDataPath = doc['document-data-path'];
|
|
4314
|
-
// If namespace was used, data path in store is namespaced
|
|
4315
|
-
const storeDataPath = namespace && originalDataPath
|
|
4316
|
-
? `${namespace}.${originalDataPath}`
|
|
4317
|
-
: originalDataPath;
|
|
4318
|
-
const oldValue = getValueByPath(oldSchemaData, originalDataPath);
|
|
4319
|
-
newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
|
|
4320
|
-
});
|
|
4321
|
-
}
|
|
4322
|
-
if (newStoreValues !== currentStoreValues) {
|
|
4323
|
-
dispatch(setValues(newStoreValues));
|
|
4324
|
-
}
|
|
4600
|
+
revertToOriginalValues();
|
|
4325
4601
|
setIsEditMode(false);
|
|
4326
4602
|
onEditModeChange?.(originalSectionId, false);
|
|
4327
4603
|
};
|
|
@@ -4372,20 +4648,27 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4372
4648
|
white-space: nowrap !important;
|
|
4373
4649
|
}
|
|
4374
4650
|
/* Readonly: prevent flex row from overflowing panel */
|
|
4375
|
-
|
|
4651
|
+
${readonlyValueRowRootsCss} {
|
|
4376
4652
|
min-width: 0 !important;
|
|
4377
4653
|
overflow: hidden !important;
|
|
4378
4654
|
}
|
|
4379
|
-
|
|
4655
|
+
${readonlyValueRowFlex1Css} {
|
|
4380
4656
|
min-width: 0 !important;
|
|
4381
4657
|
overflow: hidden !important;
|
|
4382
4658
|
}
|
|
4383
|
-
/* Readonly value
|
|
4384
|
-
|
|
4659
|
+
/* Readonly value: single-line ellipsis; full value via title on the value node */
|
|
4660
|
+
${readonlySingleLineValueTextCss} {
|
|
4385
4661
|
overflow: hidden;
|
|
4386
4662
|
text-overflow: ellipsis;
|
|
4387
4663
|
white-space: nowrap;
|
|
4388
4664
|
}
|
|
4665
|
+
/* Readonly textarea: break unbroken long tokens; title on pre keeps full text on hover */
|
|
4666
|
+
.${sectionClassId} .TextAreaDisplayWidget > .flex-1 > pre {
|
|
4667
|
+
min-width: 0;
|
|
4668
|
+
max-width: 100%;
|
|
4669
|
+
overflow-wrap: anywhere;
|
|
4670
|
+
word-break: break-word;
|
|
4671
|
+
}
|
|
4389
4672
|
|
|
4390
4673
|
/* Only apply fixed height when in edit mode */
|
|
4391
4674
|
.${sectionClassId}[data-edit-mode="true"] {
|
|
@@ -4509,6 +4792,10 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4509
4792
|
gap: 0.5rem;
|
|
4510
4793
|
}
|
|
4511
4794
|
|
|
4795
|
+
|
|
4796
|
+
|
|
4797
|
+
|
|
4798
|
+
|
|
4512
4799
|
/* IntakeForm accordion */
|
|
4513
4800
|
.${sectionClassId}.intake-form-accordion-item {
|
|
4514
4801
|
border-color: var(--owt-color-border-light, #E4E4E4);
|
|
@@ -4523,13 +4810,17 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4523
4810
|
.${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header h2 {
|
|
4524
4811
|
color: var(--owt-color-primary-dark, #F07B1A);
|
|
4525
4812
|
}
|
|
4526
|
-
|
|
4813
|
+
/* Hover / focus only shown when the header is actually interactive (standalone mode) */
|
|
4814
|
+
.${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header[data-interactive="true"]:hover {
|
|
4527
4815
|
opacity: 0.85;
|
|
4528
4816
|
}
|
|
4529
|
-
.${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header:focus-visible {
|
|
4817
|
+
.${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header[data-interactive="true"]:focus-visible {
|
|
4530
4818
|
outline: 2px solid var(--owt-color-primary, #F5BB1A);
|
|
4531
4819
|
outline-offset: 2px;
|
|
4532
4820
|
}
|
|
4821
|
+
.${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header[data-interactive="false"]:focus-visible {
|
|
4822
|
+
outline: none;
|
|
4823
|
+
}
|
|
4533
4824
|
.${sectionClassId}.intake-form-accordion-item .intake-form-accordion-content {
|
|
4534
4825
|
padding-top: 8px;
|
|
4535
4826
|
padding-bottom: 0px;
|
|
@@ -4572,7 +4863,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4572
4863
|
}),
|
|
4573
4864
|
}, children: mode === 'IntakeForm' ? (
|
|
4574
4865
|
/* IntakeForm: accordion layout - header always visible, content only when expanded */
|
|
4575
|
-
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: {
|
|
4866
|
+
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 ? 'true' : 'false', style: {
|
|
4576
4867
|
width: '100%',
|
|
4577
4868
|
display: 'flex',
|
|
4578
4869
|
alignItems: 'flex-start',
|
|
@@ -4582,7 +4873,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4582
4873
|
marginBottom: 0,
|
|
4583
4874
|
background: 'none',
|
|
4584
4875
|
border: 'none',
|
|
4585
|
-
cursor: 'pointer',
|
|
4876
|
+
cursor: sectionIndex === undefined ? 'pointer' : 'default',
|
|
4586
4877
|
textAlign: 'left',
|
|
4587
4878
|
fontFamily: 'Roboto, sans-serif',
|
|
4588
4879
|
}, 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']
|
|
@@ -4713,7 +5004,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4713
5004
|
}, 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: {
|
|
4714
5005
|
fontFamily: 'Roboto, sans-serif',
|
|
4715
5006
|
fontSize: '16px',
|
|
4716
|
-
color: 'var(--owt-color-text-muted, #727474)'
|
|
5007
|
+
color: 'var(--owt-color-text-muted, #727474)',
|
|
4717
5008
|
}, 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" })] }) }))] })] })) })] }));
|
|
4718
5009
|
};
|
|
4719
5010
|
|
|
@@ -4730,10 +5021,7 @@ function buildSectionChanges(section, storeValues, namespace, options) {
|
|
|
4730
5021
|
const widgetPath = widget['widget-data-path'];
|
|
4731
5022
|
if (!widgetPath)
|
|
4732
5023
|
return;
|
|
4733
|
-
|
|
4734
|
-
if (widgetType === 'table' ||
|
|
4735
|
-
widgetType === 'simple-table' ||
|
|
4736
|
-
widget.widget === 'table') {
|
|
5024
|
+
if (isTableLikeWidget(widget)) {
|
|
4737
5025
|
hasTable = true;
|
|
4738
5026
|
}
|
|
4739
5027
|
if (typeof widgetPath === 'object') {
|
|
@@ -4771,8 +5059,7 @@ function buildSectionChanges(section, storeValues, namespace, options) {
|
|
|
4771
5059
|
];
|
|
4772
5060
|
}
|
|
4773
5061
|
else {
|
|
4774
|
-
|
|
4775
|
-
records = tableEntry ? tableEntry[1] : [];
|
|
5062
|
+
records = extractTableRecordsFromSnapshot(snapshot, sectionWidgets);
|
|
4776
5063
|
}
|
|
4777
5064
|
const files = [];
|
|
4778
5065
|
const supportingDocs = section['section-supporting-documents'] || [];
|
|
@@ -4798,7 +5085,9 @@ const hasTableWidget = (panels) => {
|
|
|
4798
5085
|
// Check widgets in this panel
|
|
4799
5086
|
if (panel.widgets) {
|
|
4800
5087
|
for (const widget of panel.widgets) {
|
|
4801
|
-
if (widget.widget === 'table' ||
|
|
5088
|
+
if (widget.widget === 'table' ||
|
|
5089
|
+
widget.widget === 'dialog-table' ||
|
|
5090
|
+
widget['widget-type'] === 'table') {
|
|
4802
5091
|
return true;
|
|
4803
5092
|
}
|
|
4804
5093
|
}
|
|
@@ -4820,7 +5109,9 @@ const getTableWidgetColumnSpan = (panels) => {
|
|
|
4820
5109
|
// Check widgets in this panel
|
|
4821
5110
|
if (panel.widgets) {
|
|
4822
5111
|
for (const widget of panel.widgets) {
|
|
4823
|
-
if (widget.widget === 'table' ||
|
|
5112
|
+
if (widget.widget === 'table' ||
|
|
5113
|
+
widget.widget === 'dialog-table' ||
|
|
5114
|
+
widget['widget-type'] === 'table') {
|
|
4824
5115
|
// Return the widget's column span if specified, otherwise null
|
|
4825
5116
|
return widget['widget-column-span'] || null;
|
|
4826
5117
|
}
|
|
@@ -4887,6 +5178,13 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
|
|
|
4887
5178
|
}, []);
|
|
4888
5179
|
const safeSections = sections ?? [];
|
|
4889
5180
|
const prevSectionsLengthRef = React.useRef(safeSections.length);
|
|
5181
|
+
// Stable ref for namespace so formHandle useMemo doesn't depend on the (possibly inline) function identity
|
|
5182
|
+
const namespaceRef = React.useRef(namespace);
|
|
5183
|
+
namespaceRef.current = namespace;
|
|
5184
|
+
// Stable refs so formHandle closure can access current mode and accordion setter without stale captures
|
|
5185
|
+
const modeRef = React.useRef(mode);
|
|
5186
|
+
modeRef.current = mode;
|
|
5187
|
+
const setExpandedSectionIndexRef = React.useRef(setExpandedSectionIndex);
|
|
4890
5188
|
// Track dirty (unsaved changes) per section for form handle validation
|
|
4891
5189
|
const sectionDirtyMapRef = React.useRef({});
|
|
4892
5190
|
const handleSectionDirtyChange = React.useCallback((sectionId, isDirty) => {
|
|
@@ -4932,11 +5230,14 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
|
|
|
4932
5230
|
// Form handle for onFormReady - allows host to validate and get all section data from its own Submit button
|
|
4933
5231
|
const formHandle = React.useMemo(() => {
|
|
4934
5232
|
const getValues = () => store.getState().widget?.values || {};
|
|
4935
|
-
const getNamespace = (section, index) =>
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
5233
|
+
const getNamespace = (section, index) => {
|
|
5234
|
+
const ns = namespaceRef.current;
|
|
5235
|
+
return ns
|
|
5236
|
+
? typeof ns === 'string'
|
|
5237
|
+
? ns
|
|
5238
|
+
: ns(section['section-id'], index)
|
|
5239
|
+
: undefined;
|
|
5240
|
+
};
|
|
4940
5241
|
const checkNoUnsavedChanges = () => {
|
|
4941
5242
|
const hasDirty = Object.values(sectionDirtyMapRef.current).some(Boolean);
|
|
4942
5243
|
if (hasDirty) {
|
|
@@ -4945,33 +5246,52 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
|
|
|
4945
5246
|
};
|
|
4946
5247
|
return {
|
|
4947
5248
|
validate: async () => {
|
|
4948
|
-
|
|
5249
|
+
if (modeRef.current !== 'IntakeForm')
|
|
5250
|
+
checkNoUnsavedChanges();
|
|
4949
5251
|
const values = getValues();
|
|
4950
5252
|
let allValid = true;
|
|
5253
|
+
let firstInvalidIndex = null;
|
|
4951
5254
|
for (let i = 0; i < safeSections.length; i++) {
|
|
4952
5255
|
const section = safeSections[i];
|
|
4953
5256
|
const ns = getNamespace(section, i);
|
|
4954
5257
|
const sectionToValidate = ns ? namespaceSectionConfig(section, ns) : section;
|
|
4955
5258
|
const valid = sectionValidate(sectionToValidate, values, dispatch);
|
|
4956
|
-
if (!valid)
|
|
5259
|
+
if (!valid) {
|
|
5260
|
+
if (firstInvalidIndex === null)
|
|
5261
|
+
firstInvalidIndex = i;
|
|
4957
5262
|
allValid = false;
|
|
5263
|
+
}
|
|
5264
|
+
}
|
|
5265
|
+
if (!allValid && modeRef.current === 'IntakeForm' && firstInvalidIndex !== null) {
|
|
5266
|
+
setExpandedSectionIndexRef.current(firstInvalidIndex);
|
|
4958
5267
|
}
|
|
4959
5268
|
return allValid;
|
|
4960
5269
|
},
|
|
4961
5270
|
getFormData: () => getValues(),
|
|
4962
5271
|
validateAndGetData: async () => {
|
|
4963
|
-
|
|
5272
|
+
if (modeRef.current !== 'IntakeForm')
|
|
5273
|
+
checkNoUnsavedChanges();
|
|
4964
5274
|
const values = getValues();
|
|
4965
5275
|
const results = [];
|
|
5276
|
+
let firstInvalidIndex = null;
|
|
4966
5277
|
for (let i = 0; i < safeSections.length; i++) {
|
|
4967
5278
|
const section = safeSections[i];
|
|
4968
5279
|
const ns = getNamespace(section, i);
|
|
4969
5280
|
const sectionToValidate = ns ? namespaceSectionConfig(section, ns) : section;
|
|
4970
5281
|
const valid = sectionValidate(sectionToValidate, values, dispatch);
|
|
4971
5282
|
if (!valid) {
|
|
4972
|
-
|
|
5283
|
+
if (firstInvalidIndex === null)
|
|
5284
|
+
firstInvalidIndex = i;
|
|
4973
5285
|
}
|
|
4974
|
-
|
|
5286
|
+
else {
|
|
5287
|
+
results.push(buildSectionChanges(section, values, ns));
|
|
5288
|
+
}
|
|
5289
|
+
}
|
|
5290
|
+
if (firstInvalidIndex !== null) {
|
|
5291
|
+
if (modeRef.current === 'IntakeForm') {
|
|
5292
|
+
setExpandedSectionIndexRef.current(firstInvalidIndex);
|
|
5293
|
+
}
|
|
5294
|
+
throw new Error('Validation failed. Please fix the errors and try again.');
|
|
4975
5295
|
}
|
|
4976
5296
|
return results;
|
|
4977
5297
|
},
|
|
@@ -4986,7 +5306,7 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
|
|
|
4986
5306
|
return results;
|
|
4987
5307
|
},
|
|
4988
5308
|
};
|
|
4989
|
-
}, [store, dispatch, safeSections
|
|
5309
|
+
}, [store, dispatch, safeSections]);
|
|
4990
5310
|
// Call onFormReady when form is ready (sections loaded)
|
|
4991
5311
|
React.useEffect(() => {
|
|
4992
5312
|
if (onFormReady && safeSections.length > 0) {
|
|
@@ -8270,10 +8590,10 @@ const DisplayWidget = ({ config }) => {
|
|
|
8270
8590
|
const label = translateConfig(widgetConfig['widget-label']);
|
|
8271
8591
|
// If no label, render as paragraph text
|
|
8272
8592
|
if (!label || label.trim() === '') {
|
|
8273
|
-
return (jsxRuntimeExports.jsx("div", { className: "mb-3 text-base text-gray-700", title: String(displayValue ?? ''), children: displayValue }));
|
|
8593
|
+
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 }));
|
|
8274
8594
|
}
|
|
8275
|
-
// With label, render as key-value pair
|
|
8276
|
-
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 })] }));
|
|
8595
|
+
// With label, render as key-value pair (structure matches other readonly widgets for SectionRenderer ellipsis)
|
|
8596
|
+
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 }) })] }));
|
|
8277
8597
|
};
|
|
8278
8598
|
|
|
8279
8599
|
const TableCellSelect = ({ config, value, onValueChange }) => {
|
|
@@ -8287,7 +8607,7 @@ const TableCellSelect = ({ config, value, onValueChange }) => {
|
|
|
8287
8607
|
backgroundColor: isReadonly || loading ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
|
|
8288
8608
|
}, children: [jsxRuntimeExports.jsx("option", { value: "", children: translate('common.select') || 'Select' }), dataSourceOptions.map((option) => (jsxRuntimeExports.jsx("option", { value: option.value, children: option.label }, option.value)))] }));
|
|
8289
8609
|
};
|
|
8290
|
-
const SelectDisplayValue = ({ config, value }) => {
|
|
8610
|
+
const SelectDisplayValue$1 = ({ config, value }) => {
|
|
8291
8611
|
const { dataSourceOptions, loading } = useBaseWidget({ config });
|
|
8292
8612
|
if (loading) {
|
|
8293
8613
|
return jsxRuntimeExports.jsx("span", { children: "-" });
|
|
@@ -8781,7 +9101,7 @@ const TableWidget = ({ config }) => {
|
|
|
8781
9101
|
'widget-readonly': true,
|
|
8782
9102
|
'widget-data-path': undefined,
|
|
8783
9103
|
};
|
|
8784
|
-
return (jsxRuntimeExports.jsx("div", { className: "text-sm", style: getCellStyle(), children: jsxRuntimeExports.jsx(SelectDisplayValue, { config: cellConfig, value: cellValue }) }));
|
|
9104
|
+
return (jsxRuntimeExports.jsx("div", { className: "text-sm", style: getCellStyle(), children: jsxRuntimeExports.jsx(SelectDisplayValue$1, { config: cellConfig, value: cellValue }) }));
|
|
8785
9105
|
}
|
|
8786
9106
|
return (jsxRuntimeExports.jsx("div", { className: "text-sm", style: getCellStyle(), children: displayValue }));
|
|
8787
9107
|
}
|
|
@@ -8920,38 +9240,297 @@ const TableWidget = ({ config }) => {
|
|
|
8920
9240
|
}, 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] }))] })] }));
|
|
8921
9241
|
};
|
|
8922
9242
|
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
const {
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
9243
|
+
// Display select value label in view mode
|
|
9244
|
+
const SelectDisplayValue = ({ config, value }) => {
|
|
9245
|
+
const { dataSourceOptions, loading } = useBaseWidget({ config });
|
|
9246
|
+
if (loading)
|
|
9247
|
+
return jsxRuntimeExports.jsx("span", { children: "-" });
|
|
9248
|
+
if (value === null || value === undefined || value === '')
|
|
9249
|
+
return jsxRuntimeExports.jsx("span", { children: "-" });
|
|
9250
|
+
const selectedOption = dataSourceOptions.find((option) => option.value === value);
|
|
9251
|
+
return jsxRuntimeExports.jsx("span", { children: selectedOption ? selectedOption.label : String(value) });
|
|
9252
|
+
};
|
|
9253
|
+
/**
|
|
9254
|
+
* Dialog table widget:
|
|
9255
|
+
* - Table displays a subset of columns (n out of x)
|
|
9256
|
+
* - Add/Edit happens in a modal dialog that shows ALL columns as a form
|
|
9257
|
+
*
|
|
9258
|
+
* Usage in schema:
|
|
9259
|
+
* {
|
|
9260
|
+
* "widget": "dialog-table",
|
|
9261
|
+
* "widget-type": "table",
|
|
9262
|
+
* "widget-label": "Household Members",
|
|
9263
|
+
* "widget-id": "householdMembers",
|
|
9264
|
+
* "widget-data-path": "household.members",
|
|
9265
|
+
* "widget-data-columns": [ ...all columns... ],
|
|
9266
|
+
* "widget-data-visible-columns": ["firstName", "lastName", "dob"], // optional override; default = by column flag
|
|
9267
|
+
* // Per-column control (recommended): set "column-visible-in-table": false to hide it in the table
|
|
9268
|
+
* "widget-data-operations": { "add": true, "edit": true, "remove": true }
|
|
9269
|
+
* }
|
|
9270
|
+
*/
|
|
9271
|
+
const DialogTableWidget = ({ config }) => {
|
|
9272
|
+
const { value, error, touched, isEnabled, onChange, config: widgetConfig } = useBaseWidget({ config });
|
|
9273
|
+
const { translate, translateConfig } = useWidgetTranslation();
|
|
9274
|
+
const dispatch = reactRedux.useDispatch();
|
|
9275
|
+
const storeValues = reactRedux.useSelector((state) => state.widget?.values ?? {});
|
|
9276
|
+
const rows = Array.isArray(value) ? value : [];
|
|
9277
|
+
const columns = widgetConfig['widget-data-columns'] || [];
|
|
9278
|
+
const operations = widgetConfig['widget-data-operations'] || {};
|
|
9279
|
+
const isReadonly = widgetConfig['widget-readonly'] || false;
|
|
9280
|
+
const visibleColumnKeys = widgetConfig['widget-data-visible-columns'];
|
|
9281
|
+
const visibleColumns = React.useMemo(() => {
|
|
9282
|
+
// 1) If explicit list provided, it wins
|
|
9283
|
+
if (Array.isArray(visibleColumnKeys) && visibleColumnKeys.length > 0) {
|
|
9284
|
+
const keySet = new Set(visibleColumnKeys);
|
|
9285
|
+
return columns.filter((c) => keySet.has(c['column-key']));
|
|
9286
|
+
}
|
|
9287
|
+
// 2) Otherwise decide per column (default = visible)
|
|
9288
|
+
return columns.filter((c) => c['column-visible-in-table'] !== false);
|
|
9289
|
+
}, [columns, visibleColumnKeys]);
|
|
9290
|
+
const [dialogOpen, setDialogOpen] = React.useState(false);
|
|
9291
|
+
const [dialogMode, setDialogMode] = React.useState('add');
|
|
9292
|
+
const [activeRowIndex, setActiveRowIndex] = React.useState(null);
|
|
9293
|
+
const [formData, setFormData] = React.useState({});
|
|
9294
|
+
/** Unique per dialog open so Redux widget ids don't reuse stale values across rows/add sessions */
|
|
9295
|
+
const dialogSessionRef = React.useRef(0);
|
|
9296
|
+
const [dialogSessionId, setDialogSessionId] = React.useState(0);
|
|
9297
|
+
const addDialogTitle = translateConfig(widgetConfig['widget-data-dialog-title-add']) ||
|
|
9298
|
+
translate('table.addRecordDialog') ||
|
|
9299
|
+
'Add record';
|
|
9300
|
+
const editDialogTitle = translateConfig(widgetConfig['widget-data-dialog-title-edit']) ||
|
|
9301
|
+
translate('table.editRecordDialog') ||
|
|
9302
|
+
'Edit record';
|
|
9303
|
+
const buildEmptyRow = React.useCallback(() => {
|
|
9304
|
+
const emptyRow = {};
|
|
9305
|
+
columns.forEach((col) => {
|
|
9306
|
+
const key = col['column-key'];
|
|
9307
|
+
emptyRow[key] = col['widget-data-default'] ?? '';
|
|
9308
|
+
});
|
|
9309
|
+
return emptyRow;
|
|
9310
|
+
}, [columns]);
|
|
9311
|
+
const dialogFieldWidgetId = React.useCallback((columnKey) => `${widgetConfig['widget-id']}-dlg-${dialogSessionId}-${columnKey}`, [widgetConfig, dialogSessionId]);
|
|
9312
|
+
const resetDialogWidgets = React.useCallback((sessionId) => {
|
|
9313
|
+
if (sessionId <= 0)
|
|
9314
|
+
return;
|
|
9315
|
+
columns.forEach((col) => {
|
|
9316
|
+
const wid = `${widgetConfig['widget-id']}-dlg-${sessionId}-${col['column-key']}`;
|
|
9317
|
+
dispatch(resetWidget(wid));
|
|
9318
|
+
});
|
|
9319
|
+
}, [columns, widgetConfig, dispatch]);
|
|
9320
|
+
const beginDialogSession = React.useCallback(() => {
|
|
9321
|
+
dialogSessionRef.current += 1;
|
|
9322
|
+
const nextSession = dialogSessionRef.current;
|
|
9323
|
+
setDialogSessionId(nextSession);
|
|
9324
|
+
return nextSession;
|
|
9325
|
+
}, []);
|
|
9326
|
+
const openAddDialog = React.useCallback(() => {
|
|
9327
|
+
resetDialogWidgets(dialogSessionId);
|
|
9328
|
+
beginDialogSession();
|
|
9329
|
+
setDialogMode('add');
|
|
9330
|
+
setActiveRowIndex(null);
|
|
9331
|
+
setFormData(buildEmptyRow());
|
|
9332
|
+
setDialogOpen(true);
|
|
9333
|
+
}, [buildEmptyRow, beginDialogSession, resetDialogWidgets, dialogSessionId]);
|
|
9334
|
+
const openEditDialog = React.useCallback((rowIndex) => {
|
|
9335
|
+
resetDialogWidgets(dialogSessionId);
|
|
9336
|
+
beginDialogSession();
|
|
9337
|
+
const row = rows[rowIndex] || {};
|
|
9338
|
+
const nextFormData = buildEmptyRow();
|
|
9339
|
+
columns.forEach((col) => {
|
|
9340
|
+
const key = col['column-key'];
|
|
9341
|
+
if (row[key] !== undefined)
|
|
9342
|
+
nextFormData[key] = row[key];
|
|
9343
|
+
});
|
|
9344
|
+
setDialogMode('edit');
|
|
9345
|
+
setActiveRowIndex(rowIndex);
|
|
9346
|
+
setFormData(nextFormData);
|
|
9347
|
+
setDialogOpen(true);
|
|
9348
|
+
}, [rows, columns, buildEmptyRow, resetDialogWidgets, dialogSessionId, beginDialogSession]);
|
|
9349
|
+
const closeDialog = React.useCallback(() => {
|
|
9350
|
+
const sessionToClear = dialogSessionId;
|
|
9351
|
+
setDialogOpen(false);
|
|
9352
|
+
setActiveRowIndex(null);
|
|
9353
|
+
setFormData({});
|
|
9354
|
+
resetDialogWidgets(sessionToClear);
|
|
9355
|
+
setDialogSessionId(0);
|
|
9356
|
+
}, [dialogSessionId, resetDialogWidgets]);
|
|
9357
|
+
const updateField = React.useCallback((columnKey, newValue) => {
|
|
9358
|
+
setFormData((prev) => ({ ...prev, [columnKey]: newValue }));
|
|
9359
|
+
}, []);
|
|
9360
|
+
const collectMergedRowPayload = React.useCallback(() => {
|
|
9361
|
+
const merged = { ...formData };
|
|
9362
|
+
columns.forEach((col) => {
|
|
9363
|
+
const k = col['column-key'];
|
|
9364
|
+
const wid = dialogFieldWidgetId(k);
|
|
9365
|
+
const fromStore = storeValues[wid];
|
|
9366
|
+
if (fromStore !== undefined)
|
|
9367
|
+
merged[k] = fromStore;
|
|
9368
|
+
});
|
|
9369
|
+
return merged;
|
|
9370
|
+
}, [formData, columns, storeValues, dialogFieldWidgetId]);
|
|
9371
|
+
const saveDialog = React.useCallback(() => {
|
|
9372
|
+
const payload = collectMergedRowPayload();
|
|
9373
|
+
if (dialogMode === 'add') {
|
|
9374
|
+
const savedRow = { ...payload, edit_action: 'ADD' };
|
|
9375
|
+
onChange([...rows, savedRow]);
|
|
9376
|
+
closeDialog();
|
|
9377
|
+
return;
|
|
9378
|
+
}
|
|
9379
|
+
if (dialogMode === 'edit' && activeRowIndex !== null) {
|
|
9380
|
+
const newRows = [...rows];
|
|
9381
|
+
const currentRow = newRows[activeRowIndex] || {};
|
|
9382
|
+
const wasDeleted = currentRow.edit_action === 'DELETE';
|
|
9383
|
+
const editAction = wasDeleted ? 'UPDATE' : (currentRow.edit_action ?? 'UPDATE');
|
|
9384
|
+
newRows[activeRowIndex] = { ...currentRow, ...payload, edit_action: editAction };
|
|
9385
|
+
onChange(newRows);
|
|
9386
|
+
closeDialog();
|
|
9387
|
+
}
|
|
9388
|
+
}, [collectMergedRowPayload, dialogMode, onChange, rows, closeDialog, activeRowIndex]);
|
|
9389
|
+
const deleteRow = React.useCallback((rowIndex) => {
|
|
9390
|
+
const newRows = rows.filter((_, i) => i !== rowIndex);
|
|
9391
|
+
onChange(newRows);
|
|
9392
|
+
}, [rows, onChange]);
|
|
9393
|
+
const getDisplayValue = React.useCallback((rowIndex, column) => {
|
|
9394
|
+
const key = column['column-key'];
|
|
9395
|
+
const cellValue = rows[rowIndex]?.[key];
|
|
9396
|
+
const widgetType = column.widget || 'text';
|
|
9397
|
+
if (cellValue === null || cellValue === undefined || cellValue === '')
|
|
9398
|
+
return '-';
|
|
9399
|
+
if (widgetType === 'select')
|
|
9400
|
+
return null; // handled by SelectDisplayValue
|
|
9401
|
+
if (column['widget-data-format'])
|
|
9402
|
+
return formatValue(cellValue, column['widget-data-format'], column.widget);
|
|
9403
|
+
return String(cellValue);
|
|
9404
|
+
}, [rows]);
|
|
9405
|
+
const tableWidgetId = `dialog-table-widget-${widgetConfig['widget-id']}`;
|
|
9406
|
+
const columnSpan = widgetConfig['widget-column-span'] || 2;
|
|
9407
|
+
const minWidth = columnSpan * 200;
|
|
9408
|
+
return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("style", { children: `
|
|
9409
|
+
.${tableWidgetId} {
|
|
9410
|
+
width: 100%;
|
|
9411
|
+
min-width: ${minWidth}px;
|
|
9412
|
+
}
|
|
9413
|
+
|
|
9414
|
+
.widget-container[data-widget-id="${widgetConfig['widget-id']}"] {
|
|
9415
|
+
min-width: ${minWidth}px;
|
|
9416
|
+
width: 100%;
|
|
9417
|
+
flex: none;
|
|
9418
|
+
}
|
|
9419
|
+
|
|
9420
|
+
.panel-horizontal .widget-container[data-widget-id="${widgetConfig['widget-id']}"],
|
|
9421
|
+
[data-panel-orientation="horizontal"] .widget-container[data-widget-id="${widgetConfig['widget-id']}"] {
|
|
9422
|
+
grid-column: span ${columnSpan};
|
|
9423
|
+
}
|
|
9424
|
+
` }), 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: {
|
|
9425
|
+
borderRadius: 'var(--owt-btn-border-radius, 10px)',
|
|
9426
|
+
border: '1px solid var(--owt-btn-primary-border, #F07B1A)',
|
|
9427
|
+
backgroundColor: 'var(--owt-color-primary, #F5BB1A)',
|
|
9428
|
+
color: 'var(--owt-color-bg, #FFFFFF)',
|
|
9429
|
+
}, children: translate('table.addRecord') || 'Add New Record' }) })), jsxRuntimeExports.jsx("div", { className: "overflow-x-auto border", style: {
|
|
9430
|
+
borderRadius: 'var(--owt-widget-table-border-radius, 15px)',
|
|
9431
|
+
borderColor: 'var(--owt-widget-table-border-color, #C4C4C4)',
|
|
9432
|
+
}, 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: {
|
|
9433
|
+
borderBottom: '1px solid var(--owt-widget-table-row-divider, #E4E4E4)',
|
|
9434
|
+
backgroundColor: row?.edit_action === 'DELETE'
|
|
9435
|
+
? 'var(--owt-widget-table-deleted-row-bg, #FEE2E2)'
|
|
9436
|
+
: undefined,
|
|
9437
|
+
}, children: [visibleColumns.map((col) => {
|
|
9438
|
+
const key = col['column-key'];
|
|
9439
|
+
const widgetType = col.widget || 'text';
|
|
9440
|
+
const displayValue = getDisplayValue(rowIndex, col);
|
|
9441
|
+
if (widgetType === 'select' && displayValue === null) {
|
|
9442
|
+
const displayConfig = {
|
|
9443
|
+
...col,
|
|
9444
|
+
'widget-id': `${widgetConfig['widget-id']}-view-row-${rowIndex}-col-${key}`,
|
|
9445
|
+
'widget-label': '',
|
|
9446
|
+
'widget-readonly': true,
|
|
9447
|
+
'widget-data-path': undefined,
|
|
9448
|
+
};
|
|
9449
|
+
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));
|
|
9450
|
+
}
|
|
9451
|
+
return (jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", children: jsxRuntimeExports.jsx("div", { className: "text-sm", children: displayValue }) }, key));
|
|
9452
|
+
}), ((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: {
|
|
9453
|
+
borderRadius: 'var(--owt-btn-border-radius, 10px)',
|
|
9454
|
+
color: 'var(--owt-color-primary-dark, #F07B1A)',
|
|
9455
|
+
backgroundColor: 'transparent',
|
|
9456
|
+
border: 'none',
|
|
9457
|
+
}, 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: {
|
|
9458
|
+
borderRadius: 'var(--owt-btn-border-radius, 10px)',
|
|
9459
|
+
color: 'var(--owt-color-error, #B91C1C)',
|
|
9460
|
+
backgroundColor: 'transparent',
|
|
9461
|
+
border: 'none',
|
|
9462
|
+
}, 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: {
|
|
9463
|
+
maxWidth: '900px',
|
|
9464
|
+
backgroundColor: 'var(--owt-color-bg, #FFFFFF)',
|
|
9465
|
+
borderRadius: 'var(--owt-widget-card-border-radius, 20px)',
|
|
9466
|
+
}, 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: {
|
|
9467
|
+
border: 'none',
|
|
9468
|
+
background: 'transparent',
|
|
9469
|
+
color: 'var(--owt-color-text-muted, #727474)',
|
|
9470
|
+
cursor: 'pointer',
|
|
9471
|
+
fontSize: '20px',
|
|
9472
|
+
lineHeight: 1,
|
|
9473
|
+
}, "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) => {
|
|
9474
|
+
const key = col['column-key'];
|
|
9475
|
+
const widgetType = col.widget || 'text';
|
|
9476
|
+
const cellWidgetId = dialogFieldWidgetId(key);
|
|
9477
|
+
const initialValue = formData[key] ?? col['widget-data-default'] ?? '';
|
|
9478
|
+
const fieldConfig = {
|
|
9479
|
+
...col,
|
|
9480
|
+
widget: widgetType,
|
|
9481
|
+
'widget-type': col['widget-type'] || 'input',
|
|
9482
|
+
'widget-id': cellWidgetId,
|
|
9483
|
+
'widget-label': col['widget-label'],
|
|
9484
|
+
'widget-readonly': isReadonly || col['widget-readonly'] === true,
|
|
9485
|
+
'widget-data-path': undefined,
|
|
9486
|
+
'widget-data-default': initialValue,
|
|
9487
|
+
};
|
|
9488
|
+
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}`));
|
|
9489
|
+
}) }, `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: {
|
|
9490
|
+
borderRadius: 'var(--owt-btn-border-radius, 10px)',
|
|
9491
|
+
border: '1px solid var(--owt-btn-secondary-border, #C4C4C4)',
|
|
9492
|
+
backgroundColor: 'var(--owt-btn-secondary-bg, #FFFFFF)',
|
|
9493
|
+
color: 'var(--owt-btn-secondary-color, #011627)',
|
|
9494
|
+
}, 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: {
|
|
9495
|
+
borderRadius: 'var(--owt-btn-border-radius, 10px)',
|
|
9496
|
+
border: '1px solid var(--owt-btn-primary-border, #F07B1A)',
|
|
9497
|
+
backgroundColor: 'var(--owt-color-primary, #F5BB1A)',
|
|
9498
|
+
color: 'var(--owt-color-bg, #FFFFFF)',
|
|
9499
|
+
}, children: translate('common.save') || 'Save' })] })] }) }))] }));
|
|
9500
|
+
};
|
|
9501
|
+
|
|
9502
|
+
const ProfileWidget = ({ config }) => {
|
|
9503
|
+
const { value, config: widgetConfig, getFieldValue, } = useBaseWidget({ config });
|
|
9504
|
+
const { translateConfig } = useWidgetTranslation();
|
|
9505
|
+
// Get schemaData from context as fallback
|
|
9506
|
+
const { schemaData } = useWidgetContext();
|
|
9507
|
+
// Get values from Redux store
|
|
9508
|
+
const values = reactRedux.useSelector((state) => state.widget.values);
|
|
9509
|
+
// Support two approaches for data paths:
|
|
9510
|
+
// 1. Multi-path binding via widget-data-path (object) - RECOMMENDED (Approach 2)
|
|
9511
|
+
// 2. Individual path properties (widget-image-path, widget-name-path, widget-id-path) - Fallback
|
|
9512
|
+
let imageUrl = null;
|
|
9513
|
+
let displayName = '';
|
|
9514
|
+
let idValue = '';
|
|
9515
|
+
const dataPath = widgetConfig['widget-data-path'];
|
|
9516
|
+
const imagePath = widgetConfig['widget-image-path'];
|
|
9517
|
+
const namePath = widgetConfig['widget-name-path'];
|
|
9518
|
+
const idPath = widgetConfig['widget-id-path'];
|
|
9519
|
+
// Prioritize multi-path data binding (Approach 2 - Recommended)
|
|
9520
|
+
if (dataPath && typeof dataPath === 'object') {
|
|
9521
|
+
// Multi-path data binding - preferred approach (Approach 2)
|
|
9522
|
+
// Always fetch each path individually using getFieldValue for reliability
|
|
9523
|
+
// The values in the dataPath object are the actual data paths to fetch
|
|
9524
|
+
const imagePathValue = dataPath.image || dataPath.photo || dataPath.avatar;
|
|
9525
|
+
const namePathValue = dataPath.name || dataPath.displayName;
|
|
9526
|
+
const idPathValue = dataPath.id || dataPath.identifier;
|
|
9527
|
+
// Helper function to search for a path within all top-level objects
|
|
9528
|
+
const findValueInNestedObjects = (path, searchIn) => {
|
|
9529
|
+
if (!searchIn)
|
|
9530
|
+
return undefined;
|
|
9531
|
+
// First try direct path (in case it's at root level)
|
|
9532
|
+
let value = getValueByPath(searchIn, path);
|
|
9533
|
+
if (value !== undefined)
|
|
8955
9534
|
return value;
|
|
8956
9535
|
// If not found, search within each top-level object
|
|
8957
9536
|
for (const [key, obj] of Object.entries(searchIn)) {
|
|
@@ -9311,15 +9890,107 @@ const HeaderSectionWidget = ({ config }) => {
|
|
|
9311
9890
|
result = searchIn(schemaData);
|
|
9312
9891
|
return result;
|
|
9313
9892
|
}, [paths, values, schemaData]);
|
|
9314
|
-
const
|
|
9893
|
+
const imageVal = findValue('image');
|
|
9894
|
+
const imageUrlVal = findValue('imageUrl');
|
|
9895
|
+
const [previewUrl, setPreviewUrl] = React.useState(null);
|
|
9896
|
+
React.useEffect(() => {
|
|
9897
|
+
if (imageVal instanceof File) {
|
|
9898
|
+
const url = URL.createObjectURL(imageVal);
|
|
9899
|
+
setPreviewUrl(url);
|
|
9900
|
+
return () => URL.revokeObjectURL(url);
|
|
9901
|
+
}
|
|
9902
|
+
setPreviewUrl(null);
|
|
9903
|
+
}, [imageVal]);
|
|
9904
|
+
const displayImageUrl = previewUrl || (typeof imageUrlVal === 'string' && imageUrlVal ? imageUrlVal : null);
|
|
9315
9905
|
const displayName = findValue('name') || '';
|
|
9316
9906
|
const functionalId = findValue('functionalId') || '';
|
|
9317
9907
|
const statusValue = findValue('status') || '';
|
|
9318
9908
|
const statusReason = findValue('statusReason') || '';
|
|
9909
|
+
const completionScoreRaw = findValue('completionScore');
|
|
9910
|
+
const idealScoreRaw = findValue('idealScore');
|
|
9319
9911
|
const createdBy = findValue('createdBy') || '';
|
|
9320
9912
|
const createdAt = findValue('createdAt') || '';
|
|
9321
9913
|
const lastApprovedBy = findValue('lastApprovedBy') || '';
|
|
9322
9914
|
const lastApprovedAt = findValue('lastApprovedAt') || '';
|
|
9915
|
+
// ── Validation: status change requires reason ──────────────────
|
|
9916
|
+
// Behavior:
|
|
9917
|
+
// - When status changes away from its initial value, clear reason and require it.
|
|
9918
|
+
// - When status returns to initial value (or a parent "Cancel" restores it), restore initial reason.
|
|
9919
|
+
const initialStatusRef = React.useRef(null);
|
|
9920
|
+
const initialReasonRef = React.useRef(null);
|
|
9921
|
+
const prevStatusRef = React.useRef(null);
|
|
9922
|
+
const [showReasonRequired, setShowReasonRequired] = React.useState(false);
|
|
9923
|
+
React.useEffect(() => {
|
|
9924
|
+
// Capture initial status once when it becomes available.
|
|
9925
|
+
if (initialStatusRef.current === null) {
|
|
9926
|
+
const v = statusValue === undefined || statusValue === null ? '' : String(statusValue);
|
|
9927
|
+
initialStatusRef.current = v;
|
|
9928
|
+
}
|
|
9929
|
+
}, [statusValue]);
|
|
9930
|
+
React.useEffect(() => {
|
|
9931
|
+
// Capture initial reason once when it becomes available.
|
|
9932
|
+
if (initialReasonRef.current === null) {
|
|
9933
|
+
const v = statusReason === undefined || statusReason === null ? '' : String(statusReason);
|
|
9934
|
+
initialReasonRef.current = v;
|
|
9935
|
+
}
|
|
9936
|
+
}, [statusReason]);
|
|
9937
|
+
const isStatusChanged = React.useMemo(() => {
|
|
9938
|
+
const initial = initialStatusRef.current;
|
|
9939
|
+
if (initial === null)
|
|
9940
|
+
return false;
|
|
9941
|
+
return String(statusValue) !== initial;
|
|
9942
|
+
}, [statusValue]);
|
|
9943
|
+
const isReasonMissing = React.useMemo(() => {
|
|
9944
|
+
if (!isStatusChanged)
|
|
9945
|
+
return false;
|
|
9946
|
+
return String(statusReason || '').trim().length === 0;
|
|
9947
|
+
}, [isStatusChanged, statusReason]);
|
|
9948
|
+
React.useEffect(() => {
|
|
9949
|
+
// When status changes:
|
|
9950
|
+
// - If moved away from initial → clear reason.
|
|
9951
|
+
// - If returned to initial → restore initial reason.
|
|
9952
|
+
if (isReadonly)
|
|
9953
|
+
return;
|
|
9954
|
+
if (initialStatusRef.current === null)
|
|
9955
|
+
return;
|
|
9956
|
+
const currentStatus = String(statusValue || '');
|
|
9957
|
+
if (prevStatusRef.current === currentStatus)
|
|
9958
|
+
return;
|
|
9959
|
+
prevStatusRef.current = currentStatus;
|
|
9960
|
+
const initialStatus = initialStatusRef.current;
|
|
9961
|
+
const initialReason = initialReasonRef.current ?? '';
|
|
9962
|
+
if (currentStatus === initialStatus) {
|
|
9963
|
+
// Reverted / cancelled back to original
|
|
9964
|
+
if (String(statusReason || '') !== String(initialReason || '')) {
|
|
9965
|
+
updateFieldValue('statusReason', initialReason);
|
|
9966
|
+
}
|
|
9967
|
+
setShowReasonRequired(false);
|
|
9968
|
+
return;
|
|
9969
|
+
}
|
|
9970
|
+
// Status changed to a new value: clear reason (so user must re-enter)
|
|
9971
|
+
if (String(statusReason || '').trim().length > 0) {
|
|
9972
|
+
updateFieldValue('statusReason', '');
|
|
9973
|
+
}
|
|
9974
|
+
setShowReasonRequired(true);
|
|
9975
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
9976
|
+
}, [statusValue, isReadonly]);
|
|
9977
|
+
const score = React.useMemo(() => {
|
|
9978
|
+
const toNum = (v) => {
|
|
9979
|
+
if (v === null || v === undefined || String(v).trim() === '')
|
|
9980
|
+
return null;
|
|
9981
|
+
const n = typeof v === 'number' ? v : Number(String(v));
|
|
9982
|
+
return Number.isFinite(n) ? n : null;
|
|
9983
|
+
};
|
|
9984
|
+
const completion = toNum(completionScoreRaw);
|
|
9985
|
+
const ideal = toNum(idealScoreRaw);
|
|
9986
|
+
if (completion === null || ideal === null || ideal <= 0)
|
|
9987
|
+
return null;
|
|
9988
|
+
const ratio = completion / ideal;
|
|
9989
|
+
const percent = Math.max(0, Math.min(100, Math.round(ratio * 100)));
|
|
9990
|
+
const completionDisplay = Number.isInteger(completion) ? completion : Math.round(completion);
|
|
9991
|
+
const idealDisplay = Number.isInteger(ideal) ? ideal : Math.round(ideal);
|
|
9992
|
+
return { completion, ideal, completionDisplay, idealDisplay, percent };
|
|
9993
|
+
}, [completionScoreRaw, idealScoreRaw]);
|
|
9323
9994
|
// ── Format options ────────────────────────────────────────────
|
|
9324
9995
|
const format = (widgetConfig['widget-data-format'] || {});
|
|
9325
9996
|
const imageSize = format.imageSize || 120;
|
|
@@ -9344,18 +10015,20 @@ const HeaderSectionWidget = ({ config }) => {
|
|
|
9344
10015
|
return opt ? opt.label : String(statusValue);
|
|
9345
10016
|
}, [statusValue, statusOptions]);
|
|
9346
10017
|
const statusColor = statusColors[String(statusValue).toLowerCase()] || 'var(--owt-color-text-muted, #6B7280)';
|
|
10018
|
+
// ── Image edit helpers ───────────────────────────────────────
|
|
10019
|
+
const fileInputRef = React.useRef(null);
|
|
10020
|
+
const handleImageUpload = React.useCallback((e) => {
|
|
10021
|
+
const file = e.target.files?.[0];
|
|
10022
|
+
if (!file)
|
|
10023
|
+
return;
|
|
10024
|
+
updateFieldValue('image', file);
|
|
10025
|
+
e.target.value = '';
|
|
10026
|
+
}, [updateFieldValue]);
|
|
10027
|
+
const handleImageDelete = React.useCallback(() => {
|
|
10028
|
+
updateFieldValue('image', '');
|
|
10029
|
+
}, [updateFieldValue]);
|
|
9347
10030
|
// ── Scoped class for CSS isolation ────────────────────────────
|
|
9348
10031
|
const cls = `header-section-widget-${widgetConfig['widget-id']}`;
|
|
9349
|
-
// ── Indicator dot component ───────────────────────────────────
|
|
9350
|
-
const Dot = ({ color }) => (jsxRuntimeExports.jsx("span", { style: {
|
|
9351
|
-
display: 'inline-block',
|
|
9352
|
-
width: 8,
|
|
9353
|
-
height: 8,
|
|
9354
|
-
borderRadius: '50%',
|
|
9355
|
-
backgroundColor: color,
|
|
9356
|
-
flexShrink: 0,
|
|
9357
|
-
marginTop: 6,
|
|
9358
|
-
} }));
|
|
9359
10032
|
// ── RENDER ────────────────────────────────────────────────────
|
|
9360
10033
|
return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("style", { children: `
|
|
9361
10034
|
.${cls} {
|
|
@@ -9384,6 +10057,58 @@ const HeaderSectionWidget = ({ config }) => {
|
|
|
9384
10057
|
min-width: 220px;
|
|
9385
10058
|
}
|
|
9386
10059
|
|
|
10060
|
+
.${cls} .hdr-right-top {
|
|
10061
|
+
display: flex;
|
|
10062
|
+
align-items: flex-start;
|
|
10063
|
+
justify-content: space-between;
|
|
10064
|
+
gap: 14px;
|
|
10065
|
+
width: 100%;
|
|
10066
|
+
}
|
|
10067
|
+
|
|
10068
|
+
.${cls} .hdr-meta-col {
|
|
10069
|
+
display: flex;
|
|
10070
|
+
flex-direction: column;
|
|
10071
|
+
gap: 0.5rem;
|
|
10072
|
+
flex: 1 1 auto;
|
|
10073
|
+
min-width: 0;
|
|
10074
|
+
}
|
|
10075
|
+
|
|
10076
|
+
.${cls} .hdr-score-ring {
|
|
10077
|
+
--ring-size: 54px;
|
|
10078
|
+
--ring-thickness: 7px;
|
|
10079
|
+
--ring-color: var(--owt-color-primary-dark, #F07B1A);
|
|
10080
|
+
--ring-track: rgba(2, 6, 23, 0.10);
|
|
10081
|
+
width: var(--ring-size);
|
|
10082
|
+
height: var(--ring-size);
|
|
10083
|
+
border-radius: 50%;
|
|
10084
|
+
background: conic-gradient(
|
|
10085
|
+
var(--ring-color) calc(var(--pct) * 1%),
|
|
10086
|
+
var(--ring-track) 0
|
|
10087
|
+
);
|
|
10088
|
+
position: relative;
|
|
10089
|
+
flex: 0 0 auto;
|
|
10090
|
+
}
|
|
10091
|
+
|
|
10092
|
+
.${cls} .hdr-score-ring::before {
|
|
10093
|
+
content: "";
|
|
10094
|
+
position: absolute;
|
|
10095
|
+
inset: var(--ring-thickness);
|
|
10096
|
+
border-radius: 50%;
|
|
10097
|
+
background: var(--owt-color-bg, #FFFFFF);
|
|
10098
|
+
}
|
|
10099
|
+
|
|
10100
|
+
.${cls} .hdr-score-value {
|
|
10101
|
+
position: absolute;
|
|
10102
|
+
inset: 0;
|
|
10103
|
+
display: flex;
|
|
10104
|
+
align-items: center;
|
|
10105
|
+
justify-content: center;
|
|
10106
|
+
font-size: 20px;
|
|
10107
|
+
font-weight: 700;
|
|
10108
|
+
color: var(--owt-color-text, #011627);
|
|
10109
|
+
font-family: Roboto, sans-serif;
|
|
10110
|
+
}
|
|
10111
|
+
|
|
9387
10112
|
.${cls} .hdr-avatar {
|
|
9388
10113
|
width: ${imageSize}px;
|
|
9389
10114
|
height: ${imageSize}px;
|
|
@@ -9414,6 +10139,56 @@ const HeaderSectionWidget = ({ config }) => {
|
|
|
9414
10139
|
border-radius: 8px;
|
|
9415
10140
|
}
|
|
9416
10141
|
|
|
10142
|
+
.${cls} .hdr-avatar-wrapper {
|
|
10143
|
+
position: relative;
|
|
10144
|
+
width: ${imageSize}px;
|
|
10145
|
+
height: ${imageSize}px;
|
|
10146
|
+
flex-shrink: 0;
|
|
10147
|
+
}
|
|
10148
|
+
|
|
10149
|
+
.${cls} .hdr-avatar-overlay {
|
|
10150
|
+
position: absolute;
|
|
10151
|
+
inset: 0;
|
|
10152
|
+
border-radius: 8px;
|
|
10153
|
+
background: rgba(0, 0, 0, 0.55);
|
|
10154
|
+
display: flex;
|
|
10155
|
+
flex-direction: column;
|
|
10156
|
+
align-items: center;
|
|
10157
|
+
justify-content: center;
|
|
10158
|
+
gap: 6px;
|
|
10159
|
+
opacity: 0;
|
|
10160
|
+
transition: opacity 0.2s;
|
|
10161
|
+
}
|
|
10162
|
+
|
|
10163
|
+
.${cls} .hdr-avatar-wrapper:hover .hdr-avatar-overlay {
|
|
10164
|
+
opacity: 1;
|
|
10165
|
+
}
|
|
10166
|
+
|
|
10167
|
+
.${cls} .hdr-avatar-action {
|
|
10168
|
+
display: flex;
|
|
10169
|
+
align-items: center;
|
|
10170
|
+
gap: 5px;
|
|
10171
|
+
padding: 5px 14px;
|
|
10172
|
+
border: none;
|
|
10173
|
+
border-radius: 4px;
|
|
10174
|
+
background: rgba(255, 255, 255, 0.92);
|
|
10175
|
+
color: #374151;
|
|
10176
|
+
font-size: 0.7rem;
|
|
10177
|
+
font-weight: 500;
|
|
10178
|
+
cursor: pointer;
|
|
10179
|
+
font-family: Roboto, sans-serif;
|
|
10180
|
+
transition: background 0.15s;
|
|
10181
|
+
white-space: nowrap;
|
|
10182
|
+
}
|
|
10183
|
+
|
|
10184
|
+
.${cls} .hdr-avatar-action:hover {
|
|
10185
|
+
background: #fff;
|
|
10186
|
+
}
|
|
10187
|
+
|
|
10188
|
+
.${cls} .hdr-avatar-action--delete {
|
|
10189
|
+
color: #DC2626;
|
|
10190
|
+
}
|
|
10191
|
+
|
|
9417
10192
|
.${cls} .hdr-info {
|
|
9418
10193
|
display: flex;
|
|
9419
10194
|
flex-direction: column;
|
|
@@ -9510,6 +10285,19 @@ const HeaderSectionWidget = ({ config }) => {
|
|
|
9510
10285
|
box-shadow: 0 0 0 2px rgba(237, 124, 34, 0.15);
|
|
9511
10286
|
}
|
|
9512
10287
|
|
|
10288
|
+
.${cls} .hdr-input--error {
|
|
10289
|
+
border-color: var(--owt-color-danger, #DC2626);
|
|
10290
|
+
box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.12);
|
|
10291
|
+
}
|
|
10292
|
+
|
|
10293
|
+
.${cls} .hdr-error-text {
|
|
10294
|
+
margin-left: calc(0px);
|
|
10295
|
+
color: var(--owt-color-danger, #DC2626);
|
|
10296
|
+
font-size: 0.75rem;
|
|
10297
|
+
line-height: 1.2;
|
|
10298
|
+
font-weight: 500;
|
|
10299
|
+
}
|
|
10300
|
+
|
|
9513
10301
|
@media (max-width: 768px) {
|
|
9514
10302
|
.${cls} {
|
|
9515
10303
|
flex-direction: column;
|
|
@@ -9518,16 +10306,31 @@ const HeaderSectionWidget = ({ config }) => {
|
|
|
9518
10306
|
min-width: 0;
|
|
9519
10307
|
}
|
|
9520
10308
|
}
|
|
9521
|
-
` }), jsxRuntimeExports.jsxs("div", { className: cls, children: [jsxRuntimeExports.jsxs("div", { className: "hdr-left", children: [jsxRuntimeExports.jsxs("div", { children: [
|
|
10309
|
+
` }), 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) => {
|
|
9522
10310
|
e.target.style.display = 'none';
|
|
9523
10311
|
const placeholder = e.target
|
|
9524
10312
|
.parentElement?.querySelector('.hdr-avatar-placeholder');
|
|
9525
10313
|
if (placeholder)
|
|
9526
10314
|
placeholder.style.display = 'flex';
|
|
9527
|
-
} })) : null, jsxRuntimeExports.jsx("div", { className: "hdr-avatar-placeholder", style: { display:
|
|
10315
|
+
} })) : 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: () => {
|
|
10316
|
+
if (isReasonMissing)
|
|
10317
|
+
setShowReasonRequired(true);
|
|
10318
|
+
}, onChange: (e) => {
|
|
10319
|
+
updateFieldValue('statusReason', e.target.value);
|
|
10320
|
+
if (showReasonRequired && String(e.target.value || '').trim().length > 0) {
|
|
10321
|
+
setShowReasonRequired(false);
|
|
10322
|
+
}
|
|
10323
|
+
} }), !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] }) })] })] }));
|
|
9528
10324
|
};
|
|
9529
10325
|
|
|
9530
|
-
function
|
|
10326
|
+
function getValueByPathOrKey(obj, path) {
|
|
10327
|
+
if (!obj || !path)
|
|
10328
|
+
return undefined;
|
|
10329
|
+
if (Object.prototype.hasOwnProperty.call(obj, path))
|
|
10330
|
+
return obj[path];
|
|
10331
|
+
return getValueByPath(obj, path);
|
|
10332
|
+
}
|
|
10333
|
+
function tryFormatDateTime$1(value) {
|
|
9531
10334
|
if (typeof value !== 'string' || !value)
|
|
9532
10335
|
return value ? String(value) : '-';
|
|
9533
10336
|
const d = new Date(value);
|
|
@@ -9546,233 +10349,712 @@ function tryFormatDateTime(value) {
|
|
|
9546
10349
|
return value;
|
|
9547
10350
|
}
|
|
9548
10351
|
}
|
|
9549
|
-
function
|
|
9550
|
-
if (!scores || scores.length === 0)
|
|
9551
|
-
return null;
|
|
10352
|
+
function sortScores(scores) {
|
|
9552
10353
|
const withTime = scores
|
|
9553
|
-
.map((s) => {
|
|
10354
|
+
.map((s, idx) => {
|
|
9554
10355
|
const t = typeof s?.computed_at === 'string' ? new Date(s.computed_at).getTime() : NaN;
|
|
9555
|
-
return { s, t };
|
|
10356
|
+
return { s, t, idx };
|
|
9556
10357
|
})
|
|
9557
|
-
.
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
|
|
10358
|
+
.sort((a, b) => {
|
|
10359
|
+
const aHas = !Number.isNaN(a.t);
|
|
10360
|
+
const bHas = !Number.isNaN(b.t);
|
|
10361
|
+
if (aHas && bHas)
|
|
10362
|
+
return b.t - a.t;
|
|
10363
|
+
if (aHas)
|
|
10364
|
+
return -1;
|
|
10365
|
+
if (bHas)
|
|
10366
|
+
return 1;
|
|
10367
|
+
return a.idx - b.idx;
|
|
10368
|
+
});
|
|
10369
|
+
return withTime.map((x) => x.s);
|
|
9562
10370
|
}
|
|
9563
10371
|
/**
|
|
9564
|
-
* Scores Display Widget - full-width, view-only widget
|
|
10372
|
+
* Scores Display Widget - full-width, view-only widget (list)
|
|
9565
10373
|
*
|
|
9566
10374
|
* Expected config (reference):
|
|
9567
10375
|
* {
|
|
9568
10376
|
* "widget": "scores-display",
|
|
9569
10377
|
* "widget-type": "group",
|
|
9570
10378
|
* "widget-id": "record-scores",
|
|
9571
|
-
* "widget-data-
|
|
9572
|
-
* "type": "api",
|
|
9573
|
-
* "service": "staff-portal-api",
|
|
9574
|
-
* "endpoint": "get_scores",
|
|
9575
|
-
* "method": "POST",
|
|
9576
|
-
* "params": { "internal_record_id_path": "internal_record_id" }
|
|
9577
|
-
* }
|
|
10379
|
+
* "widget-data-path": "scores"
|
|
9578
10380
|
* }
|
|
9579
|
-
*
|
|
9580
|
-
* The host's `dataSourceRequestHandler` is invoked with:
|
|
9581
|
-
* - service: config.widget-data-source.service
|
|
9582
|
-
* - endpoint: config.widget-data-source.endpoint
|
|
9583
|
-
* - method: config.widget-data-source.method (default POST)
|
|
9584
|
-
* - params: { internal_record_id: <resolved from internal_record_id_path> }
|
|
9585
10381
|
*/
|
|
9586
|
-
const ScoresDisplayWidget = ({ config,
|
|
9587
|
-
const {
|
|
9588
|
-
const
|
|
9589
|
-
const schemaData = propSchemaData || ctxSchemaData || {};
|
|
10382
|
+
const ScoresDisplayWidget = ({ config, schemaData: propSchemaData, }) => {
|
|
10383
|
+
const { schemaData: ctxSchemaData } = useWidgetContext();
|
|
10384
|
+
const schemaData = (propSchemaData || ctxSchemaData || {});
|
|
9590
10385
|
const values = reactRedux.useSelector((state) => state.widget.values);
|
|
9591
|
-
const
|
|
9592
|
-
const
|
|
9593
|
-
|
|
9594
|
-
const internalIdPath = React.useMemo(() => {
|
|
9595
|
-
if (!apiDs)
|
|
9596
|
-
return undefined;
|
|
9597
|
-
const p = apiDs.params || {};
|
|
9598
|
-
const fromParams = p.internal_record_id_path || p.internalRecordIdPath;
|
|
9599
|
-
const fromConfig = typeof config.internal_record_id_path === 'string'
|
|
9600
|
-
? config.internal_record_id_path
|
|
9601
|
-
: undefined;
|
|
9602
|
-
return fromParams || fromConfig;
|
|
9603
|
-
}, [apiDs, config]);
|
|
9604
|
-
const internalRecordId = React.useMemo(() => {
|
|
9605
|
-
if (!internalIdPath)
|
|
10386
|
+
const dataPath = config['widget-data-path'];
|
|
10387
|
+
const rawScores = React.useMemo(() => {
|
|
10388
|
+
if (!dataPath || typeof dataPath !== 'string')
|
|
9606
10389
|
return undefined;
|
|
9607
|
-
const
|
|
9608
|
-
|
|
9609
|
-
|
|
10390
|
+
const valuesObj = values;
|
|
10391
|
+
const tryResolve = (path) => {
|
|
10392
|
+
const fromValues = getValueByPathOrKey(valuesObj, path);
|
|
10393
|
+
if (fromValues !== undefined)
|
|
10394
|
+
return fromValues;
|
|
10395
|
+
return getValueByPathOrKey(schemaData, path);
|
|
10396
|
+
};
|
|
10397
|
+
// 1) Try exact path (works when schema/store is already namespaced)
|
|
10398
|
+
const direct = tryResolve(dataPath);
|
|
10399
|
+
if (direct !== undefined)
|
|
10400
|
+
return direct;
|
|
10401
|
+
// 2) If section/widget config has been namespaced (e.g. "rv-section-0.scores"),
|
|
10402
|
+
// fall back to the original path ("scores") so examples still work even when
|
|
10403
|
+
// schemaData/store are not namespaced.
|
|
10404
|
+
if (dataPath.includes('.')) {
|
|
10405
|
+
const unNamespaced = dataPath.split('.').slice(1).join('.');
|
|
10406
|
+
const fallback = tryResolve(unNamespaced);
|
|
10407
|
+
if (fallback !== undefined)
|
|
10408
|
+
return fallback;
|
|
10409
|
+
}
|
|
10410
|
+
return undefined;
|
|
10411
|
+
}, [dataPath, values, schemaData]);
|
|
10412
|
+
const scores = React.useMemo(() => {
|
|
10413
|
+
if (!rawScores)
|
|
10414
|
+
return [];
|
|
10415
|
+
if (Array.isArray(rawScores))
|
|
10416
|
+
return rawScores;
|
|
10417
|
+
if (typeof rawScores === 'object') {
|
|
10418
|
+
const maybe = rawScores.scores;
|
|
10419
|
+
if (Array.isArray(maybe))
|
|
10420
|
+
return maybe;
|
|
10421
|
+
}
|
|
10422
|
+
return [];
|
|
10423
|
+
}, [rawScores]);
|
|
10424
|
+
const sortedScores = React.useMemo(() => sortScores(scores), [scores]);
|
|
10425
|
+
const cls = `scores-display-widget-${config['widget-id']}`;
|
|
10426
|
+
return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("style", { children: `
|
|
10427
|
+
.${cls} {
|
|
10428
|
+
width: 100%;
|
|
10429
|
+
font-family: Roboto, sans-serif;
|
|
10430
|
+
padding: 0;
|
|
10431
|
+
display: flex;
|
|
10432
|
+
flex-direction: column;
|
|
10433
|
+
gap: 8px;
|
|
10434
|
+
}
|
|
10435
|
+
|
|
10436
|
+
.${cls} .scores-subtle {
|
|
10437
|
+
font-size: 13px;
|
|
10438
|
+
color: var(--owt-color-text-muted, #727474);
|
|
10439
|
+
font-weight: 400;
|
|
10440
|
+
}
|
|
10441
|
+
|
|
10442
|
+
.${cls} .scores-grid {
|
|
10443
|
+
width: 100%;
|
|
10444
|
+
display: grid;
|
|
10445
|
+
grid-template-columns: repeat(3, minmax(220px, 1fr));
|
|
10446
|
+
gap: 16px;
|
|
10447
|
+
}
|
|
10448
|
+
|
|
10449
|
+
.${cls} .scores-card {
|
|
10450
|
+
border: 1px solid var(--owt-color-border-light, #E4E4E4);
|
|
10451
|
+
border-radius: 10px;
|
|
10452
|
+
background: var(--owt-color-bg, #FFFFFF);
|
|
10453
|
+
padding: 14px 14px;
|
|
10454
|
+
display: flex;
|
|
10455
|
+
flex-direction: column;
|
|
10456
|
+
gap: 10px;
|
|
10457
|
+
min-width: 0;
|
|
10458
|
+
box-shadow: 0 1px 2px rgba(1, 22, 39, 0.06), 0 6px 16px rgba(1, 22, 39, 0.06);
|
|
9610
10459
|
}
|
|
9611
|
-
|
|
9612
|
-
|
|
9613
|
-
|
|
10460
|
+
|
|
10461
|
+
.${cls} .scores-type {
|
|
10462
|
+
font-size: 16px;
|
|
10463
|
+
font-weight: 800;
|
|
10464
|
+
color: var(--owt-color-primary-dark, #F07B1A);
|
|
10465
|
+
line-height: 1.2;
|
|
10466
|
+
word-break: break-word;
|
|
9614
10467
|
}
|
|
10468
|
+
|
|
10469
|
+
.${cls} .scores-value {
|
|
10470
|
+
font-size: 34px;
|
|
10471
|
+
font-weight: 800;
|
|
10472
|
+
color: var(--owt-color-text, #011627);
|
|
10473
|
+
line-height: 1.05;
|
|
10474
|
+
letter-spacing: -0.25px;
|
|
10475
|
+
}
|
|
10476
|
+
|
|
10477
|
+
.${cls} .scores-separator {
|
|
10478
|
+
height: 1px;
|
|
10479
|
+
width: 100%;
|
|
10480
|
+
background-color: var(--owt-color-border-light, #E4E4E4);
|
|
10481
|
+
border: none;
|
|
10482
|
+
margin: 2px 0;
|
|
10483
|
+
}
|
|
10484
|
+
|
|
10485
|
+
.${cls} .scores-value .scores-muted {
|
|
10486
|
+
font-size: 18px;
|
|
10487
|
+
font-weight: 600;
|
|
10488
|
+
color: var(--owt-color-text-muted, #727474);
|
|
10489
|
+
margin-left: 6px;
|
|
10490
|
+
}
|
|
10491
|
+
|
|
10492
|
+
.${cls} .scores-meta {
|
|
10493
|
+
display: flex;
|
|
10494
|
+
flex-direction: column;
|
|
10495
|
+
gap: 4px;
|
|
10496
|
+
}
|
|
10497
|
+
|
|
10498
|
+
.${cls} .scores-meta-line {
|
|
10499
|
+
font-size: 13px;
|
|
10500
|
+
color: var(--owt-color-text-muted, #727474);
|
|
10501
|
+
font-weight: 500;
|
|
10502
|
+
}
|
|
10503
|
+
|
|
10504
|
+
.${cls} .scores-meta-line strong {
|
|
10505
|
+
color: var(--owt-color-text, #011627);
|
|
10506
|
+
font-weight: 700;
|
|
10507
|
+
}
|
|
10508
|
+
|
|
10509
|
+
@media (max-width: 1024px) {
|
|
10510
|
+
.${cls} .scores-grid {
|
|
10511
|
+
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
|
10512
|
+
}
|
|
10513
|
+
}
|
|
10514
|
+
|
|
10515
|
+
@media (max-width: 640px) {
|
|
10516
|
+
.${cls} .scores-grid {
|
|
10517
|
+
grid-template-columns: 1fr;
|
|
10518
|
+
}
|
|
10519
|
+
}
|
|
10520
|
+
` }), jsxRuntimeExports.jsx("div", { className: cls, children: sortedScores.length === 0 ? (jsxRuntimeExports.jsx("div", { className: "scores-subtle", children: "No scores available." })) : (jsxRuntimeExports.jsx("div", { className: "scores-grid", children: sortedScores.map((s, idx) => {
|
|
10521
|
+
const scoreType = s?.score_type ? String(s.score_type) : '-';
|
|
10522
|
+
const scoreValue = s?.computed_score !== undefined &&
|
|
10523
|
+
s?.computed_score !== null &&
|
|
10524
|
+
String(s.computed_score) !== ''
|
|
10525
|
+
? String(s.computed_score)
|
|
10526
|
+
: '-';
|
|
10527
|
+
const computedAt = tryFormatDateTime$1(s?.computed_at);
|
|
10528
|
+
const key = `${scoreType}-${String(s?.computed_at || '')}-${idx}`;
|
|
10529
|
+
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));
|
|
10530
|
+
}) })) })] }));
|
|
10531
|
+
};
|
|
10532
|
+
|
|
10533
|
+
function tryFormatDateTime(value) {
|
|
10534
|
+
if (typeof value !== 'string' || !value)
|
|
10535
|
+
return value ? String(value) : '-';
|
|
10536
|
+
const d = new Date(value);
|
|
10537
|
+
if (Number.isNaN(d.getTime()))
|
|
10538
|
+
return value;
|
|
10539
|
+
try {
|
|
10540
|
+
return d.toLocaleString(undefined, {
|
|
10541
|
+
year: 'numeric',
|
|
10542
|
+
month: 'short',
|
|
10543
|
+
day: '2-digit',
|
|
10544
|
+
hour: '2-digit',
|
|
10545
|
+
minute: '2-digit',
|
|
10546
|
+
});
|
|
10547
|
+
}
|
|
10548
|
+
catch {
|
|
10549
|
+
return value;
|
|
10550
|
+
}
|
|
10551
|
+
}
|
|
10552
|
+
function tryFormatDate(value) {
|
|
10553
|
+
if (typeof value !== 'string' || !value)
|
|
10554
|
+
return value ? String(value) : '-';
|
|
10555
|
+
const d = new Date(value);
|
|
10556
|
+
if (Number.isNaN(d.getTime()))
|
|
10557
|
+
return value;
|
|
10558
|
+
try {
|
|
10559
|
+
return d.toLocaleDateString(undefined, {
|
|
10560
|
+
year: 'numeric',
|
|
10561
|
+
month: 'short',
|
|
10562
|
+
day: '2-digit',
|
|
10563
|
+
});
|
|
10564
|
+
}
|
|
10565
|
+
catch {
|
|
10566
|
+
return value;
|
|
10567
|
+
}
|
|
10568
|
+
}
|
|
10569
|
+
function displayText(value) {
|
|
10570
|
+
if (value === null || value === undefined || String(value).trim() === '')
|
|
10571
|
+
return '-';
|
|
10572
|
+
return String(value);
|
|
10573
|
+
}
|
|
10574
|
+
function normalizeStatus(raw) {
|
|
10575
|
+
if (raw === null || raw === undefined || String(raw).trim() === '')
|
|
10576
|
+
return 'unknown';
|
|
10577
|
+
const v = String(raw).trim().toLowerCase();
|
|
10578
|
+
if (v === 'success' || v === 'succeeded' || v === 'ok')
|
|
10579
|
+
return 'success';
|
|
10580
|
+
if (v === 'failure' || v === 'failed' || v === 'error')
|
|
10581
|
+
return 'failure';
|
|
10582
|
+
if (v === 'not done' || v === 'not_done' || v === 'not-done' || v === 'pending')
|
|
10583
|
+
return 'not_done';
|
|
10584
|
+
return 'unknown';
|
|
10585
|
+
}
|
|
10586
|
+
/** Large enough for eSignet / OIDC login; clamped so it always fits the current screen. */
|
|
10587
|
+
function getCenteredPopupFeatures(width, height) {
|
|
10588
|
+
const dualScreenLeft = window.screenLeft ?? window.screenX ?? 0;
|
|
10589
|
+
const dualScreenTop = window.screenTop ?? window.screenY ?? 0;
|
|
10590
|
+
const viewportWidth = window.innerWidth || document.documentElement.clientWidth || (typeof screen !== 'undefined' ? screen.width : width);
|
|
10591
|
+
const viewportHeight = window.innerHeight || document.documentElement.clientHeight || (typeof screen !== 'undefined' ? screen.height : height);
|
|
10592
|
+
const maxW = Math.max(320, Math.floor(viewportWidth * 0.92));
|
|
10593
|
+
const maxH = Math.max(400, Math.floor(viewportHeight * 0.92));
|
|
10594
|
+
const w = Math.max(320, Math.min(width, maxW));
|
|
10595
|
+
const h = Math.max(400, Math.min(height, maxH));
|
|
10596
|
+
const left = Math.max(0, Math.floor(viewportWidth / 2 - w / 2 + dualScreenLeft));
|
|
10597
|
+
const top = Math.max(0, Math.floor(viewportHeight / 2 - h / 2 + dualScreenTop));
|
|
10598
|
+
return [
|
|
10599
|
+
'popup=yes',
|
|
10600
|
+
'noopener=yes',
|
|
10601
|
+
'noreferrer=yes',
|
|
10602
|
+
`width=${w}`,
|
|
10603
|
+
`height=${h}`,
|
|
10604
|
+
`left=${left}`,
|
|
10605
|
+
`top=${top}`,
|
|
10606
|
+
'scrollbars=yes',
|
|
10607
|
+
'resizable=yes',
|
|
10608
|
+
].join(',');
|
|
10609
|
+
}
|
|
10610
|
+
function pickAuthorizationUrl(resp, explicitKey) {
|
|
10611
|
+
if (!resp)
|
|
10612
|
+
return null;
|
|
10613
|
+
const tryKey = (k) => {
|
|
10614
|
+
const v = resp?.[k];
|
|
10615
|
+
if (typeof v === 'string' && v)
|
|
10616
|
+
return v;
|
|
10617
|
+
return null;
|
|
10618
|
+
};
|
|
10619
|
+
if (explicitKey) {
|
|
10620
|
+
const v = tryKey(explicitKey);
|
|
10621
|
+
if (v)
|
|
10622
|
+
return v;
|
|
10623
|
+
}
|
|
10624
|
+
return (tryKey('authentication_url') ||
|
|
10625
|
+
tryKey('authorization_url') ||
|
|
10626
|
+
tryKey('authorizationUrl') ||
|
|
10627
|
+
tryKey('auth_url') ||
|
|
10628
|
+
tryKey('authUrl') ||
|
|
10629
|
+
tryKey('url') ||
|
|
10630
|
+
null);
|
|
10631
|
+
}
|
|
10632
|
+
function resolveValueFromSources(path, values, schemaData) {
|
|
10633
|
+
if (!path)
|
|
9615
10634
|
return undefined;
|
|
9616
|
-
|
|
9617
|
-
|
|
9618
|
-
|
|
9619
|
-
|
|
10635
|
+
const fromValues = getValueByPath(values, path);
|
|
10636
|
+
if (fromValues !== undefined)
|
|
10637
|
+
return fromValues;
|
|
10638
|
+
return getValueByPath(schemaData, path);
|
|
10639
|
+
}
|
|
10640
|
+
function unwrapPayload(response) {
|
|
10641
|
+
if (response && typeof response === 'object') {
|
|
10642
|
+
if (response.response_body?.response_payload !== undefined)
|
|
10643
|
+
return response.response_body.response_payload;
|
|
10644
|
+
if (response.response_payload !== undefined)
|
|
10645
|
+
return response.response_payload;
|
|
10646
|
+
}
|
|
10647
|
+
return response;
|
|
10648
|
+
}
|
|
10649
|
+
const IdAuthenticationWidget = ({ config, schemaData: propSchemaData }) => {
|
|
10650
|
+
const { dataSourceRequestHandler, schemaData: ctxSchemaData } = useWidgetContext();
|
|
10651
|
+
const values = reactRedux.useSelector((state) => state.widget.values);
|
|
10652
|
+
const schemaData = (propSchemaData || ctxSchemaData || {});
|
|
10653
|
+
const widgetId = config['widget-id'];
|
|
10654
|
+
const dataPath = config['widget-data-path'];
|
|
10655
|
+
const paths = React.useMemo(() => {
|
|
10656
|
+
if (!dataPath || typeof dataPath !== 'object')
|
|
10657
|
+
return {};
|
|
10658
|
+
return dataPath;
|
|
10659
|
+
}, [dataPath]);
|
|
10660
|
+
const authConfig = config['widget-auth-config'];
|
|
10661
|
+
const registerId = authConfig?.registerId ?? undefined;
|
|
10662
|
+
const internalRecordId = resolveValueFromSources(paths.internalRecordId, values, schemaData);
|
|
10663
|
+
const initiatedByStaffId = resolveValueFromSources(paths.initiatedByStaffId, values, schemaData);
|
|
10664
|
+
const providerId = authConfig?.providerId;
|
|
10665
|
+
const providerName = authConfig?.providerName;
|
|
10666
|
+
const foundationalId = resolveValueFromSources(paths.foundationalId, values, schemaData);
|
|
10667
|
+
const lastAuthenticatedOn = resolveValueFromSources(paths.lastAuthenticatedOn, values, schemaData);
|
|
10668
|
+
const lastAuthStatusRaw = resolveValueFromSources(paths.lastAuthenticationStatus, values, schemaData);
|
|
10669
|
+
const expiryDate = resolveValueFromSources(paths.expiryDate, values, schemaData);
|
|
10670
|
+
const psut = resolveValueFromSources(paths.authenticationToken, values, schemaData);
|
|
10671
|
+
const status = React.useMemo(() => normalizeStatus(lastAuthStatusRaw), [lastAuthStatusRaw]);
|
|
10672
|
+
/** URL from prefetch (or default); used when opening the OIDC / eSignet popup */
|
|
10673
|
+
const [resolvedAuthUrl, setResolvedAuthUrl] = React.useState(null);
|
|
10674
|
+
const [authActionLoading, setAuthActionLoading] = React.useState(false);
|
|
10675
|
+
const [authError, setAuthError] = React.useState(null);
|
|
10676
|
+
const popupRef = React.useRef(null);
|
|
10677
|
+
const pollTimerRef = React.useRef(null);
|
|
10678
|
+
const [overlayUrl, setOverlayUrl] = React.useState(null);
|
|
10679
|
+
const emitHostEvent = React.useCallback((detail) => {
|
|
10680
|
+
if (typeof window === 'undefined')
|
|
10681
|
+
return;
|
|
10682
|
+
window.dispatchEvent(new CustomEvent('openg2p:id-authentication', {
|
|
10683
|
+
detail: {
|
|
10684
|
+
widgetId,
|
|
10685
|
+
...detail,
|
|
10686
|
+
},
|
|
10687
|
+
}));
|
|
10688
|
+
}, [widgetId]);
|
|
10689
|
+
const cleanupPopup = React.useCallback(() => {
|
|
10690
|
+
if (pollTimerRef.current) {
|
|
10691
|
+
window.clearInterval(pollTimerRef.current);
|
|
10692
|
+
pollTimerRef.current = null;
|
|
10693
|
+
}
|
|
10694
|
+
popupRef.current = null;
|
|
10695
|
+
}, []);
|
|
9620
10696
|
React.useEffect(() => {
|
|
9621
|
-
|
|
9622
|
-
|
|
9623
|
-
|
|
9624
|
-
|
|
9625
|
-
setResponse(null);
|
|
9626
|
-
return;
|
|
9627
|
-
}
|
|
9628
|
-
if (!handler) {
|
|
9629
|
-
setError(null);
|
|
9630
|
-
setResponse(null);
|
|
9631
|
-
return;
|
|
10697
|
+
return () => {
|
|
10698
|
+
cleanupPopup();
|
|
10699
|
+
try {
|
|
10700
|
+
popupRef.current?.close?.();
|
|
9632
10701
|
}
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
const method = apiDs.method || 'POST';
|
|
9636
|
-
if (!service || !endpoint) {
|
|
9637
|
-
setError('Scores widget API data source is missing service/endpoint.');
|
|
9638
|
-
setResponse(null);
|
|
9639
|
-
return;
|
|
10702
|
+
catch {
|
|
10703
|
+
// ignore
|
|
9640
10704
|
}
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
10705
|
+
};
|
|
10706
|
+
}, [cleanupPopup]);
|
|
10707
|
+
// Provider details are supplied by host; clear any previous resolved URL on provider change.
|
|
10708
|
+
React.useEffect(() => {
|
|
10709
|
+
setResolvedAuthUrl(null);
|
|
10710
|
+
}, [providerId, providerName]);
|
|
10711
|
+
const openAuthPopup = React.useCallback((authUrl) => {
|
|
10712
|
+
if (authConfig?.useIframeOverlay === true) {
|
|
10713
|
+
setOverlayUrl(authUrl);
|
|
10714
|
+
emitHostEvent({ type: 'overlay_opened' });
|
|
10715
|
+
return;
|
|
10716
|
+
}
|
|
10717
|
+
const pw = authConfig?.popupWidth ?? 1024;
|
|
10718
|
+
const ph = authConfig?.popupHeight ?? 800;
|
|
10719
|
+
const features = getCenteredPopupFeatures(pw, ph);
|
|
10720
|
+
const popup = window.open(authUrl, `${widgetId}-oidc`, features);
|
|
10721
|
+
if (!popup) {
|
|
10722
|
+
setAuthError('Popup blocked. Please allow popups and try again.');
|
|
10723
|
+
return;
|
|
10724
|
+
}
|
|
10725
|
+
popupRef.current = popup;
|
|
10726
|
+
popup.focus?.();
|
|
10727
|
+
setAuthError(null);
|
|
10728
|
+
emitHostEvent({ type: 'popup_opened' });
|
|
10729
|
+
if (pollTimerRef.current) {
|
|
10730
|
+
window.clearInterval(pollTimerRef.current);
|
|
10731
|
+
pollTimerRef.current = null;
|
|
10732
|
+
}
|
|
10733
|
+
pollTimerRef.current = window.setInterval(() => {
|
|
10734
|
+
try {
|
|
10735
|
+
const closed = !popupRef.current || popupRef.current.closed;
|
|
10736
|
+
if (closed) {
|
|
10737
|
+
cleanupPopup();
|
|
10738
|
+
emitHostEvent({ type: 'popup_closed' });
|
|
10739
|
+
}
|
|
9645
10740
|
}
|
|
10741
|
+
catch {
|
|
10742
|
+
// ignore
|
|
10743
|
+
}
|
|
10744
|
+
}, 500);
|
|
10745
|
+
}, [authConfig, cleanupPopup, emitHostEvent, widgetId]);
|
|
10746
|
+
const onAuthenticate = React.useCallback(async () => {
|
|
10747
|
+
setAuthError(null);
|
|
10748
|
+
if (!authConfig) {
|
|
10749
|
+
setAuthError('Missing widget-auth-config.');
|
|
10750
|
+
return;
|
|
10751
|
+
}
|
|
10752
|
+
const canCallAuthApi = Boolean(dataSourceRequestHandler && authConfig.service && authConfig.authenticateEndpoint);
|
|
10753
|
+
let url = resolvedAuthUrl;
|
|
10754
|
+
if (!url && canCallAuthApi) {
|
|
10755
|
+
setAuthActionLoading(true);
|
|
9646
10756
|
try {
|
|
9647
|
-
|
|
9648
|
-
|
|
9649
|
-
const rawParams = apiDs.params || {};
|
|
9650
|
-
// Never pass the path helper through to the API.
|
|
9651
|
-
const { internal_record_id_path, internalRecordIdPath, ...rest } = rawParams;
|
|
9652
|
-
void internal_record_id_path;
|
|
9653
|
-
void internalRecordIdPath;
|
|
9654
|
-
const params = {
|
|
9655
|
-
...rest,
|
|
10757
|
+
const basePayload = {
|
|
10758
|
+
register_id: registerId,
|
|
9656
10759
|
internal_record_id: internalRecordId,
|
|
10760
|
+
provider_id: providerId,
|
|
10761
|
+
initiated_by_staff_id: initiatedByStaffId,
|
|
9657
10762
|
};
|
|
9658
|
-
const
|
|
9659
|
-
|
|
9660
|
-
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
|
|
9664
|
-
|
|
9665
|
-
|
|
9666
|
-
|
|
9667
|
-
setResponse(payload);
|
|
9668
|
-
}
|
|
9669
|
-
else {
|
|
9670
|
-
setResponse({ scores: Array.isArray(payload) ? payload : [] });
|
|
9671
|
-
}
|
|
10763
|
+
const requestParams = basePayload;
|
|
10764
|
+
// eslint-disable-next-line no-console
|
|
10765
|
+
console.log('[IdAuthenticationWidget] authenticate_registrant params', requestParams);
|
|
10766
|
+
const resp = await dataSourceRequestHandler(authConfig.service, authConfig.authenticateEndpoint, authConfig.authenticateMethod || 'POST', requestParams);
|
|
10767
|
+
const payload = unwrapPayload(resp);
|
|
10768
|
+
const authUrl = pickAuthorizationUrl(payload, authConfig.authorizationUrlKey);
|
|
10769
|
+
url = authUrl || null;
|
|
10770
|
+
if (authUrl)
|
|
10771
|
+
setResolvedAuthUrl(authUrl);
|
|
9672
10772
|
}
|
|
9673
10773
|
catch (e) {
|
|
9674
|
-
if (
|
|
10774
|
+
if (!url) {
|
|
10775
|
+
setAuthError(e?.message || 'Could not load provider URL.');
|
|
9675
10776
|
return;
|
|
9676
|
-
|
|
9677
|
-
const msg = maybeErr && typeof maybeErr === 'object' && typeof maybeErr.message === 'string'
|
|
9678
|
-
? maybeErr.message
|
|
9679
|
-
: 'Failed to load scores.';
|
|
9680
|
-
setError(msg);
|
|
9681
|
-
setResponse(null);
|
|
10777
|
+
}
|
|
9682
10778
|
}
|
|
9683
10779
|
finally {
|
|
9684
|
-
|
|
9685
|
-
|
|
10780
|
+
setAuthActionLoading(false);
|
|
10781
|
+
}
|
|
10782
|
+
}
|
|
10783
|
+
if (!url) {
|
|
10784
|
+
setAuthError('No authorization URL returned from authenticate_registrant.');
|
|
10785
|
+
return;
|
|
10786
|
+
}
|
|
10787
|
+
openAuthPopup(url);
|
|
10788
|
+
}, [
|
|
10789
|
+
authConfig,
|
|
10790
|
+
dataSourceRequestHandler,
|
|
10791
|
+
openAuthPopup,
|
|
10792
|
+
registerId,
|
|
10793
|
+
internalRecordId,
|
|
10794
|
+
initiatedByStaffId,
|
|
10795
|
+
resolvedAuthUrl,
|
|
10796
|
+
]);
|
|
10797
|
+
React.useEffect(() => {
|
|
10798
|
+
const successType = authConfig?.successMessageType || 'openg2p:oidc:success';
|
|
10799
|
+
const handler = (event) => {
|
|
10800
|
+
const data = event?.data;
|
|
10801
|
+
if (!data || typeof data !== 'object')
|
|
10802
|
+
return;
|
|
10803
|
+
if (data.type !== successType)
|
|
10804
|
+
return;
|
|
10805
|
+
if (data.widgetId && data.widgetId !== widgetId)
|
|
10806
|
+
return;
|
|
10807
|
+
emitHostEvent({ type: 'authenticated', payload: data });
|
|
10808
|
+
try {
|
|
10809
|
+
popupRef.current?.close?.();
|
|
10810
|
+
}
|
|
10811
|
+
catch {
|
|
10812
|
+
// ignore
|
|
10813
|
+
}
|
|
10814
|
+
cleanupPopup();
|
|
10815
|
+
if (authConfig?.reloadOnSuccess) {
|
|
10816
|
+
window.location.reload();
|
|
9686
10817
|
}
|
|
9687
10818
|
};
|
|
9688
|
-
|
|
9689
|
-
return () =>
|
|
9690
|
-
|
|
9691
|
-
|
|
9692
|
-
|
|
9693
|
-
|
|
9694
|
-
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
|
|
10819
|
+
window.addEventListener('message', handler);
|
|
10820
|
+
return () => window.removeEventListener('message', handler);
|
|
10821
|
+
}, [authConfig?.reloadOnSuccess, authConfig?.successMessageType, cleanupPopup, emitHostEvent, widgetId]);
|
|
10822
|
+
const cls = `id-auth-widget-${widgetId}`;
|
|
10823
|
+
const statusLabel = React.useMemo(() => {
|
|
10824
|
+
if (status === 'success')
|
|
10825
|
+
return 'Success';
|
|
10826
|
+
if (status === 'failure')
|
|
10827
|
+
return 'Failure';
|
|
10828
|
+
if (status === 'not_done')
|
|
10829
|
+
return 'Not done';
|
|
10830
|
+
return 'Unknown';
|
|
10831
|
+
}, [status]);
|
|
10832
|
+
const statusColor = React.useMemo(() => {
|
|
10833
|
+
if (status === 'success')
|
|
10834
|
+
return 'var(--owt-color-success, #16A34A)';
|
|
10835
|
+
if (status === 'failure')
|
|
10836
|
+
return 'var(--owt-color-danger, #DC2626)';
|
|
10837
|
+
if (status === 'not_done')
|
|
10838
|
+
return 'var(--owt-color-warning, #D97706)';
|
|
10839
|
+
return 'var(--owt-color-text-muted, #6B7280)';
|
|
10840
|
+
}, [status]);
|
|
10841
|
+
const buttonBusy = authActionLoading;
|
|
10842
|
+
const buttonDisabled = !authConfig || buttonBusy;
|
|
9700
10843
|
return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("style", { children: `
|
|
9701
10844
|
.${cls} {
|
|
9702
10845
|
width: 100%;
|
|
9703
10846
|
font-family: Roboto, sans-serif;
|
|
9704
|
-
|
|
10847
|
+
}
|
|
10848
|
+
|
|
10849
|
+
/* Two-column field grid; primary action in a bottom band (matches section save/edit pattern). */
|
|
10850
|
+
.${cls} .auth-content {
|
|
9705
10851
|
display: flex;
|
|
9706
10852
|
flex-direction: column;
|
|
9707
|
-
gap:
|
|
10853
|
+
gap: 0;
|
|
10854
|
+
min-width: 0;
|
|
9708
10855
|
}
|
|
9709
10856
|
|
|
9710
|
-
.${cls} .
|
|
10857
|
+
.${cls} .auth-grid {
|
|
10858
|
+
display: grid;
|
|
10859
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
10860
|
+
gap: 16px 24px;
|
|
10861
|
+
min-width: 0;
|
|
10862
|
+
}
|
|
10863
|
+
|
|
10864
|
+
/* Each field: label (left) + value (right), same as DisplayWidget readonly */
|
|
10865
|
+
.${cls} .auth-cell {
|
|
10866
|
+
display: flex;
|
|
10867
|
+
flex-direction: row;
|
|
10868
|
+
align-items: flex-start;
|
|
10869
|
+
gap: 12px 16px;
|
|
10870
|
+
min-width: 0;
|
|
10871
|
+
}
|
|
10872
|
+
|
|
10873
|
+
.${cls} .auth-cell.auth-cell--full {
|
|
10874
|
+
grid-column: 1 / -1;
|
|
10875
|
+
}
|
|
10876
|
+
|
|
10877
|
+
/* Action cell: no left label spacer, align button to column start */
|
|
10878
|
+
.${cls} .auth-cell.auth-cell--action .auth-label {
|
|
10879
|
+
display: none;
|
|
10880
|
+
}
|
|
10881
|
+
.${cls} .auth-cell.auth-cell--action .auth-value {
|
|
10882
|
+
flex: 1 1 auto;
|
|
10883
|
+
}
|
|
10884
|
+
|
|
10885
|
+
.${cls} .auth-label {
|
|
10886
|
+
flex: 0 0 auto;
|
|
10887
|
+
min-width: 200px;
|
|
10888
|
+
max-width: 40%;
|
|
10889
|
+
font-size: 16px;
|
|
10890
|
+
color: rgba(0, 0, 0, 0.6);
|
|
10891
|
+
font-weight: 500;
|
|
10892
|
+
line-height: 1.45;
|
|
10893
|
+
margin: 0;
|
|
10894
|
+
word-break: break-word;
|
|
10895
|
+
}
|
|
10896
|
+
|
|
10897
|
+
.${cls} .auth-value {
|
|
10898
|
+
flex: 1 1 auto;
|
|
10899
|
+
min-width: 0;
|
|
10900
|
+
font-size: 16px;
|
|
10901
|
+
color: var(--owt-color-text, #111827);
|
|
10902
|
+
font-weight: 500;
|
|
10903
|
+
line-height: 1.45;
|
|
10904
|
+
word-break: break-word;
|
|
10905
|
+
}
|
|
10906
|
+
|
|
10907
|
+
.${cls} .auth-value--foundational {
|
|
10908
|
+
font-size: 18px;
|
|
10909
|
+
font-weight: 700;
|
|
10910
|
+
color: var(--owt-color-primary-dark, #F07B1A);
|
|
10911
|
+
letter-spacing: 0.1px;
|
|
10912
|
+
}
|
|
10913
|
+
|
|
10914
|
+
/* Button is placed inside the grid (next to PSUT) */
|
|
10915
|
+
|
|
10916
|
+
.${cls} .auth-status {
|
|
10917
|
+
display: inline-flex;
|
|
10918
|
+
align-items: center;
|
|
10919
|
+
gap: 8px;
|
|
10920
|
+
width: fit-content;
|
|
10921
|
+
padding: 4px 10px;
|
|
10922
|
+
border-radius: 999px;
|
|
10923
|
+
background: rgba(2, 6, 23, 0.04);
|
|
10924
|
+
border: 1px solid rgba(2, 6, 23, 0.08);
|
|
9711
10925
|
font-size: 13px;
|
|
9712
|
-
|
|
9713
|
-
|
|
10926
|
+
font-weight: 700;
|
|
10927
|
+
color: var(--owt-color-text, #011627);
|
|
9714
10928
|
}
|
|
9715
10929
|
|
|
9716
|
-
.${cls} .
|
|
9717
|
-
width:
|
|
10930
|
+
.${cls} .auth-dot {
|
|
10931
|
+
width: 8px;
|
|
10932
|
+
height: 8px;
|
|
10933
|
+
border-radius: 50%;
|
|
10934
|
+
background: ${statusColor};
|
|
10935
|
+
}
|
|
10936
|
+
|
|
10937
|
+
.${cls} .auth-token {
|
|
10938
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
10939
|
+
font-size: 14px;
|
|
10940
|
+
font-weight: 500;
|
|
10941
|
+
color: var(--owt-color-text, #011627);
|
|
10942
|
+
background: transparent;
|
|
9718
10943
|
border: none;
|
|
9719
10944
|
border-radius: 0;
|
|
9720
|
-
background: transparent;
|
|
9721
10945
|
padding: 0;
|
|
9722
|
-
|
|
9723
|
-
grid-template-columns: 1fr 1fr 1fr;
|
|
9724
|
-
gap: 16px;
|
|
9725
|
-
align-items: center;
|
|
10946
|
+
word-break: break-all;
|
|
9726
10947
|
}
|
|
9727
10948
|
|
|
9728
|
-
.${cls} .
|
|
9729
|
-
|
|
9730
|
-
|
|
9731
|
-
|
|
9732
|
-
|
|
10949
|
+
.${cls} .auth-button {
|
|
10950
|
+
/* Match SectionRegistryView Save CTA (SectionRenderer) */
|
|
10951
|
+
font-size: 14px;
|
|
10952
|
+
font-weight: 500;
|
|
10953
|
+
padding: 8px 24px;
|
|
10954
|
+
line-height: 1.5;
|
|
10955
|
+
border-radius: var(--owt-btn-border-radius, 10px);
|
|
10956
|
+
border: 1px solid rgb(237, 124, 34);
|
|
10957
|
+
background-color: rgb(237, 124, 34);
|
|
10958
|
+
color: var(--owt-color-bg, #FFFFFF);
|
|
10959
|
+
font-family: Roboto, sans-serif;
|
|
10960
|
+
cursor: pointer;
|
|
10961
|
+
transition: opacity 0.15s ease;
|
|
9733
10962
|
}
|
|
9734
10963
|
|
|
9735
|
-
.${cls} .
|
|
10964
|
+
.${cls} .auth-button:disabled {
|
|
10965
|
+
opacity: 0.5;
|
|
10966
|
+
cursor: not-allowed;
|
|
10967
|
+
}
|
|
10968
|
+
|
|
10969
|
+
.${cls} .auth-error {
|
|
9736
10970
|
font-size: 12px;
|
|
9737
|
-
color: var(--owt-color-
|
|
9738
|
-
font-weight:
|
|
9739
|
-
|
|
9740
|
-
text-
|
|
10971
|
+
color: var(--owt-color-danger, #DC2626);
|
|
10972
|
+
font-weight: 700;
|
|
10973
|
+
line-height: 1.3;
|
|
10974
|
+
text-align: left;
|
|
10975
|
+
max-width: 100%;
|
|
9741
10976
|
}
|
|
9742
10977
|
|
|
9743
|
-
.${cls} .
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
10978
|
+
.${cls} .overlay-backdrop {
|
|
10979
|
+
position: fixed;
|
|
10980
|
+
inset: 0;
|
|
10981
|
+
background: rgba(17, 24, 39, 0.55);
|
|
10982
|
+
z-index: 9999;
|
|
10983
|
+
display: flex;
|
|
10984
|
+
align-items: center;
|
|
10985
|
+
justify-content: center;
|
|
10986
|
+
padding: 24px;
|
|
9749
10987
|
}
|
|
9750
10988
|
|
|
9751
|
-
.${cls} .
|
|
9752
|
-
|
|
9753
|
-
|
|
10989
|
+
.${cls} .overlay-panel {
|
|
10990
|
+
width: min(1100px, 92vw);
|
|
10991
|
+
height: min(820px, 92vh);
|
|
10992
|
+
background: var(--owt-color-bg, #FFFFFF);
|
|
10993
|
+
border-radius: 12px;
|
|
10994
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.25);
|
|
10995
|
+
overflow: hidden;
|
|
10996
|
+
display: flex;
|
|
10997
|
+
flex-direction: column;
|
|
9754
10998
|
}
|
|
9755
10999
|
|
|
9756
|
-
.${cls} .
|
|
9757
|
-
display:
|
|
9758
|
-
align-items:
|
|
9759
|
-
|
|
9760
|
-
|
|
11000
|
+
.${cls} .overlay-header {
|
|
11001
|
+
display: flex;
|
|
11002
|
+
align-items: center;
|
|
11003
|
+
justify-content: space-between;
|
|
11004
|
+
padding: 10px 14px;
|
|
11005
|
+
border-bottom: 1px solid var(--owt-color-border-light, #E4E4E4);
|
|
11006
|
+
font-family: Roboto, sans-serif;
|
|
11007
|
+
}
|
|
11008
|
+
|
|
11009
|
+
.${cls} .overlay-title {
|
|
11010
|
+
font-size: 14px;
|
|
11011
|
+
color: var(--owt-color-text, #011627);
|
|
11012
|
+
font-weight: 600;
|
|
11013
|
+
min-width: 0;
|
|
11014
|
+
overflow: hidden;
|
|
11015
|
+
text-overflow: ellipsis;
|
|
11016
|
+
white-space: nowrap;
|
|
9761
11017
|
}
|
|
9762
11018
|
|
|
9763
|
-
.${cls} .
|
|
11019
|
+
.${cls} .overlay-close {
|
|
11020
|
+
border: 1px solid var(--owt-btn-secondary-border, #C4C4C4);
|
|
11021
|
+
background: var(--owt-btn-secondary-bg, #FFFFFF);
|
|
11022
|
+
color: var(--owt-btn-secondary-color, #011627);
|
|
11023
|
+
border-radius: var(--owt-btn-border-radius, 10px);
|
|
11024
|
+
padding: 6px 10px;
|
|
11025
|
+
font-size: 12px;
|
|
11026
|
+
cursor: pointer;
|
|
11027
|
+
}
|
|
9764
11028
|
|
|
9765
|
-
.${cls} .
|
|
9766
|
-
|
|
9767
|
-
|
|
11029
|
+
.${cls} .overlay-iframe {
|
|
11030
|
+
flex: 1 1 auto;
|
|
11031
|
+
width: 100%;
|
|
11032
|
+
border: none;
|
|
9768
11033
|
}
|
|
9769
11034
|
|
|
9770
|
-
@media (max-width:
|
|
9771
|
-
.${cls} .
|
|
11035
|
+
@media (max-width: 640px) {
|
|
11036
|
+
.${cls} .auth-grid {
|
|
9772
11037
|
grid-template-columns: 1fr;
|
|
9773
11038
|
}
|
|
11039
|
+
.${cls} .auth-cell {
|
|
11040
|
+
flex-direction: column;
|
|
11041
|
+
align-items: stretch;
|
|
11042
|
+
gap: 4px 0;
|
|
11043
|
+
}
|
|
11044
|
+
.${cls} .auth-label {
|
|
11045
|
+
min-width: 0;
|
|
11046
|
+
max-width: none;
|
|
11047
|
+
}
|
|
9774
11048
|
}
|
|
9775
|
-
` }), jsxRuntimeExports.
|
|
11049
|
+
` }), jsxRuntimeExports.jsxs("div", { className: cls, children: [overlayUrl ? (jsxRuntimeExports.jsx("div", { className: "overlay-backdrop", role: "dialog", "aria-modal": "true", "aria-label": "Authentication", onClick: (e) => {
|
|
11050
|
+
if (e.target === e.currentTarget) {
|
|
11051
|
+
setOverlayUrl(null);
|
|
11052
|
+
emitHostEvent({ type: 'overlay_closed' });
|
|
11053
|
+
}
|
|
11054
|
+
}, 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: () => {
|
|
11055
|
+
setOverlayUrl(null);
|
|
11056
|
+
emitHostEvent({ type: 'overlay_closed' });
|
|
11057
|
+
}, 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] })] })] }) })] })] }));
|
|
9776
11058
|
};
|
|
9777
11059
|
|
|
9778
11060
|
/**
|
|
@@ -9804,6 +11086,8 @@ const registerDefaultWidgets = () => {
|
|
|
9804
11086
|
widgetRegistry.register({ widget: 'simple-table', component: SimpleTableWidget });
|
|
9805
11087
|
// Table widget with record-level editing
|
|
9806
11088
|
widgetRegistry.register({ widget: 'table', component: TableWidget });
|
|
11089
|
+
// Table widget with add/edit popup dialog
|
|
11090
|
+
widgetRegistry.register({ widget: 'dialog-table', component: DialogTableWidget });
|
|
9807
11091
|
// Group widgets
|
|
9808
11092
|
widgetRegistry.register({ widget: 'array-widget', component: ArrayWidget });
|
|
9809
11093
|
widgetRegistry.register({ widget: 'iterable-accordion', component: IterableAccordionWidget });
|
|
@@ -9818,6 +11102,8 @@ const registerDefaultWidgets = () => {
|
|
|
9818
11102
|
widgetRegistry.register({ widget: 'header-section', component: HeaderSectionWidget });
|
|
9819
11103
|
// Scores display widget for full-width computed scores display (view-only)
|
|
9820
11104
|
widgetRegistry.register({ widget: 'scores-display', component: ScoresDisplayWidget });
|
|
11105
|
+
// ID Authentication widget for OIDC-based foundational ID authentication (view-only + action)
|
|
11106
|
+
widgetRegistry.register({ widget: 'id-authentication', component: IdAuthenticationWidget });
|
|
9821
11107
|
};
|
|
9822
11108
|
// Auto-register on import
|
|
9823
11109
|
registerDefaultWidgets();
|
|
@@ -10186,9 +11472,11 @@ exports.CheckboxWidget = CheckboxWidget;
|
|
|
10186
11472
|
exports.CurrencyInputWidget = CurrencyInputWidget;
|
|
10187
11473
|
exports.DateInputWidget = DateInputWidget;
|
|
10188
11474
|
exports.DateTimeInputWidget = DateTimeInputWidget;
|
|
11475
|
+
exports.DialogTableWidget = DialogTableWidget;
|
|
10189
11476
|
exports.DisplayWidget = DisplayWidget;
|
|
10190
11477
|
exports.FileInputWidget = FileInputWidget;
|
|
10191
11478
|
exports.HeaderSectionWidget = HeaderSectionWidget;
|
|
11479
|
+
exports.IdAuthenticationWidget = IdAuthenticationWidget;
|
|
10192
11480
|
exports.IterableAccordionWidget = IterableAccordionWidget;
|
|
10193
11481
|
exports.JSONEditorPanel = JSONEditorPanel;
|
|
10194
11482
|
exports.NumberInputWidget = NumberInputWidget;
|