@eifi1/ui-kit 0.29.0 → 0.31.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 +65 -6
- package/dist/account/account-parts.d.ts +21 -0
- package/dist/account/account-parts.js +40 -0
- package/dist/account/account-parts.js.map +1 -0
- package/dist/account/data-export-setting.d.ts +73 -0
- package/dist/account/data-export-setting.js +114 -0
- package/dist/account/data-export-setting.js.map +1 -0
- package/dist/account/delete-account-setting.d.ts +95 -0
- package/dist/account/delete-account-setting.js +200 -0
- package/dist/account/delete-account-setting.js.map +1 -0
- package/dist/account/email-change-setting.d.ts +136 -0
- package/dist/account/email-change-setting.js +278 -0
- package/dist/account/email-change-setting.js.map +1 -0
- package/dist/account/sessions-setting.d.ts +110 -0
- package/dist/account/sessions-setting.js +147 -0
- package/dist/account/sessions-setting.js.map +1 -0
- package/dist/admin/admin-action-confirm.d.ts +95 -0
- package/dist/admin/admin-action-confirm.js +276 -0
- package/dist/admin/admin-action-confirm.js.map +1 -0
- package/dist/admin/admin-action-log.d.ts +105 -0
- package/dist/admin/admin-action-log.js +153 -0
- package/dist/admin/admin-action-log.js.map +1 -0
- package/dist/admin/admin-parts.d.ts +40 -0
- package/dist/admin/admin-parts.js +45 -0
- package/dist/admin/admin-parts.js.map +1 -0
- package/dist/admin/invitations-panel.d.ts +95 -0
- package/dist/admin/invitations-panel.js +290 -0
- package/dist/admin/invitations-panel.js.map +1 -0
- package/dist/admin/reviewer-scope-editor.d.ts +95 -0
- package/dist/admin/reviewer-scope-editor.js +134 -0
- package/dist/admin/reviewer-scope-editor.js.map +1 -0
- package/dist/admin/role-select.d.ts +95 -0
- package/dist/admin/role-select.js +128 -0
- package/dist/admin/role-select.js.map +1 -0
- package/dist/admin/transfer-ownership-dialog.d.ts +95 -0
- package/dist/admin/transfer-ownership-dialog.js +83 -0
- package/dist/admin/transfer-ownership-dialog.js.map +1 -0
- package/dist/admin/user-roster.d.ts +95 -0
- package/dist/admin/user-roster.js +285 -0
- package/dist/admin/user-roster.js.map +1 -0
- package/dist/auth/auth-errors.d.ts +103 -6
- package/dist/auth/auth-errors.js +68 -2
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/forgot-password-form.d.ts +10 -3
- package/dist/auth/forgot-password-form.js +6 -3
- package/dist/auth/forgot-password-form.js.map +1 -1
- package/dist/auth/register-form.d.ts +21 -12
- package/dist/auth/register-form.js +6 -4
- package/dist/auth/register-form.js.map +1 -1
- package/dist/auth/reset-password-form.d.ts +10 -3
- package/dist/auth/reset-password-form.js +6 -3
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form.d.ts +75 -9
- package/dist/auth/sign-in-form.js +105 -15
- package/dist/auth/sign-in-form.js.map +1 -1
- package/dist/auth/verify-email.d.ts +7 -2
- package/dist/auth/verify-email.js +44 -8
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/chart.d.ts +20 -11
- package/dist/components/account-chips.d.ts +47 -7
- package/dist/components/account-chips.js +19 -3
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/account-settings-labels.d.ts +24 -1
- package/dist/components/account-settings-labels.js +6 -2
- package/dist/components/account-settings-labels.js.map +1 -1
- package/dist/components/account-settings.d.ts +69 -8
- package/dist/components/account-settings.js +133 -25
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +21 -12
- package/dist/components/button-group.d.ts +21 -12
- package/dist/components/calculator.d.ts +21 -12
- package/dist/components/confirm-dialog.d.ts +21 -12
- package/dist/components/copy-button.d.ts +18 -9
- package/dist/components/danger-confirm.d.ts +21 -12
- package/dist/components/data-table.js +8 -1
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +9 -4
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/facing-pair.d.ts +21 -12
- package/dist/components/file-button.d.ts +21 -12
- package/dist/components/file-dropzone.d.ts +21 -12
- package/dist/components/form-actions.d.ts +21 -12
- package/dist/components/iban-input.d.ts +21 -12
- package/dist/components/language-select.d.ts +21 -12
- package/dist/components/legal-page.d.ts +21 -12
- package/dist/components/legal.d.ts +21 -12
- package/dist/components/money-field.d.ts +21 -12
- package/dist/components/number-field.d.ts +21 -12
- package/dist/components/number-input.d.ts +21 -12
- package/dist/components/numpad-sheet.d.ts +21 -12
- package/dist/components/passkeys-setting.js +2 -1
- package/dist/components/passkeys-setting.js.map +1 -1
- package/dist/components/phone-input.d.ts +21 -12
- package/dist/components/series-chart.d.ts +21 -12
- package/dist/components/settings-fields.d.ts +55 -13
- package/dist/components/settings-fields.js +20 -1
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/share-card.d.ts +20 -11
- package/dist/components/text-link.d.ts +21 -12
- package/dist/components/time-input.d.ts +21 -12
- package/dist/components/ui.d.ts +17 -8
- package/dist/{register-form-V1OeNlMr.d.ts → delete-account-setting-DCFS5Vof.d.ts} +6612 -5612
- package/dist/demo/demo-banner.d.ts +51 -0
- package/dist/demo/demo-banner.js +97 -0
- package/dist/demo/demo-banner.js.map +1 -0
- package/dist/demo/demo-countdown.d.ts +58 -0
- package/dist/demo/demo-countdown.js +79 -0
- package/dist/demo/demo-countdown.js.map +1 -0
- package/dist/demo/demo-ended.d.ts +45 -0
- package/dist/demo/demo-ended.js +42 -0
- package/dist/demo/demo-ended.js.map +1 -0
- package/dist/demo/demo-labels.d.ts +53 -0
- package/dist/demo/demo-labels.js +29 -0
- package/dist/demo/demo-labels.js.map +1 -0
- package/dist/demo/demo-session.d.ts +30 -0
- package/dist/demo/demo-session.js +8 -0
- package/dist/demo/demo-session.js.map +1 -0
- package/dist/demo/demo-start.d.ts +54 -0
- package/dist/demo/demo-start.js +68 -0
- package/dist/demo/demo-start.js.map +1 -0
- package/dist/feedback/feedback-attachment.d.ts +21 -12
- package/dist/feedback/feedback-context.d.ts +21 -12
- package/dist/feedback/feedback-crash.d.ts +21 -12
- package/dist/feedback/feedback-dialog.d.ts +21 -12
- package/dist/feedback/feedback-inbox.d.ts +21 -12
- package/dist/feedback/feedback-inbox.js +12 -2
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-labels.d.ts +21 -12
- package/dist/feedback/feedback-labels.js +1 -0
- package/dist/feedback/feedback-labels.js.map +1 -1
- package/dist/feedback/feedback-menu.d.ts +21 -12
- package/dist/feedback/feedback-record.d.ts +21 -12
- package/dist/feedback/feedback-record.js +2 -0
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +21 -12
- package/dist/feedback/feedback-row-detail.js +7 -0
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.d.ts +21 -12
- package/dist/feedback/feedback-submit.d.ts +21 -12
- package/dist/feedback/feedback-swipe.d.ts +22 -13
- package/dist/feedback/feedback-swipe.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +18 -9
- package/dist/feedback/feedback-thread.d.ts +21 -12
- package/dist/feedback.d.ts +21 -12
- package/dist/hooks/use-anchored-panel.d.ts +6 -2
- package/dist/hooks/use-anchored-panel.js +2 -1
- package/dist/hooks/use-anchored-panel.js.map +1 -1
- package/dist/hooks/use-file-drop.d.ts +21 -12
- package/dist/i18n/defaults.d.ts +21 -12
- package/dist/i18n/defaults.js +28 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +21 -12
- package/dist/i18n/german.js +298 -12
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +14 -5
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +15 -6
- package/dist/i18n/locales/de-CH.d.ts +21 -12
- package/dist/i18n/locales/en.d.ts +21 -12
- package/dist/i18n/locales/es.d.ts +21 -12
- package/dist/i18n/locales/es.js +287 -10
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +21 -12
- package/dist/i18n/locales/fr.js +290 -7
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +21 -12
- package/dist/i18n/locales/hu.js +288 -12
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +21 -12
- package/dist/i18n/locales/it.js +289 -7
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +21 -12
- package/dist/i18n/locales/zh.js +279 -7
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +21 -12
- package/dist/i18n/review.js +50 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +31 -4
- package/dist/index.js +148 -0
- package/dist/index.js.map +1 -1
- package/dist/landing/access.d.ts +48 -0
- package/dist/landing/access.js +38 -0
- package/dist/landing/access.js.map +1 -0
- package/dist/landing/landing-actions.d.ts +138 -0
- package/dist/landing/landing-actions.js +39 -0
- package/dist/landing/landing-actions.js.map +1 -0
- package/dist/landing/landing-labels.d.ts +43 -0
- package/dist/landing/landing-labels.js +23 -0
- package/dist/landing/landing-labels.js.map +1 -0
- package/dist/landing/landing-sections.d.ts +218 -0
- package/dist/landing/landing-sections.js +167 -0
- package/dist/landing/landing-sections.js.map +1 -0
- package/dist/landing/page-seo.d.ts +99 -0
- package/dist/landing/page-seo.js +121 -0
- package/dist/landing/page-seo.js.map +1 -0
- package/dist/landing/public-header.d.ts +148 -0
- package/dist/landing/public-header.js +49 -0
- package/dist/landing/public-header.js.map +1 -0
- package/dist/landing/routing.d.ts +218 -0
- package/dist/landing/routing.js +81 -0
- package/dist/landing/routing.js.map +1 -0
- package/dist/rhf/fields.d.ts +21 -12
- package/dist/rhf/form.d.ts +21 -12
- package/dist/rhf.d.ts +21 -12
- package/dist/settings/settings-catalogue.d.ts +84 -0
- package/dist/settings/settings-catalogue.js +47 -0
- package/dist/settings/settings-catalogue.js.map +1 -0
- package/dist/settings/settings-context.d.ts +31 -0
- package/dist/settings/settings-context.js +11 -0
- package/dist/settings/settings-context.js.map +1 -0
- package/dist/settings/settings-heading.d.ts +50 -0
- package/dist/settings/settings-heading.js +27 -0
- package/dist/settings/settings-heading.js.map +1 -0
- package/dist/settings/settings-labels.d.ts +41 -0
- package/dist/settings/settings-labels.js +38 -0
- package/dist/settings/settings-labels.js.map +1 -0
- package/dist/settings/settings-layout.d.ts +77 -0
- package/dist/settings/settings-layout.js +242 -0
- package/dist/settings/settings-layout.js.map +1 -0
- package/dist/settings/settings-section.d.ts +133 -0
- package/dist/settings/settings-section.js +63 -0
- package/dist/settings/settings-section.js.map +1 -0
- package/dist/settings/use-account-language.d.ts +83 -0
- package/dist/settings/use-account-language.js +62 -0
- package/dist/settings/use-account-language.js.map +1 -0
- package/dist/settings/use-settings-focus.d.ts +34 -0
- package/dist/settings/use-settings-focus.js +77 -0
- package/dist/settings/use-settings-focus.js.map +1 -0
- package/dist/settings/use-settings-route.d.ts +118 -0
- package/dist/settings/use-settings-route.js +120 -0
- package/dist/settings/use-settings-route.js.map +1 -0
- package/dist/shell/app-shell.d.ts +27 -12
- package/dist/shell/app-shell.js +3 -2
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/top-bar-brand.d.ts +21 -12
- package/dist/shell.d.ts +25 -6
- package/dist/shell.js +56 -0
- package/dist/shell.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +21 -12
- package/dist/wizard.d.ts +21 -12
- package/package.json +2 -1
- package/src/account/account-parts.ts +67 -0
- package/src/account/data-export-setting.tsx +219 -0
- package/src/account/delete-account-setting.tsx +363 -0
- package/src/account/email-change-setting.tsx +454 -0
- package/src/account/sessions-setting.tsx +284 -0
- package/src/admin/admin-action-confirm.tsx +495 -0
- package/src/admin/admin-action-log.tsx +297 -0
- package/src/admin/admin-parts.ts +82 -0
- package/src/admin/invitations-panel.tsx +523 -0
- package/src/admin/reviewer-scope-editor.tsx +234 -0
- package/src/admin/role-select.tsx +286 -0
- package/src/admin/transfer-ownership-dialog.tsx +172 -0
- package/src/admin/user-roster.tsx +573 -0
- package/src/auth/auth-errors.ts +196 -8
- package/src/auth/forgot-password-form.tsx +21 -5
- package/src/auth/register-form.tsx +24 -6
- package/src/auth/reset-password-form.tsx +19 -5
- package/src/auth/sign-in-form.tsx +241 -38
- package/src/auth/verify-email.tsx +59 -8
- package/src/components/account-chips.tsx +64 -7
- package/src/components/account-settings-labels.ts +28 -1
- package/src/components/account-settings.tsx +231 -34
- package/src/components/data-table.tsx +8 -0
- package/src/components/date-picker.tsx +9 -4
- package/src/components/passkeys-setting.tsx +2 -1
- package/src/components/settings-fields.tsx +50 -1
- package/src/demo/demo-banner.tsx +159 -0
- package/src/demo/demo-countdown.ts +141 -0
- package/src/demo/demo-ended.tsx +89 -0
- package/src/demo/demo-labels.ts +78 -0
- package/src/demo/demo-session.ts +32 -0
- package/src/demo/demo-start.tsx +150 -0
- package/src/feedback/feedback-inbox.tsx +21 -8
- package/src/feedback/feedback-labels.ts +2 -1
- package/src/feedback/feedback-record.ts +10 -7
- package/src/feedback/feedback-row-detail.tsx +28 -4
- package/src/feedback/feedback-swipe.tsx +1 -1
- package/src/hooks/use-anchored-panel.ts +14 -3
- package/src/i18n/defaults.ts +28 -0
- package/src/i18n/german.ts +327 -5
- package/src/i18n/kit-labels.tsx +42 -0
- package/src/i18n/locales/es.ts +313 -3
- package/src/i18n/locales/fr.ts +316 -0
- package/src/i18n/locales/hu.ts +308 -5
- package/src/i18n/locales/it.ts +313 -0
- package/src/i18n/locales/zh.ts +286 -0
- package/src/i18n/review.ts +49 -0
- package/src/index.ts +184 -0
- package/src/landing/access.ts +97 -0
- package/src/landing/landing-actions.tsx +92 -0
- package/src/landing/landing-labels.ts +59 -0
- package/src/landing/landing-sections.tsx +337 -0
- package/src/landing/page-seo.ts +251 -0
- package/src/landing/public-header.tsx +120 -0
- package/src/landing/routing.tsx +209 -0
- package/src/settings/settings-catalogue.ts +132 -0
- package/src/settings/settings-context.ts +33 -0
- package/src/settings/settings-heading.tsx +65 -0
- package/src/settings/settings-labels.ts +75 -0
- package/src/settings/settings-layout.tsx +379 -0
- package/src/settings/settings-section.tsx +108 -0
- package/src/settings/use-account-language.ts +146 -0
- package/src/settings/use-settings-focus.ts +120 -0
- package/src/settings/use-settings-route.ts +258 -0
- package/src/shell/app-shell.tsx +10 -2
- package/src/shell.ts +45 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Children, useContext, useEffect, useId, useRef, useState } from "react";
|
|
4
|
+
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
5
|
+
import { AlertBanner } from "../components/alert-banner.js";
|
|
6
|
+
import { Checkbox } from "../components/checkbox.js";
|
|
7
|
+
import { DEFAULT_DANGER_CONFIRM_LABELS, TypedConfirmField, typedMatches } from "../components/danger-confirm.js";
|
|
8
|
+
import { DialogFrame } from "../components/dialog-frame.js";
|
|
9
|
+
import { ModalCloseContext } from "../components/modal.js";
|
|
10
|
+
import { Button } from "../components/ui.js";
|
|
11
|
+
import { authErrorCode } from "../auth/auth-errors.js";
|
|
12
|
+
import { hasMessage, usePersonLabel } from "./admin-parts.js";
|
|
13
|
+
const DEFAULT_ADMIN_ACTION_LABELS = {
|
|
14
|
+
confirm: "Confirm",
|
|
15
|
+
cancel: "Cancel",
|
|
16
|
+
close: "Close",
|
|
17
|
+
lastAdmin: "This is the last active administrator. Make someone else an administrator first.",
|
|
18
|
+
self: "You can\u2019t do this to your own account.",
|
|
19
|
+
confirmationMismatch: "The address doesn\u2019t match this account. Check which account you are on.",
|
|
20
|
+
otherCompanies: "This account also belongs to other companies, so only the platform operator can do this. You can remove it from this company instead.",
|
|
21
|
+
removeFromCompany: "Remove from company",
|
|
22
|
+
failed: "That didn\u2019t work. Please try again."
|
|
23
|
+
};
|
|
24
|
+
function isThenable(value) {
|
|
25
|
+
return typeof value === "object" && value !== null && typeof value.then === "function";
|
|
26
|
+
}
|
|
27
|
+
function refusalOf(error, present, describeError, labels) {
|
|
28
|
+
if (!present) return null;
|
|
29
|
+
const code = authErrorCode(error);
|
|
30
|
+
const own = describeError?.(error);
|
|
31
|
+
if (hasMessage(own)) return { text: own, code };
|
|
32
|
+
const text = code === "last_admin" ? labels.lastAdmin : code === "self_action" ? labels.self : code === "confirmation_mismatch" ? labels.confirmationMismatch : code === "other_companies" ? labels.otherCompanies : labels.failed;
|
|
33
|
+
return { text, code };
|
|
34
|
+
}
|
|
35
|
+
const FIELD = 'input:not([type="hidden"]):not(:disabled), select:not(:disabled), textarea:not(:disabled), [role="combobox"]';
|
|
36
|
+
function AdminActionConfirm({
|
|
37
|
+
level,
|
|
38
|
+
target,
|
|
39
|
+
confirmLabel,
|
|
40
|
+
title,
|
|
41
|
+
consequence,
|
|
42
|
+
acknowledgeLabel,
|
|
43
|
+
tone: toneProp,
|
|
44
|
+
onConfirm,
|
|
45
|
+
onClose,
|
|
46
|
+
onRemoveFromCompany,
|
|
47
|
+
describeError,
|
|
48
|
+
error: errorProp,
|
|
49
|
+
busy: busyProp,
|
|
50
|
+
children,
|
|
51
|
+
confirmDisabledReason,
|
|
52
|
+
open,
|
|
53
|
+
commit,
|
|
54
|
+
labels: labelsProp
|
|
55
|
+
}) {
|
|
56
|
+
const labels = useKitLabels("adminAction", DEFAULT_ADMIN_ACTION_LABELS, labelsProp);
|
|
57
|
+
const guardLabels = useKitLabels("dangerConfirm", DEFAULT_DANGER_CONFIRM_LABELS);
|
|
58
|
+
const nameOf = usePersonLabel();
|
|
59
|
+
const isOpen = open ?? true;
|
|
60
|
+
const [typed, setTyped] = useState("");
|
|
61
|
+
const [ticked, setTicked] = useState(false);
|
|
62
|
+
const [pending, setPending] = useState(false);
|
|
63
|
+
const [failure, setFailure] = useState(null);
|
|
64
|
+
const [done, setDone] = useState(false);
|
|
65
|
+
const confirmed = useRef(false);
|
|
66
|
+
const mounted = useRef(true);
|
|
67
|
+
const started = useRef(false);
|
|
68
|
+
const formId = useId();
|
|
69
|
+
const slotRef = useRef(null);
|
|
70
|
+
const guardRef = useRef(null);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
mounted.current = true;
|
|
73
|
+
return () => {
|
|
74
|
+
mounted.current = false;
|
|
75
|
+
};
|
|
76
|
+
}, []);
|
|
77
|
+
const [wasOpen, setWasOpen] = useState(isOpen);
|
|
78
|
+
if (wasOpen !== isOpen) {
|
|
79
|
+
setWasOpen(isOpen);
|
|
80
|
+
if (!isOpen) {
|
|
81
|
+
setTyped("");
|
|
82
|
+
setTicked(false);
|
|
83
|
+
setFailure(null);
|
|
84
|
+
setDone(false);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const busy = Boolean(busyProp) || pending;
|
|
88
|
+
const perform = (call) => {
|
|
89
|
+
let result;
|
|
90
|
+
try {
|
|
91
|
+
result = call();
|
|
92
|
+
} catch (caught) {
|
|
93
|
+
setFailure({ error: caught });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!isThenable(result)) return;
|
|
97
|
+
setFailure(null);
|
|
98
|
+
setPending(true);
|
|
99
|
+
result.then(
|
|
100
|
+
() => {
|
|
101
|
+
if (!mounted.current) return;
|
|
102
|
+
setPending(false);
|
|
103
|
+
confirmed.current = true;
|
|
104
|
+
setDone(true);
|
|
105
|
+
},
|
|
106
|
+
(caught) => {
|
|
107
|
+
if (!mounted.current) return;
|
|
108
|
+
setPending(false);
|
|
109
|
+
setFailure({ error: caught });
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (level !== "none") return;
|
|
115
|
+
if (!isOpen) {
|
|
116
|
+
started.current = false;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (started.current) return;
|
|
120
|
+
started.current = true;
|
|
121
|
+
confirmed.current = false;
|
|
122
|
+
let result;
|
|
123
|
+
try {
|
|
124
|
+
result = onConfirm({});
|
|
125
|
+
} catch (caught) {
|
|
126
|
+
result = Promise.reject(caught);
|
|
127
|
+
}
|
|
128
|
+
if (!isThenable(result)) return;
|
|
129
|
+
result.then(
|
|
130
|
+
() => {
|
|
131
|
+
if (mounted.current) onClose(true);
|
|
132
|
+
},
|
|
133
|
+
(caught) => {
|
|
134
|
+
if (mounted.current) setFailure({ error: caught });
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
}, [level, isOpen, onConfirm, onClose]);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (!isOpen || level === "none") return;
|
|
140
|
+
const field = slotRef.current?.querySelector(FIELD) ?? guardRef.current;
|
|
141
|
+
field?.focus();
|
|
142
|
+
}, [isOpen, level]);
|
|
143
|
+
const hasRefusal = failure !== null || errorProp !== void 0;
|
|
144
|
+
const refusal = refusalOf(failure ? failure.error : errorProp, hasRefusal, describeError, labels);
|
|
145
|
+
const offerRemoval = refusal?.code === "other_companies" && onRemoveFromCompany !== void 0;
|
|
146
|
+
const name = nameOf(target);
|
|
147
|
+
const identity = /* @__PURE__ */ jsx("span", { "data-private": true, children: name && name !== target.email ? `${name} \xB7 ${target.email}` : target.email });
|
|
148
|
+
const heading = title ?? confirmLabel ?? labels.confirm;
|
|
149
|
+
const refusalBanner = refusal && /* @__PURE__ */ jsx(
|
|
150
|
+
AlertBanner,
|
|
151
|
+
{
|
|
152
|
+
tone: "danger",
|
|
153
|
+
size: "sm",
|
|
154
|
+
action: offerRemoval ? /* @__PURE__ */ jsx(
|
|
155
|
+
Button,
|
|
156
|
+
{
|
|
157
|
+
type: "button",
|
|
158
|
+
size: "sm",
|
|
159
|
+
variant: "secondary",
|
|
160
|
+
disabled: busy,
|
|
161
|
+
onClick: () => perform(() => onRemoveFromCompany()),
|
|
162
|
+
children: labels.removeFromCompany
|
|
163
|
+
}
|
|
164
|
+
) : void 0,
|
|
165
|
+
children: refusal.text
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
if (level === "none") {
|
|
169
|
+
if (!isOpen || !refusal) return null;
|
|
170
|
+
return /* @__PURE__ */ jsx(
|
|
171
|
+
DialogFrame,
|
|
172
|
+
{
|
|
173
|
+
role: "alertdialog",
|
|
174
|
+
title: heading,
|
|
175
|
+
description: identity,
|
|
176
|
+
onClose: () => onClose(confirmed.current),
|
|
177
|
+
actions: (close) => /* @__PURE__ */ jsx(CloseOnDone, { done, children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", disabled: busy, onClick: close, children: labels.close }) }),
|
|
178
|
+
children: refusalBanner
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const tone = toneProp ?? (level === "type_email" ? "danger" : "warning");
|
|
183
|
+
const askTyped = level === "type_email";
|
|
184
|
+
const typedOk = !askTyped || typedMatches(typed, target.email, "caseless");
|
|
185
|
+
const tickOk = askTyped || ticked;
|
|
186
|
+
const guardReason = hasMessage(confirmDisabledReason) ? confirmDisabledReason : !tickOk ? guardLabels.needsAcknowledge : !typedOk ? typeof guardLabels.needsPhrase === "function" ? guardLabels.needsPhrase(target.email) : guardLabels.needsPhrase : void 0;
|
|
187
|
+
const submit = () => {
|
|
188
|
+
if (busy || guardReason !== void 0) return;
|
|
189
|
+
perform(
|
|
190
|
+
() => onConfirm(askTyped ? { confirmEmail: typed.trim(), acknowledged: true } : { acknowledged: true })
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
const lines = Array.isArray(consequence) ? consequence.map(
|
|
194
|
+
(item, index) => typeof item === "string" ? { key: item, text: item, severe: false } : { key: item.key ?? String(index), text: item.text, severe: Boolean(item.severe) }
|
|
195
|
+
) : null;
|
|
196
|
+
return /* @__PURE__ */ jsx(
|
|
197
|
+
DialogFrame,
|
|
198
|
+
{
|
|
199
|
+
open,
|
|
200
|
+
role: "alertdialog",
|
|
201
|
+
title: heading,
|
|
202
|
+
description: identity,
|
|
203
|
+
onClose: () => onClose(confirmed.current),
|
|
204
|
+
onKeyDown: (event) => {
|
|
205
|
+
if (busy && event.key === "Escape") event.preventDefault();
|
|
206
|
+
},
|
|
207
|
+
"aria-busy": busy || void 0,
|
|
208
|
+
actions: (close) => /* @__PURE__ */ jsxs(CloseOnDone, { done, children: [
|
|
209
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", disabled: busy, onClick: close, children: labels.cancel }),
|
|
210
|
+
/* @__PURE__ */ jsx(
|
|
211
|
+
Button,
|
|
212
|
+
{
|
|
213
|
+
type: "submit",
|
|
214
|
+
form: formId,
|
|
215
|
+
variant: tone === "danger" ? "danger" : "primary",
|
|
216
|
+
pending: busy,
|
|
217
|
+
commit,
|
|
218
|
+
disabledReason: busy ? void 0 : guardReason,
|
|
219
|
+
children: confirmLabel ?? labels.confirm
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
] }),
|
|
223
|
+
children: /* @__PURE__ */ jsxs(
|
|
224
|
+
"form",
|
|
225
|
+
{
|
|
226
|
+
id: formId,
|
|
227
|
+
noValidate: true,
|
|
228
|
+
className: "space-y-3",
|
|
229
|
+
onSubmit: (event) => {
|
|
230
|
+
event.preventDefault();
|
|
231
|
+
submit();
|
|
232
|
+
},
|
|
233
|
+
children: [
|
|
234
|
+
lines ? /* @__PURE__ */ jsx("ul", { className: "list-disc space-y-1 ps-5 text-sm text-[var(--text-secondary)]", children: lines.map((line) => /* @__PURE__ */ jsx("li", { className: line.severe ? "text-[var(--danger)]" : void 0, children: line.text }, line.key)) }) : hasMessage(consequence) && /* @__PURE__ */ jsx("p", { className: "text-sm text-[var(--text-secondary)]", children: consequence }),
|
|
235
|
+
Children.toArray(children).some(hasMessage) && /* @__PURE__ */ jsx("div", { ref: slotRef, className: "space-y-3", children }),
|
|
236
|
+
askTyped ? /* @__PURE__ */ jsx(
|
|
237
|
+
TypedConfirmField,
|
|
238
|
+
{
|
|
239
|
+
ref: guardRef,
|
|
240
|
+
target: target.email,
|
|
241
|
+
match: "caseless",
|
|
242
|
+
value: typed,
|
|
243
|
+
onValueChange: setTyped,
|
|
244
|
+
busy,
|
|
245
|
+
"data-private": true
|
|
246
|
+
}
|
|
247
|
+
) : /* @__PURE__ */ jsx(
|
|
248
|
+
Checkbox,
|
|
249
|
+
{
|
|
250
|
+
ref: guardRef,
|
|
251
|
+
label: acknowledgeLabel ?? guardLabels.acknowledge,
|
|
252
|
+
checked: ticked,
|
|
253
|
+
onCheckedChange: (next) => {
|
|
254
|
+
if (!busy) setTicked(next);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
),
|
|
258
|
+
refusalBanner
|
|
259
|
+
]
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
function CloseOnDone({ done, children }) {
|
|
266
|
+
const close = useContext(ModalCloseContext);
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
if (done) close?.();
|
|
269
|
+
}, [done, close]);
|
|
270
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
271
|
+
}
|
|
272
|
+
export {
|
|
273
|
+
AdminActionConfirm,
|
|
274
|
+
DEFAULT_ADMIN_ACTION_LABELS
|
|
275
|
+
};
|
|
276
|
+
//# sourceMappingURL=admin-action-confirm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/admin/admin-action-confirm.tsx"],"sourcesContent":["import { Children, useContext, useEffect, useId, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Checkbox } from \"../components/checkbox\";\nimport { DEFAULT_DANGER_CONFIRM_LABELS, TypedConfirmField, typedMatches } from \"../components/danger-confirm\";\nimport type { DangerConsequence } from \"../components/danger-confirm\";\nimport { DialogFrame } from \"../components/dialog-frame\";\nimport { ModalCloseContext } from \"../components/modal\";\nimport { Button } from \"../components/ui\";\nimport { authErrorCode } from \"../auth/auth-errors\";\nimport type { KitErrorCode } from \"../auth/auth-errors\";\nimport { hasMessage, usePersonLabel } from \"./admin-parts\";\nimport type { AdminPerson, MaybePromise } from \"./admin-parts\";\n\n/**\n * The confirmation in front of an admin action, at the level the SERVER states\n * (docs/user-admin-harmonization.md §4.2):\n *\n * | level | what it asks | actions |\n * |---|---|---|\n * | `none` | nothing: it confirms at once and renders nothing | resend a mail, reactivate, the reviewer scope… |\n * | `acknowledge` | a tick under the consequence | force a password change, change a role |\n * | `type_email` | the target's address, typed | deactivate, erase now, transfer |\n *\n * keksdose decides the level per action and per account on the server\n * (`admin_user_actions_service`) and Kurvenschmiede asks for the typed address on its\n * two hard-to-undo actions; both drew the dialog by hand beside the kit's `DangerConfirm`\n * and `useConfirm`. The level is a PROP, never derived here: the server enforces it and\n * a hand-written request never sees this dialog, so a page that guessed its own level\n * could only disagree with the server. The comparison (caseless), the field and the tick\n * are `DangerConfirm`'s (`TypedConfirmField`, the `dangerConfirm` words), so the three\n * confirmations in the kit cannot disagree about what counts as the address.\n *\n * WHY TYPE THE TARGET'S ADDRESS and not the admin's password: the admin proved who they\n * are at the admin gate. What is unproven is which ROW they are on (keksdose's\n * `TYPE_EMAIL` rule, Kurvenschmiede's `TypedConfirm`).\n *\n * THE REFUSALS (§4.1, §9.6) are read by code — `authErrorCode`, which knows the account\n * codes beside the sign-in ones — and stay in the dialog, which stays open so the admin\n * can read them: `last_admin`, `self_action`, `confirmation_mismatch`, and kastlan's\n * `other_companies` — with \"Remove from company\" offered in its place when the app passes\n * `onRemoveFromCompany`. Anything else is `describeError`'s words, else \"That didn't work\".\n * At level `none` nothing is drawn while the action runs; a refusal opens a dialog of its\n * own to say it.\n *\n * It never sends a request: `onConfirm` does, with what the guards were answered with.\n */\n\n/** How much confirmation an action needs — the server's `confirmation_level(action)`. */\nexport type AdminConfirmLevel = \"none\" | \"acknowledge\" | \"type_email\";\n\n/** What {@link AdminActionConfirmProps.onConfirm} receives — the guards' answers, for\n * the request body (the server re-checks both). */\nexport interface AdminActionConfirmValues {\n /** `type_email`: the address as typed — trimmed, its case the admin's; the body's\n * `confirm_email`, which the server compares with the account's (server-kit's\n * `confirm_email_matches`). */\n confirmEmail?: string;\n /** `acknowledge` and `type_email`: the action was confirmed past a guard — the body's\n * `acknowledged`, which every admin action carries (server-kit 0.4). */\n acknowledged?: true;\n}\n\n/** The account the action is about. */\nexport type AdminActionTarget = AdminPerson & { email: string };\n\n/** The `adminAction` namespace. The tick, the typed field and their held-button reasons\n * are the `dangerConfirm` namespace's, shared with `DangerConfirm`. */\nexport interface AdminActionLabels {\n /** The confirm button when the action names none. */\n confirm: string;\n cancel: string;\n /** The refusal dialog's only button (level `none`). */\n close: string;\n /** `409 last_admin`. */\n lastAdmin: string;\n /** `409 self_action`: the admin's own account. */\n self: string;\n /** `confirmation_mismatch`: the typed address is not this account's — the page and\n * the server disagree about which row this is. */\n confirmationMismatch: string;\n /** kastlan's `409 other_companies` (§9.6). */\n otherCompanies: string;\n /** The alternative offered with it. */\n removeFromCompany: string;\n /** Any other failure, unless `describeError` says better. */\n failed: string;\n}\n\nexport const DEFAULT_ADMIN_ACTION_LABELS: AdminActionLabels = {\n confirm: \"Confirm\",\n cancel: \"Cancel\",\n close: \"Close\",\n lastAdmin: \"This is the last active administrator. Make someone else an administrator first.\",\n self: \"You can’t do this to your own account.\",\n confirmationMismatch: \"The address doesn’t match this account. Check which account you are on.\",\n otherCompanies:\n \"This account also belongs to other companies, so only the platform operator can do this. You can remove it from this company instead.\",\n removeFromCompany: \"Remove from company\",\n failed: \"That didn’t work. Please try again.\",\n};\n\nexport interface AdminActionConfirmProps {\n /** The level the server states for this action on this account (§4.2). */\n level: AdminConfirmLevel;\n /** Whom it is about: the address (typed at `type_email`) and the name parts. */\n target: AdminActionTarget;\n /** The confirm button — \"Deactivate\". Also the title when no `title` is given. */\n confirmLabel?: ReactNode;\n /** The dialog's heading — \"Deactivate Ada Example?\". Default `confirmLabel`. */\n title?: ReactNode;\n /**\n * What the action does to THIS account, said before it happens: a sentence, or lines\n * (`DangerConsequence`, a `severe` one in the danger colour). Derive them from the\n * row's facts rather than reciting the worst case (keksdose dev#488).\n */\n consequence?: ReactNode | readonly (string | DangerConsequence)[];\n /** The tick's words at `acknowledge`. Default `dangerConfirm.acknowledge` — \"I have\n * read what this does and want to continue.\" */\n acknowledgeLabel?: ReactNode;\n /** `danger` (default at `type_email`): a red confirm. `warning` (default at\n * `acknowledge`) and `neutral`: the primary confirm. */\n tone?: \"danger\" | \"warning\" | \"neutral\";\n /**\n * Runs the action. Return a promise and the dialog manages itself: busy until it\n * settles, closed (`onClose(true)`) when it resolves, open with the refusal when it\n * rejects. Return nothing and the app owns the rest — `busy`, `error`, closing it.\n */\n onConfirm: (values: AdminActionConfirmValues) => MaybePromise;\n /** The dialog has closed: `true` after the action (or the alternative) went through,\n * `false` for Cancel, Escape, the backdrop or Back. */\n onClose: (done: boolean) => void;\n /** kastlan: offered with an `other_companies` refusal — `DELETE …/membership`,\n * logged as `membership_remove` (§4.1). Promise handling as `onConfirm`. */\n onRemoveFromCompany?: () => MaybePromise;\n /** The app's words for a failure the kit has no code for. Asked first. */\n describeError?: (error: unknown) => ReactNode | undefined;\n /** A refusal the app caught itself, when `onConfirm` returns nothing. */\n error?: unknown;\n /** The action is running, for an app that tracks it itself. */\n busy?: boolean;\n /**\n * Fields of the action's own, above the guard — the transfer's recipient, kastlan's\n * roles (`RolesEditor`). Their values are the app's; hold the confirm with\n * `confirmDisabledReason` until they are complete.\n */\n children?: ReactNode;\n /** Why the confirm is held for a reason of the app's (\"Choose who receives it\").\n * Named before the guard's own, since the fields are above it. */\n confirmDisabledReason?: ReactNode;\n /** Kept-mounted mode, as on `DialogFrame`; left out, open while mounted. */\n open?: boolean;\n /** The confirm COMMITS: under a locked `WriteLockProvider` it is held with the lock's\n * reason. Off by default — an admin action is not a write to the open record. */\n commit?: boolean;\n labels?: Partial<AdminActionLabels>;\n}\n\n/** Whether a callback answered a promise. */\nfunction isThenable(value: unknown): value is PromiseLike<unknown> {\n return typeof value === \"object\" && value !== null && typeof (value as PromiseLike<unknown>).then === \"function\";\n}\n\n/** The refusal's words and code. */\nfunction refusalOf(\n error: unknown,\n present: boolean,\n describeError: AdminActionConfirmProps[\"describeError\"],\n labels: AdminActionLabels,\n): { text: ReactNode; code: KitErrorCode | undefined } | null {\n if (!present) return null;\n const code = authErrorCode(error);\n const own = describeError?.(error);\n if (hasMessage(own)) return { text: own, code };\n const text =\n code === \"last_admin\"\n ? labels.lastAdmin\n : code === \"self_action\"\n ? labels.self\n : code === \"confirmation_mismatch\"\n ? labels.confirmationMismatch\n : code === \"other_companies\"\n ? labels.otherCompanies\n : labels.failed;\n return { text, code };\n}\n\n/** The selector for the first field among the caller's `children` — what opening focuses. */\nconst FIELD = 'input:not([type=\"hidden\"]):not(:disabled), select:not(:disabled), textarea:not(:disabled), [role=\"combobox\"]';\n\nexport function AdminActionConfirm({\n level,\n target,\n confirmLabel,\n title,\n consequence,\n acknowledgeLabel,\n tone: toneProp,\n onConfirm,\n onClose,\n onRemoveFromCompany,\n describeError,\n error: errorProp,\n busy: busyProp,\n children,\n confirmDisabledReason,\n open,\n commit,\n labels: labelsProp,\n}: AdminActionConfirmProps) {\n const labels = useKitLabels(\"adminAction\", DEFAULT_ADMIN_ACTION_LABELS, labelsProp);\n const guardLabels = useKitLabels(\"dangerConfirm\", DEFAULT_DANGER_CONFIRM_LABELS);\n const nameOf = usePersonLabel();\n const isOpen = open ?? true;\n const [typed, setTyped] = useState(\"\");\n const [ticked, setTicked] = useState(false);\n const [pending, setPending] = useState(false);\n // The last rejection, boxed so a rejection with `undefined` still counts as one.\n const [failure, setFailure] = useState<{ error: unknown } | null>(null);\n // Resolved: the actions row lowers the panel, and `onClose` answers true.\n const [done, setDone] = useState(false);\n const confirmed = useRef(false);\n const mounted = useRef(true);\n const started = useRef(false);\n const formId = useId();\n const slotRef = useRef<HTMLDivElement>(null);\n const guardRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n // A close (kept-mounted) wipes the guards and the refusal: a reopened dialog starts\n // over. During render, like ReauthDialog's, so no frame shows the old answers.\n const [wasOpen, setWasOpen] = useState(isOpen);\n if (wasOpen !== isOpen) {\n setWasOpen(isOpen);\n if (!isOpen) {\n setTyped(\"\");\n setTicked(false);\n setFailure(null);\n setDone(false);\n }\n }\n\n const busy = Boolean(busyProp) || pending;\n\n /** Runs `call`, managing busy, the refusal and the close — or not, when it answers\n * no promise (the app owns it then). */\n const perform = (call: () => MaybePromise) => {\n let result: MaybePromise;\n try {\n result = call();\n } catch (caught) {\n setFailure({ error: caught });\n return;\n }\n if (!isThenable(result)) return;\n setFailure(null);\n setPending(true);\n result.then(\n () => {\n if (!mounted.current) return;\n setPending(false);\n confirmed.current = true;\n setDone(true);\n },\n (caught: unknown) => {\n if (!mounted.current) return;\n setPending(false);\n setFailure({ error: caught });\n },\n );\n };\n\n // Level `none`: confirmed the moment it opens, once per opening. Nothing is drawn\n // while it runs; the promise's answer comes back asynchronously.\n useEffect(() => {\n if (level !== \"none\") return;\n if (!isOpen) {\n started.current = false;\n return;\n }\n if (started.current) return;\n started.current = true;\n confirmed.current = false;\n let result: unknown;\n try {\n result = onConfirm({});\n } catch (caught) {\n result = Promise.reject(caught);\n }\n // No promise: the app owns the rest (it closes the part, or passes `error`).\n if (!isThenable(result)) return;\n result.then(\n () => {\n if (mounted.current) onClose(true);\n },\n (caught: unknown) => {\n if (mounted.current) setFailure({ error: caught });\n },\n );\n }, [level, isOpen, onConfirm, onClose]);\n\n // Focus the first thing the dialog waits for, after Modal's own effect has recorded\n // the trigger to return to (a parent's effects run after its children's).\n useEffect(() => {\n if (!isOpen || level === \"none\") return;\n const field = slotRef.current?.querySelector<HTMLElement>(FIELD) ?? guardRef.current;\n field?.focus();\n }, [isOpen, level]);\n\n const hasRefusal = failure !== null || errorProp !== undefined;\n const refusal = refusalOf(failure ? failure.error : errorProp, hasRefusal, describeError, labels);\n const offerRemoval = refusal?.code === \"other_companies\" && onRemoveFromCompany !== undefined;\n\n const name = nameOf(target);\n const identity = (\n <span data-private>{name && name !== target.email ? `${name} · ${target.email}` : target.email}</span>\n );\n const heading = title ?? confirmLabel ?? labels.confirm;\n\n const refusalBanner = refusal && (\n <AlertBanner\n tone=\"danger\"\n size=\"sm\"\n action={\n offerRemoval ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"secondary\"\n disabled={busy}\n onClick={() => perform(() => onRemoveFromCompany!())}\n >\n {labels.removeFromCompany}\n </Button>\n ) : undefined\n }\n >\n {refusal.text}\n </AlertBanner>\n );\n\n if (level === \"none\") {\n // Nothing to ask: only a refusal is worth a dialog.\n if (!isOpen || !refusal) return null;\n return (\n <DialogFrame\n role=\"alertdialog\"\n title={heading}\n description={identity}\n onClose={() => onClose(confirmed.current)}\n actions={(close) => (\n <CloseOnDone done={done}>\n <Button type=\"button\" variant=\"secondary\" disabled={busy} onClick={close}>\n {labels.close}\n </Button>\n </CloseOnDone>\n )}\n >\n {refusalBanner}\n </DialogFrame>\n );\n }\n\n const tone = toneProp ?? (level === \"type_email\" ? \"danger\" : \"warning\");\n const askTyped = level === \"type_email\";\n const typedOk = !askTyped || typedMatches(typed, target.email, \"caseless\");\n const tickOk = askTyped || ticked;\n const guardReason: ReactNode = hasMessage(confirmDisabledReason)\n ? confirmDisabledReason\n : !tickOk\n ? guardLabels.needsAcknowledge\n : !typedOk\n ? typeof guardLabels.needsPhrase === \"function\"\n ? guardLabels.needsPhrase(target.email)\n : guardLabels.needsPhrase\n : undefined;\n\n const submit = () => {\n if (busy || guardReason !== undefined) return;\n perform(() =>\n onConfirm(askTyped ? { confirmEmail: typed.trim(), acknowledged: true } : { acknowledged: true }),\n );\n };\n\n const lines = Array.isArray(consequence)\n ? (consequence as readonly (string | DangerConsequence)[]).map((item, index) =>\n typeof item === \"string\"\n ? { key: item, text: item as ReactNode, severe: false }\n : { key: item.key ?? String(index), text: item.text, severe: Boolean(item.severe) },\n )\n : null;\n\n return (\n <DialogFrame\n open={open}\n role=\"alertdialog\"\n title={heading}\n description={identity}\n onClose={() => onClose(confirmed.current)}\n // A request that has left cannot be called back by closing the dialog.\n onKeyDown={(event: KeyboardEvent<HTMLDivElement>) => {\n if (busy && event.key === \"Escape\") event.preventDefault();\n }}\n aria-busy={busy || undefined}\n actions={(close) => (\n <CloseOnDone done={done}>\n <Button type=\"button\" variant=\"ghost\" disabled={busy} onClick={close}>\n {labels.cancel}\n </Button>\n <Button\n type=\"submit\"\n form={formId}\n variant={tone === \"danger\" ? \"danger\" : \"primary\"}\n pending={busy}\n commit={commit}\n disabledReason={busy ? undefined : guardReason}\n >\n {confirmLabel ?? labels.confirm}\n </Button>\n </CloseOnDone>\n )}\n >\n <form\n id={formId}\n noValidate\n className=\"space-y-3\"\n onSubmit={(event) => {\n event.preventDefault();\n submit();\n }}\n >\n {lines ? (\n <ul className=\"list-disc space-y-1 ps-5 text-sm text-[var(--text-secondary)]\">\n {lines.map((line) => (\n <li key={line.key} className={line.severe ? \"text-[var(--danger)]\" : undefined}>\n {line.text}\n </li>\n ))}\n </ul>\n ) : (\n hasMessage(consequence as ReactNode) && (\n <p className=\"text-sm text-[var(--text-secondary)]\">{consequence as ReactNode}</p>\n )\n )}\n {Children.toArray(children).some(hasMessage) && (\n <div ref={slotRef} className=\"space-y-3\">\n {children}\n </div>\n )}\n {askTyped ? (\n <TypedConfirmField\n ref={guardRef}\n target={target.email}\n match=\"caseless\"\n value={typed}\n onValueChange={setTyped}\n busy={busy}\n data-private\n />\n ) : (\n <Checkbox\n ref={guardRef}\n label={acknowledgeLabel ?? guardLabels.acknowledge}\n checked={ticked}\n // Not `disabled` while busy: the box may hold the focus.\n onCheckedChange={(next) => {\n if (!busy) setTicked(next);\n }}\n />\n )}\n {refusalBanner}\n </form>\n </DialogFrame>\n );\n}\n\n/**\n * The actions row's wrapper: it renders INSIDE the Modal, where the panel's animated\n * close is in context, so a resolved action lowers the panel the way Cancel does and\n * Modal then calls `onClose` (ReauthDialog's pattern).\n */\nfunction CloseOnDone({ done, children }: { done: boolean; children: ReactNode }) {\n const close = useContext(ModalCloseContext);\n useEffect(() => {\n if (done) close?.();\n }, [done, close]);\n return <>{children}</>;\n}\n"],"mappings":";AAkUI,SA2KK,UA3KL,KA0FI,YA1FJ;AAlUJ,SAAS,UAAU,YAAY,WAAW,OAAO,QAAQ,gBAAgB;AAEzE,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB;AACzB,SAAS,+BAA+B,mBAAmB,oBAAoB;AAE/E,SAAS,mBAAmB;AAC5B,SAAS,yBAAyB;AAClC,SAAS,cAAc;AACvB,SAAS,qBAAqB;AAE9B,SAAS,YAAY,sBAAsB;AA8EpC,MAAM,8BAAiD;AAAA,EAC5D,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,WAAW;AAAA,EACX,MAAM;AAAA,EACN,sBAAsB;AAAA,EACtB,gBACE;AAAA,EACF,mBAAmB;AAAA,EACnB,QAAQ;AACV;AA2DA,SAAS,WAAW,OAA+C;AACjE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,OAAQ,MAA+B,SAAS;AACxG;AAGA,SAAS,UACP,OACA,SACA,eACA,QAC4D;AAC5D,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,OAAO,cAAc,KAAK;AAChC,QAAM,MAAM,gBAAgB,KAAK;AACjC,MAAI,WAAW,GAAG,EAAG,QAAO,EAAE,MAAM,KAAK,KAAK;AAC9C,QAAM,OACJ,SAAS,eACL,OAAO,YACP,SAAS,gBACP,OAAO,OACP,SAAS,0BACP,OAAO,uBACP,SAAS,oBACP,OAAO,iBACP,OAAO;AACnB,SAAO,EAAE,MAAM,KAAK;AACtB;AAGA,MAAM,QAAQ;AAEP,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AACV,GAA4B;AAC1B,QAAM,SAAS,aAAa,eAAe,6BAA6B,UAAU;AAClF,QAAM,cAAc,aAAa,iBAAiB,6BAA6B;AAC/E,QAAM,SAAS,eAAe;AAC9B,QAAM,SAAS,QAAQ;AACvB,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAE5C,QAAM,CAAC,SAAS,UAAU,IAAI,SAAoC,IAAI;AAEtE,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,YAAY,OAAO,KAAK;AAC9B,QAAM,UAAU,OAAO,IAAI;AAC3B,QAAM,UAAU,OAAO,KAAK;AAC5B,QAAM,SAAS,MAAM;AACrB,QAAM,UAAU,OAAuB,IAAI;AAC3C,QAAM,WAAW,OAAyB,IAAI;AAE9C,YAAU,MAAM;AACd,YAAQ,UAAU;AAClB,WAAO,MAAM;AACX,cAAQ,UAAU;AAAA,IACpB;AAAA,EACF,GAAG,CAAC,CAAC;AAIL,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,MAAM;AAC7C,MAAI,YAAY,QAAQ;AACtB,eAAW,MAAM;AACjB,QAAI,CAAC,QAAQ;AACX,eAAS,EAAE;AACX,gBAAU,KAAK;AACf,iBAAW,IAAI;AACf,cAAQ,KAAK;AAAA,IACf;AAAA,EACF;AAEA,QAAM,OAAO,QAAQ,QAAQ,KAAK;AAIlC,QAAM,UAAU,CAAC,SAA6B;AAC5C,QAAI;AACJ,QAAI;AACF,eAAS,KAAK;AAAA,IAChB,SAAS,QAAQ;AACf,iBAAW,EAAE,OAAO,OAAO,CAAC;AAC5B;AAAA,IACF;AACA,QAAI,CAAC,WAAW,MAAM,EAAG;AACzB,eAAW,IAAI;AACf,eAAW,IAAI;AACf,WAAO;AAAA,MACL,MAAM;AACJ,YAAI,CAAC,QAAQ,QAAS;AACtB,mBAAW,KAAK;AAChB,kBAAU,UAAU;AACpB,gBAAQ,IAAI;AAAA,MACd;AAAA,MACA,CAAC,WAAoB;AACnB,YAAI,CAAC,QAAQ,QAAS;AACtB,mBAAW,KAAK;AAChB,mBAAW,EAAE,OAAO,OAAO,CAAC;AAAA,MAC9B;AAAA,IACF;AAAA,EACF;AAIA,YAAU,MAAM;AACd,QAAI,UAAU,OAAQ;AACtB,QAAI,CAAC,QAAQ;AACX,cAAQ,UAAU;AAClB;AAAA,IACF;AACA,QAAI,QAAQ,QAAS;AACrB,YAAQ,UAAU;AAClB,cAAU,UAAU;AACpB,QAAI;AACJ,QAAI;AACF,eAAS,UAAU,CAAC,CAAC;AAAA,IACvB,SAAS,QAAQ;AACf,eAAS,QAAQ,OAAO,MAAM;AAAA,IAChC;AAEA,QAAI,CAAC,WAAW,MAAM,EAAG;AACzB,WAAO;AAAA,MACL,MAAM;AACJ,YAAI,QAAQ,QAAS,SAAQ,IAAI;AAAA,MACnC;AAAA,MACA,CAAC,WAAoB;AACnB,YAAI,QAAQ,QAAS,YAAW,EAAE,OAAO,OAAO,CAAC;AAAA,MACnD;AAAA,IACF;AAAA,EACF,GAAG,CAAC,OAAO,QAAQ,WAAW,OAAO,CAAC;AAItC,YAAU,MAAM;AACd,QAAI,CAAC,UAAU,UAAU,OAAQ;AACjC,UAAM,QAAQ,QAAQ,SAAS,cAA2B,KAAK,KAAK,SAAS;AAC7E,WAAO,MAAM;AAAA,EACf,GAAG,CAAC,QAAQ,KAAK,CAAC;AAElB,QAAM,aAAa,YAAY,QAAQ,cAAc;AACrD,QAAM,UAAU,UAAU,UAAU,QAAQ,QAAQ,WAAW,YAAY,eAAe,MAAM;AAChG,QAAM,eAAe,SAAS,SAAS,qBAAqB,wBAAwB;AAEpF,QAAM,OAAO,OAAO,MAAM;AAC1B,QAAM,WACJ,oBAAC,UAAK,gBAAY,MAAE,kBAAQ,SAAS,OAAO,QAAQ,GAAG,IAAI,SAAM,OAAO,KAAK,KAAK,OAAO,OAAM;AAEjG,QAAM,UAAU,SAAS,gBAAgB,OAAO;AAEhD,QAAM,gBAAgB,WACpB;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,MAAK;AAAA,MACL,QACE,eACE;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,UAAU;AAAA,UACV,SAAS,MAAM,QAAQ,MAAM,oBAAqB,CAAC;AAAA,UAElD,iBAAO;AAAA;AAAA,MACV,IACE;AAAA,MAGL,kBAAQ;AAAA;AAAA,EACX;AAGF,MAAI,UAAU,QAAQ;AAEpB,QAAI,CAAC,UAAU,CAAC,QAAS,QAAO;AAChC,WACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAO;AAAA,QACP,aAAa;AAAA,QACb,SAAS,MAAM,QAAQ,UAAU,OAAO;AAAA,QACxC,SAAS,CAAC,UACR,oBAAC,eAAY,MACX,8BAAC,UAAO,MAAK,UAAS,SAAQ,aAAY,UAAU,MAAM,SAAS,OAChE,iBAAO,OACV,GACF;AAAA,QAGD;AAAA;AAAA,IACH;AAAA,EAEJ;AAEA,QAAM,OAAO,aAAa,UAAU,eAAe,WAAW;AAC9D,QAAM,WAAW,UAAU;AAC3B,QAAM,UAAU,CAAC,YAAY,aAAa,OAAO,OAAO,OAAO,UAAU;AACzE,QAAM,SAAS,YAAY;AAC3B,QAAM,cAAyB,WAAW,qBAAqB,IAC3D,wBACA,CAAC,SACC,YAAY,mBACZ,CAAC,UACC,OAAO,YAAY,gBAAgB,aACjC,YAAY,YAAY,OAAO,KAAK,IACpC,YAAY,cACd;AAER,QAAM,SAAS,MAAM;AACnB,QAAI,QAAQ,gBAAgB,OAAW;AACvC;AAAA,MAAQ,MACN,UAAU,WAAW,EAAE,cAAc,MAAM,KAAK,GAAG,cAAc,KAAK,IAAI,EAAE,cAAc,KAAK,CAAC;AAAA,IAClG;AAAA,EACF;AAEA,QAAM,QAAQ,MAAM,QAAQ,WAAW,IAClC,YAAwD;AAAA,IAAI,CAAC,MAAM,UAClE,OAAO,SAAS,WACZ,EAAE,KAAK,MAAM,MAAM,MAAmB,QAAQ,MAAM,IACpD,EAAE,KAAK,KAAK,OAAO,OAAO,KAAK,GAAG,MAAM,KAAK,MAAM,QAAQ,QAAQ,KAAK,MAAM,EAAE;AAAA,EACtF,IACA;AAEJ,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,MAAK;AAAA,MACL,OAAO;AAAA,MACP,aAAa;AAAA,MACb,SAAS,MAAM,QAAQ,UAAU,OAAO;AAAA,MAExC,WAAW,CAAC,UAAyC;AACnD,YAAI,QAAQ,MAAM,QAAQ,SAAU,OAAM,eAAe;AAAA,MAC3D;AAAA,MACA,aAAW,QAAQ;AAAA,MACnB,SAAS,CAAC,UACR,qBAAC,eAAY,MACX;AAAA,4BAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,UAAU,MAAM,SAAS,OAC5D,iBAAO,QACV;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,MAAM;AAAA,YACN,SAAS,SAAS,WAAW,WAAW;AAAA,YACxC,SAAS;AAAA,YACT;AAAA,YACA,gBAAgB,OAAO,SAAY;AAAA,YAElC,0BAAgB,OAAO;AAAA;AAAA,QAC1B;AAAA,SACF;AAAA,MAGF;AAAA,QAAC;AAAA;AAAA,UACC,IAAI;AAAA,UACJ,YAAU;AAAA,UACV,WAAU;AAAA,UACV,UAAU,CAAC,UAAU;AACnB,kBAAM,eAAe;AACrB,mBAAO;AAAA,UACT;AAAA,UAEC;AAAA,oBACC,oBAAC,QAAG,WAAU,iEACX,gBAAM,IAAI,CAAC,SACV,oBAAC,QAAkB,WAAW,KAAK,SAAS,yBAAyB,QAClE,eAAK,QADC,KAAK,GAEd,CACD,GACH,IAEA,WAAW,WAAwB,KACjC,oBAAC,OAAE,WAAU,wCAAwC,uBAAyB;AAAA,YAGjF,SAAS,QAAQ,QAAQ,EAAE,KAAK,UAAU,KACzC,oBAAC,SAAI,KAAK,SAAS,WAAU,aAC1B,UACH;AAAA,YAED,WACC;AAAA,cAAC;AAAA;AAAA,gBACC,KAAK;AAAA,gBACL,QAAQ,OAAO;AAAA,gBACf,OAAM;AAAA,gBACN,OAAO;AAAA,gBACP,eAAe;AAAA,gBACf;AAAA,gBACA,gBAAY;AAAA;AAAA,YACd,IAEA;AAAA,cAAC;AAAA;AAAA,gBACC,KAAK;AAAA,gBACL,OAAO,oBAAoB,YAAY;AAAA,gBACvC,SAAS;AAAA,gBAET,iBAAiB,CAAC,SAAS;AACzB,sBAAI,CAAC,KAAM,WAAU,IAAI;AAAA,gBAC3B;AAAA;AAAA,YACF;AAAA,YAED;AAAA;AAAA;AAAA,MACH;AAAA;AAAA,EACF;AAEJ;AAOA,SAAS,YAAY,EAAE,MAAM,SAAS,GAA2C;AAC/E,QAAM,QAAQ,WAAW,iBAAiB;AAC1C,YAAU,MAAM;AACd,QAAI,KAAM,SAAQ;AAAA,EACpB,GAAG,CAAC,MAAM,KAAK,CAAC;AAChB,SAAO,gCAAG,UAAS;AACrB;","names":[]}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { DateInput } from '../lib/format.js';
|
|
4
|
+
import { AdminPerson } from './admin-parts.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The admin actions, as the audit table records them (docs/user-admin-harmonization.md
|
|
8
|
+
* §2.3, §4.3): `GET /admin/actions?limit=&offset=&target=`, newest first, on the admin
|
|
9
|
+
* page and filtered to one account from its row.
|
|
10
|
+
*
|
|
11
|
+
* Every app writes the same small `admin_actions` row in the transaction of the action
|
|
12
|
+
* (keksdose and Kurvenschmiede had log lines; kastlan has an HTTP log with no named
|
|
13
|
+
* action), so the vocabulary is shared — {@link AdminActionKind} — and an app's own
|
|
14
|
+
* actions join it through `labels.actions`.
|
|
15
|
+
*
|
|
16
|
+
* What a row says, and what it never does: who, what, to whom, when, and a summary of the
|
|
17
|
+
* `detail` the APP writes (ids, roles, counts — never content, §4.3). Three kinds of
|
|
18
|
+
* actor: an admin; **the user themselves** (`deletion_request`: the actor is the target);
|
|
19
|
+
* and **nobody** (the scheduled erasure, the hand-over at erasure, §2.7), said as
|
|
20
|
+
* "automatically". After an erasure the target's name and address are scrubbed and the
|
|
21
|
+
* row stays: it reads "an erased account".
|
|
22
|
+
*
|
|
23
|
+
* It never fetches: the page of rows and the paging are the app's.
|
|
24
|
+
*/
|
|
25
|
+
/** The shared vocabulary of `admin_actions.action` (§4.3). */
|
|
26
|
+
type AdminActionKind = "deactivate" | "reactivate" | "role" | "membership_remove" | "password_change_require" | "password_change_withdraw" | "mail_verification" | "mail_reset" | "reviewer" | "invite" | "invite_resend" | "invite_revoke" | "transfer" | "deletion_request" | "deletion_cancel" | "erase";
|
|
27
|
+
/** Every {@link AdminActionKind}, in the contract's order. */
|
|
28
|
+
declare const ADMIN_ACTION_KINDS: readonly AdminActionKind[];
|
|
29
|
+
/** One logged action — the app's `admin_actions` row with the people joined in. */
|
|
30
|
+
interface AdminActionLogEntry {
|
|
31
|
+
id: string | number;
|
|
32
|
+
/** When, as the row's `at`. */
|
|
33
|
+
at: DateInput;
|
|
34
|
+
/**
|
|
35
|
+
* Who did it: a person; `"self"` — the user themselves (a deletion request); or `null`
|
|
36
|
+
* — nobody, the scheduled job. A person whose `id` (or address) is the target's is
|
|
37
|
+
* read as `"self"` too.
|
|
38
|
+
*/
|
|
39
|
+
actor: AdminPerson | "self" | null;
|
|
40
|
+
/** A {@link AdminActionKind}, or one of the app's own (named in `labels.actions`). */
|
|
41
|
+
action: AdminActionKind | (string & {});
|
|
42
|
+
/** Whom it was done to. `null`, or a person with nothing left in it: an erased
|
|
43
|
+
* account. An invitation's target is its address (`target_email`). */
|
|
44
|
+
target?: AdminPerson | null;
|
|
45
|
+
/** The `detail` in the app's words — "Member → Admin", "12 items to Ben Example". */
|
|
46
|
+
detail?: ReactNode;
|
|
47
|
+
}
|
|
48
|
+
/** The `adminActionLog` namespace. */
|
|
49
|
+
interface AdminActionLogLabels {
|
|
50
|
+
/** The list's accessible name. */
|
|
51
|
+
title: string;
|
|
52
|
+
empty: string;
|
|
53
|
+
loading: string;
|
|
54
|
+
/** "by Ada Example". */
|
|
55
|
+
by: (actor: string) => string;
|
|
56
|
+
/** The user acted on their own account. */
|
|
57
|
+
bySelf: string;
|
|
58
|
+
/** No actor: a scheduled job. */
|
|
59
|
+
automatic: string;
|
|
60
|
+
/** A target scrubbed by an erasure. */
|
|
61
|
+
erased: string;
|
|
62
|
+
/** The filter chip: only one account's actions are shown. */
|
|
63
|
+
filteredTo: (target: string) => string;
|
|
64
|
+
/** The chip's ×. */
|
|
65
|
+
showAll: string;
|
|
66
|
+
/** A target name as a filter button — "Show only the actions on Ada Example". */
|
|
67
|
+
filterBy: (target: string) => string;
|
|
68
|
+
/** What each action is called, past tense, keyed by the vocabulary. An app adds its
|
|
69
|
+
* own here (`labels={{ actions: { plan: "Plan changed" } }}`). */
|
|
70
|
+
actions: {
|
|
71
|
+
[K in AdminActionKind]?: string;
|
|
72
|
+
} & {
|
|
73
|
+
[action: string]: string | undefined;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
declare const DEFAULT_ADMIN_ACTION_LOG_LABELS: AdminActionLogLabels;
|
|
77
|
+
/** The paging of `GET /admin/actions` — DataTable's `ServerPagination`, which the
|
|
78
|
+
* footer is. */
|
|
79
|
+
interface AdminActionLogPaging {
|
|
80
|
+
/** 0-based. */
|
|
81
|
+
page: number;
|
|
82
|
+
pageSize: number;
|
|
83
|
+
total: number;
|
|
84
|
+
onPageChange: (page: number) => void;
|
|
85
|
+
onPageSizeChange?: (pageSize: number) => void;
|
|
86
|
+
}
|
|
87
|
+
interface AdminActionLogProps {
|
|
88
|
+
/** One page, newest first, as the server sends it. */
|
|
89
|
+
entries: readonly AdminActionLogEntry[];
|
|
90
|
+
/** Left out: no footer (a short list on a row). */
|
|
91
|
+
paging?: AdminActionLogPaging;
|
|
92
|
+
/** The page is being fetched. */
|
|
93
|
+
loading?: boolean;
|
|
94
|
+
/** The filter in force: only this account's actions are shown (`?target=`). With
|
|
95
|
+
* `onTargetFilter`, a chip says so and its × clears it. */
|
|
96
|
+
target?: AdminPerson | null;
|
|
97
|
+
/** Filter by an account — or clear it with `null`. Given, every target's name is a
|
|
98
|
+
* button that narrows the list to it. Left out, the list is not filterable here. */
|
|
99
|
+
onTargetFilter?: (target: AdminPerson | null) => void;
|
|
100
|
+
className?: string;
|
|
101
|
+
labels?: Partial<AdminActionLogLabels>;
|
|
102
|
+
}
|
|
103
|
+
declare function AdminActionLog({ entries, paging, loading, target, onTargetFilter, className, labels: labelsProp, }: AdminActionLogProps): react.JSX.Element;
|
|
104
|
+
|
|
105
|
+
export { ADMIN_ACTION_KINDS, type AdminActionKind, AdminActionLog, type AdminActionLogEntry, type AdminActionLogLabels, type AdminActionLogPaging, type AdminActionLogProps, DEFAULT_ADMIN_ACTION_LOG_LABELS };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from "../lib/cn.js";
|
|
4
|
+
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
5
|
+
import { DateMark } from "../components/account-chips.js";
|
|
6
|
+
import { Chip } from "../components/chip.js";
|
|
7
|
+
import { Pagination } from "../components/data-table-pagination.js";
|
|
8
|
+
import { Caption } from "../components/text.js";
|
|
9
|
+
import { Button, Spinner } from "../components/ui.js";
|
|
10
|
+
import { usePersonLabel } from "./admin-parts.js";
|
|
11
|
+
const ADMIN_ACTION_KINDS = [
|
|
12
|
+
"deactivate",
|
|
13
|
+
"reactivate",
|
|
14
|
+
"role",
|
|
15
|
+
"membership_remove",
|
|
16
|
+
"password_change_require",
|
|
17
|
+
"password_change_withdraw",
|
|
18
|
+
"mail_verification",
|
|
19
|
+
"mail_reset",
|
|
20
|
+
"reviewer",
|
|
21
|
+
"invite",
|
|
22
|
+
"invite_resend",
|
|
23
|
+
"invite_revoke",
|
|
24
|
+
"transfer",
|
|
25
|
+
"deletion_request",
|
|
26
|
+
"deletion_cancel",
|
|
27
|
+
"erase"
|
|
28
|
+
];
|
|
29
|
+
const DEFAULT_ADMIN_ACTION_LOG_LABELS = {
|
|
30
|
+
title: "Admin actions",
|
|
31
|
+
empty: "No admin actions yet.",
|
|
32
|
+
loading: "Loading\u2026",
|
|
33
|
+
by: (actor) => `by ${actor}`,
|
|
34
|
+
bySelf: "by the user themselves",
|
|
35
|
+
automatic: "automatically",
|
|
36
|
+
erased: "an erased account",
|
|
37
|
+
filteredTo: (target) => `Only ${target}`,
|
|
38
|
+
showAll: "Show all actions",
|
|
39
|
+
filterBy: (target) => `Show only the actions on ${target}`,
|
|
40
|
+
actions: {
|
|
41
|
+
deactivate: "Deactivated",
|
|
42
|
+
reactivate: "Reactivated",
|
|
43
|
+
role: "Role changed",
|
|
44
|
+
membership_remove: "Removed from the company",
|
|
45
|
+
password_change_require: "New password required",
|
|
46
|
+
password_change_withdraw: "Password requirement withdrawn",
|
|
47
|
+
mail_verification: "Verification mail sent",
|
|
48
|
+
mail_reset: "Password reset mail sent",
|
|
49
|
+
reviewer: "Reviewer scope changed",
|
|
50
|
+
invite: "Invited",
|
|
51
|
+
invite_resend: "Invitation resent",
|
|
52
|
+
invite_revoke: "Invitation revoked",
|
|
53
|
+
transfer: "Work transferred",
|
|
54
|
+
deletion_request: "Deletion requested",
|
|
55
|
+
deletion_cancel: "Deletion cancelled",
|
|
56
|
+
erase: "Account erased"
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
function scrubbed(person) {
|
|
60
|
+
return !person || ![person.first, person.last, person.name, person.email].some((part) => (part ?? "").trim());
|
|
61
|
+
}
|
|
62
|
+
function samePerson(a, b) {
|
|
63
|
+
if (!b) return false;
|
|
64
|
+
if (a.id !== void 0 && a.id !== null && b.id !== void 0 && b.id !== null) return a.id === b.id;
|
|
65
|
+
const ea = (a.email ?? "").trim().toLowerCase();
|
|
66
|
+
return ea !== "" && ea === (b.email ?? "").trim().toLowerCase();
|
|
67
|
+
}
|
|
68
|
+
function AdminActionLog({
|
|
69
|
+
entries,
|
|
70
|
+
paging,
|
|
71
|
+
loading = false,
|
|
72
|
+
target,
|
|
73
|
+
onTargetFilter,
|
|
74
|
+
className,
|
|
75
|
+
labels: labelsProp
|
|
76
|
+
}) {
|
|
77
|
+
const labels = useKitLabels("adminActionLog", DEFAULT_ADMIN_ACTION_LOG_LABELS, labelsProp);
|
|
78
|
+
const nameOf = usePersonLabel();
|
|
79
|
+
const filtered = target !== void 0 && target !== null && !scrubbed(target);
|
|
80
|
+
const actorText = (entry) => {
|
|
81
|
+
const { actor } = entry;
|
|
82
|
+
if (actor === null) return labels.automatic;
|
|
83
|
+
if (actor === "self" || samePerson(actor, entry.target)) return labels.bySelf;
|
|
84
|
+
return labels.by(nameOf(actor) || labels.erased);
|
|
85
|
+
};
|
|
86
|
+
const targetNode = (entry) => {
|
|
87
|
+
if (scrubbed(entry.target)) return /* @__PURE__ */ jsx("span", { className: "italic text-[var(--text-muted)]", children: labels.erased });
|
|
88
|
+
const person = entry.target;
|
|
89
|
+
const name = nameOf(person);
|
|
90
|
+
const address = (person.email ?? "").trim();
|
|
91
|
+
const text = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
92
|
+
/* @__PURE__ */ jsx("span", { "data-private": true, className: "font-medium text-[var(--text-primary)]", children: name }),
|
|
93
|
+
address && address !== name && /* @__PURE__ */ jsx("span", { "data-private": true, className: "ms-1 text-xs text-[var(--text-muted)]", children: address })
|
|
94
|
+
] });
|
|
95
|
+
if (!onTargetFilter || filtered && samePerson(person, target)) return text;
|
|
96
|
+
return /* @__PURE__ */ jsx(
|
|
97
|
+
Button,
|
|
98
|
+
{
|
|
99
|
+
type: "button",
|
|
100
|
+
variant: "link",
|
|
101
|
+
size: "sm",
|
|
102
|
+
className: "h-auto min-h-0 p-0 text-start",
|
|
103
|
+
"aria-label": labels.filterBy(name),
|
|
104
|
+
onClick: () => onTargetFilter(person),
|
|
105
|
+
children: text
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("space-y-3", className), children: [
|
|
110
|
+
filtered && onTargetFilter && /* @__PURE__ */ jsx(Chip, { size: "sm", tone: "brand", onRemove: () => onTargetFilter(null), removeLabel: labels.showAll, children: /* @__PURE__ */ jsx("span", { "data-private": true, children: labels.filteredTo(nameOf(target)) }) }),
|
|
111
|
+
loading && entries.length === 0 ? /* @__PURE__ */ jsx("div", { className: "flex justify-center py-4", children: /* @__PURE__ */ jsx(Spinner, { label: labels.loading }) }) : entries.length === 0 ? /* @__PURE__ */ jsx(Caption, { children: labels.empty }) : /* @__PURE__ */ jsx(
|
|
112
|
+
"ol",
|
|
113
|
+
{
|
|
114
|
+
"aria-label": labels.title,
|
|
115
|
+
"aria-busy": loading || void 0,
|
|
116
|
+
className: cn("divide-y divide-[var(--border)]", loading && "opacity-60"),
|
|
117
|
+
children: entries.map((entry) => /* @__PURE__ */ jsxs("li", { "data-action": entry.action, className: "space-y-0.5 py-2 first:pt-0 last:pb-0", children: [
|
|
118
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5", children: [
|
|
119
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5 text-sm", children: [
|
|
120
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-[var(--text-primary)]", children: labels.actions[entry.action] ?? entry.action }),
|
|
121
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 break-words", children: targetNode(entry) })
|
|
122
|
+
] }),
|
|
123
|
+
/* @__PURE__ */ jsx(DateMark, { value: entry.at, display: "relative", className: "text-xs text-[var(--text-muted)]" })
|
|
124
|
+
] }),
|
|
125
|
+
/* @__PURE__ */ jsxs("div", { className: "text-xs text-[var(--text-muted)]", children: [
|
|
126
|
+
/* @__PURE__ */ jsx("span", { "data-private": entry.actor !== null && entry.actor !== "self" ? true : void 0, children: actorText(entry) }),
|
|
127
|
+
entry.detail !== void 0 && entry.detail !== null && entry.detail !== "" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
128
|
+
" \xB7 ",
|
|
129
|
+
/* @__PURE__ */ jsx("span", { className: "text-[var(--text-secondary)]", children: entry.detail })
|
|
130
|
+
] })
|
|
131
|
+
] })
|
|
132
|
+
] }, entry.id))
|
|
133
|
+
}
|
|
134
|
+
),
|
|
135
|
+
paging && paging.total > 0 && /* @__PURE__ */ jsx(
|
|
136
|
+
Pagination,
|
|
137
|
+
{
|
|
138
|
+
page: paging.page,
|
|
139
|
+
pageSize: paging.pageSize,
|
|
140
|
+
total: paging.total,
|
|
141
|
+
totalPages: Math.max(1, Math.ceil(paging.total / Math.max(1, paging.pageSize))),
|
|
142
|
+
onPage: paging.onPageChange,
|
|
143
|
+
onPageSize: paging.onPageSizeChange
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
] });
|
|
147
|
+
}
|
|
148
|
+
export {
|
|
149
|
+
ADMIN_ACTION_KINDS,
|
|
150
|
+
AdminActionLog,
|
|
151
|
+
DEFAULT_ADMIN_ACTION_LOG_LABELS
|
|
152
|
+
};
|
|
153
|
+
//# sourceMappingURL=admin-action-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/admin/admin-action-log.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport type { DateInput } from \"../lib/format\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { DateMark } from \"../components/account-chips\";\nimport { Chip } from \"../components/chip\";\nimport { Pagination } from \"../components/data-table-pagination\";\nimport { Caption } from \"../components/text\";\nimport { Button, Spinner } from \"../components/ui\";\nimport { usePersonLabel } from \"./admin-parts\";\nimport type { AdminPerson } from \"./admin-parts\";\n\n/**\n * The admin actions, as the audit table records them (docs/user-admin-harmonization.md\n * §2.3, §4.3): `GET /admin/actions?limit=&offset=&target=`, newest first, on the admin\n * page and filtered to one account from its row.\n *\n * Every app writes the same small `admin_actions` row in the transaction of the action\n * (keksdose and Kurvenschmiede had log lines; kastlan has an HTTP log with no named\n * action), so the vocabulary is shared — {@link AdminActionKind} — and an app's own\n * actions join it through `labels.actions`.\n *\n * What a row says, and what it never does: who, what, to whom, when, and a summary of the\n * `detail` the APP writes (ids, roles, counts — never content, §4.3). Three kinds of\n * actor: an admin; **the user themselves** (`deletion_request`: the actor is the target);\n * and **nobody** (the scheduled erasure, the hand-over at erasure, §2.7), said as\n * \"automatically\". After an erasure the target's name and address are scrubbed and the\n * row stays: it reads \"an erased account\".\n *\n * It never fetches: the page of rows and the paging are the app's.\n */\n\n/** The shared vocabulary of `admin_actions.action` (§4.3). */\nexport type AdminActionKind =\n | \"deactivate\"\n | \"reactivate\"\n | \"role\"\n | \"membership_remove\"\n | \"password_change_require\"\n | \"password_change_withdraw\"\n | \"mail_verification\"\n | \"mail_reset\"\n | \"reviewer\"\n | \"invite\"\n | \"invite_resend\"\n | \"invite_revoke\"\n | \"transfer\"\n | \"deletion_request\"\n | \"deletion_cancel\"\n | \"erase\";\n\n/** Every {@link AdminActionKind}, in the contract's order. */\nexport const ADMIN_ACTION_KINDS: readonly AdminActionKind[] = [\n \"deactivate\",\n \"reactivate\",\n \"role\",\n \"membership_remove\",\n \"password_change_require\",\n \"password_change_withdraw\",\n \"mail_verification\",\n \"mail_reset\",\n \"reviewer\",\n \"invite\",\n \"invite_resend\",\n \"invite_revoke\",\n \"transfer\",\n \"deletion_request\",\n \"deletion_cancel\",\n \"erase\",\n];\n\n/** One logged action — the app's `admin_actions` row with the people joined in. */\nexport interface AdminActionLogEntry {\n id: string | number;\n /** When, as the row's `at`. */\n at: DateInput;\n /**\n * Who did it: a person; `\"self\"` — the user themselves (a deletion request); or `null`\n * — nobody, the scheduled job. A person whose `id` (or address) is the target's is\n * read as `\"self\"` too.\n */\n actor: AdminPerson | \"self\" | null;\n /** A {@link AdminActionKind}, or one of the app's own (named in `labels.actions`). */\n action: AdminActionKind | (string & {});\n /** Whom it was done to. `null`, or a person with nothing left in it: an erased\n * account. An invitation's target is its address (`target_email`). */\n target?: AdminPerson | null;\n /** The `detail` in the app's words — \"Member → Admin\", \"12 items to Ben Example\". */\n detail?: ReactNode;\n}\n\n/** The `adminActionLog` namespace. */\nexport interface AdminActionLogLabels {\n /** The list's accessible name. */\n title: string;\n empty: string;\n loading: string;\n /** \"by Ada Example\". */\n by: (actor: string) => string;\n /** The user acted on their own account. */\n bySelf: string;\n /** No actor: a scheduled job. */\n automatic: string;\n /** A target scrubbed by an erasure. */\n erased: string;\n /** The filter chip: only one account's actions are shown. */\n filteredTo: (target: string) => string;\n /** The chip's ×. */\n showAll: string;\n /** A target name as a filter button — \"Show only the actions on Ada Example\". */\n filterBy: (target: string) => string;\n /** What each action is called, past tense, keyed by the vocabulary. An app adds its\n * own here (`labels={{ actions: { plan: \"Plan changed\" } }}`). */\n actions: { [K in AdminActionKind]?: string } & { [action: string]: string | undefined };\n}\n\nexport const DEFAULT_ADMIN_ACTION_LOG_LABELS: AdminActionLogLabels = {\n title: \"Admin actions\",\n empty: \"No admin actions yet.\",\n loading: \"Loading…\",\n by: (actor) => `by ${actor}`,\n bySelf: \"by the user themselves\",\n automatic: \"automatically\",\n erased: \"an erased account\",\n filteredTo: (target) => `Only ${target}`,\n showAll: \"Show all actions\",\n filterBy: (target) => `Show only the actions on ${target}`,\n actions: {\n deactivate: \"Deactivated\",\n reactivate: \"Reactivated\",\n role: \"Role changed\",\n membership_remove: \"Removed from the company\",\n password_change_require: \"New password required\",\n password_change_withdraw: \"Password requirement withdrawn\",\n mail_verification: \"Verification mail sent\",\n mail_reset: \"Password reset mail sent\",\n reviewer: \"Reviewer scope changed\",\n invite: \"Invited\",\n invite_resend: \"Invitation resent\",\n invite_revoke: \"Invitation revoked\",\n transfer: \"Work transferred\",\n deletion_request: \"Deletion requested\",\n deletion_cancel: \"Deletion cancelled\",\n erase: \"Account erased\",\n },\n};\n\n/** The paging of `GET /admin/actions` — DataTable's `ServerPagination`, which the\n * footer is. */\nexport interface AdminActionLogPaging {\n /** 0-based. */\n page: number;\n pageSize: number;\n total: number;\n onPageChange: (page: number) => void;\n onPageSizeChange?: (pageSize: number) => void;\n}\n\nexport interface AdminActionLogProps {\n /** One page, newest first, as the server sends it. */\n entries: readonly AdminActionLogEntry[];\n /** Left out: no footer (a short list on a row). */\n paging?: AdminActionLogPaging;\n /** The page is being fetched. */\n loading?: boolean;\n /** The filter in force: only this account's actions are shown (`?target=`). With\n * `onTargetFilter`, a chip says so and its × clears it. */\n target?: AdminPerson | null;\n /** Filter by an account — or clear it with `null`. Given, every target's name is a\n * button that narrows the list to it. Left out, the list is not filterable here. */\n onTargetFilter?: (target: AdminPerson | null) => void;\n className?: string;\n labels?: Partial<AdminActionLogLabels>;\n}\n\n/** Whether a person has nothing left that names them — an erased target. */\nfunction scrubbed(person: AdminPerson | null | undefined): boolean {\n return !person || ![person.first, person.last, person.name, person.email].some((part) => (part ?? \"\").trim());\n}\n\nfunction samePerson(a: AdminPerson, b: AdminPerson | null | undefined): boolean {\n if (!b) return false;\n if (a.id !== undefined && a.id !== null && b.id !== undefined && b.id !== null) return a.id === b.id;\n const ea = (a.email ?? \"\").trim().toLowerCase();\n return ea !== \"\" && ea === (b.email ?? \"\").trim().toLowerCase();\n}\n\nexport function AdminActionLog({\n entries,\n paging,\n loading = false,\n target,\n onTargetFilter,\n className,\n labels: labelsProp,\n}: AdminActionLogProps) {\n const labels = useKitLabels(\"adminActionLog\", DEFAULT_ADMIN_ACTION_LOG_LABELS, labelsProp);\n const nameOf = usePersonLabel();\n const filtered = target !== undefined && target !== null && !scrubbed(target);\n\n const actorText = (entry: AdminActionLogEntry): string => {\n const { actor } = entry;\n if (actor === null) return labels.automatic;\n if (actor === \"self\" || samePerson(actor, entry.target)) return labels.bySelf;\n return labels.by(nameOf(actor) || labels.erased);\n };\n\n const targetNode = (entry: AdminActionLogEntry): ReactNode => {\n if (scrubbed(entry.target)) return <span className=\"italic text-[var(--text-muted)]\">{labels.erased}</span>;\n const person = entry.target!;\n const name = nameOf(person);\n const address = (person.email ?? \"\").trim();\n const text = (\n <>\n <span data-private className=\"font-medium text-[var(--text-primary)]\">\n {name}\n </span>\n {address && address !== name && (\n <span data-private className=\"ms-1 text-xs text-[var(--text-muted)]\">\n {address}\n </span>\n )}\n </>\n );\n if (!onTargetFilter || (filtered && samePerson(person, target))) return text;\n return (\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n className=\"h-auto min-h-0 p-0 text-start\"\n aria-label={labels.filterBy(name)}\n onClick={() => onTargetFilter(person)}\n >\n {text}\n </Button>\n );\n };\n\n return (\n <div className={cn(\"space-y-3\", className)}>\n {filtered && onTargetFilter && (\n <Chip size=\"sm\" tone=\"brand\" onRemove={() => onTargetFilter(null)} removeLabel={labels.showAll}>\n <span data-private>{labels.filteredTo(nameOf(target))}</span>\n </Chip>\n )}\n {loading && entries.length === 0 ? (\n <div className=\"flex justify-center py-4\">\n <Spinner label={labels.loading} />\n </div>\n ) : entries.length === 0 ? (\n <Caption>{labels.empty}</Caption>\n ) : (\n <ol\n aria-label={labels.title}\n aria-busy={loading || undefined}\n className={cn(\"divide-y divide-[var(--border)]\", loading && \"opacity-60\")}\n >\n {entries.map((entry) => (\n <li key={entry.id} data-action={entry.action} className=\"space-y-0.5 py-2 first:pt-0 last:pb-0\">\n <div className=\"flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5\">\n <div className=\"flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5 text-sm\">\n <span className=\"font-medium text-[var(--text-primary)]\">\n {labels.actions[entry.action] ?? entry.action}\n </span>\n <span className=\"min-w-0 break-words\">{targetNode(entry)}</span>\n </div>\n <DateMark value={entry.at} display=\"relative\" className=\"text-xs text-[var(--text-muted)]\" />\n </div>\n <div className=\"text-xs text-[var(--text-muted)]\">\n <span data-private={entry.actor !== null && entry.actor !== \"self\" ? true : undefined}>\n {actorText(entry)}\n </span>\n {entry.detail !== undefined && entry.detail !== null && entry.detail !== \"\" && (\n <>\n {\" · \"}\n <span className=\"text-[var(--text-secondary)]\">{entry.detail}</span>\n </>\n )}\n </div>\n </li>\n ))}\n </ol>\n )}\n {paging && paging.total > 0 && (\n <Pagination\n page={paging.page}\n pageSize={paging.pageSize}\n total={paging.total}\n totalPages={Math.max(1, Math.ceil(paging.total / Math.max(1, paging.pageSize)))}\n onPage={paging.onPageChange}\n onPageSize={paging.onPageSizeChange}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|