@dloizides/ui-access-admin 1.2.2 → 1.3.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/CHANGELOG.md CHANGED
@@ -5,7 +5,18 @@ All notable changes to `@dloizides/ui-access-admin` are documented here.
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
6
6
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [1.2.1]
8
+ ## [1.3.0]
9
+
10
+ ### Added
11
+
12
+ - `PermissionMatrix`: read-only roles x permissions view (FINREG-FUNDS-1 "Tenant funds, deal gate, blocking and roles", AC-R2). Data injected; one card per role below `LAYOUT_COLLAPSE_BREAKPOINT`, a table above it.
13
+ - `UserManagementPanel` edit modal: a required block reason when Active is off, sent as `blockedReason` (AC-B1). New `UserManagementPatch` type; `UserManagementUser.blockedReason`.
14
+
15
+ ### Changed
16
+
17
+ - Peer `@dloizides/ui-layout` raised to `>=1.27.0` (needs `LAYOUT_COLLAPSE_BREAKPOINT`).
18
+
19
+ ## [1.2.2]
9
20
 
10
21
  ### Fixed
11
22
 
package/dist/index.d.mts CHANGED
@@ -336,9 +336,17 @@ interface UserManagementUser {
336
336
  /** The tenant this user belongs to, or null for a platform-level account. */
337
337
  tenantId: string | null;
338
338
  active: boolean;
339
+ blockedReason?: string | null;
339
340
  /** ISO timestamp the user was created, when the directory reports it. */
340
341
  createdAt?: string;
341
342
  }
343
+ interface UserManagementPatch {
344
+ displayName?: string;
345
+ role?: string;
346
+ tenantId?: string | null;
347
+ active?: boolean;
348
+ blockedReason?: string;
349
+ }
342
350
  /** What the host is asked to do. Each rejects with an `Error` on failure. */
343
351
  interface UserManagementApi {
344
352
  listUsers(): Promise<UserManagementUser[]>;
@@ -349,12 +357,7 @@ interface UserManagementApi {
349
357
  role: string;
350
358
  tenantId: string | null;
351
359
  }): Promise<void>;
352
- updateUser(id: string, patch: {
353
- displayName?: string;
354
- role?: string;
355
- tenantId?: string | null;
356
- active?: boolean;
357
- }): Promise<void>;
360
+ updateUser(id: string, patch: UserManagementPatch): Promise<void>;
358
361
  resetPassword(id: string, password: string): Promise<void>;
359
362
  }
360
363
  /** One selectable tenant for the platform-admin tenant picker. */
