@cosmicdrift/kumiko-bundled-features 0.255.1 → 0.256.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/audit/feature.ts +17 -12
- package/src/delivery/feature.ts +6 -5
- package/src/jobs/feature.ts +30 -22
- package/src/sessions/screens.ts +14 -13
- package/src/tags/screens.ts +8 -7
- package/src/tags/web/__tests__/tag-picker.test.tsx +153 -0
- package/src/tags/web/tag-picker.tsx +8 -5
- package/src/tenant/feature.ts +2 -1
- package/src/tenant/screens.ts +16 -15
- package/src/user/screens.ts +5 -4
- package/src/user-data-rights/feature.ts +32 -10
- package/src/user-data-rights/screens.ts +2 -1
- package/src/user-profile/feature.ts +32 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.256.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -130,12 +130,12 @@
|
|
|
130
130
|
"./workflow-runner": "./src/workflow-runner/index.ts"
|
|
131
131
|
},
|
|
132
132
|
"dependencies": {
|
|
133
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
134
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
135
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
136
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
137
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
138
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
133
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.256.0",
|
|
134
|
+
"@cosmicdrift/kumiko-framework": "0.256.0",
|
|
135
|
+
"@cosmicdrift/kumiko-headless": "0.256.0",
|
|
136
|
+
"@cosmicdrift/kumiko-renderer": "0.256.0",
|
|
137
|
+
"@cosmicdrift/kumiko-renderer-web": "0.256.0",
|
|
138
|
+
"@cosmicdrift/kumiko-types": "0.256.0",
|
|
139
139
|
"@mollie/api-client": "^4.5.0",
|
|
140
140
|
"@node-rs/argon2": "^2.0.2",
|
|
141
141
|
"@types/mailparser": "^3.4.6",
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
"devDependencies": {
|
|
165
165
|
"@testing-library/user-event": "^14.6.1",
|
|
166
166
|
"@types/qrcode": "^1.5.5",
|
|
167
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
168
|
-
"@cosmicdrift/kumiko-locale-es": "0.
|
|
167
|
+
"@cosmicdrift/kumiko-locale-de": "0.256.0",
|
|
168
|
+
"@cosmicdrift/kumiko-locale-es": "0.256.0"
|
|
169
169
|
}
|
|
170
170
|
}
|
package/src/audit/feature.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
access,
|
|
3
3
|
defineFeature,
|
|
4
4
|
type FeatureDefinition,
|
|
5
|
+
i18nKey,
|
|
5
6
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
6
7
|
import { AUDIT_LOG_DETAIL_SCREEN_ID, AUDIT_LOG_SCREEN_ID, AuditQueries } from "./constants";
|
|
7
8
|
import { detailsQuery } from "./handlers/details.query";
|
|
@@ -44,9 +45,13 @@ export function createAuditFeature(): FeatureDefinition {
|
|
|
44
45
|
type: "projectionList",
|
|
45
46
|
query: AuditQueries.list,
|
|
46
47
|
columns: [
|
|
47
|
-
{
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
{
|
|
49
|
+
field: "createdAt",
|
|
50
|
+
label: i18nKey("audit.log.col.when"),
|
|
51
|
+
renderer: { format: "timestamp" },
|
|
52
|
+
},
|
|
53
|
+
{ field: "type", label: i18nKey("audit.log.col.type") },
|
|
54
|
+
{ field: "createdBy", label: i18nKey("audit.log.col.actor") },
|
|
50
55
|
],
|
|
51
56
|
searchable: true,
|
|
52
57
|
defaultSort: { field: "createdAt", dir: "desc" },
|
|
@@ -54,7 +59,7 @@ export function createAuditFeature(): FeatureDefinition {
|
|
|
54
59
|
{
|
|
55
60
|
kind: "navigate",
|
|
56
61
|
id: "details",
|
|
57
|
-
label: "audit.log.details",
|
|
62
|
+
label: i18nKey("audit.log.details"),
|
|
58
63
|
screen: AUDIT_LOG_DETAIL_SCREEN_ID,
|
|
59
64
|
entityId: "id",
|
|
60
65
|
rowClick: true,
|
|
@@ -71,14 +76,14 @@ export function createAuditFeature(): FeatureDefinition {
|
|
|
71
76
|
query: AuditQueries.details,
|
|
72
77
|
idParam: "id",
|
|
73
78
|
fieldLabels: {
|
|
74
|
-
type: "audit.log.col.type",
|
|
75
|
-
createdAt: "audit.log.col.when",
|
|
76
|
-
aggregateType: "audit.log.col.aggregateType",
|
|
77
|
-
aggregateId: "audit.log.col.aggregateId",
|
|
78
|
-
createdBy: "audit.log.col.actor",
|
|
79
|
-
id: "audit.log.detail.field.id",
|
|
80
|
-
payload: "audit.log.detail.payload",
|
|
81
|
-
metadata: "audit.log.detail.metadata",
|
|
79
|
+
type: i18nKey("audit.log.col.type"),
|
|
80
|
+
createdAt: i18nKey("audit.log.col.when"),
|
|
81
|
+
aggregateType: i18nKey("audit.log.col.aggregateType"),
|
|
82
|
+
aggregateId: i18nKey("audit.log.col.aggregateId"),
|
|
83
|
+
createdBy: i18nKey("audit.log.col.actor"),
|
|
84
|
+
id: i18nKey("audit.log.detail.field.id"),
|
|
85
|
+
payload: i18nKey("audit.log.detail.payload"),
|
|
86
|
+
metadata: i18nKey("audit.log.detail.metadata"),
|
|
82
87
|
},
|
|
83
88
|
layout: {
|
|
84
89
|
sections: [
|
package/src/delivery/feature.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
access,
|
|
4
4
|
defineFeature,
|
|
5
5
|
type FeatureDefinition,
|
|
6
|
+
i18nKey,
|
|
6
7
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
8
|
import type { z } from "zod";
|
|
8
9
|
import {
|
|
@@ -145,16 +146,16 @@ export function createDeliveryFeature(options?: DeliveryFeatureOptions): Feature
|
|
|
145
146
|
columns: [
|
|
146
147
|
{
|
|
147
148
|
field: "tenantId",
|
|
148
|
-
label: "delivery.log.col.tenantId",
|
|
149
|
+
label: i18nKey("delivery.log.col.tenantId"),
|
|
149
150
|
refEntity: "tenant:tenant",
|
|
150
151
|
refLabelField: "name",
|
|
151
152
|
},
|
|
152
|
-
{ field: "type", label: "delivery.log.col.type" },
|
|
153
|
-
{ field: "channel", label: "delivery.log.col.channel" },
|
|
154
|
-
{ field: "recipient", label: "delivery.log.col.recipient" },
|
|
153
|
+
{ field: "type", label: i18nKey("delivery.log.col.type") },
|
|
154
|
+
{ field: "channel", label: i18nKey("delivery.log.col.channel") },
|
|
155
|
+
{ field: "recipient", label: i18nKey("delivery.log.col.recipient") },
|
|
155
156
|
{
|
|
156
157
|
field: "status",
|
|
157
|
-
label: "delivery.log.col.status",
|
|
158
|
+
label: i18nKey("delivery.log.col.status"),
|
|
158
159
|
renderer: { react: { __component: DELIVERY_STATUS_CELL_COMPONENT } },
|
|
159
160
|
},
|
|
160
161
|
],
|
package/src/jobs/feature.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
defineFeature,
|
|
3
|
+
type FeatureDefinition,
|
|
4
|
+
i18nKey,
|
|
5
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
2
6
|
import { JOB_RUN_DETAIL_SCREEN_ID, JOB_RUNS_SCREEN_ID, JobHandlers, JobQueries } from "./constants";
|
|
3
7
|
import { catalogQuery } from "./handlers/catalog.query";
|
|
4
8
|
import { detailQuery } from "./handlers/detail.query";
|
|
@@ -119,22 +123,26 @@ export function createJobsFeature(options: JobsFeatureOptions = {}): FeatureDefi
|
|
|
119
123
|
type: "projectionList",
|
|
120
124
|
query: JobQueries.list,
|
|
121
125
|
columns: [
|
|
122
|
-
{ field: "jobName", label: "jobs.runs.col.job" },
|
|
123
|
-
{ field: "status", label: "jobs.runs.col.status" },
|
|
124
|
-
{
|
|
125
|
-
|
|
126
|
+
{ field: "jobName", label: i18nKey("jobs.runs.col.job") },
|
|
127
|
+
{ field: "status", label: i18nKey("jobs.runs.col.status") },
|
|
128
|
+
{
|
|
129
|
+
field: "startedAt",
|
|
130
|
+
label: i18nKey("jobs.runs.col.started"),
|
|
131
|
+
renderer: { format: "timestamp" },
|
|
132
|
+
},
|
|
133
|
+
{ field: "duration", label: i18nKey("jobs.runs.col.duration") },
|
|
126
134
|
],
|
|
127
135
|
defaultSort: { field: "startedAt", dir: "desc" },
|
|
128
136
|
facets: [
|
|
129
137
|
{
|
|
130
138
|
field: "status",
|
|
131
139
|
type: "select",
|
|
132
|
-
label: "jobs.runs.filter.status",
|
|
140
|
+
label: i18nKey("jobs.runs.filter.status"),
|
|
133
141
|
options: [
|
|
134
|
-
{ value: "queued", label: "jobs.runs.filter.queued" },
|
|
135
|
-
{ value: "running", label: "jobs.runs.filter.running" },
|
|
136
|
-
{ value: "completed", label: "jobs.runs.filter.completed" },
|
|
137
|
-
{ value: "failed", label: "jobs.runs.filter.failed" },
|
|
142
|
+
{ value: "queued", label: i18nKey("jobs.runs.filter.queued") },
|
|
143
|
+
{ value: "running", label: i18nKey("jobs.runs.filter.running") },
|
|
144
|
+
{ value: "completed", label: i18nKey("jobs.runs.filter.completed") },
|
|
145
|
+
{ value: "failed", label: i18nKey("jobs.runs.filter.failed") },
|
|
138
146
|
],
|
|
139
147
|
},
|
|
140
148
|
],
|
|
@@ -142,7 +150,7 @@ export function createJobsFeature(options: JobsFeatureOptions = {}): FeatureDefi
|
|
|
142
150
|
{
|
|
143
151
|
kind: "navigate",
|
|
144
152
|
id: "open",
|
|
145
|
-
label: "jobs.runs.open",
|
|
153
|
+
label: i18nKey("jobs.runs.open"),
|
|
146
154
|
screen: JOB_RUN_DETAIL_SCREEN_ID,
|
|
147
155
|
entityId: "id",
|
|
148
156
|
rowClick: true,
|
|
@@ -152,7 +160,7 @@ export function createJobsFeature(options: JobsFeatureOptions = {}): FeatureDefi
|
|
|
152
160
|
{
|
|
153
161
|
kind: "drawer",
|
|
154
162
|
id: "trigger",
|
|
155
|
-
label: "jobs.trigger.title",
|
|
163
|
+
label: i18nKey("jobs.trigger.title"),
|
|
156
164
|
screen: "job-trigger",
|
|
157
165
|
style: "primary",
|
|
158
166
|
},
|
|
@@ -169,7 +177,7 @@ export function createJobsFeature(options: JobsFeatureOptions = {}): FeatureDefi
|
|
|
169
177
|
payload: { type: "longText", multiline: true, default: "{}" },
|
|
170
178
|
},
|
|
171
179
|
layout: { sections: [{ fields: ["jobName", "payload"] }] },
|
|
172
|
-
submitLabel: "jobs.trigger.submit",
|
|
180
|
+
submitLabel: i18nKey("jobs.trigger.submit"),
|
|
173
181
|
access: systemAdminAccess,
|
|
174
182
|
description: "Manually trigger a job by name with an optional JSON object payload.",
|
|
175
183
|
});
|
|
@@ -179,14 +187,14 @@ export function createJobsFeature(options: JobsFeatureOptions = {}): FeatureDefi
|
|
|
179
187
|
query: JobQueries.details,
|
|
180
188
|
idParam: "runId",
|
|
181
189
|
fieldLabels: {
|
|
182
|
-
jobName: "jobs.detail.field.job",
|
|
183
|
-
status: "jobs.detail.field.status",
|
|
184
|
-
id: "jobs.detail.field.id",
|
|
185
|
-
startedAt: "jobs.detail.field.started",
|
|
186
|
-
finishedAt: "jobs.detail.field.finished",
|
|
187
|
-
duration: "jobs.detail.field.duration",
|
|
188
|
-
error: "jobs.detail.field.error",
|
|
189
|
-
logs: "jobs.detail.logs",
|
|
190
|
+
jobName: i18nKey("jobs.detail.field.job"),
|
|
191
|
+
status: i18nKey("jobs.detail.field.status"),
|
|
192
|
+
id: i18nKey("jobs.detail.field.id"),
|
|
193
|
+
startedAt: i18nKey("jobs.detail.field.started"),
|
|
194
|
+
finishedAt: i18nKey("jobs.detail.field.finished"),
|
|
195
|
+
duration: i18nKey("jobs.detail.field.duration"),
|
|
196
|
+
error: i18nKey("jobs.detail.field.error"),
|
|
197
|
+
logs: i18nKey("jobs.detail.logs"),
|
|
190
198
|
},
|
|
191
199
|
layout: {
|
|
192
200
|
sections: [
|
|
@@ -213,7 +221,7 @@ export function createJobsFeature(options: JobsFeatureOptions = {}): FeatureDefi
|
|
|
213
221
|
{
|
|
214
222
|
kind: "writeHandler",
|
|
215
223
|
id: "retry",
|
|
216
|
-
label: "jobs.detail.retry",
|
|
224
|
+
label: i18nKey("jobs.detail.retry"),
|
|
217
225
|
handler: JobHandlers.retry,
|
|
218
226
|
payload: { map: { runId: "id" } },
|
|
219
227
|
visible: { field: "status", eq: "failed" },
|
package/src/sessions/screens.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
access,
|
|
3
|
+
i18nKey,
|
|
3
4
|
type ProjectionDetailScreenDefinition,
|
|
4
5
|
type ProjectionListScreenDefinition,
|
|
5
6
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
@@ -16,26 +17,26 @@ export const sessionListScreen: ProjectionListScreenDefinition = {
|
|
|
16
17
|
// be present once the query accepts `sort` (fw#2230).
|
|
17
18
|
defaultSort: { field: "createdAt", dir: "desc" },
|
|
18
19
|
columns: [
|
|
19
|
-
{ field: "id", label: "sessions.list.col.id" },
|
|
20
|
+
{ field: "id", label: i18nKey("sessions.list.col.id") },
|
|
20
21
|
{
|
|
21
22
|
field: "userId",
|
|
22
|
-
label: "sessions.list.col.userId",
|
|
23
|
+
label: i18nKey("sessions.list.col.userId"),
|
|
23
24
|
refEntity: "user:user",
|
|
24
25
|
refLabelField: "displayName",
|
|
25
26
|
},
|
|
26
27
|
{
|
|
27
28
|
field: "createdAt",
|
|
28
|
-
label: "sessions.list.col.createdAt",
|
|
29
|
+
label: i18nKey("sessions.list.col.createdAt"),
|
|
29
30
|
renderer: { format: "timestamp" },
|
|
30
31
|
},
|
|
31
32
|
{
|
|
32
33
|
field: "expiresAt",
|
|
33
|
-
label: "sessions.list.col.expiresAt",
|
|
34
|
+
label: i18nKey("sessions.list.col.expiresAt"),
|
|
34
35
|
renderer: { format: "timestamp" },
|
|
35
36
|
},
|
|
36
37
|
{
|
|
37
38
|
field: "revokedAt",
|
|
38
|
-
label: "sessions.list.col.revokedAt",
|
|
39
|
+
label: i18nKey("sessions.list.col.revokedAt"),
|
|
39
40
|
renderer: { format: "timestamp" },
|
|
40
41
|
},
|
|
41
42
|
],
|
|
@@ -43,7 +44,7 @@ export const sessionListScreen: ProjectionListScreenDefinition = {
|
|
|
43
44
|
{
|
|
44
45
|
kind: "navigate",
|
|
45
46
|
id: "open",
|
|
46
|
-
label: "sessions.list.action.open",
|
|
47
|
+
label: i18nKey("sessions.list.action.open"),
|
|
47
48
|
screen: SESSION_DETAIL_SCREEN_ID,
|
|
48
49
|
entityId: "id",
|
|
49
50
|
rowClick: true,
|
|
@@ -77,13 +78,13 @@ export const sessionDetailScreen: ProjectionDetailScreenDefinition = {
|
|
|
77
78
|
],
|
|
78
79
|
},
|
|
79
80
|
fieldLabels: {
|
|
80
|
-
id: "sessions.detail.field.id",
|
|
81
|
-
userId: "sessions.detail.field.userId",
|
|
82
|
-
createdAt: "sessions.detail.field.createdAt",
|
|
83
|
-
expiresAt: "sessions.detail.field.expiresAt",
|
|
84
|
-
revokedAt: "sessions.detail.field.revokedAt",
|
|
85
|
-
ip: "sessions.detail.field.ip",
|
|
86
|
-
userAgent: "sessions.detail.field.userAgent",
|
|
81
|
+
id: i18nKey("sessions.detail.field.id"),
|
|
82
|
+
userId: i18nKey("sessions.detail.field.userId"),
|
|
83
|
+
createdAt: i18nKey("sessions.detail.field.createdAt"),
|
|
84
|
+
expiresAt: i18nKey("sessions.detail.field.expiresAt"),
|
|
85
|
+
revokedAt: i18nKey("sessions.detail.field.revokedAt"),
|
|
86
|
+
ip: i18nKey("sessions.detail.field.ip"),
|
|
87
|
+
userAgent: i18nKey("sessions.detail.field.userAgent"),
|
|
87
88
|
},
|
|
88
89
|
access: listAccess,
|
|
89
90
|
};
|
package/src/tags/screens.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
AccessRule,
|
|
3
|
-
EntityEditScreenDefinition,
|
|
4
|
-
EntityListScreenDefinition,
|
|
1
|
+
import {
|
|
2
|
+
type AccessRule,
|
|
3
|
+
type EntityEditScreenDefinition,
|
|
4
|
+
type EntityListScreenDefinition,
|
|
5
|
+
i18nKey,
|
|
5
6
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
6
7
|
import { DEFAULT_TAG_ACCESS, TAGS_EDIT_SCREEN_ID, TAGS_SCREEN_ID } from "./constants";
|
|
7
8
|
|
|
@@ -23,18 +24,18 @@ export function createTagListScreen(
|
|
|
23
24
|
{
|
|
24
25
|
kind: "navigate",
|
|
25
26
|
id: "edit",
|
|
26
|
-
label: "kumiko.actions.edit",
|
|
27
|
+
label: i18nKey("kumiko.actions.edit"),
|
|
27
28
|
screen: TAGS_EDIT_SCREEN_ID,
|
|
28
29
|
entityId: "id",
|
|
29
30
|
},
|
|
30
31
|
{
|
|
31
32
|
kind: "writeHandler",
|
|
32
33
|
id: "delete",
|
|
33
|
-
label: "kumiko.actions.delete",
|
|
34
|
+
label: i18nKey("kumiko.actions.delete"),
|
|
34
35
|
// Convention QN — same cascade body as legacy tags:write:delete-tag.
|
|
35
36
|
handler: "tags:write:tag:delete",
|
|
36
37
|
payload: { pick: ["id"] },
|
|
37
|
-
confirm: "kumiko.actions.delete-confirm",
|
|
38
|
+
confirm: i18nKey("kumiko.actions.delete-confirm"),
|
|
38
39
|
style: "danger",
|
|
39
40
|
},
|
|
40
41
|
],
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
createStaticLocaleResolver,
|
|
4
|
+
LocaleProvider,
|
|
5
|
+
PrimitivesProvider,
|
|
6
|
+
} from "@cosmicdrift/kumiko-renderer";
|
|
7
|
+
import { defaultPrimitives } from "@cosmicdrift/kumiko-renderer-web";
|
|
8
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
9
|
+
import { type ReactNode, useState } from "react";
|
|
10
|
+
import { TagsQueries } from "../../constants";
|
|
11
|
+
import { defaultTranslations } from "../i18n";
|
|
12
|
+
import type { TagPicker as TagPickerComponent } from "../tag-picker";
|
|
13
|
+
|
|
14
|
+
// tag-filter.test.tsx and tag-section.test.tsx register a process-wide
|
|
15
|
+
// mock.module stub on the resolved "../tag-picker" path; the query-string
|
|
16
|
+
// specifier loads the real component past it. It goes through a variable
|
|
17
|
+
// because tsc cannot resolve the suffixed specifier, so the namespace type
|
|
18
|
+
// comes from the type-only import above.
|
|
19
|
+
const REAL_TAG_PICKER_SPECIFIER = "../tag-picker?real=tag-picker-test";
|
|
20
|
+
const { TagPicker } = (await import(REAL_TAG_PICKER_SPECIFIER)) as {
|
|
21
|
+
readonly TagPicker: typeof TagPickerComponent;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type TagRow = { id: string; name: string; color?: string; scope?: string; version: number };
|
|
25
|
+
type AssignmentRow = { tagId: string };
|
|
26
|
+
|
|
27
|
+
let catalogRows: readonly TagRow[] = [];
|
|
28
|
+
let assignmentRows: readonly AssignmentRow[] = [];
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
catalogRows = [
|
|
32
|
+
{ id: "t1", name: "urgent", color: "#ef4444", version: 1 },
|
|
33
|
+
{ id: "t2", name: "later", version: 1 },
|
|
34
|
+
];
|
|
35
|
+
assignmentRows = [];
|
|
36
|
+
dispatchSpy.mockClear();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const dispatchSpy = mock(async () => ({ isSuccess: true, data: undefined }));
|
|
40
|
+
|
|
41
|
+
const useQuerySpy = mock((type: string) => {
|
|
42
|
+
if (type === TagsQueries.tagList) {
|
|
43
|
+
return {
|
|
44
|
+
data: { rows: catalogRows },
|
|
45
|
+
loading: false,
|
|
46
|
+
error: null,
|
|
47
|
+
refetch: mock(async () => {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
data: { rows: assignmentRows },
|
|
52
|
+
loading: false,
|
|
53
|
+
error: null,
|
|
54
|
+
refetch: mock(async () => {}),
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const actual_renderer = await import("@cosmicdrift/kumiko-renderer");
|
|
59
|
+
mock.module("@cosmicdrift/kumiko-renderer", () => ({
|
|
60
|
+
...actual_renderer,
|
|
61
|
+
useDispatcher: mock(() => ({ write: dispatchSpy, query: mock(), batch: mock() })),
|
|
62
|
+
useQuery: useQuerySpy,
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
function Wrapper({ children }: { readonly children: ReactNode }): ReactNode {
|
|
66
|
+
return (
|
|
67
|
+
<LocaleProvider resolver={createStaticLocaleResolver()} fallbackBundles={[defaultTranslations]}>
|
|
68
|
+
<PrimitivesProvider value={defaultPrimitives}>{children}</PrimitivesProvider>
|
|
69
|
+
</LocaleProvider>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function Host({
|
|
74
|
+
initialValue,
|
|
75
|
+
initialOpen,
|
|
76
|
+
}: {
|
|
77
|
+
readonly initialValue: readonly string[];
|
|
78
|
+
readonly initialOpen: boolean;
|
|
79
|
+
}): ReactNode {
|
|
80
|
+
const [open, setOpen] = useState(initialOpen);
|
|
81
|
+
const [applied, setApplied] = useState<readonly string[]>(initialValue);
|
|
82
|
+
const [bump, setBump] = useState(0);
|
|
83
|
+
const value = applied.map((id) => id);
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<button type="button" data-testid="host-open" onClick={() => setOpen(true)}>
|
|
87
|
+
open
|
|
88
|
+
</button>
|
|
89
|
+
<button type="button" data-testid="host-rerender" onClick={() => setBump(bump + 1)}>
|
|
90
|
+
rerender {bump}
|
|
91
|
+
</button>
|
|
92
|
+
<span data-testid="host-applied">{value.join(",")}</span>
|
|
93
|
+
<TagPicker
|
|
94
|
+
entityType="note"
|
|
95
|
+
value={value}
|
|
96
|
+
onChange={setApplied}
|
|
97
|
+
open={open}
|
|
98
|
+
onOpenChange={setOpen}
|
|
99
|
+
/>
|
|
100
|
+
</>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
describe("TagPicker — open/confirm/cancel sync", () => {
|
|
105
|
+
test("confirm hands the toggled selection back to the caller", async () => {
|
|
106
|
+
render(
|
|
107
|
+
<Wrapper>
|
|
108
|
+
<Host initialValue={[]} initialOpen={true} />
|
|
109
|
+
</Wrapper>,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
fireEvent.click(await screen.findByTestId("tag-manager-toggle-t1"));
|
|
113
|
+
fireEvent.click(screen.getByTestId("tag-picker-dialog-confirm"));
|
|
114
|
+
|
|
115
|
+
await waitFor(() => expect(screen.getByTestId("host-applied").textContent).toBe("t1"));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("cancel discards the buffered selection; reopening starts from the caller's value", async () => {
|
|
119
|
+
render(
|
|
120
|
+
<Wrapper>
|
|
121
|
+
<Host initialValue={["t1"]} initialOpen={true} />
|
|
122
|
+
</Wrapper>,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
fireEvent.click(await screen.findByTestId("tag-manager-toggle-t2"));
|
|
126
|
+
fireEvent.click(screen.getByTestId("tag-picker-dialog-cancel"));
|
|
127
|
+
|
|
128
|
+
await waitFor(() => expect(screen.queryByTestId("tag-picker-dialog")).toBeNull());
|
|
129
|
+
expect(screen.getByTestId("host-applied").textContent).toBe("t1");
|
|
130
|
+
|
|
131
|
+
fireEvent.click(screen.getByTestId("host-open"));
|
|
132
|
+
fireEvent.click(await screen.findByTestId("tag-picker-dialog-confirm"));
|
|
133
|
+
|
|
134
|
+
await waitFor(() => expect(screen.getByTestId("host-applied").textContent).toBe("t1"));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("a parent re-render while open keeps the in-flight selection", async () => {
|
|
138
|
+
render(
|
|
139
|
+
<Wrapper>
|
|
140
|
+
<Host initialValue={[]} initialOpen={true} />
|
|
141
|
+
</Wrapper>,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
fireEvent.click(await screen.findByTestId("tag-manager-toggle-t1"));
|
|
145
|
+
expect(screen.getByTestId("tag-manager-toggle-t1").textContent).toBe("✓");
|
|
146
|
+
|
|
147
|
+
fireEvent.click(screen.getByTestId("host-rerender"));
|
|
148
|
+
await waitFor(() => expect(screen.getByTestId("tag-manager-toggle-t1").textContent).toBe("✓"));
|
|
149
|
+
|
|
150
|
+
fireEvent.click(screen.getByTestId("tag-picker-dialog-confirm"));
|
|
151
|
+
await waitFor(() => expect(screen.getByTestId("host-applied").textContent).toBe("t1"));
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// caller's entityType so only global + matching labels are offered.
|
|
8
8
|
|
|
9
9
|
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
10
|
-
import { type ReactNode,
|
|
10
|
+
import { type ReactNode, useState } from "react";
|
|
11
11
|
import { TagManager } from "./tag-manager";
|
|
12
12
|
|
|
13
13
|
export function TagPicker({
|
|
@@ -26,11 +26,14 @@ export function TagPicker({
|
|
|
26
26
|
const { Dialog } = usePrimitives();
|
|
27
27
|
const t = useTranslation();
|
|
28
28
|
const [buffer, setBuffer] = useState<readonly string[]>(value);
|
|
29
|
-
|
|
30
|
-
//
|
|
31
|
-
|
|
29
|
+
const [wasOpen, setWasOpen] = useState(open);
|
|
30
|
+
// Syncing on the open transition alone, not on `value`: both call sites derive
|
|
31
|
+
// `value` freshly each render, so a value-keyed effect discarded the in-flight
|
|
32
|
+
// selection on every parent re-render.
|
|
33
|
+
if (open !== wasOpen) {
|
|
34
|
+
setWasOpen(open);
|
|
32
35
|
if (open) setBuffer(value);
|
|
33
|
-
}
|
|
36
|
+
}
|
|
34
37
|
|
|
35
38
|
return (
|
|
36
39
|
<Dialog
|
package/src/tenant/feature.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
defineEntityUpdateHandler,
|
|
8
8
|
defineFeature,
|
|
9
9
|
type FeatureDefinition,
|
|
10
|
+
i18nKey,
|
|
10
11
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
11
12
|
import { activeTenantIdsQuery } from "./handlers/active-tenant-ids.query";
|
|
12
13
|
import { addMemberWrite } from "./handlers/add-member.write";
|
|
@@ -176,7 +177,7 @@ export function createTenantFeature(options?: TenantFeatureOptions): FeatureDefi
|
|
|
176
177
|
}
|
|
177
178
|
r.nav({
|
|
178
179
|
id: "members",
|
|
179
|
-
label: "tenant.nav.members",
|
|
180
|
+
label: i18nKey("tenant.nav.members"),
|
|
180
181
|
icon: "users",
|
|
181
182
|
screen: "tenant:screen:members",
|
|
182
183
|
order: 20,
|
package/src/tenant/screens.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
access,
|
|
4
4
|
type EntityEditScreenDefinition,
|
|
5
5
|
type EntityListScreenDefinition,
|
|
6
|
+
i18nKey,
|
|
6
7
|
type ScreenDefinition,
|
|
7
8
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
8
9
|
import {
|
|
@@ -35,7 +36,7 @@ export const tenantListScreen: EntityListScreenDefinition = {
|
|
|
35
36
|
{
|
|
36
37
|
kind: "navigate",
|
|
37
38
|
id: "edit",
|
|
38
|
-
label: "kumiko.actions.edit",
|
|
39
|
+
label: i18nKey("kumiko.actions.edit"),
|
|
39
40
|
screen: "tenant-edit",
|
|
40
41
|
entityId: "id",
|
|
41
42
|
},
|
|
@@ -81,25 +82,25 @@ export function createMembersScreen(options?: {
|
|
|
81
82
|
type: "projectionList",
|
|
82
83
|
query: TenantQueries.teamList,
|
|
83
84
|
columns: [
|
|
84
|
-
{ field: "email", label: "tenant.members.col.email" },
|
|
85
|
+
{ field: "email", label: i18nKey("tenant.members.col.email") },
|
|
85
86
|
{
|
|
86
87
|
field: "roles",
|
|
87
|
-
label: "tenant.members.col.roles",
|
|
88
|
+
label: i18nKey("tenant.members.col.roles"),
|
|
88
89
|
renderer: { react: { __component: MEMBER_ROLES_CELL_COMPONENT } },
|
|
89
90
|
},
|
|
90
91
|
{
|
|
91
92
|
field: "status",
|
|
92
|
-
label: "tenant.members.col.status",
|
|
93
|
+
label: i18nKey("tenant.members.col.status"),
|
|
93
94
|
renderer: { react: { __component: MEMBER_STATUS_CELL_COMPONENT } },
|
|
94
95
|
},
|
|
95
96
|
{
|
|
96
97
|
field: "createdAt",
|
|
97
|
-
label: "tenant.members.col.created",
|
|
98
|
+
label: i18nKey("tenant.members.col.created"),
|
|
98
99
|
renderer: { format: "timestamp" },
|
|
99
100
|
},
|
|
100
101
|
{
|
|
101
102
|
field: "lastSeenAt",
|
|
102
|
-
label: "tenant.members.col.lastActivity",
|
|
103
|
+
label: i18nKey("tenant.members.col.lastActivity"),
|
|
103
104
|
renderer: { format: "timestamp" },
|
|
104
105
|
},
|
|
105
106
|
],
|
|
@@ -109,10 +110,10 @@ export function createMembersScreen(options?: {
|
|
|
109
110
|
{
|
|
110
111
|
field: "status",
|
|
111
112
|
type: "select",
|
|
112
|
-
label: "tenant.members.filter.status",
|
|
113
|
+
label: i18nKey("tenant.members.filter.status"),
|
|
113
114
|
options: [
|
|
114
|
-
{ value: "active", label: "tenant.members.filter.status.option.active" },
|
|
115
|
-
{ value: "pending", label: "tenant.members.filter.status.option.pending" },
|
|
115
|
+
{ value: "active", label: i18nKey("tenant.members.filter.status.option.active") },
|
|
116
|
+
{ value: "pending", label: i18nKey("tenant.members.filter.status.option.pending") },
|
|
116
117
|
],
|
|
117
118
|
},
|
|
118
119
|
],
|
|
@@ -120,7 +121,7 @@ export function createMembersScreen(options?: {
|
|
|
120
121
|
{
|
|
121
122
|
kind: "navigate",
|
|
122
123
|
id: "edit-roles",
|
|
123
|
-
label: "tenant.members.actions.editRoles",
|
|
124
|
+
label: i18nKey("tenant.members.actions.editRoles"),
|
|
124
125
|
screen: MEMBER_ROLES_EDIT_SCREEN_ID,
|
|
125
126
|
params: { map: { userId: "userId", roles: "roles" } },
|
|
126
127
|
visible: { field: "status", eq: "active" },
|
|
@@ -128,10 +129,10 @@ export function createMembersScreen(options?: {
|
|
|
128
129
|
{
|
|
129
130
|
kind: "writeHandler",
|
|
130
131
|
id: "cancel-invitation",
|
|
131
|
-
label: "tenant.members.cancel",
|
|
132
|
+
label: i18nKey("tenant.members.cancel"),
|
|
132
133
|
handler: TenantHandlers.cancelInvitation,
|
|
133
134
|
payload: { map: { invitationId: "id" } },
|
|
134
|
-
confirm: "tenant.members.cancel.confirm",
|
|
135
|
+
confirm: i18nKey("tenant.members.cancel.confirm"),
|
|
135
136
|
style: "danger",
|
|
136
137
|
visible: { field: "status", eq: "pending" },
|
|
137
138
|
},
|
|
@@ -141,7 +142,7 @@ export function createMembersScreen(options?: {
|
|
|
141
142
|
{
|
|
142
143
|
kind: "drawer",
|
|
143
144
|
id: "invite",
|
|
144
|
-
label: "tenant.members.invite.title",
|
|
145
|
+
label: i18nKey("tenant.members.invite.title"),
|
|
145
146
|
screen: INVITE_CREATE_SCREEN_ID,
|
|
146
147
|
style: "primary",
|
|
147
148
|
},
|
|
@@ -168,7 +169,7 @@ export const inviteCreateScreen = {
|
|
|
168
169
|
layout: {
|
|
169
170
|
sections: [{ fields: ["email", "role"] }],
|
|
170
171
|
},
|
|
171
|
-
submitLabel: "tenant.members.invite.submit",
|
|
172
|
+
submitLabel: i18nKey("tenant.members.invite.submit"),
|
|
172
173
|
access: { roles: access.admin },
|
|
173
174
|
} satisfies ScreenDefinition;
|
|
174
175
|
|
|
@@ -190,6 +191,6 @@ export const memberRolesEditScreen = {
|
|
|
190
191
|
// Prefill userId from rowAction; readOnly so the operator cannot retarget.
|
|
191
192
|
sections: [{ fields: [{ field: "userId", readOnly: true }, "roles"] }],
|
|
192
193
|
},
|
|
193
|
-
submitLabel: "tenant.members.roles.edit.submit",
|
|
194
|
+
submitLabel: i18nKey("tenant.members.roles.edit.submit"),
|
|
194
195
|
access: { roles: access.admin },
|
|
195
196
|
} satisfies ScreenDefinition;
|
package/src/user/screens.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
EntityEditScreenDefinition,
|
|
3
|
-
EntityListScreenDefinition,
|
|
1
|
+
import {
|
|
2
|
+
type EntityEditScreenDefinition,
|
|
3
|
+
type EntityListScreenDefinition,
|
|
4
|
+
i18nKey,
|
|
4
5
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
5
6
|
|
|
6
7
|
// Cross-tenant platform admin view of the user identity. Because the user
|
|
@@ -21,7 +22,7 @@ export const userListScreen: EntityListScreenDefinition = {
|
|
|
21
22
|
{
|
|
22
23
|
kind: "navigate",
|
|
23
24
|
id: "edit",
|
|
24
|
-
label: "kumiko.actions.edit",
|
|
25
|
+
label: i18nKey("kumiko.actions.edit"),
|
|
25
26
|
screen: "user-edit",
|
|
26
27
|
entityId: "id",
|
|
27
28
|
},
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
defineFeature,
|
|
5
5
|
EXT_USER_DATA,
|
|
6
6
|
type FeatureDefinition,
|
|
7
|
+
i18nKey,
|
|
7
8
|
SYSTEM_USER_ID,
|
|
8
9
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
9
10
|
import { validateGdprHookCompleteness, validateGdprPiiHookCoverage } from "./boot-checks";
|
|
@@ -300,6 +301,27 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
|
|
|
300
301
|
});
|
|
301
302
|
|
|
302
303
|
r.translations({ keys: USER_DATA_RIGHTS_I18N });
|
|
304
|
+
// Privacy-center dot-form labels (i18nKey()-marked below), duplicated in
|
|
305
|
+
// web/i18n.ts's client bundle since privacy-center-screen.tsx renders
|
|
306
|
+
// them client-side.
|
|
307
|
+
r.translations({
|
|
308
|
+
keys: {
|
|
309
|
+
"userDataRights.privacyCenter.field.status": { en: "Account status" },
|
|
310
|
+
"userDataRights.privacyCenter.field.gracePeriodEnd": { en: "Scheduled deletion date" },
|
|
311
|
+
"userDataRights.privacyCenter.export.title": { en: "Export your data (Art. 20)" },
|
|
312
|
+
"userDataRights.privacyCenter.restriction.title": { en: "Restrict processing (Art. 18)" },
|
|
313
|
+
"userDataRights.privacyCenter.restriction.restrict": { en: "Restrict account" },
|
|
314
|
+
"userDataRights.privacyCenter.restriction.dialogDescription": {
|
|
315
|
+
en: "You will be signed out immediately and cannot sign in again until support lifts the restriction.",
|
|
316
|
+
},
|
|
317
|
+
"userDataRights.privacyCenter.deletion.title": { en: "Delete account (Art. 17)" },
|
|
318
|
+
"userDataRights.privacyCenter.deletion.delete": { en: "Delete account" },
|
|
319
|
+
"userDataRights.privacyCenter.deletion.cancel": { en: "Cancel deletion" },
|
|
320
|
+
"userDataRights.privacyCenter.deletion.dialogDescription": {
|
|
321
|
+
en: "Confirming starts the deletion grace period. You can cancel the deletion until it ends.",
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
});
|
|
303
325
|
|
|
304
326
|
// Self-service screen (Art. 15/17/18/20): export, restriction, deletion
|
|
305
327
|
// in one projectionDetail screen bound to the user's own `me` row. No
|
|
@@ -326,14 +348,14 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
|
|
|
326
348
|
description:
|
|
327
349
|
"Logged-in GDPR self-service page where a user requests and downloads a data export (Art. 20), restricts processing of their account (Art. 18) and requests its deletion (Art. 17).",
|
|
328
350
|
fieldLabels: {
|
|
329
|
-
status: "userDataRights.privacyCenter.field.status",
|
|
330
|
-
gracePeriodEnd: "userDataRights.privacyCenter.field.gracePeriodEnd",
|
|
351
|
+
status: i18nKey("userDataRights.privacyCenter.field.status"),
|
|
352
|
+
gracePeriodEnd: i18nKey("userDataRights.privacyCenter.field.gracePeriodEnd"),
|
|
331
353
|
},
|
|
332
354
|
layout: {
|
|
333
355
|
sections: [
|
|
334
356
|
{
|
|
335
357
|
kind: "extension",
|
|
336
|
-
title: "userDataRights.privacyCenter.export.title",
|
|
358
|
+
title: i18nKey("userDataRights.privacyCenter.export.title"),
|
|
337
359
|
component: { react: { __component: EXPORT_SECTION_EXTENSION_NAME } },
|
|
338
360
|
entityName: "export-job",
|
|
339
361
|
},
|
|
@@ -342,7 +364,7 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
|
|
|
342
364
|
// section always on the screen — Deletion is dropped entirely
|
|
343
365
|
// when `showDeletion` is false, and status covers restriction
|
|
344
366
|
// AND deletion state, so it can't hang off the optional section.
|
|
345
|
-
title: "userDataRights.privacyCenter.restriction.title",
|
|
367
|
+
title: i18nKey("userDataRights.privacyCenter.restriction.title"),
|
|
346
368
|
description: "userDataRights.privacyCenter.restriction.explainer",
|
|
347
369
|
fields: [
|
|
348
370
|
{
|
|
@@ -354,7 +376,7 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
|
|
|
354
376
|
...(showDeletion
|
|
355
377
|
? [
|
|
356
378
|
{
|
|
357
|
-
title: "userDataRights.privacyCenter.deletion.title",
|
|
379
|
+
title: i18nKey("userDataRights.privacyCenter.deletion.title"),
|
|
358
380
|
description: "userDataRights.privacyCenter.deletion.explainer",
|
|
359
381
|
// gracePeriodEnd is only meaningful once a deletion is
|
|
360
382
|
// actually pending — hide it instead of showing an empty
|
|
@@ -374,9 +396,9 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
|
|
|
374
396
|
actions: [
|
|
375
397
|
{
|
|
376
398
|
id: "restrict",
|
|
377
|
-
label: "userDataRights.privacyCenter.restriction.restrict",
|
|
399
|
+
label: i18nKey("userDataRights.privacyCenter.restriction.restrict"),
|
|
378
400
|
handler: UserDataRightsHandlers.restrictAccount,
|
|
379
|
-
confirm: "userDataRights.privacyCenter.restriction.dialogDescription",
|
|
401
|
+
confirm: i18nKey("userDataRights.privacyCenter.restriction.dialogDescription"),
|
|
380
402
|
visible: { field: "status", ne: "restricted" },
|
|
381
403
|
style: "danger",
|
|
382
404
|
},
|
|
@@ -384,15 +406,15 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
|
|
|
384
406
|
? [
|
|
385
407
|
{
|
|
386
408
|
id: "request-deletion",
|
|
387
|
-
label: "userDataRights.privacyCenter.deletion.delete",
|
|
409
|
+
label: i18nKey("userDataRights.privacyCenter.deletion.delete"),
|
|
388
410
|
handler: UserDataRightsHandlers.requestDeletion,
|
|
389
|
-
confirm: "userDataRights.privacyCenter.deletion.dialogDescription",
|
|
411
|
+
confirm: i18nKey("userDataRights.privacyCenter.deletion.dialogDescription"),
|
|
390
412
|
visible: { field: "status", ne: "deletionRequested" },
|
|
391
413
|
style: "danger" as const,
|
|
392
414
|
},
|
|
393
415
|
{
|
|
394
416
|
id: "cancel-deletion",
|
|
395
|
-
label: "userDataRights.privacyCenter.deletion.cancel",
|
|
417
|
+
label: i18nKey("userDataRights.privacyCenter.deletion.cancel"),
|
|
396
418
|
handler: UserDataRightsHandlers.cancelDeletion,
|
|
397
419
|
visible: { field: "status", eq: "deletionRequested" },
|
|
398
420
|
style: "secondary" as const,
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
access,
|
|
3
3
|
type EntityEditScreenDefinition,
|
|
4
4
|
type EntityListScreenDefinition,
|
|
5
|
+
i18nKey,
|
|
5
6
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
6
7
|
|
|
7
8
|
// Read-only operator inspector for the GDPR data-rights read-models. All
|
|
@@ -23,7 +24,7 @@ export const exportJobListScreen: EntityListScreenDefinition = {
|
|
|
23
24
|
{
|
|
24
25
|
kind: "navigate",
|
|
25
26
|
id: "view",
|
|
26
|
-
label: "kumiko.actions.view",
|
|
27
|
+
label: i18nKey("kumiko.actions.view"),
|
|
27
28
|
screen: "export-job-detail",
|
|
28
29
|
entityId: "id",
|
|
29
30
|
},
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
defineFeature,
|
|
3
|
+
type FeatureDefinition,
|
|
4
|
+
i18nKey,
|
|
5
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
2
6
|
import {
|
|
3
7
|
CHANGE_EMAIL_SECTION_EXTENSION_NAME,
|
|
4
8
|
CHANGE_PASSWORD_SECTION_EXTENSION_NAME,
|
|
@@ -38,12 +42,26 @@ export function createUserProfileFeature(): FeatureDefinition {
|
|
|
38
42
|
changeEmail: r.writeHandler(changeEmailWrite),
|
|
39
43
|
};
|
|
40
44
|
|
|
41
|
-
// Boot-validator requires a "screen:<id>.title" key per r.screen()
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// jsx tsconfig (i18n.ts
|
|
45
|
-
//
|
|
46
|
-
|
|
45
|
+
// Boot-validator requires a "screen:<id>.title" key per r.screen(), plus
|
|
46
|
+
// one entry per i18nKey()-marked dot-form label below — kept inline (not
|
|
47
|
+
// imported from ./i18n) so feature.ts, part of the server barrel, stays
|
|
48
|
+
// importable from a server-only sample without a jsx tsconfig (i18n.ts
|
|
49
|
+
// pulls @cosmicdrift/kumiko-renderer's types, see index.ts). Values are
|
|
50
|
+
// duplicated in i18n.ts's client bundle.
|
|
51
|
+
r.translations({
|
|
52
|
+
keys: {
|
|
53
|
+
"screen:profile.title": { en: "Profile" },
|
|
54
|
+
"profile.email.title": { en: "Email address" },
|
|
55
|
+
"profile.password.title": { en: "Password" },
|
|
56
|
+
"profile.danger.title": { en: "Delete account" },
|
|
57
|
+
"profile.danger.gracePeriodEnd": { en: "Deletion date" },
|
|
58
|
+
"profile.danger.delete": { en: "Delete account" },
|
|
59
|
+
"profile.danger.dialogDescription": {
|
|
60
|
+
en: "After the grace period your data will be permanently deleted. Until then you can cancel.",
|
|
61
|
+
},
|
|
62
|
+
"profile.danger.cancelDeletion": { en: "Cancel deletion" },
|
|
63
|
+
},
|
|
64
|
+
});
|
|
47
65
|
|
|
48
66
|
// Self-service account screen: change-email/change-password stay
|
|
49
67
|
// self-persisting extension sections (re-auth, own dispatcher writes —
|
|
@@ -65,24 +83,24 @@ export function createUserProfileFeature(): FeatureDefinition {
|
|
|
65
83
|
"a verification-mail follow-up), and request or cancel account deletion " +
|
|
66
84
|
"(user-data-rights grace period).",
|
|
67
85
|
fieldLabels: {
|
|
68
|
-
gracePeriodEnd: "profile.danger.gracePeriodEnd",
|
|
86
|
+
gracePeriodEnd: i18nKey("profile.danger.gracePeriodEnd"),
|
|
69
87
|
},
|
|
70
88
|
layout: {
|
|
71
89
|
sections: [
|
|
72
90
|
{
|
|
73
91
|
kind: "extension",
|
|
74
|
-
title: "profile.email.title",
|
|
92
|
+
title: i18nKey("profile.email.title"),
|
|
75
93
|
component: { react: { __component: CHANGE_EMAIL_SECTION_EXTENSION_NAME } },
|
|
76
94
|
entityName: "user",
|
|
77
95
|
},
|
|
78
96
|
{
|
|
79
97
|
kind: "extension",
|
|
80
|
-
title: "profile.password.title",
|
|
98
|
+
title: i18nKey("profile.password.title"),
|
|
81
99
|
component: { react: { __component: CHANGE_PASSWORD_SECTION_EXTENSION_NAME } },
|
|
82
100
|
entityName: "user",
|
|
83
101
|
},
|
|
84
102
|
{
|
|
85
|
-
title: "profile.danger.title",
|
|
103
|
+
title: i18nKey("profile.danger.title"),
|
|
86
104
|
description: "profile.danger.explainer",
|
|
87
105
|
// gracePeriodEnd is only meaningful once a deletion is actually
|
|
88
106
|
// pending — hide it instead of showing an empty date when status
|
|
@@ -100,15 +118,15 @@ export function createUserProfileFeature(): FeatureDefinition {
|
|
|
100
118
|
actions: [
|
|
101
119
|
{
|
|
102
120
|
id: "request-deletion",
|
|
103
|
-
label: "profile.danger.delete",
|
|
121
|
+
label: i18nKey("profile.danger.delete"),
|
|
104
122
|
handler: UserDataRightsHandlers.requestDeletion,
|
|
105
|
-
confirm: "profile.danger.dialogDescription",
|
|
123
|
+
confirm: i18nKey("profile.danger.dialogDescription"),
|
|
106
124
|
visible: { field: "status", ne: "deletionRequested" },
|
|
107
125
|
style: "danger",
|
|
108
126
|
},
|
|
109
127
|
{
|
|
110
128
|
id: "cancel-deletion",
|
|
111
|
-
label: "profile.danger.cancelDeletion",
|
|
129
|
+
label: i18nKey("profile.danger.cancelDeletion"),
|
|
112
130
|
handler: UserDataRightsHandlers.cancelDeletion,
|
|
113
131
|
visible: { field: "status", eq: "deletionRequested" },
|
|
114
132
|
style: "secondary",
|