@cosmicdrift/kumiko-samples 0.355.0 → 0.357.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.
Files changed (53) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +1 -1
  3. package/packages/bundled-features/src/auth-mfa/changes.json +6 -0
  4. package/packages/bundled-features/src/auth-mfa/i18n.ts +1 -0
  5. package/packages/bundled-features/src/auth-mfa/screens.ts +8 -1
  6. package/packages/bundled-features/src/cap-counter/feature.ts +7 -1
  7. package/packages/bundled-features/src/cap-overview/screens.ts +3 -1
  8. package/packages/bundled-features/src/compliance-profiles/handlers/set-profile.write.ts +18 -24
  9. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  10. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +16 -1
  11. package/packages/bundled-features/src/custom-fields/handlers/define-system-field.write.ts +10 -17
  12. package/packages/bundled-features/src/custom-fields/handlers/delete-system-field.write.ts +5 -2
  13. package/packages/bundled-features/src/custom-fields/lib/define-or-resurrect.ts +12 -3
  14. package/packages/bundled-features/src/delivery/changes.json +6 -0
  15. package/packages/bundled-features/src/delivery/chat-webhook-sender.ts +1 -1
  16. package/packages/bundled-features/src/delivery/constants.ts +5 -0
  17. package/packages/bundled-features/src/delivery/delivery-service.ts +118 -50
  18. package/packages/bundled-features/src/delivery/feature.ts +7 -1
  19. package/packages/bundled-features/src/delivery/jobs.ts +27 -1
  20. package/packages/bundled-features/src/delivery/testing.ts +1 -0
  21. package/packages/bundled-features/src/managed-pages/handlers/set.write.ts +8 -25
  22. package/packages/bundled-features/src/notes-history/entity.ts +7 -6
  23. package/packages/bundled-features/src/notes-history-user-data/hooks.ts +109 -62
  24. package/packages/bundled-features/src/notes-history-user-data/index.ts +3 -1
  25. package/packages/bundled-features/src/personal-access-tokens/screens.ts +2 -0
  26. package/packages/bundled-features/src/sessions/screens.ts +2 -0
  27. package/packages/bundled-features/src/shared/index.ts +1 -0
  28. package/packages/bundled-features/src/shared/require-for-tenant.ts +18 -0
  29. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +6 -4
  30. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +7 -7
  31. package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +7 -6
  32. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +7 -4
  33. package/packages/bundled-features/src/tenant/cancel-pending-invitation.ts +3 -1
  34. package/packages/bundled-features/src/tenant/changes.json +6 -0
  35. package/packages/bundled-features/src/tenant/constants.ts +6 -3
  36. package/packages/bundled-features/src/tenant/feature.ts +2 -0
  37. package/packages/bundled-features/src/tenant/handlers/member-role-availability.query.ts +25 -0
  38. package/packages/bundled-features/src/tenant/handlers/remove-member.write.ts +22 -8
  39. package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +19 -11
  40. package/packages/bundled-features/src/tenant/member-role-options.ts +6 -0
  41. package/packages/bundled-features/src/tenant/screens.ts +9 -6
  42. package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +9 -23
  43. package/packages/bundled-features/src/user/screens.ts +8 -1
  44. package/packages/bundled-features/src/user-data-rights/screens.ts +8 -1
  45. package/packages/bundled-features/src/waitlist/screens.ts +8 -1
  46. package/samples/apps/marketing-demo/src/features/assets/schema/asset.ts +4 -4
  47. package/samples/apps/marketing-demo/src/features/helpdesk/schema/ticket.ts +4 -4
  48. package/samples/apps/showcase/src/features/items/schema/item.ts +1 -1
  49. package/samples/apps/styleguide/src/features/demo/feature.ts +3 -3
  50. package/samples/apps/styleguide/src/features/gallery/web/Gallery.tsx +1 -1
  51. package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +6 -2
  52. package/samples/apps/ui-walkthrough/src/features/vehicles/screens.ts +9 -2
  53. package/samples/apps/use-all-bundled/feature-manifest.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { createEventStoreExecutor, type TenantDb } from "@cosmicdrift/kumiko-framework/db";
