@dloizides/ui-access-admin 1.2.2 → 1.4.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/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,
@@ -730,7 +755,7 @@ function buildUserColumns({
730
755
  variant: "secondary"
731
756
  }
732
757
  ),
733
- /* @__PURE__ */ jsxRuntime.jsx(
758
+ onReset !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
734
759
  uiButtons.Button,
735
760
  {
736
761
  accessibilityHint: t(ACCESS_ADMIN_I18N.usersResetHint),
@@ -741,7 +766,7 @@ function buildUserColumns({
741
766
  testID: `${testIDPrefix}-reset-${user.id}`,
742
767
  variant: "secondary"
743
768
  }
744
- )
769
+ ) : null
745
770
  ] })
746
771
  }
747
772
  ];
@@ -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,
@@ -1075,6 +1122,8 @@ var ResetPasswordModal = ({
1075
1122
  ] })
1076
1123
  ] }) });
1077
1124
  };
1125
+ var MISSING_CREATE = "createUser is not provided";
1126
+ var MISSING_RESET = "resetPassword is not provided";
1078
1127
  function useUserManagement(api, t) {
1079
1128
  const [users, setUsers] = react.useState([]);
1080
1129
  const [loading, setLoading] = react.useState(true);
@@ -1116,7 +1165,7 @@ function useUserManagement(api, t) {
1116
1165
  );
1117
1166
  const create = react.useCallback(
1118
1167
  (input) => runWrite(
1119
- () => api.createUser(input),
1168
+ () => api.createUser ? api.createUser(input) : Promise.reject(new Error(MISSING_CREATE)),
1120
1169
  ACCESS_ADMIN_I18N.usersCreateSuccess,
1121
1170
  ACCESS_ADMIN_I18N.usersCreateFailed
1122
1171
  ),
@@ -1132,7 +1181,7 @@ function useUserManagement(api, t) {
1132
1181
  );
1133
1182
  const resetPassword = react.useCallback(
1134
1183
  (id, password) => runWrite(
1135
- () => api.resetPassword(id, password),
1184
+ () => api.resetPassword ? api.resetPassword(id, password) : Promise.reject(new Error(MISSING_RESET)),
1136
1185
  ACCESS_ADMIN_I18N.usersResetSuccess,
1137
1186
  ACCESS_ADMIN_I18N.usersResetFailed
1138
1187
  ),
@@ -1164,7 +1213,7 @@ var UserManagementPanel = ({
1164
1213
  testID = ACCESS_ADMIN_TEST_IDS.usersPanel
1165
1214
  }) => {
1166
1215
  const palette = useAccessAdminPalette();
1167
- const styles = react.useMemo(() => buildStyles3(palette), [palette]);
1216
+ const styles2 = react.useMemo(() => buildStyles3(palette), [palette]);
1168
1217
  const controller = useUserManagement(api, t);
1169
1218
  const [query, setQuery] = react.useState("");
1170
1219
  const [openModal, setOpenModal] = react.useState("none" /* None */);
@@ -1182,18 +1231,26 @@ var UserManagementPanel = ({
1182
1231
  setOpenModal("reset" /* Reset */);
1183
1232
  };
1184
1233
  const closeModal = () => setOpenModal("none" /* None */);
1234
+ const canCreate = api.createUser !== void 0;
1235
+ const canReset = api.resetPassword !== void 0;
1185
1236
  const columns = react.useMemo(
1186
- () => buildUserColumns({ t, tenants, testIDPrefix: testID, onEdit: openEdit, onReset: openReset }),
1187
- [t, tenants, testID]
1237
+ () => buildUserColumns({
1238
+ t,
1239
+ tenants,
1240
+ testIDPrefix: testID,
1241
+ onEdit: openEdit,
1242
+ onReset: canReset ? openReset : void 0
1243
+ }),
1244
+ [t, tenants, testID, canReset]
1188
1245
  );
1189
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.panel, testID, children: [
1246
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.panel, testID, children: [
1190
1247
  /* @__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) }),
1248
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.heading, children: t(ACCESS_ADMIN_I18N.usersHeading) }),
1249
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.note, children: t(ACCESS_ADMIN_I18N.usersNote) }),
1193
1250
  /* @__PURE__ */ jsxRuntime.jsx(
1194
1251
  uiTables.FilterBar,
1195
1252
  {
1196
- actions: /* @__PURE__ */ jsxRuntime.jsx(
1253
+ actions: canCreate ? /* @__PURE__ */ jsxRuntime.jsx(
1197
1254
  uiButtons.Button,
1198
1255
  {
1199
1256
  accessibilityHint: t(ACCESS_ADMIN_I18N.usersCreateHint),
@@ -1202,11 +1259,11 @@ var UserManagementPanel = ({
1202
1259
  onPress: () => setOpenModal("create" /* Create */),
1203
1260
  testID: `${testID}-create`
1204
1261
  }
1205
- ),
1262
+ ) : void 0,
1206
1263
  resultsCount: visibleUsers.length,
1207
1264
  resultsLabel: t(ACCESS_ADMIN_I18N.usersResultsLabel),
1208
1265
  testID: `${testID}-filter-bar`,
1209
- children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.search, children: /* @__PURE__ */ jsxRuntime.jsx(
1266
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.search, children: /* @__PURE__ */ jsxRuntime.jsx(
1210
1267
  uiForms.FormField,
1211
1268
  {
1212
1269
  accessibilityLabel: t(ACCESS_ADMIN_I18N.usersSearchLabel),
@@ -1221,8 +1278,8 @@ var UserManagementPanel = ({
1221
1278
  ) })
1222
1279
  }
1223
1280
  ),
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) }),
1281
+ controller.loadFailed ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.errorBox, testID: `${testID}-load-error`, children: [
1282
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.warning, children: t(ACCESS_ADMIN_I18N.usersLoadError) }),
1226
1283
  /* @__PURE__ */ jsxRuntime.jsx(
1227
1284
  uiButtons.Button,
1228
1285
  {
@@ -1247,7 +1304,7 @@ var UserManagementPanel = ({
1247
1304
  zebra: true
1248
1305
  }
1249
1306
  ),
1250
- /* @__PURE__ */ jsxRuntime.jsx(
1307
+ canCreate ? /* @__PURE__ */ jsxRuntime.jsx(
1251
1308
  CreateUserModal,
1252
1309
  {
1253
1310
  assignableRoles,
@@ -1259,7 +1316,7 @@ var UserManagementPanel = ({
1259
1316
  testIDPrefix: `${testID}-create-modal`,
1260
1317
  visible: openModal === "create" /* Create */
1261
1318
  }
1262
- ),
1319
+ ) : null,
1263
1320
  /* @__PURE__ */ jsxRuntime.jsx(
1264
1321
  EditUserModal,
1265
1322
  {
@@ -1274,7 +1331,7 @@ var UserManagementPanel = ({
1274
1331
  visible: openModal === "edit" /* Edit */
1275
1332
  }
1276
1333
  ),
1277
- /* @__PURE__ */ jsxRuntime.jsx(
1334
+ canReset ? /* @__PURE__ */ jsxRuntime.jsx(
1278
1335
  ResetPasswordModal,
1279
1336
  {
1280
1337
  onCancel: closeModal,
@@ -1284,7 +1341,7 @@ var UserManagementPanel = ({
1284
1341
  user: activeUser,
1285
1342
  visible: openModal === "reset" /* Reset */
1286
1343
  }
1287
- )
1344
+ ) : null
1288
1345
  ] });
1289
1346
  };
1290
1347
  var buildStyles3 = (c) => reactNative.StyleSheet.create({
@@ -1304,16 +1361,128 @@ var buildStyles3 = (c) => reactNative.StyleSheet.create({
1304
1361
  warning: { color: c.danger, fontSize: FONT_SM3, fontWeight: "600" }
1305
1362
  });
1306
1363
 
1364
+ // src/PermissionMatrix/permissionMatrixModel.ts
1365
+ function buildPermissionMatrix(roles, permissions, grants) {
1366
+ return roles.map((role) => {
1367
+ const held = new Set(grants[role.id] ?? []);
1368
+ const cells = permissions.map((permission) => ({ permission, granted: held.has(permission.id) }));
1369
+ return { role, cells, grantedCount: cells.filter((cell) => cell.granted).length };
1370
+ });
1371
+ }
1372
+ function grantStatusLabel(granted, t) {
1373
+ return granted ? t(ACCESS_ADMIN_I18N.permissionsGranted) : t(ACCESS_ADMIN_I18N.permissionsNotGranted);
1374
+ }
1375
+ function cellAccessibilityLabel(role, permission, granted, t) {
1376
+ return t(ACCESS_ADMIN_I18N.permissionsCellLabel, role.label, permission.label, grantStatusLabel(granted, t));
1377
+ }
1378
+ var SPACE_XS4 = 4;
1379
+ var SPACE_SM5 = 8;
1380
+ var SPACE_MD5 = 12;
1381
+ var RADIUS = 8;
1382
+ var MAX_WIDTH = 1200;
1383
+ var LABEL_COLUMN_WIDTH = 280;
1384
+ var ROLE_COLUMN_WIDTH = 140;
1385
+ var HEADING_SIZE = 18;
1386
+ var BODY_SIZE = 14;
1387
+ var LINE_HEIGHT = 20;
1388
+ var styles = reactNative.StyleSheet.create({
1389
+ root: { alignSelf: "center", gap: SPACE_MD5, maxWidth: MAX_WIDTH, width: "100%" },
1390
+ heading: { fontSize: HEADING_SIZE, fontWeight: "600", lineHeight: LINE_HEIGHT + SPACE_XS4 },
1391
+ body: { fontSize: BODY_SIZE, lineHeight: LINE_HEIGHT },
1392
+ strong: { fontSize: BODY_SIZE, fontWeight: "600", lineHeight: LINE_HEIGHT },
1393
+ list: { gap: SPACE_MD5 },
1394
+ card: { borderRadius: RADIUS, borderWidth: reactNative.StyleSheet.hairlineWidth, gap: SPACE_SM5, padding: SPACE_MD5 },
1395
+ stackRow: { flexDirection: "row", flexWrap: "wrap", gap: SPACE_SM5, justifyContent: "space-between" },
1396
+ stackLabel: { flexBasis: 0, flexGrow: 1, minWidth: 0 },
1397
+ tableRow: { borderBottomWidth: reactNative.StyleSheet.hairlineWidth, flexDirection: "row" },
1398
+ labelCell: { padding: SPACE_SM5, width: LABEL_COLUMN_WIDTH },
1399
+ roleCell: { padding: SPACE_SM5, width: ROLE_COLUMN_WIDTH }
1400
+ });
1401
+ var statusColor = (granted, palette) => granted ? palette.textPrimary : palette.textSecondary;
1402
+ var StackedMatrix = ({ columns, palette, t, testID }) => /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.list, children: columns.map(({ role, cells, grantedCount }) => /* @__PURE__ */ jsxRuntime.jsxs(
1403
+ reactNative.View,
1404
+ {
1405
+ style: [styles.card, { backgroundColor: palette.panelSurface, borderColor: palette.border }],
1406
+ testID: `${testID}-role-${role.id}`,
1407
+ children: [
1408
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.strong, { color: palette.textPrimary }], children: role.label }),
1409
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.body, { color: palette.textSecondary }], children: t(ACCESS_ADMIN_I18N.permissionsRoleCount, String(grantedCount), String(cells.length)) }),
1410
+ cells.map(({ permission, granted }) => /* @__PURE__ */ jsxRuntime.jsxs(
1411
+ reactNative.View,
1412
+ {
1413
+ accessibilityHint: t(ACCESS_ADMIN_I18N.permissionsReadOnlyHint),
1414
+ accessibilityLabel: cellAccessibilityLabel(role, permission, granted, t),
1415
+ accessible: true,
1416
+ style: styles.stackRow,
1417
+ testID: `${testID}-cell-${role.id}-${permission.id}`,
1418
+ children: [
1419
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.body, styles.stackLabel, { color: palette.textPrimary }], children: permission.label }),
1420
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [granted ? styles.strong : styles.body, { color: statusColor(granted, palette) }], children: grantStatusLabel(granted, t) })
1421
+ ]
1422
+ },
1423
+ permission.id
1424
+ ))
1425
+ ]
1426
+ },
1427
+ role.id
1428
+ )) });
1429
+ 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: [
1430
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles.tableRow, { borderColor: palette.border }], children: [
1431
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.strong, styles.labelCell, { color: palette.textPrimary }], children: t(ACCESS_ADMIN_I18N.permissionsColPermission) }),
1432
+ columns.map(({ role }) => /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.strong, styles.roleCell, { color: palette.textPrimary }], children: role.label }, role.id))
1433
+ ] }),
1434
+ permissions.map((permission, index) => /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles.tableRow, { borderColor: palette.border }], children: [
1435
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.body, styles.labelCell, { color: palette.textPrimary }], children: permission.label }),
1436
+ columns.map(({ role, cells }) => {
1437
+ const granted = cells[index]?.granted === true;
1438
+ return /* @__PURE__ */ jsxRuntime.jsx(
1439
+ reactNative.Text,
1440
+ {
1441
+ accessibilityHint: t(ACCESS_ADMIN_I18N.permissionsReadOnlyHint),
1442
+ accessibilityLabel: cellAccessibilityLabel(role, permission, granted, t),
1443
+ style: [granted ? styles.strong : styles.body, styles.roleCell, { color: statusColor(granted, palette) }],
1444
+ testID: `${testID}-cell-${role.id}-${permission.id}`,
1445
+ children: grantStatusLabel(granted, t)
1446
+ },
1447
+ role.id
1448
+ );
1449
+ })
1450
+ ] }, permission.id))
1451
+ ] }) });
1452
+ var PermissionMatrix = ({
1453
+ roles,
1454
+ permissions,
1455
+ grants,
1456
+ t,
1457
+ testID = ACCESS_ADMIN_TEST_IDS.permissionMatrix
1458
+ }) => {
1459
+ const palette = useAccessAdminPalette();
1460
+ const { width } = reactNative.useWindowDimensions();
1461
+ const columns = react.useMemo(() => buildPermissionMatrix(roles, permissions, grants), [roles, permissions, grants]);
1462
+ const isWide = width >= uiLayout.LAYOUT_COLLAPSE_BREAKPOINT;
1463
+ const isEmpty = roles.length === 0 || permissions.length === 0;
1464
+ const partProps = { columns, permissions, palette, t, testID };
1465
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.root, testID, children: [
1466
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { accessibilityRole: "header", style: [styles.heading, { color: palette.textPrimary }], children: t(ACCESS_ADMIN_I18N.permissionsHeading) }),
1467
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.body, { color: palette.textSecondary }], children: t(ACCESS_ADMIN_I18N.permissionsNote) }),
1468
+ isEmpty ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.body, { color: palette.textSecondary }], testID: `${testID}-empty`, children: t(ACCESS_ADMIN_I18N.permissionsEmpty) }) : null,
1469
+ !isEmpty && isWide ? /* @__PURE__ */ jsxRuntime.jsx(TableMatrix, { ...partProps }) : null,
1470
+ !isEmpty && !isWide ? /* @__PURE__ */ jsxRuntime.jsx(StackedMatrix, { ...partProps }) : null
1471
+ ] });
1472
+ };
1473
+
1307
1474
  exports.ACCESS_ADMIN_I18N = ACCESS_ADMIN_I18N;
1308
1475
  exports.ACCESS_ADMIN_TEST_IDS = ACCESS_ADMIN_TEST_IDS;
1309
1476
  exports.AccessLinkPanel = AccessLinkPanel;
1310
1477
  exports.AccessResultTone = AccessResultTone;
1311
1478
  exports.AccountLinkPanel = AccountLinkPanel;
1479
+ exports.PermissionMatrix = PermissionMatrix;
1312
1480
  exports.UserManagementPanel = UserManagementPanel;
1313
1481
  exports.accountDisplayLabel = accountDisplayLabel;
1314
1482
  exports.activeLinksForSubject = activeLinksForSubject;
1315
1483
  exports.buildAccountOptions = buildAccountOptions;
1316
1484
  exports.buildCapabilityLinkMailto = buildCapabilityLinkMailto;
1485
+ exports.buildPermissionMatrix = buildPermissionMatrix;
1317
1486
  exports.capabilityExpiryDate = capabilityExpiryDate;
1318
1487
  exports.capabilityExpiryDays = capabilityExpiryDays;
1319
1488
  exports.daysUntilAnchor = daysUntilAnchor;