@openg2p/registry-widgets 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3516,6 +3516,39 @@ const sectionValidate = (section, currentSchemaData, dispatch) => {
3516
3516
  return isValid;
3517
3517
  };
3518
3518
 
3519
+ /** Root class on readonly label/value rows; SectionRenderer scopes overflow/ellipsis rules here. */
3520
+ const READONLY_VALUE_ROW_ROOT_CLASSES = [
3521
+ 'TextDisplayWidget',
3522
+ 'TextAreaDisplayWidget',
3523
+ 'SelectDisplayWidget',
3524
+ 'PhoneDisplayWidget',
3525
+ 'NumberDisplayWidget',
3526
+ 'CurrencyDisplayWidget',
3527
+ 'RadioDisplayWidget',
3528
+ 'DateDisplayWidget',
3529
+ 'DateTimeDisplayWidget',
3530
+ 'CheckboxDisplayWidget',
3531
+ 'BooleanDisplayWidget',
3532
+ 'FileDisplayWidget',
3533
+ 'DisplayFieldWidget',
3534
+ ];
3535
+ /** Rows whose value is one line in .flex-1 > .text-gray-900 (ellipsis; full string via title on the element). */
3536
+ const READONLY_SINGLE_LINE_VALUE_ROW_CLASSES = [
3537
+ 'TextDisplayWidget',
3538
+ 'SelectDisplayWidget',
3539
+ 'PhoneDisplayWidget',
3540
+ 'NumberDisplayWidget',
3541
+ 'CurrencyDisplayWidget',
3542
+ 'RadioDisplayWidget',
3543
+ 'DateDisplayWidget',
3544
+ 'DateTimeDisplayWidget',
3545
+ 'CheckboxDisplayWidget',
3546
+ 'BooleanDisplayWidget',
3547
+ 'DisplayFieldWidget',
3548
+ ];
3549
+ function scopedClassSelectors(sectionClassId, classNames) {
3550
+ return classNames.map((c) => `.${sectionClassId} .${c}`).join(',\n ');
3551
+ }
3519
3552
  /**
3520
3553
  * Renders a section with its panels
3521
3554
  *
@@ -3542,41 +3575,22 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
3542
3575
  }
3543
3576
  return section;
3544
3577
  }, [section, namespace]);
3545
- // Create namespaced schemaData if namespace is provided
3546
- // This ensures widgets can read initial values from schemaData at namespaced paths
3578
+ // Create namespaced schemaData if namespace is provided.
3579
+ // Widgets with namespaced data-paths (e.g. "rv-section-0.a1a4d25a.birth_date")
3580
+ // need a nested object at values[namespace] so getValueByPath can traverse it.
3547
3581
  const namespacedSchemaData = React.useMemo(() => {
3548
3582
  if (!namespace || !currentSchemaData) {
3549
3583
  return schemaData;
3550
3584
  }
3551
- // Create a namespaced version of schemaData by copying values to namespaced paths
3552
- const namespaced = { ...currentSchemaData };
3553
- // Copy all top-level keys to namespaced paths
3554
- Object.keys(currentSchemaData).forEach(key => {
3555
- const namespacedKey = `${namespace}.${key}`;
3556
- if (!(namespacedKey in namespaced)) {
3557
- namespaced[namespacedKey] = currentSchemaData[key];
3558
- }
3559
- });
3560
- // Also handle nested objects - copy nested values to namespaced paths
3561
- const copyNestedValues = (obj, prefix = '') => {
3562
- if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
3563
- Object.keys(obj).forEach(key => {
3564
- const fullPath = prefix ? `${prefix}.${key}` : key;
3565
- const namespacedPath = `${namespace}.${fullPath}`;
3566
- if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
3567
- copyNestedValues(obj[key], fullPath);
3568
- // Also set the nested object at the namespaced path
3569
- setValueByPath(namespaced, namespacedPath, obj[key]);
3570
- }
3571
- else {
3572
- setValueByPath(namespaced, namespacedPath, obj[key]);
3573
- }
3574
- });
3575
- }
3576
- };
3577
- copyNestedValues(currentSchemaData);
3578
- return namespaced;
3585
+ return { ...currentSchemaData, [namespace]: currentSchemaData };
3579
3586
  }, [namespace, schemaData, currentSchemaData]);
3587
+ // Populate the store with namespaced schema data so that namespaced widgets
3588
+ // can read their initial values via getValueByPath on the namespaced paths.
3589
+ React.useEffect(() => {
3590
+ if (namespace && namespacedSchemaData) {
3591
+ dispatch(setValues(namespacedSchemaData));
3592
+ }
3593
+ }, [namespace, namespacedSchemaData, dispatch]);
3580
3594
  const crViewData = React.useMemo(() => {
3581
3595
  if (mode !== 'CRView')
3582
3596
  return null;
@@ -3599,6 +3613,9 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
3599
3613
  const sectionId = sectionToRender['section-id'];
3600
3614
  const gridId = `section-panels-${sectionId}`;
3601
3615
  const sectionClassId = `section-${sectionId}`;
3616
+ const readonlyValueRowRootsCss = React.useMemo(() => scopedClassSelectors(sectionClassId, READONLY_VALUE_ROW_ROOT_CLASSES), [sectionClassId]);
3617
+ const readonlyValueRowFlex1Css = React.useMemo(() => READONLY_VALUE_ROW_ROOT_CLASSES.map((c) => `.${sectionClassId} .${c} > .flex-1`).join(',\n '), [sectionClassId]);
3618
+ const readonlySingleLineValueTextCss = React.useMemo(() => READONLY_SINGLE_LINE_VALUE_ROW_CLASSES.map((c) => `.${sectionClassId} .${c} > .flex-1 > .text-gray-900`).join(',\n '), [sectionClassId]);
3602
3619
  // IntakeForm mode: accordion expand/collapse state (supports toggle)
3603
3620
  const [standaloneExpanded, setStandaloneExpanded] = React.useState(true); // For sectionIndex undefined (standalone use)
3604
3621
  const isExpandedFromContainer = typeof sectionIndex === 'number' && expandedSectionIndex === sectionIndex;
@@ -3938,6 +3955,8 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
3938
3955
  const baselineSnapshotRef = React.useRef(null);
3939
3956
  // IntakeForm only: increment when baseline is updated after save - forces badge to update (refs don't trigger re-renders)
3940
3957
  const [intakeFormBaselineTrigger, setIntakeFormBaselineTrigger] = React.useState(0);
3958
+ // IntakeForm only: tracks whether the user has actually saved this section (prevents "Saved" badge on initial load)
3959
+ const [hasBeenSavedByUser, setHasBeenSavedByUser] = React.useState(false);
3941
3960
  // IntakeForm: treat as edit mode for dirty tracking when isDraft. RegistryView: use isEditMode.
3942
3961
  const effectiveEditModeForDirty = mode === 'IntakeForm' ? (isDraft !== false) : isEditMode;
3943
3962
  // Compute isDirty: compare current store state to baseline (only when in edit mode)
@@ -3980,18 +3999,68 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
3980
3999
  const intakeFormSectionStatus = React.useMemo(() => {
3981
4000
  if (mode !== 'IntakeForm' || isDraft === false)
3982
4001
  return null;
3983
- const hasValue = (v) => v !== undefined && v !== null && (typeof v !== 'string' || v.trim().length > 0);
3984
- const currentSnapshot = buildSectionSnapshot(storeValues, namespace);
3985
- const record = currentSnapshot.records?.[0];
3986
- const hasData = record &&
3987
- typeof record === 'object' &&
3988
- Object.values(record).some((v) => hasValue(v));
3989
4002
  if (isDirty)
3990
4003
  return 'modified';
3991
- if (hasData)
4004
+ if (hasBeenSavedByUser)
3992
4005
  return 'saved';
3993
4006
  return null;
3994
- }, [mode, isDirty, storeValues, namespace, buildSectionSnapshot]);
4007
+ }, [mode, isDirty, hasBeenSavedByUser]);
4008
+ // Revert store values to the original schemaData for this section's widgets.
4009
+ // Used by both handleSave (RegistryView raises a CR, so values should not persist)
4010
+ // and handleCancel.
4011
+ const revertToOriginalValues = React.useCallback(() => {
4012
+ const sectionWidgets = collectWidgets(originalSection.panels);
4013
+ const oldSchemaData = schemaData || contextSchemaData;
4014
+ const currentStoreValues = store.getState().widget.values;
4015
+ let newStoreValues = currentStoreValues;
4016
+ sectionWidgets.forEach(widget => {
4017
+ const originalWidgetId = widget['widget-id'];
4018
+ const namespacedWidgetId = namespace ? `${namespace}__${originalWidgetId}` : originalWidgetId;
4019
+ const widgetId = namespacedWidgetId;
4020
+ const originalDataPath = widget['widget-data-path'];
4021
+ const storeDataPath = namespace && originalDataPath
4022
+ ? (typeof originalDataPath === 'string'
4023
+ ? `${namespace}.${originalDataPath}`
4024
+ : Object.fromEntries(Object.entries(originalDataPath).map(([key, path]) => [key, `${namespace}.${path}`])))
4025
+ : originalDataPath;
4026
+ if (widgetId && originalDataPath) {
4027
+ let oldValue;
4028
+ if (typeof originalDataPath === 'object') {
4029
+ oldValue = {};
4030
+ Object.entries(originalDataPath).forEach(([key, path]) => {
4031
+ if (typeof path === 'string') {
4032
+ oldValue[key] = getValueByPath(oldSchemaData, path);
4033
+ }
4034
+ });
4035
+ }
4036
+ else if (typeof originalDataPath === 'string') {
4037
+ oldValue = getValueByPath(oldSchemaData, originalDataPath);
4038
+ }
4039
+ if (oldValue !== undefined) {
4040
+ newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
4041
+ // Also revert the widgetId-based entry — useBaseWidget.handleChange
4042
+ // sets values[widgetId] during editing, and useBaseWidget.currentValue
4043
+ // reads values[widgetId] first before falling through to the dataPath.
4044
+ newStoreValues = { ...newStoreValues, [widgetId]: oldValue };
4045
+ }
4046
+ }
4047
+ });
4048
+ if (hasSupportingDocuments) {
4049
+ const originalSupportingDocuments = originalSection['section-supporting-documents'] || [];
4050
+ originalSupportingDocuments.forEach((doc, index) => {
4051
+ const widgetId = `supporting-doc-${sectionId}-${index}`;
4052
+ const originalDataPath = doc['document-data-path'];
4053
+ const storeDataPath = namespace && originalDataPath
4054
+ ? `${namespace}.${originalDataPath}`
4055
+ : originalDataPath;
4056
+ const oldValue = getValueByPath(oldSchemaData, originalDataPath);
4057
+ newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
4058
+ });
4059
+ }
4060
+ if (newStoreValues !== currentStoreValues) {
4061
+ dispatch(setValues(newStoreValues));
4062
+ }
4063
+ }, [originalSection, schemaData, contextSchemaData, store, namespace, hasSupportingDocuments, sectionId, dispatch]);
3995
4064
  // Handle save button click
3996
4065
  const handleSave = async () => {
3997
4066
  if (!store || !onSectionSave) {
@@ -4028,12 +4097,24 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4028
4097
  });
4029
4098
  }
4030
4099
  if (JSON.stringify(oldSchemaData) !== JSON.stringify(newSchemaData)) {
4100
+ let profileImage = null;
4101
+ for (const record of newSchemaData) {
4102
+ if (typeof record === 'object' && record !== null) {
4103
+ for (const [key, value] of Object.entries(record)) {
4104
+ if (value instanceof File) {
4105
+ profileImage = value;
4106
+ record[key] = '';
4107
+ }
4108
+ }
4109
+ }
4110
+ }
4031
4111
  try {
4032
4112
  const sectionchanges = {
4033
4113
  section_id: dbSectionId ?? originalSection['section-id'],
4034
4114
  section_register_id: sectionRegisterId,
4035
4115
  records: [...newSchemaData],
4036
- files: [...sectionFiles]
4116
+ files: [...sectionFiles],
4117
+ ...(profileImage ? { image: profileImage } : {}),
4037
4118
  };
4038
4119
  await onSectionSave(sectionchanges);
4039
4120
  }
@@ -4041,6 +4122,12 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4041
4122
  console.error('Section Changes Save failed', error);
4042
4123
  }
4043
4124
  }
4125
+ // In RegistryView, save raises a CR — the actual data update follows a
4126
+ // separate approval workflow, so revert the displayed values to the
4127
+ // originals so the view doesn't show unapproved edits.
4128
+ if (mode === 'RegistryView') {
4129
+ revertToOriginalValues();
4130
+ }
4044
4131
  setIsEditMode(false);
4045
4132
  onEditModeChange?.(originalSectionId, false);
4046
4133
  };
@@ -4070,12 +4157,24 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4070
4157
  });
4071
4158
  }
4072
4159
  if (JSON.stringify(oldSchemaData) !== JSON.stringify(newSchemaData)) {
4160
+ let profileImage = null;
4161
+ for (const record of newSchemaData) {
4162
+ if (typeof record === 'object' && record !== null) {
4163
+ for (const [key, value] of Object.entries(record)) {
4164
+ if (value instanceof File) {
4165
+ profileImage = value;
4166
+ record[key] = '';
4167
+ }
4168
+ }
4169
+ }
4170
+ }
4073
4171
  try {
4074
4172
  await onSectionSave({
4075
4173
  section_id: dbSectionId ?? originalSection['section-id'],
4076
4174
  section_register_id: sectionRegisterId,
4077
4175
  records: [...newSchemaData],
4078
4176
  files: [...sectionFiles],
4177
+ ...(profileImage ? { image: profileImage } : {}),
4079
4178
  });
4080
4179
  }
4081
4180
  catch (error) {
@@ -4086,6 +4185,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4086
4185
  if (mode === 'IntakeForm') {
4087
4186
  baselineSnapshotRef.current = buildSectionSnapshot(currentSchemaData, namespace);
4088
4187
  setIntakeFormBaselineTrigger((prev) => prev + 1);
4188
+ setHasBeenSavedByUser(true);
4089
4189
  }
4090
4190
  onSectionDirtyChange?.(sectionId, false);
4091
4191
  }
@@ -4094,64 +4194,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4094
4194
  }, [store, onSectionSave, onSectionSaveSuccess, sectionIndex, originalSection, schemaData, contextSchemaData, namespace, hasSupportingDocuments, dbSectionId, sectionRegisterId, dispatch, buildSectionSnapshot, sectionId, onSectionDirtyChange, mode, isDraft]);
4095
4195
  // Handle cancel button click
4096
4196
  const handleCancel = () => {
4097
- // Revert values in store to original schema data
4098
- // Use original section (without namespace) for collecting widgets
4099
- const sectionWidgets = collectWidgets(originalSection.panels);
4100
- const oldSchemaData = schemaData || contextSchemaData;
4101
- const currentStoreValues = store.getState().widget.values;
4102
- let newStoreValues = currentStoreValues;
4103
- sectionWidgets.forEach(widget => {
4104
- const originalWidgetId = widget['widget-id'];
4105
- // If namespace was used, we need to use namespaced widget ID and data path
4106
- const namespacedWidgetId = namespace ? `${namespace}__${originalWidgetId}` : originalWidgetId;
4107
- const widgetId = namespacedWidgetId;
4108
- const originalDataPath = widget['widget-data-path'];
4109
- // If namespace was used, data path in store is namespaced, but we read from original schema using original path
4110
- const storeDataPath = namespace && originalDataPath
4111
- ? (typeof originalDataPath === 'string'
4112
- ? `${namespace}.${originalDataPath}`
4113
- : Object.fromEntries(Object.entries(originalDataPath).map(([key, path]) => [key, `${namespace}.${path}`])))
4114
- : originalDataPath;
4115
- if (widgetId && originalDataPath) {
4116
- // Handle multi-path (object) or single path (string)
4117
- // Read from original schema data using original paths
4118
- let oldValue;
4119
- if (typeof originalDataPath === 'object') {
4120
- // Multi-path: get values for each path
4121
- oldValue = {};
4122
- Object.entries(originalDataPath).forEach(([key, path]) => {
4123
- if (typeof path === 'string') {
4124
- oldValue[key] = getValueByPath(oldSchemaData, path);
4125
- }
4126
- });
4127
- }
4128
- else if (typeof originalDataPath === 'string') {
4129
- oldValue = getValueByPath(oldSchemaData, originalDataPath);
4130
- }
4131
- // Set in store using namespaced data path (if namespace was used)
4132
- if (oldValue !== undefined) {
4133
- newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
4134
- }
4135
- }
4136
- });
4137
- // Also revert supporting documents if any
4138
- if (hasSupportingDocuments) {
4139
- // Use original section's supporting documents to get original data paths
4140
- const originalSupportingDocuments = originalSection['section-supporting-documents'] || [];
4141
- originalSupportingDocuments.forEach((doc, index) => {
4142
- const widgetId = `supporting-doc-${sectionId}-${index}`;
4143
- const originalDataPath = doc['document-data-path'];
4144
- // If namespace was used, data path in store is namespaced
4145
- const storeDataPath = namespace && originalDataPath
4146
- ? `${namespace}.${originalDataPath}`
4147
- : originalDataPath;
4148
- const oldValue = getValueByPath(oldSchemaData, originalDataPath);
4149
- newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
4150
- });
4151
- }
4152
- if (newStoreValues !== currentStoreValues) {
4153
- dispatch(setValues(newStoreValues));
4154
- }
4197
+ revertToOriginalValues();
4155
4198
  setIsEditMode(false);
4156
4199
  onEditModeChange?.(originalSectionId, false);
4157
4200
  };
@@ -4202,20 +4245,27 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4202
4245
  white-space: nowrap !important;
4203
4246
  }
4204
4247
  /* Readonly: prevent flex row from overflowing panel */
