@dloizides/ui-access-admin 1.0.1 → 1.2.2

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
@@ -7,6 +7,7 @@ var uiFeedback = require('@dloizides/ui-feedback');
7
7
  var uiLayout = require('@dloizides/ui-layout');
8
8
  var jsxRuntime = require('react/jsx-runtime');
9
9
  var uiForms = require('@dloizides/ui-forms');
10
+ var uiTables = require('@dloizides/ui-tables');
10
11
 
11
12
  // src/AccessLinkPanel/AccessLinkPanel.tsx
12
13
 
@@ -88,11 +89,92 @@ var ACCESS_ADMIN_I18N = {
88
89
  /** `{{p1}}` = subject display name. */
89
90
  accountUnlinkSuccess: "accessAdmin.account.unlinkSuccess",
90
91
  accountClose: "accessAdmin.account.close",
91
- accountCloseHint: "accessAdmin.account.closeHint"
92
+ accountCloseHint: "accessAdmin.account.closeHint",
93
+ // ── UserManagementPanel: framing + toolbar ──────────────────────────────────
94
+ /** Panel heading. */
95
+ usersHeading: "accessAdmin.users.heading",
96
+ /** Supporting line under the heading. */
97
+ usersNote: "accessAdmin.users.note",
98
+ /** Placeholder in the search box. */
99
+ usersSearchPlaceholder: "accessAdmin.users.searchPlaceholder",
100
+ /** Accessible name of the search box. */
101
+ usersSearchLabel: "accessAdmin.users.searchLabel",
102
+ usersSearchHint: "accessAdmin.users.searchHint",
103
+ /** Word shown next to the live result count in the FilterBar. */
104
+ usersResultsLabel: "accessAdmin.users.resultsLabel",
105
+ usersCreate: "accessAdmin.users.create",
106
+ usersCreateHint: "accessAdmin.users.createHint",
107
+ // ── UserManagementPanel: table columns ──────────────────────────────────────
108
+ usersColEmail: "accessAdmin.users.colEmail",
109
+ usersColDisplayName: "accessAdmin.users.colDisplayName",
110
+ usersColRoles: "accessAdmin.users.colRoles",
111
+ usersColTenant: "accessAdmin.users.colTenant",
112
+ usersColActive: "accessAdmin.users.colActive",
113
+ usersColCreated: "accessAdmin.users.colCreated",
114
+ usersColActions: "accessAdmin.users.colActions",
115
+ // ── UserManagementPanel: cell values ────────────────────────────────────────
116
+ usersActiveYes: "accessAdmin.users.activeYes",
117
+ usersActiveNo: "accessAdmin.users.activeNo",
118
+ /** Tenant cell for a user bound to no tenant (a platform-level account). */
119
+ usersTenantNone: "accessAdmin.users.tenantNone",
120
+ /** Roles cell for a user with no roles yet. */
121
+ usersNoRoles: "accessAdmin.users.noRoles",
122
+ /** Created cell when the API did not return a timestamp. */
123
+ usersCreatedUnknown: "accessAdmin.users.createdUnknown",
124
+ // ── UserManagementPanel: per-row actions ────────────────────────────────────
125
+ usersEdit: "accessAdmin.users.edit",
126
+ usersEditHint: "accessAdmin.users.editHint",
127
+ usersReset: "accessAdmin.users.reset",
128
+ usersResetHint: "accessAdmin.users.resetHint",
129
+ // ── UserManagementPanel: load states ────────────────────────────────────────
130
+ usersLoading: "accessAdmin.users.loading",
131
+ usersEmpty: "accessAdmin.users.empty",
132
+ /** Shown when the user list FAILED to load — never conflated with "there are none". */
133
+ usersLoadError: "accessAdmin.users.loadError",
134
+ usersRetry: "accessAdmin.users.retry",
135
+ usersRetryHint: "accessAdmin.users.retryHint",
136
+ // ── UserManagementPanel: modals — titles ────────────────────────────────────
137
+ usersCreateTitle: "accessAdmin.users.createTitle",
138
+ usersEditTitle: "accessAdmin.users.editTitle",
139
+ usersResetTitle: "accessAdmin.users.resetTitle",
140
+ // ── UserManagementPanel: modals — fields (shared across modals) ──────────────
141
+ usersFieldEmail: "accessAdmin.users.fieldEmail",
142
+ usersEmailPlaceholder: "accessAdmin.users.emailPlaceholder",
143
+ usersFieldPassword: "accessAdmin.users.fieldPassword",
144
+ usersPasswordPlaceholder: "accessAdmin.users.passwordPlaceholder",
145
+ usersFieldDisplayName: "accessAdmin.users.fieldDisplayName",
146
+ usersDisplayNamePlaceholder: "accessAdmin.users.displayNamePlaceholder",
147
+ usersFieldRole: "accessAdmin.users.fieldRole",
148
+ usersRolePlaceholder: "accessAdmin.users.rolePlaceholder",
149
+ usersRolePickerHint: "accessAdmin.users.rolePickerHint",
150
+ usersFieldTenant: "accessAdmin.users.fieldTenant",
151
+ usersTenantPlaceholder: "accessAdmin.users.tenantPlaceholder",
152
+ usersTenantPickerHint: "accessAdmin.users.tenantPickerHint",
153
+ usersFieldActive: "accessAdmin.users.fieldActive",
154
+ usersActiveToggleHint: "accessAdmin.users.activeToggleHint",
155
+ usersFieldNewPassword: "accessAdmin.users.fieldNewPassword",
156
+ usersNewPasswordPlaceholder: "accessAdmin.users.newPasswordPlaceholder",
157
+ // ── UserManagementPanel: modals — actions ───────────────────────────────────
158
+ usersSubmitCreate: "accessAdmin.users.submitCreate",
159
+ usersSubmitCreateHint: "accessAdmin.users.submitCreateHint",
160
+ usersSubmitSave: "accessAdmin.users.submitSave",
161
+ usersSubmitSaveHint: "accessAdmin.users.submitSaveHint",
162
+ usersSubmitReset: "accessAdmin.users.submitReset",
163
+ usersSubmitResetHint: "accessAdmin.users.submitResetHint",
164
+ usersCancel: "accessAdmin.users.cancel",
165
+ usersCancelHint: "accessAdmin.users.cancelHint",
166
+ // ── UserManagementPanel: outcome toasts ─────────────────────────────────────
167
+ usersCreateSuccess: "accessAdmin.users.createSuccess",
168
+ usersCreateFailed: "accessAdmin.users.createFailed",
169
+ usersUpdateSuccess: "accessAdmin.users.updateSuccess",
170
+ usersUpdateFailed: "accessAdmin.users.updateFailed",
171
+ usersResetSuccess: "accessAdmin.users.resetSuccess",
172
+ usersResetFailed: "accessAdmin.users.resetFailed"
92
173
  };
