ui-soxo-bootstrap-core 2.6.1-dev.3 → 2.6.1-dev.31

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.
Files changed (68) hide show
  1. package/core/components/extra-info/extra-info-details.js +2 -2
  2. package/core/components/index.js +2 -11
  3. package/core/components/landing-api/landing-api.js +216 -18
  4. package/core/components/landing-api/landing-api.scss +22 -0
  5. package/core/components/license-management/license-alert.js +97 -0
  6. package/core/lib/Store.js +8 -4
  7. package/core/lib/components/global-header/global-header.js +217 -242
  8. package/core/lib/components/index.js +2 -2
  9. package/core/lib/components/sidemenu/sidemenu.js +19 -13
  10. package/core/lib/components/sidemenu/sidemenu.scss +1 -1
  11. package/core/lib/elements/basic/country-phone-input/country-phone-input.js +14 -9
  12. package/core/lib/elements/basic/dragabble-wrapper/draggable-wrapper.js +1 -1
  13. package/core/lib/elements/basic/menu-tree/menu-tree.js +26 -13
  14. package/core/lib/models/forms/components/form-creator/form-creator.js +525 -468
  15. package/core/lib/models/forms/components/form-creator/form-creator.scss +30 -26
  16. package/core/lib/models/menus/components/menu-list/menu-list.js +424 -467
  17. package/core/lib/models/process/components/process-dashboard/process-dashboard.js +469 -3
  18. package/core/lib/models/process/components/process-dashboard/process-dashboard.scss +4 -0
  19. package/core/lib/modules/generic/generic-list/ExportReactCSV.js +28 -2
  20. package/core/lib/pages/change-password/change-password.js +17 -24
  21. package/core/lib/pages/change-password/change-password.scss +45 -48
  22. package/core/lib/pages/login/commnication-mode-selection.js +2 -2
  23. package/core/lib/pages/login/login.js +53 -64
  24. package/core/lib/pages/login/login.scss +9 -0
  25. package/core/lib/pages/login/reset-password.js +17 -17
  26. package/core/lib/pages/login/reset-password.scss +10 -1
  27. package/core/lib/pages/profile/themes.json +4 -4
  28. package/core/lib/utils/api/api.utils.js +53 -45
  29. package/core/lib/utils/common/common.utils.js +49 -35
  30. package/core/lib/utils/generic/generic.utils.js +2 -1
  31. package/core/lib/utils/http/http.utils.js +33 -4
  32. package/core/lib/utils/index.js +4 -1
  33. package/core/models/base/base.js +7 -3
  34. package/core/models/core-scripts/core-scripts.js +147 -126
  35. package/core/models/doctor/components/doctor-add/doctor-add.js +9 -4
  36. package/core/models/menus/components/menu-add/menu-add.js +1 -1
  37. package/core/models/menus/components/menu-lists/menu-lists.js +53 -54
  38. package/core/models/menus/menus.js +49 -2
  39. package/core/models/roles/components/role-add/role-add.js +92 -59
  40. package/core/models/roles/components/role-list/role-list.js +1 -1
  41. package/core/models/staff/components/staff-add/staff-add.js +20 -32
  42. package/core/models/users/components/assign-role/assign-role.js +145 -50
  43. package/core/models/users/components/assign-role/assign-role.scss +209 -45
  44. package/core/models/users/components/assign-role/avatar-props.js +45 -0
  45. package/core/models/users/components/user-add/user-add.js +46 -55
  46. package/core/models/users/components/user-add/user-edit.js +25 -4
  47. package/core/models/users/users.js +9 -1
  48. package/core/modules/dashboard/components/dashboard-card/menu-dashboard-card.js +1 -1
  49. package/core/modules/reporting/components/reporting-dashboard/README.md +316 -0
  50. package/core/modules/reporting/components/reporting-dashboard/adavance-search/advance-search.js +174 -0
  51. package/core/modules/reporting/components/reporting-dashboard/adavance-search/advance-search.scss +76 -0
  52. package/core/modules/reporting/components/reporting-dashboard/display-columns/build-display-columns.js +90 -0
  53. package/core/modules/reporting/components/reporting-dashboard/display-columns/build-display-columns.test.js +74 -0
  54. package/core/modules/reporting/components/reporting-dashboard/display-columns/display-cell-renderer.js +448 -0
  55. package/core/modules/reporting/components/reporting-dashboard/display-columns/display-cell-renderer.test.js +199 -0
  56. package/core/modules/reporting/components/reporting-dashboard/reporting-dashboard.js +195 -822
  57. package/core/modules/reporting/components/reporting-dashboard/reporting-dashboard.scss +43 -0
  58. package/core/modules/reporting/components/reporting-dashboard/reporting-table.js +517 -0
  59. package/core/modules/steps/action-buttons.js +30 -16
  60. package/core/modules/steps/action-buttons.scss +55 -9
  61. package/core/modules/steps/chat-assistant.js +141 -0
  62. package/core/modules/steps/openai-realtime.js +275 -0
  63. package/core/modules/steps/readme.md +167 -0
  64. package/core/modules/steps/steps.js +1286 -60
  65. package/core/modules/steps/steps.scss +703 -86
  66. package/core/modules/steps/timeline.js +21 -19
  67. package/core/modules/steps/voice-navigation.js +709 -0
  68. package/package.json +2 -1