4205
- .${sectionClassId} .TextDisplayWidget {
4248
+ ${readonlyValueRowRootsCss} {
4206
4249
  min-width: 0 !important;
4207
4250
  overflow: hidden !important;
4208
4251
  }
4209
- .${sectionClassId} .TextDisplayWidget > .flex-1 {
4252
+ ${readonlyValueRowFlex1Css} {
4210
4253
  min-width: 0 !important;
4211
4254
  overflow: hidden !important;
4212
4255
  }
4213
- /* Readonly value text truncation */
4214
- .${sectionClassId} .TextDisplayWidget > .flex-1 > .text-gray-900 {
4256
+ /* Readonly value: single-line ellipsis; full value via title on the value node */
4257
+ ${readonlySingleLineValueTextCss} {
4215
4258
  overflow: hidden;
4216
4259
  text-overflow: ellipsis;
4217
4260
  white-space: nowrap;
4218
4261
  }
4262
+ /* Readonly textarea: break unbroken long tokens; title on pre keeps full text on hover */
4263
+ .${sectionClassId} .TextAreaDisplayWidget > .flex-1 > pre {
4264
+ min-width: 0;
4265
+ max-width: 100%;
4266
+ overflow-wrap: anywhere;
4267
+ word-break: break-word;
4268
+ }
4219
4269
 
4220
4270
  /* Only apply fixed height when in edit mode */
4221
4271
  .${sectionClassId}[data-edit-mode="true"] {
@@ -4717,6 +4767,9 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
4717
4767
  }, []);
4718
4768
  const safeSections = sections ?? [];
4719
4769
  const prevSectionsLengthRef = React.useRef(safeSections.length);
4770
+ // Stable ref for namespace so formHandle useMemo doesn't depend on the (possibly inline) function identity
4771
+ const namespaceRef = React.useRef(namespace);
4772
+ namespaceRef.current = namespace;
4720
4773
  // Track dirty (unsaved changes) per section for form handle validation
4721
4774
  const sectionDirtyMapRef = React.useRef({});
4722
4775
  const handleSectionDirtyChange = React.useCallback((sectionId, isDirty) => {
@@ -4762,11 +4815,14 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
4762
4815
  // Form handle for onFormReady - allows host to validate and get all section data from its own Submit button
4763
4816
  const formHandle = React.useMemo(() => {
4764
4817
  const getValues = () => store.getState().widget?.values || {};
4765
- const getNamespace = (section, index) => namespace
4766
- ? typeof namespace === 'string'
4767
- ? namespace
4768
- : namespace(section['section-id'], index)
4769
- : undefined;
4818
+ const getNamespace = (section, index) => {
4819
+ const ns = namespaceRef.current;
4820
+ return ns
4821
+ ? typeof ns === 'string'
4822
+ ? ns
4823
+ : ns(section['section-id'], index)
4824
+ : undefined;
4825
+ };
4770
4826
  const checkNoUnsavedChanges = () => {
4771
4827
  const hasDirty = Object.values(sectionDirtyMapRef.current).some(Boolean);
4772
4828
  if (hasDirty) {
@@ -4816,7 +4872,7 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
4816
4872
  return results;
4817
4873
  },
4818
4874
  };
4819
- }, [store, dispatch, safeSections, namespace]);
4875
+ }, [store, dispatch, safeSections]);
4820
4876
  // Call onFormReady when form is ready (sections loaded)
4821
4877
  React.useEffect(() => {
4822
4878
  if (onFormReady && safeSections.length > 0) {
@@ -8099,10 +8155,10 @@ const DisplayWidget = ({ config }) => {
8099
8155
  const label = translateConfig(widgetConfig['widget-label']);
8100
8156
  // If no label, render as paragraph text
8101
8157
  if (!label || label.trim() === '') {
8102
- return (jsxRuntimeExports.jsx("div", { className: "mb-3 text-base text-gray-700", title: String(displayValue ?? ''), children: displayValue }));
8158
+ 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 }));
8103
8159
  }
8104
- // With label, render as key-value pair
8105
- 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 })] }));
8160
+ // With label, render as key-value pair (structure matches other readonly widgets for SectionRenderer ellipsis)
8161
+ 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 }) })] }));
8106
8162
  };
