@visns-studio/visns-components 6.32.3 → 6.34.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/README.md +470 -0
- package/package.json +1 -1
- package/src/components/DataGrid.jsx +44 -2
- package/src/components/Field.jsx +32 -0
- package/src/components/Form.jsx +39 -2
- package/src/components/Navigation.jsx +274 -15
- package/src/components/Notification.jsx +13 -2
- package/src/components/TableFilter.jsx +14 -2
- package/src/components/auth/Login.jsx +9 -2
- package/src/components/auth/PasskeyEnrolPrompt.jsx +269 -0
- package/src/components/auth/Profile.jsx +422 -26
- package/src/components/auth/authEndpoints.js +43 -0
- package/src/components/auth/passkeyClient.js +102 -0
- package/src/components/auth/passkeyPrompt.js +408 -0
- package/src/components/auth/profileLayout.js +45 -0
- package/src/components/controls/DataGridSearch.jsx +16 -2
- package/src/components/controls/DataGridSortSheet.jsx +2 -0
- package/src/components/generic/ActionButtons.jsx +1 -1
- package/src/components/generic/GenericAuth.jsx +197 -21
- package/src/components/generic/GenericDetail.jsx +95 -3
- package/src/components/generic/GenericMain.jsx +7 -0
- package/src/components/generic/StandardModal.jsx +71 -158
- package/src/components/sms/SmsCampaigns.jsx +2487 -0
- package/src/components/sms/SmsInbox.jsx +18 -0
- package/src/components/sms/SmsThreadPanel.jsx +20 -0
- package/src/components/sms/smsEndpoints.js +23 -0
- package/src/components/styles/DataGrid.module.scss +22 -0
- package/src/components/styles/Form.module.scss +100 -2
- package/src/components/styles/Navigation.module.scss +322 -5
- package/src/components/styles/Notification.module.scss +140 -8
- package/src/components/styles/PasskeyEnrolPrompt.module.scss +105 -0
- package/src/components/styles/Profile.module.scss +157 -0
- package/src/components/styles/Sms.module.scss +451 -0
- package/src/components/styles/StandardModal.module.scss +273 -0
- package/src/components/styles/global.css +24 -5
- package/src/components/utils/navCollapsed.js +201 -0
- package/src/components/utils/rowActionSettings.js +76 -0
- package/src/components/utils/usePasskeysEnabled.js +84 -0
- package/src/components/utils/useShellLayout.js +131 -0
- package/src/index.js +103 -1
- package/src/utils/rememberTab.js +70 -0
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useCallback,
|
|
3
|
+
useState,
|
|
4
|
+
useEffect,
|
|
5
|
+
useInsertionEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
} from 'react';
|
|
2
8
|
import {
|
|
3
9
|
useNavigate,
|
|
4
10
|
useLocation,
|
|
@@ -33,6 +39,24 @@ import {
|
|
|
33
39
|
// the same one for the transitions they own; identical object, so a screen's
|
|
34
40
|
// curtain and this one are indistinguishable when both are up.
|
|
35
41
|
import AuthLoading from '../auth/AuthLoading';
|
|
42
|
+
// The shell layout mirrors itself onto <html> the way `density` does, so the
|
|
43
|
+
// screens below the route table can read it without being handed the prop.
|
|
44
|
+
import {
|
|
45
|
+
SHELL_LAYOUT_ATTRIBUTE,
|
|
46
|
+
normaliseShellLayout,
|
|
47
|
+
} from '../utils/useShellLayout';
|
|
48
|
+
// The offer to enrol a passkey, made once after a sign-in. Everything about
|
|
49
|
+
// WHEN it appears is in auth/passkeyPrompt.js; this file supplies the two
|
|
50
|
+
// facts only it knows — whether the host turned the feature on, and whether
|
|
51
|
+
// this page load contains an actual sign-in.
|
|
52
|
+
import PasskeyEnrolPrompt from '../auth/PasskeyEnrolPrompt';
|
|
53
|
+
import { resolvePasskeyOptions } from '../auth/passkeyPrompt';
|
|
54
|
+
// `data-passkeys` on <html>, for the screens this file does not mount — the
|
|
55
|
+
// profile page is routed by the consuming app, not from here. See the module.
|
|
56
|
+
import {
|
|
57
|
+
PASSKEYS_ATTRIBUTE,
|
|
58
|
+
PASSKEYS_ENABLED_VALUE,
|
|
59
|
+
} from '../utils/usePasskeysEnabled';
|
|
36
60
|
|
|
37
61
|
/**
|
|
38
62
|
* Path segments that mean "an auth screen that hands off to the app".
|
|
@@ -94,8 +118,30 @@ const GenericAuth = ({
|
|
|
94
118
|
logo,
|
|
95
119
|
headerLogo,
|
|
96
120
|
providers,
|
|
97
|
-
|
|
98
|
-
|
|
121
|
+
/*
|
|
122
|
+
* Whether this host offers passkeys (WebAuthn), and what to do about it.
|
|
123
|
+
* Config-driven exactly like `providers`: the server says whether the
|
|
124
|
+
* feature exists, the browser is asked separately whether it can do it.
|
|
125
|
+
*
|
|
126
|
+
* false / absent nothing. No button on the login screen, no tab on
|
|
127
|
+
* the profile, no prompt. Byte-identical to 6.32.
|
|
128
|
+
* true the login button (what it has always meant), plus
|
|
129
|
+
* the profile's passkeys tab and the enrol-once prompt
|
|
130
|
+
* after signing in.
|
|
131
|
+
* {…} the same, with the details named:
|
|
132
|
+
* enabled default true — an object is the
|
|
133
|
+
* opt-in, so this is only written to
|
|
134
|
+
* switch the feature off from a
|
|
135
|
+
* config object the host already has.
|
|
136
|
+
* promptOnLogin default true. False offers passkeys
|
|
137
|
+
* without ever interrupting anybody.
|
|
138
|
+
* snoozeDays default 30 — how long "Not now"
|
|
139
|
+
* holds for.
|
|
140
|
+
* copy the dialog's words, merged over the
|
|
141
|
+
* defaults, like Login's `copy`.
|
|
142
|
+
*
|
|
143
|
+
* See auth/passkeyPrompt.js, which owns the shape and every gate.
|
|
144
|
+
*/
|
|
99
145
|
passkeys = false,
|
|
100
146
|
navigation,
|
|
101
147
|
routeConfig,
|
|
@@ -132,6 +178,14 @@ const GenericAuth = ({
|
|
|
132
178
|
// the websocket lives) simply passes its own `navBadges` from there
|
|
133
179
|
// instead, which lands after this one and wins.
|
|
134
180
|
navBadges = {},
|
|
181
|
+
/*
|
|
182
|
+
* The square brand mark for the collapsed sidebar rail, if the project has
|
|
183
|
+
* one. `headerLogo` is a wide lock-up and there is no room for a wordmark
|
|
184
|
+
* in a 64px strip; passing nothing leaves the collapsed plate holding the
|
|
185
|
+
* collapse toggle alone. Sidebar layouts only — the header bar never
|
|
186
|
+
* collapses. Threaded straight through the shell to Navigation.
|
|
187
|
+
*/
|
|
188
|
+
collapsedLogo,
|
|
135
189
|
}) => {
|
|
136
190
|
const navigate = useNavigate();
|
|
137
191
|
const location = useLocation();
|
|
@@ -146,6 +200,65 @@ const GenericAuth = ({
|
|
|
146
200
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
147
201
|
const [isClientAuthenticated, setIsClientAuthenticated] = useState(false);
|
|
148
202
|
|
|
203
|
+
// The `passkeys` prop in one shape, whichever of the three the host used.
|
|
204
|
+
const passkeyOptions = resolvePasskeyOptions(passkeys);
|
|
205
|
+
|
|
206
|
+
/*
|
|
207
|
+
* THE SIGN-IN EVENT — "somebody signed in during this page's life".
|
|
208
|
+
*
|
|
209
|
+
* The passkey prompt must not appear on a refresh, and from below this
|
|
210
|
+
* component the two are indistinguishable: either way the shell mounts
|
|
211
|
+
* with a live session and a profile. The difference is visible in exactly
|
|
212
|
+
* one place, here, because there are two ways `isAuthenticated` becomes
|
|
213
|
+
* true and only one of them is a sign-in:
|
|
214
|
+
*
|
|
215
|
+
* - `updateAuthStatus` sets it after `/ajax/user/profile` answers. That
|
|
216
|
+
* is a session that ALREADY existed — a refresh, a second tab, a
|
|
217
|
+
* bookmark opened this morning.
|
|
218
|
+
* - an auth screen calls the setter this component handed it. That is
|
|
219
|
+
* somebody typing a password, entering a 2FA code, or using a passkey,
|
|
220
|
+
* right now.
|
|
221
|
+
*
|
|
222
|
+
* So the screens get `markAuthenticated` instead of the raw setter, and
|
|
223
|
+
* `updateAuthStatus` keeps the raw one. A ref rather than a boolean state
|
|
224
|
+
* would do as well, except the prompt has to re-render when it flips, and
|
|
225
|
+
* a ref does not cause one.
|
|
226
|
+
*
|
|
227
|
+
* It is deliberately NOT sessionStorage. A flag in storage outlives the
|
|
228
|
+
* page that wrote it — a duplicated tab, a restored session, a back button
|
|
229
|
+
* — and would have to be cleared by whoever read it first, which is a
|
|
230
|
+
* second piece of state to get wrong. This value cannot survive a reload,
|
|
231
|
+
* which is exactly the property being tested for.
|
|
232
|
+
*/
|
|
233
|
+
const [signInEvent, setSignInEvent] = useState(null);
|
|
234
|
+
|
|
235
|
+
// Read inside the stable callback below so its identity never changes:
|
|
236
|
+
// the auth screens are memoised on their props, and a setter with a new
|
|
237
|
+
// identity every render would re-render each of them on every render.
|
|
238
|
+
const promptOnLoginRef = useRef(passkeyOptions.promptOnLogin);
|
|
239
|
+
promptOnLoginRef.current = passkeyOptions.promptOnLogin;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* What the auth screens are given in place of `setIsAuthenticated`.
|
|
243
|
+
*
|
|
244
|
+
* `meta.method` says how the sign-in happened — Login passes 'passkey' for
|
|
245
|
+
* the passkey button and 'password' for the form; a screen that passes
|
|
246
|
+
* nothing counts as 'password'. Nothing else changes: the boolean still
|
|
247
|
+
* goes straight to the same state setter, and a host that passes this
|
|
248
|
+
* component's setter somewhere expecting a plain `useState` setter is
|
|
249
|
+
* unaffected, because the extra argument is optional.
|
|
250
|
+
*/
|
|
251
|
+
const markAuthenticated = useCallback((value, meta) => {
|
|
252
|
+
if (value && promptOnLoginRef.current) {
|
|
253
|
+
setSignInEvent({
|
|
254
|
+
method: meta?.method || 'password',
|
|
255
|
+
at: Date.now(),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
setIsAuthenticated(value);
|
|
260
|
+
}, []);
|
|
261
|
+
|
|
149
262
|
// Mirror the `density` prop onto <html> as data-density, which is what
|
|
150
263
|
// styles/density.css keys off. 'default' writes nothing, so an app that
|
|
151
264
|
// never passes the prop leaves no attribute and matches no rule.
|
|
@@ -166,6 +279,30 @@ const GenericAuth = ({
|
|
|
166
279
|
};
|
|
167
280
|
}, [density]);
|
|
168
281
|
|
|
282
|
+
// Same mechanism, same node: the shell layout mirrors itself onto <html>
|
|
283
|
+
// as `data-layout`, so a screen several components below the route table
|
|
284
|
+
// can find out whether the app has a rail down its left edge without the
|
|
285
|
+
// prop being threaded to it. See utils/useShellLayout.js for the reading
|
|
286
|
+
// half and for why 'full' — GenericAuth's own default — is what an absent
|
|
287
|
+
// attribute means. Written for EVERY layout, 'full' included: the value is
|
|
288
|
+
// the contract, and nothing in this library styles off `data-layout`, so
|
|
289
|
+
// an app that never passed the prop renders exactly what it rendered
|
|
290
|
+
// before with one more attribute on <html>.
|
|
291
|
+
useInsertionEffect(() => {
|
|
292
|
+
if (typeof document === 'undefined') {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
document.documentElement.setAttribute(
|
|
297
|
+
SHELL_LAYOUT_ATTRIBUTE,
|
|
298
|
+
normaliseShellLayout(layout)
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
return () => {
|
|
302
|
+
document.documentElement.removeAttribute(SHELL_LAYOUT_ATTRIBUTE);
|
|
303
|
+
};
|
|
304
|
+
}, [layout]);
|
|
305
|
+
|
|
169
306
|
// Same mechanism, same node, same reasoning: `useCardRowsEnabled` in
|
|
170
307
|
// utils/useDensity.js reads this attribute off <html>, so an app opts its
|
|
171
308
|
// grids in from one place and every grid — including one mounted inside a
|
|
@@ -182,6 +319,26 @@ const GenericAuth = ({
|
|
|
182
319
|
};
|
|
183
320
|
}, [cardRows]);
|
|
184
321
|
|
|
322
|
+
// And once more for passkeys. The profile screen is mounted by the
|
|
323
|
+
// consuming app's own `routeConfig` — this file never sees it — so the one
|
|
324
|
+
// way to tell it the feature is on, without every app editing its route
|
|
325
|
+
// config, is the document. Absent means off, which is what an app that
|
|
326
|
+
// never passed the prop writes.
|
|
327
|
+
useInsertionEffect(() => {
|
|
328
|
+
if (typeof document === 'undefined' || !passkeyOptions.enabled) {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
document.documentElement.setAttribute(
|
|
333
|
+
PASSKEYS_ATTRIBUTE,
|
|
334
|
+
PASSKEYS_ENABLED_VALUE
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
return () => {
|
|
338
|
+
document.documentElement.removeAttribute(PASSKEYS_ATTRIBUTE);
|
|
339
|
+
};
|
|
340
|
+
}, [passkeyOptions.enabled]);
|
|
341
|
+
|
|
185
342
|
const updateAuthStatus = async () => {
|
|
186
343
|
// The loading curtain covers two moments and stays down for neither
|
|
187
344
|
// more nor less: the first profile check of a page load (no usable
|
|
@@ -298,8 +455,8 @@ const GenericAuth = ({
|
|
|
298
455
|
loginBg={loginBg}
|
|
299
456
|
logo={logo}
|
|
300
457
|
providers={providers}
|
|
301
|
-
passkeys={
|
|
302
|
-
setIsAuthenticated={
|
|
458
|
+
passkeys={passkeyOptions.enabled}
|
|
459
|
+
setIsAuthenticated={markAuthenticated}
|
|
303
460
|
setUserProfile={setUserProfile}
|
|
304
461
|
config={config}
|
|
305
462
|
endpoints={endpoints}
|
|
@@ -337,7 +494,7 @@ const GenericAuth = ({
|
|
|
337
494
|
<TwoFactorAuthComponent
|
|
338
495
|
loginBg={loginBg}
|
|
339
496
|
logo={logo}
|
|
340
|
-
setIsAuthenticated={
|
|
497
|
+
setIsAuthenticated={markAuthenticated}
|
|
341
498
|
setUserProfile={setUserProfile}
|
|
342
499
|
config={config}
|
|
343
500
|
endpoints={endpoints}
|
|
@@ -428,21 +585,40 @@ const GenericAuth = ({
|
|
|
428
585
|
<Route
|
|
429
586
|
path="/*"
|
|
430
587
|
element={
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
588
|
+
<>
|
|
589
|
+
<MainShell
|
|
590
|
+
component={mainComponent}
|
|
591
|
+
layout={layout}
|
|
592
|
+
logo={headerLogo}
|
|
593
|
+
collapsedLogo={collapsedLogo}
|
|
594
|
+
navigation={navigation}
|
|
595
|
+
routeConfig={routeConfig}
|
|
596
|
+
setUserProfile={setUserProfile}
|
|
597
|
+
setSystemAuth={setIsAuthenticated}
|
|
598
|
+
themeType={themeType}
|
|
599
|
+
userProfile={userProfile}
|
|
600
|
+
enforce2FA={enforce2FA}
|
|
601
|
+
density={density}
|
|
602
|
+
navBadges={navBadges}
|
|
603
|
+
{...mainComponentProps}
|
|
604
|
+
/>
|
|
605
|
+
{/* Inside the protected route on purpose: it
|
|
606
|
+
can then only ever mount for a signed-in
|
|
607
|
+
user, in the authenticated shell, and it
|
|
608
|
+
renders nothing at all until every gate in
|
|
609
|
+
passkeyPrompt.js has passed. An app that
|
|
610
|
+
left `passkeys` alone never even mounts
|
|
611
|
+
it. */}
|
|
612
|
+
{passkeyOptions.promptOnLogin ? (
|
|
613
|
+
<PasskeyEnrolPrompt
|
|
614
|
+
passkeys={passkeys}
|
|
615
|
+
userProfile={userProfile}
|
|
616
|
+
endpoints={endpoints}
|
|
617
|
+
justSignedIn={!!signInEvent}
|
|
618
|
+
signInMethod={signInEvent?.method}
|
|
619
|
+
/>
|
|
620
|
+
) : null}
|
|
621
|
+
</>
|
|
446
622
|
}
|
|
447
623
|
/>
|
|
448
624
|
</Route>
|
|
@@ -102,6 +102,7 @@ import ProposalTemplatePreview from '../proposal/ProposalTemplatePreview';
|
|
|
102
102
|
import GalleryModal from '../modals/GalleryModal';
|
|
103
103
|
|
|
104
104
|
import styles from '../styles/GenericDetail.module.scss';
|
|
105
|
+
import { openTabFromUrl, writeTabToUrl } from '../../utils/rememberTab.js';
|
|
105
106
|
|
|
106
107
|
const localizer = momentLocalizer(moment);
|
|
107
108
|
const DnDCalendar = withDragAndDrop(Calendar);
|
|
@@ -257,6 +258,28 @@ function GenericDetail({
|
|
|
257
258
|
* wrapped this component for one page knows something the config does not.
|
|
258
259
|
*/
|
|
259
260
|
editPlacement = null,
|
|
261
|
+
/**
|
|
262
|
+
* REMEMBER THE OPEN TAB IN THE ADDRESS BAR — `?tab=<filter id>` — so a
|
|
263
|
+
* refresh, a copied link or the back button lands on the tab the reader
|
|
264
|
+
* was on rather than on the first one (product owner, 2026-09-14: "when
|
|
265
|
+
* refreshing on project page, can it remember which tab you are on").
|
|
266
|
+
*
|
|
267
|
+
* READ once, when the tab list is built from the config: a `?tab` naming
|
|
268
|
+
* a top-level filter id makes that tab the shown one, in the same
|
|
269
|
+
* `show`/`active`/`class` vocabulary the config itself uses (the class is
|
|
270
|
+
* borrowed from whichever tab the config marks shown, so a page that
|
|
271
|
+
* says `subactive` keeps saying it). An id that names nothing is ignored
|
|
272
|
+
* and the config's own default stands — a stale link is a first-tab
|
|
273
|
+
* landing, never a blank page.
|
|
274
|
+
*
|
|
275
|
+
* WRITTEN with `history.replaceState`, never a router navigation: the
|
|
276
|
+
* host may have rewritten the path to a slug the router does not know
|
|
277
|
+
* about, and a navigation would put the numeric id back and remount the
|
|
278
|
+
* page around the reader. Only the `tab` parameter is touched; every
|
|
279
|
+
* other query parameter and the hash are kept. `false` turns both halves
|
|
280
|
+
* off for a host that owns its query string.
|
|
281
|
+
*/
|
|
282
|
+
rememberTab = true,
|
|
260
283
|
/**
|
|
261
284
|
* Host-supplied components for `type: "custom"` tabs, keyed by the tab's
|
|
262
285
|
* `renderer`.
|
|
@@ -851,6 +874,19 @@ function GenericDetail({
|
|
|
851
874
|
return action.label;
|
|
852
875
|
};
|
|
853
876
|
|
|
877
|
+
/**
|
|
878
|
+
* The native tooltip for a host-configured action button.
|
|
879
|
+
*
|
|
880
|
+
* A tooltip is meant to say what pressing the button DOES rather than
|
|
881
|
+
* repeat the word written on it — but a custom action's behaviour lives
|
|
882
|
+
* in the consuming application's config and not here, so the only
|
|
883
|
+
* sentence this library can write is the one the host hands it.
|
|
884
|
+
* `tooltip` is that opening; everything else falls back to the label,
|
|
885
|
+
* which at least rescues a button whose text the layout has truncated.
|
|
886
|
+
*/
|
|
887
|
+
const getActionTooltip = (action) =>
|
|
888
|
+
action?.tooltip || getActionLabel(action) || undefined;
|
|
889
|
+
|
|
854
890
|
const getNestedValue = (obj, path) => {
|
|
855
891
|
return path.split('.').reduce((current, key) => current?.[key], obj);
|
|
856
892
|
};
|
|
@@ -1373,6 +1409,16 @@ function GenericDetail({
|
|
|
1373
1409
|
{passwordVisible ? stringValue : '******'}
|
|
1374
1410
|
<button
|
|
1375
1411
|
type="button"
|
|
1412
|
+
title={
|
|
1413
|
+
passwordVisible
|
|
1414
|
+
? 'Hide this value again'
|
|
1415
|
+
: 'Show this value in plain text'
|
|
1416
|
+
}
|
|
1417
|
+
aria-label={
|
|
1418
|
+
passwordVisible
|
|
1419
|
+
? 'Hide this value again'
|
|
1420
|
+
: 'Show this value in plain text'
|
|
1421
|
+
}
|
|
1376
1422
|
onClick={togglePasswordVisibility}
|
|
1377
1423
|
style={{
|
|
1378
1424
|
marginLeft: '10px',
|
|
@@ -3821,7 +3867,7 @@ function GenericDetail({
|
|
|
3821
3867
|
|
|
3822
3868
|
useEffect(() => {
|
|
3823
3869
|
setSubnav(() => {
|
|
3824
|
-
|
|
3870
|
+
const built = filters.map((f) => {
|
|
3825
3871
|
if (
|
|
3826
3872
|
f.hasOwnProperty('url') &&
|
|
3827
3873
|
f.baseUrl &&
|
|
@@ -3836,9 +3882,24 @@ function GenericDetail({
|
|
|
3836
3882
|
return f;
|
|
3837
3883
|
}
|
|
3838
3884
|
});
|
|
3885
|
+
|
|
3886
|
+
return rememberTab ? openTabFromUrl(built) : built;
|
|
3839
3887
|
});
|
|
3840
3888
|
}, [filters]);
|
|
3841
3889
|
|
|
3890
|
+
/**
|
|
3891
|
+
* The tab strip's setter, with the address bar kept in step. TableFilter
|
|
3892
|
+
* hands back the whole updated array (never a function), and the shown
|
|
3893
|
+
* top-level entry is what goes into `?tab=`.
|
|
3894
|
+
*/
|
|
3895
|
+
const setSubnavRemembered = (next) => {
|
|
3896
|
+
setSubnav(next);
|
|
3897
|
+
|
|
3898
|
+
if (rememberTab && Array.isArray(next)) {
|
|
3899
|
+
writeTabToUrl(next);
|
|
3900
|
+
}
|
|
3901
|
+
};
|
|
3902
|
+
|
|
3842
3903
|
/** Modal States */
|
|
3843
3904
|
const [formData, setFormData] = useState({});
|
|
3844
3905
|
const [formType, setFormType] = useState('');
|
|
@@ -3928,7 +3989,9 @@ function GenericDetail({
|
|
|
3928
3989
|
className={styles.btn}
|
|
3929
3990
|
title={
|
|
3930
3991
|
activeTabConfig.form
|
|
3931
|
-
.
|
|
3992
|
+
.inPlaceEdit
|
|
3993
|
+
? 'Open this record’s fields for editing, here on the page'
|
|
3994
|
+
: 'Open this record’s fields for editing'
|
|
3932
3995
|
}
|
|
3933
3996
|
aria-label={
|
|
3934
3997
|
activeTabConfig.form
|
|
@@ -3974,6 +4037,7 @@ function GenericDetail({
|
|
|
3974
4037
|
className={
|
|
3975
4038
|
styles.btnSecondary
|
|
3976
4039
|
}
|
|
4040
|
+
title="Stop editing and leave the record as it was — nothing you have typed is kept"
|
|
3977
4041
|
onClick={
|
|
3978
4042
|
cancelInPlaceEditing
|
|
3979
4043
|
}
|
|
@@ -3984,6 +4048,7 @@ function GenericDetail({
|
|
|
3984
4048
|
className={
|
|
3985
4049
|
styles.btn
|
|
3986
4050
|
}
|
|
4051
|
+
title="Save what you have typed and close the fields"
|
|
3987
4052
|
onClick={() => {
|
|
3988
4053
|
// Find the form's save button and click it
|
|
3989
4054
|
const saveButton =
|
|
@@ -4042,6 +4107,7 @@ function GenericDetail({
|
|
|
4042
4107
|
className={
|
|
4043
4108
|
styles.btn
|
|
4044
4109
|
}
|
|
4110
|
+
title="Save the template as it is laid out below"
|
|
4045
4111
|
onClick={() => {
|
|
4046
4112
|
// Handle the specific "ocrTemplate" item
|
|
4047
4113
|
const ocrItem =
|
|
@@ -4066,6 +4132,11 @@ function GenericDetail({
|
|
|
4066
4132
|
{activeTabConfig.export && (
|
|
4067
4133
|
<button
|
|
4068
4134
|
className={styles.btn}
|
|
4135
|
+
title={
|
|
4136
|
+
activeTabConfig.export
|
|
4137
|
+
.tooltip ||
|
|
4138
|
+
'Download what this tab is showing as a file'
|
|
4139
|
+
}
|
|
4069
4140
|
onClick={() =>
|
|
4070
4141
|
handleExport(
|
|
4071
4142
|
activeTabConfig.export
|
|
@@ -4080,6 +4151,9 @@ function GenericDetail({
|
|
|
4080
4151
|
{activeTabConfig.customAction && (
|
|
4081
4152
|
<button
|
|
4082
4153
|
className={styles.btn}
|
|
4154
|
+
title={getActionTooltip(
|
|
4155
|
+
activeTabConfig.customAction
|
|
4156
|
+
)}
|
|
4083
4157
|
onClick={() =>
|
|
4084
4158
|
handleCustomAction(
|
|
4085
4159
|
activeTabConfig.customAction
|
|
@@ -4097,6 +4171,7 @@ function GenericDetail({
|
|
|
4097
4171
|
<button
|
|
4098
4172
|
key={index}
|
|
4099
4173
|
className={styles.btn}
|
|
4174
|
+
title={getActionTooltip(customAction)}
|
|
4100
4175
|
onClick={() =>
|
|
4101
4176
|
handleCustomAction(customAction)
|
|
4102
4177
|
}
|
|
@@ -4118,6 +4193,12 @@ function GenericDetail({
|
|
|
4118
4193
|
<>
|
|
4119
4194
|
<button
|
|
4120
4195
|
className={styles.btn}
|
|
4196
|
+
title={
|
|
4197
|
+
activeTabConfig.functions
|
|
4198
|
+
.checkboxUpdate
|
|
4199
|
+
.tooltip ||
|
|
4200
|
+
'Apply this change to every row you have ticked'
|
|
4201
|
+
}
|
|
4121
4202
|
onClick={
|
|
4122
4203
|
handleCheckboxUpdate
|
|
4123
4204
|
}
|
|
@@ -4149,6 +4230,13 @@ function GenericDetail({
|
|
|
4149
4230
|
?.checkboxDelete && (
|
|
4150
4231
|
<button
|
|
4151
4232
|
className={styles.btn}
|
|
4233
|
+
title={
|
|
4234
|
+
activeTabConfig
|
|
4235
|
+
.functions
|
|
4236
|
+
.checkboxDelete
|
|
4237
|
+
.tooltip ||
|
|
4238
|
+
'Delete every row you have ticked'
|
|
4239
|
+
}
|
|
4152
4240
|
onClick={
|
|
4153
4241
|
handleCheckboxDelete
|
|
4154
4242
|
}
|
|
@@ -4163,6 +4251,9 @@ function GenericDetail({
|
|
|
4163
4251
|
{activeTabConfig.customAction && (
|
|
4164
4252
|
<button
|
|
4165
4253
|
className={styles.btn}
|
|
4254
|
+
title={getActionTooltip(
|
|
4255
|
+
activeTabConfig.customAction
|
|
4256
|
+
)}
|
|
4166
4257
|
onClick={() =>
|
|
4167
4258
|
handleCustomAction(
|
|
4168
4259
|
activeTabConfig.customAction
|
|
@@ -4181,6 +4272,7 @@ function GenericDetail({
|
|
|
4181
4272
|
<button
|
|
4182
4273
|
key={index}
|
|
4183
4274
|
className={styles.btn}
|
|
4275
|
+
title={getActionTooltip(customAction)}
|
|
4184
4276
|
onClick={() =>
|
|
4185
4277
|
handleCustomAction(customAction)
|
|
4186
4278
|
}
|
|
@@ -4489,7 +4581,7 @@ function GenericDetail({
|
|
|
4489
4581
|
>
|
|
4490
4582
|
<TableFilter
|
|
4491
4583
|
filters={subnav}
|
|
4492
|
-
setFilters={
|
|
4584
|
+
setFilters={setSubnavRemembered}
|
|
4493
4585
|
variant={tabsUnderlined ? 'underline' : undefined}
|
|
4494
4586
|
/>
|
|
4495
4587
|
</div>
|
|
@@ -43,6 +43,12 @@ const GenericMain = ({
|
|
|
43
43
|
// `navRenderers` is — the shell does not know what a project counts, who
|
|
44
44
|
// is allowed to see it, or how often it changes. See navBadges.js.
|
|
45
45
|
navBadges = {},
|
|
46
|
+
/*
|
|
47
|
+
* The square brand mark the sidebar rail wears when it is collapsed.
|
|
48
|
+
* Accepted here so it is part of the main-layout interface; the header
|
|
49
|
+
* layout has no rail and passes it on to a Navigation that ignores it.
|
|
50
|
+
*/
|
|
51
|
+
collapsedLogo,
|
|
46
52
|
}) => {
|
|
47
53
|
const navigate = useNavigate();
|
|
48
54
|
const location = useLocation();
|
|
@@ -183,6 +189,7 @@ const GenericMain = ({
|
|
|
183
189
|
<Navigation
|
|
184
190
|
layout={layout}
|
|
185
191
|
logo={logo}
|
|
192
|
+
collapsedLogo={collapsedLogo}
|
|
186
193
|
navConfig={navigation}
|
|
187
194
|
setSystemAuth={setSystemAuth}
|
|
188
195
|
themeType={themeType}
|