93
174
  var ACCESS_ADMIN_TEST_IDS = {
94
175
  linkPanel: "access-link-panel",
95
- accountPanel: "account-link-panel"
176
+ accountPanel: "account-link-panel",
177
+ usersPanel: "user-management-panel"
96
178
  };
97
179
 
98
180
  // src/expiry/expiryPolicy.ts
@@ -555,11 +637,679 @@ var buildStyles2 = (c) => reactNative.StyleSheet.create({
555
637
  }
556
638
  });
557
639
 
640
+ // src/UserManagementPanel/userManagementFormat.ts
641
+ var ROLE_SEPARATOR = ", ";
642
+ var NO_TENANT_VALUE = "";
643
+ function rolesLabel(roles, t) {
644
+ return roles.length > 0 ? roles.join(ROLE_SEPARATOR) : t(ACCESS_ADMIN_I18N.usersNoRoles);
645
+ }
646
+ function tenantLabel(tenantId, tenants, t) {
647
+ if (tenantId === null) return t(ACCESS_ADMIN_I18N.usersTenantNone);
648
+ const match = tenants.find((tenant) => tenant.id === tenantId);
649
+ return match !== void 0 ? match.label : tenantId;
650
+ }
651
+ function activeLabel(active, t) {
652
+ return active ? t(ACCESS_ADMIN_I18N.usersActiveYes) : t(ACCESS_ADMIN_I18N.usersActiveNo);
653
+ }
654
+ function createdLabel(createdAt, t) {
655
+ if (createdAt === void 0 || createdAt === "") return t(ACCESS_ADMIN_I18N.usersCreatedUnknown);
656
+ const parsed = new Date(createdAt);
657
+ if (Number.isNaN(parsed.getTime())) return t(ACCESS_ADMIN_I18N.usersCreatedUnknown);
658
+ return parsed.toISOString().slice(0, "YYYY-MM-DD".length);
659
+ }
660
+ function filterUsers(users, query) {
661
+ const needle = query.trim().toLowerCase();
662
+ if (needle === "") return users;
663
+ return users.filter((user) => {
664
+ const haystack = `${user.email} ${user.displayName} ${user.roles.join(" ")}`.toLowerCase();
665
+ return haystack.includes(needle);
666
+ });
667
+ }
668
+ function tenantOptions(tenants, t) {
669
+ const platform = { label: t(ACCESS_ADMIN_I18N.usersTenantNone), value: NO_TENANT_VALUE };
670
+ return [platform, ...tenants.map((tenant) => ({ label: tenant.label, value: tenant.id }))];
671
+ }
672
+ function roleOptions(assignableRoles) {
673
+ return assignableRoles.map((role) => ({ label: role, value: role }));
674
+ }
675
+ function tenantValueToId(value) {
676
+ return value === NO_TENANT_VALUE ? null : value;
677
+ }
678
+ var ACTIONS_GAP = 8;
679
+ function buildUserColumns({
680
+ t,
681
+ tenants,
682
+ testIDPrefix,
683
+ onEdit,
684
+ onReset
685
+ }) {
686
+ return [
687
+ {
688
+ key: "email",
689
+ header: t(ACCESS_ADMIN_I18N.usersColEmail),
690
+ render: (user) => user.email
691
+ },
692
+ {
693
+ key: "displayName",
694
+ header: t(ACCESS_ADMIN_I18N.usersColDisplayName),
695
+ render: (user) => user.displayName
696
+ },
697
+ {
698
+ key: "roles",
699
+ header: t(ACCESS_ADMIN_I18N.usersColRoles),
700
+ render: (user) => rolesLabel(user.roles, t)
701
+ },
702
+ {
703
+ key: "tenant",
704
+ header: t(ACCESS_ADMIN_I18N.usersColTenant),
705
+ render: (user) => tenantLabel(user.tenantId, tenants, t)
706
+ },
707
+ {
708
+ key: "active",
709
+ header: t(ACCESS_ADMIN_I18N.usersColActive),
710
+ render: (user) => activeLabel(user.active, t)
711
+ },
712
+ {
713
+ key: "created",
714
+ header: t(ACCESS_ADMIN_I18N.usersColCreated),
715
+ render: (user) => createdLabel(user.createdAt, t)
716
+ },
717
+ {
718
+ key: "actions",
719
+ header: t(ACCESS_ADMIN_I18N.usersColActions),
720
+ render: (user) => /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flexDirection: "row", flexWrap: "wrap", gap: ACTIONS_GAP }, children: [
721
+ /* @__PURE__ */ jsxRuntime.jsx(
722
+ uiButtons.Button,
723
+ {
724
+ accessibilityHint: t(ACCESS_ADMIN_I18N.usersEditHint),
725
+ accessibilityLabel: t(ACCESS_ADMIN_I18N.usersEdit),
726
+ label: t(ACCESS_ADMIN_I18N.usersEdit),
727
+ onPress: () => onEdit(user),
728
+ size: "sm",
729
+ testID: `${testIDPrefix}-edit-${user.id}`,
730
+ variant: "secondary"
731
+ }
732
+ ),
733
+ /* @__PURE__ */ jsxRuntime.jsx(
734
+ uiButtons.Button,
735
+ {
736
+ accessibilityHint: t(ACCESS_ADMIN_I18N.usersResetHint),
737
+ accessibilityLabel: t(ACCESS_ADMIN_I18N.usersReset),
738
+ label: t(ACCESS_ADMIN_I18N.usersReset),
739
+ onPress: () => onReset(user),
740
+ size: "sm",
741
+ testID: `${testIDPrefix}-reset-${user.id}`,
742
+ variant: "secondary"
743
+ }
744
+ )
745
+ ] })
746
+ }
747
+ ];
748
+ }
749
+ var PickerField = ({
750
+ testID,
751
+ label,
752
+ hint,
753
+ placeholder,
754
+ value,
755
+ options,
756
+ onChange
757
+ }) => {
758
+ const selected = options.find((option) => option.value === value);
759
+ const selectionLabel = selected !== void 0 ? selected.label : placeholder;
760
+ return /* @__PURE__ */ jsxRuntime.jsx(uiForms.Field, { label, children: /* @__PURE__ */ jsxRuntime.jsx(
761
+ uiLayout.ModalDropdown,
762
+ {
763
+ accessibilityHint: hint,
764
+ accessibilityLabel: `${label}: ${selectionLabel}`,
765
+ onChange,
766
+ options,
767
+ testID,
768
+ value,
769
+ variant: uiLayout.DropdownVariant.Modal
770
+ }
771
+ ) });
772
+ };
773
+ var SPACE_SM3 = 8;
774
+ var SPACE_MD3 = 12;
775
+ var modalFormStyles = reactNative.StyleSheet.create({
776
+ actions: { flexDirection: "row", flexWrap: "wrap", gap: SPACE_SM3, marginTop: SPACE_SM3 },
777
+ body: { gap: SPACE_MD3, padding: SPACE_MD3 }
778
+ });
779
+ var CreateUserModal = ({
780
+ visible,
781
+ isPlatformAdmin,
782
+ assignableRoles,
783
+ tenants,
784
+ t,
785
+ testIDPrefix,
786
+ onSubmit,
787
+ onCancel
788
+ }) => {
789
+ const [email, setEmail] = react.useState("");
790
+ const [password, setPassword] = react.useState("");
791
+ const [displayName, setDisplayName] = react.useState("");
792
+ const [role, setRole] = react.useState("");
793
+ const [tenantValue, setTenantValue] = react.useState(NO_TENANT_VALUE);
794
+ const [submitting, setSubmitting] = react.useState(false);
795
+ react.useEffect(() => {
796
+ if (!visible) return;
797
+ setEmail("");
798
+ setPassword("");
799
+ setDisplayName("");
800
+ setRole("");
801
+ setTenantValue(NO_TENANT_VALUE);
802
+ setSubmitting(false);
803
+ }, [visible]);
804
+ const canSubmit = !submitting && email.trim() !== "" && password !== "" && displayName.trim() !== "" && role !== "";
805
+ const handleSubmit = async () => {
806
+ setSubmitting(true);
807
+ const tenantId = isPlatformAdmin ? tenantValueToId(tenantValue) : null;
808
+ const ok = await onSubmit({ email: email.trim(), password, displayName: displayName.trim(), role, tenantId });
809
+ setSubmitting(false);
810
+ if (ok) onCancel();
811
+ };
812
+ return /* @__PURE__ */ jsxRuntime.jsx(uiLayout.ModalShell, { onCancel, title: t(ACCESS_ADMIN_I18N.usersCreateTitle), visible, children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: modalFormStyles.body, testID: testIDPrefix, children: [
813
+ /* @__PURE__ */ jsxRuntime.jsx(
814
+ uiForms.FormField,
815
+ {
816
+ autoCapitalize: "none",
817
+ keyboardType: "email-address",
818
+ label: t(ACCESS_ADMIN_I18N.usersFieldEmail),
819
+ onChangeText: setEmail,
820
+ placeholder: t(ACCESS_ADMIN_I18N.usersEmailPlaceholder),
821
+ required: true,
822
+ testID: `${testIDPrefix}-email`,
823
+ value: email
824
+ }
825
+ ),
826
+ /* @__PURE__ */ jsxRuntime.jsx(
827
+ uiForms.FormField,
828
+ {
829
+ label: t(ACCESS_ADMIN_I18N.usersFieldPassword),
830
+ onChangeText: setPassword,
831
+ placeholder: t(ACCESS_ADMIN_I18N.usersPasswordPlaceholder),
832
+ required: true,
833
+ secureTextEntry: true,
834
+ testID: `${testIDPrefix}-password`,
835
+ value: password
836
+ }
837
+ ),
838
+ /* @__PURE__ */ jsxRuntime.jsx(
839
+ uiForms.FormField,
840
+ {
841
+ label: t(ACCESS_ADMIN_I18N.usersFieldDisplayName),
842
+ onChangeText: setDisplayName,
843
+ placeholder: t(ACCESS_ADMIN_I18N.usersDisplayNamePlaceholder),
844
+ required: true,
845
+ testID: `${testIDPrefix}-display-name`,
846
+ value: displayName
847
+ }
848
+ ),
849
+ /* @__PURE__ */ jsxRuntime.jsx(
850
+ PickerField,
851
+ {
852
+ hint: t(ACCESS_ADMIN_I18N.usersRolePickerHint),
853
+ label: t(ACCESS_ADMIN_I18N.usersFieldRole),
854
+ onChange: setRole,
855
+ options: roleOptions(assignableRoles),
856
+ placeholder: t(ACCESS_ADMIN_I18N.usersRolePlaceholder),
857
+ testID: `${testIDPrefix}-role`,
858
+ value: role
859
+ }
860
+ ),
861
+ isPlatformAdmin ? /* @__PURE__ */ jsxRuntime.jsx(
862
+ PickerField,
863
+ {
864
+ hint: t(ACCESS_ADMIN_I18N.usersTenantPickerHint),
865
+ label: t(ACCESS_ADMIN_I18N.usersFieldTenant),
866
+ onChange: setTenantValue,
867
+ options: tenantOptions(tenants, t),
868
+ placeholder: t(ACCESS_ADMIN_I18N.usersTenantPlaceholder),
869
+ testID: `${testIDPrefix}-tenant`,
870
+ value: tenantValue
871
+ }
872
+ ) : null,
873
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: modalFormStyles.actions, children: [
874
+ /* @__PURE__ */ jsxRuntime.jsx(
875
+ uiButtons.Button,
876
+ {
877
+ accessibilityHint: t(ACCESS_ADMIN_I18N.usersSubmitCreateHint),
878
+ accessibilityLabel: t(ACCESS_ADMIN_I18N.usersSubmitCreate),
879
+ disabled: !canSubmit,
880
+ label: t(ACCESS_ADMIN_I18N.usersSubmitCreate),
881
+ loading: submitting,
882
+ onPress: handleSubmit,
883
+ testID: `${testIDPrefix}-submit`
884
+ }
885
+ ),
886
+ /* @__PURE__ */ jsxRuntime.jsx(
887
+ uiButtons.Button,
888
+ {
889
+ accessibilityHint: t(ACCESS_ADMIN_I18N.usersCancelHint),
890
+ accessibilityLabel: t(ACCESS_ADMIN_I18N.usersCancel),
891
+ disabled: submitting,
892
+ label: t(ACCESS_ADMIN_I18N.usersCancel),
893
+ onPress: onCancel,
894
+ testID: `${testIDPrefix}-cancel`,
895
+ variant: "secondary"
896
+ }
897
+ )
898
+ ] })
899
+ ] }) });
900
+ };
901
+ var EditUserModal = ({
902
+ visible,
903
+ user,
904
+ isPlatformAdmin,
905
+ assignableRoles,
906
+ tenants,
907
+ t,
908
+ testIDPrefix,
909
+ onSubmit,
910
+ onCancel
911
+ }) => {
912
+ const [displayName, setDisplayName] = react.useState("");
913
+ const [role, setRole] = react.useState("");
914
+ const [tenantValue, setTenantValue] = react.useState(NO_TENANT_VALUE);
915
+ const [active, setActive] = react.useState(true);
916
+ const [submitting, setSubmitting] = react.useState(false);
917
+ const userId = user?.id;
918
+ react.useEffect(() => {
919
+ if (!visible || user === null) return;
920
+ setDisplayName(user.displayName);
921
+ setRole(user.roles[0] ?? "");
922
+ setTenantValue(user.tenantId ?? NO_TENANT_VALUE);
923
+ setActive(user.active);
924
+ setSubmitting(false);
925
+ }, [visible, userId, user]);
926
+ const canSubmit = !submitting && displayName.trim() !== "" && role !== "";
927
+ const handleSubmit = async () => {
928
+ if (user === null) return;
929
+ setSubmitting(true);
930
+ const patch = {
931
+ displayName: displayName.trim(),
932
+ role,
933
+ active
934
+ };
935
+ if (isPlatformAdmin) patch.tenantId = tenantValueToId(tenantValue);
936
+ const ok = await onSubmit(user.id, patch);
937
+ setSubmitting(false);
938
+ if (ok) onCancel();
939
+ };
940
+ 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
+ /* @__PURE__ */ jsxRuntime.jsx(
942
+ uiForms.FormField,
943
+ {
944
+ label: t(ACCESS_ADMIN_I18N.usersFieldDisplayName),
945
+ onChangeText: setDisplayName,
946
+ placeholder: t(ACCESS_ADMIN_I18N.usersDisplayNamePlaceholder),
947
+ required: true,
948
+ testID: `${testIDPrefix}-display-name`,
949
+ value: displayName
950
+ }
951
+ ),
952
+ /* @__PURE__ */ jsxRuntime.jsx(
953
+ PickerField,
954
+ {
955
+ hint: t(ACCESS_ADMIN_I18N.usersRolePickerHint),
956
+ label: t(ACCESS_ADMIN_I18N.usersFieldRole),
957
+ onChange: setRole,
958
+ options: roleOptions(assignableRoles),
959
+ placeholder: t(ACCESS_ADMIN_I18N.usersRolePlaceholder),
960
+ testID: `${testIDPrefix}-role`,
961
+ value: role
962
+ }
963
+ ),
964
+ isPlatformAdmin ? /* @__PURE__ */ jsxRuntime.jsx(
965
+ PickerField,
966
+ {
967
+ hint: t(ACCESS_ADMIN_I18N.usersTenantPickerHint),
968
+ label: t(ACCESS_ADMIN_I18N.usersFieldTenant),
969
+ onChange: setTenantValue,
970
+ options: tenantOptions(tenants, t),
971
+ placeholder: t(ACCESS_ADMIN_I18N.usersTenantPlaceholder),
972
+ testID: `${testIDPrefix}-tenant`,
973
+ value: tenantValue
974
+ }
975
+ ) : null,
976
+ /* @__PURE__ */ jsxRuntime.jsx(
977
+ uiForms.FormSwitch,
978
+ {
979
+ accessibilityHint: t(ACCESS_ADMIN_I18N.usersActiveToggleHint),
980
+ label: t(ACCESS_ADMIN_I18N.usersFieldActive),
981
+ onValueChange: setActive,
982
+ testID: `${testIDPrefix}-active`,
983
+ value: active
984
+ }
985
+ ),
986
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: modalFormStyles.actions, children: [
987
+ /* @__PURE__ */ jsxRuntime.jsx(
988
+ uiButtons.Button,
989
+ {
990
+ accessibilityHint: t(ACCESS_ADMIN_I18N.usersSubmitSaveHint),
991
+ accessibilityLabel: t(ACCESS_ADMIN_I18N.usersSubmitSave),
992
+ disabled: !canSubmit,
993
+ label: t(ACCESS_ADMIN_I18N.usersSubmitSave),
994
+ loading: submitting,
995
+ onPress: handleSubmit,
996
+ testID: `${testIDPrefix}-submit`
997
+ }
998
+ ),
999
+ /* @__PURE__ */ jsxRuntime.jsx(
1000
+ uiButtons.Button,
1001
+ {
1002
+ accessibilityHint: t(ACCESS_ADMIN_I18N.usersCancelHint),
1003
+ accessibilityLabel: t(ACCESS_ADMIN_I18N.usersCancel),
1004
+ disabled: submitting,
1005
+ label: t(ACCESS_ADMIN_I18N.usersCancel),
1006
+ onPress: onCancel,
1007
+ testID: `${testIDPrefix}-cancel`,
1008
+ variant: "secondary"
1009
+ }
1010
+ )
1011
+ ] })
1012
+ ] }) });
1013
+ };
1014
+ var ResetPasswordModal = ({
1015
+ visible,
1016
+ user,
1017
+ t,
1018
+ testIDPrefix,
1019
+ onSubmit,
1020
+ onCancel
1021
+ }) => {
1022
+ const [password, setPassword] = react.useState("");
1023
+ const [submitting, setSubmitting] = react.useState(false);
1024
+ react.useEffect(() => {
1025
+ if (!visible) return;
1026
+ setPassword("");
1027
+ setSubmitting(false);
1028
+ }, [visible]);
1029
+ const canSubmit = !submitting && password !== "";
1030
+ const handleSubmit = async () => {
1031
+ if (user === null) return;
1032
+ setSubmitting(true);
1033
+ const ok = await onSubmit(user.id, password);
1034
+ setSubmitting(false);
1035
+ if (ok) onCancel();
1036
+ };
1037
+ return /* @__PURE__ */ jsxRuntime.jsx(uiLayout.ModalShell, { onCancel, title: t(ACCESS_ADMIN_I18N.usersResetTitle), visible, children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: modalFormStyles.body, testID: testIDPrefix, children: [
1038
+ /* @__PURE__ */ jsxRuntime.jsx(
1039
+ uiForms.FormField,
1040
+ {
1041
+ label: t(ACCESS_ADMIN_I18N.usersFieldNewPassword),
1042
+ onChangeText: setPassword,
1043
+ placeholder: t(ACCESS_ADMIN_I18N.usersNewPasswordPlaceholder),
1044
+ required: true,
1045
+ secureTextEntry: true,
1046
+ testID: `${testIDPrefix}-password`,
1047
+ value: password
1048
+ }
1049
+ ),
1050
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: modalFormStyles.actions, children: [
1051
+ /* @__PURE__ */ jsxRuntime.jsx(
1052
+ uiButtons.Button,
1053
+ {
1054
+ accessibilityHint: t(ACCESS_ADMIN_I18N.usersSubmitResetHint),
1055
+ accessibilityLabel: t(ACCESS_ADMIN_I18N.usersSubmitReset),
1056
+ disabled: !canSubmit,
1057
+ label: t(ACCESS_ADMIN_I18N.usersSubmitReset),
1058
+ loading: submitting,
1059
+ onPress: handleSubmit,
1060
+ testID: `${testIDPrefix}-submit`
1061
+ }
1062
+ ),
1063
+ /* @__PURE__ */ jsxRuntime.jsx(
1064
+ uiButtons.Button,
1065
+ {
1066
+ accessibilityHint: t(ACCESS_ADMIN_I18N.usersCancelHint),
1067
+ accessibilityLabel: t(ACCESS_ADMIN_I18N.usersCancel),
1068
+ disabled: submitting,
1069
+ label: t(ACCESS_ADMIN_I18N.usersCancel),
1070
+ onPress: onCancel,
1071
+ testID: `${testIDPrefix}-cancel`,
1072
+ variant: "secondary"
1073
+ }
1074
+ )
1075
+ ] })
1076
+ ] }) });
1077
+ };
1078
+ function useUserManagement(api, t) {
1079
+ const [users, setUsers] = react.useState([]);
1080
+ const [loading, setLoading] = react.useState(true);
1081
+ const [loadFailed, setLoadFailed] = react.useState(false);
1082
+ const bus = react.useMemo(() => uiFeedback.createToastBus(), []);
1083
+ const mountedRef = react.useRef(true);
1084
+ react.useEffect(() => {
1085
+ return () => {
1086
+ mountedRef.current = false;
1087
+ };
1088
+ }, []);
1089
+ const reload = react.useCallback(() => {
1090
+ setLoading(true);
1091
+ setLoadFailed(false);
1092
+ void api.listUsers().then((next) => {
1093
+ if (!mountedRef.current) return;
1094
+ setUsers(next);
1095
+ setLoading(false);
1096
+ }).catch(() => {
1097
+ if (!mountedRef.current) return;
1098
+ setLoadFailed(true);
1099
+ setLoading(false);
1100
+ });
1101
+ }, [api]);
1102
+ react.useEffect(() => reload(), [reload]);
1103
+ const runWrite = react.useCallback(
1104
+ async (write, successKey, failureKey) => {
1105
+ try {
1106
+ await write();
1107
+ bus.success(t(successKey));
1108
+ reload();
1109
+ return true;
1110
+ } catch {
1111
+ bus.error(t(failureKey));
1112
+ return false;
1113
+ }
1114
+ },
1115
+ [bus, reload, t]
1116
+ );
1117
+ const create = react.useCallback(
1118
+ (input) => runWrite(
1119
+ () => api.createUser(input),
1120
+ ACCESS_ADMIN_I18N.usersCreateSuccess,
1121
+ ACCESS_ADMIN_I18N.usersCreateFailed
1122
+ ),
1123
+ [api, runWrite]
1124
+ );
1125
+ const update = react.useCallback(
1126
+ (id, patch) => runWrite(
1127
+ () => api.updateUser(id, patch),
1128
+ ACCESS_ADMIN_I18N.usersUpdateSuccess,
1129
+ ACCESS_ADMIN_I18N.usersUpdateFailed
1130
+ ),
1131
+ [api, runWrite]
1132
+ );
1133
+ const resetPassword = react.useCallback(
1134
+ (id, password) => runWrite(
1135
+ () => api.resetPassword(id, password),
1136
+ ACCESS_ADMIN_I18N.usersResetSuccess,
1137
+ ACCESS_ADMIN_I18N.usersResetFailed
1138
+ ),
1139
+ [api, runWrite]
1140
+ );
1141
+ return {
1142
+ users,
1143
+ loading,
1144
+ loadFailed,
1145
+ reload,
1146
+ create,
1147
+ update,
1148
+ resetPassword,
1149
+ toastSubscribe: bus.subscribe
1150
+ };
1151
+ }
1152
+ var SPACE_XS3 = 4;
1153
+ var SPACE_SM4 = 8;
1154
+ var SPACE_MD4 = 12;
1155
+ var FONT_SM3 = 12;
1156
+ var FONT_MD3 = 13;
1157
+ var SEARCH_MIN_WIDTH = 220;
1158
+ var UserManagementPanel = ({
1159
+ api,
1160
+ isPlatformAdmin,
1161
+ assignableRoles,
1162
+ tenants = [],
1163
+ t,
1164
+ testID = ACCESS_ADMIN_TEST_IDS.usersPanel
1165
+ }) => {
1166
+ const palette = useAccessAdminPalette();
1167
+ const styles = react.useMemo(() => buildStyles3(palette), [palette]);
1168
+ const controller = useUserManagement(api, t);
1169
+ const [query, setQuery] = react.useState("");
1170
+ const [openModal, setOpenModal] = react.useState("none" /* None */);
1171
+ const [activeUser, setActiveUser] = react.useState(null);
1172
+ const visibleUsers = react.useMemo(
1173
+ () => filterUsers(controller.users, query),
1174
+ [controller.users, query]
1175
+ );
1176
+ const openEdit = (user) => {
1177
+ setActiveUser(user);
1178
+ setOpenModal("edit" /* Edit */);
1179
+ };
1180
+ const openReset = (user) => {
1181
+ setActiveUser(user);
1182
+ setOpenModal("reset" /* Reset */);
1183
+ };
1184
+ const closeModal = () => setOpenModal("none" /* None */);
1185
+ const columns = react.useMemo(
1186
+ () => buildUserColumns({ t, tenants, testIDPrefix: testID, onEdit: openEdit, onReset: openReset }),
1187
+ [t, tenants, testID]
1188
+ );
1189
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.panel, testID, children: [
1190
+ /* @__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) }),
1193
+ /* @__PURE__ */ jsxRuntime.jsx(
1194
+ uiTables.FilterBar,
1195
+ {
1196
+ actions: /* @__PURE__ */ jsxRuntime.jsx(
1197
+ uiButtons.Button,
1198
+ {
1199
+ accessibilityHint: t(ACCESS_ADMIN_I18N.usersCreateHint),
1200
+ accessibilityLabel: t(ACCESS_ADMIN_I18N.usersCreate),
1201
+ label: t(ACCESS_ADMIN_I18N.usersCreate),
1202
+ onPress: () => setOpenModal("create" /* Create */),
1203
+ testID: `${testID}-create`
1204
+ }
1205
+ ),
1206
+ resultsCount: visibleUsers.length,
1207
+ resultsLabel: t(ACCESS_ADMIN_I18N.usersResultsLabel),
1208
+ testID: `${testID}-filter-bar`,
1209
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.search, children: /* @__PURE__ */ jsxRuntime.jsx(
1210
+ uiForms.FormField,
1211
+ {
1212
+ accessibilityLabel: t(ACCESS_ADMIN_I18N.usersSearchLabel),
1213
+ accessibilityHint: t(ACCESS_ADMIN_I18N.usersSearchHint),
1214
+ autoCapitalize: "none",
1215
+ label: t(ACCESS_ADMIN_I18N.usersSearchLabel),
1216
+ onChangeText: setQuery,
1217
+ placeholder: t(ACCESS_ADMIN_I18N.usersSearchPlaceholder),
1218
+ testID: `${testID}-search`,
1219
+ value: query
1220
+ }
1221
+ ) })
1222
+ }
1223
+ ),
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) }),
1226
+ /* @__PURE__ */ jsxRuntime.jsx(
1227
+ uiButtons.Button,
1228
+ {
1229
+ accessibilityHint: t(ACCESS_ADMIN_I18N.usersRetryHint),
1230
+ accessibilityLabel: t(ACCESS_ADMIN_I18N.usersRetry),
1231
+ label: t(ACCESS_ADMIN_I18N.usersRetry),
1232
+ onPress: controller.reload,
1233
+ testID: `${testID}-retry`,
1234
+ variant: "secondary"
1235
+ }
1236
+ )
1237
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(
1238
+ uiTables.DataTable,
1239
+ {
1240
+ columns,
1241
+ emptyLabel: t(ACCESS_ADMIN_I18N.usersEmpty),
1242
+ keyExtractor: (user) => user.id,
1243
+ loading: controller.loading,
1244
+ loadingLabel: t(ACCESS_ADMIN_I18N.usersLoading),
1245
+ rows: visibleUsers,
1246
+ testID: `${testID}-table`,
1247
+ zebra: true
1248
+ }
1249
+ ),
1250
+ /* @__PURE__ */ jsxRuntime.jsx(
1251
+ CreateUserModal,
1252
+ {
1253
+ assignableRoles,
1254
+ isPlatformAdmin,
1255
+ onCancel: closeModal,
1256
+ onSubmit: controller.create,
1257
+ t,
1258
+ tenants,
1259
+ testIDPrefix: `${testID}-create-modal`,
1260
+ visible: openModal === "create" /* Create */
1261
+ }
1262
+ ),
1263
+ /* @__PURE__ */ jsxRuntime.jsx(
1264
+ EditUserModal,
1265
+ {
1266
+ assignableRoles,
1267
+ isPlatformAdmin,
1268
+ onCancel: closeModal,
1269
+ onSubmit: controller.update,
1270
+ t,
1271
+ tenants,
1272
+ testIDPrefix: `${testID}-edit-modal`,
1273
+ user: activeUser,
1274
+ visible: openModal === "edit" /* Edit */
1275
+ }
1276
+ ),
1277
+ /* @__PURE__ */ jsxRuntime.jsx(
1278
+ ResetPasswordModal,
1279
+ {
1280
+ onCancel: closeModal,
1281
+ onSubmit: controller.resetPassword,
1282
+ t,
1283
+ testIDPrefix: `${testID}-reset-modal`,
1284
+ user: activeUser,
1285
+ visible: openModal === "reset" /* Reset */
1286
+ }
1287
+ )
1288
+ ] });
1289
+ };
1290
+ var buildStyles3 = (c) => reactNative.StyleSheet.create({
1291
+ errorBox: {
1292
+ backgroundColor: c.panelSurface,
1293
+ borderColor: c.border,
1294
+ borderRadius: SPACE_SM4,
1295
+ borderWidth: 1,
1296
+ gap: SPACE_SM4,
1297
+ marginTop: SPACE_SM4,
1298
+ padding: SPACE_MD4
1299
+ },
1300
+ heading: { color: c.textPrimary, fontSize: FONT_MD3, fontWeight: "700" },
1301
+ note: { color: c.textSecondary, fontSize: FONT_SM3 },
1302
+ panel: { gap: SPACE_XS3 },
1303
+ search: { minWidth: SEARCH_MIN_WIDTH },
1304
+ warning: { color: c.danger, fontSize: FONT_SM3, fontWeight: "600" }
1305
+ });
1306
+
558
1307
  exports.ACCESS_ADMIN_I18N = ACCESS_ADMIN_I18N;
559
1308
  exports.ACCESS_ADMIN_TEST_IDS = ACCESS_ADMIN_TEST_IDS;
560
1309
  exports.AccessLinkPanel = AccessLinkPanel;
561
1310
  exports.AccessResultTone = AccessResultTone;
562
1311
  exports.AccountLinkPanel = AccountLinkPanel;
1312
+ exports.UserManagementPanel = UserManagementPanel;
563
1313
  exports.accountDisplayLabel = accountDisplayLabel;
564
1314
  exports.activeLinksForSubject = activeLinksForSubject;
565
1315
  exports.buildAccountOptions = buildAccountOptions;