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.
- package/core/components/index.js +2 -11
- package/core/components/landing-api/landing-api.js +165 -5
- package/core/components/license-management/license-alert.js +97 -0
- package/core/lib/components/global-header/global-header.js +16 -76
- package/core/lib/components/index.js +2 -2
- package/core/lib/components/sidemenu/sidemenu.js +13 -8
- package/core/lib/models/forms/components/form-creator/form-creator.js +55 -37
- package/core/models/core-scripts/core-scripts.js +14 -1
- package/core/models/menus/menus.js +29 -1
- package/core/modules/reporting/components/reporting-dashboard/adavance-search/advance-search.js +18 -4
- package/core/modules/reporting/components/reporting-dashboard/display-columns/display-cell-renderer.js +202 -5
- package/core/modules/reporting/components/reporting-dashboard/display-columns/display-cell-renderer.test.js +73 -0
- package/core/modules/reporting/components/reporting-dashboard/reporting-dashboard.js +161 -531
- package/core/modules/reporting/components/reporting-dashboard/reporting-table.js +519 -0
- package/package.json +1 -1
|
@@ -117,7 +117,8 @@ function CollapsedIconMenu({ menu, collapsed, icon, caption }) {
|
|
|
117
117
|
return () => window.removeEventListener('resize', handleResize);
|
|
118
118
|
}, []);
|
|
119
119
|
|
|
120
|
-
const
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
|
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={
|
|
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;
|
package/core/modules/reporting/components/reporting-dashboard/adavance-search/advance-search.js
CHANGED
|
@@ -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
|
-
|
|
146
|
-
|
|
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={
|
|
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
|
-
|
|
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' },
|