ui-soxo-bootstrap-core 2.6.29 → 2.6.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/lib/components/global-header/global-header.scss +6 -6
- package/core/lib/components/sidemenu/sidemenu.js +133 -3
- package/core/lib/components/sidemenu/sidemenu.scss +40 -3
- package/core/lib/models/forms/components/form-creator/form-creator.js +55 -37
- package/core/lib/pages/login/login.js +63 -42
- package/core/lib/utils/font-awesome.utils.js +168 -0
- package/core/modules/reporting/components/reporting-dashboard/adavance-search/advance-search.js +18 -4
- package/core/modules/reporting/components/reporting-dashboard/reporting-dashboard.js +18 -4
- package/package.json +1 -1
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
&.open {
|
|
84
|
-
width:
|
|
84
|
+
width: var(--sidemenu-width, 211px);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
&.close {
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
transition: 0.3s;
|
|
93
93
|
|
|
94
94
|
&.open {
|
|
95
|
-
width:
|
|
95
|
+
width: var(--sidemenu-width, 211px);
|
|
96
96
|
transition: 0.1s;
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
@media only screen and (min-width: 800px) {
|
|
112
|
-
width:
|
|
112
|
+
width: var(--sidemenu-width, 211px);
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
overflow-x: scroll;
|
|
127
127
|
position: relative;
|
|
128
128
|
padding: 15px;
|
|
129
|
-
margin-left:
|
|
129
|
+
margin-left: calc(var(--sidemenu-width, 211px) + 1px);
|
|
130
130
|
|
|
131
131
|
&.kioskon {
|
|
132
132
|
overflow-x: hidden;
|
|
@@ -213,9 +213,9 @@
|
|
|
213
213
|
padding: 15px;
|
|
214
214
|
|
|
215
215
|
&.open {
|
|
216
|
-
width: calc(100% -
|
|
216
|
+
width: calc(100% - (var(--sidemenu-width, 211px) + 1px));
|
|
217
217
|
transition: 0.1s;
|
|
218
|
-
left:
|
|
218
|
+
left: calc(var(--sidemenu-width, 211px) + 1px);
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
@media only screen and (max-width: 768px) {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import React, { useState, useEffect, useContext } from 'react';
|
|
11
|
+
import React, { useState, useEffect, useContext, useCallback, useRef } from 'react';
|
|
12
12
|
|
|
13
13
|
import { animationControls, motion, useAnimation } from 'framer-motion';
|
|
14
14
|
|
|
@@ -24,10 +24,75 @@ import { useTranslation, Trans } from 'react-i18next';
|
|
|
24
24
|
|
|
25
25
|
import { Menus } from '../../models';
|
|
26
26
|
|
|
27
|
+
import { expandFaAliases, ensureFontAwesomeAvailable } from '../../utils/font-awesome.utils';
|
|
28
|
+
|
|
27
29
|
import './sidemenu.scss';
|
|
28
30
|
|
|
29
31
|
const { SubMenu } = Menu;
|
|
30
32
|
|
|
33
|
+
const SIDEMENU_WIDTH_KEY = 'soxo:sidemenu-width';
|
|
34
|
+
const DEFAULT_SIDEMENU_WIDTH = 211;
|
|
35
|
+
const MIN_SIDEMENU_WIDTH = 180;
|
|
36
|
+
const MAX_SIDEMENU_WIDTH = 420;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Renders the menu's image if present, falling back to the Font Awesome icon.
|
|
40
|
+
* The FA icon is rendered immediately so something is always visible during
|
|
41
|
+
* a slow image fetch (e.g. cold reload, route change before the image is
|
|
42
|
+
* cached). The image takes over only after it successfully loads; if it
|
|
43
|
+
* fails, the FA icon stays.
|
|
44
|
+
*/
|
|
45
|
+
function MenuIcon({ menu, icon, size = 25 }) {
|
|
46
|
+
const src = menu && typeof menu.image_path === 'string' ? menu.image_path.trim() : '';
|
|
47
|
+
const [imageStatus, setImageStatus] = useState(src ? 'loading' : 'none');
|
|
48
|
+
const imgRef = useRef(null);
|
|
49
|
+
|
|
50
|
+
// Reset and probe whenever src changes. This covers two cases that were
|
|
51
|
+
// breaking icon rendering on direct URL navigation / reload:
|
|
52
|
+
// 1. Cached images: the browser may fire `load` synchronously before
|
|
53
|
+
// React attaches the onLoad handler, leaving status stuck at
|
|
54
|
+
// 'loading'. We re-check `img.complete` after mount.
|
|
55
|
+
// 2. Broken responses (200 with empty/corrupt body, CSP blocked, etc.)
|
|
56
|
+
// that fire `load` but with naturalWidth=0 — treat as failed so the
|
|
57
|
+
// Font Awesome fallback shows instead of an invisible <img>.
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!src) {
|
|
60
|
+
setImageStatus('none');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const img = imgRef.current;
|
|
64
|
+
if (img && img.complete) {
|
|
65
|
+
setImageStatus(img.naturalWidth > 0 ? 'loaded' : 'failed');
|
|
66
|
+
} else {
|
|
67
|
+
setImageStatus('loading');
|
|
68
|
+
}
|
|
69
|
+
}, [src]);
|
|
70
|
+
|
|
71
|
+
const handleLoad = () => {
|
|
72
|
+
const img = imgRef.current;
|
|
73
|
+
setImageStatus(img && img.naturalWidth > 0 ? 'loaded' : 'failed');
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const showImage = src && imageStatus !== 'failed';
|
|
77
|
+
const showIcon = !showImage || imageStatus !== 'loaded';
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<>
|
|
81
|
+
{showIcon && <i className={`fa-solid fas ${expandFaAliases(icon)}`} />}
|
|
82
|
+
{showImage && (
|
|
83
|
+
<img
|
|
84
|
+
ref={imgRef}
|
|
85
|
+
style={{ width: size, display: imageStatus === 'loaded' ? 'inline-block' : 'none' }}
|
|
86
|
+
src={src}
|
|
87
|
+
alt=""
|
|
88
|
+
onLoad={handleLoad}
|
|
89
|
+
onError={() => setImageStatus('failed')}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
</>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
31
96
|
/**
|
|
32
97
|
*
|
|
33
98
|
* @param {*} collapsed
|
|
@@ -59,7 +124,7 @@ function CollapsedIconMenu({ menu, collapsed, icon, caption }) {
|
|
|
59
124
|
{!collapsed ? (
|
|
60
125
|
<div className="menu-collapsed">
|
|
61
126
|
<div>
|
|
62
|
-
|
|
127
|
+
<MenuIcon menu={menu} icon={icon} />
|
|
63
128
|
</div>
|
|
64
129
|
|
|
65
130
|
<div style={{ color: state.theme.colors.leftSectionColor }}>
|
|
@@ -73,7 +138,7 @@ function CollapsedIconMenu({ menu, collapsed, icon, caption }) {
|
|
|
73
138
|
) : (
|
|
74
139
|
<div className="menu-collapsed">
|
|
75
140
|
<span className="anticon">
|
|
76
|
-
|
|
141
|
+
<MenuIcon menu={menu} icon={icon} />
|
|
77
142
|
</span>
|
|
78
143
|
|
|
79
144
|
<span style={{ color: state.theme.colors.colorPrimaryText, paddingLeft: '6px' }}>
|
|
@@ -106,8 +171,62 @@ export default function SideMenu({ loading, modules = [], callback, appSettings,
|
|
|
106
171
|
const [openKeys, setOpenKeys] = useState([]);
|
|
107
172
|
const [menu, setMenu] = useState({});
|
|
108
173
|
|
|
174
|
+
const [sidebarWidth, setSidebarWidth] = useState(() => {
|
|
175
|
+
const saved = parseInt(typeof window !== 'undefined' ? localStorage.getItem(SIDEMENU_WIDTH_KEY) : null, 10);
|
|
176
|
+
return Number.isFinite(saved) && saved >= MIN_SIDEMENU_WIDTH && saved <= MAX_SIDEMENU_WIDTH ? saved : DEFAULT_SIDEMENU_WIDTH;
|
|
177
|
+
});
|
|
178
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
179
|
+
|
|
109
180
|
const { user = { locations: [] }, dispatch, state } = useContext(GlobalContext);
|
|
110
181
|
|
|
182
|
+
// Expose the current expanded sidebar width as a CSS variable so the
|
|
183
|
+
// surrounding layout (right-section, page-wrapper) can follow the drag.
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (typeof document === 'undefined') return;
|
|
186
|
+
document.documentElement.style.setProperty('--sidemenu-width', `${sidebarWidth}px`);
|
|
187
|
+
}, [sidebarWidth]);
|
|
188
|
+
|
|
189
|
+
// Make sure Font Awesome is actually rendering glyphs in this app; if the
|
|
190
|
+
// host hasn't loaded it (or shipped a broken build), inject FA6 from a CDN
|
|
191
|
+
// so menu icons appear.
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
ensureFontAwesomeAvailable();
|
|
194
|
+
}, []);
|
|
195
|
+
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (typeof window === 'undefined') return;
|
|
198
|
+
localStorage.setItem(SIDEMENU_WIDTH_KEY, String(sidebarWidth));
|
|
199
|
+
}, [sidebarWidth]);
|
|
200
|
+
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (!isDragging) return;
|
|
203
|
+
|
|
204
|
+
const handleMouseMove = (e) => {
|
|
205
|
+
const next = Math.min(Math.max(e.clientX, MIN_SIDEMENU_WIDTH), MAX_SIDEMENU_WIDTH);
|
|
206
|
+
setSidebarWidth(next);
|
|
207
|
+
};
|
|
208
|
+
const handleMouseUp = () => {
|
|
209
|
+
setIsDragging(false);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
document.body.style.cursor = 'col-resize';
|
|
213
|
+
document.body.style.userSelect = 'none';
|
|
214
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
215
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
216
|
+
|
|
217
|
+
return () => {
|
|
218
|
+
document.body.style.cursor = '';
|
|
219
|
+
document.body.style.userSelect = '';
|
|
220
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
221
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
222
|
+
};
|
|
223
|
+
}, [isDragging]);
|
|
224
|
+
|
|
225
|
+
const handleResizeStart = useCallback((e) => {
|
|
226
|
+
e.preventDefault();
|
|
227
|
+
setIsDragging(true);
|
|
228
|
+
}, []);
|
|
229
|
+
|
|
111
230
|
useEffect(() => {
|
|
112
231
|
// Here we have to consider three cases now ,
|
|
113
232
|
// One is firebase for which it is primarly designed for
|
|
@@ -528,6 +647,17 @@ export default function SideMenu({ loading, modules = [], callback, appSettings,
|
|
|
528
647
|
{/* {renderFooter(footerLogo)} */}
|
|
529
648
|
{/* Footer Logo Ends */}
|
|
530
649
|
</div>
|
|
650
|
+
|
|
651
|
+
{!collapsed && (
|
|
652
|
+
<div
|
|
653
|
+
className={`sidemenu-resize-handle${isDragging ? ' dragging' : ''}`}
|
|
654
|
+
onMouseDown={handleResizeStart}
|
|
655
|
+
role="separator"
|
|
656
|
+
aria-orientation="vertical"
|
|
657
|
+
aria-label="Resize sidebar"
|
|
658
|
+
title="Drag to resize"
|
|
659
|
+
/>
|
|
660
|
+
)}
|
|
531
661
|
</div>
|
|
532
662
|
);
|
|
533
663
|
}
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
padding: 5px 16px;
|
|
41
41
|
position: fixed;
|
|
42
42
|
z-index: 1000;
|
|
43
|
-
width:
|
|
43
|
+
width: var(--sidemenu-width, 211px);
|
|
44
44
|
background: #fff;
|
|
45
45
|
// border-bottom: 1.5px solid #24aeb8;
|
|
46
46
|
&.close {
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
margin-top: 0;
|
|
107
107
|
overflow-y: scroll;
|
|
108
108
|
overflow-x: hidden;
|
|
109
|
-
width: 211px;
|
|
109
|
+
width: var(--sidemenu-width, 211px);
|
|
110
110
|
padding-bottom: 50px;
|
|
111
111
|
|
|
112
112
|
/* Custom Scrollbar like macOS */
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
}
|
|
133
133
|
// when toggle open and close
|
|
134
134
|
&.open {
|
|
135
|
-
width: 211px !important;
|
|
135
|
+
width: var(--sidemenu-width, 211px) !important;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
&.close {
|
|
@@ -202,6 +202,39 @@
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
.sidemenu-resize-handle {
|
|
206
|
+
position: fixed;
|
|
207
|
+
top: 0;
|
|
208
|
+
bottom: 0;
|
|
209
|
+
left: var(--sidemenu-width, 211px);
|
|
210
|
+
width: 6px;
|
|
211
|
+
margin-left: -3px;
|
|
212
|
+
cursor: col-resize;
|
|
213
|
+
z-index: 1100;
|
|
214
|
+
background: transparent;
|
|
215
|
+
transition: background 0.15s ease;
|
|
216
|
+
|
|
217
|
+
&::after {
|
|
218
|
+
content: '';
|
|
219
|
+
position: absolute;
|
|
220
|
+
top: 0;
|
|
221
|
+
bottom: 0;
|
|
222
|
+
left: 50%;
|
|
223
|
+
width: 1px;
|
|
224
|
+
background: transparent;
|
|
225
|
+
transition: background 0.15s ease;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
&:hover::after,
|
|
229
|
+
&.dragging::after {
|
|
230
|
+
background: #24aeb8;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
&.dragging {
|
|
234
|
+
background: rgba(36, 174, 184, 0.08);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
205
238
|
.sidebar-footer {
|
|
206
239
|
position: fixed;
|
|
207
240
|
|
|
@@ -258,6 +291,10 @@
|
|
|
258
291
|
.sidebar-footer {
|
|
259
292
|
width: 100% !important;
|
|
260
293
|
}
|
|
294
|
+
|
|
295
|
+
.sidemenu-resize-handle {
|
|
296
|
+
display: none;
|
|
297
|
+
}
|
|
261
298
|
}
|
|
262
299
|
}
|
|
263
300
|
|
|
@@ -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} />
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useContext, useEffect, useState } from 'react';
|
|
2
2
|
|
|
3
|
-
import { Form, Input, message,
|
|
3
|
+
import { Divider, Form, Input, message, Radio, Typography } from 'antd';
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
import backgroundImage from './../../../assets/images/vector.png';
|
|
5
|
+
import { Link, withRouter } from 'react-router-dom';
|
|
8
6
|
|
|
9
7
|
import OTPInput from 'otp-input-react';
|
|
10
8
|
|
|
@@ -32,7 +30,7 @@ import { Location } from '../../utils';
|
|
|
32
30
|
|
|
33
31
|
import { checkLicenseStatus, formatMobile, safeJSON } from '../../utils/common/common.utils';
|
|
34
32
|
|
|
35
|
-
import { MailOutlined, MessageOutlined
|
|
33
|
+
import { MailOutlined, MessageOutlined } from '@ant-design/icons';
|
|
36
34
|
|
|
37
35
|
import ResetPassword from './reset-password';
|
|
38
36
|
|
|
@@ -50,7 +48,6 @@ const tailLayout = {
|
|
|
50
48
|
|
|
51
49
|
const LICENSE_EXPIRY = '2026-12-12';
|
|
52
50
|
|
|
53
|
-
|
|
54
51
|
const headers = {
|
|
55
52
|
db_ptr: 'nuraho',
|
|
56
53
|
};
|
|
@@ -443,6 +440,20 @@ function LoginPhone({ history, appSettings }) {
|
|
|
443
440
|
setOtpSuccess(false);
|
|
444
441
|
};
|
|
445
442
|
|
|
443
|
+
// Paste handler runs in capture phase so it pre-empts otp-input-react's
|
|
444
|
+
// internal handler, which otherwise drops digits when the focused box is
|
|
445
|
+
// not the first one and does not strip spaces/dashes/surrounding text
|
|
446
|
+
const handleOtpPaste = (e) => {
|
|
447
|
+
const raw = e.clipboardData?.getData('text') || '';
|
|
448
|
+
const digits = raw.replace(/\D/g, '').slice(0, 6);
|
|
449
|
+
if (!digits) return;
|
|
450
|
+
e.preventDefault();
|
|
451
|
+
e.stopPropagation();
|
|
452
|
+
setOtpValue(digits);
|
|
453
|
+
setOtpError(false);
|
|
454
|
+
setOtpSuccess(false);
|
|
455
|
+
};
|
|
456
|
+
|
|
446
457
|
/**
|
|
447
458
|
* Otp resend Logic
|
|
448
459
|
*/
|
|
@@ -495,6 +506,18 @@ function LoginPhone({ history, appSettings }) {
|
|
|
495
506
|
setOtpError(false);
|
|
496
507
|
}
|
|
497
508
|
}, [otpExpired]);
|
|
509
|
+
|
|
510
|
+
// Auto-submit once the user has filled all 6 digits (typed or pasted).
|
|
511
|
+
// verifyOtp clears otpValue on failure (back to length 0) and sets
|
|
512
|
+
// otpSuccess on success, both of which prevent this from re-firing.
|
|
513
|
+
useEffect(() => {
|
|
514
|
+
if (otpValue.length !== 6) return;
|
|
515
|
+
if (loading || otpExpired || otpSuccess || !otpVerification) return;
|
|
516
|
+
verifyOtp();
|
|
517
|
+
// verifyOtp intentionally omitted from deps — it's redefined every render;
|
|
518
|
+
// including it would re-fire on every state change.
|
|
519
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
520
|
+
}, [otpValue, otpVerification]);
|
|
498
521
|
/**
|
|
499
522
|
* Function to get Ldap users
|
|
500
523
|
* @returns
|
|
@@ -611,28 +634,28 @@ function LoginPhone({ history, appSettings }) {
|
|
|
611
634
|
return user.username;
|
|
612
635
|
};
|
|
613
636
|
|
|
614
|
-
const { globalCustomerHeader = () => {
|
|
637
|
+
const { globalCustomerHeader = () => {} } = appSettings;
|
|
615
638
|
|
|
616
639
|
const themeName = process.env.REACT_APP_THEME; // e.g., 'purple'
|
|
617
640
|
const isPurple = themeName === 'purple';
|
|
618
641
|
|
|
619
642
|
const sectionStyle = isPurple
|
|
620
643
|
? {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
644
|
+
width: '100%',
|
|
645
|
+
height: '100vh',
|
|
646
|
+
backgroundImage: `${state.theme.colors.loginPageBackground}`,
|
|
647
|
+
backgroundPosition: 'center bottom, center',
|
|
648
|
+
backgroundRepeat: 'no-repeat, no-repeat',
|
|
649
|
+
backgroundSize: 'cover, cover',
|
|
650
|
+
}
|
|
628
651
|
: {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
652
|
+
width: '100%',
|
|
653
|
+
height: '100vh',
|
|
654
|
+
background: 'linear-gradient(to bottom, #F7F6E3 0%, #EEF1DE 20%, #D5E4DA 45%, #9DBFC8 75%, #4F89A6 100%)',
|
|
655
|
+
backgroundPosition: 'center bottom, center',
|
|
656
|
+
backgroundRepeat: 'no-repeat, no-repeat',
|
|
657
|
+
backgroundSize: 'cover, cover',
|
|
658
|
+
};
|
|
636
659
|
|
|
637
660
|
return (
|
|
638
661
|
<section className="full-page" style={sectionStyle}>
|
|
@@ -732,26 +755,24 @@ function LoginPhone({ history, appSettings }) {
|
|
|
732
755
|
<strong>{getOtpDestinationText()}</strong>
|
|
733
756
|
</span>
|
|
734
757
|
</p>
|
|
735
|
-
<
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
? '2px solid #52c41a'
|
|
744
|
-
:
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
}}
|
|
754
|
-
/>
|
|
758
|
+
<div onPasteCapture={handleOtpPaste}>
|
|
759
|
+
<OTPInput
|
|
760
|
+
value={otpValue}
|
|
761
|
+
onChange={handleOtpChange}
|
|
762
|
+
autoFocus
|
|
763
|
+
OTPLength={6}
|
|
764
|
+
disabled={loading || otpExpired}
|
|
765
|
+
inputStyles={{
|
|
766
|
+
border: otpSuccess ? '2px solid #52c41a' : otpError ? '2px solid #FF5C5C' : '1px solid #d9d9d9',
|
|
767
|
+
borderRadius: 4,
|
|
768
|
+
width: isMobile ? 28 : 45,
|
|
769
|
+
height: isMobile ? 36 : 40,
|
|
770
|
+
margin: isMobile ? '2px' : '0 4px',
|
|
771
|
+
fontSize: isMobile ? 16 : 18,
|
|
772
|
+
textAlign: 'center',
|
|
773
|
+
}}
|
|
774
|
+
/>
|
|
775
|
+
</div>
|
|
755
776
|
{/* Timer below OTP */}
|
|
756
777
|
<div className="otp-timer">
|
|
757
778
|
{/* {!otpExpired ? */}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font Awesome compatibility helpers.
|
|
3
|
+
*
|
|
4
|
+
* Two failure modes are addressed here:
|
|
5
|
+
* 1. The host app ships FA6 Free but menu/data uses FA5 names that were
|
|
6
|
+
* renamed (e.g. fa-search-dollar → fa-magnifying-glass-dollar). Rendering
|
|
7
|
+
* the old class alone produces a blank glyph.
|
|
8
|
+
* 2. The host app doesn't ship Font Awesome at all, or ships a build whose
|
|
9
|
+
* font file fails to load. Every <i> renders blank regardless of class.
|
|
10
|
+
*
|
|
11
|
+
* `expandFaAliases` handles (1) by emitting both old and new class names so
|
|
12
|
+
* whichever the loaded stylesheet recognises will paint the glyph.
|
|
13
|
+
* `ensureFontAwesomeAvailable` handles (2) by probing the page for a working
|
|
14
|
+
* FA install and, if absent, injecting FA6 Free from a CDN.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Font Awesome 5 → Font Awesome 6 Free renames. FA Pro keeps the FA5 names
|
|
18
|
+
// as aliases so emitting both is safe there too.
|
|
19
|
+
export const FA5_TO_FA6_FREE_ALIASES = {
|
|
20
|
+
// search / magnifier
|
|
21
|
+
'fa-search': 'fa-magnifying-glass',
|
|
22
|
+
'fa-search-dollar': 'fa-magnifying-glass-dollar',
|
|
23
|
+
'fa-search-plus': 'fa-magnifying-glass-plus',
|
|
24
|
+
'fa-search-minus': 'fa-magnifying-glass-minus',
|
|
25
|
+
'fa-search-location': 'fa-magnifying-glass-location',
|
|
26
|
+
// layout / lists
|
|
27
|
+
'fa-th': 'fa-table-cells',
|
|
28
|
+
'fa-th-large': 'fa-table-cells-large',
|
|
29
|
+
'fa-th-list': 'fa-table-list',
|
|
30
|
+
'fa-list-alt': 'fa-rectangle-list',
|
|
31
|
+
'fa-tasks': 'fa-list-check',
|
|
32
|
+
// auth / users
|
|
33
|
+
'fa-sign-in-alt': 'fa-right-to-bracket',
|
|
34
|
+
'fa-sign-out-alt': 'fa-right-from-bracket',
|
|
35
|
+
'fa-user-circle': 'fa-circle-user',
|
|
36
|
+
'fa-user-cog': 'fa-user-gear',
|
|
37
|
+
'fa-user-md': 'fa-user-doctor',
|
|
38
|
+
'fa-users-cog': 'fa-users-gear',
|
|
39
|
+
// common actions
|
|
40
|
+
'fa-times': 'fa-xmark',
|
|
41
|
+
'fa-times-circle': 'fa-circle-xmark',
|
|
42
|
+
'fa-trash-alt': 'fa-trash-can',
|
|
43
|
+
'fa-edit': 'fa-pen-to-square',
|
|
44
|
+
'fa-pencil-alt': 'fa-pencil',
|
|
45
|
+
'fa-save': 'fa-floppy-disk',
|
|
46
|
+
'fa-share-alt': 'fa-share-nodes',
|
|
47
|
+
'fa-unlink': 'fa-link-slash',
|
|
48
|
+
// settings / dashboards
|
|
49
|
+
'fa-cog': 'fa-gear',
|
|
50
|
+
'fa-cogs': 'fa-gears',
|
|
51
|
+
'fa-tachometer-alt': 'fa-gauge',
|
|
52
|
+
'fa-tachometer': 'fa-gauge',
|
|
53
|
+
// navigation / pages
|
|
54
|
+
'fa-home': 'fa-house',
|
|
55
|
+
'fa-file-alt': 'fa-file-lines',
|
|
56
|
+
// feedback / status
|
|
57
|
+
'fa-info-circle': 'fa-circle-info',
|
|
58
|
+
'fa-question-circle': 'fa-circle-question',
|
|
59
|
+
'fa-check-circle': 'fa-circle-check',
|
|
60
|
+
'fa-check-square': 'fa-square-check',
|
|
61
|
+
'fa-exclamation-triangle': 'fa-triangle-exclamation',
|
|
62
|
+
'fa-exclamation-circle': 'fa-circle-exclamation',
|
|
63
|
+
'fa-plus-circle': 'fa-circle-plus',
|
|
64
|
+
'fa-plus-square': 'fa-square-plus',
|
|
65
|
+
'fa-minus-circle': 'fa-circle-minus',
|
|
66
|
+
'fa-minus-square': 'fa-square-minus',
|
|
67
|
+
// charts
|
|
68
|
+
'fa-chart-bar': 'fa-chart-column',
|
|
69
|
+
// calendar / time
|
|
70
|
+
'fa-calendar-alt': 'fa-calendar-days',
|
|
71
|
+
'fa-calendar-times': 'fa-calendar-xmark',
|
|
72
|
+
'fa-history': 'fa-clock-rotate-left',
|
|
73
|
+
// money / billing
|
|
74
|
+
'fa-money-check-alt': 'fa-money-check-dollar',
|
|
75
|
+
'fa-money-bill-alt': 'fa-money-bill-1',
|
|
76
|
+
// shopping
|
|
77
|
+
'fa-shopping-cart': 'fa-cart-shopping',
|
|
78
|
+
'fa-shopping-bag': 'fa-bag-shopping',
|
|
79
|
+
'fa-shopping-basket': 'fa-basket-shopping',
|
|
80
|
+
// devices / contact
|
|
81
|
+
'fa-mobile-alt': 'fa-mobile-screen-button',
|
|
82
|
+
'fa-tablet-alt': 'fa-tablet-screen-button',
|
|
83
|
+
'fa-phone-alt': 'fa-phone',
|
|
84
|
+
'fa-mail-bulk': 'fa-envelopes-bulk',
|
|
85
|
+
// map / location
|
|
86
|
+
'fa-map-marker': 'fa-location-pin',
|
|
87
|
+
'fa-map-marker-alt': 'fa-location-dot',
|
|
88
|
+
'fa-globe-americas': 'fa-earth-americas',
|
|
89
|
+
// security / id
|
|
90
|
+
'fa-shield-alt': 'fa-shield-halved',
|
|
91
|
+
'fa-id-card-alt': 'fa-id-card-clip',
|
|
92
|
+
// healthcare (this codebase skews this way)
|
|
93
|
+
'fa-prescription-bottle-alt': 'fa-prescription-bottle-medical',
|
|
94
|
+
'fa-procedures': 'fa-bed-pulse',
|
|
95
|
+
'fa-heartbeat': 'fa-heart-pulse',
|
|
96
|
+
'fa-clinic-medical': 'fa-house-chimney-medical',
|
|
97
|
+
'fa-hospital-alt': 'fa-hospital',
|
|
98
|
+
'fa-allergies': 'fa-hand-dots',
|
|
99
|
+
'fa-file-medical-alt': 'fa-file-waveform',
|
|
100
|
+
'fa-running': 'fa-person-running',
|
|
101
|
+
'fa-walking': 'fa-person-walking',
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Expand a Font Awesome icon class string so that both the FA5 name and its
|
|
106
|
+
* FA6-Free equivalent are present. Same-name tokens are emitted once.
|
|
107
|
+
* Non-string input is returned as-is.
|
|
108
|
+
*/
|
|
109
|
+
export function expandFaAliases(icon) {
|
|
110
|
+
if (typeof icon !== 'string' || !icon) return icon || '';
|
|
111
|
+
const seen = new Set();
|
|
112
|
+
const out = [];
|
|
113
|
+
for (const token of icon.split(/\s+/)) {
|
|
114
|
+
if (!token || seen.has(token)) continue;
|
|
115
|
+
seen.add(token);
|
|
116
|
+
out.push(token);
|
|
117
|
+
const alias = FA5_TO_FA6_FREE_ALIASES[token];
|
|
118
|
+
if (alias && !seen.has(alias)) {
|
|
119
|
+
seen.add(alias);
|
|
120
|
+
out.push(alias);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out.join(' ');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const FA_FALLBACK_LINK_ID = 'soxo-fa-fallback';
|
|
127
|
+
const FA_FALLBACK_HREF = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css';
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Probe the page for a working Font Awesome install. If the probe glyph
|
|
131
|
+
* doesn't render, inject FA6 Free from a CDN. No-op when FA already works
|
|
132
|
+
* or when the fallback has already been injected.
|
|
133
|
+
*/
|
|
134
|
+
export function ensureFontAwesomeAvailable() {
|
|
135
|
+
if (typeof document === 'undefined' || !document.body) return;
|
|
136
|
+
if (document.getElementById(FA_FALLBACK_LINK_ID)) return;
|
|
137
|
+
|
|
138
|
+
// Render a hidden probe <i> with a class that exists in every recent FA
|
|
139
|
+
// build (`fa-circle`). If the ::before content is empty/none or the
|
|
140
|
+
// font-family doesn't claim to be Font Awesome, FA isn't usable here.
|
|
141
|
+
const probe = document.createElement('i');
|
|
142
|
+
probe.className = 'fa-solid fas fa-circle';
|
|
143
|
+
probe.style.cssText = 'position:absolute;left:-9999px;top:-9999px;visibility:hidden;';
|
|
144
|
+
document.body.appendChild(probe);
|
|
145
|
+
|
|
146
|
+
let working = false;
|
|
147
|
+
try {
|
|
148
|
+
const before = window.getComputedStyle(probe, '::before');
|
|
149
|
+
const content = before && before.content;
|
|
150
|
+
const fontFamily = before && before.fontFamily;
|
|
151
|
+
const hasGlyph = content && content !== 'none' && content !== '""' && content !== "''";
|
|
152
|
+
const isFaFont = fontFamily && /font\s*awesome/i.test(fontFamily);
|
|
153
|
+
working = Boolean(hasGlyph && isFaFont);
|
|
154
|
+
} catch (e) {
|
|
155
|
+
working = false;
|
|
156
|
+
} finally {
|
|
157
|
+
document.body.removeChild(probe);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (working) return;
|
|
161
|
+
|
|
162
|
+
const link = document.createElement('link');
|
|
163
|
+
link.id = FA_FALLBACK_LINK_ID;
|
|
164
|
+
link.rel = 'stylesheet';
|
|
165
|
+
link.href = FA_FALLBACK_HREF;
|
|
166
|
+
link.crossOrigin = 'anonymous';
|
|
167
|
+
document.head.appendChild(link);
|
|
168
|
+
}
|
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}
|
|
@@ -435,7 +435,12 @@ export default function ReportingDashboard({
|
|
|
435
435
|
|
|
436
436
|
if (!hasSearchValues) {
|
|
437
437
|
const currentUrlParams = Location.search();
|
|
438
|
-
const
|
|
438
|
+
const searchKeys = new Set(searchParameters.map((parameter) => parameter.field));
|
|
439
|
+
const cleanParams = Object.fromEntries(
|
|
440
|
+
Object.entries(currentUrlParams).filter(
|
|
441
|
+
([key]) => key !== 'script_id' && key !== 'selected_card' && !searchKeys.has(key)
|
|
442
|
+
)
|
|
443
|
+
);
|
|
439
444
|
|
|
440
445
|
const newParams = new URLSearchParams({
|
|
441
446
|
...cleanParams,
|
|
@@ -520,14 +525,22 @@ export default function ReportingDashboard({
|
|
|
520
525
|
return parameter;
|
|
521
526
|
});
|
|
522
527
|
|
|
523
|
-
|
|
528
|
+
const currentParams = Location.search();
|
|
529
|
+
// Mark existing empty values as null so Location.search removes stale query params.
|
|
524
530
|
const filteredParams = Object.fromEntries(
|
|
525
|
-
Object.entries(urlsToUpdate).
|
|
531
|
+
Object.entries(urlsToUpdate).flatMap(([key, value]) => {
|
|
532
|
+
const shouldRemove = value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0);
|
|
533
|
+
if (shouldRemove && !Object.prototype.hasOwnProperty.call(currentParams, key)) {
|
|
534
|
+
return [];
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return [[key, shouldRemove ? null : value]];
|
|
538
|
+
})
|
|
526
539
|
);
|
|
527
540
|
|
|
528
541
|
setformContents(formContent);
|
|
529
542
|
setLiveFormContents(formContent);
|
|
530
|
-
Location.search({ ...
|
|
543
|
+
Location.search({ ...currentParams, ...filteredParams });
|
|
531
544
|
}
|
|
532
545
|
|
|
533
546
|
// Reset Pagination Data
|
|
@@ -568,6 +581,7 @@ export default function ReportingDashboard({
|
|
|
568
581
|
<MenuDashBoardComponent
|
|
569
582
|
dashBoardIds={dashBoardIds} //Pass the available dashboard IDs to the componen
|
|
570
583
|
activeId={Number(urlParams?.selected_card || 0)}
|
|
584
|
+
dbPtr={dbPtr}
|
|
571
585
|
callback={(record) => {
|
|
572
586
|
const selectedCard = record?.id;
|
|
573
587
|
if (record.other_details) {
|