@scalio-cloud/ui 0.23.0 → 0.24.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/CHANGELOG.md +11 -0
- package/README.md +28 -0
- package/nav.css +35 -0
- package/package.json +1 -1
- package/react/index.js +1 -1
- package/react/labels.js +27 -0
- package/react/wizard.js +239 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.24.0
|
|
4
|
+
Einrichtungsassistent als Standard für alle Apps (scalio-docs ADR 0060, contract `interfaces/setup-wizard.md`).
|
|
5
|
+
- New `SetupWizard`: the first-step dialog of Nexus and Kontor as a shared shell. Steps come as configuration
|
|
6
|
+
(`id`, `label`, `title`, `description`, `content`, `onNext`, `canNext`, `optional`); stepper on top, «Schritt n von
|
|
7
|
+
m», Zurück/Weiter, «Überspringen» for optional steps, errors of `onNext` keep the step open, short success message
|
|
8
|
+
at the end. Without `onClose` it cannot be closed; `wide` for steps with tables. Texts DE/EN in `labels.wizard`.
|
|
9
|
+
- `setupWizardApi(api, {base})` for `GET/PUT /api/setup/wizard` (reads snake_case and camelCase), `useSetupWizard()`
|
|
10
|
+
(loads the state, opens once by itself at the saved step while the setup is needed, saves each step, completes),
|
|
11
|
+
`openSetupWizard()` and `SetupWizardButton` for Einstellungen.
|
|
12
|
+
- Styles `.scalio-wizard*` in nav.css. No token changes, nothing changes until an app uses the wizard.
|
|
13
|
+
|
|
3
14
|
## 0.23.0
|
|
4
15
|
Benutzerverwaltung in allen Apps angleichen (scalio-docs ADR 0059, `users/B1`–`B6`, contract `interfaces/app-users.md`).
|
|
5
16
|
- New `UserAdmin` for Konten und Rollen → Benutzer, the same in every app: list with search and badges (Microsoft,
|
package/README.md
CHANGED
|
@@ -236,6 +236,34 @@ const users = usersApi(api); // once, outside the component
|
|
|
236
236
|
<MicrosoftConnection linked={user.microsoft} canUnlink={user.has_password} onLink={...} onUnlink={...} lang={lang} />
|
|
237
237
|
```
|
|
238
238
|
|
|
239
|
+
## Setup wizard (React)
|
|
240
|
+
|
|
241
|
+
Einrichtungsassistent after the first sign-in, the same in every app (ADR 0060, UISPEC section 3). The app writes
|
|
242
|
+
only its steps; the backend follows scalio-docs `interfaces/setup-wizard.md` (`GET/PUT /api/setup/wizard`).
|
|
243
|
+
|
|
244
|
+
```jsx
|
|
245
|
+
import { SetupWizard, SetupWizardButton, useSetupWizard, setupWizardApi } from "@scalio-cloud/ui/react";
|
|
246
|
+
|
|
247
|
+
const wizardClient = setupWizardApi(api); // once, outside the component ({ base: "/wizard" } for another route)
|
|
248
|
+
|
|
249
|
+
function AppWizard() {
|
|
250
|
+
const w = useSetupWizard({ client: wizardClient, enabled: isAdmin }); // opens by itself while needed
|
|
251
|
+
const steps = [
|
|
252
|
+
{ id: "tenant", label: t("Tenant"), title: t("Connect Microsoft"), description: "...", content: <TenantsSection />,
|
|
253
|
+
canNext: connected, onNext: async () => { /* save; a rejected promise keeps the step open */ } },
|
|
254
|
+
{ id: "mail", label: t("Email"), title: t("Email (SMTP)"), content: <SmtpForm />, optional: true },
|
|
255
|
+
];
|
|
256
|
+
return <SetupWizard {...w.props} lang={lang} title={t("Set up Foundry")} subtitle={t("...")} steps={steps}
|
|
257
|
+
success={{ title: t("Setup complete"), text: t("...") }} />;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Einstellungen: open it again (admins only)
|
|
261
|
+
<SetupWizardButton lang={lang} />
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Test IDs: `setup-wizard`, `wizard-stepper(-item)`, `wizard-step-{id}`, `wizard-next`, `wizard-back`, `wizard-skip`,
|
|
265
|
+
`wizard-close`, `wizard-error`, `wizard-success`, `wizard-done`, `settings-open-wizard`.
|
|
266
|
+
|
|
239
267
|
## Two-factor card and license panel (React)
|
|
240
268
|
|
|
241
269
|
The same backend routes in every app, so the apps pass only their axios instance and a few helpers.
|
package/nav.css
CHANGED
|
@@ -464,6 +464,41 @@
|
|
|
464
464
|
.scalio-dialog-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.5rem; }
|
|
465
465
|
.scalio-dialog-left { margin-right: auto; }
|
|
466
466
|
|
|
467
|
+
/* Setup wizard (SetupWizard, ADR 0060): design of the Nexus wizard, first built in Kontor */
|
|
468
|
+
.scalio-wizard-root { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; padding: 1rem; background: rgb(0 0 0 / 0.5); }
|
|
469
|
+
.scalio-wizard {
|
|
470
|
+
display: flex; flex-direction: column; gap: 1rem; width: calc(100vw - 2rem); max-width: 42rem; max-height: 90vh; overflow-y: auto;
|
|
471
|
+
padding: 1.5rem; border: 1px solid hsl(var(--border)); border-radius: 2px; background: hsl(var(--background)); color: hsl(var(--foreground));
|
|
472
|
+
box-shadow: 0 16px 40px rgb(0 0 0 / 0.25); font-size: 0.875rem;
|
|
473
|
+
}
|
|
474
|
+
.scalio-wizard.is-wide { max-width: 56rem; }
|
|
475
|
+
.scalio-wizard-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
|
|
476
|
+
.scalio-wizard-title { margin: 0; font-family: 'Chivo', sans-serif; font-size: 1.5rem; font-weight: 900; line-height: 1.2; text-transform: uppercase; letter-spacing: -0.025em; }
|
|
477
|
+
.scalio-wizard-subtitle { margin: 0.25rem 0 0; color: hsl(var(--muted-foreground)); }
|
|
478
|
+
.scalio-wizard-stepper { display: flex; align-items: center; gap: 0.5rem; margin: 0; padding: 0; list-style: none; }
|
|
479
|
+
.scalio-wizard-stepper-item { display: flex; flex: 1 1 0; min-width: 0; align-items: center; gap: 0.5rem; }
|
|
480
|
+
.scalio-wizard-dot {
|
|
481
|
+
display: flex; flex-shrink: 0; align-items: center; justify-content: center; width: 1.75rem; height: 1.75rem; border-radius: 999px;
|
|
482
|
+
background: hsl(var(--muted)); color: hsl(var(--muted-foreground)); font-size: 0.75rem; font-weight: 700;
|
|
483
|
+
}
|
|
484
|
+
.scalio-wizard-dot.is-current { background: hsl(var(--primary)); color: hsl(var(--primary-foreground)); }
|
|
485
|
+
.scalio-wizard-dot.is-done, .scalio-wizard-success-icon { background: #059669; color: #fff; }
|
|
486
|
+
.scalio-wizard-label { display: none; white-space: nowrap; font-size: 0.75rem; font-weight: 500; color: hsl(var(--muted-foreground)); }
|
|
487
|
+
.scalio-wizard-label.is-current { color: hsl(var(--foreground)); }
|
|
488
|
+
@media (min-width: 1024px) { .scalio-wizard-label { display: block; } }
|
|
489
|
+
.scalio-wizard-line { flex: 1 1 0; min-width: 0.5rem; height: 1px; background: hsl(var(--border)); }
|
|
490
|
+
.scalio-wizard-count { margin: 0; font-size: 0.75rem; font-weight: 500; color: hsl(var(--muted-foreground)); }
|
|
491
|
+
.scalio-wizard-step { display: flex; flex-direction: column; gap: 0.75rem; }
|
|
492
|
+
.scalio-wizard-step-title { margin: 0; font-size: 0.875rem; font-weight: 700; }
|
|
493
|
+
.scalio-wizard-text { margin: 0; color: hsl(var(--muted-foreground)); }
|
|
494
|
+
.scalio-wizard-actions { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-top: 0.5rem; }
|
|
495
|
+
.scalio-wizard-actions-right { display: flex; align-items: center; gap: 0.5rem; }
|
|
496
|
+
.scalio-wizard-success { display: flex; flex-direction: column; align-items: center; gap: 0.75rem; padding: 1.5rem 0; text-align: center; }
|
|
497
|
+
.scalio-wizard-success .scalio-wizard-step-title { font-size: 1.125rem; }
|
|
498
|
+
.scalio-wizard-success .scalio-wizard-text { max-width: 28rem; }
|
|
499
|
+
.scalio-wizard-success .scalio-btn { margin-top: 0.5rem; }
|
|
500
|
+
.scalio-wizard-success-icon { display: flex; align-items: center; justify-content: center; width: 3rem; height: 3rem; border-radius: 999px; }
|
|
501
|
+
|
|
467
502
|
/* License panel (LicensePanel) */
|
|
468
503
|
.scalio-license { border: 1px solid hsl(var(--border)); border-radius: 2px; background: hsl(var(--card)); color: hsl(var(--card-foreground)); font-size: 0.875rem; }
|
|
469
504
|
.scalio-license-part { padding: 1.25rem; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalio-cloud/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Scalio Control Room design tokens, Tailwind preset, self-hosted fonts, the shared app header, sign-in layout, sidebar and page blocks",
|
|
5
5
|
"license": "SEE LICENSE IN README.md",
|
|
6
6
|
"private": false,
|
package/react/index.js
CHANGED
|
@@ -378,6 +378,6 @@ module.exports = {
|
|
|
378
378
|
TEST_IDS,
|
|
379
379
|
initials,
|
|
380
380
|
};
|
|
381
|
-
Object.assign(module.exports, Nav, Blocks, require("./settings"), require("./support"), require("./beacon"), require("./security"), require("./license"), require("./palette"), require("./unify"), require("./profile"), require("./actions"), require("./selftest"), require("./users"));
|
|
381
|
+
Object.assign(module.exports, Nav, Blocks, require("./settings"), require("./support"), require("./beacon"), require("./security"), require("./license"), require("./palette"), require("./unify"), require("./profile"), require("./actions"), require("./selftest"), require("./users"), require("./wizard"));
|
|
382
382
|
// After the exports above: auth.js uses LanguageToggle and ThemeToggle from this module.
|
|
383
383
|
Object.assign(module.exports, require("./auth"));
|
package/react/labels.js
CHANGED
|
@@ -13,6 +13,19 @@ const LABELS = {
|
|
|
13
13
|
userMenu: "Benutzermenü",
|
|
14
14
|
menu: "Menü",
|
|
15
15
|
version: "Version",
|
|
16
|
+
wizard: {
|
|
17
|
+
step: "Schritt {n} von {total}",
|
|
18
|
+
back: "Zurück",
|
|
19
|
+
next: "Weiter",
|
|
20
|
+
finish: "Einrichtung abschliessen",
|
|
21
|
+
skip: "Überspringen",
|
|
22
|
+
close: "Später",
|
|
23
|
+
done: "Los geht's",
|
|
24
|
+
successTitle: "Einrichtung abgeschlossen",
|
|
25
|
+
successText: "Alles ist bereit. Sie können jede Einstellung später unter Einstellungen ändern.",
|
|
26
|
+
open: "Einrichtungsassistent",
|
|
27
|
+
openHint: "Die Fragen der Ersteinrichtung noch einmal durchgehen.",
|
|
28
|
+
},
|
|
16
29
|
// Sign-in and profile (same words in every app)
|
|
17
30
|
signIn: "Anmelden",
|
|
18
31
|
microsoft: "Mit Microsoft anmelden",
|
|
@@ -391,6 +404,19 @@ const LABELS = {
|
|
|
391
404
|
userMenu: "User menu",
|
|
392
405
|
menu: "Menu",
|
|
393
406
|
version: "Version",
|
|
407
|
+
wizard: {
|
|
408
|
+
step: "Step {n} of {total}",
|
|
409
|
+
back: "Back",
|
|
410
|
+
next: "Continue",
|
|
411
|
+
finish: "Finish setup",
|
|
412
|
+
skip: "Skip",
|
|
413
|
+
close: "Later",
|
|
414
|
+
done: "Let's go",
|
|
415
|
+
successTitle: "Setup complete",
|
|
416
|
+
successText: "Everything is ready. You can change any setting later under Settings.",
|
|
417
|
+
open: "Setup wizard",
|
|
418
|
+
openHint: "Go through the first-time setup questions again.",
|
|
419
|
+
},
|
|
394
420
|
signIn: "Sign in",
|
|
395
421
|
microsoft: "Sign in with Microsoft",
|
|
396
422
|
redirecting: "Redirecting …",
|
|
@@ -766,6 +792,7 @@ function labelsFor(lang, overrides) {
|
|
|
766
792
|
accountTabs: Object.assign({}, base.accountTabs, o.accountTabs),
|
|
767
793
|
passwordPolicy: Object.assign({}, base.passwordPolicy, o.passwordPolicy),
|
|
768
794
|
profile_: Object.assign({}, base.profile_, o.profile_),
|
|
795
|
+
wizard: Object.assign({}, base.wizard, o.wizard),
|
|
769
796
|
});
|
|
770
797
|
}
|
|
771
798
|
|
package/react/wizard.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Setup wizard ("Einrichtungsassistent", ADR 0060, UISPEC section 3): the same first-step dialog in every app.
|
|
3
|
+
// Design of the Nexus SetupWizard, first built in Kontor (#59): dialog, stepper on top, Zurück/Weiter, Überspringen
|
|
4
|
+
// for optional steps, a short success message at the end. The shell knows no app: steps come as configuration.
|
|
5
|
+
// Progress lives in the app's backend (GET/PUT /api/setup/wizard, scalio-docs interfaces/setup-wizard.md);
|
|
6
|
+
// setupWizardApi() and useSetupWizard() do the wiring, so an app only writes its steps.
|
|
7
|
+
const React = require("react");
|
|
8
|
+
const h = React.createElement;
|
|
9
|
+
const Icons = require("./icons");
|
|
10
|
+
const { labelsFor } = require("./labels");
|
|
11
|
+
const { Notice } = require("./blocks");
|
|
12
|
+
|
|
13
|
+
const OPEN_SETUP_WIZARD = "scalio:open-setup-wizard";
|
|
14
|
+
|
|
15
|
+
// Opens the wizard from anywhere (Einstellungen button, dashboard card). useSetupWizard listens for it.
|
|
16
|
+
function openSetupWizard(step) {
|
|
17
|
+
if (typeof window === "undefined") return;
|
|
18
|
+
window.dispatchEvent(new window.CustomEvent(OPEN_SETUP_WIZARD, { detail: { step: step || 0 } }));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeWizardState(d) {
|
|
22
|
+
const o = d || {};
|
|
23
|
+
const pick = (a, b) => (o[a] !== undefined ? o[a] : o[b]);
|
|
24
|
+
return {
|
|
25
|
+
needed: !!o.needed,
|
|
26
|
+
completed: !!o.completed,
|
|
27
|
+
lastStep: Number(pick("last_step", "lastStep")) || 0,
|
|
28
|
+
canEdit: pick("can_edit", "canEdit") === undefined ? true : !!pick("can_edit", "canEdit"),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The shared route. `api` is the app's axios-like client (get/put returning { data }).
|
|
33
|
+
function setupWizardApi(api, opts) {
|
|
34
|
+
const base = (opts && opts.base) || "/setup/wizard";
|
|
35
|
+
return {
|
|
36
|
+
get: () => api.get(base).then((r) => normalizeWizardState(r.data)),
|
|
37
|
+
save: (step) => api.put(base, { last_step: step }).then(() => true),
|
|
38
|
+
complete: () => api.put(base, { completed: true }).then(() => true),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// State, auto-open and saving for SetupWizard. Spread `props` into <SetupWizard> and add title and steps.
|
|
43
|
+
// Opens by itself once per mount while the setup is needed and the person may edit it, at the saved step;
|
|
44
|
+
// openSetupWizard() opens it again at the beginning. `closable: false` hides «Später» (e.g. nothing exists yet).
|
|
45
|
+
function useSetupWizard(opts) {
|
|
46
|
+
const client = opts && opts.client;
|
|
47
|
+
const enabled = !opts || opts.enabled !== false;
|
|
48
|
+
const [state, setState] = React.useState(null);
|
|
49
|
+
const [open, setOpen] = React.useState(false);
|
|
50
|
+
const [step, setStep] = React.useState(0);
|
|
51
|
+
const auto = React.useRef(false);
|
|
52
|
+
const stateRef = React.useRef(null);
|
|
53
|
+
stateRef.current = state;
|
|
54
|
+
|
|
55
|
+
const reload = React.useCallback(() => {
|
|
56
|
+
if (!client) return Promise.resolve(null);
|
|
57
|
+
return client.get().then((s) => { setState(s); return s; }, () => null);
|
|
58
|
+
}, [client]);
|
|
59
|
+
|
|
60
|
+
React.useEffect(() => {
|
|
61
|
+
if (enabled) reload();
|
|
62
|
+
}, [enabled, reload]);
|
|
63
|
+
|
|
64
|
+
React.useEffect(() => {
|
|
65
|
+
if (state && state.needed && state.canEdit && !auto.current) {
|
|
66
|
+
auto.current = true;
|
|
67
|
+
setStep(state.lastStep);
|
|
68
|
+
setOpen(true);
|
|
69
|
+
}
|
|
70
|
+
}, [state]);
|
|
71
|
+
|
|
72
|
+
React.useEffect(() => {
|
|
73
|
+
if (typeof window === "undefined") return undefined;
|
|
74
|
+
const onOpen = (e) => {
|
|
75
|
+
const s = stateRef.current;
|
|
76
|
+
if (s && !s.canEdit) return;
|
|
77
|
+
setStep((e && e.detail && e.detail.step) || 0);
|
|
78
|
+
setOpen(true);
|
|
79
|
+
};
|
|
80
|
+
window.addEventListener(OPEN_SETUP_WIZARD, onOpen);
|
|
81
|
+
return () => window.removeEventListener(OPEN_SETUP_WIZARD, onOpen);
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const close = () => setOpen(false);
|
|
85
|
+
const props = {
|
|
86
|
+
open,
|
|
87
|
+
step,
|
|
88
|
+
onStepChange: (n) => {
|
|
89
|
+
setStep(n);
|
|
90
|
+
if (client) client.save(n).catch(() => {});
|
|
91
|
+
},
|
|
92
|
+
onFinish: async () => {
|
|
93
|
+
if (client) await client.complete();
|
|
94
|
+
setState((s) => Object.assign({}, s, { needed: false, completed: true }));
|
|
95
|
+
if (opts && opts.onCompleted) opts.onCompleted();
|
|
96
|
+
},
|
|
97
|
+
onClose: opts && opts.closable === false ? undefined : close,
|
|
98
|
+
onDone: close,
|
|
99
|
+
};
|
|
100
|
+
return { state, open, setOpen, step, setStep, reload, props };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function StepDot({ index, current }) {
|
|
104
|
+
const cls = index < current ? "is-done" : index === current ? "is-current" : "";
|
|
105
|
+
return h("span", { className: `scalio-wizard-dot ${cls}`.trim() }, index < current ? h(Icons.Check, { width: 14, height: 14 }) : index + 1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function SetupWizard(props) {
|
|
109
|
+
const { open, title, subtitle, steps, step, onStepChange, onFinish, onClose, firstStepAction, onDone, testId, wide } = props;
|
|
110
|
+
const L = Object.assign({}, labelsFor(props.lang).wizard, props.labels);
|
|
111
|
+
const success = props.success || { title: L.successTitle, text: L.successText };
|
|
112
|
+
const [busy, setBusy] = React.useState(false);
|
|
113
|
+
const [error, setError] = React.useState(null);
|
|
114
|
+
const [finished, setFinished] = React.useState(false);
|
|
115
|
+
const list = steps || [];
|
|
116
|
+
const index = Math.min(Math.max(step || 0, 0), list.length - 1);
|
|
117
|
+
const current = list[index];
|
|
118
|
+
const last = index === list.length - 1;
|
|
119
|
+
|
|
120
|
+
React.useEffect(() => setError(null), [index]);
|
|
121
|
+
React.useEffect(() => {
|
|
122
|
+
if (!open) setFinished(false);
|
|
123
|
+
}, [open]);
|
|
124
|
+
React.useEffect(() => {
|
|
125
|
+
if (!open || !onClose) return undefined;
|
|
126
|
+
const onKey = (e) => {
|
|
127
|
+
if (e.key === "Escape" && !busy) onClose();
|
|
128
|
+
};
|
|
129
|
+
window.addEventListener("keydown", onKey);
|
|
130
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
131
|
+
}, [open, onClose, busy]);
|
|
132
|
+
|
|
133
|
+
if (!open || !current) return null;
|
|
134
|
+
|
|
135
|
+
async function next(save) {
|
|
136
|
+
setBusy(true);
|
|
137
|
+
setError(null);
|
|
138
|
+
try {
|
|
139
|
+
if (save && current.onNext) await current.onNext();
|
|
140
|
+
if (last) {
|
|
141
|
+
await onFinish();
|
|
142
|
+
setFinished(true);
|
|
143
|
+
} else {
|
|
144
|
+
onStepChange(index + 1);
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
setError((err && err.message) || String(err));
|
|
148
|
+
} finally {
|
|
149
|
+
setBusy(false);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const left = index > 0
|
|
154
|
+
? { label: L.back, onClick: () => onStepChange(index - 1) }
|
|
155
|
+
: firstStepAction || (onClose ? { label: L.close, onClick: onClose } : null);
|
|
156
|
+
|
|
157
|
+
const head = h(
|
|
158
|
+
"div",
|
|
159
|
+
{ className: "scalio-wizard-head" },
|
|
160
|
+
h("div", null,
|
|
161
|
+
h("h2", { className: "scalio-wizard-title" }, title),
|
|
162
|
+
subtitle && !finished ? h("p", { className: "scalio-wizard-subtitle" }, subtitle) : null),
|
|
163
|
+
onClose
|
|
164
|
+
? h("button", { type: "button", className: "scalio-btn scalio-btn-icon is-ghost", onClick: onClose, "aria-label": L.close, "data-testid": "wizard-close" },
|
|
165
|
+
h(Icons.X, null))
|
|
166
|
+
: null
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const body = finished
|
|
170
|
+
? h(
|
|
171
|
+
"div",
|
|
172
|
+
{ className: "scalio-wizard-success", "data-testid": "wizard-success" },
|
|
173
|
+
h("span", { className: "scalio-wizard-success-icon" }, h(Icons.Check, { width: 24, height: 24 })),
|
|
174
|
+
h("h3", { className: "scalio-wizard-step-title" }, success.title),
|
|
175
|
+
h("p", { className: "scalio-wizard-text" }, success.text),
|
|
176
|
+
h("button", { type: "button", className: "scalio-btn is-primary", onClick: onDone || onClose, "data-testid": "wizard-done", autoFocus: true }, L.done)
|
|
177
|
+
)
|
|
178
|
+
: h(
|
|
179
|
+
React.Fragment,
|
|
180
|
+
null,
|
|
181
|
+
h(
|
|
182
|
+
"ol",
|
|
183
|
+
{ className: "scalio-wizard-stepper", "data-testid": "wizard-stepper" },
|
|
184
|
+
list.map((s, i) =>
|
|
185
|
+
h(
|
|
186
|
+
"li",
|
|
187
|
+
{ key: s.id, className: "scalio-wizard-stepper-item", "data-testid": "wizard-stepper-item", "aria-current": i === index ? "step" : undefined },
|
|
188
|
+
h(StepDot, { index: i, current: index }),
|
|
189
|
+
h("span", { className: `scalio-wizard-label${i === index ? " is-current" : ""}` }, s.label),
|
|
190
|
+
i < list.length - 1 ? h("span", { className: "scalio-wizard-line" }) : null
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
),
|
|
194
|
+
h("p", { className: "scalio-wizard-count" }, L.step.replace("{n}", String(index + 1)).replace("{total}", String(list.length))),
|
|
195
|
+
h(
|
|
196
|
+
"section",
|
|
197
|
+
{ className: "scalio-wizard-step", "data-testid": `wizard-step-${current.id}` },
|
|
198
|
+
h("div", null,
|
|
199
|
+
h("h3", { className: "scalio-wizard-step-title" }, current.title),
|
|
200
|
+
current.description ? h("p", { className: "scalio-wizard-text" }, current.description) : null),
|
|
201
|
+
current.content
|
|
202
|
+
),
|
|
203
|
+
error ? h(Notice, { kind: "error", icon: false, testId: "wizard-error", lang: props.lang }, error) : null,
|
|
204
|
+
h(
|
|
205
|
+
"div",
|
|
206
|
+
{ className: "scalio-wizard-actions" },
|
|
207
|
+
left
|
|
208
|
+
? h("button", { type: "button", className: "scalio-btn", onClick: left.onClick, disabled: busy, "data-testid": "wizard-back" }, left.label)
|
|
209
|
+
: h("span", null),
|
|
210
|
+
h(
|
|
211
|
+
"div",
|
|
212
|
+
{ className: "scalio-wizard-actions-right" },
|
|
213
|
+
current.optional
|
|
214
|
+
? h("button", { type: "button", className: "scalio-btn is-ghost", onClick: () => next(false), disabled: busy, "data-testid": "wizard-skip" }, L.skip)
|
|
215
|
+
: null,
|
|
216
|
+
h(
|
|
217
|
+
"button",
|
|
218
|
+
{ type: "button", className: "scalio-btn is-primary", onClick: () => next(true), disabled: busy || current.canNext === false, "data-testid": "wizard-next" },
|
|
219
|
+
busy ? h("span", { className: "scalio-spinner", "aria-hidden": "true" }) : null,
|
|
220
|
+
last ? L.finish : L.next
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
return h(
|
|
227
|
+
"div",
|
|
228
|
+
{ className: "scalio-wizard-root", onMouseDown: (e) => { if (e.target === e.currentTarget && !busy && onClose) onClose(); } },
|
|
229
|
+
h("div", { role: "dialog", "aria-modal": "true", "aria-label": title, className: wide ? "scalio-wizard is-wide" : "scalio-wizard", "data-testid": testId || "setup-wizard" }, head, body)
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Button for Einstellungen (UISPEC: the wizard can always be opened again, by people who may edit the setup).
|
|
234
|
+
function SetupWizardButton({ lang, labels, testId, className }) {
|
|
235
|
+
const L = Object.assign({}, labelsFor(lang).wizard, labels);
|
|
236
|
+
return h("button", { type: "button", className: className || "scalio-btn", onClick: () => openSetupWizard(0), "data-testid": testId || "settings-open-wizard", title: L.openHint }, L.open);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = { SetupWizard, SetupWizardButton, useSetupWizard, setupWizardApi, normalizeWizardState, openSetupWizard, OPEN_SETUP_WIZARD };
|