ui-soxo-bootstrap-core 2.6.46-dev.4 → 2.6.46-dev.6
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/landing-api/landing-api.js +124 -14
- package/core/lib/components/index.js +2 -2
- package/core/lib/modules/generic/generic-list/ExportReactCSV.js +15 -3
- package/core/models/core-scripts/core-scripts.js +14 -1
- package/core/models/menus/menus.js +23 -1
- package/core/modules/reporting/components/reporting-dashboard/display-columns/display-cell-renderer.js +201 -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 +54 -540
- package/core/modules/reporting/components/reporting-dashboard/reporting-table.js +517 -0
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { useContext, useEffect, useRef, useState } from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { Route, Switch } from 'react-router-dom';
|
|
4
4
|
|
|
5
|
-
import { Skeleton } from 'antd';
|
|
5
|
+
import { Skeleton, message, Modal } from 'antd';
|
|
6
6
|
|
|
7
7
|
import { Card, ChangePassword, GlobalContext, GlobalHeader, ModuleRoutes, Profile, SettingsUtil, SpotlightSearch, useTranslation } from '../../lib';
|
|
8
8
|
|
|
@@ -46,7 +46,6 @@ function getRandomMessage(previousMessage = '') {
|
|
|
46
46
|
*/
|
|
47
47
|
export default function LandingApi({ history, CustomComponents, CustomModels, appSettings, transitionPending = false, onHomeReady, ...props }) {
|
|
48
48
|
const [loader, setLoader] = useState(false);
|
|
49
|
-
|
|
50
49
|
// const [modules, setModules] = useState([]);
|
|
51
50
|
|
|
52
51
|
const [connected] = useState();
|
|
@@ -66,7 +65,67 @@ export default function LandingApi({ history, CustomComponents, CustomModels, ap
|
|
|
66
65
|
|
|
67
66
|
var config = {};
|
|
68
67
|
|
|
69
|
-
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
// Variable decides the control of homepage
|
|
71
|
+
// #TODO This is a temporary fix - Homemage
|
|
72
|
+
|
|
73
|
+
let disableHomepage = false;
|
|
74
|
+
|
|
75
|
+
if (process.env.REACT_APP_DISABLEHOMEPAGE) {
|
|
76
|
+
disableHomepage = JSON.parse(process.env.REACT_APP_DISABLEHOMEPAGE);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Normalizes the user's branch access list from `organization_details`.
|
|
81
|
+
*
|
|
82
|
+
* The API can return `organization_details` as a JSON string, so we always
|
|
83
|
+
* parse it through `safeJSON` before reading the branch collection.
|
|
84
|
+
*
|
|
85
|
+
* @returns {Array} List of branches the current user can access.
|
|
86
|
+
*/
|
|
87
|
+
const getAccessibleBranches = () => {
|
|
88
|
+
const orgDetails = safeJSON(user?.organization_details);
|
|
89
|
+
return Array.isArray(orgDetails?.branch) ? orgDetails.branch : [];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolves the currently selected branch record using the persisted db pointer.
|
|
94
|
+
*
|
|
95
|
+
* @param {Array} branches
|
|
96
|
+
* @returns {Object|null}
|
|
97
|
+
*/
|
|
98
|
+
const getCurrentBranchRecord = (branches) => {
|
|
99
|
+
const currentDbPtr = localStorage.getItem('db_ptr');
|
|
100
|
+
return branches.find((branch) => String(branch.dbPtr) === String(currentDbPtr)) || null;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolves the target branch from the URL `index` query parameter.
|
|
105
|
+
*
|
|
106
|
+
* @param {Array} branches
|
|
107
|
+
* @param {string|null} branchId
|
|
108
|
+
* @returns {Object|null}
|
|
109
|
+
*/
|
|
110
|
+
const getBranchRecordById = (branches, branchId) => {
|
|
111
|
+
return branches.find((branch) => String(branch.branch_id) === String(branchId)) || null;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Persists branch-specific auth data after a successful switch.
|
|
116
|
+
*
|
|
117
|
+
* @param {Object} tokenBundle
|
|
118
|
+
* @param {string} dbPtr
|
|
119
|
+
*/
|
|
120
|
+
const persistBranchSession = (tokenBundle, dbPtr) => {
|
|
121
|
+
const accessToken = tokenBundle?.access_token;
|
|
122
|
+
const refreshToken = tokenBundle?.refresh_token;
|
|
123
|
+
|
|
124
|
+
if (accessToken) localStorage.setItem('access_token', accessToken);
|
|
125
|
+
if (refreshToken) localStorage.setItem('refresh_token', refreshToken);
|
|
126
|
+
if (dbPtr) localStorage.setItem('db_ptr', dbPtr);
|
|
127
|
+
};
|
|
128
|
+
|
|
70
129
|
const fetchSummary = async () => {
|
|
71
130
|
try {
|
|
72
131
|
const res = await MenusAPI.getSummary();
|
|
@@ -81,16 +140,6 @@ export default function LandingApi({ history, CustomComponents, CustomModels, ap
|
|
|
81
140
|
console.error(err);
|
|
82
141
|
}
|
|
83
142
|
};
|
|
84
|
-
|
|
85
|
-
// Variable decides the control of homepage
|
|
86
|
-
// #TODO This is a temporary fix - Homemage
|
|
87
|
-
|
|
88
|
-
let disableHomepage = false;
|
|
89
|
-
|
|
90
|
-
if (process.env.REACT_APP_DISABLEHOMEPAGE) {
|
|
91
|
-
disableHomepage = JSON.parse(process.env.REACT_APP_DISABLEHOMEPAGE);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
143
|
// useEffect(() => {
|
|
95
144
|
|
|
96
145
|
// // Initialize the menus for the logged in user
|
|
@@ -106,6 +155,67 @@ export default function LandingApi({ history, CustomComponents, CustomModels, ap
|
|
|
106
155
|
// }
|
|
107
156
|
// }, [loader]);
|
|
108
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Synchronizes the active branch with the `index` query parameter.
|
|
160
|
+
*
|
|
161
|
+
* Flow:
|
|
162
|
+
* 1. Read the target branch id from the URL.
|
|
163
|
+
* 2. Compare it against the branch represented by the current `db_ptr`.
|
|
164
|
+
* 3. Switch branch only when the user has access and the branch actually differs.
|
|
165
|
+
* 4. Refresh auth/profile state and reload menus for the new branch context.
|
|
166
|
+
*/
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
const handleUrlBranchSwitch = async () => {
|
|
169
|
+
const searchParams = new URLSearchParams(history.location.search);
|
|
170
|
+
const urlDbPtr = searchParams.get('index');
|
|
171
|
+
if (!urlDbPtr) return;
|
|
172
|
+
|
|
173
|
+
const accessibleBranches = getAccessibleBranches();
|
|
174
|
+
const currentBranch = getCurrentBranchRecord(accessibleBranches);
|
|
175
|
+
const targetBranch = getBranchRecordById(accessibleBranches, urlDbPtr);
|
|
176
|
+
|
|
177
|
+
if (!targetBranch || String(currentBranch?.branch_id) === String(urlDbPtr)) return;
|
|
178
|
+
|
|
179
|
+
setLoader(true);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const switchResult = await MenusAPI.switchBranch(
|
|
183
|
+
{
|
|
184
|
+
firm_id: targetBranch.firm_ptr,
|
|
185
|
+
branch_id: targetBranch.branch_id,
|
|
186
|
+
},
|
|
187
|
+
targetBranch.dbPtr
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
if (!switchResult?.success) {
|
|
191
|
+
Modal.error({
|
|
192
|
+
title: 'Branch Switch Failed',
|
|
193
|
+
content: switchResult?.message || 'An error occurred while attempting to switch branches.',
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
persistBranchSession(switchResult?.token, targetBranch.dbPtr);
|
|
199
|
+
window.dispatchEvent(new CustomEvent('branchChanged', { detail: targetBranch.dbPtr }));
|
|
200
|
+
|
|
201
|
+
const accessToken = switchResult?.token?.access_token;
|
|
202
|
+
const profileResult = await MenusAPI.getProfile(accessToken);
|
|
203
|
+
const updatedUser = { ...profileResult, loggedCheckDone: true };
|
|
204
|
+
|
|
205
|
+
dispatch({ type: 'user', payload: updatedUser });
|
|
206
|
+
localStorage.setItem('userInfo', JSON.stringify(updatedUser));
|
|
207
|
+
|
|
208
|
+
await initializeUserMenus();
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error('Auto branch switch failed:', error);
|
|
211
|
+
} finally {
|
|
212
|
+
setLoader(false);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
if (user?.id && history?.location) handleUrlBranchSwitch();
|
|
217
|
+
}, [history?.location?.search, user?.id]);
|
|
218
|
+
|
|
109
219
|
useEffect(() => {
|
|
110
220
|
// Initialize the menus for the logged in user
|
|
111
221
|
initializeUserMenus();
|
|
@@ -183,7 +293,7 @@ export default function LandingApi({ history, CustomComponents, CustomModels, ap
|
|
|
183
293
|
setLoader(true);
|
|
184
294
|
|
|
185
295
|
// setReports(report)
|
|
186
|
-
|
|
296
|
+
fetchSummary();
|
|
187
297
|
const result = await MenusAPI.getMenus(user);
|
|
188
298
|
|
|
189
299
|
// console.log(result);
|
|
@@ -109,7 +109,7 @@ import ConsentComponent from './consent/consent'
|
|
|
109
109
|
import TaskOverviewLegacy from './../models/process/components/task-overview-legacy/task-overview-legacy'
|
|
110
110
|
|
|
111
111
|
import ReportingDashboard from '../../modules/reporting/components/reporting-dashboard/reporting-dashboard';
|
|
112
|
-
|
|
112
|
+
import ReportingTable from '../../modules/reporting/components/reporting-dashboard/reporting-table'
|
|
113
113
|
import ProcessStepsPage from '../../modules/steps/steps'
|
|
114
114
|
export {
|
|
115
115
|
|
|
@@ -198,7 +198,7 @@ export {
|
|
|
198
198
|
TaskOverviewLegacy,
|
|
199
199
|
// WebCamera,
|
|
200
200
|
ConsentComponent,
|
|
201
|
-
|
|
201
|
+
ReportingTable,
|
|
202
202
|
ReportingDashboard,
|
|
203
203
|
ProcessStepsPage
|
|
204
204
|
|
|
@@ -143,10 +143,22 @@ export const ExportReactCSV = ({
|
|
|
143
143
|
const sanitizeValue = (value) => {
|
|
144
144
|
if (value === undefined || value === null) return '';
|
|
145
145
|
if (typeof value === 'object') return JSON.stringify(value);
|
|
146
|
-
|
|
146
|
+
// Normalize all line-ending variants (CRLF/CR) to LF so a lone "\r"
|
|
147
|
+
// never reaches pdf-lib's WinAnsi encoder, which cannot encode it.
|
|
148
|
+
return String(value).replace(/\r\n|\r/g, '\n');
|
|
147
149
|
};
|
|
148
150
|
|
|
149
|
-
const getTextWidth = (text, textFont, size) =>
|
|
151
|
+
const getTextWidth = (text, textFont, size) => {
|
|
152
|
+
const sanitized = sanitizeValue(text);
|
|
153
|
+
// widthOfTextAtSize measures a single line; strip any remaining
|
|
154
|
+
// newlines defensively so multi-line values never crash the encoder.
|
|
155
|
+
return textFont.widthOfTextAtSize(sanitized.replace(/\n/g, ' '), size);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const getMaxLineWidth = (value, textFont, size) =>
|
|
159
|
+
sanitizeValue(value)
|
|
160
|
+
.split('\n')
|
|
161
|
+
.reduce((maxWidth, line) => Math.max(maxWidth, textFont.widthOfTextAtSize(line, size)), 0);
|
|
150
162
|
|
|
151
163
|
const splitLongWord = (word, maxWidth, textFont, size) => {
|
|
152
164
|
const characters = Array.from(word);
|
|
@@ -223,7 +235,7 @@ export const ExportReactCSV = ({
|
|
|
223
235
|
const compactColumn = label === '#' || ['slno', 'sl no'].includes(label.toLowerCase());
|
|
224
236
|
const minWidth = compactColumn ? 28 : 36;
|
|
225
237
|
const values = [label, ...sampleRows.map((row) => sanitizeValue(row[header.key]))];
|
|
226
|
-
const longestValueWidth = values.reduce((maxWidth, value) => Math.max(maxWidth,
|
|
238
|
+
const longestValueWidth = values.reduce((maxWidth, value) => Math.max(maxWidth, getMaxLineWidth(value, font, fontSize)), 0);
|
|
227
239
|
const longestWordWidth = values.reduce((maxWidth, value) => {
|
|
228
240
|
const words = value.split(/\s+/).filter(Boolean);
|
|
229
241
|
return words.reduce((wordMaxWidth, word) => Math.max(wordMaxWidth, getTextWidth(word, font, fontSize)), maxWidth);
|
|
@@ -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: {
|
|
@@ -116,6 +115,20 @@ class CoreScript extends Base {
|
|
|
116
115
|
});
|
|
117
116
|
};
|
|
118
117
|
|
|
118
|
+
getCorescript = (formBody,dbPtr) => {
|
|
119
|
+
|
|
120
|
+
if (!dbPtr) dbPtr = localStorage.db_ptr;
|
|
121
|
+
return ApiUtils.post({
|
|
122
|
+
// baseUrl: 'http://localhost:8002/dev/',
|
|
123
|
+
url: `core-scripts/get-core-script`,
|
|
124
|
+
headers: {
|
|
125
|
+
'Content-Type': 'application/json',
|
|
126
|
+
Authorization: 'Bearer ' + localStorage.access_token,
|
|
127
|
+
db_ptr: dbPtr,
|
|
128
|
+
},
|
|
129
|
+
formBody,
|
|
130
|
+
});
|
|
131
|
+
};
|
|
119
132
|
/**
|
|
120
133
|
*
|
|
121
134
|
*/
|
|
@@ -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,
|
|
@@ -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,153 @@ 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
|
+
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
|
+
}
|
|
173
358
|
/**
|
|
174
359
|
* Renders table cell content for a configured display column.
|
|
175
360
|
*
|
|
@@ -210,10 +395,21 @@ export function renderDisplayCell({ entry, record, CustomComponents, refresh })
|
|
|
210
395
|
}
|
|
211
396
|
return null;
|
|
212
397
|
}
|
|
213
|
-
|
|
214
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
|
+
|
|
215
404
|
const redirectLink = getRedirectLink(entry, record);
|
|
216
|
-
|
|
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>;
|
|
217
413
|
}
|
|
218
414
|
|
|
219
415
|
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' },
|