@@ -403,6 +406,37 @@ interface UserManagementPanelProps {
403
406
 
404
407
  declare const UserManagementPanel: ({ api, isPlatformAdmin, assignableRoles, tenants, t, testID, }: UserManagementPanelProps) => React.ReactElement;
405
408
 
409
+ type PermissionMatrixTranslate = (key: string, p1?: string, p2?: string, p3?: string) => string;
410
+ interface PermissionMatrixRole {
411
+ readonly id: string;
412
+ readonly label: string;
413
+ }
414
+ interface PermissionMatrixPermission {
415
+ readonly id: string;
416
+ readonly label: string;
417
+ }
418
+ type PermissionMatrixGrants = Readonly<Record<string, readonly string[] | undefined>>;
419
+ interface PermissionMatrixCell {
420
+ readonly permission: PermissionMatrixPermission;
421
+ readonly granted: boolean;
422
+ }
423
+ interface PermissionMatrixColumn {
424
+ readonly role: PermissionMatrixRole;
425
+ readonly cells: readonly PermissionMatrixCell[];
426
+ readonly grantedCount: number;
427
+ }
428
+ interface PermissionMatrixProps {
429
+ readonly roles: readonly PermissionMatrixRole[];
430
+ readonly permissions: readonly PermissionMatrixPermission[];
431
+ readonly grants: PermissionMatrixGrants;
432
+ readonly t: PermissionMatrixTranslate;
433
+ readonly testID?: string;
434
+ }
435
+
436
+ declare const PermissionMatrix: ({ roles, permissions, grants, t, testID, }: PermissionMatrixProps) => React.ReactElement;
437
+
438
+ declare function buildPermissionMatrix(roles: readonly PermissionMatrixRole[], permissions: readonly PermissionMatrixPermission[], grants: PermissionMatrixGrants): PermissionMatrixColumn[];
439
+
406
440
  /** Translate function — mirrors the fleet's `FM(key, p1?, p2?, p3?)` shape. */
407
441
  type AccessTranslate = (key: string, p1?: string, p2?: string, p3?: string) => string;
408
442
  interface UseCapabilityLinkMintOptions {
@@ -702,6 +736,18 @@ declare const ACCESS_ADMIN_I18N: {
702
736
  readonly usersUpdateFailed: "accessAdmin.users.updateFailed";
703
737
  readonly usersResetSuccess: "accessAdmin.users.resetSuccess";
704
738
  readonly usersResetFailed: "accessAdmin.users.resetFailed";
739
+ readonly usersFieldBlockedReason: "accessAdmin.users.fieldBlockedReason";
740
+ readonly usersBlockedReasonPlaceholder: "accessAdmin.users.blockedReasonPlaceholder";
741
+ readonly usersBlockedReasonHint: "accessAdmin.users.blockedReasonHint";
742
+ readonly permissionsHeading: "accessAdmin.permissions.heading";
743
+ readonly permissionsNote: "accessAdmin.permissions.note";
744
+ readonly permissionsColPermission: "accessAdmin.permissions.colPermission";
745
+ readonly permissionsGranted: "accessAdmin.permissions.granted";
746
+ readonly permissionsNotGranted: "accessAdmin.permissions.notGranted";
747
+ readonly permissionsCellLabel: "accessAdmin.permissions.cellLabel";
748
+ readonly permissionsReadOnlyHint: "accessAdmin.permissions.readOnlyHint";
749
+ readonly permissionsRoleCount: "accessAdmin.permissions.roleCount";
750
+ readonly permissionsEmpty: "accessAdmin.permissions.empty";
705
751
  };
706
752
  /** The shape of {@link ACCESS_ADMIN_I18N} — useful for typing an override map. */
707
753
  type AccessAdminI18n = typeof ACCESS_ADMIN_I18N;
@@ -714,6 +760,7 @@ declare const ACCESS_ADMIN_TEST_IDS: {
714
760
  readonly linkPanel: "access-link-panel";
715
761
  readonly accountPanel: "account-link-panel";
716
762
  readonly usersPanel: "user-management-panel";
763
+ readonly permissionMatrix: "permission-matrix";
717
764
  };
718
765
 
719
- export { ACCESS_ADMIN_I18N, ACCESS_ADMIN_TEST_IDS, type AccessAdminI18n, type AccessAdminPalette, AccessLinkPanel, type AccessLinkPanelProps, type AccessResultReporter, AccessResultTone, type AccessSubject, type AccessTranslate, AccountLinkPanel, type AccountLinkPanelProps, type CapabilityLink, type CapabilityLinkMintState, type DescribeError, type ExpiryPolicy, type LinkSubjectAccount, type LinkableAccount, type MintCapabilityLink, type MintCapabilityLinkInput, type SubjectAccountLinkState, type UseCapabilityLinkMintOptions, type UseSubjectAccountLinkOptions, type UserManagementApi, UserManagementPanel, type UserManagementPanelProps, type UserManagementTenant, type UserManagementTranslate, type UserManagementUser, accountDisplayLabel, activeLinksForSubject, buildAccountOptions, buildCapabilityLinkMailto, capabilityExpiryDate, capabilityExpiryDays, daysUntilAnchor, formatLinkDate, linksForSubject, useAccessAdminPalette, useCapabilityLinkMint, useSubjectAccountLink };
766
+ export { ACCESS_ADMIN_I18N, ACCESS_ADMIN_TEST_IDS, type AccessAdminI18n, type AccessAdminPalette, AccessLinkPanel, type AccessLinkPanelProps, type AccessResultReporter, AccessResultTone, type AccessSubject, type AccessTranslate, AccountLinkPanel, type AccountLinkPanelProps, type CapabilityLink, type CapabilityLinkMintState, type DescribeError, type ExpiryPolicy, type LinkSubjectAccount, type LinkableAccount, type MintCapabilityLink, type MintCapabilityLinkInput, PermissionMatrix, type PermissionMatrixColumn, type PermissionMatrixGrants, type PermissionMatrixPermission, type PermissionMatrixProps, type PermissionMatrixRole, type PermissionMatrixTranslate, type SubjectAccountLinkState, type UseCapabilityLinkMintOptions, type UseSubjectAccountLinkOptions, type UserManagementApi, UserManagementPanel, type UserManagementPanelProps, type UserManagementPatch, type UserManagementTenant, type UserManagementTranslate, type UserManagementUser, accountDisplayLabel, activeLinksForSubject, buildAccountOptions, buildCapabilityLinkMailto, buildPermissionMatrix, capabilityExpiryDate, capabilityExpiryDays, daysUntilAnchor, formatLinkDate, linksForSubject, useAccessAdminPalette, useCapabilityLinkMint, useSubjectAccountLink };
package/dist/index.d.ts CHANGED
@@ -336,9 +336,17 @@ interface UserManagementUser {
336
336
  /** The tenant this user belongs to, or null for a platform-level account. */
337
337
  tenantId: string | null;
338
338
  active: boolean;
339
+ blockedReason?: string | null;
339
340
  /** ISO timestamp the user was created, when the directory reports it. */
340
341
  createdAt?: string;
341
342
  }
343
+ interface UserManagementPatch {
344
+ displayName?: string;
345
+ role?: string;
346
+ tenantId?: string | null;
347
+ active?: boolean;
348
+ blockedReason?: string;
349
+ }
342
350
  /** What the host is asked to do. Each rejects with an `Error` on failure. */
343
351
  interface UserManagementApi {
344
352
  listUsers(): Promise<UserManagementUser[]>;
@@ -349,12 +357,7 @@ interface UserManagementApi {
349
357
  role: string;
350
358
  tenantId: string | null;
351
359
  }): Promise<void>;
352
- updateUser(id: string, patch: {
353
- displayName?: string;
354
- role?: string;
355
- tenantId?: string | null;
356
- active?: boolean;
357
- }): Promise<void>;
360
+ updateUser(id: string, patch: UserManagementPatch): Promise<void>;
358
361
  resetPassword(id: string, password: string): Promise<void>;
359
362
  }
360
363
  /** One selectable tenant for the platform-admin tenant picker. */
@@ -403,6 +406,37 @@ interface UserManagementPanelProps {
403
406
 
404
407
  declare const UserManagementPanel: ({ api, isPlatformAdmin, assignableRoles, tenants, t, testID, }: UserManagementPanelProps) => React.ReactElement;
405
408
 
409
+ type PermissionMatrixTranslate = (key: string, p1?: string, p2?: string, p3?: string) => string;
410
+ interface PermissionMatrixRole {
411
+ readonly id: string;
412
+ readonly label: string;
413
+ }
414
+ interface PermissionMatrixPermission {
415
+ readonly id: string;
416
+ readonly label: string;
417
+ }
418
+ type PermissionMatrixGrants = Readonly<Record<string, readonly string[] | undefined>>;
419
+ interface PermissionMatrixCell {
420
+ readonly permission: PermissionMatrixPermission;
421
+ readonly granted: boolean;
422
+ }
423
+ interface PermissionMatrixColumn {
424
+ readonly role: PermissionMatrixRole;
425
+ readonly cells: readonly PermissionMatrixCell[];
426
+ readonly grantedCount: number;
427
+ }
428
+ interface PermissionMatrixProps {
429
+ readonly roles: readonly PermissionMatrixRole[];
430
+ readonly permissions: readonly PermissionMatrixPermission[];
431
+ readonly grants: PermissionMatrixGrants;
432
+ readonly t: PermissionMatrixTranslate;
433
+ readonly testID?: string;
434
+ }
435
+
436
+ declare const PermissionMatrix: ({ roles, permissions, grants, t, testID, }: PermissionMatrixProps) => React.ReactElement;
437
+
438
+ declare function buildPermissionMatrix(roles: readonly PermissionMatrixRole[], permissions: readonly PermissionMatrixPermission[], grants: PermissionMatrixGrants): PermissionMatrixColumn[];
439
+
406
440
  /** Translate function — mirrors the fleet's `FM(key, p1?, p2?, p3?)` shape. */
407
441
  type AccessTranslate = (key: string, p1?: string, p2?: string, p3?: string) => string;
408
442
  interface UseCapabilityLinkMintOptions {
@@ -702,6 +736,18 @@ declare const ACCESS_ADMIN_I18N: {
702
736
  readonly usersUpdateFailed: "accessAdmin.users.updateFailed";
703
737
  readonly usersResetSuccess: "accessAdmin.users.resetSuccess";
704
738
  readonly usersResetFailed: "accessAdmin.users.resetFailed";
739
+ readonly usersFieldBlockedReason: "accessAdmin.users.fieldBlockedReason";
740
+ readonly usersBlockedReasonPlaceholder: "accessAdmin.users.blockedReasonPlaceholder";
741
+ readonly usersBlockedReasonHint: "accessAdmin.users.blockedReasonHint";
742
+ readonly permissionsHeading: "accessAdmin.permissions.heading";
743
+ readonly permissionsNote: "accessAdmin.permissions.note";
744
+ readonly permissionsColPermission: "accessAdmin.permissions.colPermission";
745
+ readonly permissionsGranted: "accessAdmin.permissions.granted";
746
+ readonly permissionsNotGranted: "accessAdmin.permissions.notGranted";
747
+ readonly permissionsCellLabel: "accessAdmin.permissions.cellLabel";
748
+ readonly permissionsReadOnlyHint: "accessAdmin.permissions.readOnlyHint";
749
+ readonly permissionsRoleCount: "accessAdmin.permissions.roleCount";
750
+ readonly permissionsEmpty: "accessAdmin.permissions.empty";
705
751
  };
706
752
  /** The shape of {@link ACCESS_ADMIN_I18N} — useful for typing an override map. */
707
753
  type AccessAdminI18n = typeof ACCESS_ADMIN_I18N;
@@ -714,6 +760,7 @@ declare const ACCESS_ADMIN_TEST_IDS: {
714
760
  readonly linkPanel: "access-link-panel";
715
761
  readonly accountPanel: "account-link-panel";
716
762
  readonly usersPanel: "user-management-panel";
763
+ readonly permissionMatrix: "permission-matrix";
717
764
  };
718
765
 
719
- export { ACCESS_ADMIN_I18N, ACCESS_ADMIN_TEST_IDS, type AccessAdminI18n, type AccessAdminPalette, AccessLinkPanel, type AccessLinkPanelProps, type AccessResultReporter, AccessResultTone, type AccessSubject, type AccessTranslate, AccountLinkPanel, type AccountLinkPanelProps, type CapabilityLink, type CapabilityLinkMintState, type DescribeError, type ExpiryPolicy, type LinkSubjectAccount, type LinkableAccount, type MintCapabilityLink, type MintCapabilityLinkInput, type SubjectAccountLinkState, type UseCapabilityLinkMintOptions, type UseSubjectAccountLinkOptions, type UserManagementApi, UserManagementPanel, type UserManagementPanelProps, type UserManagementTenant, type UserManagementTranslate, type UserManagementUser, accountDisplayLabel, activeLinksForSubject, buildAccountOptions, buildCapabilityLinkMailto, capabilityExpiryDate, capabilityExpiryDays, daysUntilAnchor, formatLinkDate, linksForSubject, useAccessAdminPalette, useCapabilityLinkMint, useSubjectAccountLink };
766
+ export { ACCESS_ADMIN_I18N, ACCESS_ADMIN_TEST_IDS, type AccessAdminI18n, type AccessAdminPalette, AccessLinkPanel, type AccessLinkPanelProps, type AccessResultReporter, AccessResultTone, type AccessSubject, type AccessTranslate, AccountLinkPanel, type AccountLinkPanelProps, type CapabilityLink, type CapabilityLinkMintState, type DescribeError, type ExpiryPolicy, type LinkSubjectAccount, type LinkableAccount, type MintCapabilityLink, type MintCapabilityLinkInput, PermissionMatrix, type PermissionMatrixColumn, type PermissionMatrixGrants, type PermissionMatrixPermission, type PermissionMatrixProps, type PermissionMatrixRole, type PermissionMatrixTranslate, type SubjectAccountLinkState, type UseCapabilityLinkMintOptions, type UseSubjectAccountLinkOptions, type UserManagementApi, UserManagementPanel, type UserManagementPanelProps, type UserManagementPatch, type UserManagementTenant, type UserManagementTranslate, type UserManagementUser, accountDisplayLabel, activeLinksForSubject, buildAccountOptions, buildCapabilityLinkMailto, buildPermissionMatrix, capabilityExpiryDate, capabilityExpiryDays, daysUntilAnchor, formatLinkDate, linksForSubject, useAccessAdminPalette, useCapabilityLinkMint, useSubjectAccountLink };
package/dist/index.js CHANGED
@@ -169,12 +169,25 @@ var ACCESS_ADMIN_I18N = {
169
169
  usersUpdateSuccess: "accessAdmin.users.updateSuccess",
170
170
  usersUpdateFailed: "accessAdmin.users.updateFailed",
171
171
  usersResetSuccess: "accessAdmin.users.resetSuccess",
172
- usersResetFailed: "accessAdmin.users.resetFailed"
172
+ usersResetFailed: "accessAdmin.users.resetFailed",
173
+ usersFieldBlockedReason: "accessAdmin.users.fieldBlockedReason",
174
+ usersBlockedReasonPlaceholder: "accessAdmin.users.blockedReasonPlaceholder",
175
+ usersBlockedReasonHint: "accessAdmin.users.blockedReasonHint",
176
+ permissionsHeading: "accessAdmin.permissions.heading",
177
+ permissionsNote: "accessAdmin.permissions.note",
178
+ permissionsColPermission: "accessAdmin.permissions.colPermission",
179
+ permissionsGranted: "accessAdmin.permissions.granted",
180
+ permissionsNotGranted: "accessAdmin.permissions.notGranted",
181
+ permissionsCellLabel: "accessAdmin.permissions.cellLabel",
182
+ permissionsReadOnlyHint: "accessAdmin.permissions.readOnlyHint",
183
+ permissionsRoleCount: "accessAdmin.permissions.roleCount",
184
+ permissionsEmpty: "accessAdmin.permissions.empty"
173
185
  };
174
186
  var ACCESS_ADMIN_TEST_IDS = {
175
187
  linkPanel: "access-link-panel",
176
188
  accountPanel: "account-link-panel",
177
- usersPanel: "user-management-panel"
189
+ usersPanel: "user-management-panel",
190
+ permissionMatrix: "permission-matrix"
178
191
  };
179
192
 
180
193
  // src/expiry/expiryPolicy.ts
@@ -319,7 +332,7 @@ var AccessLinkPanel = ({
319
332
  }) => {
320
333
  const { t } = uiFeedback.useUi();
321
334
  const palette = useAccessAdminPalette();
322
- const styles = react.useMemo(() => buildStyles(palette), [palette]);
335
+ const styles2 = react.useMemo(() => buildStyles(palette), [palette]);
323
336
  const state = useCapabilityLinkMint({
324
337
  subject,
325
338
  policy,
@@ -347,16 +360,16 @@ var AccessLinkPanel = ({
347
360
  onResult(t(ACCESS_ADMIN_I18N.copyFailed, subject.displayName), "error" /* Error */);
348
361
  });
349
362
  };
350
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.panel, testID: testIDPrefix, children: [
351
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.heading, children: t(ACCESS_ADMIN_I18N.panelHeading, subject.displayName) }),
352
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.note, children: t(ACCESS_ADMIN_I18N.panelNote) }),
353
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.warning, testID: `${testIDPrefix}-capability-warning`, children: t(ACCESS_ADMIN_I18N.capabilityWarning) }),
354
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.note, testID: `${testIDPrefix}-expiry-preview`, children: t(ACCESS_ADMIN_I18N.expiryPreview, expiryDate) }),
355
- state.minted !== null ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.revealBox, testID: `${testIDPrefix}-minted`, children: [
356
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.revealTitle, children: t(ACCESS_ADMIN_I18N.mintedTitle, subject.displayName) }),
357
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.note, children: t(ACCESS_ADMIN_I18N.mintedHint) }),
358
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { selectable: true, style: styles.url, testID: `${testIDPrefix}-minted-url`, children: mintedUrl }),
359
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.actions, children: [
363
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.panel, testID: testIDPrefix, children: [
364
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.heading, children: t(ACCESS_ADMIN_I18N.panelHeading, subject.displayName) }),
365
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.note, children: t(ACCESS_ADMIN_I18N.panelNote) }),
366
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.warning, testID: `${testIDPrefix}-capability-warning`, children: t(ACCESS_ADMIN_I18N.capabilityWarning) }),
367
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.note, testID: `${testIDPrefix}-expiry-preview`, children: t(ACCESS_ADMIN_I18N.expiryPreview, expiryDate) }),
368
+ state.minted !== null ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.revealBox, testID: `${testIDPrefix}-minted`, children: [
369
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.revealTitle, children: t(ACCESS_ADMIN_I18N.mintedTitle, subject.displayName) }),
370
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.note, children: t(ACCESS_ADMIN_I18N.mintedHint) }),
371
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { selectable: true, style: styles2.url, testID: `${testIDPrefix}-minted-url`, children: mintedUrl }),
372
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.actions, children: [
360
373
  /* @__PURE__ */ jsxRuntime.jsx(
361
374
  uiButtons.Button,
362
375
  {
@@ -390,13 +403,13 @@ var AccessLinkPanel = ({
390
403
  }
391
404
  )
392
405
  ] }),