8107
8163
 
8108
8164
  const TableCellSelect = ({ config, value, onValueChange }) => {
@@ -9070,7 +9126,18 @@ const HeaderSectionWidget = ({ config }) => {
9070
9126
  result = searchIn(schemaData);
9071
9127
  return result;
9072
9128
  }, [paths, values, schemaData]);
9073
- const imageUrl = findValue('image') || null;
9129
+ const imageVal = findValue('image');
9130
+ const imageUrlVal = findValue('imageUrl');
9131
+ const [previewUrl, setPreviewUrl] = React.useState(null);
9132
+ React.useEffect(() => {
9133
+ if (imageVal instanceof File) {
9134
+ const url = URL.createObjectURL(imageVal);
9135
+ setPreviewUrl(url);
9136
+ return () => URL.revokeObjectURL(url);
9137
+ }
9138
+ setPreviewUrl(null);
9139
+ }, [imageVal]);
9140
+ const displayImageUrl = previewUrl || (typeof imageUrlVal === 'string' && imageUrlVal ? imageUrlVal : null);
9074
9141
  const displayName = findValue('name') || '';
9075
9142
  const functionalId = findValue('functionalId') || '';
9076
9143
  const statusValue = findValue('status') || '';
@@ -9103,18 +9170,20 @@ const HeaderSectionWidget = ({ config }) => {
9103
9170
  return opt ? opt.label : String(statusValue);
9104
9171
  }, [statusValue, statusOptions]);