@@ -0,0 +1,448 @@
1
+ import React, { useState, useContext } from 'react';
2
+ import { Modal, Tag } from 'antd';
3
+ import * as Icons from '@ant-design/icons';
4
+ import { Link } from 'react-router-dom';
5
+ import { GlobalContext, safeJSON, Location } from '../../../../../lib';
6
+ // import { PdfViewer } from '../../../../../lib';
7
+ import { CoreScripts } from '../../../../../models';
8
+
9
+ /**
10
+ * Utilities for rendering Reporting Dashboard display columns.
11
+ *
12
+ * Backward-compatibility contract for action columns:
13
+ * 1. `entry.field === 'action'` is always treated as legacy action behavior.
14
+ * 2. `entry.type === 'action' && entry.field !== 'action'` is treated as the new action layer.
15
+ * 3. All other entries follow existing non-action render logic.
16
+ */
17
+
18
+ /**
19
+ * @typedef {Object} ReplaceVariable
20
+ * @property {string} field Field name from row data used to replace `@field;` placeholders in `redirect_link`.
21
+ */
22
+
23
+ /**
24
+ * @typedef {Object} DisplayColumnEntry
25
+ * @property {string} [field] Source field name from row data.
26
+ * @property {string} [type] Supported values include `link`, `action`, `number`.
27
+ * @property {string} [title] Display title.
28
+ * @property {string} [tooltip] Optional tooltip text for table header.
29
+ * @property {string} [color] Static text color for default text rendering.
30
+ * @property {boolean} [enableColor] Enables color-based rendering using `record.color_code`.
31
+ * @property {string} [columnType] When `enableColor` is true, supports `tag` and `span`.
32
+ * @property {Object.<string, {icon: string, color: string, size: string}>} [displayIcons] Icon mapping keyed by row value.
33
+ * @property {string} [display_name_link] Legacy action link label fallback.
34
+ * @property {string} [label] New action link label fallback.
35
+ * @property {string} [redirect_link] Redirect template (example: `/path/@opb_id;`).
36
+ * @property {ReplaceVariable[]} [replace_variables] Placeholder replacement config for `redirect_link`.
37
+ * @property {string} [component] Custom component name for `field === 'custom'`.
38
+ * @property {{field: string, value: string}[]} [props] Mapping from record fields to custom component prop keys.
39
+ * @property {Object} [config] Static props passed to custom component.
40
+ * @property {(record: DisplayRecord) => React.ReactNode} [render] Optional custom render override.
41
+ */
42
+
43
+ /**
44
+ * @typedef {Object.<string, any>} DisplayRecord
45
+ */
46
+
47
+ /**
48
+ * Checks if the column entry is a legacy action configuration.
49
+ *
50
+ * @param {DisplayColumnEntry} entry
51
+ * @returns {boolean}
52
+ */
53
+ export function isLegacyActionEntry(entry = {}) {
54
+ return entry.field === 'action';
55
+ }
56
+
57
+ /**
58
+ * Checks if the column entry is a new action-type configuration.
59
+ *
60
+ * @param {DisplayColumnEntry} entry
61
+ * @returns {boolean}
62
+ */
63
+ export function isActionTypeEntry(entry = {}) {
64
+ return entry.type === 'action' && entry.field !== 'action';
65
+ }
66
+
67
+ /**
68
+ * Builds redirect link by replacing configured placeholders with row values.
69
+ *
70
+ * Placeholder format in `redirect_link` is `@field;`.
71
+ * Example:
72
+ * redirect_link: `/visit/@opb_id;/bill/@opno;`
73
+ * replace_variables: [{ field: 'opb_id' }, { field: 'opno' }]
74
+ * result: `/visit/123/bill/OP-100`
75
+ *
76
+ * @param {DisplayColumnEntry} entry
77
+ * @param {DisplayRecord} record
78
+ * @returns {string}
79
+ */
80
+ export function getRedirectLink(entry = {}, record = {}, CustomComponents = {}) {
81
+ let redirectLink = entry.redirect_link || '';
82
+
83
+ if (Array.isArray(entry.replace_variables)) {
84
+ entry.replace_variables.forEach((replacement) => {
85
+ const value = record[replacement.field] || '';
86
+ redirectLink = redirectLink.replace(new RegExp(`@${replacement.field};`, 'g'), value);
87
+ });
88
+ }
89
+
90
+ return redirectLink;
91
+ }
92
+
93
+ /**
94
+ * Resolves the PDF/file location for file-based action entries.
95
+ *
96
+ * Supports either:
97
+ * - `entry.file_location` as a direct URL/path
98
+ * - `entry.file_location` as a record field name
99
+ *
100
+ * @param {DisplayColumnEntry} entry
101
+ * @param {DisplayRecord} record
102
+ * @returns {string}
103
+ */
104
+ export function getFileLocation(entry = {}, record = {}) {
105
+ if (entry.file_location && typeof entry.file_location === 'string' && record[entry.file_location]) {
106
+ return record[entry.file_location];
107
+ }
108
+
109
+ return record.file_location || entry.file_location || '';
110
+ }
111
+
112
+ /**
113
+ * Resolves a value from the row when a config field points to a record key,
114
+ * otherwise returns the provided literal value.
115
+ *
116
+ * @param {*} value
117
+ * @param {DisplayRecord} record
118
+ * @returns {*}
119
+ */
120
+ function resolveRecordValue(value, record = {}) {
121
+ if (typeof value === 'string' && Object.prototype.hasOwnProperty.call(record, value)) {
122
+ return record[value];
123
+ }
124
+
125
+ return value;
126
+ }
127
+
128
+ /**
129
+ * Resolves action label text with backward-compatible precedence.
130
+ *
131
+ * Legacy action (`field === 'action'`):
132
+ * - `entry.display_name_link`
133
+ * - `"View"`
134
+ *
135
+ * New action layer (`type === 'action' && field !== 'action'`):
136
+ * - `record[entry.field]`
137
+ * - `entry.label`
138
+ * - `entry.display_name_link`
139
+ * - `"View"`
140
+ *
141
+ * @param {DisplayColumnEntry} entry
142
+ * @param {DisplayRecord} record
143
+ * @returns {string}
144
+ */
145
+ export function getActionLabel(entry = {}, record = {}) {
146
+ // Legacy action path should stay unchanged.
147
+ if (isLegacyActionEntry(entry)) {
148
+ return entry.display_name_link ? entry.display_name_link : 'View';
149
+ }
150
+
151
+ const fieldValue = entry.field ? record[entry.field] : null;
152
+ if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') {
153
+ return fieldValue;
154
+ }
155
+
156
+ if (entry.label) {
157
+ return entry.label;
158
+ }
159
+
160
+ if (entry.display_name_link) {
161
+ return entry.display_name_link;
162
+ }
163
+
164
+ return 'View';
165
+ }
166
+
167
+ /**
168
+ * Renders configured custom component cells (`field === 'custom'`).
169
+ *
170
+ * Each `entry.props` item maps a record field to a prop name:
171
+ * `{ field: 'status_text', value: 'description' }` becomes
172
+ * `LoadedComponent({ description: record.status_text })`.
173
+ *
174
+ * @param {Object} root0
175
+ * @param {DisplayColumnEntry} root0.entry
176
+ * @param {DisplayRecord} root0.record
177
+ * @param {Object.<string, React.ComponentType<any>>} root0.CustomComponents
178
+ * @param {Function} root0.refresh
179
+ * @returns {React.ReactNode}
180
+ */
181
+ function renderCustomComponent({ entry, record, CustomComponents, refresh }) {
182
+ const componentName = entry.component;
183
+ const genericComponents = CustomComponents || {};
184
+
185
+ if (!componentName || !genericComponents[componentName]) {
186
+ return null;
187
+ }
188
+
189
+ const LoadedComponent = genericComponents[componentName];
190
+ const propValue = {};
191
+
192
+ if (Array.isArray(entry.props)) {
193
+ entry.props.forEach((values) => {
194
+ const valueCreation = record[values.field];
195
+ propValue[values.value] = valueCreation;
196
+ });
197
+ }
198
+
199
+ return (
200
+ <LoadedComponent
201
+ {...entry.config}
202
+ callback={() => {
203
+ refresh();
204
+ }}
205
+ {...record}
206
+ {...propValue}
207
+ />
208
+ );
209
+ }
210
+
211
+ /**
212
+ * Renders a PDF file action inside a modal viewer.
213
+ *
214
+ * @param {Object} root0
215
+ * @param {DisplayColumnEntry} root0.entry
216
+ * @param {DisplayRecord} root0.record
217
+ * @param {Object.<string, React.ComponentType<any>>} root0.CustomComponents
218
+ * @returns {React.ReactNode}
219
+ */
220
+ function FileActionLink({ entry, record, CustomComponents }) {
221
+ const [isPdfVisible, setIsPdfVisible] = useState(false);
222
+ const fileUrl = getFileLocation(entry, record);
223
+ const actionLabel = getActionLabel(entry, record);
224
+ const FileLoaderComponent = CustomComponents?.FileLoader;
225
+
226
+ if (!fileUrl) {
227
+ return null;
228
+ }
229
+
230
+ const fileLoaderProps = {
231
+ url: fileUrl,
232
+ type: resolveRecordValue(entry.file_type, record) || 'pdf',
233
+ defaultScale: resolveRecordValue(entry.default_scale, record),
234
+ viewerType: resolveRecordValue(entry.viewer_type, record),
235
+ config: {
236
+ requireLinuxPath: resolveRecordValue(entry.require_linux_path, record),
237
+ replaceBranch: resolveRecordValue(entry.replace_branch, record),
238
+ ...(entry.file_loader_config || {}),
239
+ },
240
+ entry,
241
+ record,
242
+ };
243
+
244
+ return (
245
+ <>
246
+ {/* 1. Add the trigger link/button here */}
247
+ <a
248
+ onClick={(e) => {
249
+ e.preventDefault();
250
+ setIsPdfVisible(true);
251
+ }}
252
+ style={{ cursor: 'pointer' }}
253
+ >
254
+ {actionLabel}
255
+ </a>
256
+
257
+ <Modal
258
+ open={isPdfVisible}
259
+ onCancel={() => setIsPdfVisible(false)}
260
+ footer={null}
261
+ destroyOnClose
262
+ width={950}
263
+ style={{ top: 10 }}
264
+ title={actionLabel}
265
+ >
266
+ {/* 2. Ensure the loader component exists */}
267
+ {FileLoaderComponent ? <FileLoaderComponent {...fileLoaderProps} /> : <p>Loader not found</p>}
268
+ </Modal>
269
+ </>
270
+ );
271
+ }
272
+
273
+ /**
274
+ * Returns the branch access list from the signed-in user's organization details.
275
+ *
276
+ * `organization_details` may arrive as a JSON string, so this helper keeps the
277
+ * parsing logic in one place before branch comparisons are made.
278
+ *
279
+ * @param {Object} user
280
+ * @returns {Array}
281
+ */
282
+ function getAccessibleBranches(user = {}) {
283
+ const orgDetails = safeJSON(user?.organization_details);
284
+ return Array.isArray(orgDetails?.branch) ? orgDetails.branch : [];
285
+ }
286
+
287
+ /**
288
+ * Resolves branch metadata used to decide whether an action link should prompt
289
+ * for a branch switch before navigation.
290
+ *
291
+ * When an action entry includes `replace_variables` with the `index` field, the
292
+ * row is treated as branch-aware content. We compare the record's branch id
293
+ * against the active branch from `localStorage.db_ptr`, and also resolve whether
294
+ * the user has access to the target branch.
295
+ *
296
+ * @param {DisplayColumnEntry} entry
297
+ * @param {DisplayRecord} record
298
+ * @param {Object} user
299
+ * @returns {{requiresBranchSwitch: boolean, hasTargetBranchAccess: boolean}}
300
+ */
301
+ function getBranchNavigationState(entry = {}, record = {}, user = {}) {
302
+ const branchFieldConfig = entry.replace_variables?.find((variable) => variable.field === 'index');
303
+
304
+ if (!branchFieldConfig) {
305
+ return { requiresBranchSwitch: false, hasTargetBranchAccess: false };
306
+ }
307
+
308
+ const accessibleBranches = getAccessibleBranches(user);
309
+ const activeDbPtr = localStorage.getItem('db_ptr');
310
+ const activeBranch = accessibleBranches.find((branch) => String(branch.dbPtr) === String(activeDbPtr));
311
+ const targetBranchId = record[branchFieldConfig.field];
312
+ const targetBranch = accessibleBranches.find((branch) => String(branch.branch_id) === String(targetBranchId));
313
+
314
+ return {
315
+ requiresBranchSwitch: Boolean(targetBranchId) && String(activeBranch?.branch_id) !== String(targetBranchId),
316
+ hasTargetBranchAccess: Boolean(targetBranch),
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Renders a branch-switch confirmation link for cross-branch records.
322
+ *
323
+ * The actual branch change is handled by the landing page via the `index`
324
+ * query parameter. This link only confirms intent and blocks navigation when
325
+ * the user does not have access to the target branch.
326
+ *
327
+ * @param {Object} root0
328
+ * @param {string} root0.label
329
+ * @param {string} root0.redirectLink
330
+ * @param {boolean} root0.hasTargetBranchAccess
331
+ * @returns {React.ReactNode}
332
+ */
333
+ function BranchAwareActionLink({ label, redirectLink, hasTargetBranchAccess }) {
334
+ return (
335
+ <a
336
+ onClick={(e) => {
337
+ e.preventDefault();
338
+ Modal.confirm({
339
+ content: 'This data is another branch. Switch to that branch to view details?',
340
+ onOk: () => {
341
+ if (hasTargetBranchAccess) {
342
+ Location.navigate({ url: redirectLink });
343
+ return;
344
+ }
345
+
346
+ Modal.error({
347
+ title: 'Branch Switch Failed',
348
+ content: "You don't have permission to view this branch's details.",
349
+ });
350
+ },
351
+ });
352
+ }}
353
+ >
354
+ {label}
355
+ </a>
356
+ );
357
+ }
358
+ /**
359
+ * Renders table cell content for a configured display column.
360
+ *
361
+ * Render priority:
362
+ * 1. `entry.render(record)` override
363
+ * 2. `type === 'link'`
364
+ * 3. action columns (legacy or new action layer)
365
+ * 4. custom component (`field === 'custom'`)
366
+ * 5. color tag/span via `record.color_code` + `enableColor`
367
+ * 6. dynamic icon mapping (`displayIcons`)
368
+ * 7. default text span
369
+ *
370
+ * @param {Object} root0
371
+ * @param {DisplayColumnEntry} root0.entry
372
+ * @param {DisplayRecord} root0.record
373
+ * @param {Object.<string, React.ComponentType<any>>} root0.CustomComponents
374
+ * @param {Function} root0.refresh
375
+ * @returns {React.ReactNode}
376
+ */
377
+ export function renderDisplayCell({ entry, record, CustomComponents, refresh }) {
378
+ let textColor = 'inherit';
379
+
380
+ if (entry.color) {
381
+ textColor = entry.color;
382
+ }
383
+
384
+ if (entry.render) {
385
+ return entry.render(record);
386
+ }
387
+
388
+ if (entry.type === 'link') {
389
+ if (record[entry.field]) {
390
+ return (
391
+ <a href={record[entry.field]} target="_blank" rel="noopener noreferrer">
392
+ View
393
+ </a>
394
+ );
395
+ }
396
+ return null;
397
+ }
398
+ if (isLegacyActionEntry(entry) || isActionTypeEntry(entry)) {
399
+ if (entry.redirect_link_type === 'file') {
400
+ return <FileActionLink entry={entry} record={record} CustomComponents={CustomComponents} />;
401
+ }
402
+ const { user = {} } = useContext(GlobalContext);
403
+
404
+ const redirectLink = getRedirectLink(entry, record);
405
+ const label = getActionLabel(entry, record);
406
+ const { requiresBranchSwitch, hasTargetBranchAccess } = getBranchNavigationState(entry, record, user);
407
+
408
+ if (requiresBranchSwitch) {
409
+ return <BranchAwareActionLink label={label} redirectLink={redirectLink} hasTargetBranchAccess={hasTargetBranchAccess} />;
410
+ }
411
+
412
+ return <Link to={`${redirectLink}`}>{label}</Link>;
413
+ }
414
+
415
+ if (entry.field === 'custom') {
416
+ return renderCustomComponent({ entry, record, CustomComponents, refresh });
417
+ }
418
+
419
+ if (record.color_code && entry.enableColor) {
420
+ if (entry.columnType === 'tag') {
421
+ return <Tag color={record.color_code}>{record[entry.field]}</Tag>;
422
+ }
423
+ if (entry.columnType === 'span') {
424
+ return <span style={{ color: record.color_code, overflowWrap: 'break-word', WebkitLineClamp: 3 }}>{record[entry.field]}</span>;
425
+ }
426
+ }
427
+
428
+ if (entry.displayIcons) {
429
+ const fieldValue = record[entry.field]?.toString();
430
+ const displayConfig = entry.displayIcons[fieldValue];
431
+ if (displayConfig) {
432
+ const DynamicIcon = Icons[displayConfig.icon];
433
+ if (DynamicIcon) {
434
+ return (
435
+ <DynamicIcon
436
+ style={{
437
+ color: displayConfig.color,
438
+ fontSize: displayConfig.size,
439
+ }}
440
+ />
441
+ );
442
+ }
443
+ }
444
+ return null;
445
+ }
446
+
447
+ return <span style={{ color: textColor, whiteSpace: 'pre-wrap', overflowWrap: 'break-word' }}>{record[entry.field]}</span>;
448
+ }
@@ -0,0 +1,199 @@
1
+ import React from 'react';
2
+ import {
3
+ isLegacyActionEntry,
4
+ isActionTypeEntry,
5
+ getRedirectLink,
6
+ getFileLocation,
7
+ getActionLabel,
8
+ renderDisplayCell,
9
+ } from './display-cell-renderer';
10
+
11
+ describe('display-cell-renderer', () => {
12
+ test('identifies legacy and new action entries correctly', () => {
13
+ expect(isLegacyActionEntry({ field: 'action' })).toBe(true);
14
+ expect(isLegacyActionEntry({ field: 'action_text', type: 'action' })).toBe(false);
15
+
16
+ expect(isActionTypeEntry({ field: 'action_text', type: 'action' })).toBe(true);
17
+ expect(isActionTypeEntry({ field: 'action', type: 'action' })).toBe(false);
18
+ });
19
+
20
+ test('builds redirect link from replace variables', () => {
21
+ const link = getRedirectLink(
22
+ {
23
+ redirect_link: '/bill/@opb_id;/visit/@opno;',
24
+ replace_variables: [{ field: 'opb_id' }, { field: 'opno' }],
25
+ },
26
+ { opb_id: 10, opno: 'OP100' },
27
+ );
28
+
29
+ expect(link).toBe('/bill/10/visit/OP100');
30
+ });
31
+
32
+ test('resolves file location from direct config value or record field', () => {
33
+ expect(getFileLocation({ file_location: 'https://example.com/report.pdf' }, {})).toBe('https://example.com/report.pdf');
34
+ expect(getFileLocation({ file_location: 'document_url' }, { document_url: 'https://example.com/record.pdf' })).toBe(
35
+ 'https://example.com/record.pdf',
36
+ );
37
+ });
38
+
39
+ test('keeps legacy action label behavior unchanged', () => {
40
+ const entry = {
41
+ field: 'action',
42
+ display_name_link: 'Open',
43
+ };
44
+
45
+ expect(getActionLabel(entry, { action: 'Dynamic Value' })).toBe('Open');
46
+ expect(getActionLabel({ field: 'action' }, { action: 'Dynamic Value' })).toBe('View');
47
+ });
48
+
49
+ test('resolves new action label with fallback precedence', () => {
50
+ expect(getActionLabel({ type: 'action', field: 'action_text' }, { action_text: 'Review' })).toBe('Review');
51
+ expect(getActionLabel({ type: 'action', field: 'action_text', label: 'Open' }, {})).toBe('Open');
52
+ expect(getActionLabel({ type: 'action', field: 'action_text', display_name_link: 'Inspect' }, {})).toBe('Inspect');
53
+ expect(getActionLabel({ type: 'action', field: 'action_text' }, {})).toBe('View');
54
+ });
55
+
56
+ test('renders link column as anchor', () => {
57
+ const element = renderDisplayCell({
58
+ entry: { type: 'link', field: 'report_url' },
59
+ record: { report_url: 'https://example.com/report' },
60
+ refresh: jest.fn(),
61
+ CustomComponents: {},
62
+ });
63
+
64
+ expect(element.type).toBe('a');
65
+ expect(element.props.href).toBe('https://example.com/report');
66
+ });
67
+
68
+ test('renders custom component with mapped props', () => {
69
+ const MockComponent = () => null;
70
+ const element = renderDisplayCell({
71
+ entry: {
72
+ field: 'custom',
73
+ component: 'MockComponent',
74
+ props: [{ field: 'status', value: 'description' }],
75
+ config: { sample: true },
76
+ },
77
+ record: { status: 'Pending', opb_id: 1 },
78
+ refresh: jest.fn(),
79
+ CustomComponents: { MockComponent },
80
+ });
81
+
82
+ expect(element.type).toBe(MockComponent);
83
+ expect(element.props.description).toBe('Pending');
84
+ expect(element.props.sample).toBe(true);
85
+ expect(element.props.opb_id).toBe(1);
86
+ });
87
+
88
+ test('renders file action entry as pdf viewer trigger', () => {
89
+ const element = renderDisplayCell({
90
+ entry: {
91
+ type: 'action',
92
+ field: 'action_text',
93
+ redirect_link_type: 'file',
94
+ file_location: 'https://example.com/report.pdf',
95
+ label: 'Open PDF',
96
+ },
97
+ record: { action_text: 'Preview PDF' },
98
+ refresh: jest.fn(),
99
+ CustomComponents: {},
100
+ });
101
+
102
+ expect(typeof element.type).toBe('function');
103
+ expect(element.props.entry.redirect_link_type).toBe('file');
104
+ expect(element.props.entry.file_location).toBe('https://example.com/report.pdf');
105
+ });
106
+
107
+ test('passes file action props to CustomComponents.FileUpload when available', () => {
108
+ const FileUpload = () => null;
109
+ const element = renderDisplayCell({
110
+ entry: {
111
+ type: 'action',
112
+ field: 'action_text',
113
+ redirect_link_type: 'file',
114
+ file_location: 'document_path',
115
+ file_type: 'document_type',
116
+ default_scale: 'zoom_level',
117
+ viewer_type: 'inline_viewer',
118
+ require_linux_path: 'needs_linux_path',
119
+ replace_branch: 'should_replace_branch',
120
+ file_loader_config: {
121
+ sample: true,
122
+ },
123
+ },
124
+ record: {
125
+ action_text: 'Preview',
126
+ document_path: '\\\\server\\reports\\doc.pdf',
127
+ document_type: 'pdf',
128
+ zoom_level: 1.25,
129
+ inline_viewer: true,
130
+ needs_linux_path: true,
131
+ should_replace_branch: false,
132
+ },
133
+ refresh: jest.fn(),
134
+ CustomComponents: { FileUpload },
135
+ });
136
+
137
+ expect(typeof element.type).toBe('function');
138
+
139
+ const modalContent = element.props.children[1].props.children;
140
+ expect(modalContent.type).toBe(FileUpload);
141
+ expect(modalContent.props.url).toBe('\\\\server\\reports\\doc.pdf');
142
+ expect(modalContent.props.type).toBe('pdf');
143
+ expect(modalContent.props.defaultScale).toBe(1.25);
144
+ expect(modalContent.props.viewerType).toBe(true);
145
+ expect(modalContent.props.config).toEqual({
146
+ requireLinuxPath: true,
147
+ replaceBranch: false,
148
+ sample: true,
149
+ });
150
+ expect(modalContent.props.record.document_path).toBe('\\\\server\\reports\\doc.pdf');
151
+ });
152
+
153
+ test('renders styled tag/span/icon/text for non-action path', () => {
154
+ const tagElement = renderDisplayCell({
155
+ entry: { field: 'status', enableColor: true, columnType: 'tag' },
156
+ record: { color_code: 'green', status: 'Done' },
157
+ refresh: jest.fn(),
158
+ CustomComponents: {},
159
+ });
160
+ expect(tagElement.props.color).toBe('green');
161
+ expect(tagElement.props.children).toBe('Done');
162
+
163
+ const spanElement = renderDisplayCell({
164
+ entry: { field: 'status', enableColor: true, columnType: 'span' },
165
+ record: { color_code: '#ff0000', status: 'Hold' },
166
+ refresh: jest.fn(),
167
+ CustomComponents: {},
168
+ });
169
+ expect(spanElement.type).toBe('span');
170
+ expect(spanElement.props.style.color).toBe('#ff0000');
171
+
172
+ const iconElement = renderDisplayCell({
173
+ entry: {
174
+ field: 'state',
175
+ displayIcons: {
176
+ Pending: {
177
+ icon: 'ClockCircleOutlined',
178
+ color: 'orange',
179
+ size: '14px',
180
+ },
181
+ },
182
+ },
183
+ record: { state: 'Pending' },
184
+ refresh: jest.fn(),
185
+ CustomComponents: {},
186
+ });
187
+ expect(iconElement.props.style.color).toBe('orange');
188
+
189
+ const textElement = renderDisplayCell({
190
+ entry: { field: 'remarks', color: '#333' },
191
+ record: { remarks: 'All good' },
192
+ refresh: jest.fn(),
193
+ CustomComponents: {},
194
+ });
195
+ expect(textElement.type).toBe('span');
196
+ expect(textElement.props.children).toBe('All good');
197
+ });
198
+ });
199
+