ui-soxo-bootstrap-core 2.6.30 → 2.6.32-dev.0

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.
@@ -117,7 +117,8 @@ function CollapsedIconMenu({ menu, collapsed, icon, caption }) {
117
117
  return () => window.removeEventListener('resize', handleResize);
118
118
  }, []);
119
119
 
120
- const menuText = t(caption);
120
+ const hasCaption = typeof caption === 'string' && caption.length > 0;
121
+ const menuText = hasCaption ? t(caption) : '';
121
122
  const menuContent = (
122
123
  <>
123
124
  {/* If value of collapsed is false show caption & icon else hiding caption and showing only icon*/}
@@ -129,9 +130,7 @@ function CollapsedIconMenu({ menu, collapsed, icon, caption }) {
129
130
 
130
131
  <div style={{ color: state.theme.colors.leftSectionColor }}>
131
132
  <span className="caption">
132
- {/* {caption} */}
133
- {/* <Trans i18nKey="Appointments"></Trans> */}
134
- {t(`${caption}`)}
133
+ {menuText}
135
134
  </span>
136
135
  </div>
137
136
  </div>
@@ -142,8 +141,7 @@ function CollapsedIconMenu({ menu, collapsed, icon, caption }) {
142
141
  </span>
143
142
 
144
143
  <span style={{ color: state.theme.colors.colorPrimaryText, paddingLeft: '6px' }}>
145
- {/* <>{caption}</> */}
146
- {t(`${caption}`)}
144
+ {menuText}
147
145
  </span>
148
146
  </div>
149
147
  )}
@@ -151,7 +149,7 @@ function CollapsedIconMenu({ menu, collapsed, icon, caption }) {
151
149
  );
152
150
 
153
151
  // On mobile, or when the menu is collapsed (based on original logic), don't show the popover tooltip.
154
- return isMobile || collapsed ? (
152
+ return isMobile || collapsed || !hasCaption ? (
155
153
  menuContent
156
154
  ) : (
157
155
  <Popover content={menuText} placement="right">
@@ -497,6 +495,11 @@ export default function SideMenu({ loading, modules = [], callback, appSettings,
497
495
  .filter((record) => {
498
496
  icon = record;
499
497
 
498
+ // Drop entries without a caption — they have no permission/label
499
+ // to render, so showing just an icon is misleading.
500
+ const hasCaption = typeof record.caption === 'string' && record.caption.trim().length > 0;
501
+ if (!hasCaption) return false;
502
+
500
503
  if (record.id) {
501
504
  if (record.is_visible) {
502
505
  return true;
@@ -514,7 +517,9 @@ export default function SideMenu({ loading, modules = [], callback, appSettings,
514
517
  .map((menu, index) => {
515
518
  // return <MenuItem menu={menu} index={index} />
516
519
 
517
- let sub_menus = menu && menu.sub_menus ? Menus.screenMenus(menu.sub_menus) : [];
520
+ let sub_menus = menu && menu.sub_menus
521
+ ? Menus.screenMenus(menu.sub_menus).filter((s) => typeof s.caption === 'string' && s.caption.trim().length > 0)
522
+ : [];
518
523
 
519
524
  if (menu && sub_menus && sub_menus.length) {
520
525
  // let randomIndex = parseInt(Math.random() * 10000000000);
@@ -203,6 +203,53 @@ function FormCreator({
203
203
  }
204
204
  }
205
205
 
206
+ const submitFormValues = async (values) => {
207
+ setLoading(true);
208
+
209
+ const nextValues = { ...values };
210
+
211
+ // Keep the same value preparation path for normal submit and search reset.
212
+ fields.forEach((field) => {
213
+
214
+ if (field.field && field.field.includes('date')) {
215
+
216
+ nextValues[field.field] = moment(nextValues[field.field]).valueOf();
217
+
218
+ }
219
+
220
+ if (field.type === ('time')) {
221
+
222
+ nextValues[field.field] = moment(nextValues[field.field]).format('HH:mm A');
223
+
224
+ }
225
+ })
226
+
227
+ try {
228
+ if (onSubmit) {
229
+ await onSubmit(nextValues);
230
+ }
231
+ } finally {
232
+ setLoading(false);
233
+ }
234
+ }
235
+
236
+ const handleSearchReset = async (fieldName) => {
237
+ if (!fieldName) return;
238
+
239
+ const values = {
240
+ ...form.getFieldsValue(true),
241
+ [fieldName]: [],
242
+ };
243
+
244
+ form.setFieldsValue({ [fieldName]: [] });
245
+
246
+ if (onFormValuesChange) {
247
+ onFormValuesChange(values);
248
+ }
249
+
250
+ await submitFormValues(values);
251
+ }
252
+
206
253
  return (
207
254
  <section className="form-creator">
208
255
 
@@ -223,41 +270,7 @@ function FormCreator({
223
270
  {...layoutValue}
224
271
  className="new-record"
225
272
  name="new-record"
226
- onFinish={(values) => {
227
-
228
- setLoading(true);
229
-
230
- // Do a screening to check if date fields are
231
- fields.forEach((field) => {
232
-
233
- if (field.field && field.field.includes('date')) {
234
-
235
- // values[field.field] = new Timestamp(new Date());
236
-
237
- values[field.field] = moment(values[field.field]).valueOf();
238
-
239
- } else {
240
-
241
- }
242
-
243
- if (field.type === ('time')) {
244
-
245
- // values[field.field] = new Timestamp(new Date());
246
-
247
- values[field.field] = moment(values[field.field]).format('HH:mm A');
248
-
249
- } else {
250
-
251
- }
252
- })
253
-
254
- onSubmit(values).then(() => {
255
-
256
- setLoading(false);
257
-
258
- });
259
-
260
- }}
273
+ onFinish={submitFormValues}
261
274
  // layout="inline"
262
275
 
263
276
  onFieldsChange={onFieldsChange}
@@ -276,6 +289,7 @@ function FormCreator({
276
289
  fields={fields}
277
290
  reportId={reportId}
278
291
  onChange={onChange}
292
+ onSearchReset={handleSearchReset}
279
293
  selectedInformation={selectedInformation}
280
294
  onUpload={onUpload}
281
295
  onFieldUpdate={onFieldUpdate}
@@ -311,6 +325,7 @@ function FieldMapper({
311
325
  fields = [],
312
326
  reportId,
313
327
  onChange,
328
+ onSearchReset,
314
329
  selectedInformation,
315
330
  onUpload,
316
331
  onFieldUpdate,
@@ -350,6 +365,7 @@ function FieldMapper({
350
365
  fields={tab.fields}
351
366
  reportId={reportId}
352
367
  onChange={onChange}
368
+ onSearchReset={onSearchReset}
353
369
  onUpload={onUpload}
354
370
  onFieldUpdate={onFieldUpdate}
355
371
  onFieldRemove={onFieldRemove}
@@ -370,6 +386,7 @@ function FieldMapper({
370
386
  ?
371
387
  <UserInput
372
388
  onChange={onChange}
389
+ onSearchReset={onSearchReset}
373
390
  reportId={reportId}
374
391
  index={index}
375
392
  key={index}
@@ -384,6 +401,7 @@ function FieldMapper({
384
401
  } else {
385
402
  return <UserInput
386
403
  onChange={onChange}
404
+ onSearchReset={onSearchReset}
387
405
  reportId={reportId}
388
406
  key={index}
389
407
  selectedInformation={selectedInformation}
@@ -411,7 +429,7 @@ function FieldMapper({
411
429
  *
412
430
  * @param {*} param0
413
431
  */
414
- function UserInput({ field, onUpload, selectedInformation, onChange, onFieldUpdate, onFieldRemove, index, reportId }) {
432
+ function UserInput({ field, onUpload, selectedInformation, onChange, onSearchReset, onFieldUpdate, onFieldRemove, index, reportId }) {
415
433
 
416
434
  let props = {};
417
435
 
@@ -438,7 +456,7 @@ function UserInput({ field, onUpload, selectedInformation, onChange, onFieldUpda
438
456
  switch (field.type) {
439
457
 
440
458
  case 'search':
441
- return <AdvancedSearchSelect {...field} reportId={reportId} style={{ width: '100%' }} />
459
+ return <AdvancedSearchSelect {...field} reportId={reportId} style={{ width: '100%' }} onReset={onSearchReset} />
442
460
 
443
461
  case 'number':
444
462
  return <InputNumber required={field.required} />
@@ -69,7 +69,6 @@ class CoreScript extends Base {
69
69
  // Settings db pointer
70
70
  if (!dbPtr) dbPtr = localStorage.db_ptr;
71
71
  return ApiUtils.post({
72
- // baseUrl: 'http://localhost:8002/dev/',
73
72
  url: `core-scripts/dashboardquery/${id}`,
74
73
  formBody,
75
74
  headers: {
@@ -91,6 +90,20 @@ class CoreScript extends Base {
91
90
  });
92
91
  };
93
92
 
93
+ getCorescript = (formBody,dbPtr) => {
94
+
95
+ if (!dbPtr) dbPtr = localStorage.db_ptr;
96
+ return ApiUtils.post({
97
+ // baseUrl: 'http://localhost:8002/dev/',
98
+ url: `core-scripts/get-core-script`,
99
+ headers: {
100
+ 'Content-Type': 'application/json',
101
+ Authorization: 'Bearer ' + localStorage.access_token,
102
+ db_ptr: dbPtr,
103
+ },
104
+ formBody,
105
+ });
106
+ };
94
107
  getQuery = (formBody) => {
95
108
  return ApiUtils.post({
96
109
  url: `core-scripts/execute-script-api`,
@@ -160,6 +160,29 @@ class MenusAPI extends Base {
160
160
  });
161
161
  };
162
162
 
163
+ getBranches = () => {
164
+ return ApiUtils.get({
165
+ url: 'branches',
166
+ });
167
+ };
168
+
169
+ switchBranch = (formBody, dbPtr) => {
170
+ return ApiUtils.post({
171
+ url: `auth/switch-branch`,
172
+ headers: { db_ptr: dbPtr },
173
+ formBody,
174
+ });
175
+ };
176
+
177
+ getProfile = (token) => {
178
+ return ApiUtils.get({
179
+ url: 'auth/profile',
180
+ headers: {
181
+ Authorization: `Bearer ${token}`,
182
+ },
183
+ });
184
+ };
185
+
163
186
  /**
164
187
  * create menu
165
188
  */
@@ -185,7 +208,6 @@ class MenusAPI extends Base {
185
208
  : 'menus/get-menus'; // NURA
186
209
 
187
210
  if (!dbPtr) dbPtr = localStorage.db_ptr;
188
-
189
211
  return this.get({
190
212
  url,
191
213
  config,
@@ -305,6 +327,12 @@ class MenusAPI extends Base {
305
327
  // }
306
328
  ];
307
329
  };
330
+ // license summary api call
331
+ getSummary = () => {
332
+ return ApiUtils.get({
333
+ url: 'license/summary',
334
+ });
335
+ };
308
336
  }
309
337
 
310
338
  export default MenusAPI;
@@ -140,10 +140,24 @@ export default function AdvancedSearchSelect({ reportId, onReset, field, value,
140
140
  onChange(newValues);
141
141
  };
142
142
 
143
+ const notifyReset = () => {
144
+ if (finalOnReset) {
145
+ finalOnReset(fieldName);
146
+ }
147
+ };
148
+
143
149
  const handleReset = () => {
144
150
  onChange([]);
145
- if (finalOnReset) {
146
- finalOnReset(); // 🔥 trigger dashboard refresh
151
+ notifyReset();
152
+ };
153
+
154
+ const handleSelectChange = (nextValue) => {
155
+ const nextValues = Array.isArray(nextValue) ? nextValue : [];
156
+
157
+ onChange(nextValues);
158
+
159
+ if (safeValue.length > 0 && nextValues.length === 0) {
160
+ notifyReset();
147
161
  }
148
162
  };
149
163
 
@@ -177,7 +191,7 @@ export default function AdvancedSearchSelect({ reportId, onReset, field, value,
177
191
  // Always pass an array back to the parent to be consistent with the Select mode.
178
192
  onChange(text ? [text] : []);
179
193
  if (!text && finalOnReset) {
180
- finalOnReset();
194
+ finalOnReset(fieldName);
181
195
  }
182
196
  }}
183
197
  />
@@ -205,7 +219,7 @@ export default function AdvancedSearchSelect({ reportId, onReset, field, value,
205
219
  }}
206
220
  allowClear
207
221
  maxTagCount={1}
208
- onChange={onChange}
222
+ onChange={handleSelectChange}
209
223
  maxTagPlaceholder={(omittedValues) => (
210
224
  <span className="tag-placeholder-count">
211
225
  +{omittedValues.length}
@@ -1,7 +1,10 @@
1
- import React from 'react';
2
- import { Tag } from 'antd';
1
+ import React, { useState, useContext } from 'react';
2
+ import { Modal, Tag } from 'antd';
3
3
  import * as Icons from '@ant-design/icons';
4
4
  import { Link } from 'react-router-dom';
5
+ import { GlobalContext, safeJSON, Location } from '../../../../../lib';
6
+ // import { PdfViewer } from '../../../../../lib';
7
+ import { CoreScripts } from '../../../../../models';
5
8
 
6
9
  /**
7
10
  * Utilities for rendering Reporting Dashboard display columns.
@@ -74,7 +77,7 @@ export function isActionTypeEntry(entry = {}) {
74
77
  * @param {DisplayRecord} record
75
78
  * @returns {string}
76
79
  */
77
- export function getRedirectLink(entry = {}, record = {}) {
80
+ export function getRedirectLink(entry = {}, record = {}, CustomComponents = {}) {
78
81
  let redirectLink = entry.redirect_link || '';
79
82
 
80
83
  if (Array.isArray(entry.replace_variables)) {
@@ -87,6 +90,41 @@ export function getRedirectLink(entry = {}, record = {}) {
87
90
  return redirectLink;
88
91
  }
89
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
+
90
128
  /**
91
129
  * Resolves action label text with backward-compatible precedence.
92
130
  *
@@ -170,6 +208,154 @@ function renderCustomComponent({ entry, record, CustomComponents, refresh }) {
170
208
  );
171
209
  }
172
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
+ title: 'Switch Branch?', // Adding a title makes it look more standard
340
+ content: 'This record belongs to another branch. Would you like to switch branches to view the details?',
341
+ onOk: () => {
342
+ if (hasTargetBranchAccess) {
343
+ Location.navigate({ url: redirectLink });
344
+ return;
345
+ }
346
+
347
+ Modal.error({
348
+ title: 'Access Denied',
349
+ content: 'This data belongs to another branch.Would you like to switch branches to view the details?',
350
+ });
351
+ },
352
+ });
353
+ }}
354
+ >
355
+ {label}
356
+ </a>
357
+ );
358
+ }
173
359
  /**
174
360
  * Renders table cell content for a configured display column.
175
361
  *
@@ -210,10 +396,21 @@ export function renderDisplayCell({ entry, record, CustomComponents, refresh })
210
396
  }
211
397
  return null;
212
398
  }
213
-
214
399
  if (isLegacyActionEntry(entry) || isActionTypeEntry(entry)) {
400
+ if (entry.redirect_link_type === 'file') {
401
+ return <FileActionLink entry={entry} record={record} CustomComponents={CustomComponents} />;
402
+ }
403
+ const { user = {} } = useContext(GlobalContext);
404
+
215
405
  const redirectLink = getRedirectLink(entry, record);
216
- return <Link to={`${redirectLink}`}>{getActionLabel(entry, record)}</Link>;
406
+ const label = getActionLabel(entry, record);
407
+ const { requiresBranchSwitch, hasTargetBranchAccess } = getBranchNavigationState(entry, record, user);
408
+
409
+ if (requiresBranchSwitch) {
410
+ return <BranchAwareActionLink label={label} redirectLink={redirectLink} hasTargetBranchAccess={hasTargetBranchAccess} />;
411
+ }
412
+
413
+ return <Link to={`${redirectLink}`}>{label}</Link>;
217
414
  }
218
415
 
219
416
  if (entry.field === 'custom') {
@@ -3,6 +3,7 @@ import {
3
3
  isLegacyActionEntry,
4
4
  isActionTypeEntry,
5
5
  getRedirectLink,
6
+ getFileLocation,
6
7
  getActionLabel,
7
8
  renderDisplayCell,
8
9
  } from './display-cell-renderer';
@@ -28,6 +29,13 @@ describe('display-cell-renderer', () => {
28
29
  expect(link).toBe('/bill/10/visit/OP100');
29
30
  });
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
+
31
39
  test('keeps legacy action label behavior unchanged', () => {
32
40
  const entry = {
33
41
  field: 'action',
@@ -77,6 +85,71 @@ describe('display-cell-renderer', () => {
77
85
  expect(element.props.opb_id).toBe(1);
78
86
  });
79
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
+
80
153
  test('renders styled tag/span/icon/text for non-action path', () => {
81
154
  const tagElement = renderDisplayCell({
82
155
  entry: { field: 'status', enableColor: true, columnType: 'tag' },