9105
9172
  const statusColor = statusColors[String(statusValue).toLowerCase()] || '#6B7280';
9173
+ // ── Image edit helpers ───────────────────────────────────────
9174
+ const fileInputRef = React.useRef(null);
9175
+ const handleImageUpload = React.useCallback((e) => {
9176
+ const file = e.target.files?.[0];
9177
+ if (!file)
9178
+ return;
9179
+ updateFieldValue('image', file);
9180
+ e.target.value = '';
9181
+ }, [updateFieldValue]);
9182
+ const handleImageDelete = React.useCallback(() => {
9183
+ updateFieldValue('image', '');
9184
+ }, [updateFieldValue]);
9106
9185
  // ── Scoped class for CSS isolation ────────────────────────────
9107
9186
  const cls = `header-section-widget-${widgetConfig['widget-id']}`;
9108
- // ── Indicator dot component ───────────────────────────────────
9109
- const Dot = ({ color }) => (jsxRuntimeExports.jsx("span", { style: {
9110
- display: 'inline-block',
9111
- width: 8,
9112
- height: 8,
9113
- borderRadius: '50%',
9114
- backgroundColor: color,
9115
- flexShrink: 0,
9116
- marginTop: 6,
9117
- } }));
9118
9187
  // ── RENDER ────────────────────────────────────────────────────
