@openg2p/registry-widgets 1.1.0-dev.1 → 1.1.0-dev.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/SectionRenderer.d.ts +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 +31 -1
- package/dist/index.esm.js +1464 -265
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1465 -264
- package/dist/index.js.map +1 -1
- package/dist/registry/defaultWidgets.d.ts.map +1 -1
- package/dist/utils/dataSource.d.ts.map +1 -1
- package/dist/widgets/DialogTableWidget.d.ts +25 -0
- package/dist/widgets/DialogTableWidget.d.ts.map +1 -0
- package/dist/widgets/DisplayWidget.d.ts.map +1 -1
- package/dist/widgets/HeaderSectionWidget.d.ts.map +1 -1
- package/dist/widgets/IdAuthenticationWidget.d.ts +8 -0
- package/dist/widgets/IdAuthenticationWidget.d.ts.map +1 -0
- package/dist/widgets/index.d.ts +2 -0
- package/dist/widgets/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -1004,6 +1004,19 @@ const getApiDataSource = async (dataSource, allValues, dataSourceRequestHandler,
|
|
|
1004
1004
|
// If not found and doesn't contain dots, try as widget-id
|
|
1005
1005
|
if ((depValue === null || depValue === undefined) && !dataSource.dependsOn.includes('.')) {
|
|
1006
1006
|
depValue = allValues[dataSource.dependsOn];
|
|
1007
|
+
// Smart resolution: If not found at top level, try to find the dependency in the same nested object
|
|
1008
|
+
// by looking for other keys in allValues that might contain the dependency.
|
|
1009
|
+
// We look for objects that contain both the current widget's path (if we can guess it) and the dependency.
|
|
1010
|
+
// But since we don't know the current widget's path here, we search for any object that has this dependency key.
|
|
1011
|
+
if (depValue === null || depValue === undefined || depValue === '') {
|
|
1012
|
+
for (const val of Object.values(allValues)) {
|
|
1013
|
+
if (val && typeof val === 'object' && !Array.isArray(val) && dataSource.dependsOn in val) {
|
|
1014
|
+
depValue = val[dataSource.dependsOn];
|
|
1015
|
+
if (depValue !== null && depValue !== undefined && depValue !== '')
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1007
1020
|
}
|
|
1008
1021
|
if (depValue === null || depValue === undefined || depValue === '') {
|
|
1009
1022
|
// If dependency is empty, return empty array
|
|
@@ -1047,8 +1060,8 @@ const getApiDataSource = async (dataSource, allValues, dataSourceRequestHandler,
|
|
|
1047
1060
|
}
|
|
1048
1061
|
}
|
|
1049
1062
|
else if (staticParams.level_id) {
|
|
1050
|
-
// First level has no parent
|
|
1051
|
-
requestParams.parent_level_value_id =
|
|
1063
|
+
// First level has no parent, send empty string as many OpenG2P APIs expect it
|
|
1064
|
+
requestParams.parent_level_value_id = "";
|
|
1052
1065
|
}
|
|
1053
1066
|
// Get service mnemonic and endpoint (required)
|
|
1054
1067
|
const service = dataSource.service;
|
|
@@ -1117,10 +1130,12 @@ const transformDataSourceOptions = (data, valueKey, labelKey) => {
|
|
|
1117
1130
|
return { value: item, label: String(item) };
|
|
1118
1131
|
});
|
|
1119
1132
|
}
|
|
1120
|
-
return data.map((item) =>
|
|
1121
|
-
value
|
|
1122
|
-
label
|
|
1123
|
-
|
|
1133
|
+
return data.map((item) => {
|
|
1134
|
+
const value = item[valueKey];
|
|
1135
|
+
// Try multiple common label keys if the primary one is missing
|
|
1136
|
+
const label = item[labelKey] || item.name || item.label || item.mnemonic || item.level_value_mnemonic || String(value);
|
|
1137
|
+
return { value, label };
|
|
1138
|
+
});
|
|
1124
1139
|
};
|
|
1125
1140
|
|
|
1126
1141
|
const WidgetEventBusContext = React.createContext(null);
|
|
@@ -1898,46 +1913,71 @@ const useBaseWidget = (options) => {
|
|
|
1898
1913
|
const userHasSetValueRef = useRef(false);
|
|
1899
1914
|
// Use ref for values to avoid stale closures in handleChange
|
|
1900
1915
|
const valuesRef = useRef(values);
|
|
1916
|
+
const loadingRef = useRef(loading);
|
|
1917
|
+
const dataSourceOptionsRef = useRef(dataSourceOptions);
|
|
1901
1918
|
useEffect(() => {
|
|
1902
1919
|
valuesRef.current = values;
|
|
1903
|
-
|
|
1920
|
+
loadingRef.current = loading;
|
|
1921
|
+
dataSourceOptionsRef.current = dataSourceOptions;
|
|
1922
|
+
}, [values, loading, dataSourceOptions]);
|
|
1904
1923
|
// Track last dispatched value to prevent duplicate dispatches
|
|
1905
1924
|
const lastDispatchedValueRef = useRef(null);
|
|
1906
|
-
//
|
|
1907
|
-
const
|
|
1908
|
-
if (
|
|
1909
|
-
return
|
|
1925
|
+
// Helper to extract displayable value from object (especially geo hierarchy objects)
|
|
1926
|
+
const extractValueFromObject = useCallback((obj) => {
|
|
1927
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
1928
|
+
return obj;
|
|
1910
1929
|
}
|
|
1911
|
-
//
|
|
1912
|
-
const
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
if (
|
|
1919
|
-
return
|
|
1930
|
+
// Check for geo hierarchy structure first
|
|
1931
|
+
const geoConfig = config['widget-geo-config'];
|
|
1932
|
+
if (geoConfig) {
|
|
1933
|
+
// If we have a geo hierarchy object, extract the value for this specific level
|
|
1934
|
+
const hierarchy = obj.hierarchy || obj.geo_code_hierarchy_json?.hierarchy;
|
|
1935
|
+
if (Array.isArray(hierarchy)) {
|
|
1936
|
+
const levelData = hierarchy.find((l) => l.level === geoConfig.level);
|
|
1937
|
+
if (levelData) {
|
|
1938
|
+
return levelData.level_value_id;
|
|
1920
1939
|
}
|
|
1921
|
-
// If it's a geo hierarchy object but no extractable ID, return undefined to avoid rendering object
|
|
1922
|
-
return undefined;
|
|
1923
1940
|
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1941
|
+
}
|
|
1942
|
+
if ('geo_code_hierarchy_json' in obj || 'geo_lowest_level_value_id' in obj || 'hierarchy' in obj) {
|
|
1943
|
+
if ('geo_lowest_level_value_id' in obj) {
|
|
1944
|
+
return obj.geo_lowest_level_value_id;
|
|
1927
1945
|
}
|
|
1928
|
-
if ('
|
|
1929
|
-
return obj.
|
|
1946
|
+
if ('lowest_level_value_id' in obj) {
|
|
1947
|
+
return obj.lowest_level_value_id;
|
|
1930
1948
|
}
|
|
1931
|
-
|
|
1932
|
-
|
|
1949
|
+
// Fallback for nested geo_code_hierarchy_json
|
|
1950
|
+
if (obj.geo_code_hierarchy_json?.lowest_level_value_id) {
|
|
1951
|
+
return obj.geo_code_hierarchy_json.lowest_level_value_id;
|
|
1933
1952
|
}
|
|
1934
|
-
if (
|
|
1935
|
-
return obj.
|
|
1953
|
+
if (obj.geo_code_hierarchy_json?.geo_lowest_level_value_id) {
|
|
1954
|
+
return obj.geo_code_hierarchy_json.geo_lowest_level_value_id;
|
|
1936
1955
|
}
|
|
1937
|
-
// If no extractable
|
|
1938
|
-
// This prevents "Objects are not valid as a React child" errors
|
|
1956
|
+
// If it's a geo hierarchy object but no extractable ID, return undefined to avoid rendering object
|
|
1939
1957
|
return undefined;
|
|
1940
|
-
}
|
|
1958
|
+
}
|
|
1959
|
+
// Try common value fields
|
|
1960
|
+
if ('value' in obj) {
|
|
1961
|
+
return obj.value;
|
|
1962
|
+
}
|
|
1963
|
+
if ('id' in obj) {
|
|
1964
|
+
return obj.id;
|
|
1965
|
+
}
|
|
1966
|
+
if ('label' in obj) {
|
|
1967
|
+
return obj.label;
|
|
1968
|
+
}
|
|
1969
|
+
if ('name' in obj) {
|
|
1970
|
+
return obj.name;
|
|
1971
|
+
}
|
|
1972
|
+
// If no extractable value found, return undefined to avoid rendering object as React child
|
|
1973
|
+
// This prevents "Objects are not valid as a React child" errors
|
|
1974
|
+
return undefined;
|
|
1975
|
+
}, [config]);
|
|
1976
|
+
// Get current value
|
|
1977
|
+
const currentValue = useMemo(() => {
|
|
1978
|
+
if (isLayoutWidget) {
|
|
1979
|
+
return undefined; // Layout widgets don't have values
|
|
1980
|
+
}
|
|
1941
1981
|
// Try to get value from widgetId first (this should have the actual selected value)
|
|
1942
1982
|
// For geo widgets with dataPath, widgetId stores the actual ID, while dataPath stores the hierarchy object
|
|
1943
1983
|
let value = values[widgetId];
|
|
@@ -1978,6 +2018,33 @@ const useBaseWidget = (options) => {
|
|
|
1978
2018
|
}
|
|
1979
2019
|
return value !== undefined ? value : config['widget-data-default'];
|
|
1980
2020
|
}, [values, config, widgetId, isLayoutWidget]);
|
|
2021
|
+
// Track the last value we attempted to mirror to prevent infinite loops
|
|
2022
|
+
const lastMirroredValueRef = useRef(null);
|
|
2023
|
+
// Mirror value from dataPath to widgetId in Redux state if it's not already there.
|
|
2024
|
+
// This is essential for widgets that depend on this widget via 'dependsOn' using its widgetId,
|
|
2025
|
+
// especially when the actual data is stored in a nested path.
|
|
2026
|
+
// CRITICAL: This ensures that dependencies are resolved correctly when entering Edit mode.
|
|
2027
|
+
useEffect(() => {
|
|
2028
|
+
if (isLayoutWidget || !config['widget-data-path']) {
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
const rawValue = getWidgetValue(values, config['widget-data-path'], widgetId);
|
|
2032
|
+
if (rawValue !== undefined && rawValue !== null) {
|
|
2033
|
+
const extractedValue = extractValueFromObject(rawValue);
|
|
2034
|
+
// Only mirror if:
|
|
2035
|
+
// 1. The top-level value is undefined (initial load or entering edit mode)
|
|
2036
|
+
// 2. We haven't already tried to mirror this specific value (prevents loops if dispatch is ignored or delayed)
|
|
2037
|
+
// 3. The extracted value is valid
|
|
2038
|
+
if (values[widgetId] === undefined &&
|
|
2039
|
+
extractedValue !== undefined &&
|
|
2040
|
+
extractedValue !== null &&
|
|
2041
|
+
lastMirroredValueRef.current !== extractedValue) {
|
|
2042
|
+
lastMirroredValueRef.current = extractedValue;
|
|
2043
|
+
dispatch(setValue({ widgetId, value: extractedValue }));
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2047
|
+
}, [values, config['widget-data-path'], widgetId, isLayoutWidget]);
|
|
1981
2048
|
// Initialize default value only once on mount (skip for layout widgets)
|
|
1982
2049
|
useEffect(() => {
|
|
1983
2050
|
if (isLayoutWidget) {
|
|
@@ -2000,6 +2067,20 @@ const useBaseWidget = (options) => {
|
|
|
2000
2067
|
if (currentValue === newValue) {
|
|
2001
2068
|
return;
|
|
2002
2069
|
}
|
|
2070
|
+
// CRITICAL FIX: Ignore auto-clears (empty string or undefined) from UI components
|
|
2071
|
+
// when the widget's data source is currently loading OR if options are empty.
|
|
2072
|
+
// This prevents data disappearance when switching to Edit mode and components
|
|
2073
|
+
// incorrectly clear values before options load or if handler is temporarily missing.
|
|
2074
|
+
if (newValue === '' || newValue === null || newValue === undefined) {
|
|
2075
|
+
if (loadingRef.current) {
|
|
2076
|
+
console.warn(`[useBaseWidget] Ignoring empty value for ${widgetId} because data source is loading`);
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
if (config['widget-data-source']?.type === 'api' && dataSourceOptionsRef.current.length === 0) {
|
|
2080
|
+
console.warn(`[useBaseWidget] Ignoring empty value for ${widgetId} because API options are empty`);
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2003
2084
|
// Mark that user has set a value (unless this is the default initialization)
|
|
2004
2085
|
if (newValue !== config['widget-data-default'] || userHasSetValueRef.current) {
|
|
2005
2086
|
userHasSetValueRef.current = true;
|
|
@@ -2017,6 +2098,13 @@ const useBaseWidget = (options) => {
|
|
|
2017
2098
|
}
|
|
2018
2099
|
else {
|
|
2019
2100
|
// Has dataPath: update both widgetId and dataPath
|
|
2101
|
+
// CRITICAL: For geo widgets, we do NOT want to overwrite the shared hierarchy dataPath
|
|
2102
|
+
// with a primitive value (the selected ID). The hierarchy object is managed by useGeoWidgetCascade.
|
|
2103
|
+
if (config['widget-geo-config']) {
|
|
2104
|
+
dispatch(setValue({ widgetId, value: newValue }));
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
// For non-geo widgets, update both widgetId and dataPath
|
|
2020
2108
|
// CRITICAL: Create updated values object with newValue already set
|
|
2021
2109
|
// This prevents setWidgetValue from reading stale values
|
|
2022
2110
|
const currentValuesWithUpdate = {
|
|
@@ -2111,6 +2199,17 @@ const useBaseWidget = (options) => {
|
|
|
2111
2199
|
const apiService = dataSource?.type === 'api' ? dataSource.service : '';
|
|
2112
2200
|
const apiEndpoint = dataSource?.type === 'api' ? dataSource.endpoint : '';
|
|
2113
2201
|
const configKey = `${widgetId}-${isReadonly}-${dataSource?.type || 'none'}-${apiService}-${apiEndpoint}`;
|
|
2202
|
+
// Extract dependency value using a granular selector to prevent unnecessary re-renders
|
|
2203
|
+
// and infinite loops when other unrelated values in the state change.
|
|
2204
|
+
const dependencyValue = useSelector((state) => {
|
|
2205
|
+
if (dataSource?.type !== 'api' || !dataSource.dependsOn) {
|
|
2206
|
+
return null;
|
|
2207
|
+
}
|
|
2208
|
+
if (dataSource.dependsOn.includes('.')) {
|
|
2209
|
+
return getWidgetValue(state.widget.values, dataSource.dependsOn, '');
|
|
2210
|
+
}
|
|
2211
|
+
return state.widget.values[dataSource.dependsOn];
|
|
2212
|
+
});
|
|
2114
2213
|
// Handle data source loading
|
|
2115
2214
|
useEffect(() => {
|
|
2116
2215
|
if (!dataSource) {
|
|
@@ -2131,6 +2230,17 @@ const useBaseWidget = (options) => {
|
|
|
2131
2230
|
}
|
|
2132
2231
|
else {
|
|
2133
2232
|
depValue = values[dataSource.dependsOn];
|
|
2233
|
+
// Smart resolution: If not found at top level, and current widget has a nested dataPath,
|
|
2234
|
+
// try to find the dependency in the same nested object.
|
|
2235
|
+
if ((depValue === undefined || depValue === null || depValue === '') &&
|
|
2236
|
+
typeof config['widget-data-path'] === 'string' &&
|
|
2237
|
+
config['widget-data-path'].includes('.')) {
|
|
2238
|
+
const pathParts = config['widget-data-path'].split('.');
|
|
2239
|
+
pathParts.pop(); // Remove current field name
|
|
2240
|
+
const prefix = pathParts.join('.');
|
|
2241
|
+
const tryPath = `${prefix}.${dataSource.dependsOn}`;
|
|
2242
|
+
depValue = getWidgetValue(values, tryPath, '');
|
|
2243
|
+
}
|
|
2134
2244
|
}
|
|
2135
2245
|
// If dependency is empty, don't load (will load when dependency has value)
|
|
2136
2246
|
if (depValue === null || depValue === undefined || depValue === '') {
|
|
@@ -2164,7 +2274,7 @@ const useBaseWidget = (options) => {
|
|
|
2164
2274
|
}
|
|
2165
2275
|
// Extract level_id from widget-geo-config.level if available
|
|
2166
2276
|
const levelId = geoConfig?.level;
|
|
2167
|
-
data = await getApiDataSource(dataSource,
|
|
2277
|
+
data = await getApiDataSource(dataSource, valuesRef.current, currentHandler, levelId);
|
|
2168
2278
|
}
|
|
2169
2279
|
else if (dataSource.type === 'schema') {
|
|
2170
2280
|
data = getSchemaDataSource(dataSource, schemaData || {});
|
|
@@ -2199,9 +2309,9 @@ const useBaseWidget = (options) => {
|
|
|
2199
2309
|
}
|
|
2200
2310
|
};
|
|
2201
2311
|
loadDataSource();
|
|
2202
|
-
// Use configKey to ensure effect runs when
|
|
2312
|
+
// Use configKey and dependencyValue to ensure effect runs only when relevant state changes
|
|
2203
2313
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2204
|
-
}, [configKey,
|
|
2314
|
+
}, [configKey, dependencyValue, dataSourceRequestHandler, schemaData, widgetId, dispatch]);
|
|
2205
2315
|
return {
|
|
2206
2316
|
widgetId,
|
|
2207
2317
|
value: currentValue,
|
|
@@ -2402,6 +2512,9 @@ const useGeoWidgetCascade = (options) => {
|
|
|
2402
2512
|
const geoConfig = config['widget-geo-config'];
|
|
2403
2513
|
const dataSource = config['widget-data-source'];
|
|
2404
2514
|
const dataPath = config['widget-data-path'];
|
|
2515
|
+
const groupId = typeof dataPath === 'string' && dataPath.includes('.')
|
|
2516
|
+
? dataPath.split('.').slice(0, -1).join('.')
|
|
2517
|
+
: 'default';
|
|
2405
2518
|
const valuesRef = useRef(values);
|
|
2406
2519
|
const handlerRef = useRef(dataSourceRequestHandler);
|
|
2407
2520
|
// Keep refs updated
|
|
@@ -2411,10 +2524,36 @@ const useGeoWidgetCascade = (options) => {
|
|
|
2411
2524
|
}, [values, dataSourceRequestHandler]);
|
|
2412
2525
|
// Get current value and data source options
|
|
2413
2526
|
const currentValue = useSelector((state) => {
|
|
2414
|
-
|
|
2415
|
-
|
|
2527
|
+
// Try to get value from widgetId first (most recent selection)
|
|
2528
|
+
let value = state.widget.values[widgetId];
|
|
2529
|
+
// If not found in widgetId, try dataPath
|
|
2530
|
+
if (value === undefined && dataPath) {
|
|
2531
|
+
value = getWidgetValue(state.widget.values, dataPath, widgetId);
|
|
2532
|
+
}
|
|
2533
|
+
// Extract value if it's a geo hierarchy object
|
|
2534
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && geoConfig) {
|
|
2535
|
+
const hierarchy = value.hierarchy || value.geo_code_hierarchy_json?.hierarchy;
|
|
2536
|
+
if (Array.isArray(hierarchy)) {
|
|
2537
|
+
const levelData = hierarchy.find((l) => l.level === geoConfig.level);
|
|
2538
|
+
if (levelData) {
|
|
2539
|
+
return levelData.level_value_id;
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
// Extended fallbacks (matching useBaseWidget)
|
|
2543
|
+
if ('geo_lowest_level_value_id' in value) {
|
|
2544
|
+
return value.geo_lowest_level_value_id;
|
|
2545
|
+
}
|
|
2546
|
+
if ('lowest_level_value_id' in value) {
|
|
2547
|
+
return value.lowest_level_value_id;
|
|
2548
|
+
}
|
|
2549
|
+
if (value.geo_code_hierarchy_json?.lowest_level_value_id) {
|
|
2550
|
+
return value.geo_code_hierarchy_json.lowest_level_value_id;
|
|
2551
|
+
}
|
|
2552
|
+
if (value.geo_code_hierarchy_json?.geo_lowest_level_value_id) {
|
|
2553
|
+
return value.geo_code_hierarchy_json.geo_lowest_level_value_id;
|
|
2554
|
+
}
|
|
2416
2555
|
}
|
|
2417
|
-
return
|
|
2556
|
+
return value;
|
|
2418
2557
|
});
|
|
2419
2558
|
// Memoize selector to avoid returning new array reference
|
|
2420
2559
|
const dataSourceOptions = useSelector((state) => state.widget.dataSources[widgetId] ?? EMPTY_DATA_SOURCE);
|
|
@@ -2434,11 +2573,17 @@ const useGeoWidgetCascade = (options) => {
|
|
|
2434
2573
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
2435
2574
|
const currentValues = valuesRef.current;
|
|
2436
2575
|
const currentHandler = handlerRef.current;
|
|
2437
|
-
// CRITICAL:
|
|
2438
|
-
|
|
2439
|
-
|
|
2576
|
+
// CRITICAL: Try to get parent value from event first, then from Redux
|
|
2577
|
+
let parentValue = event.value;
|
|
2578
|
+
if (parentValue === undefined || parentValue === null) {
|
|
2579
|
+
parentValue = currentValues[parentWidgetId];
|
|
2580
|
+
// If not found in top-level values, try to find it via dataPath or dependsOn
|
|
2581
|
+
if (parentValue === undefined && dataSource.dependsOn) {
|
|
2582
|
+
parentValue = getWidgetValue(currentValues, dataSource.dependsOn, '');
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2440
2585
|
// Remove this level and all below from hierarchy
|
|
2441
|
-
geoHierarchyBuilder.removeLevelAndBelow(level);
|
|
2586
|
+
geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
|
|
2442
2587
|
// Clear this widget's value
|
|
2443
2588
|
// CRITICAL: Only dispatch setValue for THIS widget, not for parent or other widgets
|
|
2444
2589
|
// setWidgetValue returns the entire updated state, but we only want to update this widget
|
|
@@ -2493,7 +2638,8 @@ const useGeoWidgetCascade = (options) => {
|
|
|
2493
2638
|
}
|
|
2494
2639
|
}
|
|
2495
2640
|
else {
|
|
2496
|
-
// If parent value is cleared, clear the data source
|
|
2641
|
+
// If parent value is cleared, clear the data source and hierarchy
|
|
2642
|
+
geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
|
|
2497
2643
|
dispatch(setDataSource({ widgetId, data: [] }));
|
|
2498
2644
|
}
|
|
2499
2645
|
};
|
|
@@ -2508,25 +2654,49 @@ const useGeoWidgetCascade = (options) => {
|
|
|
2508
2654
|
if (!geoConfig) {
|
|
2509
2655
|
return;
|
|
2510
2656
|
}
|
|
2511
|
-
// Skip if value is
|
|
2512
|
-
if
|
|
2513
|
-
|
|
2657
|
+
// Skip if value is undefined (it might still be loading or rehydrating)
|
|
2658
|
+
// ONLY clear hierarchy if the value is explicitly null or empty string (user action)
|
|
2659
|
+
if (currentValue === null || currentValue === '') {
|
|
2514
2660
|
const { level } = geoConfig;
|
|
2515
|
-
geoHierarchyBuilder.removeLevelAndBelow(level);
|
|
2661
|
+
geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
|
|
2662
|
+
// If we have a dataPath, we need to update Redux with the cleared hierarchy
|
|
2663
|
+
if (dataPath) {
|
|
2664
|
+
const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson(groupId);
|
|
2665
|
+
let finalUpdatedValues = valuesRef.current;
|
|
2666
|
+
// Use logic similar to the build section below to update the dataPath
|
|
2667
|
+
if (typeof dataPath === 'string' && dataPath.endsWith('.geo_code_hierarchy_json')) {
|
|
2668
|
+
const prefix = dataPath.substring(0, dataPath.lastIndexOf('.'));
|
|
2669
|
+
finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson?.geo_code_hierarchy_json);
|
|
2670
|
+
finalUpdatedValues = setWidgetValue(finalUpdatedValues, `${prefix}.geo_lowest_level_value_id`, widgetId, hierarchyJson?.geo_lowest_level_value_id);
|
|
2671
|
+
}
|
|
2672
|
+
else {
|
|
2673
|
+
finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson?.geo_code_hierarchy_json);
|
|
2674
|
+
}
|
|
2675
|
+
dispatch(setValues(finalUpdatedValues));
|
|
2676
|
+
}
|
|
2516
2677
|
return;
|
|
2517
2678
|
}
|
|
2679
|
+
if (currentValue === undefined) {
|
|
2680
|
+
return; // Skip if undefined (still initializing)
|
|
2681
|
+
}
|
|
2518
2682
|
const { level, isLastLevel } = geoConfig;
|
|
2519
|
-
//
|
|
2520
|
-
if (
|
|
2683
|
+
// Check if hierarchy is already built to prevent endless loops
|
|
2684
|
+
if (dataPath) {
|
|
2521
2685
|
const currentHierarchy = getWidgetValue(valuesRef.current, dataPath, widgetId);
|
|
2522
2686
|
// If hierarchy JSON is already set and matches current value, skip rebuilding
|
|
2523
|
-
if (currentHierarchy && typeof currentHierarchy === 'object'
|
|
2524
|
-
// Check if
|
|
2525
|
-
const
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2687
|
+
if (currentHierarchy && typeof currentHierarchy === 'object') {
|
|
2688
|
+
// Check if this specific level's value matches the hierarchy
|
|
2689
|
+
const hierarchyArray = currentHierarchy.hierarchy || currentHierarchy.geo_code_hierarchy_json?.hierarchy;
|
|
2690
|
+
if (Array.isArray(hierarchyArray)) {
|
|
2691
|
+
const currentLevelValue = typeof currentValue === 'object'
|
|
2692
|
+
? (currentValue.level_value_id || currentValue.id || currentValue.value)
|
|
2693
|
+
: currentValue;
|
|
2694
|
+
const levelData = hierarchyArray.find((l) => l.level === geoConfig.level);
|
|
2695
|
+
// If this level is already correctly represented in the hierarchy, skip rebuilding
|
|
2696
|
+
// String conversion ensures comparison works for mixed types
|
|
2697
|
+
if (levelData && String(levelData.level_value_id) === String(currentLevelValue)) {
|
|
2698
|
+
return;
|
|
2699
|
+
}
|
|
2530
2700
|
}
|
|
2531
2701
|
}
|
|
2532
2702
|
}
|
|
@@ -2549,23 +2719,31 @@ const useGeoWidgetCascade = (options) => {
|
|
|
2549
2719
|
return;
|
|
2550
2720
|
}
|
|
2551
2721
|
// When a widget's own value changes, remove this level and all below from hierarchy first
|
|
2552
|
-
|
|
2553
|
-
// The addLevel method already handles removing existing levels, but we explicitly clear to be safe
|
|
2554
|
-
geoHierarchyBuilder.removeLevelAndBelow(level);
|
|
2722
|
+
geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
|
|
2555
2723
|
// Add level to hierarchy
|
|
2556
|
-
geoHierarchyBuilder.addLevel(level, level_value_id, level_value_mnemonic);
|
|
2557
|
-
//
|
|
2558
|
-
if (
|
|
2559
|
-
const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson();
|
|
2724
|
+
geoHierarchyBuilder.addLevel(level, level_value_id, level_value_mnemonic, groupId);
|
|
2725
|
+
// Build and store hierarchy JSON on every change
|
|
2726
|
+
if (dataPath) {
|
|
2727
|
+
const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson(groupId);
|
|
2560
2728
|
if (hierarchyJson) {
|
|
2561
|
-
//
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2729
|
+
// Fix: Avoid double nesting of geo_code_hierarchy_json
|
|
2730
|
+
// If dataPath ends with .geo_code_hierarchy_json, we want to save the content directly to it
|
|
2731
|
+
// and save the lowest level ID as a sibling
|
|
2732
|
+
let finalUpdatedValues = valuesRef.current;
|
|
2733
|
+
if (typeof dataPath === 'string' && dataPath.endsWith('.geo_code_hierarchy_json')) {
|
|
2734
|
+
const prefix = dataPath.substring(0, dataPath.lastIndexOf('.'));
|
|
2735
|
+
// Save hierarchy JSON content directly to dataPath (avoiding double nesting)
|
|
2736
|
+
finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson.geo_code_hierarchy_json);
|
|
2737
|
+
// Save lowest level ID as sibling
|
|
2738
|
+
finalUpdatedValues = setWidgetValue(finalUpdatedValues, `${prefix}.geo_lowest_level_value_id`, widgetId, hierarchyJson.geo_lowest_level_value_id);
|
|
2739
|
+
}
|
|
2740
|
+
else {
|
|
2741
|
+
// Fallback if path doesn't follow the naming convention
|
|
2742
|
+
finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson.geo_code_hierarchy_json);
|
|
2743
|
+
}
|
|
2744
|
+
// CRITICAL: Use setValues for deep merge instead of replacing root keys with setValue
|
|
2745
|
+
// setWidgetValue returns the complete updated state object with all keys preserved
|
|
2746
|
+
dispatch(setValues(finalUpdatedValues));
|
|
2569
2747
|
}
|
|
2570
2748
|
}
|
|
2571
2749
|
}, [geoConfig, currentValue, widgetId, dataPath, dispatch, dataSourceOptions]);
|
|
@@ -3670,6 +3848,39 @@ const sectionValidate = (section, currentSchemaData, dispatch) => {
|
|
|
3670
3848
|
return isValid;
|
|
3671
3849
|
};
|
|
3672
3850
|
|
|
3851
|
+
/** Root class on readonly label/value rows; SectionRenderer scopes overflow/ellipsis rules here. */
|
|
3852
|
+
const READONLY_VALUE_ROW_ROOT_CLASSES = [
|
|
3853
|
+
'TextDisplayWidget',
|
|
3854
|
+
'TextAreaDisplayWidget',
|
|
3855
|
+
'SelectDisplayWidget',
|
|
3856
|
+
'PhoneDisplayWidget',
|
|
3857
|
+
'NumberDisplayWidget',
|
|
3858
|
+
'CurrencyDisplayWidget',
|
|
3859
|
+
'RadioDisplayWidget',
|
|
3860
|
+
'DateDisplayWidget',
|
|
3861
|
+
'DateTimeDisplayWidget',
|
|
3862
|
+
'CheckboxDisplayWidget',
|
|
3863
|
+
'BooleanDisplayWidget',
|
|
3864
|
+
'FileDisplayWidget',
|
|
3865
|
+
'DisplayFieldWidget',
|
|
3866
|
+
];
|
|
3867
|
+
/** Rows whose value is one line in .flex-1 > .text-gray-900 (ellipsis; full string via title on the element). */
|
|
3868
|
+
const READONLY_SINGLE_LINE_VALUE_ROW_CLASSES = [
|
|
3869
|
+
'TextDisplayWidget',
|
|
3870
|
+
'SelectDisplayWidget',
|
|
3871
|
+
'PhoneDisplayWidget',
|
|
3872
|
+
'NumberDisplayWidget',
|
|
3873
|
+
'CurrencyDisplayWidget',
|
|
3874
|
+
'RadioDisplayWidget',
|
|
3875
|
+
'DateDisplayWidget',
|
|
3876
|
+
'DateTimeDisplayWidget',
|
|
3877
|
+
'CheckboxDisplayWidget',
|
|
3878
|
+
'BooleanDisplayWidget',
|
|
3879
|
+
'DisplayFieldWidget',
|
|
3880
|
+
];
|
|
3881
|
+
function scopedClassSelectors(sectionClassId, classNames) {
|
|
3882
|
+
return classNames.map((c) => `.${sectionClassId} .${c}`).join(',\n ');
|
|
3883
|
+
}
|
|
3673
3884
|
/**
|
|
3674
3885
|
* Renders a section with its panels
|
|
3675
3886
|
*
|
|
@@ -3698,41 +3909,22 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
3698
3909
|
}
|
|
3699
3910
|
return section;
|
|
3700
3911
|
}, [section, namespace]);
|
|
3701
|
-
// Create namespaced schemaData if namespace is provided
|
|
3702
|
-
//
|
|
3912
|
+
// Create namespaced schemaData if namespace is provided.
|
|
3913
|
+
// Widgets with namespaced data-paths (e.g. "rv-section-0.a1a4d25a.birth_date")
|
|
3914
|
+
// need a nested object at values[namespace] so getValueByPath can traverse it.
|
|
3703
3915
|
const namespacedSchemaData = useMemo(() => {
|
|
3704
3916
|
if (!namespace || !currentSchemaData) {
|
|
3705
3917
|
return schemaData;
|
|
3706
3918
|
}
|
|
3707
|
-
|
|
3708
|
-
const namespaced = { ...currentSchemaData };
|
|
3709
|
-
// Copy all top-level keys to namespaced paths
|
|
3710
|
-
Object.keys(currentSchemaData).forEach(key => {
|
|
3711
|
-
const namespacedKey = `${namespace}.${key}`;
|
|
3712
|
-
if (!(namespacedKey in namespaced)) {
|
|
3713
|
-
namespaced[namespacedKey] = currentSchemaData[key];
|
|
3714
|
-
}
|
|
3715
|
-
});
|
|
3716
|
-
// Also handle nested objects - copy nested values to namespaced paths
|
|
3717
|
-
const copyNestedValues = (obj, prefix = '') => {
|
|
3718
|
-
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
3719
|
-
Object.keys(obj).forEach(key => {
|
|
3720
|
-
const fullPath = prefix ? `${prefix}.${key}` : key;
|
|
3721
|
-
const namespacedPath = `${namespace}.${fullPath}`;
|
|
3722
|
-
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
|
|
3723
|
-
copyNestedValues(obj[key], fullPath);
|
|
3724
|
-
// Also set the nested object at the namespaced path
|
|
3725
|
-
setValueByPath(namespaced, namespacedPath, obj[key]);
|
|
3726
|
-
}
|
|
3727
|
-
else {
|
|
3728
|
-
setValueByPath(namespaced, namespacedPath, obj[key]);
|
|
3729
|
-
}
|
|
3730
|
-
});
|
|
3731
|
-
}
|
|
3732
|
-
};
|
|
3733
|
-
copyNestedValues(currentSchemaData);
|
|
3734
|
-
return namespaced;
|
|
3919
|
+
return { ...currentSchemaData, [namespace]: currentSchemaData };
|
|
3735
3920
|
}, [namespace, schemaData, currentSchemaData]);
|
|
3921
|
+
// Populate the store with namespaced schema data so that namespaced widgets
|
|
3922
|
+
// can read their initial values via getValueByPath on the namespaced paths.
|
|
3923
|
+
useEffect(() => {
|
|
3924
|
+
if (namespace && namespacedSchemaData) {
|
|
3925
|
+
dispatch(setValues(namespacedSchemaData));
|
|
3926
|
+
}
|
|
3927
|
+
}, [namespace, namespacedSchemaData, dispatch]);
|
|
3736
3928
|
const crViewData = useMemo(() => {
|
|
3737
3929
|
if (mode !== 'CRView')
|
|
3738
3930
|
return null;
|
|
@@ -3755,6 +3947,9 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
3755
3947
|
const sectionId = sectionToRender['section-id'];
|
|
3756
3948
|
const gridId = `section-panels-${sectionId}`;
|
|
3757
3949
|
const sectionClassId = `section-${sectionId}`;
|
|
3950
|
+
const readonlyValueRowRootsCss = useMemo(() => scopedClassSelectors(sectionClassId, READONLY_VALUE_ROW_ROOT_CLASSES), [sectionClassId]);
|
|
3951
|
+
const readonlyValueRowFlex1Css = useMemo(() => READONLY_VALUE_ROW_ROOT_CLASSES.map((c) => `.${sectionClassId} .${c} > .flex-1`).join(',\n '), [sectionClassId]);
|
|
3952
|
+
const readonlySingleLineValueTextCss = useMemo(() => READONLY_SINGLE_LINE_VALUE_ROW_CLASSES.map((c) => `.${sectionClassId} .${c} > .flex-1 > .text-gray-900`).join(',\n '), [sectionClassId]);
|
|
3758
3953
|
// IntakeForm mode: accordion expand/collapse state (supports toggle)
|
|
3759
3954
|
const [standaloneExpanded, setStandaloneExpanded] = useState(true); // For sectionIndex undefined (standalone use)
|
|
3760
3955
|
const isExpandedFromContainer = typeof sectionIndex === 'number' && expandedSectionIndex === sectionIndex;
|
|
@@ -4107,6 +4302,8 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4107
4302
|
const baselineSnapshotRef = useRef(null);
|
|
4108
4303
|
// IntakeForm only: increment when baseline is updated after save - forces badge to update (refs don't trigger re-renders)
|
|
4109
4304
|
const [intakeFormBaselineTrigger, setIntakeFormBaselineTrigger] = useState(0);
|
|
4305
|
+
// IntakeForm only: tracks whether the user has actually saved this section (prevents "Saved" badge on initial load)
|
|
4306
|
+
const [hasBeenSavedByUser, setHasBeenSavedByUser] = useState(false);
|
|
4110
4307
|
// IntakeForm: treat as edit mode for dirty tracking when isDraft. RegistryView: use isEditMode.
|
|
4111
4308
|
const effectiveEditModeForDirty = mode === 'IntakeForm' ? (isDraft !== false) : isEditMode;
|
|
4112
4309
|
// Compute isDirty: compare current store state to baseline (only when in edit mode)
|
|
@@ -4149,18 +4346,68 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4149
4346
|
const intakeFormSectionStatus = useMemo(() => {
|
|
4150
4347
|
if (mode !== 'IntakeForm' || isDraft === false)
|
|
4151
4348
|
return null;
|
|
4152
|
-
const hasValue = (v) => v !== undefined && v !== null && (typeof v !== 'string' || v.trim().length > 0);
|
|
4153
|
-
const currentSnapshot = buildSectionSnapshot(storeValues, namespace);
|
|
4154
|
-
const record = currentSnapshot.records?.[0];
|
|
4155
|
-
const hasData = record &&
|
|
4156
|
-
typeof record === 'object' &&
|
|
4157
|
-
Object.values(record).some((v) => hasValue(v));
|
|
4158
4349
|
if (isDirty)
|
|
4159
4350
|
return 'modified';
|
|
4160
|
-
if (
|
|
4351
|
+
if (hasBeenSavedByUser)
|
|
4161
4352
|
return 'saved';
|
|
4162
4353
|
return null;
|
|
4163
|
-
}, [mode, isDirty,
|
|
4354
|
+
}, [mode, isDirty, hasBeenSavedByUser]);
|
|
4355
|
+
// Revert store values to the original schemaData for this section's widgets.
|
|
4356
|
+
// Used by both handleSave (RegistryView raises a CR, so values should not persist)
|
|
4357
|
+
// and handleCancel.
|
|
4358
|
+
const revertToOriginalValues = useCallback(() => {
|
|
4359
|
+
const sectionWidgets = collectWidgets(originalSection.panels);
|
|
4360
|
+
const oldSchemaData = schemaData || contextSchemaData;
|
|
4361
|
+
const currentStoreValues = store.getState().widget.values;
|
|
4362
|
+
let newStoreValues = currentStoreValues;
|
|
4363
|
+
sectionWidgets.forEach(widget => {
|
|
4364
|
+
const originalWidgetId = widget['widget-id'];
|
|
4365
|
+
const namespacedWidgetId = namespace ? `${namespace}__${originalWidgetId}` : originalWidgetId;
|
|
4366
|
+
const widgetId = namespacedWidgetId;
|
|
4367
|
+
const originalDataPath = widget['widget-data-path'];
|
|
4368
|
+
const storeDataPath = namespace && originalDataPath
|
|
4369
|
+
? (typeof originalDataPath === 'string'
|
|
4370
|
+
? `${namespace}.${originalDataPath}`
|
|
4371
|
+
: Object.fromEntries(Object.entries(originalDataPath).map(([key, path]) => [key, `${namespace}.${path}`])))
|
|
4372
|
+
: originalDataPath;
|
|
4373
|
+
if (widgetId && originalDataPath) {
|
|
4374
|
+
let oldValue;
|
|
4375
|
+
if (typeof originalDataPath === 'object') {
|
|
4376
|
+
oldValue = {};
|
|
4377
|
+
Object.entries(originalDataPath).forEach(([key, path]) => {
|
|
4378
|
+
if (typeof path === 'string') {
|
|
4379
|
+
oldValue[key] = getValueByPath(oldSchemaData, path);
|
|
4380
|
+
}
|
|
4381
|
+
});
|
|
4382
|
+
}
|
|
4383
|
+
else if (typeof originalDataPath === 'string') {
|
|
4384
|
+
oldValue = getValueByPath(oldSchemaData, originalDataPath);
|
|
4385
|
+
}
|
|
4386
|
+
if (oldValue !== undefined) {
|
|
4387
|
+
newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
|
|
4388
|
+
// Also revert the widgetId-based entry — useBaseWidget.handleChange
|
|
4389
|
+
// sets values[widgetId] during editing, and useBaseWidget.currentValue
|
|
4390
|
+
// reads values[widgetId] first before falling through to the dataPath.
|
|
4391
|
+
newStoreValues = { ...newStoreValues, [widgetId]: oldValue };
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
4394
|
+
});
|
|
4395
|
+
if (hasSupportingDocuments) {
|
|
4396
|
+
const originalSupportingDocuments = originalSection['section-supporting-documents'] || [];
|
|
4397
|
+
originalSupportingDocuments.forEach((doc, index) => {
|
|
4398
|
+
const widgetId = `supporting-doc-${sectionId}-${index}`;
|
|
4399
|
+
const originalDataPath = doc['document-data-path'];
|
|
4400
|
+
const storeDataPath = namespace && originalDataPath
|
|
4401
|
+
? `${namespace}.${originalDataPath}`
|
|
4402
|
+
: originalDataPath;
|
|
4403
|
+
const oldValue = getValueByPath(oldSchemaData, originalDataPath);
|
|
4404
|
+
newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
|
|
4405
|
+
});
|
|
4406
|
+
}
|
|
4407
|
+
if (newStoreValues !== currentStoreValues) {
|
|
4408
|
+
dispatch(setValues(newStoreValues));
|
|
4409
|
+
}
|
|
4410
|
+
}, [originalSection, schemaData, contextSchemaData, store, namespace, hasSupportingDocuments, sectionId, dispatch]);
|
|
4164
4411
|
// Handle save button click
|
|
4165
4412
|
const handleSave = async () => {
|
|
4166
4413
|
if (!store || !onSectionSave) {
|
|
@@ -4197,12 +4444,24 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4197
4444
|
});
|
|
4198
4445
|
}
|
|
4199
4446
|
if (JSON.stringify(oldSchemaData) !== JSON.stringify(newSchemaData)) {
|
|
4447
|
+
let profileImage = null;
|
|
4448
|
+
for (const record of newSchemaData) {
|
|
4449
|
+
if (typeof record === 'object' && record !== null) {
|
|
4450
|
+
for (const [key, value] of Object.entries(record)) {
|
|
4451
|
+
if (value instanceof File) {
|
|
4452
|
+
profileImage = value;
|
|
4453
|
+
record[key] = '';
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4200
4458
|
try {
|
|
4201
4459
|
const sectionchanges = {
|
|
4202
4460
|
section_id: dbSectionId ?? originalSection['section-id'],
|
|
4203
4461
|
section_register_id: sectionRegisterId,
|
|
4204
4462
|
records: [...newSchemaData],
|
|
4205
|
-
files: [...sectionFiles]
|
|
4463
|
+
files: [...sectionFiles],
|
|
4464
|
+
...(profileImage ? { image: profileImage } : {}),
|
|
4206
4465
|
};
|
|
4207
4466
|
await onSectionSave(sectionchanges);
|
|
4208
4467
|
}
|
|
@@ -4210,6 +4469,12 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4210
4469
|
console.error('Section Changes Save failed', error);
|
|
4211
4470
|
}
|
|
4212
4471
|
}
|
|
4472
|
+
// In RegistryView, save raises a CR — the actual data update follows a
|
|
4473
|
+
// separate approval workflow, so revert the displayed values to the
|
|
4474
|
+
// originals so the view doesn't show unapproved edits.
|
|
4475
|
+
if (mode === 'RegistryView') {
|
|
4476
|
+
revertToOriginalValues();
|
|
4477
|
+
}
|
|
4213
4478
|
setIsEditMode(false);
|
|
4214
4479
|
onEditModeChange?.(originalSectionId, false);
|
|
4215
4480
|
};
|
|
@@ -4239,12 +4504,24 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4239
4504
|
});
|
|
4240
4505
|
}
|
|
4241
4506
|
if (JSON.stringify(oldSchemaData) !== JSON.stringify(newSchemaData)) {
|
|
4507
|
+
let profileImage = null;
|
|
4508
|
+
for (const record of newSchemaData) {
|
|
4509
|
+
if (typeof record === 'object' && record !== null) {
|
|
4510
|
+
for (const [key, value] of Object.entries(record)) {
|
|
4511
|
+
if (value instanceof File) {
|
|
4512
|
+
profileImage = value;
|
|
4513
|
+
record[key] = '';
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
4242
4518
|
try {
|
|
4243
4519
|
await onSectionSave({
|
|
4244
4520
|
section_id: dbSectionId ?? originalSection['section-id'],
|
|
4245
4521
|
section_register_id: sectionRegisterId,
|
|
4246
4522
|
records: [...newSchemaData],
|
|
4247
4523
|
files: [...sectionFiles],
|
|
4524
|
+
...(profileImage ? { image: profileImage } : {}),
|
|
4248
4525
|
});
|
|
4249
4526
|
}
|
|
4250
4527
|
catch (error) {
|
|
@@ -4255,6 +4532,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4255
4532
|
if (mode === 'IntakeForm') {
|
|
4256
4533
|
baselineSnapshotRef.current = buildSectionSnapshot(currentSchemaData, namespace);
|
|
4257
4534
|
setIntakeFormBaselineTrigger((prev) => prev + 1);
|
|
4535
|
+
setHasBeenSavedByUser(true);
|
|
4258
4536
|
}
|
|
4259
4537
|
onSectionDirtyChange?.(sectionId, false);
|
|
4260
4538
|
}
|
|
@@ -4263,64 +4541,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4263
4541
|
}, [store, onSectionSave, onSectionSaveSuccess, sectionIndex, originalSection, schemaData, contextSchemaData, namespace, hasSupportingDocuments, dbSectionId, sectionRegisterId, dispatch, buildSectionSnapshot, sectionId, onSectionDirtyChange, mode, isDraft]);
|
|
4264
4542
|
// Handle cancel button click
|
|
4265
4543
|
const handleCancel = () => {
|
|
4266
|
-
|
|
4267
|
-
// Use original section (without namespace) for collecting widgets
|
|
4268
|
-
const sectionWidgets = collectWidgets(originalSection.panels);
|
|
4269
|
-
const oldSchemaData = schemaData || contextSchemaData;
|
|
4270
|
-
const currentStoreValues = store.getState().widget.values;
|
|
4271
|
-
let newStoreValues = currentStoreValues;
|
|
4272
|
-
sectionWidgets.forEach(widget => {
|
|
4273
|
-
const originalWidgetId = widget['widget-id'];
|
|
4274
|
-
// If namespace was used, we need to use namespaced widget ID and data path
|
|
4275
|
-
const namespacedWidgetId = namespace ? `${namespace}__${originalWidgetId}` : originalWidgetId;
|
|
4276
|
-
const widgetId = namespacedWidgetId;
|
|
4277
|
-
const originalDataPath = widget['widget-data-path'];
|
|
4278
|
-
// If namespace was used, data path in store is namespaced, but we read from original schema using original path
|
|
4279
|
-
const storeDataPath = namespace && originalDataPath
|
|
4280
|
-
? (typeof originalDataPath === 'string'
|
|
4281
|
-
? `${namespace}.${originalDataPath}`
|
|
4282
|
-
: Object.fromEntries(Object.entries(originalDataPath).map(([key, path]) => [key, `${namespace}.${path}`])))
|
|
4283
|
-
: originalDataPath;
|
|
4284
|
-
if (widgetId && originalDataPath) {
|
|
4285
|
-
// Handle multi-path (object) or single path (string)
|
|
4286
|
-
// Read from original schema data using original paths
|
|
4287
|
-
let oldValue;
|
|
4288
|
-
if (typeof originalDataPath === 'object') {
|
|
4289
|
-
// Multi-path: get values for each path
|
|
4290
|
-
oldValue = {};
|
|
4291
|
-
Object.entries(originalDataPath).forEach(([key, path]) => {
|
|
4292
|
-
if (typeof path === 'string') {
|
|
4293
|
-
oldValue[key] = getValueByPath(oldSchemaData, path);
|
|
4294
|
-
}
|
|
4295
|
-
});
|
|
4296
|
-
}
|
|
4297
|
-
else if (typeof originalDataPath === 'string') {
|
|
4298
|
-
oldValue = getValueByPath(oldSchemaData, originalDataPath);
|
|
4299
|
-
}
|
|
4300
|
-
// Set in store using namespaced data path (if namespace was used)
|
|
4301
|
-
if (oldValue !== undefined) {
|
|
4302
|
-
newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
|
|
4303
|
-
}
|
|
4304
|
-
}
|
|
4305
|
-
});
|
|
4306
|
-
// Also revert supporting documents if any
|
|
4307
|
-
if (hasSupportingDocuments) {
|
|
4308
|
-
// Use original section's supporting documents to get original data paths
|
|
4309
|
-
const originalSupportingDocuments = originalSection['section-supporting-documents'] || [];
|
|
4310
|
-
originalSupportingDocuments.forEach((doc, index) => {
|
|
4311
|
-
const widgetId = `supporting-doc-${sectionId}-${index}`;
|
|
4312
|
-
const originalDataPath = doc['document-data-path'];
|
|
4313
|
-
// If namespace was used, data path in store is namespaced
|
|
4314
|
-
const storeDataPath = namespace && originalDataPath
|
|
4315
|
-
? `${namespace}.${originalDataPath}`
|
|
4316
|
-
: originalDataPath;
|
|
4317
|
-
const oldValue = getValueByPath(oldSchemaData, originalDataPath);
|
|
4318
|
-
newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
|
|
4319
|
-
});
|
|
4320
|
-
}
|
|
4321
|
-
if (newStoreValues !== currentStoreValues) {
|
|
4322
|
-
dispatch(setValues(newStoreValues));
|
|
4323
|
-
}
|
|
4544
|
+
revertToOriginalValues();
|
|
4324
4545
|
setIsEditMode(false);
|
|
4325
4546
|
onEditModeChange?.(originalSectionId, false);
|
|
4326
4547
|
};
|
|
@@ -4371,20 +4592,27 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4371
4592
|
white-space: nowrap !important;
|
|
4372
4593
|
}
|
|
4373
4594
|
/* Readonly: prevent flex row from overflowing panel */
|
|
4374
|
-
|
|
4595
|
+
${readonlyValueRowRootsCss} {
|
|
4375
4596
|
min-width: 0 !important;
|
|
4376
4597
|
overflow: hidden !important;
|
|
4377
4598
|
}
|
|
4378
|
-
|
|
4599
|
+
${readonlyValueRowFlex1Css} {
|
|
4379
4600
|
min-width: 0 !important;
|
|
4380
4601
|
overflow: hidden !important;
|
|
4381
4602
|
}
|
|
4382
|
-
/* Readonly value
|
|
4383
|
-
|
|
4603
|
+
/* Readonly value: single-line ellipsis; full value via title on the value node */
|
|
4604
|
+
${readonlySingleLineValueTextCss} {
|
|
4384
4605
|
overflow: hidden;
|
|
4385
4606
|
text-overflow: ellipsis;
|
|
4386
4607
|
white-space: nowrap;
|
|
4387
4608
|
}
|
|
4609
|
+
/* Readonly textarea: break unbroken long tokens; title on pre keeps full text on hover */
|
|
4610
|
+
.${sectionClassId} .TextAreaDisplayWidget > .flex-1 > pre {
|
|
4611
|
+
min-width: 0;
|
|
4612
|
+
max-width: 100%;
|
|
4613
|
+
overflow-wrap: anywhere;
|
|
4614
|
+
word-break: break-word;
|
|
4615
|
+
}
|
|
4388
4616
|
|
|
4389
4617
|
/* Only apply fixed height when in edit mode */
|
|
4390
4618
|
.${sectionClassId}[data-edit-mode="true"] {
|
|
@@ -4508,6 +4736,10 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4508
4736
|
gap: 0.5rem;
|
|
4509
4737
|
}
|
|
4510
4738
|
|
|
4739
|
+
|
|
4740
|
+
|
|
4741
|
+
|
|
4742
|
+
|
|
4511
4743
|
/* IntakeForm accordion */
|
|
4512
4744
|
.${sectionClassId}.intake-form-accordion-item {
|
|
4513
4745
|
border-color: var(--owt-color-border-light, #E4E4E4);
|
|
@@ -4712,7 +4944,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4712
4944
|
}, children: crViewData.approvedDate }))] })] })] })), mode === 'RegistryView' && !hideEditButton && (jsxRuntimeExports.jsx("hr", { className: "w-full", style: { height: '1px', marginTop: !isEditMode ? '10px' : 0, marginBottom: '14px', border: 'none', backgroundColor: 'var(--owt-color-border, #C4C4C4)' } })), mode === 'RegistryView' && !isEditMode && !hideEditButton && (jsxRuntimeExports.jsx("div", { className: "flex justify-center items-center", style: { marginBottom: '20px' }, children: jsxRuntimeExports.jsxs("button", { onClick: handleEdit, className: "font-normal inline-flex items-center gap-2 bg-transparent border-0 p-0 cursor-pointer hover:opacity-80", style: {
|
|
4713
4945
|
fontFamily: 'Roboto, sans-serif',
|
|
4714
4946
|
fontSize: '16px',
|
|
4715
|
-
color: 'var(--owt-color-text-muted, #727474)'
|
|
4947
|
+
color: 'var(--owt-color-text-muted, #727474)',
|
|
4716
4948
|
}, children: [translate('common.editDetails') || 'Edit Details', jsxRuntimeExports.jsx("img", { src: img$8, alt: "right-arrow", className: "w-3.5 h-3.5 brightness-0 opacity-50" })] }) }))] })] })) })] }));
|
|
4717
4949
|
};
|
|
4718
4950
|
|
|
@@ -4886,6 +5118,9 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
|
|
|
4886
5118
|
}, []);
|
|
4887
5119
|
const safeSections = sections ?? [];
|
|
4888
5120
|
const prevSectionsLengthRef = useRef(safeSections.length);
|
|
5121
|
+
// Stable ref for namespace so formHandle useMemo doesn't depend on the (possibly inline) function identity
|
|
5122
|
+
const namespaceRef = useRef(namespace);
|
|
5123
|
+
namespaceRef.current = namespace;
|
|
4889
5124
|
// Track dirty (unsaved changes) per section for form handle validation
|
|
4890
5125
|
const sectionDirtyMapRef = useRef({});
|
|
4891
5126
|
const handleSectionDirtyChange = useCallback((sectionId, isDirty) => {
|
|
@@ -4931,11 +5166,14 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
|
|
|
4931
5166
|
// Form handle for onFormReady - allows host to validate and get all section data from its own Submit button
|
|
4932
5167
|
const formHandle = useMemo(() => {
|
|
4933
5168
|
const getValues = () => store.getState().widget?.values || {};
|
|
4934
|
-
const getNamespace = (section, index) =>
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
5169
|
+
const getNamespace = (section, index) => {
|
|
5170
|
+
const ns = namespaceRef.current;
|
|
5171
|
+
return ns
|
|
5172
|
+
? typeof ns === 'string'
|
|
5173
|
+
? ns
|
|
5174
|
+
: ns(section['section-id'], index)
|
|
5175
|
+
: undefined;
|
|
5176
|
+
};
|
|
4939
5177
|
const checkNoUnsavedChanges = () => {
|
|
4940
5178
|
const hasDirty = Object.values(sectionDirtyMapRef.current).some(Boolean);
|
|
4941
5179
|
if (hasDirty) {
|
|
@@ -4985,7 +5223,7 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
|
|
|
4985
5223
|
return results;
|
|
4986
5224
|
},
|
|
4987
5225
|
};
|
|
4988
|
-
}, [store, dispatch, safeSections
|
|
5226
|
+
}, [store, dispatch, safeSections]);
|
|
4989
5227
|
// Call onFormReady when form is ready (sections loaded)
|
|
4990
5228
|
useEffect(() => {
|
|
4991
5229
|
if (onFormReady && safeSections.length > 0) {
|
|
@@ -8269,10 +8507,10 @@ const DisplayWidget = ({ config }) => {
|
|
|
8269
8507
|
const label = translateConfig(widgetConfig['widget-label']);
|
|
8270
8508
|
// If no label, render as paragraph text
|
|
8271
8509
|
if (!label || label.trim() === '') {
|
|
8272
|
-
return (jsxRuntimeExports.jsx("div", { className: "mb-3 text-base text-gray-700", title: String(displayValue ?? ''), children: displayValue }));
|
|
8510
|
+
return (jsxRuntimeExports.jsx("div", { className: "DisplayFieldWidget mb-3 min-w-0 w-full overflow-hidden text-ellipsis whitespace-nowrap text-base text-gray-700", title: String(displayValue ?? ''), children: displayValue }));
|
|
8273
8511
|
}
|
|
8274
|
-
// With label, render as key-value pair
|
|
8275
|
-
return (jsxRuntimeExports.jsxs("div", { className: "mb-[10px] flex flex-col sm:flex-row sm:items-start", children: [jsxRuntimeExports.jsxs("div", { className: "text-base text-gray-600 font-medium md:min-w-[120px] sm:pr-4 mb-1 sm:mb-0", style: { fontFamily: 'Roboto, sans-serif' }, title: label, children: [label, ":"] }), jsxRuntimeExports.jsx("div", { className: "flex-1 text-base text-gray-900 font-medium", title: String(displayValue ?? ''), children: displayValue })] }));
|
|
8512
|
+
// With label, render as key-value pair (structure matches other readonly widgets for SectionRenderer ellipsis)
|
|
8513
|
+
return (jsxRuntimeExports.jsxs("div", { className: "mb-[10px] DisplayFieldWidget flex flex-col sm:flex-row sm:items-start", children: [jsxRuntimeExports.jsxs("div", { className: "text-base text-gray-600 font-medium md:min-w-[120px] sm:pr-4 mb-1 sm:mb-0", style: { fontFamily: 'Roboto, sans-serif' }, title: label, children: [label, ":"] }), jsxRuntimeExports.jsx("div", { className: "flex-1 min-w-0", children: jsxRuntimeExports.jsx("div", { className: "text-base text-gray-900 font-medium", title: String(displayValue ?? ''), children: displayValue }) })] }));
|
|
8276
8514
|
};
|
|
8277
8515
|
|
|
8278
8516
|
const TableCellSelect = ({ config, value, onValueChange }) => {
|
|
@@ -8286,7 +8524,7 @@ const TableCellSelect = ({ config, value, onValueChange }) => {
|
|
|
8286
8524
|
backgroundColor: isReadonly || loading ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
|
|
8287
8525
|
}, children: [jsxRuntimeExports.jsx("option", { value: "", children: translate('common.select') || 'Select' }), dataSourceOptions.map((option) => (jsxRuntimeExports.jsx("option", { value: option.value, children: option.label }, option.value)))] }));
|
|
8288
8526
|
};
|
|
8289
|
-
const SelectDisplayValue = ({ config, value }) => {
|
|
8527
|
+
const SelectDisplayValue$1 = ({ config, value }) => {
|
|
8290
8528
|
const { dataSourceOptions, loading } = useBaseWidget({ config });
|
|
8291
8529
|
if (loading) {
|
|
8292
8530
|
return jsxRuntimeExports.jsx("span", { children: "-" });
|
|
@@ -8780,7 +9018,7 @@ const TableWidget = ({ config }) => {
|
|
|
8780
9018
|
'widget-readonly': true,
|
|
8781
9019
|
'widget-data-path': undefined,
|
|
8782
9020
|
};
|
|
8783
|
-
return (jsxRuntimeExports.jsx("div", { className: "text-sm", style: getCellStyle(), children: jsxRuntimeExports.jsx(SelectDisplayValue, { config: cellConfig, value: cellValue }) }));
|
|
9021
|
+
return (jsxRuntimeExports.jsx("div", { className: "text-sm", style: getCellStyle(), children: jsxRuntimeExports.jsx(SelectDisplayValue$1, { config: cellConfig, value: cellValue }) }));
|
|
8784
9022
|
}
|
|
8785
9023
|
return (jsxRuntimeExports.jsx("div", { className: "text-sm", style: getCellStyle(), children: displayValue }));
|
|
8786
9024
|
}
|
|
@@ -8919,69 +9157,288 @@ const TableWidget = ({ config }) => {
|
|
|
8919
9157
|
}, children: translate('common.cancel') || 'Cancel' })] }) })] }))] })] }) }), touched && error.length > 0 && (jsxRuntimeExports.jsx("p", { className: "text-sm mt-1", style: { color: 'var(--owt-widget-error-color, #B91C1C)' }, children: error[0] }))] })] }));
|
|
8920
9158
|
};
|
|
8921
9159
|
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
const {
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
}
|
|
8983
|
-
|
|
8984
|
-
|
|
9160
|
+
// Display select value label in view mode
|
|
9161
|
+
const SelectDisplayValue = ({ config, value }) => {
|
|
9162
|
+
const { dataSourceOptions, loading } = useBaseWidget({ config });
|
|
9163
|
+
if (loading)
|
|
9164
|
+
return jsxRuntimeExports.jsx("span", { children: "-" });
|
|
9165
|
+
if (value === null || value === undefined || value === '')
|
|
9166
|
+
return jsxRuntimeExports.jsx("span", { children: "-" });
|
|
9167
|
+
const selectedOption = dataSourceOptions.find((option) => option.value === value);
|
|
9168
|
+
return jsxRuntimeExports.jsx("span", { children: selectedOption ? selectedOption.label : String(value) });
|
|
9169
|
+
};
|
|
9170
|
+
/**
|
|
9171
|
+
* Dialog table widget:
|
|
9172
|
+
* - Table displays a subset of columns (n out of x)
|
|
9173
|
+
* - Add/Edit happens in a modal dialog that shows ALL columns as a form
|
|
9174
|
+
*
|
|
9175
|
+
* Usage in schema:
|
|
9176
|
+
* {
|
|
9177
|
+
* "widget": "dialog-table",
|
|
9178
|
+
* "widget-type": "table",
|
|
9179
|
+
* "widget-label": "Household Members",
|
|
9180
|
+
* "widget-id": "householdMembers",
|
|
9181
|
+
* "widget-data-path": "household.members",
|
|
9182
|
+
* "widget-data-columns": [ ...all columns... ],
|
|
9183
|
+
* "widget-data-visible-columns": ["firstName", "lastName", "dob"], // optional override; default = by column flag
|
|
9184
|
+
* // Per-column control (recommended): set "column-visible-in-table": false to hide it in the table
|
|
9185
|
+
* "widget-data-operations": { "add": true, "edit": true, "remove": true }
|
|
9186
|
+
* }
|
|
9187
|
+
*/
|
|
9188
|
+
const DialogTableWidget = ({ config }) => {
|
|
9189
|
+
const { value, error, touched, isEnabled, onChange, config: widgetConfig } = useBaseWidget({ config });
|
|
9190
|
+
const { translate, translateConfig } = useWidgetTranslation();
|
|
9191
|
+
const rows = Array.isArray(value) ? value : [];
|
|
9192
|
+
const columns = widgetConfig['widget-data-columns'] || [];
|
|
9193
|
+
const operations = widgetConfig['widget-data-operations'] || {};
|
|
9194
|
+
const isReadonly = widgetConfig['widget-readonly'] || false;
|
|
9195
|
+
const visibleColumnKeys = widgetConfig['widget-data-visible-columns'];
|
|
9196
|
+
const visibleColumns = useMemo(() => {
|
|
9197
|
+
// 1) If explicit list provided, it wins
|
|
9198
|
+
if (Array.isArray(visibleColumnKeys) && visibleColumnKeys.length > 0) {
|
|
9199
|
+
const keySet = new Set(visibleColumnKeys);
|
|
9200
|
+
return columns.filter((c) => keySet.has(c['column-key']));
|
|
9201
|
+
}
|
|
9202
|
+
// 2) Otherwise decide per column (default = visible)
|
|
9203
|
+
return columns.filter((c) => c['column-visible-in-table'] !== false);
|
|
9204
|
+
}, [columns, visibleColumnKeys]);
|
|
9205
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
9206
|
+
const [dialogMode, setDialogMode] = useState('add');
|
|
9207
|
+
const [activeRowIndex, setActiveRowIndex] = useState(null);
|
|
9208
|
+
const [formData, setFormData] = useState({});
|
|
9209
|
+
const addDialogTitle = translateConfig(widgetConfig['widget-data-dialog-title-add']) ||
|
|
9210
|
+
translate('table.addRecordDialog') ||
|
|
9211
|
+
'Add record';
|
|
9212
|
+
const editDialogTitle = translateConfig(widgetConfig['widget-data-dialog-title-edit']) ||
|
|
9213
|
+
translate('table.editRecordDialog') ||
|
|
9214
|
+
'Edit record';
|
|
9215
|
+
const buildEmptyRow = useCallback(() => {
|
|
9216
|
+
const emptyRow = {};
|
|
9217
|
+
columns.forEach((col) => {
|
|
9218
|
+
const key = col['column-key'];
|
|
9219
|
+
emptyRow[key] = col['widget-data-default'] ?? '';
|
|
9220
|
+
});
|
|
9221
|
+
return emptyRow;
|
|
9222
|
+
}, [columns]);
|
|
9223
|
+
const openAddDialog = useCallback(() => {
|
|
9224
|
+
setDialogMode('add');
|
|
9225
|
+
setActiveRowIndex(null);
|
|
9226
|
+
setFormData(buildEmptyRow());
|
|
9227
|
+
setDialogOpen(true);
|
|
9228
|
+
}, [buildEmptyRow]);
|
|
9229
|
+
const openEditDialog = useCallback((rowIndex) => {
|
|
9230
|
+
const row = rows[rowIndex] || {};
|
|
9231
|
+
const nextFormData = buildEmptyRow();
|
|
9232
|
+
columns.forEach((col) => {
|
|
9233
|
+
const key = col['column-key'];
|
|
9234
|
+
if (row[key] !== undefined)
|
|
9235
|
+
nextFormData[key] = row[key];
|
|
9236
|
+
});
|
|
9237
|
+
setDialogMode('edit');
|
|
9238
|
+
setActiveRowIndex(rowIndex);
|
|
9239
|
+
setFormData(nextFormData);
|
|
9240
|
+
setDialogOpen(true);
|
|
9241
|
+
}, [rows, columns, buildEmptyRow]);
|
|
9242
|
+
const closeDialog = useCallback(() => {
|
|
9243
|
+
setDialogOpen(false);
|
|
9244
|
+
setActiveRowIndex(null);
|
|
9245
|
+
setFormData({});
|
|
9246
|
+
}, []);
|
|
9247
|
+
const updateField = useCallback((columnKey, newValue) => {
|
|
9248
|
+
setFormData((prev) => ({ ...prev, [columnKey]: newValue }));
|
|
9249
|
+
}, []);
|
|
9250
|
+
const saveDialog = useCallback(() => {
|
|
9251
|
+
if (dialogMode === 'add') {
|
|
9252
|
+
const savedRow = { ...formData, edit_action: 'ADD' };
|
|
9253
|
+
onChange([...rows, savedRow]);
|
|
9254
|
+
closeDialog();
|
|
9255
|
+
return;
|
|
9256
|
+
}
|
|
9257
|
+
if (dialogMode === 'edit' && activeRowIndex !== null) {
|
|
9258
|
+
const newRows = [...rows];
|
|
9259
|
+
const currentRow = newRows[activeRowIndex] || {};
|
|
9260
|
+
const wasDeleted = currentRow.edit_action === 'DELETE';
|
|
9261
|
+
const editAction = wasDeleted ? 'UPDATE' : (currentRow.edit_action ?? 'UPDATE');
|
|
9262
|
+
newRows[activeRowIndex] = { ...currentRow, ...formData, edit_action: editAction };
|
|
9263
|
+
onChange(newRows);
|
|
9264
|
+
closeDialog();
|
|
9265
|
+
}
|
|
9266
|
+
}, [dialogMode, formData, onChange, rows, closeDialog, activeRowIndex]);
|
|
9267
|
+
const deleteRow = useCallback((rowIndex) => {
|
|
9268
|
+
const newRows = rows.filter((_, i) => i !== rowIndex);
|
|
9269
|
+
onChange(newRows);
|
|
9270
|
+
}, [rows, onChange]);
|
|
9271
|
+
const getDisplayValue = useCallback((rowIndex, column) => {
|
|
9272
|
+
const key = column['column-key'];
|
|
9273
|
+
const cellValue = rows[rowIndex]?.[key];
|
|
9274
|
+
const widgetType = column.widget || 'text';
|
|
9275
|
+
if (cellValue === null || cellValue === undefined || cellValue === '')
|
|
9276
|
+
return '-';
|
|
9277
|
+
if (widgetType === 'select')
|
|
9278
|
+
return null; // handled by SelectDisplayValue
|
|
9279
|
+
if (column['widget-data-format'])
|
|
9280
|
+
return formatValue(cellValue, column['widget-data-format'], column.widget);
|
|
9281
|
+
return String(cellValue);
|
|
9282
|
+
}, [rows]);
|
|
9283
|
+
const tableWidgetId = `dialog-table-widget-${widgetConfig['widget-id']}`;
|
|
9284
|
+
const columnSpan = widgetConfig['widget-column-span'] || 2;
|
|
9285
|
+
const minWidth = columnSpan * 200;
|
|
9286
|
+
return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("style", { children: `
|
|
9287
|
+
.${tableWidgetId} {
|
|
9288
|
+
width: 100%;
|
|
9289
|
+
min-width: ${minWidth}px;
|
|
9290
|
+
}
|
|
9291
|
+
|
|
9292
|
+
.widget-container[data-widget-id="${widgetConfig['widget-id']}"] {
|
|
9293
|
+
min-width: ${minWidth}px;
|
|
9294
|
+
width: 100%;
|
|
9295
|
+
flex: none;
|
|
9296
|
+
}
|
|
9297
|
+
|
|
9298
|
+
.panel-horizontal .widget-container[data-widget-id="${widgetConfig['widget-id']}"],
|
|
9299
|
+
[data-panel-orientation="horizontal"] .widget-container[data-widget-id="${widgetConfig['widget-id']}"] {
|
|
9300
|
+
grid-column: span ${columnSpan};
|
|
9301
|
+
}
|
|
9302
|
+
` }), 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: {
|
|
9303
|
+
borderRadius: 'var(--owt-btn-border-radius, 10px)',
|
|
9304
|
+
border: '1px solid var(--owt-btn-primary-border, #F07B1A)',
|
|
9305
|
+
backgroundColor: 'var(--owt-color-primary, #F5BB1A)',
|
|
9306
|
+
color: 'var(--owt-color-bg, #FFFFFF)',
|
|
9307
|
+
}, children: translate('table.addRecord') || 'Add New Record' }) })), jsxRuntimeExports.jsx("div", { className: "overflow-x-auto border", style: {
|
|
9308
|
+
borderRadius: 'var(--owt-widget-table-border-radius, 15px)',
|
|
9309
|
+
borderColor: 'var(--owt-widget-table-border-color, #C4C4C4)',
|
|
9310
|
+
}, 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: {
|
|
9311
|
+
borderBottom: '1px solid var(--owt-widget-table-row-divider, #E4E4E4)',
|
|
9312
|
+
backgroundColor: row?.edit_action === 'DELETE'
|
|
9313
|
+
? 'var(--owt-widget-table-deleted-row-bg, #FEE2E2)'
|
|
9314
|
+
: undefined,
|
|
9315
|
+
}, children: [visibleColumns.map((col) => {
|
|
9316
|
+
const key = col['column-key'];
|
|
9317
|
+
const widgetType = col.widget || 'text';
|
|
9318
|
+
const displayValue = getDisplayValue(rowIndex, col);
|
|
9319
|
+
if (widgetType === 'select' && displayValue === null) {
|
|
9320
|
+
const displayConfig = {
|
|
9321
|
+
...col,
|
|
9322
|
+
'widget-id': `${widgetConfig['widget-id']}-view-row-${rowIndex}-col-${key}`,
|
|
9323
|
+
'widget-label': '',
|
|
9324
|
+
'widget-readonly': true,
|
|
9325
|
+
'widget-data-path': undefined,
|
|
9326
|
+
};
|
|
9327
|
+
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));
|
|
9328
|
+
}
|
|
9329
|
+
return (jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", children: jsxRuntimeExports.jsx("div", { className: "text-sm", children: displayValue }) }, key));
|
|
9330
|
+
}), ((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: {
|
|
9331
|
+
borderRadius: 'var(--owt-btn-border-radius, 10px)',
|
|
9332
|
+
color: 'var(--owt-color-primary-dark, #F07B1A)',
|
|
9333
|
+
backgroundColor: 'transparent',
|
|
9334
|
+
border: 'none',
|
|
9335
|
+
}, 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: {
|
|
9336
|
+
borderRadius: 'var(--owt-btn-border-radius, 10px)',
|
|
9337
|
+
color: 'var(--owt-color-error, #B91C1C)',
|
|
9338
|
+
backgroundColor: 'transparent',
|
|
9339
|
+
border: 'none',
|
|
9340
|
+
}, 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: {
|
|
9341
|
+
maxWidth: '900px',
|
|
9342
|
+
backgroundColor: 'var(--owt-color-bg, #FFFFFF)',
|
|
9343
|
+
borderRadius: 'var(--owt-widget-card-border-radius, 20px)',
|
|
9344
|
+
}, 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: {
|
|
9345
|
+
border: 'none',
|
|
9346
|
+
background: 'transparent',
|
|
9347
|
+
color: 'var(--owt-color-text-muted, #727474)',
|
|
9348
|
+
cursor: 'pointer',
|
|
9349
|
+
fontSize: '20px',
|
|
9350
|
+
lineHeight: 1,
|
|
9351
|
+
}, "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) => {
|
|
9352
|
+
const key = col['column-key'];
|
|
9353
|
+
const widgetType = col.widget || 'text';
|
|
9354
|
+
const cellWidgetId = `${widgetConfig['widget-id']}-dialog-${dialogMode}-${key}`;
|
|
9355
|
+
const fieldConfig = {
|
|
9356
|
+
...col,
|
|
9357
|
+
widget: widgetType,
|
|
9358
|
+
'widget-type': col['widget-type'] || 'input',
|
|
9359
|
+
'widget-id': cellWidgetId,
|
|
9360
|
+
'widget-label': col['widget-label'],
|
|
9361
|
+
'widget-readonly': isReadonly || col['widget-readonly'] === true,
|
|
9362
|
+
'widget-data-path': undefined,
|
|
9363
|
+
'widget-data-default': formData[key] ?? col['widget-data-default'] ?? '',
|
|
9364
|
+
};
|
|
9365
|
+
return (jsxRuntimeExports.jsx("div", { className: "min-w-0", children: jsxRuntimeExports.jsx(WidgetRenderer, { config: fieldConfig, schemaData: { [cellWidgetId]: formData[key] ?? col['widget-data-default'] ?? '' }, onValueChange: (_widgetId, newValue) => updateField(key, newValue) }) }, key));
|
|
9366
|
+
}) }), 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: {
|
|
9367
|
+
borderRadius: 'var(--owt-btn-border-radius, 10px)',
|
|
9368
|
+
border: '1px solid var(--owt-btn-secondary-border, #C4C4C4)',
|
|
9369
|
+
backgroundColor: 'var(--owt-btn-secondary-bg, #FFFFFF)',
|
|
9370
|
+
color: 'var(--owt-btn-secondary-color, #011627)',
|
|
9371
|
+
}, 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: {
|
|
9372
|
+
borderRadius: 'var(--owt-btn-border-radius, 10px)',
|
|
9373
|
+
border: '1px solid var(--owt-btn-primary-border, #F07B1A)',
|
|
9374
|
+
backgroundColor: 'var(--owt-color-primary, #F5BB1A)',
|
|
9375
|
+
color: 'var(--owt-color-bg, #FFFFFF)',
|
|
9376
|
+
}, children: translate('common.save') || 'Save' })] })] }) }))] }));
|
|
9377
|
+
};
|
|
9378
|
+
|
|
9379
|
+
const ProfileWidget = ({ config }) => {
|
|
9380
|
+
const { value, config: widgetConfig, getFieldValue, } = useBaseWidget({ config });
|
|
9381
|
+
const { translateConfig } = useWidgetTranslation();
|
|
9382
|
+
// Get schemaData from context as fallback
|
|
9383
|
+
const { schemaData } = useWidgetContext();
|
|
9384
|
+
// Get values from Redux store
|
|
9385
|
+
const values = useSelector((state) => state.widget.values);
|
|
9386
|
+
// Support two approaches for data paths:
|
|
9387
|
+
// 1. Multi-path binding via widget-data-path (object) - RECOMMENDED (Approach 2)
|
|
9388
|
+
// 2. Individual path properties (widget-image-path, widget-name-path, widget-id-path) - Fallback
|
|
9389
|
+
let imageUrl = null;
|
|
9390
|
+
let displayName = '';
|
|
9391
|
+
let idValue = '';
|
|
9392
|
+
const dataPath = widgetConfig['widget-data-path'];
|
|
9393
|
+
const imagePath = widgetConfig['widget-image-path'];
|
|
9394
|
+
const namePath = widgetConfig['widget-name-path'];
|
|
9395
|
+
const idPath = widgetConfig['widget-id-path'];
|
|
9396
|
+
// Prioritize multi-path data binding (Approach 2 - Recommended)
|
|
9397
|
+
if (dataPath && typeof dataPath === 'object') {
|
|
9398
|
+
// Multi-path data binding - preferred approach (Approach 2)
|
|
9399
|
+
// Always fetch each path individually using getFieldValue for reliability
|
|
9400
|
+
// The values in the dataPath object are the actual data paths to fetch
|
|
9401
|
+
const imagePathValue = dataPath.image || dataPath.photo || dataPath.avatar;
|
|
9402
|
+
const namePathValue = dataPath.name || dataPath.displayName;
|
|
9403
|
+
const idPathValue = dataPath.id || dataPath.identifier;
|
|
9404
|
+
// Helper function to search for a path within all top-level objects
|
|
9405
|
+
const findValueInNestedObjects = (path, searchIn) => {
|
|
9406
|
+
if (!searchIn)
|
|
9407
|
+
return undefined;
|
|
9408
|
+
// First try direct path (in case it's at root level)
|
|
9409
|
+
let value = getValueByPath(searchIn, path);
|
|
9410
|
+
if (value !== undefined)
|
|
9411
|
+
return value;
|
|
9412
|
+
// If not found, search within each top-level object
|
|
9413
|
+
for (const [key, obj] of Object.entries(searchIn)) {
|
|
9414
|
+
if (obj && typeof obj === 'object') {
|
|
9415
|
+
value = getValueByPath(obj, path);
|
|
9416
|
+
if (value !== undefined) {
|
|
9417
|
+
return value;
|
|
9418
|
+
}
|
|
9419
|
+
}
|
|
9420
|
+
}
|
|
9421
|
+
return undefined;
|
|
9422
|
+
};
|
|
9423
|
+
// Try to get values from Redux store first, then fallback to schemaData
|
|
9424
|
+
if (imagePathValue) {
|
|
9425
|
+
let fetchedImage = findValueInNestedObjects(imagePathValue, values);
|
|
9426
|
+
// If not found in Redux, try schemaData
|
|
9427
|
+
if (fetchedImage === undefined && schemaData) {
|
|
9428
|
+
fetchedImage = findValueInNestedObjects(imagePathValue, schemaData);
|
|
9429
|
+
}
|
|
9430
|
+
imageUrl = fetchedImage || null;
|
|
9431
|
+
}
|
|
9432
|
+
if (namePathValue) {
|
|
9433
|
+
let fetchedName = findValueInNestedObjects(namePathValue, values);
|
|
9434
|
+
// If not found in Redux, try schemaData
|
|
9435
|
+
if (fetchedName === undefined && schemaData) {
|
|
9436
|
+
fetchedName = findValueInNestedObjects(namePathValue, schemaData);
|
|
9437
|
+
}
|
|
9438
|
+
displayName = fetchedName || '';
|
|
9439
|
+
}
|
|
9440
|
+
if (idPathValue) {
|
|
9441
|
+
let fetchedId = findValueInNestedObjects(idPathValue, values);
|
|
8985
9442
|
// If not found in Redux, try schemaData
|
|
8986
9443
|
if (fetchedId === undefined && schemaData) {
|
|
8987
9444
|
fetchedId = findValueInNestedObjects(idPathValue, schemaData);
|
|
@@ -9310,15 +9767,105 @@ const HeaderSectionWidget = ({ config }) => {
|
|
|
9310
9767
|
result = searchIn(schemaData);
|
|
9311
9768
|
return result;
|
|
9312
9769
|
}, [paths, values, schemaData]);
|
|
9313
|
-
const
|
|
9770
|
+
const imageVal = findValue('image');
|
|
9771
|
+
const imageUrlVal = findValue('imageUrl');
|
|
9772
|
+
const [previewUrl, setPreviewUrl] = useState(null);
|
|
9773
|
+
useEffect(() => {
|
|
9774
|
+
if (imageVal instanceof File) {
|
|
9775
|
+
const url = URL.createObjectURL(imageVal);
|
|
9776
|
+
setPreviewUrl(url);
|
|
9777
|
+
return () => URL.revokeObjectURL(url);
|
|
9778
|
+
}
|
|
9779
|
+
setPreviewUrl(null);
|
|
9780
|
+
}, [imageVal]);
|
|
9781
|
+
const displayImageUrl = previewUrl || (typeof imageUrlVal === 'string' && imageUrlVal ? imageUrlVal : null);
|
|
9314
9782
|
const displayName = findValue('name') || '';
|
|
9315
9783
|
const functionalId = findValue('functionalId') || '';
|
|
9316
9784
|
const statusValue = findValue('status') || '';
|
|
9317
9785
|
const statusReason = findValue('statusReason') || '';
|
|
9786
|
+
const completionScoreRaw = findValue('completionScore');
|
|
9787
|
+
const idealScoreRaw = findValue('idealScore');
|
|
9318
9788
|
const createdBy = findValue('createdBy') || '';
|
|
9319
9789
|
const createdAt = findValue('createdAt') || '';
|
|
9320
9790
|
const lastApprovedBy = findValue('lastApprovedBy') || '';
|
|
9321
9791
|
const lastApprovedAt = findValue('lastApprovedAt') || '';
|
|
9792
|
+
// ── Validation: status change requires reason ──────────────────
|
|
9793
|
+
// Behavior:
|
|
9794
|
+
// - When status changes away from its initial value, clear reason and require it.
|
|
9795
|
+
// - When status returns to initial value (or a parent "Cancel" restores it), restore initial reason.
|
|
9796
|
+
const initialStatusRef = useRef(null);
|
|
9797
|
+
const initialReasonRef = useRef(null);
|
|
9798
|
+
const prevStatusRef = useRef(null);
|
|
9799
|
+
const [showReasonRequired, setShowReasonRequired] = useState(false);
|
|
9800
|
+
useEffect(() => {
|
|
9801
|
+
// Capture initial status once when it becomes available.
|
|
9802
|
+
if (initialStatusRef.current === null) {
|
|
9803
|
+
const v = statusValue === undefined || statusValue === null ? '' : String(statusValue);
|
|
9804
|
+
initialStatusRef.current = v;
|
|
9805
|
+
}
|
|
9806
|
+
}, [statusValue]);
|
|
9807
|
+
useEffect(() => {
|
|
9808
|
+
// Capture initial reason once when it becomes available.
|
|
9809
|
+
if (initialReasonRef.current === null) {
|
|
9810
|
+
const v = statusReason === undefined || statusReason === null ? '' : String(statusReason);
|
|
9811
|
+
initialReasonRef.current = v;
|
|
9812
|
+
}
|
|
9813
|
+
}, [statusReason]);
|
|
9814
|
+
const isStatusChanged = useMemo(() => {
|
|
9815
|
+
const initial = initialStatusRef.current;
|
|
9816
|
+
if (initial === null)
|
|
9817
|
+
return false;
|
|
9818
|
+
return String(statusValue) !== initial;
|
|
9819
|
+
}, [statusValue]);
|
|
9820
|
+
const isReasonMissing = useMemo(() => {
|
|
9821
|
+
if (!isStatusChanged)
|
|
9822
|
+
return false;
|
|
9823
|
+
return String(statusReason || '').trim().length === 0;
|
|
9824
|
+
}, [isStatusChanged, statusReason]);
|
|
9825
|
+
useEffect(() => {
|
|
9826
|
+
// When status changes:
|
|
9827
|
+
// - If moved away from initial → clear reason.
|
|
9828
|
+
// - If returned to initial → restore initial reason.
|
|
9829
|
+
if (isReadonly)
|
|
9830
|
+
return;
|
|
9831
|
+
if (initialStatusRef.current === null)
|
|
9832
|
+
return;
|
|
9833
|
+
const currentStatus = String(statusValue || '');
|
|
9834
|
+
if (prevStatusRef.current === currentStatus)
|
|
9835
|
+
return;
|
|
9836
|
+
prevStatusRef.current = currentStatus;
|
|
9837
|
+
const initialStatus = initialStatusRef.current;
|
|
9838
|
+
const initialReason = initialReasonRef.current ?? '';
|
|
9839
|
+
if (currentStatus === initialStatus) {
|
|
9840
|
+
// Reverted / cancelled back to original
|
|
9841
|
+
if (String(statusReason || '') !== String(initialReason || '')) {
|
|
9842
|
+
updateFieldValue('statusReason', initialReason);
|
|
9843
|
+
}
|
|
9844
|
+
setShowReasonRequired(false);
|
|
9845
|
+
return;
|
|
9846
|
+
}
|
|
9847
|
+
// Status changed to a new value: clear reason (so user must re-enter)
|
|
9848
|
+
if (String(statusReason || '').trim().length > 0) {
|
|
9849
|
+
updateFieldValue('statusReason', '');
|
|
9850
|
+
}
|
|
9851
|
+
setShowReasonRequired(true);
|
|
9852
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
9853
|
+
}, [statusValue, isReadonly]);
|
|
9854
|
+
const score = useMemo(() => {
|
|
9855
|
+
const toNum = (v) => {
|
|
9856
|
+
if (v === null || v === undefined || String(v).trim() === '')
|
|
9857
|
+
return null;
|
|
9858
|
+
const n = typeof v === 'number' ? v : Number(String(v));
|
|
9859
|
+
return Number.isFinite(n) ? n : null;
|
|
9860
|
+
};
|
|
9861
|
+
const completion = toNum(completionScoreRaw);
|
|
9862
|
+
const ideal = toNum(idealScoreRaw);
|
|
9863
|
+
if (completion === null || ideal === null || ideal <= 0)
|
|
9864
|
+
return null;
|
|
9865
|
+
const ratio = completion / ideal;
|
|
9866
|
+
const percent = Math.max(0, Math.min(100, Math.round(ratio * 100)));
|
|
9867
|
+
return { completion, ideal, percent };
|
|
9868
|
+
}, [completionScoreRaw, idealScoreRaw]);
|
|
9322
9869
|
// ── Format options ────────────────────────────────────────────
|
|
9323
9870
|
const format = (widgetConfig['widget-data-format'] || {});
|
|
9324
9871
|
const imageSize = format.imageSize || 120;
|
|
@@ -9343,18 +9890,20 @@ const HeaderSectionWidget = ({ config }) => {
|
|
|
9343
9890
|
return opt ? opt.label : String(statusValue);
|
|
9344
9891
|
}, [statusValue, statusOptions]);
|
|
9345
9892
|
const statusColor = statusColors[String(statusValue).toLowerCase()] || 'var(--owt-color-text-muted, #6B7280)';
|
|
9893
|
+
// ── Image edit helpers ───────────────────────────────────────
|
|
9894
|
+
const fileInputRef = useRef(null);
|
|
9895
|
+
const handleImageUpload = useCallback((e) => {
|
|
9896
|
+
const file = e.target.files?.[0];
|
|
9897
|
+
if (!file)
|
|
9898
|
+
return;
|
|
9899
|
+
updateFieldValue('image', file);
|
|
9900
|
+
e.target.value = '';
|
|
9901
|
+
}, [updateFieldValue]);
|
|
9902
|
+
const handleImageDelete = useCallback(() => {
|
|
9903
|
+
updateFieldValue('image', '');
|
|
9904
|
+
}, [updateFieldValue]);
|
|
9346
9905
|
// ── Scoped class for CSS isolation ────────────────────────────
|
|
9347
9906
|
const cls = `header-section-widget-${widgetConfig['widget-id']}`;
|
|
9348
|
-
// ── Indicator dot component ───────────────────────────────────
|
|
9349
|
-
const Dot = ({ color }) => (jsxRuntimeExports.jsx("span", { style: {
|
|
9350
|
-
display: 'inline-block',
|
|
9351
|
-
width: 8,
|
|
9352
|
-
height: 8,
|
|
9353
|
-
borderRadius: '50%',
|
|
9354
|
-
backgroundColor: color,
|
|
9355
|
-
flexShrink: 0,
|
|
9356
|
-
marginTop: 6,
|
|
9357
|
-
} }));
|
|
9358
9907
|
// ── RENDER ────────────────────────────────────────────────────
|
|
9359
9908
|
return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("style", { children: `
|
|
9360
9909
|
.${cls} {
|
|
@@ -9383,6 +9932,58 @@ const HeaderSectionWidget = ({ config }) => {
|
|
|
9383
9932
|
min-width: 220px;
|
|
9384
9933
|
}
|
|
9385
9934
|
|
|
9935
|
+
.${cls} .hdr-right-top {
|
|
9936
|
+
display: flex;
|
|
9937
|
+
align-items: flex-start;
|
|
9938
|
+
justify-content: space-between;
|
|
9939
|
+
gap: 14px;
|
|
9940
|
+
width: 100%;
|
|
9941
|
+
}
|
|
9942
|
+
|
|
9943
|
+
.${cls} .hdr-meta-col {
|
|
9944
|
+
display: flex;
|
|
9945
|
+
flex-direction: column;
|
|
9946
|
+
gap: 0.5rem;
|
|
9947
|
+
flex: 1 1 auto;
|
|
9948
|
+
min-width: 0;
|
|
9949
|
+
}
|
|
9950
|
+
|
|
9951
|
+
.${cls} .hdr-score-ring {
|
|
9952
|
+
--ring-size: 54px;
|
|
9953
|
+
--ring-thickness: 7px;
|
|
9954
|
+
--ring-color: var(--owt-color-primary-dark, #F07B1A);
|
|
9955
|
+
--ring-track: rgba(2, 6, 23, 0.10);
|
|
9956
|
+
width: var(--ring-size);
|
|
9957
|
+
height: var(--ring-size);
|
|
9958
|
+
border-radius: 50%;
|
|
9959
|
+
background: conic-gradient(
|
|
9960
|
+
var(--ring-color) calc(var(--pct) * 1%),
|
|
9961
|
+
var(--ring-track) 0
|
|
9962
|
+
);
|
|
9963
|
+
position: relative;
|
|
9964
|
+
flex: 0 0 auto;
|
|
9965
|
+
}
|
|
9966
|
+
|
|
9967
|
+
.${cls} .hdr-score-ring::before {
|
|
9968
|
+
content: "";
|
|
9969
|
+
position: absolute;
|
|
9970
|
+
inset: var(--ring-thickness);
|
|
9971
|
+
border-radius: 50%;
|
|
9972
|
+
background: var(--owt-color-bg, #FFFFFF);
|
|
9973
|
+
}
|
|
9974
|
+
|
|
9975
|
+
.${cls} .hdr-score-value {
|
|
9976
|
+
position: absolute;
|
|
9977
|
+
inset: 0;
|
|
9978
|
+
display: flex;
|
|
9979
|
+
align-items: center;
|
|
9980
|
+
justify-content: center;
|
|
9981
|
+
font-size: 20px;
|
|
9982
|
+
font-weight: 700;
|
|
9983
|
+
color: var(--owt-color-text, #011627);
|
|
9984
|
+
font-family: Roboto, sans-serif;
|
|
9985
|
+
}
|
|
9986
|
+
|
|
9386
9987
|
.${cls} .hdr-avatar {
|
|
9387
9988
|
width: ${imageSize}px;
|
|
9388
9989
|
height: ${imageSize}px;
|
|
@@ -9413,6 +10014,56 @@ const HeaderSectionWidget = ({ config }) => {
|
|
|
9413
10014
|
border-radius: 8px;
|
|
9414
10015
|
}
|
|
9415
10016
|
|
|
10017
|
+
.${cls} .hdr-avatar-wrapper {
|
|
10018
|
+
position: relative;
|
|
10019
|
+
width: ${imageSize}px;
|
|
10020
|
+
height: ${imageSize}px;
|
|
10021
|
+
flex-shrink: 0;
|
|
10022
|
+
}
|
|
10023
|
+
|
|
10024
|
+
.${cls} .hdr-avatar-overlay {
|
|
10025
|
+
position: absolute;
|
|
10026
|
+
inset: 0;
|
|
10027
|
+
border-radius: 8px;
|
|
10028
|
+
background: rgba(0, 0, 0, 0.55);
|
|
10029
|
+
display: flex;
|
|
10030
|
+
flex-direction: column;
|
|
10031
|
+
align-items: center;
|
|
10032
|
+
justify-content: center;
|
|
10033
|
+
gap: 6px;
|
|
10034
|
+
opacity: 0;
|
|
10035
|
+
transition: opacity 0.2s;
|
|
10036
|
+
}
|
|
10037
|
+
|
|
10038
|
+
.${cls} .hdr-avatar-wrapper:hover .hdr-avatar-overlay {
|
|
10039
|
+
opacity: 1;
|
|
10040
|
+
}
|
|
10041
|
+
|
|
10042
|
+
.${cls} .hdr-avatar-action {
|
|
10043
|
+
display: flex;
|
|
10044
|
+
align-items: center;
|
|
10045
|
+
gap: 5px;
|
|
10046
|
+
padding: 5px 14px;
|
|
10047
|
+
border: none;
|
|
10048
|
+
border-radius: 4px;
|
|
10049
|
+
background: rgba(255, 255, 255, 0.92);
|
|
10050
|
+
color: #374151;
|
|
10051
|
+
font-size: 0.7rem;
|
|
10052
|
+
font-weight: 500;
|
|
10053
|
+
cursor: pointer;
|
|
10054
|
+
font-family: Roboto, sans-serif;
|
|
10055
|
+
transition: background 0.15s;
|
|
10056
|
+
white-space: nowrap;
|
|
10057
|
+
}
|
|
10058
|
+
|
|
10059
|
+
.${cls} .hdr-avatar-action:hover {
|
|
10060
|
+
background: #fff;
|
|
10061
|
+
}
|
|
10062
|
+
|
|
10063
|
+
.${cls} .hdr-avatar-action--delete {
|
|
10064
|
+
color: #DC2626;
|
|
10065
|
+
}
|
|
10066
|
+
|
|
9416
10067
|
.${cls} .hdr-info {
|
|
9417
10068
|
display: flex;
|
|
9418
10069
|
flex-direction: column;
|
|
@@ -9509,6 +10160,19 @@ const HeaderSectionWidget = ({ config }) => {
|
|
|
9509
10160
|
box-shadow: 0 0 0 2px rgba(237, 124, 34, 0.15);
|
|
9510
10161
|
}
|
|
9511
10162
|
|
|
10163
|
+
.${cls} .hdr-input--error {
|
|
10164
|
+
border-color: var(--owt-color-danger, #DC2626);
|
|
10165
|
+
box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.12);
|
|
10166
|
+
}
|
|
10167
|
+
|
|
10168
|
+
.${cls} .hdr-error-text {
|
|
10169
|
+
margin-left: calc(0px);
|
|
10170
|
+
color: var(--owt-color-danger, #DC2626);
|
|
10171
|
+
font-size: 0.75rem;
|
|
10172
|
+
line-height: 1.2;
|
|
10173
|
+
font-weight: 500;
|
|
10174
|
+
}
|
|
10175
|
+
|
|
9512
10176
|
@media (max-width: 768px) {
|
|
9513
10177
|
.${cls} {
|
|
9514
10178
|
flex-direction: column;
|
|
@@ -9517,13 +10181,21 @@ const HeaderSectionWidget = ({ config }) => {
|
|
|
9517
10181
|
min-width: 0;
|
|
9518
10182
|
}
|
|
9519
10183
|
}
|
|
9520
|
-
` }), jsxRuntimeExports.jsxs("div", { className: cls, children: [jsxRuntimeExports.jsxs("div", { className: "hdr-left", children: [jsxRuntimeExports.jsxs("div", { children: [
|
|
10184
|
+
` }), jsxRuntimeExports.jsxs("div", { className: cls, children: [jsxRuntimeExports.jsxs("div", { className: "hdr-left", children: [jsxRuntimeExports.jsxs("div", { className: "hdr-avatar-wrapper", children: [displayImageUrl ? (jsxRuntimeExports.jsx("img", { src: displayImageUrl, alt: displayName || 'Profile', className: "hdr-avatar", onError: (e) => {
|
|
9521
10185
|
e.target.style.display = 'none';
|
|
9522
10186
|
const placeholder = e.target
|
|
9523
10187
|
.parentElement?.querySelector('.hdr-avatar-placeholder');
|
|
9524
10188
|
if (placeholder)
|
|
9525
10189
|
placeholder.style.display = 'flex';
|
|
9526
|
-
} })) : null, jsxRuntimeExports.jsx("div", { className: "hdr-avatar-placeholder", style: { display:
|
|
10190
|
+
} })) : 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: () => {
|
|
10191
|
+
if (isReasonMissing)
|
|
10192
|
+
setShowReasonRequired(true);
|
|
10193
|
+
}, onChange: (e) => {
|
|
10194
|
+
updateFieldValue('statusReason', e.target.value);
|
|
10195
|
+
if (showReasonRequired && String(e.target.value || '').trim().length > 0) {
|
|
10196
|
+
setShowReasonRequired(false);
|
|
10197
|
+
}
|
|
10198
|
+
} }), !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.completion} of ${score.ideal} (${score.percent}%)`, title: `${score.completion} / ${score.ideal} (${score.percent}%)`, children: jsxRuntimeExports.jsx("div", { className: "hdr-score-value", children: String(score.completion) }) })) : null] }) })] })] }));
|
|
9527
10199
|
};
|
|
9528
10200
|
|
|
9529
10201
|
function getValueByPathOrKey(obj, path) {
|
|
@@ -9533,7 +10205,7 @@ function getValueByPathOrKey(obj, path) {
|
|
|
9533
10205
|
return obj[path];
|
|
9534
10206
|
return getValueByPath(obj, path);
|
|
9535
10207
|
}
|
|
9536
|
-
function tryFormatDateTime(value) {
|
|
10208
|
+
function tryFormatDateTime$1(value) {
|
|
9537
10209
|
if (typeof value !== 'string' || !value)
|
|
9538
10210
|
return value ? String(value) : '-';
|
|
9539
10211
|
const d = new Date(value);
|
|
@@ -9727,12 +10399,535 @@ const ScoresDisplayWidget = ({ config, schemaData: propSchemaData, }) => {
|
|
|
9727
10399
|
String(s.computed_score) !== ''
|
|
9728
10400
|
? String(s.computed_score)
|
|
9729
10401
|
: '-';
|
|
9730
|
-
const computedAt = tryFormatDateTime(s?.computed_at);
|
|
10402
|
+
const computedAt = tryFormatDateTime$1(s?.computed_at);
|
|
9731
10403
|
const key = `${scoreType}-${String(s?.computed_at || '')}-${idx}`;
|
|
9732
10404
|
return (jsxRuntimeExports.jsxs("div", { className: "scores-card", "aria-live": idx === 0 ? 'polite' : undefined, children: [jsxRuntimeExports.jsx("div", { className: "scores-type", children: scoreType }), jsxRuntimeExports.jsx("div", { className: "scores-value", children: scoreValue }), jsxRuntimeExports.jsx("hr", { className: "scores-separator" }), jsxRuntimeExports.jsx("div", { className: "scores-meta", children: jsxRuntimeExports.jsxs("div", { className: "scores-meta-line", children: ["Computed at: ", jsxRuntimeExports.jsx("strong", { children: computedAt })] }) })] }, key));
|
|
9733
10405
|
}) })) })] }));
|
|
9734
10406
|
};
|
|
9735
10407
|
|
|
10408
|
+
function tryFormatDateTime(value) {
|
|
10409
|
+
if (typeof value !== 'string' || !value)
|
|
10410
|
+
return value ? String(value) : '-';
|
|
10411
|
+
const d = new Date(value);
|
|
10412
|
+
if (Number.isNaN(d.getTime()))
|
|
10413
|
+
return value;
|
|
10414
|
+
try {
|
|
10415
|
+
return d.toLocaleString(undefined, {
|
|
10416
|
+
year: 'numeric',
|
|
10417
|
+
month: 'short',
|
|
10418
|
+
day: '2-digit',
|
|
10419
|
+
hour: '2-digit',
|
|
10420
|
+
minute: '2-digit',
|
|
10421
|
+
});
|
|
10422
|
+
}
|
|
10423
|
+
catch {
|
|
10424
|
+
return value;
|
|
10425
|
+
}
|
|
10426
|
+
}
|
|
10427
|
+
function tryFormatDate(value) {
|
|
10428
|
+
if (typeof value !== 'string' || !value)
|
|
10429
|
+
return value ? String(value) : '-';
|
|
10430
|
+
const d = new Date(value);
|
|
10431
|
+
if (Number.isNaN(d.getTime()))
|
|
10432
|
+
return value;
|
|
10433
|
+
try {
|
|
10434
|
+
return d.toLocaleDateString(undefined, {
|
|
10435
|
+
year: 'numeric',
|
|
10436
|
+
month: 'short',
|
|
10437
|
+
day: '2-digit',
|
|
10438
|
+
});
|
|
10439
|
+
}
|
|
10440
|
+
catch {
|
|
10441
|
+
return value;
|
|
10442
|
+
}
|
|
10443
|
+
}
|
|
10444
|
+
function displayText(value) {
|
|
10445
|
+
if (value === null || value === undefined || String(value).trim() === '')
|
|
10446
|
+
return '-';
|
|
10447
|
+
return String(value);
|
|
10448
|
+
}
|
|
10449
|
+
function normalizeStatus(raw) {
|
|
10450
|
+
if (raw === null || raw === undefined || String(raw).trim() === '')
|
|
10451
|
+
return 'unknown';
|
|
10452
|
+
const v = String(raw).trim().toLowerCase();
|
|
10453
|
+
if (v === 'success' || v === 'succeeded' || v === 'ok')
|
|
10454
|
+
return 'success';
|
|
10455
|
+
if (v === 'failure' || v === 'failed' || v === 'error')
|
|
10456
|
+
return 'failure';
|
|
10457
|
+
if (v === 'not done' || v === 'not_done' || v === 'not-done' || v === 'pending')
|
|
10458
|
+
return 'not_done';
|
|
10459
|
+
return 'unknown';
|
|
10460
|
+
}
|
|
10461
|
+
/** Large enough for eSignet / OIDC login; clamped so it always fits the current screen. */
|
|
10462
|
+
function getCenteredPopupFeatures(width, height) {
|
|
10463
|
+
const dualScreenLeft = window.screenLeft ?? window.screenX ?? 0;
|
|
10464
|
+
const dualScreenTop = window.screenTop ?? window.screenY ?? 0;
|
|
10465
|
+
const viewportWidth = window.innerWidth || document.documentElement.clientWidth || (typeof screen !== 'undefined' ? screen.width : width);
|
|
10466
|
+
const viewportHeight = window.innerHeight || document.documentElement.clientHeight || (typeof screen !== 'undefined' ? screen.height : height);
|
|
10467
|
+
const maxW = Math.max(320, Math.floor(viewportWidth * 0.92));
|
|
10468
|
+
const maxH = Math.max(400, Math.floor(viewportHeight * 0.92));
|
|
10469
|
+
const w = Math.max(320, Math.min(width, maxW));
|
|
10470
|
+
const h = Math.max(400, Math.min(height, maxH));
|
|
10471
|
+
const left = Math.max(0, Math.floor(viewportWidth / 2 - w / 2 + dualScreenLeft));
|
|
10472
|
+
const top = Math.max(0, Math.floor(viewportHeight / 2 - h / 2 + dualScreenTop));
|
|
10473
|
+
return [
|
|
10474
|
+
'popup=yes',
|
|
10475
|
+
'noopener=yes',
|
|
10476
|
+
'noreferrer=yes',
|
|
10477
|
+
`width=${w}`,
|
|
10478
|
+
`height=${h}`,
|
|
10479
|
+
`left=${left}`,
|
|
10480
|
+
`top=${top}`,
|
|
10481
|
+
'scrollbars=yes',
|
|
10482
|
+
'resizable=yes',
|
|
10483
|
+
].join(',');
|
|
10484
|
+
}
|
|
10485
|
+
function pickAuthorizationUrl(resp, explicitKey) {
|
|
10486
|
+
if (!resp)
|
|
10487
|
+
return null;
|
|
10488
|
+
const tryKey = (k) => {
|
|
10489
|
+
const v = resp?.[k];
|
|
10490
|
+
if (typeof v === 'string' && v)
|
|
10491
|
+
return v;
|
|
10492
|
+
return null;
|
|
10493
|
+
};
|
|
10494
|
+
if (explicitKey) {
|
|
10495
|
+
const v = tryKey(explicitKey);
|
|
10496
|
+
if (v)
|
|
10497
|
+
return v;
|
|
10498
|
+
}
|
|
10499
|
+
return (tryKey('authentication_url') ||
|
|
10500
|
+
tryKey('authorization_url') ||
|
|
10501
|
+
tryKey('authorizationUrl') ||
|
|
10502
|
+
tryKey('auth_url') ||
|
|
10503
|
+
tryKey('authUrl') ||
|
|
10504
|
+
tryKey('url') ||
|
|
10505
|
+
null);
|
|
10506
|
+
}
|
|
10507
|
+
function resolveValueFromSources(path, values, schemaData) {
|
|
10508
|
+
if (!path)
|
|
10509
|
+
return undefined;
|
|
10510
|
+
const fromValues = getValueByPath(values, path);
|
|
10511
|
+
if (fromValues !== undefined)
|
|
10512
|
+
return fromValues;
|
|
10513
|
+
return getValueByPath(schemaData, path);
|
|
10514
|
+
}
|
|
10515
|
+
function unwrapPayload(response) {
|
|
10516
|
+
if (response && typeof response === 'object') {
|
|
10517
|
+
if (response.response_body?.response_payload !== undefined)
|
|
10518
|
+
return response.response_body.response_payload;
|
|
10519
|
+
if (response.response_payload !== undefined)
|
|
10520
|
+
return response.response_payload;
|
|
10521
|
+
}
|
|
10522
|
+
return response;
|
|
10523
|
+
}
|
|
10524
|
+
const IdAuthenticationWidget = ({ config, schemaData: propSchemaData }) => {
|
|
10525
|
+
const { dataSourceRequestHandler, schemaData: ctxSchemaData } = useWidgetContext();
|
|
10526
|
+
const values = useSelector((state) => state.widget.values);
|
|
10527
|
+
const schemaData = (propSchemaData || ctxSchemaData || {});
|
|
10528
|
+
const widgetId = config['widget-id'];
|
|
10529
|
+
const dataPath = config['widget-data-path'];
|
|
10530
|
+
const paths = useMemo(() => {
|
|
10531
|
+
if (!dataPath || typeof dataPath !== 'object')
|
|
10532
|
+
return {};
|
|
10533
|
+
return dataPath;
|
|
10534
|
+
}, [dataPath]);
|
|
10535
|
+
const authConfig = config['widget-auth-config'];
|
|
10536
|
+
const registerId = resolveValueFromSources(paths.registerId, values, schemaData);
|
|
10537
|
+
const internalRecordId = resolveValueFromSources(paths.internalRecordId, values, schemaData);
|
|
10538
|
+
const initiatedByStaffId = resolveValueFromSources(paths.initiatedByStaffId, values, schemaData);
|
|
10539
|
+
const providerId = authConfig?.providerId;
|
|
10540
|
+
const providerName = authConfig?.providerName;
|
|
10541
|
+
const foundationalId = resolveValueFromSources(paths.foundationalId, values, schemaData);
|
|
10542
|
+
const lastAuthenticatedOn = resolveValueFromSources(paths.lastAuthenticatedOn, values, schemaData);
|
|
10543
|
+
const lastAuthStatusRaw = resolveValueFromSources(paths.lastAuthenticationStatus, values, schemaData);
|
|
10544
|
+
const expiryDate = resolveValueFromSources(paths.expiryDate, values, schemaData);
|
|
10545
|
+
const psut = resolveValueFromSources(paths.authenticationToken, values, schemaData);
|
|
10546
|
+
const status = useMemo(() => normalizeStatus(lastAuthStatusRaw), [lastAuthStatusRaw]);
|
|
10547
|
+
/** URL from prefetch (or default); used when opening the OIDC / eSignet popup */
|
|
10548
|
+
const [resolvedAuthUrl, setResolvedAuthUrl] = useState(null);
|
|
10549
|
+
const [authActionLoading, setAuthActionLoading] = useState(false);
|
|
10550
|
+
const [authError, setAuthError] = useState(null);
|
|
10551
|
+
const popupRef = useRef(null);
|
|
10552
|
+
const pollTimerRef = useRef(null);
|
|
10553
|
+
const [overlayUrl, setOverlayUrl] = useState(null);
|
|
10554
|
+
const emitHostEvent = useCallback((detail) => {
|
|
10555
|
+
if (typeof window === 'undefined')
|
|
10556
|
+
return;
|
|
10557
|
+
window.dispatchEvent(new CustomEvent('openg2p:id-authentication', {
|
|
10558
|
+
detail: {
|
|
10559
|
+
widgetId,
|
|
10560
|
+
...detail,
|
|
10561
|
+
},
|
|
10562
|
+
}));
|
|
10563
|
+
}, [widgetId]);
|
|
10564
|
+
const cleanupPopup = useCallback(() => {
|
|
10565
|
+
if (pollTimerRef.current) {
|
|
10566
|
+
window.clearInterval(pollTimerRef.current);
|
|
10567
|
+
pollTimerRef.current = null;
|
|
10568
|
+
}
|
|
10569
|
+
popupRef.current = null;
|
|
10570
|
+
}, []);
|
|
10571
|
+
useEffect(() => {
|
|
10572
|
+
return () => {
|
|
10573
|
+
cleanupPopup();
|
|
10574
|
+
try {
|
|
10575
|
+
popupRef.current?.close?.();
|
|
10576
|
+
}
|
|
10577
|
+
catch {
|
|
10578
|
+
// ignore
|
|
10579
|
+
}
|
|
10580
|
+
};
|
|
10581
|
+
}, [cleanupPopup]);
|
|
10582
|
+
// Provider details are supplied by host; clear any previous resolved URL on provider change.
|
|
10583
|
+
useEffect(() => {
|
|
10584
|
+
setResolvedAuthUrl(null);
|
|
10585
|
+
}, [providerId, providerName]);
|
|
10586
|
+
const openAuthPopup = useCallback((authUrl) => {
|
|
10587
|
+
if (authConfig?.useIframeOverlay !== false) {
|
|
10588
|
+
setOverlayUrl(authUrl);
|
|
10589
|
+
emitHostEvent({ type: 'overlay_opened' });
|
|
10590
|
+
return;
|
|
10591
|
+
}
|
|
10592
|
+
const pw = authConfig?.popupWidth ?? 1024;
|
|
10593
|
+
const ph = authConfig?.popupHeight ?? 800;
|
|
10594
|
+
const features = getCenteredPopupFeatures(pw, ph);
|
|
10595
|
+
const popup = window.open(authUrl, `${widgetId}-oidc`, features);
|
|
10596
|
+
if (!popup) {
|
|
10597
|
+
setAuthError('Popup blocked. Please allow popups and try again.');
|
|
10598
|
+
return;
|
|
10599
|
+
}
|
|
10600
|
+
popupRef.current = popup;
|
|
10601
|
+
popup.focus?.();
|
|
10602
|
+
setAuthError(null);
|
|
10603
|
+
emitHostEvent({ type: 'popup_opened' });
|
|
10604
|
+
if (pollTimerRef.current) {
|
|
10605
|
+
window.clearInterval(pollTimerRef.current);
|
|
10606
|
+
pollTimerRef.current = null;
|
|
10607
|
+
}
|
|
10608
|
+
pollTimerRef.current = window.setInterval(() => {
|
|
10609
|
+
try {
|
|
10610
|
+
const closed = !popupRef.current || popupRef.current.closed;
|
|
10611
|
+
if (closed) {
|
|
10612
|
+
cleanupPopup();
|
|
10613
|
+
emitHostEvent({ type: 'popup_closed' });
|
|
10614
|
+
}
|
|
10615
|
+
}
|
|
10616
|
+
catch {
|
|
10617
|
+
// ignore
|
|
10618
|
+
}
|
|
10619
|
+
}, 500);
|
|
10620
|
+
}, [authConfig, cleanupPopup, emitHostEvent, widgetId]);
|
|
10621
|
+
const onAuthenticate = useCallback(async () => {
|
|
10622
|
+
setAuthError(null);
|
|
10623
|
+
if (!authConfig) {
|
|
10624
|
+
setAuthError('Missing widget-auth-config.');
|
|
10625
|
+
return;
|
|
10626
|
+
}
|
|
10627
|
+
const canCallAuthApi = Boolean(dataSourceRequestHandler && authConfig.service && authConfig.authenticateEndpoint);
|
|
10628
|
+
let url = resolvedAuthUrl;
|
|
10629
|
+
if (!url && canCallAuthApi) {
|
|
10630
|
+
setAuthActionLoading(true);
|
|
10631
|
+
try {
|
|
10632
|
+
const resp = await dataSourceRequestHandler(authConfig.service, authConfig.authenticateEndpoint, authConfig.authenticateMethod || 'POST', {
|
|
10633
|
+
register_id: registerId,
|
|
10634
|
+
internal_record_id: internalRecordId,
|
|
10635
|
+
provider_id: authConfig.providerId,
|
|
10636
|
+
initiated_by_staff_id: initiatedByStaffId,
|
|
10637
|
+
});
|
|
10638
|
+
const payload = unwrapPayload(resp);
|
|
10639
|
+
const authUrl = pickAuthorizationUrl(payload, authConfig.authorizationUrlKey);
|
|
10640
|
+
url = authUrl || null;
|
|
10641
|
+
if (authUrl)
|
|
10642
|
+
setResolvedAuthUrl(authUrl);
|
|
10643
|
+
}
|
|
10644
|
+
catch (e) {
|
|
10645
|
+
if (!url) {
|
|
10646
|
+
setAuthError(e?.message || 'Could not load provider URL.');
|
|
10647
|
+
return;
|
|
10648
|
+
}
|
|
10649
|
+
}
|
|
10650
|
+
finally {
|
|
10651
|
+
setAuthActionLoading(false);
|
|
10652
|
+
}
|
|
10653
|
+
}
|
|
10654
|
+
if (!url) {
|
|
10655
|
+
setAuthError('No authorization URL returned from authenticate_registrant.');
|
|
10656
|
+
return;
|
|
10657
|
+
}
|
|
10658
|
+
openAuthPopup(url);
|
|
10659
|
+
}, [
|
|
10660
|
+
authConfig,
|
|
10661
|
+
dataSourceRequestHandler,
|
|
10662
|
+
openAuthPopup,
|
|
10663
|
+
registerId,
|
|
10664
|
+
internalRecordId,
|
|
10665
|
+
initiatedByStaffId,
|
|
10666
|
+
resolvedAuthUrl,
|
|
10667
|
+
]);
|
|
10668
|
+
useEffect(() => {
|
|
10669
|
+
const successType = authConfig?.successMessageType || 'openg2p:oidc:success';
|
|
10670
|
+
const handler = (event) => {
|
|
10671
|
+
const data = event?.data;
|
|
10672
|
+
if (!data || typeof data !== 'object')
|
|
10673
|
+
return;
|
|
10674
|
+
if (data.type !== successType)
|
|
10675
|
+
return;
|
|
10676
|
+
if (data.widgetId && data.widgetId !== widgetId)
|
|
10677
|
+
return;
|
|
10678
|
+
emitHostEvent({ type: 'authenticated', payload: data });
|
|
10679
|
+
try {
|
|
10680
|
+
popupRef.current?.close?.();
|
|
10681
|
+
}
|
|
10682
|
+
catch {
|
|
10683
|
+
// ignore
|
|
10684
|
+
}
|
|
10685
|
+
cleanupPopup();
|
|
10686
|
+
if (authConfig?.reloadOnSuccess) {
|
|
10687
|
+
window.location.reload();
|
|
10688
|
+
}
|
|
10689
|
+
};
|
|
10690
|
+
window.addEventListener('message', handler);
|
|
10691
|
+
return () => window.removeEventListener('message', handler);
|
|
10692
|
+
}, [authConfig?.reloadOnSuccess, authConfig?.successMessageType, cleanupPopup, emitHostEvent, widgetId]);
|
|
10693
|
+
const cls = `id-auth-widget-${widgetId}`;
|
|
10694
|
+
const statusLabel = useMemo(() => {
|
|
10695
|
+
if (status === 'success')
|
|
10696
|
+
return 'Success';
|
|
10697
|
+
if (status === 'failure')
|
|
10698
|
+
return 'Failure';
|
|
10699
|
+
if (status === 'not_done')
|
|
10700
|
+
return 'Not done';
|
|
10701
|
+
return 'Unknown';
|
|
10702
|
+
}, [status]);
|
|
10703
|
+
const statusColor = useMemo(() => {
|
|
10704
|
+
if (status === 'success')
|
|
10705
|
+
return 'var(--owt-color-success, #16A34A)';
|
|
10706
|
+
if (status === 'failure')
|
|
10707
|
+
return 'var(--owt-color-danger, #DC2626)';
|
|
10708
|
+
if (status === 'not_done')
|
|
10709
|
+
return 'var(--owt-color-warning, #D97706)';
|
|
10710
|
+
return 'var(--owt-color-text-muted, #6B7280)';
|
|
10711
|
+
}, [status]);
|
|
10712
|
+
const buttonBusy = authActionLoading;
|
|
10713
|
+
const buttonDisabled = !authConfig || buttonBusy;
|
|
10714
|
+
return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("style", { children: `
|
|
10715
|
+
.${cls} {
|
|
10716
|
+
width: 100%;
|
|
10717
|
+
font-family: Roboto, sans-serif;
|
|
10718
|
+
}
|
|
10719
|
+
|
|
10720
|
+
/* Two-column field grid; primary action in a bottom band (matches section save/edit pattern). */
|
|
10721
|
+
.${cls} .auth-content {
|
|
10722
|
+
display: flex;
|
|
10723
|
+
flex-direction: column;
|
|
10724
|
+
gap: 0;
|
|
10725
|
+
min-width: 0;
|
|
10726
|
+
}
|
|
10727
|
+
|
|
10728
|
+
.${cls} .auth-grid {
|
|
10729
|
+
display: grid;
|
|
10730
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
10731
|
+
gap: 16px 24px;
|
|
10732
|
+
min-width: 0;
|
|
10733
|
+
}
|
|
10734
|
+
|
|
10735
|
+
/* Each field: label (left) + value (right), same as DisplayWidget readonly */
|
|
10736
|
+
.${cls} .auth-cell {
|
|
10737
|
+
display: flex;
|
|
10738
|
+
flex-direction: row;
|
|
10739
|
+
align-items: flex-start;
|
|
10740
|
+
gap: 12px 16px;
|
|
10741
|
+
min-width: 0;
|
|
10742
|
+
}
|
|
10743
|
+
|
|
10744
|
+
.${cls} .auth-cell.auth-cell--full {
|
|
10745
|
+
grid-column: 1 / -1;
|
|
10746
|
+
}
|
|
10747
|
+
|
|
10748
|
+
/* Action cell: no left label spacer, align button to column start */
|
|
10749
|
+
.${cls} .auth-cell.auth-cell--action .auth-label {
|
|
10750
|
+
display: none;
|
|
10751
|
+
}
|
|
10752
|
+
.${cls} .auth-cell.auth-cell--action .auth-value {
|
|
10753
|
+
flex: 1 1 auto;
|
|
10754
|
+
}
|
|
10755
|
+
|
|
10756
|
+
.${cls} .auth-label {
|
|
10757
|
+
flex: 0 0 auto;
|
|
10758
|
+
min-width: 200px;
|
|
10759
|
+
max-width: 40%;
|
|
10760
|
+
font-size: 16px;
|
|
10761
|
+
color: rgba(0, 0, 0, 0.6);
|
|
10762
|
+
font-weight: 500;
|
|
10763
|
+
line-height: 1.45;
|
|
10764
|
+
margin: 0;
|
|
10765
|
+
word-break: break-word;
|
|
10766
|
+
}
|
|
10767
|
+
|
|
10768
|
+
.${cls} .auth-value {
|
|
10769
|
+
flex: 1 1 auto;
|
|
10770
|
+
min-width: 0;
|
|
10771
|
+
font-size: 16px;
|
|
10772
|
+
color: var(--owt-color-text, #111827);
|
|
10773
|
+
font-weight: 500;
|
|
10774
|
+
line-height: 1.45;
|
|
10775
|
+
word-break: break-word;
|
|
10776
|
+
}
|
|
10777
|
+
|
|
10778
|
+
.${cls} .auth-value--foundational {
|
|
10779
|
+
font-size: 18px;
|
|
10780
|
+
font-weight: 700;
|
|
10781
|
+
color: var(--owt-color-primary-dark, #F07B1A);
|
|
10782
|
+
letter-spacing: 0.1px;
|
|
10783
|
+
}
|
|
10784
|
+
|
|
10785
|
+
/* Button is placed inside the grid (next to PSUT) */
|
|
10786
|
+
|
|
10787
|
+
.${cls} .auth-status {
|
|
10788
|
+
display: inline-flex;
|
|
10789
|
+
align-items: center;
|
|
10790
|
+
gap: 8px;
|
|
10791
|
+
width: fit-content;
|
|
10792
|
+
padding: 4px 10px;
|
|
10793
|
+
border-radius: 999px;
|
|
10794
|
+
background: rgba(2, 6, 23, 0.04);
|
|
10795
|
+
border: 1px solid rgba(2, 6, 23, 0.08);
|
|
10796
|
+
font-size: 13px;
|
|
10797
|
+
font-weight: 700;
|
|
10798
|
+
color: var(--owt-color-text, #011627);
|
|
10799
|
+
}
|
|
10800
|
+
|
|
10801
|
+
.${cls} .auth-dot {
|
|
10802
|
+
width: 8px;
|
|
10803
|
+
height: 8px;
|
|
10804
|
+
border-radius: 50%;
|
|
10805
|
+
background: ${statusColor};
|
|
10806
|
+
}
|
|
10807
|
+
|
|
10808
|
+
.${cls} .auth-token {
|
|
10809
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
10810
|
+
font-size: 14px;
|
|
10811
|
+
font-weight: 500;
|
|
10812
|
+
color: var(--owt-color-text, #011627);
|
|
10813
|
+
background: transparent;
|
|
10814
|
+
border: none;
|
|
10815
|
+
border-radius: 0;
|
|
10816
|
+
padding: 0;
|
|
10817
|
+
word-break: break-all;
|
|
10818
|
+
}
|
|
10819
|
+
|
|
10820
|
+
.${cls} .auth-button {
|
|
10821
|
+
/* Match SectionRegistryView Save CTA (SectionRenderer) */
|
|
10822
|
+
font-size: 14px;
|
|
10823
|
+
font-weight: 500;
|
|
10824
|
+
padding: 8px 24px;
|
|
10825
|
+
line-height: 1.5;
|
|
10826
|
+
border-radius: var(--owt-btn-border-radius, 10px);
|
|
10827
|
+
border: 1px solid rgb(237, 124, 34);
|
|
10828
|
+
background-color: rgb(237, 124, 34);
|
|
10829
|
+
color: var(--owt-color-bg, #FFFFFF);
|
|
10830
|
+
font-family: Roboto, sans-serif;
|
|
10831
|
+
cursor: pointer;
|
|
10832
|
+
transition: opacity 0.15s ease;
|
|
10833
|
+
}
|
|
10834
|
+
|
|
10835
|
+
.${cls} .auth-button:disabled {
|
|
10836
|
+
opacity: 0.5;
|
|
10837
|
+
cursor: not-allowed;
|
|
10838
|
+
}
|
|
10839
|
+
|
|
10840
|
+
.${cls} .auth-error {
|
|
10841
|
+
font-size: 12px;
|
|
10842
|
+
color: var(--owt-color-danger, #DC2626);
|
|
10843
|
+
font-weight: 700;
|
|
10844
|
+
line-height: 1.3;
|
|
10845
|
+
text-align: left;
|
|
10846
|
+
max-width: 100%;
|
|
10847
|
+
}
|
|
10848
|
+
|
|
10849
|
+
.${cls} .overlay-backdrop {
|
|
10850
|
+
position: fixed;
|
|
10851
|
+
inset: 0;
|
|
10852
|
+
background: rgba(17, 24, 39, 0.55);
|
|
10853
|
+
z-index: 9999;
|
|
10854
|
+
display: flex;
|
|
10855
|
+
align-items: center;
|
|
10856
|
+
justify-content: center;
|
|
10857
|
+
padding: 24px;
|
|
10858
|
+
}
|
|
10859
|
+
|
|
10860
|
+
.${cls} .overlay-panel {
|
|
10861
|
+
width: min(1100px, 92vw);
|
|
10862
|
+
height: min(820px, 92vh);
|
|
10863
|
+
background: var(--owt-color-bg, #FFFFFF);
|
|
10864
|
+
border-radius: 12px;
|
|
10865
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.25);
|
|
10866
|
+
overflow: hidden;
|
|
10867
|
+
display: flex;
|
|
10868
|
+
flex-direction: column;
|
|
10869
|
+
}
|
|
10870
|
+
|
|
10871
|
+
.${cls} .overlay-header {
|
|
10872
|
+
display: flex;
|
|
10873
|
+
align-items: center;
|
|
10874
|
+
justify-content: space-between;
|
|
10875
|
+
padding: 10px 14px;
|
|
10876
|
+
border-bottom: 1px solid var(--owt-color-border-light, #E4E4E4);
|
|
10877
|
+
font-family: Roboto, sans-serif;
|
|
10878
|
+
}
|
|
10879
|
+
|
|
10880
|
+
.${cls} .overlay-title {
|
|
10881
|
+
font-size: 14px;
|
|
10882
|
+
color: var(--owt-color-text, #011627);
|
|
10883
|
+
font-weight: 600;
|
|
10884
|
+
min-width: 0;
|
|
10885
|
+
overflow: hidden;
|
|
10886
|
+
text-overflow: ellipsis;
|
|
10887
|
+
white-space: nowrap;
|
|
10888
|
+
}
|
|
10889
|
+
|
|
10890
|
+
.${cls} .overlay-close {
|
|
10891
|
+
border: 1px solid var(--owt-btn-secondary-border, #C4C4C4);
|
|
10892
|
+
background: var(--owt-btn-secondary-bg, #FFFFFF);
|
|
10893
|
+
color: var(--owt-btn-secondary-color, #011627);
|
|
10894
|
+
border-radius: var(--owt-btn-border-radius, 10px);
|
|
10895
|
+
padding: 6px 10px;
|
|
10896
|
+
font-size: 12px;
|
|
10897
|
+
cursor: pointer;
|
|
10898
|
+
}
|
|
10899
|
+
|
|
10900
|
+
.${cls} .overlay-iframe {
|
|
10901
|
+
flex: 1 1 auto;
|
|
10902
|
+
width: 100%;
|
|
10903
|
+
border: none;
|
|
10904
|
+
}
|
|
10905
|
+
|
|
10906
|
+
@media (max-width: 640px) {
|
|
10907
|
+
.${cls} .auth-grid {
|
|
10908
|
+
grid-template-columns: 1fr;
|
|
10909
|
+
}
|
|
10910
|
+
.${cls} .auth-cell {
|
|
10911
|
+
flex-direction: column;
|
|
10912
|
+
align-items: stretch;
|
|
10913
|
+
gap: 4px 0;
|
|
10914
|
+
}
|
|
10915
|
+
.${cls} .auth-label {
|
|
10916
|
+
min-width: 0;
|
|
10917
|
+
max-width: none;
|
|
10918
|
+
}
|
|
10919
|
+
}
|
|
10920
|
+
` }), jsxRuntimeExports.jsxs("div", { className: cls, children: [overlayUrl ? (jsxRuntimeExports.jsx("div", { className: "overlay-backdrop", role: "dialog", "aria-modal": "true", "aria-label": "Authentication", onClick: (e) => {
|
|
10921
|
+
if (e.target === e.currentTarget) {
|
|
10922
|
+
setOverlayUrl(null);
|
|
10923
|
+
emitHostEvent({ type: 'overlay_closed' });
|
|
10924
|
+
}
|
|
10925
|
+
}, 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: () => {
|
|
10926
|
+
setOverlayUrl(null);
|
|
10927
|
+
emitHostEvent({ type: 'overlay_closed' });
|
|
10928
|
+
}, 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] })] })] }) })] })] }));
|
|
10929
|
+
};
|
|
10930
|
+
|
|
9736
10931
|
/**
|
|
9737
10932
|
* Register all default/generic widgets
|
|
9738
10933
|
* This is called automatically when the package is imported
|
|
@@ -9762,6 +10957,8 @@ const registerDefaultWidgets = () => {
|
|
|
9762
10957
|
widgetRegistry.register({ widget: 'simple-table', component: SimpleTableWidget });
|
|
9763
10958
|
// Table widget with record-level editing
|
|
9764
10959
|
widgetRegistry.register({ widget: 'table', component: TableWidget });
|
|
10960
|
+
// Table widget with add/edit popup dialog
|
|
10961
|
+
widgetRegistry.register({ widget: 'dialog-table', component: DialogTableWidget });
|
|
9765
10962
|
// Group widgets
|
|
9766
10963
|
widgetRegistry.register({ widget: 'array-widget', component: ArrayWidget });
|
|
9767
10964
|
widgetRegistry.register({ widget: 'iterable-accordion', component: IterableAccordionWidget });
|
|
@@ -9776,6 +10973,8 @@ const registerDefaultWidgets = () => {
|
|
|
9776
10973
|
widgetRegistry.register({ widget: 'header-section', component: HeaderSectionWidget });
|
|
9777
10974
|
// Scores display widget for full-width computed scores display (view-only)
|
|
9778
10975
|
widgetRegistry.register({ widget: 'scores-display', component: ScoresDisplayWidget });
|
|
10976
|
+
// ID Authentication widget for OIDC-based foundational ID authentication (view-only + action)
|
|
10977
|
+
widgetRegistry.register({ widget: 'id-authentication', component: IdAuthenticationWidget });
|
|
9779
10978
|
};
|
|
9780
10979
|
// Auto-register on import
|
|
9781
10980
|
registerDefaultWidgets();
|
|
@@ -10137,5 +11336,5 @@ const translateUISchema = (schema, translate) => {
|
|
|
10137
11336
|
};
|
|
10138
11337
|
};
|
|
10139
11338
|
|
|
10140
|
-
export { ArrayWidget, BooleanWidget, CheckboxWidget, CurrencyInputWidget, DateInputWidget, DateTimeInputWidget, DisplayWidget, FileInputWidget, HeaderSectionWidget, IterableAccordionWidget, JSONEditorPanel, NumberInputWidget, PanelRenderer, PhoneInputWidget, ProfileWidget, PropertyEditor, RadioWidget, ScoresDisplayWidget, SectionBuilder, SectionRenderer, SectionTree, SectionsContainer, SelectWidget, SimpleTableWidget, TableWidget, TextAreaWidget, TextInputWidget, VisualBuilderPanel, WidgetEventBus, WidgetProvider, WidgetRenderer, applyCaseControl, applyDecimalPrecision, applyMask, createWidgetStore, createZodSchema, defaultTheme, evaluateCondition, filterByCharacterType, formatCurrency, formatDate, formatNumber, formatPhone, formatValue, geoHierarchyBuilder, getApiDataSource, getFormattedNumberLength, getSchemaDataSource, getStaticDataSource, getValueByPath, getWidgetValue, initI18n, isAllowedKey, parseDataPath, parseNumber, registerDefaultWidgets, removeMask, resetAll, resetWidget, resolveTheme, setDataSource, setError, setLoading, setTouched, setValue, setValueByPath, setValues, setWidgetValue, shouldEnableWidget, shouldShowWidget, transformDataSourceOptions, translatePanelConfig, translateUISchema, translateWidgetConfig, useBaseWidget, useGeoWidgetCascade, useWidgetCascade, useWidgetContext, useWidgetEventBus, useWidgetTheme, useWidgetTranslation, validateNumericValue, validateWidget, widgetRegistry };
|
|
11339
|
+
export { ArrayWidget, BooleanWidget, CheckboxWidget, CurrencyInputWidget, DateInputWidget, DateTimeInputWidget, DialogTableWidget, DisplayWidget, FileInputWidget, HeaderSectionWidget, IdAuthenticationWidget, IterableAccordionWidget, JSONEditorPanel, NumberInputWidget, PanelRenderer, PhoneInputWidget, ProfileWidget, PropertyEditor, RadioWidget, ScoresDisplayWidget, SectionBuilder, SectionRenderer, SectionTree, SectionsContainer, SelectWidget, SimpleTableWidget, TableWidget, TextAreaWidget, TextInputWidget, VisualBuilderPanel, WidgetEventBus, WidgetProvider, WidgetRenderer, applyCaseControl, applyDecimalPrecision, applyMask, createWidgetStore, createZodSchema, defaultTheme, evaluateCondition, filterByCharacterType, formatCurrency, formatDate, formatNumber, formatPhone, formatValue, geoHierarchyBuilder, getApiDataSource, getFormattedNumberLength, getSchemaDataSource, getStaticDataSource, getValueByPath, getWidgetValue, initI18n, isAllowedKey, parseDataPath, parseNumber, registerDefaultWidgets, removeMask, resetAll, resetWidget, resolveTheme, setDataSource, setError, setLoading, setTouched, setValue, setValueByPath, setValues, setWidgetValue, shouldEnableWidget, shouldShowWidget, transformDataSourceOptions, translatePanelConfig, translateUISchema, translateWidgetConfig, useBaseWidget, useGeoWidgetCascade, useWidgetCascade, useWidgetContext, useWidgetEventBus, useWidgetTheme, useWidgetTranslation, validateNumericValue, validateWidget, widgetRegistry };
|
|
10141
11340
|
//# sourceMappingURL=index.esm.js.map
|