@cosmicdrift/kumiko-bundled-features 0.240.0 → 0.242.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/package.json +9 -9
- package/src/admin-shell/__tests__/admin-shell-security.integration.test.ts +16 -7
- package/src/admin-shell/__tests__/overview-allowlist.test.ts +30 -0
- package/src/admin-shell/__tests__/overview-screens.boot.test.ts +31 -23
- package/src/admin-shell/changes.json +6 -0
- package/src/admin-shell/feature.ts +60 -6
- package/src/admin-shell/i18n.ts +0 -7
- package/src/admin-shell/web/client-plugin.tsx +1 -11
- package/src/agent-tools/__tests__/agent-doc-lint.test.ts +40 -0
- package/src/agent-tools/__tests__/agent-manifest.test.ts +36 -0
- package/src/agent-tools/__tests__/tool-catalog.test.ts +51 -0
- package/src/agent-tools/agent-doc-lint.ts +14 -3
- package/src/agent-tools/agent-manifest.ts +16 -3
- package/src/agent-tools/feature.ts +1 -1
- package/src/audit/__tests__/audit-screens.boot.test.ts +26 -6
- package/src/audit/changes.json +7 -0
- package/src/audit/feature.ts +57 -7
- package/src/audit/handlers/list.query.ts +50 -14
- package/src/audit/i18n.ts +3 -18
- package/src/audit/web/client-plugin.tsx +1 -7
- package/src/audit/web/index.ts +0 -2
- package/src/config/__tests__/config.integration.test.ts +31 -0
- package/src/config/handlers/readiness.query.ts +8 -1
- package/src/jobs/__tests__/jobs-screens.boot.test.ts +7 -2
- package/src/jobs/__tests__/jobs-security.integration.test.ts +41 -2
- package/src/jobs/changes.json +7 -0
- package/src/jobs/feature.ts +101 -9
- package/src/jobs/handlers/list.query.ts +21 -4
- package/src/jobs/handlers/trigger.write.ts +16 -3
- package/src/jobs/i18n.ts +3 -25
- package/src/jobs/index.ts +1 -0
- package/src/jobs/web/client-plugin.tsx +1 -7
- package/src/jobs/web/index.ts +0 -2
- package/src/tenant/__tests__/tenant.integration.test.ts +32 -0
- package/src/tenant/handlers/list.query.ts +1 -0
- package/src/tier-engine/__tests__/drift.test.ts +1 -1
- package/src/tier-engine/__tests__/tier-engine-screens.boot.test.ts +82 -0
- package/src/tier-engine/changes.json +9 -1
- package/src/tier-engine/constants.ts +5 -5
- package/src/tier-engine/feature.ts +36 -10
- package/src/tier-engine/i18n.ts +19 -22
- package/src/tier-engine/web/client-plugin.tsx +5 -11
- package/src/tier-engine/web/index.ts +0 -1
- package/src/user-data-rights/__tests__/inspector-screens.boot.test.ts +99 -0
- package/src/user-data-rights/changes.json +7 -0
- package/src/user-data-rights/constants.ts +20 -6
- package/src/user-data-rights/feature.ts +113 -14
- package/src/user-data-rights/web/__tests__/privacy-center-screen.test.tsx +29 -71
- package/src/user-data-rights/web/client-plugin.tsx +19 -21
- package/src/user-data-rights/web/i18n.ts +6 -11
- package/src/user-data-rights/web/index.ts +1 -1
- package/src/user-data-rights/web/privacy-center-screen.tsx +32 -245
- package/src/user-profile/__tests__/profile-screen.boot.test.ts +97 -0
- package/src/user-profile/__tests__/profile-screen.test.tsx +140 -104
- package/src/user-profile/changes.json +7 -0
- package/src/user-profile/constants.ts +15 -7
- package/src/user-profile/feature.ts +96 -9
- package/src/user-profile/i18n.ts +8 -12
- package/src/user-profile/web/client-plugin.ts +22 -5
- package/src/user-profile/web/index.ts +1 -1
- package/src/user-profile/web/profile-screen.tsx +122 -265
- package/src/admin-shell/web/overview-layout.tsx +0 -69
- package/src/admin-shell/web/overview-query.ts +0 -15
- package/src/admin-shell/web/platform-overview-screen.tsx +0 -91
- package/src/admin-shell/web/tenant-overview-screen.tsx +0 -91
- package/src/audit/__tests__/audit-log-screen.test.tsx +0 -302
- package/src/audit/web/audit-log-detail-screen.tsx +0 -169
- package/src/audit/web/audit-log-screen.tsx +0 -290
- package/src/jobs/web/__tests__/job-runs-screen.test.tsx +0 -225
- package/src/jobs/web/job-run-detail-screen.tsx +0 -181
- package/src/jobs/web/job-runs-screen.tsx +0 -290
- package/src/tier-engine/web/tier-admin-screen.tsx +0 -168
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
// @runtime client
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// (
|
|
6
|
-
//
|
|
2
|
+
// Export section (Art. 20) — the one part of user-data-rights' privacy-center
|
|
3
|
+
// screen that stays custom (fw#2312). Registered as an EditExtensionSection
|
|
4
|
+
// component (see web/client-plugin.tsx); the projectionDetail screen
|
|
5
|
+
// (feature.ts) supplies the surrounding Section chrome (title, testId), so
|
|
6
|
+
// this component renders only its own body content.
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// Art. 18 Lift ist hier bewusst NICHT actionbar: ein eingeschränktes Konto ist
|
|
13
|
-
// vom Login geblockt und erreicht diesen Screen gar nicht erst — das Aufheben
|
|
14
|
-
// läuft über Support / Magic-Link, nicht über die Self-Service-UI.
|
|
8
|
+
// Export needs async-job polling (worker-Lane-Cron, ~1 Min) plus a
|
|
9
|
+
// signed-URL download — logic a declarative field/action can't express.
|
|
10
|
+
// Restriction (Art. 18) and Deletion (Art. 17) moved onto the screen's
|
|
11
|
+
// declarative fields/actions instead (feature.ts).
|
|
15
12
|
|
|
16
13
|
import {
|
|
17
14
|
useDispatcher,
|
|
@@ -19,30 +16,20 @@ import {
|
|
|
19
16
|
useQuery,
|
|
20
17
|
useTranslation,
|
|
21
18
|
} from "@cosmicdrift/kumiko-renderer";
|
|
22
|
-
import {
|
|
19
|
+
import { postWithDownload } from "@cosmicdrift/kumiko-renderer-web";
|
|
23
20
|
import { type ReactNode, useEffect, useState } from "react";
|
|
24
21
|
import {
|
|
25
22
|
EXPORT_JOB_STATUS,
|
|
26
23
|
type ExportJobStatus,
|
|
27
|
-
USER_ME_QUERY,
|
|
28
24
|
UserDataRightsHandlers,
|
|
29
25
|
UserDataRightsQueries,
|
|
30
26
|
} from "../constants";
|
|
31
27
|
|
|
32
|
-
const STATUS_DELETION_REQUESTED = "deletionRequested";
|
|
33
|
-
const STATUS_RESTRICTED = "restricted";
|
|
34
28
|
// Export-Job läuft async (worker-Lane-Cron, ~1 Min). Solange er pending/running
|
|
35
29
|
// ist, pollt der Screen den Status, damit der Download ohne manuellen Reload
|
|
36
30
|
// erscheint.
|
|
37
31
|
const EXPORT_POLL_MS = 4000;
|
|
38
32
|
|
|
39
|
-
type MeRow = {
|
|
40
|
-
readonly id: string;
|
|
41
|
-
readonly email: string;
|
|
42
|
-
readonly status?: string;
|
|
43
|
-
readonly gracePeriodEnd?: string | null;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
33
|
type ExportJob = {
|
|
47
34
|
readonly id: string;
|
|
48
35
|
readonly status: ExportJobStatus;
|
|
@@ -86,9 +73,14 @@ function StatusBanner({ status }: { readonly status: SectionStatus }): ReactNode
|
|
|
86
73
|
return null;
|
|
87
74
|
}
|
|
88
75
|
|
|
89
|
-
|
|
76
|
+
// Mounted by the renderer's ExtensionSectionMount (already wrapped in
|
|
77
|
+
// <Section title={...} testId="section-extension-...">) — takes no props of
|
|
78
|
+
// its own, ExtensionSectionProps (entityName/entityId/...) are unused since
|
|
79
|
+
// export status/actions are scoped to the current user session, not to a
|
|
80
|
+
// host entity row.
|
|
81
|
+
export function ExportSection(): ReactNode {
|
|
90
82
|
const t = useTranslation();
|
|
91
|
-
const {
|
|
83
|
+
const { Button, Banner } = usePrimitives();
|
|
92
84
|
const dispatcher = useDispatcher();
|
|
93
85
|
const statusQuery = useQuery<ExportStatusResult | null>(UserDataRightsQueries.exportStatus, {});
|
|
94
86
|
const [status, setStatus] = useState<SectionStatus>({ kind: "idle" });
|
|
@@ -131,26 +123,7 @@ function ExportSection(): ReactNode {
|
|
|
131
123
|
}, [inProgress, refetch]);
|
|
132
124
|
|
|
133
125
|
return (
|
|
134
|
-
|
|
135
|
-
title={t("userDataRights.privacyCenter.export.title")}
|
|
136
|
-
testId="privacy-export"
|
|
137
|
-
actions={
|
|
138
|
-
!inProgress ? (
|
|
139
|
-
<Button
|
|
140
|
-
onClick={() => void request()}
|
|
141
|
-
disabled={submitting}
|
|
142
|
-
loading={submitting}
|
|
143
|
-
testId="privacy-export-request"
|
|
144
|
-
>
|
|
145
|
-
{done
|
|
146
|
-
? t("userDataRights.privacyCenter.export.requestNew")
|
|
147
|
-
: submitting
|
|
148
|
-
? t("userDataRights.privacyCenter.export.requesting")
|
|
149
|
-
: t("userDataRights.privacyCenter.export.request")}
|
|
150
|
-
</Button>
|
|
151
|
-
) : undefined
|
|
152
|
-
}
|
|
153
|
-
>
|
|
126
|
+
<>
|
|
154
127
|
<p className="text-sm text-muted-foreground">
|
|
155
128
|
{t("userDataRights.privacyCenter.export.intro")}
|
|
156
129
|
</p>
|
|
@@ -190,209 +163,23 @@ function ExportSection(): ReactNode {
|
|
|
190
163
|
</div>
|
|
191
164
|
</Banner>
|
|
192
165
|
)}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function RestrictionSection({
|
|
199
|
-
me,
|
|
200
|
-
onChanged,
|
|
201
|
-
}: {
|
|
202
|
-
readonly me: MeRow;
|
|
203
|
-
readonly onChanged: () => void;
|
|
204
|
-
}): ReactNode {
|
|
205
|
-
const t = useTranslation();
|
|
206
|
-
const { Section, Button, Banner, Dialog } = usePrimitives();
|
|
207
|
-
const dispatcher = useDispatcher();
|
|
208
|
-
const [dialogOpen, setDialogOpen] = useState(false);
|
|
209
|
-
const [status, setStatus] = useState<SectionStatus>({ kind: "idle" });
|
|
210
|
-
const restricted = me.status === STATUS_RESTRICTED;
|
|
211
|
-
|
|
212
|
-
// Erfolg ⇒ alle Sessions revoked, der User wird abgemeldet. onChanged ist
|
|
213
|
-
// best-effort (das anschließende Refetch läuft typisch in 401 + Logout-
|
|
214
|
-
// Redirect der App-Auth-Schicht).
|
|
215
|
-
const restrict = async (): Promise<void> => {
|
|
216
|
-
const res = await dispatcher.write(UserDataRightsHandlers.restrictAccount, {});
|
|
217
|
-
if (!res.isSuccess) {
|
|
218
|
-
setStatus({ kind: "error", messageKey: failureKey(res.error) });
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
onChanged();
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
return (
|
|
225
|
-
<Section
|
|
226
|
-
title={t("userDataRights.privacyCenter.restriction.title")}
|
|
227
|
-
testId="privacy-restriction"
|
|
228
|
-
variant="destructive"
|
|
229
|
-
actions={
|
|
230
|
-
restricted ? undefined : (
|
|
166
|
+
{!inProgress && (
|
|
167
|
+
<div className="mt-2">
|
|
231
168
|
<Button
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
{t("userDataRights.privacyCenter.restriction.restrict")}
|
|
237
|
-
</Button>
|
|
238
|
-
)
|
|
239
|
-
}
|
|
240
|
-
>
|
|
241
|
-
{restricted ? (
|
|
242
|
-
<Banner variant="error" testId="privacy-restriction-active">
|
|
243
|
-
{t("userDataRights.privacyCenter.restriction.restricted")}
|
|
244
|
-
</Banner>
|
|
245
|
-
) : (
|
|
246
|
-
<>
|
|
247
|
-
<p className="text-sm text-muted-foreground">
|
|
248
|
-
{t("userDataRights.privacyCenter.restriction.explainer")}
|
|
249
|
-
</p>
|
|
250
|
-
<StatusBanner status={status} />
|
|
251
|
-
<Dialog
|
|
252
|
-
open={dialogOpen}
|
|
253
|
-
onOpenChange={setDialogOpen}
|
|
254
|
-
title={t("userDataRights.privacyCenter.restriction.dialogTitle")}
|
|
255
|
-
description={t("userDataRights.privacyCenter.restriction.dialogDescription")}
|
|
256
|
-
variant="danger"
|
|
257
|
-
confirmLabel={t("userDataRights.privacyCenter.restriction.restrict")}
|
|
258
|
-
onConfirm={restrict}
|
|
259
|
-
testId="privacy-restriction-dialog"
|
|
260
|
-
/>
|
|
261
|
-
</>
|
|
262
|
-
)}
|
|
263
|
-
</Section>
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function DeletionSection({
|
|
268
|
-
me,
|
|
269
|
-
onChanged,
|
|
270
|
-
}: {
|
|
271
|
-
readonly me: MeRow;
|
|
272
|
-
readonly onChanged: () => void;
|
|
273
|
-
}): ReactNode {
|
|
274
|
-
const t = useTranslation();
|
|
275
|
-
const { Section, Button, Banner, Dialog } = usePrimitives();
|
|
276
|
-
const dispatcher = useDispatcher();
|
|
277
|
-
const [dialogOpen, setDialogOpen] = useState(false);
|
|
278
|
-
const [status, setStatus] = useState<SectionStatus>({ kind: "idle" });
|
|
279
|
-
const deletionRequested = me.status === STATUS_DELETION_REQUESTED;
|
|
280
|
-
|
|
281
|
-
const requestDeletion = async (): Promise<void> => {
|
|
282
|
-
const res = await dispatcher.write(UserDataRightsHandlers.requestDeletion, {});
|
|
283
|
-
if (!res.isSuccess) {
|
|
284
|
-
setStatus({ kind: "error", messageKey: failureKey(res.error) });
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
setStatus({ kind: "idle" });
|
|
288
|
-
onChanged();
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
const cancelDeletion = async (): Promise<void> => {
|
|
292
|
-
const res = await dispatcher.write(UserDataRightsHandlers.cancelDeletion, {});
|
|
293
|
-
if (!res.isSuccess) {
|
|
294
|
-
setStatus({ kind: "error", messageKey: failureKey(res.error) });
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
setStatus({
|
|
298
|
-
kind: "success",
|
|
299
|
-
messageKey: "userDataRights.privacyCenter.deletion.cancelSuccess",
|
|
300
|
-
});
|
|
301
|
-
onChanged();
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
return (
|
|
305
|
-
<Section
|
|
306
|
-
title={t("userDataRights.privacyCenter.deletion.title")}
|
|
307
|
-
testId="privacy-deletion"
|
|
308
|
-
variant="destructive"
|
|
309
|
-
actions={
|
|
310
|
-
deletionRequested ? (
|
|
311
|
-
<Button
|
|
312
|
-
variant="secondary"
|
|
313
|
-
onClick={() => void cancelDeletion()}
|
|
314
|
-
testId="privacy-deletion-cancel"
|
|
315
|
-
>
|
|
316
|
-
{t("userDataRights.privacyCenter.deletion.cancel")}
|
|
317
|
-
</Button>
|
|
318
|
-
) : (
|
|
319
|
-
<Button
|
|
320
|
-
variant="danger"
|
|
321
|
-
onClick={() => setDialogOpen(true)}
|
|
322
|
-
testId="privacy-deletion-delete"
|
|
169
|
+
onClick={() => void request()}
|
|
170
|
+
disabled={submitting}
|
|
171
|
+
loading={submitting}
|
|
172
|
+
testId="privacy-export-request"
|
|
323
173
|
>
|
|
324
|
-
{
|
|
174
|
+
{done
|
|
175
|
+
? t("userDataRights.privacyCenter.export.requestNew")
|
|
176
|
+
: submitting
|
|
177
|
+
? t("userDataRights.privacyCenter.export.requesting")
|
|
178
|
+
: t("userDataRights.privacyCenter.export.request")}
|
|
325
179
|
</Button>
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
>
|
|
329
|
-
{deletionRequested ? (
|
|
330
|
-
<>
|
|
331
|
-
<Banner variant="error" testId="privacy-deletion-requested">
|
|
332
|
-
{t("userDataRights.privacyCenter.deletion.requested", {
|
|
333
|
-
date: formatDate(me.gracePeriodEnd),
|
|
334
|
-
})}
|
|
335
|
-
</Banner>
|
|
336
|
-
<StatusBanner status={status} />
|
|
337
|
-
</>
|
|
338
|
-
) : (
|
|
339
|
-
<>
|
|
340
|
-
<p className="text-sm text-muted-foreground">
|
|
341
|
-
{t("userDataRights.privacyCenter.deletion.explainer")}
|
|
342
|
-
</p>
|
|
343
|
-
<StatusBanner status={status} />
|
|
344
|
-
<Dialog
|
|
345
|
-
open={dialogOpen}
|
|
346
|
-
onOpenChange={setDialogOpen}
|
|
347
|
-
title={t("userDataRights.privacyCenter.deletion.dialogTitle")}
|
|
348
|
-
description={t("userDataRights.privacyCenter.deletion.dialogDescription")}
|
|
349
|
-
variant="danger"
|
|
350
|
-
confirmLabel={t("userDataRights.privacyCenter.deletion.delete")}
|
|
351
|
-
onConfirm={requestDeletion}
|
|
352
|
-
testId="privacy-deletion-dialog"
|
|
353
|
-
/>
|
|
354
|
-
</>
|
|
180
|
+
</div>
|
|
355
181
|
)}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
export function PrivacyCenterScreen({
|
|
361
|
-
showDeletion = true,
|
|
362
|
-
}: {
|
|
363
|
-
readonly showDeletion?: boolean;
|
|
364
|
-
} = {}): ReactNode {
|
|
365
|
-
const t = useTranslation();
|
|
366
|
-
const { Banner, Heading } = usePrimitives();
|
|
367
|
-
const meQuery = useQuery<MeRow | null>(USER_ME_QUERY, {});
|
|
368
|
-
|
|
369
|
-
if (meQuery.error) {
|
|
370
|
-
return (
|
|
371
|
-
<Banner padded variant="error" testId="privacy-error">
|
|
372
|
-
{t("userDataRights.privacyCenter.loadError")}
|
|
373
|
-
</Banner>
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
const me = meQuery.data;
|
|
377
|
-
if (me === null || me === undefined) {
|
|
378
|
-
return (
|
|
379
|
-
<Banner padded variant="loading" testId="privacy-loading">
|
|
380
|
-
{t("userDataRights.privacyCenter.loading")}
|
|
381
|
-
</Banner>
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const refetch = (): void => {
|
|
386
|
-
void meQuery.refetch?.();
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
return (
|
|
390
|
-
<FormScreenShell className="flex flex-col gap-6" testId="privacy-center-screen">
|
|
391
|
-
<Heading variant="page">{t("userDataRights.privacyCenter.title")}</Heading>
|
|
392
|
-
<p className="text-sm text-muted-foreground">{t("userDataRights.privacyCenter.intro")}</p>
|
|
393
|
-
<ExportSection />
|
|
394
|
-
<RestrictionSection me={me} onChanged={refetch} />
|
|
395
|
-
{showDeletion && <DeletionSection me={me} onChanged={refetch} />}
|
|
396
|
-
</FormScreenShell>
|
|
182
|
+
<StatusBanner status={status} />
|
|
183
|
+
</>
|
|
397
184
|
);
|
|
398
185
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { validateBoot } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { createFilesFeature } from "@cosmicdrift/kumiko-framework/files";
|
|
4
|
+
import { createAuthEmailPasswordFeature } from "../../auth-email-password/feature";
|
|
5
|
+
import { authFoundationFeature } from "../../auth-foundation";
|
|
6
|
+
import { createComplianceProfilesFeature } from "../../compliance-profiles/feature";
|
|
7
|
+
import { createConfigFeature } from "../../config/feature";
|
|
8
|
+
import { createDataRetentionFeature } from "../../data-retention/feature";
|
|
9
|
+
import { createSessionsFeature } from "../../sessions/feature";
|
|
10
|
+
import { createTenantFeature } from "../../tenant/feature";
|
|
11
|
+
import { createUserFeature } from "../../user/feature";
|
|
12
|
+
import { createUserDataRightsFeature } from "../../user-data-rights/feature";
|
|
13
|
+
import { createUserDataRightsDefaultsFeature } from "../../user-data-rights-defaults/feature";
|
|
14
|
+
import { createUserProfileFeature } from "../feature";
|
|
15
|
+
|
|
16
|
+
// fw#2312 — the `profile` screen converted from a custom React component to
|
|
17
|
+
// a declarative projectionDetail (change-password/change-email stay
|
|
18
|
+
// EditExtensionSection components, deletion is fields+actions). `validateBoot`
|
|
19
|
+
// below proves the screen clears every boot-validator rule for
|
|
20
|
+
// projectionDetail extension sections (entityName set, contributesToFormSubmit
|
|
21
|
+
// not true, action handlers resolve) — same pattern as user-data-rights'
|
|
22
|
+
// privacy-center screen (inspector-screens.boot.test.ts), which this mirrors.
|
|
23
|
+
describe("user-profile screen (fw#2312 projectionDetail conversion)", () => {
|
|
24
|
+
const features = [
|
|
25
|
+
createConfigFeature(),
|
|
26
|
+
createUserFeature(),
|
|
27
|
+
createTenantFeature(),
|
|
28
|
+
createAuthEmailPasswordFeature(),
|
|
29
|
+
createDataRetentionFeature(),
|
|
30
|
+
createComplianceProfilesFeature(),
|
|
31
|
+
authFoundationFeature,
|
|
32
|
+
createSessionsFeature(),
|
|
33
|
+
createFilesFeature(),
|
|
34
|
+
createUserDataRightsFeature(),
|
|
35
|
+
createUserDataRightsDefaultsFeature(),
|
|
36
|
+
createUserProfileFeature(),
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
test("the assembled feature set boot-validates", () => {
|
|
40
|
+
expect(() => validateBoot(features)).not.toThrow();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("is a projectionDetail bound to the user's own `me` query", () => {
|
|
44
|
+
const f = createUserProfileFeature();
|
|
45
|
+
const screen = f.screens["profile"];
|
|
46
|
+
expect(screen?.type).toBe("projectionDetail");
|
|
47
|
+
if (screen?.type === "projectionDetail") {
|
|
48
|
+
expect(screen.query).toBe("user:query:user:me");
|
|
49
|
+
expect(screen.access).toEqual({ openToAll: true });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("change-email/change-password are self-persisting extensions scoped to `user`", () => {
|
|
54
|
+
const f = createUserProfileFeature();
|
|
55
|
+
const screen = f.screens["profile"];
|
|
56
|
+
if (screen?.type !== "projectionDetail") throw new Error("expected projectionDetail");
|
|
57
|
+
const extensionSections = screen.layout.sections.filter((s) => s.kind === "extension");
|
|
58
|
+
expect(extensionSections).toHaveLength(2);
|
|
59
|
+
for (const section of extensionSections) {
|
|
60
|
+
expect(section.entityName).toBe("user");
|
|
61
|
+
expect(section.contributesToFormSubmit).not.toBe(true);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("gracePeriodEnd is hidden unless a deletion is actually pending", () => {
|
|
66
|
+
const f = createUserProfileFeature();
|
|
67
|
+
const screen = f.screens["profile"];
|
|
68
|
+
if (screen?.type !== "projectionDetail") throw new Error("expected projectionDetail");
|
|
69
|
+
const graceField = screen.layout.sections
|
|
70
|
+
.flatMap((s) => ("fields" in s ? s.fields : []))
|
|
71
|
+
.find((field) => typeof field === "object" && field.field === "gracePeriodEnd");
|
|
72
|
+
expect(typeof graceField === "object" ? graceField.visible : undefined).toEqual({
|
|
73
|
+
field: "status",
|
|
74
|
+
eq: "deletionRequested",
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("request-deletion/cancel-deletion actions dispatch user-data-rights handlers with the original confirm+visibility rules", () => {
|
|
79
|
+
const f = createUserProfileFeature();
|
|
80
|
+
const screen = f.screens["profile"];
|
|
81
|
+
if (screen?.type !== "projectionDetail") throw new Error("expected projectionDetail");
|
|
82
|
+
const byId = Object.fromEntries((screen.actions ?? []).map((a) => [a.id, a]));
|
|
83
|
+
|
|
84
|
+
expect(byId["request-deletion"]).toMatchObject({
|
|
85
|
+
handler: "user-data-rights:write:request-deletion",
|
|
86
|
+
confirm: "profile.danger.dialogDescription",
|
|
87
|
+
visible: { field: "status", ne: "deletionRequested" },
|
|
88
|
+
style: "danger",
|
|
89
|
+
});
|
|
90
|
+
// Cancel has no confirm — the pre-#2312 custom screen executed it directly.
|
|
91
|
+
expect(byId["cancel-deletion"]).toMatchObject({
|
|
92
|
+
handler: "user-data-rights:write:cancel-deletion",
|
|
93
|
+
visible: { field: "status", eq: "deletionRequested" },
|
|
94
|
+
});
|
|
95
|
+
expect((byId["cancel-deletion"] as { readonly confirm?: string }).confirm).toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
});
|