9119
9188
  return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("style", { children: `
9120
9189
  .${cls} {
@@ -9173,6 +9242,56 @@ const HeaderSectionWidget = ({ config }) => {
9173
9242
  border-radius: 8px;
9174
9243
  }
9175
9244
 
9245
+ .${cls} .hdr-avatar-wrapper {
9246
+ position: relative;
9247
+ width: ${imageSize}px;
9248
+ height: ${imageSize}px;
9249
+ flex-shrink: 0;
9250
+ }
9251
+
9252
+ .${cls} .hdr-avatar-overlay {
9253
+ position: absolute;
9254
+ inset: 0;
9255
+ border-radius: 8px;
9256
+ background: rgba(0, 0, 0, 0.55);
9257
+ display: flex;
9258
+ flex-direction: column;
9259
+ align-items: center;
9260
+ justify-content: center;
9261
+ gap: 6px;
9262
+ opacity: 0;
9263
+ transition: opacity 0.2s;
9264
+ }
9265
+
9266
+ .${cls} .hdr-avatar-wrapper:hover .hdr-avatar-overlay {
9267
+ opacity: 1;
9268
+ }
9269
+
9270
+ .${cls} .hdr-avatar-action {
9271
+ display: flex;
9272
+ align-items: center;
9273
+ gap: 5px;
9274
+ padding: 5px 14px;
9275
+ border: none;
9276
+ border-radius: 4px;
9277
+ background: rgba(255, 255, 255, 0.92);
9278
+ color: #374151;
9279
+ font-size: 0.7rem;
9280
+ font-weight: 500;
9281
+ cursor: pointer;
9282
+ font-family: Roboto, sans-serif;
9283
+ transition: background 0.15s;
9284
+ white-space: nowrap;
9285
+ }
9286
+
9287
+ .${cls} .hdr-avatar-action:hover {
9288
+ background: #fff;
9289
+ }
9290
+
9291
+ .${cls} .hdr-avatar-action--delete {
9292
+ color: #DC2626;
9293
+ }
9294
+
9176
9295
  .${cls} .hdr-info {
9177
9296
  display: flex;
9178
9297
  flex-direction: column;
@@ -9277,13 +9396,13 @@ const HeaderSectionWidget = ({ config }) => {
9277
9396
  min-width: 0;
9278
9397
  }
9279
9398
  }
9280
- ` }), jsxRuntimeExports.jsxs("div", { className: cls, children: [jsxRuntimeExports.jsxs("div", { className: "hdr-left", children: [jsxRuntimeExports.jsxs("div", { children: [imageUrl ? (jsxRuntimeExports.jsx("img", { src: imageUrl, alt: displayName || 'Profile', className: "hdr-avatar", onError: (e) => {
9399
+ ` }), 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) => {
9281
9400
  e.target.style.display = 'none';
9282
9401
  const placeholder = e.target
9283
9402
  .parentElement?.querySelector('.hdr-avatar-placeholder');
9284
9403
  if (placeholder)
9285
9404
  placeholder.style.display = 'flex';
9286
- } })) : null, jsxRuntimeExports.jsx("div", { className: "hdr-avatar-placeholder", style: { display: imageUrl ? 'none' : 'flex' }, children: jsxRuntimeExports.jsx("img", { src: img$f, alt: "Profile Placeholder" }) })] }), 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.jsx(Dot, { color: "#9CA3AF" }), 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(Dot, { color: isReadonly ? statusColor : '#F59E0B' }), 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.jsx(Dot, { color: isReadonly ? '#9CA3AF' : '#F59E0B' }), jsxRuntimeExports.jsxs("span", { className: "hdr-field-label", children: [getLabel('statusReason'), " :"] }), isReadonly ? (jsxRuntimeExports.jsx("span", { className: "hdr-field-value", children: statusReason || '-' })) : (jsxRuntimeExports.jsx("input", { type: "text", className: "hdr-input", value: statusReason, placeholder: getLabel('enterReason'), onChange: (e) => updateFieldValue('statusReason', e.target.value) }))] })] })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-right", 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 || '-' })] })] })] })] }));
9405
+ } })) : 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.jsx("input", { type: "text", className: "hdr-input", value: statusReason, placeholder: getLabel('enterReason'), onChange: (e) => updateFieldValue('statusReason', e.target.value) }))] })] })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-right", 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 || '-' })] })] })] })] }));
9287
9406
  };
9288
9407
 
9289
9408
  /**