393
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.note, children: t(ACCESS_ADMIN_I18N.emailNote) })
406
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.note, children: t(ACCESS_ADMIN_I18N.emailNote) })
394
407
  ] }) : null,
395
- linksFailed ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.warning, testID: `${testIDPrefix}-load-error`, children: t(ACCESS_ADMIN_I18N.liveLoadError) }) : null,
396
- !linksFailed && liveLinks.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.liveBox, children: [
397
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.subheading, children: t(ACCESS_ADMIN_I18N.liveHeading, String(liveLinks.length)) }),
398
- liveLinks.map((link) => /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.liveRow, children: [
399
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.liveMeta, children: t(
408
+ linksFailed ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.warning, testID: `${testIDPrefix}-load-error`, children: t(ACCESS_ADMIN_I18N.liveLoadError) }) : null,
409
+ !linksFailed && liveLinks.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.liveBox, children: [
410
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.subheading, children: t(ACCESS_ADMIN_I18N.liveHeading, String(liveLinks.length)) }),
411
+ liveLinks.map((link) => /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.liveRow, children: [
412
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.liveMeta, children: t(
400
413
  ACCESS_ADMIN_I18N.liveMeta,
401
414
  formatLinkDate(link.createdAt, ""),
402
415
  formatLinkDate(link.expiresAt, t(ACCESS_ADMIN_I18N.expiresNever))
@@ -404,8 +417,8 @@ var AccessLinkPanel = ({
404
417
  renderLinkActions === void 0 ? null : renderLinkActions(link)
405
418
  ] }, link.id))
406
419
  ] }) : null,
