ui-soxo-bootstrap-core 2.6.56 → 2.6.61
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 +5 -0
- package/core/lib/Store.js +6 -0
- package/core/lib/elements/basic/reference-select/reference-select.js +5 -1
- package/core/modules/reporting/components/reporting-dashboard/reporting-table.js +17 -2
- package/core/modules/steps/steps.js +127 -8
- package/core/modules/steps/steps.scss +17 -0
- package/package.json +1 -1
|
@@ -363,6 +363,11 @@ export default function LandingApi({ history, CustomComponents, CustomModels, ap
|
|
|
363
363
|
dispatch({ type: 'settings', payload: result.result.settings });
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
+
// Role of the logged in user comes along with the menus response (NURA)
|
|
367
|
+
if (result.result.role) {
|
|
368
|
+
dispatch({ type: 'role', payload: result.result.role });
|
|
369
|
+
}
|
|
370
|
+
|
|
366
371
|
// Reports length
|
|
367
372
|
if (reports.length) {
|
|
368
373
|
reportMenus = [
|
package/core/lib/Store.js
CHANGED
|
@@ -73,6 +73,7 @@ const initialState = {
|
|
|
73
73
|
defaultBranch: {},
|
|
74
74
|
theme: initialTheme(), // Set the initial theme from themes.json
|
|
75
75
|
settings: {},
|
|
76
|
+
role: {},
|
|
76
77
|
};
|
|
77
78
|
|
|
78
79
|
/**
|
|
@@ -231,6 +232,7 @@ export const GlobalProvider = ({ children, CustomModels,CustomComponents, appSet
|
|
|
231
232
|
app: app,
|
|
232
233
|
user: state.user,
|
|
233
234
|
settings: state.settings,
|
|
235
|
+
role: state.role,
|
|
234
236
|
dispatch: dispatch,
|
|
235
237
|
twilio: state.twilio,
|
|
236
238
|
isMobile,
|
|
@@ -308,6 +310,10 @@ export const AppReducer = (state, action) => {
|
|
|
308
310
|
case 'settings':
|
|
309
311
|
return { ...state, settings: action.payload };
|
|
310
312
|
|
|
313
|
+
// Role of the logged in user, received along with the menus response
|
|
314
|
+
case 'role':
|
|
315
|
+
return { ...state, role: action.payload };
|
|
316
|
+
|
|
311
317
|
case 'slot':
|
|
312
318
|
return { ...state, user: action.payload };
|
|
313
319
|
|
|
@@ -42,6 +42,7 @@ function ReferenceSelect({
|
|
|
42
42
|
allowClear,
|
|
43
43
|
sortKey, // Key for sorting
|
|
44
44
|
sortOrder = 'asc', // default to ascending,
|
|
45
|
+
dbPtr, // db pointer to load the options from , defaults to the logged in db when not passed
|
|
45
46
|
...componentProps
|
|
46
47
|
}) {
|
|
47
48
|
let [options, setOptions] = useState([]);
|
|
@@ -87,8 +88,11 @@ function ReferenceSelect({
|
|
|
87
88
|
|
|
88
89
|
// Case 1: 'nura-base' mode – used for general fetch via config
|
|
89
90
|
if (mode === 'nura-base') {
|
|
91
|
+
// Point the request to the passed db , else keep the default db_ptr flow
|
|
92
|
+
const requestConfig = dbPtr ? { ...config, headers: { ...config?.headers, db_ptr: dbPtr } } : { ...config };
|
|
93
|
+
|
|
90
94
|
// Fetch data from model using config
|
|
91
|
-
model.get(
|
|
95
|
+
model.get(requestConfig).then((response) => {
|
|
92
96
|
// Extract result from response or fallback to response directly
|
|
93
97
|
let data = response.result || response;
|
|
94
98
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import React, { useState, useEffect, useMemo } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useMemo, useContext } from 'react';
|
|
2
2
|
import { Table, Skeleton, Input, Modal, message, Pagination } from 'antd';
|
|
3
3
|
import { QrcodeOutlined } from '@ant-design/icons';
|
|
4
|
-
import { ExportReactCSV, getExportData, Card, TableComponent, QrScanner } from './../../../../lib/';
|
|
4
|
+
import { ExportReactCSV, getExportData, Card, TableComponent, QrScanner, GlobalContext, safeJSON } from './../../../../lib/';
|
|
5
5
|
import moment from 'moment-timezone';
|
|
6
6
|
import { CoreScripts } from './../../../../models/';
|
|
7
7
|
import Button from '../../../../lib/elements/basic/button/button';
|
|
@@ -93,6 +93,19 @@ export default function ReportingTable({
|
|
|
93
93
|
const [internalConfig, setInternalConfig] = useState({});
|
|
94
94
|
const [internalPagination, setInternalPagination] = useState({ current: 1, pageSize: 20, total: 0 });
|
|
95
95
|
|
|
96
|
+
const { role = {} } = useContext(GlobalContext);
|
|
97
|
+
// When any of the logged in user's roles has `enableUserFilter: true` in its other details,
|
|
98
|
+
// reports receive `enableUserFilter: true` so they can restrict rows to the user's own records.
|
|
99
|
+
// `role` may be a single role object or an array of roles.
|
|
100
|
+
const roles = Array.isArray(role) ? role : role ? [role] : [];
|
|
101
|
+
const isUserFilterEnabled = roles.some((r) => {
|
|
102
|
+
const details = r?.other_details;
|
|
103
|
+
// Some records store other_details with single quotes, e.g. "{'enableUserFilter':true}"
|
|
104
|
+
const parsed = safeJSON(details) || (typeof details === 'string' ? safeJSON(details.replace(/'/g, '"')) : null) || {};
|
|
105
|
+
return parsed.enableUserFilter === true;
|
|
106
|
+
});
|
|
107
|
+
const userFilter = isUserFilterEnabled ? { enableUserFilter: true } : {};
|
|
108
|
+
|
|
96
109
|
// Independent mode is enabled when enough identifiers are present for the
|
|
97
110
|
// table to load its own schema and data instead of relying on parent props.
|
|
98
111
|
const shouldFetchData = !!(requestId || reportId || replacements?.submode || replacements?.mode);
|
|
@@ -189,6 +202,7 @@ export default function ReportingTable({
|
|
|
189
202
|
page: currentPager.current,
|
|
190
203
|
limit: currentPager.pageSize,
|
|
191
204
|
...(requestPayload?.body ? {} : { mode, submode }),
|
|
205
|
+
...userFilter,
|
|
192
206
|
},
|
|
193
207
|
};
|
|
194
208
|
// if (!isNuradesk) {
|
|
@@ -199,6 +213,7 @@ export default function ReportingTable({
|
|
|
199
213
|
...replacements,
|
|
200
214
|
page: currentPager.current,
|
|
201
215
|
limit: currentPager.pageSize,
|
|
216
|
+
...userFilter,
|
|
202
217
|
},
|
|
203
218
|
};
|
|
204
219
|
}
|
|
@@ -34,6 +34,8 @@ import './steps.scss';
|
|
|
34
34
|
const TOUCH_NAV_HIDE_DELAY = 2800;
|
|
35
35
|
const SWIPE_DISTANCE_THRESHOLD = 60;
|
|
36
36
|
const SWIPE_VERTICAL_TOLERANCE = 80;
|
|
37
|
+
const ZOOM_MIN = 1;
|
|
38
|
+
const ZOOM_MAX = 3;
|
|
37
39
|
|
|
38
40
|
/**
|
|
39
41
|
* First-step CTA labels keyed by normalized process name.
|
|
@@ -148,7 +150,11 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
148
150
|
const [realtimeStatus, setRealtimeStatus] = useState('idle');
|
|
149
151
|
const [isTouchDevice, setIsTouchDevice] = useState(false);
|
|
150
152
|
const [touchNavVisible, setTouchNavVisible] = useState(false);
|
|
153
|
+
const [zoom, setZoom] = useState(ZOOM_MIN);
|
|
151
154
|
|
|
155
|
+
const stageBodyRef = useRef(null);
|
|
156
|
+
const zoomLayerRef = useRef(null);
|
|
157
|
+
const pinchRef = useRef(null);
|
|
152
158
|
const narrationUtteranceRef = useRef(null);
|
|
153
159
|
const narrationAudioRef = useRef(null);
|
|
154
160
|
const narrationAudioUrlRef = useRef(null);
|
|
@@ -1139,11 +1145,91 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
1139
1145
|
return;
|
|
1140
1146
|
}
|
|
1141
1147
|
|
|
1148
|
+
if (isStepFullscreen && event.touches.length >= 2) {
|
|
1149
|
+
// Two fingers = pinch. Cancel any pending swipe so it cannot navigate.
|
|
1150
|
+
touchStartRef.current = null;
|
|
1151
|
+
const stage = stageBodyRef.current;
|
|
1152
|
+
const rect = stage.getBoundingClientRect();
|
|
1153
|
+
const [first, second] = [event.touches[0], event.touches[1]];
|
|
1154
|
+
const midX = (first.clientX + second.clientX) / 2 - rect.left;
|
|
1155
|
+
const midY = (first.clientY + second.clientY) / 2 - rect.top;
|
|
1156
|
+
pinchRef.current = {
|
|
1157
|
+
left: rect.left,
|
|
1158
|
+
top: rect.top,
|
|
1159
|
+
distance: getTouchDistance(first, second),
|
|
1160
|
+
scale: zoom,
|
|
1161
|
+
nextScale: zoom,
|
|
1162
|
+
// Content point under the pinch midpoint, in unscaled coordinates.
|
|
1163
|
+
contentX: (stage.scrollLeft + midX) / zoom,
|
|
1164
|
+
contentY: (stage.scrollTop + midY) / zoom,
|
|
1165
|
+
};
|
|
1166
|
+
// Pinch writes to the DOM directly (no React re-render per frame).
|
|
1167
|
+
stage.style.overflow = 'auto';
|
|
1168
|
+
zoomLayerRef.current.style.willChange = 'transform';
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
if (pinchRef.current || (isStepFullscreen && zoom > 1)) {
|
|
1173
|
+
// Zoomed in: horizontal drags scroll the content, not swipe between steps.
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1142
1177
|
const touch = event.touches[0];
|
|
1143
1178
|
touchStartRef.current = { x: touch.clientX, y: touch.clientY };
|
|
1144
1179
|
revealTouchNav();
|
|
1145
1180
|
};
|
|
1146
1181
|
|
|
1182
|
+
const getTouchDistance = (first, second) => Math.hypot(first.clientX - second.clientX, first.clientY - second.clientY) || 1;
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* onTouchMove for the stage body (full screen pinch-to-zoom).
|
|
1186
|
+
* - Scale follows the finger distance, clamped to ZOOM_MIN..ZOOM_MAX.
|
|
1187
|
+
* - Content is scaled from its top-left; the stage body scrolls natively
|
|
1188
|
+
* while zoomed, so scroll is set to keep the pinch midpoint anchored.
|
|
1189
|
+
* - Transform and scroll are written straight to the DOM; React state is
|
|
1190
|
+
* committed once, on touch end, so the heavy step tree is not re-rendered
|
|
1191
|
+
* on every touchmove.
|
|
1192
|
+
*/
|
|
1193
|
+
const handleStageTouchMove = (event) => {
|
|
1194
|
+
const pinch = pinchRef.current;
|
|
1195
|
+
if (!pinch || !isStepFullscreen || event.touches.length < 2) {
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
const stage = stageBodyRef.current;
|
|
1200
|
+
const [first, second] = [event.touches[0], event.touches[1]];
|
|
1201
|
+
const midX = (first.clientX + second.clientX) / 2 - pinch.left;
|
|
1202
|
+
const midY = (first.clientY + second.clientY) / 2 - pinch.top;
|
|
1203
|
+
const nextScale = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, pinch.scale * (getTouchDistance(first, second) / pinch.distance)));
|
|
1204
|
+
|
|
1205
|
+
pinch.nextScale = nextScale;
|
|
1206
|
+
zoomLayerRef.current.style.transform = `scale(${nextScale})`;
|
|
1207
|
+
stage.scrollLeft = pinch.contentX * nextScale - midX;
|
|
1208
|
+
stage.scrollTop = pinch.contentY * nextScale - midY;
|
|
1209
|
+
};
|
|
1210
|
+
|
|
1211
|
+
/** Commits the pinch result to state and hands styling back to React. */
|
|
1212
|
+
const finishPinch = () => {
|
|
1213
|
+
const pinch = pinchRef.current;
|
|
1214
|
+
pinchRef.current = null;
|
|
1215
|
+
if (!pinch) {
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const stage = stageBodyRef.current;
|
|
1220
|
+
const layer = zoomLayerRef.current;
|
|
1221
|
+
// Snap tiny zooms back to 100% so the view never rests at ~1.01.
|
|
1222
|
+
const finalScale = pinch.nextScale < 1.05 ? ZOOM_MIN : pinch.nextScale;
|
|
1223
|
+
stage.style.overflow = '';
|
|
1224
|
+
layer.style.willChange = '';
|
|
1225
|
+
layer.style.transform = finalScale === 1 ? '' : `scale(${finalScale})`;
|
|
1226
|
+
if (finalScale === 1) {
|
|
1227
|
+
stage.scrollLeft = 0;
|
|
1228
|
+
stage.scrollTop = 0;
|
|
1229
|
+
}
|
|
1230
|
+
setZoom(finalScale);
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1147
1233
|
/**
|
|
1148
1234
|
* onTouchEnd for the stage body.
|
|
1149
1235
|
* - Computes the horizontal/vertical delta against the stored touch origin.
|
|
@@ -1155,6 +1241,13 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
1155
1241
|
* the user can continue tapping if they prefer.
|
|
1156
1242
|
*/
|
|
1157
1243
|
const handleStageTouchEnd = (event) => {
|
|
1244
|
+
if (pinchRef.current) {
|
|
1245
|
+
// Pinch ends when a finger lifts; the leftover finger must not trigger a swipe.
|
|
1246
|
+
finishPinch();
|
|
1247
|
+
touchStartRef.current = null;
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1158
1251
|
const start = touchStartRef.current;
|
|
1159
1252
|
touchStartRef.current = null;
|
|
1160
1253
|
|
|
@@ -1188,6 +1281,19 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
1188
1281
|
}
|
|
1189
1282
|
};
|
|
1190
1283
|
|
|
1284
|
+
/**
|
|
1285
|
+
* Zoom is per step and full-screen only: reset when leaving full screen or
|
|
1286
|
+
* moving to another step so the next view never opens pre-zoomed.
|
|
1287
|
+
*/
|
|
1288
|
+
useEffect(() => {
|
|
1289
|
+
pinchRef.current = null;
|
|
1290
|
+
if (stageBodyRef.current) {
|
|
1291
|
+
stageBodyRef.current.scrollLeft = 0;
|
|
1292
|
+
stageBodyRef.current.scrollTop = 0;
|
|
1293
|
+
}
|
|
1294
|
+
setZoom(ZOOM_MIN);
|
|
1295
|
+
}, [isStepFullscreen, activeStep]);
|
|
1296
|
+
|
|
1191
1297
|
/**
|
|
1192
1298
|
* Cleanup any pending auto-hide timeout on unmount so the callback cannot
|
|
1193
1299
|
* fire against a stale component and emit a React warning.
|
|
@@ -1446,9 +1552,12 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
1446
1552
|
desktop event trees untouched.
|
|
1447
1553
|
*/}
|
|
1448
1554
|
<div
|
|
1449
|
-
|
|
1555
|
+
ref={stageBodyRef}
|
|
1556
|
+
className={`steps-stage-body${isTouchDevice ? ' is-swipe-enabled' : ''}${isStepFullscreen && zoom > 1 ? ' is-zoomed' : ''}`}
|
|
1450
1557
|
onTouchStart={isTouchDevice ? handleStageTouchStart : undefined}
|
|
1558
|
+
onTouchMove={isTouchDevice ? handleStageTouchMove : undefined}
|
|
1451
1559
|
onTouchEnd={isTouchDevice ? handleStageTouchEnd : undefined}
|
|
1560
|
+
onTouchCancel={isTouchDevice ? handleStageTouchEnd : undefined}
|
|
1452
1561
|
>
|
|
1453
1562
|
{/*
|
|
1454
1563
|
Floating prev/next arrow buttons.
|
|
@@ -1505,14 +1614,24 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
1505
1614
|
</div> */}
|
|
1506
1615
|
|
|
1507
1616
|
{/* <div className="steps-chat-step-component"> */}
|
|
1508
|
-
{
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1617
|
+
{/*
|
|
1618
|
+
Zoom layer: scaled via transform only in full screen, so
|
|
1619
|
+
the normal view keeps its untouched layout.
|
|
1620
|
+
*/}
|
|
1621
|
+
<div
|
|
1622
|
+
ref={zoomLayerRef}
|
|
1623
|
+
className="steps-zoom-layer"
|
|
1624
|
+
style={isStepFullscreen && zoom !== 1 ? { transform: `scale(${zoom})` } : undefined}
|
|
1625
|
+
>
|
|
1626
|
+
{loading ? (
|
|
1627
|
+
<div className="steps-chat-loading">
|
|
1628
|
+
<div className="steps-chat-loading__card">
|
|
1629
|
+
<Skeleton active title={{ width: '32%' }} paragraph={{ rows: 6, width: ['90%', '82%', '86%', '70%', '78%', '60%'] }} />
|
|
1630
|
+
</div>
|
|
1512
1631
|
</div>
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1632
|
+
) : null}
|
|
1633
|
+
{!loading ? renderDynamicStep() : null}
|
|
1634
|
+
</div>
|
|
1516
1635
|
{/* </div> */}
|
|
1517
1636
|
{/* </div> */}
|
|
1518
1637
|
</div>
|
|
@@ -477,6 +477,23 @@
|
|
|
477
477
|
.steps-stage-body.is-swipe-enabled {
|
|
478
478
|
touch-action: pan-y;
|
|
479
479
|
}
|
|
480
|
+
/* Zoomed in (full screen): scroll natively in both axes. */
|
|
481
|
+
.steps-stage-body.is-zoomed {
|
|
482
|
+
overflow: auto;
|
|
483
|
+
}
|
|
484
|
+
.steps-stage-body.is-swipe-enabled.is-zoomed {
|
|
485
|
+
touch-action: pan-x pan-y;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/* Wrapper for step content; transformed for full-screen zoom. */
|
|
489
|
+
.steps-zoom-layer {
|
|
490
|
+
flex: 1 1 auto;
|
|
491
|
+
min-height: 0;
|
|
492
|
+
min-width: 0;
|
|
493
|
+
display: flex;
|
|
494
|
+
flex-direction: column;
|
|
495
|
+
transform-origin: 0 0;
|
|
496
|
+
}
|
|
480
497
|
|
|
481
498
|
/* ── Touch-device floating nav arrows ───────────────────── */
|
|
482
499
|
|