2
- import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import type { WriteFailure } from "@cosmicdrift/kumiko-framework/errors";
4
4
  import type { Redis } from "ioredis";
5
5
  // kumiko-lint-ignore cross-feature-import cancel needs invite-token-store for Redis cleanup
@@ -23,6 +23,7 @@ export async function cancelPendingInvitation(
23
23
  invitation: { readonly id: string; readonly version: number },
24
24
  actor: SessionUser,
25
25
  redis: Redis | undefined,
26
+ streamTenantId?: TenantId,
26
27
  ): Promise<WriteFailure | undefined> {
27
28
  const updateResult = await invitationExecutor.update(
28
29
  {
@@ -32,6 +33,7 @@ export async function cancelPendingInvitation(
32
33
  },
33
34
  actor,
34
35
  db,
36
+ { streamTenantId },
35
37
  );
36
38
  if (!updateResult.isSuccess) return updateResult;
37
39
 
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.357.0",
4
+ "type": "improvement",
5
+ "title": "The member roles form only offers roles the signed-in admin may assign",
6
+ "detail": "New query tenant:query:member-role-availability; TenantAdmin still sees every role."
7
+ },
2
8
  {
3
9
  "version": "0.354.0",
4
10
  "type": "breaking",
@@ -26,9 +26,10 @@ export const MEMBER_ROLES_CELL_COMPONENT = "MemberRolesCell" as const;
26
26
  // Admin (rank 2) shares the invite-create screen with TenantAdmin — TenantAdmin
27
27
  // must not appear here or Admin sees an unassignable option (fw#2414).
28
28
  export const DEFAULT_INVITE_ROLE_OPTIONS = ["User", "Editor", "Admin"] as const;
29
- // Full assignable rank list, used by memberRolesEditScreen: that screen is
30
- // prefilled with a member's *current* roles, so it must be able to represent
31
- // every rank a membership can actually hold, including TenantAdmin (fw#2452).
29
+ // Full rank list for memberRolesEditScreen: it is prefilled with a member's
30
+ // *current* roles, so it must represent every rank a membership can hold,
31
+ // including TenantAdmin (fw#2452). The screen hides what the caller may not
32
+ // assign via TenantQueries.memberRoleAvailability.
32
33
  export const OWNER_INVITE_ROLE_OPTIONS = [
33
34
  "User",
34
35
  "Member",
@@ -67,6 +68,8 @@ export const TenantQueries = {
67
68
  invitations: "tenant:query:invitations",
68
69
  // Combined active-members + pending-invitations list backing /members.
69
70
  teamList: "tenant:query:team:list",
71
+ // Per-role assignability for the caller, mirrors the rank rule of updateMemberRoles.
72
+ memberRoleAvailability: "tenant:query:member-role-availability",
70
73
  } as const;
71
74
 
72
75
  // Error codes
@@ -20,6 +20,7 @@ import { invitationsQuery } from "./handlers/invitations.query.js";
20
20
  import { listQuery } from "./handlers/list.query.js";
21
21
  import { meQuery } from "./handlers/me.query.js";
22
22
  import { memberDirectoryQuery } from "./handlers/member-directory.query.js";
23
+ import { memberRoleAvailabilityQuery } from "./handlers/member-role-availability.query.js";
23
24
  import { membersQuery } from "./handlers/members.query.js";
24
25
  import { membershipsQuery } from "./handlers/memberships.query.js";
25
26
  import { removeMemberWrite } from "./handlers/remove-member.write.js";
@@ -166,6 +167,7 @@ export function createTenantFeature(options?: TenantFeatureOptions): FeatureDefi
166
167
  resolveUserIds: r.queryHandler(resolveUserIdsQuery),
167
168
  invitations: r.queryHandler(invitationsQuery),
168
169
  teamList: r.queryHandler(teamListQuery),
170
+ memberRoleAvailability: r.queryHandler(memberRoleAvailabilityQuery),
169
171
  };
170
172
 
171
173
  // Entity-convention handlers for the SystemAdmin entityList/entityEdit
@@ -0,0 +1,25 @@
1
+ import {
2
+ access,
3
+ assignableAppRolesOf,
4
+ canActorAssignRole,
5
+ defineQueryHandler,
6
+ } from "@cosmicdrift/kumiko-framework/engine";
7
+ import * as z from "zod";
8
+ import { memberRoleOptions } from "../member-role-options.js";
9
+
10
+ export const memberRoleAvailabilityQuery = defineQueryHandler({
11
+ name: "member-role-availability",
12
+ schema: z.object({}),
13
+ access: { roles: access.admin },
14
+ description:
15
+ "Lists every role the member roles form offers and whether the caller's own rank may assign it; the form hides the roles marked disabled.",
16
+ handler: async (query, ctx) => {
17
+ const assignableAppRoles = assignableAppRolesOf(ctx.registry);
18
+ return {
19
+ rows: memberRoleOptions(assignableAppRoles).map((value) => ({
20
+ value,
21
+ disabled: !canActorAssignRole(query.user.roles, value, assignableAppRoles),
22
+ })),
23
+ };
24
+ },
25
+ });
@@ -7,7 +7,9 @@ import {
7
7
  import {
8
8
  createSystemUser,
9
9
  defineWriteHandler,
10
+ parseTenantId,
10
11
  type SessionUser,
12
+ type TenantId,
11
13
  withResponseData,
12
14
  } from "@cosmicdrift/kumiko-framework/engine";
13
15
  import {
@@ -34,6 +36,7 @@ type PendingInvitationCancel = {
34
36
  readonly userId: string;
35
37
  readonly tenantId: string;
36
38
  readonly actor: SessionUser;
39
+ readonly streamTenantId: TenantId;
37
40
  readonly redis: Redis | undefined;
38
41
  };
39
42
 
@@ -51,7 +54,13 @@ async function cancelPendingInvitationsOfUser(
51
54
  { tenantId: options.tenantId, email, status: INVITATION_STATUS.pending },
52
55
  );
53
56
  for (const invitation of pendingInvitations) {
54
- const failure = await cancelPendingInvitation(db, invitation, options.actor, options.redis);
57
+ const failure = await cancelPendingInvitation(
58
+ db,
59
+ invitation,
60
+ options.actor,
61
+ options.redis,
62
+ options.streamTenantId,
63
+ );
55
64
  if (failure) return failure;
56
65
  }
57
66
  return undefined;
@@ -79,11 +88,15 @@ export const removeMemberWrite = defineWriteHandler({
79
88
  const db = ctx.systemDb.acknowledgeCrossTenant(
80
89
  "SystemAdmin manages memberships across tenants",
81
90
  );
82
- const existing = await fetchOne(db, tenantMembershipsTable, {
83
- userId: event.payload.userId,
84
- tenantId: event.payload.tenantId,
85
- });
86
- if (!existing) {
91
+ const streamTenantId = parseTenantId(event.payload.tenantId);
92
+ const existing =
93
+ streamTenantId === null
94
+ ? undefined
95
+ : await fetchOne(db, tenantMembershipsTable, {
96
+ userId: event.payload.userId,
97
+ tenantId: streamTenantId,
98
+ });
99
+ if (!existing || streamTenantId === null) {
87
100
  return writeFailure(
88
101
  new NotFoundError("membership", undefined, {
89
102
  i18nKey: "tenant.errors.membershipNotFound",
@@ -125,14 +138,15 @@ export const removeMemberWrite = defineWriteHandler({
125
138
  { id: (existing as DbRow)["id"] as string }, // @cast-boundary db-row
126
139
  event.user,
127
140
  db,
141
+ { streamTenantId },
128
142
  );
129
143
  if (!result.isSuccess) return result;
130
144
 
131
- // Actor tenant = invitation tenant so the update hits the invitation's stream.
132
145
  const cancelFailure = await cancelPendingInvitationsOfUser(db, {
133
146
  userId: event.payload.userId,
134
147
  tenantId: event.payload.tenantId,
135
- actor: { ...event.user, tenantId: event.payload.tenantId },
148
+ actor: event.user,
149
+ streamTenantId,
136
150
  redis: ctx.redis,
137
151
  });
138
152
  if (cancelFailure !== undefined) return cancelFailure;
@@ -6,8 +6,12 @@ import {
6
6
  createSystemUser,
7
7
  defineWriteHandler,
8
8
  findForbiddenRoleAssignment,
9
+ mayOverrideTenant,
9
10
  mergeAssignedRoles,
11
+ parseTenantId,
12
+ type SessionUser,
10
13
  SYSTEM_ROLE,
14
+ type TenantId,
11
15
  withResponseData,
12
16
  } from "@cosmicdrift/kumiko-framework/engine";
13
17
  import {
@@ -40,9 +44,14 @@ const REVOKE_ALL_SESSIONS_QN = "sessions:write:user-session:revoke-all-for-user"
40
44
  function resolveTargetTenantId(
41
45
  isSystem: boolean,
42
46
  payloadTenantId: string | undefined,
43
- sessionTenantId: string | undefined,
44
- ): string | undefined {
45
- return isSystem ? (payloadTenantId ?? sessionTenantId) : sessionTenantId;
47
+ sessionTenantId: TenantId,
48
+ ): TenantId | undefined {
49
+ if (!isSystem || payloadTenantId === undefined) return sessionTenantId;
50
+ return parseTenantId(payloadTenantId) ?? undefined;
51
+ }
52
+
53
+ function foreignStreamTenant(user: SessionUser, targetTenantId: TenantId): TenantId | undefined {
54
+ return targetTenantId === user.tenantId ? undefined : targetTenantId;
46
55
  }
47
56
 
48
57
  // Editing your OWN roles keeps the current session: roles are re-derived per
@@ -50,7 +59,7 @@ function resolveTargetTenantId(
50
59
  // of that user and any foreign edit still revoke all.
51
60
  function revokeSessionsPayload(
52
61
  targetUserId: string,
53
- targetTenantId: string,
62
+ targetTenantId: TenantId,
54
63
  actor: { readonly id: string; readonly sid?: string | undefined },
55
64
  ): { userId: string; tenantId: string; exceptSessionId?: string } {
56
65
  const payload = { userId: targetUserId, tenantId: targetTenantId };
@@ -82,7 +91,7 @@ export const updateMemberRolesWrite = defineWriteHandler({
82
91
  }
83
92
 
84
93
  const isSystemActor = event.user.roles.includes(SYSTEM_ROLE);
85
- const isSystem = event.user.roles.includes("SystemAdmin") || isSystemActor;
94
+ const isSystem = mayOverrideTenant(event.user) || isSystemActor;
86
95
  const assignableAppRoles = assignableAppRolesOf(ctx.registry);
87
96
  const targetTenantId = resolveTargetTenantId(
88
97
  isSystem,
@@ -185,19 +194,18 @@ export const updateMemberRolesWrite = defineWriteHandler({
185
194
  );
186
195
  }
187
196
 
188
- // Stream tenant follows the actor via streamTenantFor — use the
189
- // membership tenant so SystemAdmin/cross-tenant and TenantAdmin paths
190
- // hit the same stream seedTenantMembership/invite-accept wrote.
191
- // Keep the real actor for audit metadata; only override tenantId so
192
- // streamTenantFor writes into the membership tenant (#2401).
197
+ // The membership stream lives on the membership's tenant (where seedTenantMembership/
198
+ // invite-accept wrote it); a cross-tenant operator addresses it without becoming a member.
199
+ // Same-tenant writes run on a tenant-mode db, which refuses a stream override.
193
200
  const result = await executor.update(
194
201
  {
195
202
  id: row["id"] as string, // @cast-boundary db-row
196
203
  version: row["version"] as number, // @cast-boundary db-row
197
204
  changes: { roles: JSON.stringify(nextRoles) },
198
205
  },
199
- { ...event.user, tenantId: targetTenantId },
206
+ event.user,
200
207
  db,
208
+ { streamTenantId: foreignStreamTenant(event.user, targetTenantId) },
201
209
  );
202
210
 
203
211
  return withResponseData(result, {
@@ -0,0 +1,6 @@
1
+ import type { AssignableAppRoles } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { OWNER_INVITE_ROLE_OPTIONS } from "./constants.js";
3
+
4
+ export function memberRoleOptions(assignableAppRoles: AssignableAppRoles): readonly string[] {
5
+ return [...OWNER_INVITE_ROLE_OPTIONS, ...assignableAppRoles.keys()];
6
+ }
@@ -15,10 +15,10 @@ import {
15
15
  MEMBER_ROLES_EDIT_SCREEN_ID,
16
16
  MEMBER_STATUS_CELL_COMPONENT,
17
17
  MEMBERS_SCREEN_ID,
18
- OWNER_INVITE_ROLE_OPTIONS,
19
18
  TenantHandlers,
20
19
  TenantQueries,
21
20
  } from "./constants.js";
21
+ import { memberRoleOptions } from "./member-role-options.js";
22
22
 
23
23
  // Cross-tenant SystemAdmin platform view of the tenants themselves. The tenant
24
24
  // feature runs with `r.systemScope()`, so the entityList returns every tenant.
@@ -107,6 +107,7 @@ export function createMembersScreen(options?: {
107
107
  field: "lastSeenAt",
108
108
  label: i18nKey("tenant.members.col.lastActivity"),
109
109
  renderer: { format: "timestamp" },
110
+ hideOnNarrow: true,
110
111
  },
111
112
  ],
112
113
  defaultSort: { field: "createdAt", dir: "desc" },
@@ -199,13 +200,15 @@ export function createMemberRolesEditScreen(assignableAppRoles: AssignableAppRol
199
200
  cancelTarget: MEMBERS_SCREEN_ID,
200
201
  fields: {
201
202
  userId: { type: "text", required: true },
202
- // Prefilled with the member's current roles, so the option list must be
203
- // able to represent every assignable rank — DEFAULT_INVITE_ROLE_OPTIONS
204
- // omits TenantAdmin and would silently strip it on submit. Escalation
205
- // still stays server-side via findForbiddenRoleAssignment (update-member-roles.write.ts).
203
+ // Prefilled with the member's current roles, so the static options must
204
+ // represent every rank (DEFAULT_INVITE_ROLE_OPTIONS omits TenantAdmin and
205
+ // would silently strip it on submit); the availability query hides what the
206
+ // caller may not assign. Enforcement stays server-side via
207
+ // findForbiddenRoleAssignment (update-member-roles.write.ts).
206
208
  roles: {
207
209
  type: "multiSelect",
208
- options: [...OWNER_INVITE_ROLE_OPTIONS, ...assignableAppRoles.keys()],
210
+ options: memberRoleOptions(assignableAppRoles),
211
+ optionsAvailabilityQuery: TenantQueries.memberRoleAvailability,
209
212
  required: true,
210
213
  },
211
214
  },
@@ -1,11 +1,8 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
- import {
3
- buildEntityTable,
4
- createEventStoreExecutor,
5
- createTenantDb,
6
- } from "@cosmicdrift/kumiko-framework/db";
2
+ import { buildEntityTable, createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
7
3
  import { defineWriteHandler, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
8
4
  import * as z from "zod";
5
+ import { requireForTenant } from "../../shared/index.js";
9
6
  import { tierAssignmentAggregateId } from "../aggregate-id.js";
10
7
  import { TierAssignmentSources } from "../constants.js";
11
8
  import { type TierAssignmentRow, tierAssignmentEntity } from "../entity.js";
@@ -14,13 +11,8 @@ import { type TierAssignmentRow, tierAssignmentEntity } from "../entity.js";
14
11
  // Billing. Cross-tenant, daher SystemAdmin-only (kein TenantAdmin: sonst
15
12
  // Gratis-Self-Upgrade).
16
13
  //
17
- // **Cross-tenant-Mechanik:** ein "system"-mode TenantDb auf den Ziel-Tenant legt
18
- // KEINEN Tenant-Filter an (tenant-db.ts:141 — mode==="system" überspringt ihn);
19
- // der executor-user wird ebenfalls auf den Ziel-Tenant gestellt, sonst landet das
20
- // Event im Stream des Admins (Memory feedback_event_store_tenant_consistency).
21
- // Das set.write-"override-user"-Muster trägt NICHT für beliebige Tenants — es
22
- // funktioniert nur für SYSTEM_TENANT_ID (immer im IN-Filter). Dies ist das
23
- // auto-default-Hook-Muster (feature.ts), generalisiert auf einen Request-Handler.
14
+ // Cross-tenant mechanics: ctx.forTenant(tenantId) yields a tenant-mode db bound to the target plus
15
+ // the streamTenantId, so the event lands in the target's stream with the operator as actor.
24
16
  //
25
17
  // `source: TierAssignmentSources.manual` marks the grant so the billing sync skips it.
26
18
  // Upsert: one aggregate per tenant (deterministic aggregate id).
@@ -38,8 +30,6 @@ const executor = createEventStoreExecutor(tierAssignmentTable, tierAssignmentEnt
38
30
  entityName: "tier-assignment",
39
31
  });
40
32
 
41
- const SET_TENANT_TIER_REASON = "SystemAdmin assigns the tier of the tenant named in the payload";
42
-
43
33
  export type SetTenantTierOptions = {
44
34
  /** Runs after the write commits so feature.ts can update the resolver cache
45
35
  * and notify the other processes. Without a tierMap there is no resolver
@@ -68,10 +58,6 @@ export function createSetTenantTierWrite(opts: SetTenantTierOptions = {}) {
68
58
  }),
69
59
  access: { roles: ["SystemAdmin"] },
70
60
  agent: { risk: "high" },
71
- escapeHatch: {
72
- grants: ["unsafeRaw"],
73
- reason: SET_TENANT_TIER_REASON,
74
- },
75
61
  handler: async (event, ctx) => {
76
62
  // Without a commit sink (unit harness) there is no transaction to wait for.
77
63
  const afterCommit = (tenantId: TenantId, tier: string): void => {
@@ -80,9 +66,7 @@ export function createSetTenantTierWrite(opts: SetTenantTierOptions = {}) {
80
66
  else opts.onAssigned?.(tenantId, tier);
81
67
  };
82
68
  const tenantId = event.payload.tenantId as TenantId; // @cast-boundary engine-bridge
83
- const rawDb = ctx.db.unsafeRaw();
84
- const tdb = createTenantDb(rawDb, tenantId, "system");
85
- const systemUser = { ...event.user, tenantId };
69
+ const { db: tdb, streamTenantId } = requireForTenant(ctx, tenantId);
86
70
  const tier = event.payload.tier;
87
71
 
88
72
  const existing = await fetchOne<TierAssignmentRow>(tdb, tierAssignmentTable, { tenantId });
@@ -94,8 +78,9 @@ export function createSetTenantTierWrite(opts: SetTenantTierOptions = {}) {
94
78
  version: existing.version,
95
79
  changes: { tier, source: TierAssignmentSources.manual },
96
80
  },
97
- systemUser,
81
+ event.user,
98
82
  tdb,
83
+ { streamTenantId },
99
84
  );
100
85
  if (!result.isSuccess) return result;
101
86
  afterCommit(tenantId, tier);
@@ -109,8 +94,9 @@ export function createSetTenantTierWrite(opts: SetTenantTierOptions = {}) {
109
94
  source: TierAssignmentSources.manual,
110
95
  tenantId,
111
96
  },
112
- systemUser,
97
+ event.user,
113
98
  tdb,
99
+ { streamTenantId },
114
100
  );
115
101
  if (!result.isSuccess) return result;
116
102
  afterCommit(tenantId, tier);
@@ -17,7 +17,14 @@ export const userListScreen: EntityListScreenDefinition = {
17
17
  id: "user-list",
18
18
  type: "entityList",
19
19
  entity: "user",
20
- columns: ["email", "displayName", "roles", "tenants", "status", "emailVerified"],
20
+ columns: [
21
+ "email",
22
+ "displayName",
23
+ "roles",
24
+ "tenants",
25
+ "status",
26
+ { field: "emailVerified", hideOnNarrow: true },
27
+ ],
21
28
  rowActions: [
22
29
  {
23
30
  kind: "navigate",
@@ -68,7 +68,14 @@ export const downloadAttemptListScreen: EntityListScreenDefinition = {
68
68
  id: "download-attempt-list",
69
69
  type: "entityList",
70
70
  entity: "download-attempt",
71
- columns: ["attemptedAt", "result", "via", "ip", "attemptedByUserId", "jobId"],
71
+ columns: [
72
+ "attemptedAt",
73
+ "result",
74
+ "via",
75
+ "ip",
76
+ { field: "attemptedByUserId", hideOnNarrow: true },
77
+ { field: "jobId", hideOnNarrow: true },
78
+ ],
72
79
  searchable: false,
73
80
  access: { roles: access.systemAdmin },
74
81
  // Inert until an app navs it (see file header) — no nav area to resolve
@@ -6,7 +6,14 @@ export const waitlistListScreen: EntityListScreenDefinition = {
6
6
  id: "waitlist-list",
7
7
  type: "entityList",
8
8
  entity: "waitlistEntry",
9
- columns: ["name", "email", "company", "status", "locale", "submittedAt"],
9
+ columns: [
10
+ "name",
11
+ "email",
12
+ "company",
13
+ "status",
14
+ "locale",
15
+ { field: "submittedAt", hideOnNarrow: true },
16
+ ],
10
17
  defaultSort: { field: "submittedAt", dir: "desc" },
11
18
  access: adminAccess,
12
19
  searchable: true,
@@ -116,10 +116,10 @@ export const assetListScreen: EntityListScreenDefinition = {
116
116
  "status",
117
117
  "department",
118
118
  "owner",
119
- "location",
120
- "vendor",
121
- "purchaseDate",
122
- "warrantyUntil",
119
+ { field: "location", hideOnNarrow: true },
120
+ { field: "vendor", hideOnNarrow: true },
121
+ { field: "purchaseDate", hideOnNarrow: true },
122
+ { field: "warrantyUntil", hideOnNarrow: true },
123
123
  ],
124
124
  pageSize: 25,
125
125
  defaultSort: { field: "name", dir: "asc" },
@@ -123,10 +123,10 @@ export const ticketListScreen: EntityListScreenDefinition = {
123
123
  "severity",
124
124
  "status",
125
125
  "department",
126
- "assignee",
127
- "reporter",
128
- "dueDate",
129
- "spentMinutes",
126
+ { field: "assignee", hideOnNarrow: true },
127
+ { field: "reporter", hideOnNarrow: true },
128
+ { field: "dueDate", hideOnNarrow: true },
129
+ { field: "spentMinutes", hideOnNarrow: true },
130
130
  ],
131
131
  pageSize: 25,
132
132
  defaultSort: { field: "severity", dir: "desc" },
@@ -102,7 +102,7 @@ export const itemListScreen: EntityListScreenDefinition = {
102
102
  "dueDate",
103
103
  // Tier 2.7e-4 Demo — Reference-Spalte. Renderer macht Bulk-Lookup
104
104
  // auf item:list (limit:200), zeigt parentId als parent.title.
105
- "parentId",
105
+ { field: "parentId", hideOnNarrow: true },
106
106
  ],
107
107
  // Server-side Pagination Demo — Showcase seedet ~200 items, der
108
108
  // Pager hat 4 Seiten zum Durchklicken bei pageSize: 50.
@@ -228,9 +228,9 @@ export const demoFeature = defineFeature("styleguide", (r) => {
228
228
  "status",
229
229
  "isActive",
230
230
  "quantity",
231
- "rating",
232
- "publishedAt",
233
- "price",
231
+ { field: "rating", hideOnNarrow: true },
232
+ { field: "publishedAt", hideOnNarrow: true },
233
+ { field: "price", hideOnNarrow: true },
234
234
  ],
235
235
  pagination: "pages",
236
236
  pageSize: 25,
@@ -331,7 +331,7 @@ export function Gallery(): ReactNode {
331
331
  {/* Standalone <Section> (outside Form) = the calculator-result-card
332
332
  standard. title-only = existing consumer (publicstatus/tier-admin),
333
333
  must NOT regress from the border-b removal; subtitle +
334
- actions footer = new. No divider under the title (shadcn pattern). */}
334
+ actions (title row, top right) = new. No divider under the title (shadcn pattern). */}
335
335
  <div className="flex max-w-md flex-col gap-6">
336
336
  <Section title="Affordable rate / month">
337
337
  <div className="text-2xl font-semibold tabular-nums">€2,500.00</div>
@@ -27,7 +27,7 @@ const OPEN_FILL_COLUMNS = [
27
27
  "einheit",
28
28
  "liegenschaft",
29
29
  "beginn",
30
- "ende",
30
+ { field: "ende", hideOnNarrow: true },
31
31
  "status",
32
32
  ] as const;
33
33
 
@@ -145,7 +145,11 @@ const POSITION_COLUMNS = [
145
145
  label: "rental:entity:leasePosition:field:gueltigVon",
146
146
  sortable: true,
147
147
  },
148
- { field: "gueltigBis", label: "rental:entity:leasePosition:field:gueltigBis" },
148
+ {
149
+ field: "gueltigBis",
150
+ label: "rental:entity:leasePosition:field:gueltigBis",
151
+ hideOnNarrow: true,
152
+ },
149
153
  ] as const;
150
154
 
151
155
  export const leaseDetailScreen: ProjectionDetailScreenDefinition = {
@@ -31,7 +31,14 @@ export const vehicleListScreen: EntityListScreenDefinition = {
31
31
  id: "vehicle-list",
32
32
  type: "entityList",
33
33
  entity: "vehicle",
34
- columns: ["marke", "modell", "baujahr", "preis", "zustand", "scheckheftGepflegt"],
34
+ columns: [
35
+ "marke",
36
+ "modell",
37
+ "baujahr",
38
+ "preis",
39
+ "zustand",
40
+ { field: "scheckheftGepflegt", hideOnNarrow: true },
41
+ ],
35
42
  searchPlaceholder: "vehicles.search.placeholder",
36
43
  defaultSort: { field: "marke", dir: "asc" },
37
44
  rowActions: [
@@ -129,7 +136,7 @@ export const campaignListScreen: EntityListScreenDefinition = {
129
136
  "aktionStartet",
130
137
  "aktionEndet",
131
138
  "meta",
132
- "gepostet",
139
+ { field: "gepostet", hideOnNarrow: true },
133
140
  { field: "gestartetAm", hideOnNarrow: true },
134
141
  ],
135
142
  searchPlaceholder: "vehicles.search.placeholder",
@@ -1954,7 +1954,7 @@
1954
1954
  },
1955
1955
  {
1956
1956
  "name": "notes-history-user-data",
1957
- "description": "GDPR (Art. 20 export / Art. 17 erasure) coverage for the `notes-history` feature's `note-entry` and `note-mention` entities. Mounts the export hook so a user's authored notes are included in the user-data export bundle; the note-entry delete hook looks up `note-mention` for notes that structurally @-mention the forgotten user and crypto-shreds each reached note's row-subject key (after consulting that note's host entity's retention strategy — blockDelete/anonymize win over erasure), keeping the append-only history intact for notes without such a mention. `note-mention`'s own hooks are a no-op — it is a plain FK pointer, not separately exportable content. Kept separate from `notes-history` so notes consumers without the user-data-rights pipeline don't pull a hard dependency — requires `user-data-rights`, optionalRequires `notes-history`.",
1957
+ "description": "GDPR (Art. 20 export / Art. 17 erasure) coverage for the `notes-history` feature's `note-entry` and `note-mention` entities. Mounts the export hook so a user's authored notes are included in the user-data export bundle; the note-entry delete hook crypto-shreds the row-subject key of each note that structurally @-mentions the forgotten user (`note-mention`) or hangs on the user as host entity (after consulting that note's host entity's retention strategy — blockDelete/anonymize win over erasure), keeping the append-only history intact for all other notes. The manual `crypto-shredding` forget-subject command reaches the same notes for user and record subjects through the exported `eraseNoteBodiesReachingSubject`, and the export lists them under `note-mention`. `note-mention`'s own hooks are a no-op — it is a plain FK pointer, not separately exportable content. Kept separate from `notes-history` so notes consumers without the user-data-rights pipeline don't pull a hard dependency — requires `user-data-rights`, optionalRequires `notes-history`.",
1958
1958
  "toggleableDefault": null,
1959
1959
  "requires": [
1960
1960
  "user-data-rights"