@cosmicdrift/kumiko-bundled-features 0.255.2 → 0.257.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/ledger/__tests__/ledger.integration.test.ts +111 -4
- package/src/ledger/entity.ts +30 -0
- package/src/ledger/handlers/confirm-schedule-period.write.ts +7 -0
- package/src/ledger/handlers/create-transaction.write.ts +2 -0
- package/src/ledger/schemas.ts +3 -0
- package/src/sessions/screens.ts +14 -13
- package/src/tags/screens.ts +8 -7
- 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.257.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.257.0",
|
|
134
|
+
"@cosmicdrift/kumiko-framework": "0.257.0",
|
|
135
|
+
"@cosmicdrift/kumiko-headless": "0.257.0",
|
|
136
|
+
"@cosmicdrift/kumiko-renderer": "0.257.0",
|
|
137
|
+
"@cosmicdrift/kumiko-renderer-web": "0.257.0",
|
|
138
|
+
"@cosmicdrift/kumiko-types": "0.257.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.257.0",
|
|
168
|
+
"@cosmicdrift/kumiko-locale-es": "0.257.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" },
|
|
@@ -64,7 +64,12 @@ async function createAccount(name: string, type: AccountType, user = admin): Pro
|
|
|
64
64
|
|
|
65
65
|
async function createTransaction(
|
|
66
66
|
lines: readonly Posting[],
|
|
67
|
-
opts: {
|
|
67
|
+
opts: {
|
|
68
|
+
date?: string;
|
|
69
|
+
description?: string;
|
|
70
|
+
subjectType?: string;
|
|
71
|
+
subjectId?: string;
|
|
72
|
+
} = {},
|
|
68
73
|
user = admin,
|
|
69
74
|
): Promise<{ id: string }> {
|
|
70
75
|
return stack.http.writeOk<{ id: string }>(
|
|
@@ -73,15 +78,20 @@ async function createTransaction(
|
|
|
73
78
|
date: opts.date ?? "2026-01-15",
|
|
74
79
|
description: opts.description ?? "Test entry",
|
|
75
80
|
lines,
|
|
81
|
+
...(opts.subjectType !== undefined && { subjectType: opts.subjectType }),
|
|
82
|
+
...(opts.subjectId !== undefined && { subjectId: opts.subjectId }),
|
|
76
83
|
},
|
|
77
84
|
user,
|
|
78
85
|
);
|
|
79
86
|
}
|
|
80
87
|
|
|
81
|
-
async function listTransactions(
|
|
88
|
+
async function listTransactions(
|
|
89
|
+
user = admin,
|
|
90
|
+
query: Record<string, unknown> = {},
|
|
91
|
+
): Promise<Array<Record<string, unknown>>> {
|
|
82
92
|
const res = await stack.http.queryOk<{ rows: Array<Record<string, unknown>> }>(
|
|
83
93
|
LedgerQueries.transactionList,
|
|
84
|
-
|
|
94
|
+
query,
|
|
85
95
|
user,
|
|
86
96
|
);
|
|
87
97
|
return res.rows;
|
|
@@ -350,7 +360,13 @@ describe("ledger integration — confirm-schedule-period (recurring)", () => {
|
|
|
350
360
|
async function createSchedule(
|
|
351
361
|
debitAccountId: string,
|
|
352
362
|
creditAccountId: string,
|
|
353
|
-
over: {
|
|
363
|
+
over: {
|
|
364
|
+
amount?: number;
|
|
365
|
+
description?: string;
|
|
366
|
+
startDate?: string;
|
|
367
|
+
subjectType?: string;
|
|
368
|
+
subjectId?: string;
|
|
369
|
+
} = {},
|
|
354
370
|
): Promise<string> {
|
|
355
371
|
const s = await stack.http.writeOk<{ id: string }>(
|
|
356
372
|
LedgerHandlers.createSchedule,
|
|
@@ -361,6 +377,8 @@ describe("ledger integration — confirm-schedule-period (recurring)", () => {
|
|
|
361
377
|
amount: over.amount ?? 50000,
|
|
362
378
|
debitAccountId,
|
|
363
379
|
creditAccountId,
|
|
380
|
+
...(over.subjectType !== undefined && { subjectType: over.subjectType }),
|
|
381
|
+
...(over.subjectId !== undefined && { subjectId: over.subjectId }),
|
|
364
382
|
},
|
|
365
383
|
admin,
|
|
366
384
|
);
|
|
@@ -457,4 +475,93 @@ describe("ledger integration — confirm-schedule-period (recurring)", () => {
|
|
|
457
475
|
// Books net to zero (original cancels Storno) plus the fresh 48000 confirmation.
|
|
458
476
|
expect(trialBalance(rows)).toBe(0);
|
|
459
477
|
});
|
|
478
|
+
|
|
479
|
+
test("confirming a period on a schedule with a subject stamps the same subject onto the transaction", async () => {
|
|
480
|
+
const bank = await createAccount("Bank", "asset");
|
|
481
|
+
const rent = await createAccount("Mieterträge", "income");
|
|
482
|
+
const scheduleId = await createSchedule(bank, rent, {
|
|
483
|
+
subjectType: "lease",
|
|
484
|
+
subjectId: "lease-1",
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
await confirm(scheduleId, "2026-01");
|
|
488
|
+
|
|
489
|
+
const rows = await listTransactions();
|
|
490
|
+
expect(rows[0]?.["subjectType"]).toBe("lease");
|
|
491
|
+
expect(rows[0]?.["subjectId"]).toBe("lease-1");
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("confirming a period on a schedule without a subject leaves the transaction without one", async () => {
|
|
495
|
+
const bank = await createAccount("Bank", "asset");
|
|
496
|
+
const rent = await createAccount("Mieterträge", "income");
|
|
497
|
+
const scheduleId = await createSchedule(bank, rent);
|
|
498
|
+
|
|
499
|
+
await confirm(scheduleId, "2026-01");
|
|
500
|
+
|
|
501
|
+
const rows = await listTransactions();
|
|
502
|
+
expect(rows[0]?.["subjectType"]).toBeNull();
|
|
503
|
+
expect(rows[0]?.["subjectId"]).toBeNull();
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
describe("ledger integration — subject dimension (filterable business-object reference)", () => {
|
|
508
|
+
test("a booking with subjectType/subjectId round-trips and is findable via an eq filter", async () => {
|
|
509
|
+
const bank = await createAccount("Bank", "asset");
|
|
510
|
+
const rent = await createAccount("Mieterträge", "income");
|
|
511
|
+
|
|
512
|
+
const tx = await createTransaction(
|
|
513
|
+
[
|
|
514
|
+
{ accountId: bank, amount: 100000 },
|
|
515
|
+
{ accountId: rent, amount: -100000 },
|
|
516
|
+
],
|
|
517
|
+
{ description: "Miete WE1", subjectType: "lease", subjectId: "lease-1" },
|
|
518
|
+
);
|
|
519
|
+
await createTransaction(
|
|
520
|
+
[
|
|
521
|
+
{ accountId: bank, amount: 60000 },
|
|
522
|
+
{ accountId: rent, amount: -60000 },
|
|
523
|
+
],
|
|
524
|
+
{ description: "Miete WE2", subjectType: "lease", subjectId: "lease-2" },
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
const rows = await listTransactions(admin, {
|
|
528
|
+
filter: { field: "subjectId", op: "eq", value: "lease-1" },
|
|
529
|
+
});
|
|
530
|
+
expect(rows).toHaveLength(1);
|
|
531
|
+
expect(rows[0]?.["id"]).toBe(tx.id);
|
|
532
|
+
expect(rows[0]?.["subjectType"]).toBe("lease");
|
|
533
|
+
expect(rows[0]?.["subjectId"]).toBe("lease-1");
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test("an in-filter over multiple subjectId values returns exactly the matching bookings", async () => {
|
|
537
|
+
const bank = await createAccount("Bank", "asset");
|
|
538
|
+
const rent = await createAccount("Mieterträge", "income");
|
|
539
|
+
|
|
540
|
+
await createTransaction(
|
|
541
|
+
[
|
|
542
|
+
{ accountId: bank, amount: 10000 },
|
|
543
|
+
{ accountId: rent, amount: -10000 },
|
|
544
|
+
],
|
|
545
|
+
{ subjectType: "lease", subjectId: "lease-1" },
|
|
546
|
+
);
|
|
547
|
+
await createTransaction(
|
|
548
|
+
[
|
|
549
|
+
{ accountId: bank, amount: 20000 },
|
|
550
|
+
{ accountId: rent, amount: -20000 },
|
|
551
|
+
],
|
|
552
|
+
{ subjectType: "lease", subjectId: "lease-2" },
|
|
553
|
+
);
|
|
554
|
+
await createTransaction(
|
|
555
|
+
[
|
|
556
|
+
{ accountId: bank, amount: 30000 },
|
|
557
|
+
{ accountId: rent, amount: -30000 },
|
|
558
|
+
],
|
|
559
|
+
{ subjectType: "lease", subjectId: "lease-3" },
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
const rows = await listTransactions(admin, {
|
|
563
|
+
filters: [{ field: "subjectId", op: "in", value: ["lease-1", "lease-3"] }],
|
|
564
|
+
});
|
|
565
|
+
expect(rows.map((r) => r["subjectId"]).sort()).toEqual(["lease-1", "lease-3"]);
|
|
566
|
+
});
|
|
460
567
|
});
|
package/src/ledger/entity.ts
CHANGED
|
@@ -78,7 +78,22 @@ export const transactionEntity = createEntity({
|
|
|
78
78
|
},
|
|
79
79
|
{ required: true },
|
|
80
80
|
),
|
|
81
|
+
// Optional business-object reference (e.g. a lease contract) so entries can
|
|
82
|
+
// be filtered by what they're about, not just grepped out of `reference`.
|
|
83
|
+
subjectType: createTextField({
|
|
84
|
+
maxLength: 64,
|
|
85
|
+
personal: false,
|
|
86
|
+
reason: "technical_reference",
|
|
87
|
+
filterable: true,
|
|
88
|
+
}),
|
|
89
|
+
subjectId: createTextField({
|
|
90
|
+
maxLength: 128,
|
|
91
|
+
personal: false,
|
|
92
|
+
reason: "technical_reference",
|
|
93
|
+
filterable: true,
|
|
94
|
+
}),
|
|
81
95
|
},
|
|
96
|
+
indexes: [{ columns: ["tenantId", "subjectType", "subjectId"] }],
|
|
82
97
|
});
|
|
83
98
|
|
|
84
99
|
// schedule — a recurring booking template (Dauerauftrag): "book `amount` from
|
|
@@ -116,5 +131,20 @@ export const scheduleEntity = createEntity({
|
|
|
116
131
|
personal: false,
|
|
117
132
|
reason: "technical_reference",
|
|
118
133
|
}),
|
|
134
|
+
// Confirmed periods inherit this from the schedule (see
|
|
135
|
+
// confirm-schedule-period.write.ts) rather than repeating it per period.
|
|
136
|
+
subjectType: createTextField({
|
|
137
|
+
maxLength: 64,
|
|
138
|
+
personal: false,
|
|
139
|
+
reason: "technical_reference",
|
|
140
|
+
filterable: true,
|
|
141
|
+
}),
|
|
142
|
+
subjectId: createTextField({
|
|
143
|
+
maxLength: 128,
|
|
144
|
+
personal: false,
|
|
145
|
+
reason: "technical_reference",
|
|
146
|
+
filterable: true,
|
|
147
|
+
}),
|
|
119
148
|
},
|
|
149
|
+
indexes: [{ columns: ["tenantId", "subjectType", "subjectId"] }],
|
|
120
150
|
});
|
|
@@ -96,6 +96,11 @@ export function createConfirmSchedulePeriodHandler(
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
const amount = payload.amount ?? Number(schedule["amount"]);
|
|
99
|
+
// The subject is a property of the schedule, not the confirm call — a
|
|
100
|
+
// period without one stays without one, no backfill/error.
|
|
101
|
+
const subjectType =
|
|
102
|
+
typeof schedule["subjectType"] === "string" ? schedule["subjectType"] : null;
|
|
103
|
+
const subjectId = typeof schedule["subjectId"] === "string" ? schedule["subjectId"] : null;
|
|
99
104
|
const created = await transactionExecutor.create(
|
|
100
105
|
{
|
|
101
106
|
id: generateId(),
|
|
@@ -107,6 +112,8 @@ export function createConfirmSchedulePeriodHandler(
|
|
|
107
112
|
{ accountId: debitAccountId, amount },
|
|
108
113
|
{ accountId: creditAccountId, amount: -amount },
|
|
109
114
|
],
|
|
115
|
+
subjectType,
|
|
116
|
+
subjectId,
|
|
110
117
|
},
|
|
111
118
|
event.user,
|
|
112
119
|
ctx.db,
|
|
@@ -35,6 +35,8 @@ export function createCreateTransactionHandler(
|
|
|
35
35
|
reference: payload.reference ?? null,
|
|
36
36
|
status: payload.status ?? "posted",
|
|
37
37
|
lines: payload.lines,
|
|
38
|
+
subjectType: payload.subjectType ?? null,
|
|
39
|
+
subjectId: payload.subjectId ?? null,
|
|
38
40
|
},
|
|
39
41
|
event.user,
|
|
40
42
|
ctx.db,
|
package/src/ledger/schemas.ts
CHANGED
|
@@ -27,6 +27,9 @@ export const createTransactionPayloadSchema = z
|
|
|
27
27
|
// the Soll/recurring work — Phase 0 only ever creates posted entries.
|
|
28
28
|
status: z.enum(TRANSACTION_STATUS).optional(),
|
|
29
29
|
lines: z.array(postingSchema).min(2),
|
|
30
|
+
// Business-object reference (e.g. a lease contract) this entry is about.
|
|
31
|
+
subjectType: z.string().max(64).optional(),
|
|
32
|
+
subjectId: z.string().max(128).optional(),
|
|
30
33
|
})
|
|
31
34
|
.refine((p) => sumIsZero(p.lines), {
|
|
32
35
|
message: "Transaction must balance: Σ of posting amounts must equal 0",
|
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
|
],
|
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",
|