407
- !linksFailed && liveLinks.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.note, testID: `${testIDPrefix}-no-live`, children: t(ACCESS_ADMIN_I18N.noLive) }) : null,
408
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.actions, children: [
420
+ !linksFailed && liveLinks.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.note, testID: `${testIDPrefix}-no-live`, children: t(ACCESS_ADMIN_I18N.noLive) }) : null,
421
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.actions, children: [
409
422
  /* @__PURE__ */ jsxRuntime.jsx(
410
423
  uiButtons.Button,
411
424
  {
@@ -551,7 +564,7 @@ var AccountLinkPanel = ({
551
564
  }) => {
552
565
  const { t } = uiFeedback.useUi();
553
566
  const palette = useAccessAdminPalette();
554
- const styles = react.useMemo(() => buildStyles2(palette), [palette]);
567
+ const styles2 = react.useMemo(() => buildStyles2(palette), [palette]);
555
568
  const state = useSubjectAccountLink({
556
569
  subject,
557
570
  boundAccountId,
@@ -566,9 +579,9 @@ var AccountLinkPanel = ({
566
579
  [accounts, bindings, subject, unnamedLabel]
567
580
  );
568
581
  const selectedLabel = options.find((option) => option.value === state.selectedAccountId)?.label ?? "";
569
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.panel, testID: testIDPrefix, children: [
570
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.heading, children: t(ACCESS_ADMIN_I18N.accountPanelHeading, subject.displayName) }),
571
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.note, children: t(ACCESS_ADMIN_I18N.accountPanelNote) }),
582
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.panel, testID: testIDPrefix, children: [
583
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.heading, children: t(ACCESS_ADMIN_I18N.accountPanelHeading, subject.displayName) }),
584
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.note, children: t(ACCESS_ADMIN_I18N.accountPanelNote) }),
572
585
  options.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
573
586
  uiForms.SelectControl,
574
587
  {
@@ -581,8 +594,8 @@ var AccountLinkPanel = ({
581
594
  testID: `${testIDPrefix}-picker`,
582
595
  value: state.selectedAccountId ?? ""
583
596
  }
584
- ) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.note, testID: `${testIDPrefix}-no-accounts`, children: t(ACCESS_ADMIN_I18N.accountNone) }),
585
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.actions, children: [
597
+ ) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.note, testID: `${testIDPrefix}-no-accounts`, children: t(ACCESS_ADMIN_I18N.accountNone) }),
598
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.actions, children: [
586
599
  /* @__PURE__ */ jsxRuntime.jsx(
587
600
  uiButtons.Button,
588
601
  {
@@ -675,6 +688,18 @@ function roleOptions(assignableRoles) {
675
688
  function tenantValueToId(value) {
676
689
  return value === NO_TENANT_VALUE ? null : value;
677
690
  }
691
+ function isBlocking(wasActive, active) {
692
+ return wasActive && !active;
693
+ }
694
+ function isBlockReasonMissing(wasActive, active, blockedReason) {
695
+ return isBlocking(wasActive, active) && blockedReason.trim() === "";
696
+ }
697
+ function buildEditPatch(input) {
698
+ const patch = { displayName: input.displayName.trim(), role: input.role, active: input.active };
699
+ if (isBlocking(input.wasActive, input.active)) patch.blockedReason = input.blockedReason.trim();
700
+ if (input.tenantId !== void 0) patch.tenantId = input.tenantId;
701
+ return patch;
702
+ }
678
703
  var ACTIONS_GAP = 8;
679
704
  function buildUserColumns({
680
705
  t,
@@ -913,6 +938,7 @@ var EditUserModal = ({
913
938
  const [role, setRole] = react.useState("");
914
939
  const [tenantValue, setTenantValue] = react.useState(NO_TENANT_VALUE);
915
940
  const [active, setActive] = react.useState(true);
941
+ const [blockedReason, setBlockedReason] = react.useState("");
916
942
  const [submitting, setSubmitting] = react.useState(false);
917
943
  const userId = user?.id;
918
944
  react.useEffect(() => {
@@ -921,28 +947,36 @@ var EditUserModal = ({
921
947
  setRole(user.roles[0] ?? "");
922
948
  setTenantValue(user.tenantId ?? NO_TENANT_VALUE);
923
949
  setActive(user.active);
950
+ setBlockedReason(user.blockedReason ?? "");
924
951
  setSubmitting(false);
925
952
  }, [visible, userId, user]);
926
- const canSubmit = !submitting && displayName.trim() !== "" && role !== "";
953
+ const wasActive = user?.active ?? true;
954
+ const canSubmit = !submitting && displayName.trim() !== "" && role !== "" && !isBlockReasonMissing(wasActive, active, blockedReason);
927
955
  const handleSubmit = async () => {
928
956
  if (user === null) return;
929
957
  setSubmitting(true);
930
- const patch = {
931
- displayName: displayName.trim(),
958
+ const patch = buildEditPatch({
959
+ displayName,
932
960
  role,
933
- active
934
- };
935
- if (isPlatformAdmin) patch.tenantId = tenantValueToId(tenantValue);
961
+ wasActive,
962
+ active,
963
+ blockedReason,
964
+ tenantId: isPlatformAdmin ? tenantValueToId(tenantValue) : void 0
965
+ });
936
966
  const ok = await onSubmit(user.id, patch);
937
967
  setSubmitting(false);
938
968
  if (ok) onCancel();
939
969
  };
970
+ const submitIfReady = () => {
971
+ if (canSubmit) void handleSubmit();
972
+ };
940
973
  return /* @__PURE__ */ jsxRuntime.jsx(uiLayout.ModalShell, { onCancel, title: t(ACCESS_ADMIN_I18N.usersEditTitle), visible, children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: modalFormStyles.body, testID: testIDPrefix, children: [
941
974
  /* @__PURE__ */ jsxRuntime.jsx(
942
975
  uiForms.FormField,
943
976
  {
944
977
  label: t(ACCESS_ADMIN_I18N.usersFieldDisplayName),
945
978
  onChangeText: setDisplayName,
979
+ onSubmitEditing: submitIfReady,
946
980
  placeholder: t(ACCESS_ADMIN_I18N.usersDisplayNamePlaceholder),
947
981
  required: true,
948
982
  testID: `${testIDPrefix}-display-name`,
@@ -983,6 +1017,19 @@ var EditUserModal = ({
983
1017
  value: active
984
1018
  }
985
1019
  ),
1020
+ active ? null : /* @__PURE__ */ jsxRuntime.jsx(
1021
+ uiForms.FormField,
1022
+ {
1023
+ accessibilityHint: t(ACCESS_ADMIN_I18N.usersBlockedReasonHint),
1024
+ label: t(ACCESS_ADMIN_I18N.usersFieldBlockedReason),
1025
+ onChangeText: setBlockedReason,
1026
+ onSubmitEditing: submitIfReady,
1027
+ placeholder: t(ACCESS_ADMIN_I18N.usersBlockedReasonPlaceholder),
1028
+ required: wasActive,
1029
+ testID: `${testIDPrefix}-blocked-reason`,
1030
+ value: blockedReason
1031
+ }
1032
+ ),
986
1033
  /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: modalFormStyles.actions, children: [
987
1034
  /* @__PURE__ */ jsxRuntime.jsx(
988
1035
  uiButtons.Button,
@@ -1164,7 +1211,7 @@ var UserManagementPanel = ({
1164
1211
  testID = ACCESS_ADMIN_TEST_IDS.usersPanel
1165
1212
  }) => {
1166
1213
  const palette = useAccessAdminPalette();
1167
- const styles = react.useMemo(() => buildStyles3(palette), [palette]);
1214
+ const styles2 = react.useMemo(() => buildStyles3(palette), [palette]);
1168
1215
  const controller = useUserManagement(api, t);
1169
1216
  const [query, setQuery] = react.useState("");
1170
1217
  const [openModal, setOpenModal] = react.useState("none" /* None */);
@@ -1186,10 +1233,10 @@ var UserManagementPanel = ({
1186
1233
  () => buildUserColumns({ t, tenants, testIDPrefix: testID, onEdit: openEdit, onReset: openReset }),
1187
1234
  [t, tenants, testID]
1188
1235
  );
1189
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.panel, testID, children: [
1236
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.panel, testID, children: [
1190
1237
  /* @__PURE__ */ jsxRuntime.jsx(uiFeedback.ToastHost, { subscribe: controller.toastSubscribe, toastTestID: `${testID}-toast` }),
1191
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.heading, children: t(ACCESS_ADMIN_I18N.usersHeading) }),
1192
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.note, children: t(ACCESS_ADMIN_I18N.usersNote) }),
1238
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.heading, children: t(ACCESS_ADMIN_I18N.usersHeading) }),
1239
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.note, children: t(ACCESS_ADMIN_I18N.usersNote) }),
1193
1240
  /* @__PURE__ */ jsxRuntime.jsx(
1194
1241
  uiTables.FilterBar,
1195
1242
  {
@@ -1206,7 +1253,7 @@ var UserManagementPanel = ({
1206
1253
  resultsCount: visibleUsers.length,
1207
1254
  resultsLabel: t(ACCESS_ADMIN_I18N.usersResultsLabel),
1208
1255
  testID: `${testID}-filter-bar`,
1209
- children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.search, children: /* @__PURE__ */ jsxRuntime.jsx(
1256
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.search, children: /* @__PURE__ */ jsxRuntime.jsx(
1210
1257
  uiForms.FormField,
1211
1258
  {
1212
1259
  accessibilityLabel: t(ACCESS_ADMIN_I18N.usersSearchLabel),
@@ -1221,8 +1268,8 @@ var UserManagementPanel = ({
1221
1268
  ) })
1222
1269
  }
1223
1270
  ),
1224
- controller.loadFailed ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.errorBox, testID: `${testID}-load-error`, children: [
1225
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.warning, children: t(ACCESS_ADMIN_I18N.usersLoadError) }),
1271
+ controller.loadFailed ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.errorBox, testID: `${testID}-load-error`, children: [
1272
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.warning, children: t(ACCESS_ADMIN_I18N.usersLoadError) }),
1226
1273
  /* @__PURE__ */ jsxRuntime.jsx(
1227
1274
  uiButtons.Button,
1228
1275
  {
@@ -1304,16 +1351,128 @@ var buildStyles3 = (c) => reactNative.StyleSheet.create({
1304
1351
  warning: { color: c.danger, fontSize: FONT_SM3, fontWeight: "600" }
1305
1352
  });
1306
1353
 
1354
+ // src/PermissionMatrix/permissionMatrixModel.ts
1355
+ function buildPermissionMatrix(roles, permissions, grants) {
1356
+ return roles.map((role) => {
1357
+ const held = new Set(grants[role.id] ?? []);
1358
+ const cells = permissions.map((permission) => ({ permission, granted: held.has(permission.id) }));
1359
+ return { role, cells, grantedCount: cells.filter((cell) => cell.granted).length };
1360
+ });
1361
+ }
1362
+ function grantStatusLabel(granted, t) {
1363
+ return granted ? t(ACCESS_ADMIN_I18N.permissionsGranted) : t(ACCESS_ADMIN_I18N.permissionsNotGranted);
1364
+ }
1365
+ function cellAccessibilityLabel(role, permission, granted, t) {
1366
+ return t(ACCESS_ADMIN_I18N.permissionsCellLabel, role.label, permission.label, grantStatusLabel(granted, t));
1367
+ }
1368
+ var SPACE_XS4 = 4;
1369
+ var SPACE_SM5 = 8;
1370
+ var SPACE_MD5 = 12;
1371
+ var RADIUS = 8;
1372
+ var MAX_WIDTH = 1200;
1373
+ var LABEL_COLUMN_WIDTH = 280;
1374
+ var ROLE_COLUMN_WIDTH = 140;
1375
+ var HEADING_SIZE = 18;
1376
+ var BODY_SIZE = 14;
1377
+ var LINE_HEIGHT = 20;
1378
+ var styles = reactNative.StyleSheet.create({
1379
+ root: { alignSelf: "center", gap: SPACE_MD5, maxWidth: MAX_WIDTH, width: "100%" },
1380
+ heading: { fontSize: HEADING_SIZE, fontWeight: "600", lineHeight: LINE_HEIGHT + SPACE_XS4 },
1381
+ body: { fontSize: BODY_SIZE, lineHeight: LINE_HEIGHT },
1382
+ strong: { fontSize: BODY_SIZE, fontWeight: "600", lineHeight: LINE_HEIGHT },
1383
+ list: { gap: SPACE_MD5 },
1384
+ card: { borderRadius: RADIUS, borderWidth: reactNative.StyleSheet.hairlineWidth, gap: SPACE_SM5, padding: SPACE_MD5 },
1385
+ stackRow: { flexDirection: "row", flexWrap: "wrap", gap: SPACE_SM5, justifyContent: "space-between" },
1386
+ stackLabel: { flexBasis: 0, flexGrow: 1, minWidth: 0 },
1387
+ tableRow: { borderBottomWidth: reactNative.StyleSheet.hairlineWidth, flexDirection: "row" },
1388
+ labelCell: { padding: SPACE_SM5, width: LABEL_COLUMN_WIDTH },
1389
+ roleCell: { padding: SPACE_SM5, width: ROLE_COLUMN_WIDTH }
1390
+ });
1391
+ var statusColor = (granted, palette) => granted ? palette.textPrimary : palette.textSecondary;
1392
+ var StackedMatrix = ({ columns, palette, t, testID }) => /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.list, children: columns.map(({ role, cells, grantedCount }) => /* @__PURE__ */ jsxRuntime.jsxs(
1393
+ reactNative.View,
1394
+ {
1395
+ style: [styles.card, { backgroundColor: palette.panelSurface, borderColor: palette.border }],
1396
+ testID: `${testID}-role-${role.id}`,
1397
+ children: [
1398
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.strong, { color: palette.textPrimary }], children: role.label }),
1399
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.body, { color: palette.textSecondary }], children: t(ACCESS_ADMIN_I18N.permissionsRoleCount, String(grantedCount), String(cells.length)) }),
1400
+ cells.map(({ permission, granted }) => /* @__PURE__ */ jsxRuntime.jsxs(
1401
+ reactNative.View,
1402
+ {
1403
+ accessibilityHint: t(ACCESS_ADMIN_I18N.permissionsReadOnlyHint),
1404
+ accessibilityLabel: cellAccessibilityLabel(role, permission, granted, t),
1405
+ accessible: true,
1406
+ style: styles.stackRow,
1407
+ testID: `${testID}-cell-${role.id}-${permission.id}`,
1408
+ children: [
1409
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.body, styles.stackLabel, { color: palette.textPrimary }], children: permission.label }),
1410
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [granted ? styles.strong : styles.body, { color: statusColor(granted, palette) }], children: grantStatusLabel(granted, t) })
1411
+ ]
1412
+ },
1413
+ permission.id
1414
+ ))
1415
+ ]
1416
+ },
1417
+ role.id
1418
+ )) });
1419
+ var TableMatrix = ({ columns, permissions, palette, t, testID }) => /* @__PURE__ */ jsxRuntime.jsx(reactNative.ScrollView, { horizontal: true, testID: `${testID}-table`, children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles.card, { backgroundColor: palette.panelSurface, borderColor: palette.border }], children: [
1420
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles.tableRow, { borderColor: palette.border }], children: [
1421
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.strong, styles.labelCell, { color: palette.textPrimary }], children: t(ACCESS_ADMIN_I18N.permissionsColPermission) }),
1422
+ columns.map(({ role }) => /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.strong, styles.roleCell, { color: palette.textPrimary }], children: role.label }, role.id))
1423
+ ] }),
1424
+ permissions.map((permission, index) => /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles.tableRow, { borderColor: palette.border }], children: [
1425
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.body, styles.labelCell, { color: palette.textPrimary }], children: permission.label }),
1426
+ columns.map(({ role, cells }) => {
1427
+ const granted = cells[index]?.granted === true;
1428
+ return /* @__PURE__ */ jsxRuntime.jsx(
1429
+ reactNative.Text,
1430
+ {
1431
+ accessibilityHint: t(ACCESS_ADMIN_I18N.permissionsReadOnlyHint),
1432
+ accessibilityLabel: cellAccessibilityLabel(role, permission, granted, t),
1433
+ style: [granted ? styles.strong : styles.body, styles.roleCell, { color: statusColor(granted, palette) }],
1434
+ testID: `${testID}-cell-${role.id}-${permission.id}`,
1435
+ children: grantStatusLabel(granted, t)
1436
+ },
1437
+ role.id
1438
+ );
1439
+ })
1440
+ ] }, permission.id))
1441
+ ] }) });
1442
+ var PermissionMatrix = ({
1443
+ roles,
1444
+ permissions,
1445
+ grants,
1446
+ t,
1447
+ testID = ACCESS_ADMIN_TEST_IDS.permissionMatrix
1448
+ }) => {
1449
+ const palette = useAccessAdminPalette();
1450
+ const { width } = reactNative.useWindowDimensions();
1451
+ const columns = react.useMemo(() => buildPermissionMatrix(roles, permissions, grants), [roles, permissions, grants]);
1452
+ const isWide = width >= uiLayout.LAYOUT_COLLAPSE_BREAKPOINT;
1453
+ const isEmpty = roles.length === 0 || permissions.length === 0;
1454
+ const partProps = { columns, permissions, palette, t, testID };
1455
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.root, testID, children: [
1456
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { accessibilityRole: "header", style: [styles.heading, { color: palette.textPrimary }], children: t(ACCESS_ADMIN_I18N.permissionsHeading) }),
1457
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.body, { color: palette.textSecondary }], children: t(ACCESS_ADMIN_I18N.permissionsNote) }),
1458
+ isEmpty ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.body, { color: palette.textSecondary }], testID: `${testID}-empty`, children: t(ACCESS_ADMIN_I18N.permissionsEmpty) }) : null,
1459
+ !isEmpty && isWide ? /* @__PURE__ */ jsxRuntime.jsx(TableMatrix, { ...partProps }) : null,
1460
+ !isEmpty && !isWide ? /* @__PURE__ */ jsxRuntime.jsx(StackedMatrix, { ...partProps }) : null
1461
+ ] });
1462
+ };
1463
+
1307
1464
  exports.ACCESS_ADMIN_I18N = ACCESS_ADMIN_I18N;
1308
1465
  exports.ACCESS_ADMIN_TEST_IDS = ACCESS_ADMIN_TEST_IDS;
1309
1466
  exports.AccessLinkPanel = AccessLinkPanel;
1310
1467
  exports.AccessResultTone = AccessResultTone;
1311
1468
  exports.AccountLinkPanel = AccountLinkPanel;
1469
+ exports.PermissionMatrix = PermissionMatrix;
1312
1470
  exports.UserManagementPanel = UserManagementPanel;
1313
1471
  exports.accountDisplayLabel = accountDisplayLabel;
1314
1472
  exports.activeLinksForSubject = activeLinksForSubject;
1315
1473
  exports.buildAccountOptions = buildAccountOptions;
1316
1474
  exports.buildCapabilityLinkMailto = buildCapabilityLinkMailto;
1475
+ exports.buildPermissionMatrix = buildPermissionMatrix;
1317
1476
  exports.capabilityExpiryDate = capabilityExpiryDate;
1318
1477
  exports.capabilityExpiryDays = capabilityExpiryDays;
1319
1478
  exports.daysUntilAnchor = daysUntilAnchor;