@eifi1/ui-kit 0.29.0 → 0.30.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 +26 -4
- 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 +113 -0
- package/dist/account/data-export-setting.js.map +1 -0
- package/dist/account/delete-account-setting.d.ts +91 -0
- package/dist/account/delete-account-setting.js +199 -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 +277 -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 +146 -0
- package/dist/account/sessions-setting.js.map +1 -0
- package/dist/admin/admin-action-confirm.d.ts +91 -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 +91 -0
- package/dist/admin/invitations-panel.js +289 -0
- package/dist/admin/invitations-panel.js.map +1 -0
- package/dist/admin/reviewer-scope-editor.d.ts +91 -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 +91 -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 +91 -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 +91 -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 +85 -6
- package/dist/auth/auth-errors.js +62 -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 +17 -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 +61 -9
- package/dist/auth/sign-in-form.js +94 -13
- 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 +16 -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 +12 -1
- package/dist/components/account-settings-labels.js +4 -1
- package/dist/components/account-settings-labels.js.map +1 -1
- package/dist/components/account-settings.d.ts +61 -6
- package/dist/components/account-settings.js +115 -15
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +17 -12
- package/dist/components/button-group.d.ts +17 -12
- package/dist/components/calculator.d.ts +17 -12
- package/dist/components/confirm-dialog.d.ts +17 -12
- package/dist/components/copy-button.d.ts +14 -9
- package/dist/components/danger-confirm.d.ts +17 -12
- package/dist/components/facing-pair.d.ts +17 -12
- package/dist/components/file-button.d.ts +17 -12
- package/dist/components/file-dropzone.d.ts +17 -12
- package/dist/components/form-actions.d.ts +17 -12
- package/dist/components/iban-input.d.ts +17 -12
- package/dist/components/language-select.d.ts +17 -12
- package/dist/components/legal-page.d.ts +17 -12
- package/dist/components/legal.d.ts +17 -12
- package/dist/components/money-field.d.ts +17 -12
- package/dist/components/number-field.d.ts +17 -12
- package/dist/components/number-input.d.ts +17 -12
- package/dist/components/numpad-sheet.d.ts +17 -12
- package/dist/components/phone-input.d.ts +17 -12
- package/dist/components/series-chart.d.ts +17 -12
- package/dist/components/settings-fields.d.ts +16 -11
- package/dist/components/share-card.d.ts +16 -11
- package/dist/components/text-link.d.ts +17 -12
- package/dist/components/time-input.d.ts +17 -12
- package/dist/components/ui.d.ts +13 -8
- package/dist/{register-form-V1OeNlMr.d.ts → delete-account-setting-CGVMckMw.d.ts} +6616 -5645
- package/dist/feedback/feedback-attachment.d.ts +17 -12
- package/dist/feedback/feedback-context.d.ts +17 -12
- package/dist/feedback/feedback-crash.d.ts +17 -12
- package/dist/feedback/feedback-dialog.d.ts +17 -12
- package/dist/feedback/feedback-inbox.d.ts +17 -12
- package/dist/feedback/feedback-labels.d.ts +17 -12
- package/dist/feedback/feedback-menu.d.ts +17 -12
- package/dist/feedback/feedback-record.d.ts +17 -12
- package/dist/feedback/feedback-row-detail.d.ts +17 -12
- package/dist/feedback/feedback-status-undo.d.ts +17 -12
- package/dist/feedback/feedback-submit.d.ts +17 -12
- package/dist/feedback/feedback-swipe.d.ts +17 -12
- package/dist/feedback/feedback-table.d.ts +14 -9
- package/dist/feedback/feedback-thread.d.ts +17 -12
- package/dist/feedback.d.ts +17 -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 +17 -12
- package/dist/i18n/defaults.d.ts +17 -12
- package/dist/i18n/defaults.js +22 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +17 -12
- package/dist/i18n/german.js +215 -11
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +10 -5
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +11 -6
- package/dist/i18n/locales/de-CH.d.ts +17 -12
- package/dist/i18n/locales/en.d.ts +17 -12
- package/dist/i18n/locales/es.d.ts +17 -12
- package/dist/i18n/locales/es.js +207 -9
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +17 -12
- package/dist/i18n/locales/fr.js +209 -6
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +17 -12
- package/dist/i18n/locales/hu.js +207 -11
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +17 -12
- package/dist/i18n/locales/it.js +209 -6
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +17 -12
- package/dist/i18n/locales/zh.js +200 -6
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +17 -12
- package/dist/i18n/review.js +37 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -1
- package/dist/rhf/fields.d.ts +17 -12
- package/dist/rhf/form.d.ts +17 -12
- package/dist/rhf.d.ts +17 -12
- package/dist/shell/app-shell.d.ts +17 -12
- package/dist/shell/top-bar-brand.d.ts +17 -12
- package/dist/shell.d.ts +11 -6
- package/dist/wizard/stepper-nav.d.ts +17 -12
- package/dist/wizard.d.ts +17 -12
- package/package.json +1 -1
- package/src/account/account-parts.ts +67 -0
- package/src/account/data-export-setting.tsx +218 -0
- package/src/account/delete-account-setting.tsx +362 -0
- package/src/account/email-change-setting.tsx +457 -0
- package/src/account/sessions-setting.tsx +283 -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 +516 -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 +165 -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 +207 -35
- package/src/auth/verify-email.tsx +59 -8
- package/src/components/account-chips.tsx +64 -7
- package/src/components/account-settings-labels.ts +15 -1
- package/src/components/account-settings.tsx +199 -23
- package/src/hooks/use-anchored-panel.ts +14 -3
- package/src/i18n/defaults.ts +22 -0
- package/src/i18n/german.ts +241 -5
- package/src/i18n/kit-labels.tsx +33 -0
- package/src/i18n/locales/es.ts +230 -3
- package/src/i18n/locales/fr.ts +232 -0
- package/src/i18n/locales/hu.ts +224 -5
- package/src/i18n/locales/it.ts +230 -0
- package/src/i18n/locales/zh.ts +205 -0
- package/src/i18n/review.ts +36 -0
- package/src/index.ts +93 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
import type { DateInput } from "../lib/format";
|
|
4
|
+
import { useKitLabels } from "../i18n/kit-labels";
|
|
5
|
+
import { DateMark } from "../components/account-chips";
|
|
6
|
+
import { Chip } from "../components/chip";
|
|
7
|
+
import { Pagination } from "../components/data-table-pagination";
|
|
8
|
+
import { Caption } from "../components/text";
|
|
9
|
+
import { Button, Spinner } from "../components/ui";
|
|
10
|
+
import { usePersonLabel } from "./admin-parts";
|
|
11
|
+
import type { AdminPerson } from "./admin-parts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The admin actions, as the audit table records them (docs/user-admin-harmonization.md
|
|
15
|
+
* §2.3, §4.3): `GET /admin/actions?limit=&offset=&target=`, newest first, on the admin
|
|
16
|
+
* page and filtered to one account from its row.
|
|
17
|
+
*
|
|
18
|
+
* Every app writes the same small `admin_actions` row in the transaction of the action
|
|
19
|
+
* (keksdose and Kurvenschmiede had log lines; kastlan has an HTTP log with no named
|
|
20
|
+
* action), so the vocabulary is shared — {@link AdminActionKind} — and an app's own
|
|
21
|
+
* actions join it through `labels.actions`.
|
|
22
|
+
*
|
|
23
|
+
* What a row says, and what it never does: who, what, to whom, when, and a summary of the
|
|
24
|
+
* `detail` the APP writes (ids, roles, counts — never content, §4.3). Three kinds of
|
|
25
|
+
* actor: an admin; **the user themselves** (`deletion_request`: the actor is the target);
|
|
26
|
+
* and **nobody** (the scheduled erasure, the hand-over at erasure, §2.7), said as
|
|
27
|
+
* "automatically". After an erasure the target's name and address are scrubbed and the
|
|
28
|
+
* row stays: it reads "an erased account".
|
|
29
|
+
*
|
|
30
|
+
* It never fetches: the page of rows and the paging are the app's.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** The shared vocabulary of `admin_actions.action` (§4.3). */
|
|
34
|
+
export type AdminActionKind =
|
|
35
|
+
| "deactivate"
|
|
36
|
+
| "reactivate"
|
|
37
|
+
| "role"
|
|
38
|
+
| "membership_remove"
|
|
39
|
+
| "password_change_require"
|
|
40
|
+
| "password_change_withdraw"
|
|
41
|
+
| "mail_verification"
|
|
42
|
+
| "mail_reset"
|
|
43
|
+
| "reviewer"
|
|
44
|
+
| "invite"
|
|
45
|
+
| "invite_resend"
|
|
46
|
+
| "invite_revoke"
|
|
47
|
+
| "transfer"
|
|
48
|
+
| "deletion_request"
|
|
49
|
+
| "deletion_cancel"
|
|
50
|
+
| "erase";
|
|
51
|
+
|
|
52
|
+
/** Every {@link AdminActionKind}, in the contract's order. */
|
|
53
|
+
export const ADMIN_ACTION_KINDS: readonly AdminActionKind[] = [
|
|
54
|
+
"deactivate",
|
|
55
|
+
"reactivate",
|
|
56
|
+
"role",
|
|
57
|
+
"membership_remove",
|
|
58
|
+
"password_change_require",
|
|
59
|
+
"password_change_withdraw",
|
|
60
|
+
"mail_verification",
|
|
61
|
+
"mail_reset",
|
|
62
|
+
"reviewer",
|
|
63
|
+
"invite",
|
|
64
|
+
"invite_resend",
|
|
65
|
+
"invite_revoke",
|
|
66
|
+
"transfer",
|
|
67
|
+
"deletion_request",
|
|
68
|
+
"deletion_cancel",
|
|
69
|
+
"erase",
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/** One logged action — the app's `admin_actions` row with the people joined in. */
|
|
73
|
+
export interface AdminActionLogEntry {
|
|
74
|
+
id: string | number;
|
|
75
|
+
/** When, as the row's `at`. */
|
|
76
|
+
at: DateInput;
|
|
77
|
+
/**
|
|
78
|
+
* Who did it: a person; `"self"` — the user themselves (a deletion request); or `null`
|
|
79
|
+
* — nobody, the scheduled job. A person whose `id` (or address) is the target's is
|
|
80
|
+
* read as `"self"` too.
|
|
81
|
+
*/
|
|
82
|
+
actor: AdminPerson | "self" | null;
|
|
83
|
+
/** A {@link AdminActionKind}, or one of the app's own (named in `labels.actions`). */
|
|
84
|
+
action: AdminActionKind | (string & {});
|
|
85
|
+
/** Whom it was done to. `null`, or a person with nothing left in it: an erased
|
|
86
|
+
* account. An invitation's target is its address (`target_email`). */
|
|
87
|
+
target?: AdminPerson | null;
|
|
88
|
+
/** The `detail` in the app's words — "Member → Admin", "12 items to Ben Example". */
|
|
89
|
+
detail?: ReactNode;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The `adminActionLog` namespace. */
|
|
93
|
+
export interface AdminActionLogLabels {
|
|
94
|
+
/** The list's accessible name. */
|
|
95
|
+
title: string;
|
|
96
|
+
empty: string;
|
|
97
|
+
loading: string;
|
|
98
|
+
/** "by Ada Example". */
|
|
99
|
+
by: (actor: string) => string;
|
|
100
|
+
/** The user acted on their own account. */
|
|
101
|
+
bySelf: string;
|
|
102
|
+
/** No actor: a scheduled job. */
|
|
103
|
+
automatic: string;
|
|
104
|
+
/** A target scrubbed by an erasure. */
|
|
105
|
+
erased: string;
|
|
106
|
+
/** The filter chip: only one account's actions are shown. */
|
|
107
|
+
filteredTo: (target: string) => string;
|
|
108
|
+
/** The chip's ×. */
|
|
109
|
+
showAll: string;
|
|
110
|
+
/** A target name as a filter button — "Show only the actions on Ada Example". */
|
|
111
|
+
filterBy: (target: string) => string;
|
|
112
|
+
/** What each action is called, past tense, keyed by the vocabulary. An app adds its
|
|
113
|
+
* own here (`labels={{ actions: { plan: "Plan changed" } }}`). */
|
|
114
|
+
actions: { [K in AdminActionKind]?: string } & { [action: string]: string | undefined };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const DEFAULT_ADMIN_ACTION_LOG_LABELS: AdminActionLogLabels = {
|
|
118
|
+
title: "Admin actions",
|
|
119
|
+
empty: "No admin actions yet.",
|
|
120
|
+
loading: "Loading…",
|
|
121
|
+
by: (actor) => `by ${actor}`,
|
|
122
|
+
bySelf: "by the user themselves",
|
|
123
|
+
automatic: "automatically",
|
|
124
|
+
erased: "an erased account",
|
|
125
|
+
filteredTo: (target) => `Only ${target}`,
|
|
126
|
+
showAll: "Show all actions",
|
|
127
|
+
filterBy: (target) => `Show only the actions on ${target}`,
|
|
128
|
+
actions: {
|
|
129
|
+
deactivate: "Deactivated",
|
|
130
|
+
reactivate: "Reactivated",
|
|
131
|
+
role: "Role changed",
|
|
132
|
+
membership_remove: "Removed from the company",
|
|
133
|
+
password_change_require: "New password required",
|
|
134
|
+
password_change_withdraw: "Password requirement withdrawn",
|
|
135
|
+
mail_verification: "Verification mail sent",
|
|
136
|
+
mail_reset: "Password reset mail sent",
|
|
137
|
+
reviewer: "Reviewer scope changed",
|
|
138
|
+
invite: "Invited",
|
|
139
|
+
invite_resend: "Invitation resent",
|
|
140
|
+
invite_revoke: "Invitation revoked",
|
|
141
|
+
transfer: "Work transferred",
|
|
142
|
+
deletion_request: "Deletion requested",
|
|
143
|
+
deletion_cancel: "Deletion cancelled",
|
|
144
|
+
erase: "Account erased",
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** The paging of `GET /admin/actions` — DataTable's `ServerPagination`, which the
|
|
149
|
+
* footer is. */
|
|
150
|
+
export interface AdminActionLogPaging {
|
|
151
|
+
/** 0-based. */
|
|
152
|
+
page: number;
|
|
153
|
+
pageSize: number;
|
|
154
|
+
total: number;
|
|
155
|
+
onPageChange: (page: number) => void;
|
|
156
|
+
onPageSizeChange?: (pageSize: number) => void;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface AdminActionLogProps {
|
|
160
|
+
/** One page, newest first, as the server sends it. */
|
|
161
|
+
entries: readonly AdminActionLogEntry[];
|
|
162
|
+
/** Left out: no footer (a short list on a row). */
|
|
163
|
+
paging?: AdminActionLogPaging;
|
|
164
|
+
/** The page is being fetched. */
|
|
165
|
+
loading?: boolean;
|
|
166
|
+
/** The filter in force: only this account's actions are shown (`?target=`). With
|
|
167
|
+
* `onTargetFilter`, a chip says so and its × clears it. */
|
|
168
|
+
target?: AdminPerson | null;
|
|
169
|
+
/** Filter by an account — or clear it with `null`. Given, every target's name is a
|
|
170
|
+
* button that narrows the list to it. Left out, the list is not filterable here. */
|
|
171
|
+
onTargetFilter?: (target: AdminPerson | null) => void;
|
|
172
|
+
className?: string;
|
|
173
|
+
labels?: Partial<AdminActionLogLabels>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Whether a person has nothing left that names them — an erased target. */
|
|
177
|
+
function scrubbed(person: AdminPerson | null | undefined): boolean {
|
|
178
|
+
return !person || ![person.first, person.last, person.name, person.email].some((part) => (part ?? "").trim());
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function samePerson(a: AdminPerson, b: AdminPerson | null | undefined): boolean {
|
|
182
|
+
if (!b) return false;
|
|
183
|
+
if (a.id !== undefined && a.id !== null && b.id !== undefined && b.id !== null) return a.id === b.id;
|
|
184
|
+
const ea = (a.email ?? "").trim().toLowerCase();
|
|
185
|
+
return ea !== "" && ea === (b.email ?? "").trim().toLowerCase();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function AdminActionLog({
|
|
189
|
+
entries,
|
|
190
|
+
paging,
|
|
191
|
+
loading = false,
|
|
192
|
+
target,
|
|
193
|
+
onTargetFilter,
|
|
194
|
+
className,
|
|
195
|
+
labels: labelsProp,
|
|
196
|
+
}: AdminActionLogProps) {
|
|
197
|
+
const labels = useKitLabels("adminActionLog", DEFAULT_ADMIN_ACTION_LOG_LABELS, labelsProp);
|
|
198
|
+
const nameOf = usePersonLabel();
|
|
199
|
+
const filtered = target !== undefined && target !== null && !scrubbed(target);
|
|
200
|
+
|
|
201
|
+
const actorText = (entry: AdminActionLogEntry): string => {
|
|
202
|
+
const { actor } = entry;
|
|
203
|
+
if (actor === null) return labels.automatic;
|
|
204
|
+
if (actor === "self" || samePerson(actor, entry.target)) return labels.bySelf;
|
|
205
|
+
return labels.by(nameOf(actor) || labels.erased);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const targetNode = (entry: AdminActionLogEntry): ReactNode => {
|
|
209
|
+
if (scrubbed(entry.target)) return <span className="italic text-[var(--text-muted)]">{labels.erased}</span>;
|
|
210
|
+
const person = entry.target!;
|
|
211
|
+
const name = nameOf(person);
|
|
212
|
+
const address = (person.email ?? "").trim();
|
|
213
|
+
const text = (
|
|
214
|
+
<>
|
|
215
|
+
<span data-private className="font-medium text-[var(--text-primary)]">
|
|
216
|
+
{name}
|
|
217
|
+
</span>
|
|
218
|
+
{address && address !== name && (
|
|
219
|
+
<span data-private className="ms-1 text-xs text-[var(--text-muted)]">
|
|
220
|
+
{address}
|
|
221
|
+
</span>
|
|
222
|
+
)}
|
|
223
|
+
</>
|
|
224
|
+
);
|
|
225
|
+
if (!onTargetFilter || (filtered && samePerson(person, target))) return text;
|
|
226
|
+
return (
|
|
227
|
+
<Button
|
|
228
|
+
type="button"
|
|
229
|
+
variant="link"
|
|
230
|
+
size="sm"
|
|
231
|
+
className="h-auto min-h-0 p-0 text-start"
|
|
232
|
+
aria-label={labels.filterBy(name)}
|
|
233
|
+
onClick={() => onTargetFilter(person)}
|
|
234
|
+
>
|
|
235
|
+
{text}
|
|
236
|
+
</Button>
|
|
237
|
+
);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<div className={cn("space-y-3", className)}>
|
|
242
|
+
{filtered && onTargetFilter && (
|
|
243
|
+
<Chip size="sm" tone="brand" onRemove={() => onTargetFilter(null)} removeLabel={labels.showAll}>
|
|
244
|
+
<span data-private>{labels.filteredTo(nameOf(target))}</span>
|
|
245
|
+
</Chip>
|
|
246
|
+
)}
|
|
247
|
+
{loading && entries.length === 0 ? (
|
|
248
|
+
<div className="flex justify-center py-4">
|
|
249
|
+
<Spinner label={labels.loading} />
|
|
250
|
+
</div>
|
|
251
|
+
) : entries.length === 0 ? (
|
|
252
|
+
<Caption>{labels.empty}</Caption>
|
|
253
|
+
) : (
|
|
254
|
+
<ol
|
|
255
|
+
aria-label={labels.title}
|
|
256
|
+
aria-busy={loading || undefined}
|
|
257
|
+
className={cn("divide-y divide-[var(--border)]", loading && "opacity-60")}
|
|
258
|
+
>
|
|
259
|
+
{entries.map((entry) => (
|
|
260
|
+
<li key={entry.id} data-action={entry.action} className="space-y-0.5 py-2 first:pt-0 last:pb-0">
|
|
261
|
+
<div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5">
|
|
262
|
+
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5 text-sm">
|
|
263
|
+
<span className="font-medium text-[var(--text-primary)]">
|
|
264
|
+
{labels.actions[entry.action] ?? entry.action}
|
|
265
|
+
</span>
|
|
266
|
+
<span className="min-w-0 break-words">{targetNode(entry)}</span>
|
|
267
|
+
</div>
|
|
268
|
+
<DateMark value={entry.at} display="relative" className="text-xs text-[var(--text-muted)]" />
|
|
269
|
+
</div>
|
|
270
|
+
<div className="text-xs text-[var(--text-muted)]">
|
|
271
|
+
<span data-private={entry.actor !== null && entry.actor !== "self" ? true : undefined}>
|
|
272
|
+
{actorText(entry)}
|
|
273
|
+
</span>
|
|
274
|
+
{entry.detail !== undefined && entry.detail !== null && entry.detail !== "" && (
|
|
275
|
+
<>
|
|
276
|
+
{" · "}
|
|
277
|
+
<span className="text-[var(--text-secondary)]">{entry.detail}</span>
|
|
278
|
+
</>
|
|
279
|
+
)}
|
|
280
|
+
</div>
|
|
281
|
+
</li>
|
|
282
|
+
))}
|
|
283
|
+
</ol>
|
|
284
|
+
)}
|
|
285
|
+
{paging && paging.total > 0 && (
|
|
286
|
+
<Pagination
|
|
287
|
+
page={paging.page}
|
|
288
|
+
pageSize={paging.pageSize}
|
|
289
|
+
total={paging.total}
|
|
290
|
+
totalPages={Math.max(1, Math.ceil(paging.total / Math.max(1, paging.pageSize)))}
|
|
291
|
+
onPage={paging.onPageChange}
|
|
292
|
+
onPageSize={paging.onPageSizeChange}
|
|
293
|
+
/>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { useKitDateFormatter, useKitLocale } from "../i18n/kit-labels";
|
|
4
|
+
import { formatDate, toDate } from "../lib/format";
|
|
5
|
+
import type { DateInput } from "../lib/format";
|
|
6
|
+
import { toLocalIso } from "../lib/dates";
|
|
7
|
+
import { formatPersonName } from "../lib/person-name";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* What the admin parts share and do not export (0.30.0). Internal to `src/admin`.
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** A callback that may answer a promise: resolve = done, reject = stay and say why. */
|
|
16
|
+
export type MaybePromise<T = unknown> = void | Promise<T>;
|
|
17
|
+
|
|
18
|
+
/** A person as the admin parts show one: the two name parts of the auth contract
|
|
19
|
+
* (§3.2), an older whole `name` as the fallback, and the address. */
|
|
20
|
+
export interface AdminPerson {
|
|
21
|
+
id?: string | number | null;
|
|
22
|
+
first?: string | null;
|
|
23
|
+
last?: string | null;
|
|
24
|
+
/** A whole name, for an API that still sends `display_name` only. */
|
|
25
|
+
name?: string | null;
|
|
26
|
+
email?: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A label that is really there — `null`, `false` and `""` are what `cond && text`
|
|
30
|
+
* evaluates to on the path where there is none. */
|
|
31
|
+
export function hasMessage(node: ReactNode): boolean {
|
|
32
|
+
return node !== undefined && node !== null && node !== false && node !== "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The name to say for a person, in the READER's order (auth §3.2,
|
|
37
|
+
* {@link formatPersonName}), else the whole `name`, else the address, else `""`.
|
|
38
|
+
*/
|
|
39
|
+
export function personLabel(person: AdminPerson | null | undefined, locale: string | undefined): string {
|
|
40
|
+
if (!person) return "";
|
|
41
|
+
return (
|
|
42
|
+
formatPersonName({ first: person.first, last: person.last }, locale) ||
|
|
43
|
+
(person.name ?? "").trim() ||
|
|
44
|
+
(person.email ?? "").trim()
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** {@link personLabel} in the provider's locale. */
|
|
49
|
+
export function usePersonLabel(localeProp?: string): (person: AdminPerson | null | undefined) => string {
|
|
50
|
+
const locale = useKitLocale(localeProp);
|
|
51
|
+
return useCallback((person) => personLabel(person, locale), [locale]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A day as words in a sentence ("6 Nov 2026"): the provider's `formatDate` when the app
|
|
56
|
+
* set one (keksdose K12), asked for no weekday — a date in a sentence carries none — else
|
|
57
|
+
* `Intl`'s medium date. `""` for no date.
|
|
58
|
+
*/
|
|
59
|
+
export function useDayText(localeProp?: string): (value: DateInput) => string {
|
|
60
|
+
const locale = useKitLocale(localeProp);
|
|
61
|
+
const fromProvider = useKitDateFormatter();
|
|
62
|
+
return useCallback(
|
|
63
|
+
(value) => {
|
|
64
|
+
const date = toDate(value);
|
|
65
|
+
if (!date) return "";
|
|
66
|
+
const own = fromProvider
|
|
67
|
+
? fromProvider(toLocalIso(date), { unit: "day", source: "dateMark", locale, weekday: false })
|
|
68
|
+
: "";
|
|
69
|
+
return own || formatDate(date, "medium", { locale });
|
|
70
|
+
},
|
|
71
|
+
[fromProvider, locale],
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Call a callback that may throw or answer a promise, as a promise. */
|
|
76
|
+
export function settle<T>(call: () => MaybePromise<T>): Promise<T | void> {
|
|
77
|
+
try {
|
|
78
|
+
return Promise.resolve(call());
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return Promise.reject(error);
|
|
81
|
+
}
|
|
82
|
+
}
|