@burdenoff/microfe-admin 2026.805.1 → 2026.829.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.
@@ -1,215 +1,214 @@
1
1
  import { useToast as e } from "./Toast.js";
2
2
  import { Button as t, Card as n, CardContent as r, CardHeader as i, CardTitle as a, Input as o, Separator as s } from "./ui.js";
3
3
  import { formatDate as c } from "../utils/formatters.js";
4
- import { tWithFallback as l } from "../utils/i18n.js";
5
- import { ConfirmDialog as u } from "./ConfirmDialog.js";
6
- import { useAddTenantMemberMutation as d, useListTenantMembersQuery as f, useRemoveTenantMemberMutation as p, useUpdateTenantMemberRoleMutation as m } from "../../generated/global-operations.js";
4
+ import { ConfirmDialog as l } from "./ConfirmDialog.js";
5
+ import { useAddTenantMemberMutation as u, useListTenantMembersQuery as d, useRemoveTenantMemberMutation as f, useUpdateTenantMemberRoleMutation as p } from "../../generated/global-operations.js";
6
+ import { useTr as m } from "../hooks/useTr.js";
7
7
  import { TenantMemberRole as h } from "../../generated/global-types.js";
8
8
  import { useState as g } from "react";
9
- import { Loader2 as _, Trash2 as v, UserPlus as ee, Users as y } from "lucide-react";
10
- import { Fragment as b, jsx as x, jsxs as S } from "react/jsx-runtime";
11
- import { useI18n as C } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
9
+ import { Loader2 as _, Trash2 as v, UserPlus as y, Users as b } from "lucide-react";
10
+ import { Fragment as x, jsx as S, jsxs as C } from "react/jsx-runtime";
12
11
  //#region src/admin/components/TenantMembersPanel.tsx
13
12
  var w = {
14
13
  userId: "",
15
14
  role: h.Member
16
15
  }, T = 100;
17
16
  function E({ tenantId: E, tenantType: D, canManage: O }) {
18
- let { t: k } = C(), A = (e, t, n) => l(k, e, t, n), j = e(), [M, N] = g(w), [P, F] = g(null), [I, L] = g(T), R = D?.toUpperCase() === "SHARED", { data: z, loading: B, error: V, refetch: H } = f({
17
+ let k = m(), A = e(), [j, M] = g(w), [N, P] = g(null), [F, I] = g(T), L = D?.toUpperCase() === "SHARED", { data: R, loading: z, error: B, refetch: V } = d({
19
18
  variables: {
20
19
  tenantId: E,
21
- limit: I,
20
+ limit: F,
22
21
  offset: 0
23
22
  },
24
- skip: !E || R,
23
+ skip: !E || L,
25
24
  fetchPolicy: "cache-and-network"
26
- }), [U, { loading: W }] = d(), [G, { loading: K }] = p(), [q] = m();
27
- if (R) return /* @__PURE__ */ S(n, { children: [/* @__PURE__ */ x(i, { children: /* @__PURE__ */ S(a, {
25
+ }), [H, { loading: U }] = u(), [W, { loading: G }] = f(), [K] = p();
26
+ if (L) return /* @__PURE__ */ C(n, { children: [/* @__PURE__ */ S(i, { children: /* @__PURE__ */ C(a, {
28
27
  className: "flex items-center gap-2 text-lg",
29
- children: [/* @__PURE__ */ x(y, { className: "h-4 w-4" }), A("admin.tenantMembers.title", "Tenant Members")]
30
- }) }), /* @__PURE__ */ x(r, { children: /* @__PURE__ */ x("p", {
28
+ children: [/* @__PURE__ */ S(b, { className: "h-4 w-4" }), k("admin.tenantMembers.title", "Tenant Members")]
29
+ }) }), /* @__PURE__ */ S(r, { children: /* @__PURE__ */ S("p", {
31
30
  className: "text-sm text-text-secondary",
32
- children: A("admin.tenantMembers.sharedImplicit", "This is the shared tenant. Every authenticated user has implicit access — no explicit membership rows are stored.")
31
+ children: k("admin.tenantMembers.sharedImplicit", "This is the shared tenant. Every authenticated user has implicit access — no explicit membership rows are stored.")
33
32
  }) })] });
34
- let J = async (e) => {
33
+ let q = async (e) => {
35
34
  e.preventDefault();
36
- let t = M.userId.trim();
35
+ let t = j.userId.trim();
37
36
  if (t) try {
38
- await U({ variables: { input: {
37
+ await H({ variables: { input: {
39
38
  tenantId: E,
40
39
  userId: t,
41
- role: M.role
42
- } } }), j.success(A("admin.tenantMembers.toast.addedTitle", "Member added"), A("admin.tenantMembers.toast.addedDescription", "User {{userId}} can now access this tenant.", { userId: t })), N(w), await H();
40
+ role: j.role
41
+ } } }), A.success(k("admin.tenantMembers.toast.addedTitle", "Member added"), k("admin.tenantMembers.toast.addedDescription", "User {{userId}} can now access this tenant.", { userId: t })), M(w), await V();
43
42
  } catch (e) {
44
- j.error(A("admin.tenantMembers.toast.errorTitle", "Error"), e instanceof Error ? e.message : A("admin.tenantMembers.toast.addFailed", "Failed to add tenant member."));
43
+ A.error(k("admin.tenantMembers.toast.errorTitle", "Error"), e instanceof Error ? e.message : k("admin.tenantMembers.toast.addFailed", "Failed to add tenant member."));
45
44
  }
46
- }, Y = async () => {
47
- if (!P) return;
48
- let { userId: e } = P;
45
+ }, J = async () => {
46
+ if (!N) return;
47
+ let { userId: e } = N;
49
48
  try {
50
- await G({ variables: {
49
+ await W({ variables: {
51
50
  tenantId: E,
52
51
  userId: e
53
- } }), j.success(A("admin.tenantMembers.toast.removedTitle", "Member removed"), A("admin.tenantMembers.toast.removedDescription", "User {{userId}} no longer has access.", { userId: e })), F(null), await H();
52
+ } }), A.success(k("admin.tenantMembers.toast.removedTitle", "Member removed"), k("admin.tenantMembers.toast.removedDescription", "User {{userId}} no longer has access.", { userId: e })), P(null), await V();
54
53
  } catch (e) {
55
- j.error(A("admin.tenantMembers.toast.errorTitle", "Error"), e instanceof Error ? e.message : A("admin.tenantMembers.toast.removeFailed", "Failed to remove tenant member."));
54
+ A.error(k("admin.tenantMembers.toast.errorTitle", "Error"), e instanceof Error ? e.message : k("admin.tenantMembers.toast.removeFailed", "Failed to remove tenant member."));
56
55
  }
57
- }, X = async (e, t) => {
56
+ }, Y = async (e, t) => {
58
57
  try {
59
- await q({ variables: {
58
+ await K({ variables: {
60
59
  tenantId: E,
61
60
  userId: e,
62
61
  role: t
63
- } }), await H();
62
+ } }), await V();
64
63
  } catch (e) {
65
- j.error(A("admin.tenantMembers.toast.errorTitle", "Error"), e instanceof Error ? e.message : A("admin.tenantMembers.toast.updateRoleFailed", "Failed to update role."));
64
+ A.error(k("admin.tenantMembers.toast.errorTitle", "Error"), e instanceof Error ? e.message : k("admin.tenantMembers.toast.updateRoleFailed", "Failed to update role."));
66
65
  }
67
- }, Z = z?.tenantMembers?.items ?? [], Q = z?.tenantMembers?.total ?? 0, te = z?.tenantMembers?.hasMore ?? !1, $ = B && Z.length > 0;
68
- return /* @__PURE__ */ S(b, { children: [/* @__PURE__ */ S(n, { children: [/* @__PURE__ */ x(i, { children: /* @__PURE__ */ S(a, {
66
+ }, X = R?.tenantMembers?.items ?? [], Z = R?.tenantMembers?.total ?? 0, Q = R?.tenantMembers?.hasMore ?? !1, $ = z && X.length > 0;
67
+ return /* @__PURE__ */ C(x, { children: [/* @__PURE__ */ C(n, { children: [/* @__PURE__ */ S(i, { children: /* @__PURE__ */ C(a, {
69
68
  className: "flex items-center gap-2 text-lg",
70
69
  children: [
71
- /* @__PURE__ */ x(y, { className: "h-4 w-4" }),
72
- A("admin.tenantMembers.title", "Tenant Members"),
73
- Q > 0 && /* @__PURE__ */ S("span", {
70
+ /* @__PURE__ */ S(b, { className: "h-4 w-4" }),
71
+ k("admin.tenantMembers.title", "Tenant Members"),
72
+ Z > 0 && /* @__PURE__ */ C("span", {
74
73
  className: "text-sm text-text-secondary",
75
74
  children: [
76
75
  "(",
77
- Q,
76
+ Z,
78
77
  ")"
79
78
  ]
80
79
  })
81
80
  ]
82
- }) }), /* @__PURE__ */ S(r, {
81
+ }) }), /* @__PURE__ */ C(r, {
83
82
  className: "space-y-4",
84
83
  children: [
85
- /* @__PURE__ */ x("p", {
84
+ /* @__PURE__ */ S("p", {
86
85
  className: "text-sm text-text-secondary",
87
- children: A("admin.tenantMembers.description", "Grant a user explicit access to this dedicated tenant. They will be able to create organizations under it and see it in their tenant switcher.")
86
+ children: k("admin.tenantMembers.description", "Grant a user explicit access to this dedicated tenant. They will be able to create organizations under it and see it in their tenant switcher.")
88
87
  }),
89
- O && /* @__PURE__ */ S(b, { children: [/* @__PURE__ */ S("form", {
90
- onSubmit: J,
88
+ O && /* @__PURE__ */ C(x, { children: [/* @__PURE__ */ C("form", {
89
+ onSubmit: q,
91
90
  className: "flex flex-wrap items-end gap-2",
92
91
  children: [
93
- /* @__PURE__ */ S("div", {
92
+ /* @__PURE__ */ C("div", {
94
93
  className: "flex-1 min-w-[240px]",
95
- children: [/* @__PURE__ */ x("label", {
94
+ children: [/* @__PURE__ */ S("label", {
96
95
  className: "mb-1 block text-xs font-medium text-text-secondary",
97
- children: A("admin.tenantMembers.form.userIdLabel", "User ID")
98
- }), /* @__PURE__ */ x(o, {
99
- value: M.userId,
100
- onChange: (e) => N({
101
- ...M,
96
+ children: k("admin.tenantMembers.form.userIdLabel", "User ID")
97
+ }), /* @__PURE__ */ S(o, {
98
+ value: j.userId,
99
+ onChange: (e) => M({
100
+ ...j,
102
101
  userId: e.target.value
103
102
  }),
104
- placeholder: A("admin.tenantMembers.form.userIdPlaceholder", "UUID of the user to grant access"),
103
+ placeholder: k("admin.tenantMembers.form.userIdPlaceholder", "UUID of the user to grant access"),
105
104
  required: !0,
106
- disabled: W
105
+ disabled: U
107
106
  })]
108
107
  }),
109
- /* @__PURE__ */ S("div", {
108
+ /* @__PURE__ */ C("div", {
110
109
  className: "min-w-[140px]",
111
- children: [/* @__PURE__ */ x("label", {
110
+ children: [/* @__PURE__ */ S("label", {
112
111
  className: "mb-1 block text-xs font-medium text-text-secondary",
113
- children: A("admin.tenantMembers.form.roleLabel", "Role")
114
- }), /* @__PURE__ */ S("select", {
115
- value: M.role,
116
- onChange: (e) => N({
117
- ...M,
112
+ children: k("admin.tenantMembers.form.roleLabel", "Role")
113
+ }), /* @__PURE__ */ C("select", {
114
+ value: j.role,
115
+ onChange: (e) => M({
116
+ ...j,
118
117
  role: e.target.value
119
118
  }),
120
- disabled: W,
119
+ disabled: U,
121
120
  className: "h-9 w-full rounded-md border border-border-default bg-bg-surface px-3 text-sm",
122
- children: [/* @__PURE__ */ x("option", {
121
+ children: [/* @__PURE__ */ S("option", {
123
122
  value: h.Member,
124
- children: A("admin.tenantMembers.role.member", "Member")
125
- }), /* @__PURE__ */ x("option", {
123
+ children: k("admin.tenantMembers.role.member", "Member")
124
+ }), /* @__PURE__ */ S("option", {
126
125
  value: h.Admin,
127
- children: A("admin.tenantMembers.role.admin", "Admin")
126
+ children: k("admin.tenantMembers.role.admin", "Admin")
128
127
  })]
129
128
  })]
130
129
  }),
131
- /* @__PURE__ */ S(t, {
130
+ /* @__PURE__ */ C(t, {
132
131
  type: "submit",
133
- disabled: W || !M.userId.trim(),
134
- children: [W ? /* @__PURE__ */ x(_, { className: "mr-2 h-4 w-4 animate-spin" }) : /* @__PURE__ */ x(ee, { className: "mr-2 h-4 w-4" }), A("admin.tenantMembers.form.submit", "Grant access")]
132
+ disabled: U || !j.userId.trim(),
133
+ children: [U ? /* @__PURE__ */ S(_, { className: "mr-2 h-4 w-4 animate-spin" }) : /* @__PURE__ */ S(y, { className: "mr-2 h-4 w-4" }), k("admin.tenantMembers.form.submit", "Grant access")]
135
134
  })
136
135
  ]
137
- }), /* @__PURE__ */ x(s, {})] }),
138
- B && Z.length === 0 ? /* @__PURE__ */ S("div", {
136
+ }), /* @__PURE__ */ S(s, {})] }),
137
+ z && X.length === 0 ? /* @__PURE__ */ C("div", {
139
138
  className: "flex h-24 items-center justify-center text-text-secondary",
140
- children: [/* @__PURE__ */ x(_, { className: "mr-2 h-4 w-4 animate-spin" }), A("admin.tenantMembers.loading", "Loading members...")]
141
- }) : V ? /* @__PURE__ */ x("div", {
139
+ children: [/* @__PURE__ */ S(_, { className: "mr-2 h-4 w-4 animate-spin" }), k("admin.tenantMembers.loading", "Loading members...")]
140
+ }) : B ? /* @__PURE__ */ S("div", {
142
141
  className: "rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-3 text-sm text-status-error-text",
143
- children: V.message
144
- }) : Z.length === 0 ? /* @__PURE__ */ x("p", {
142
+ children: B.message
143
+ }) : X.length === 0 ? /* @__PURE__ */ S("p", {
145
144
  className: "text-sm text-text-secondary",
146
- children: A("admin.tenantMembers.empty", "No explicit members yet. Grant access above to add one.")
147
- }) : /* @__PURE__ */ S("div", {
145
+ children: k("admin.tenantMembers.empty", "No explicit members yet. Grant access above to add one.")
146
+ }) : /* @__PURE__ */ C("div", {
148
147
  className: "space-y-2",
149
- children: [Z.map((e) => /* @__PURE__ */ S("div", {
148
+ children: [X.map((e) => /* @__PURE__ */ C("div", {
150
149
  className: "flex flex-wrap items-center justify-between gap-2 rounded-lg border border-border-subtle p-3",
151
- children: [/* @__PURE__ */ S("div", {
150
+ children: [/* @__PURE__ */ C("div", {
152
151
  className: "min-w-0 flex-1",
153
- children: [/* @__PURE__ */ x("p", {
152
+ children: [/* @__PURE__ */ S("p", {
154
153
  className: "truncate font-mono text-xs text-text-primary",
155
154
  children: e.userId
156
- }), /* @__PURE__ */ x("p", {
155
+ }), /* @__PURE__ */ S("p", {
157
156
  className: "text-xs text-text-secondary",
158
- children: A("admin.tenantMembers.addedAt", "Added {{when}} by {{by}}", {
157
+ children: k("admin.tenantMembers.addedAt", "Added {{when}} by {{by}}", {
159
158
  when: c(e.createdAt),
160
159
  by: e.addedBy
161
160
  })
162
161
  })]
163
- }), /* @__PURE__ */ S("div", {
162
+ }), /* @__PURE__ */ C("div", {
164
163
  className: "flex items-center gap-2",
165
- children: [/* @__PURE__ */ S("select", {
164
+ children: [/* @__PURE__ */ C("select", {
166
165
  value: e.role,
167
- onChange: (t) => X(e.userId, t.target.value),
166
+ onChange: (t) => Y(e.userId, t.target.value),
168
167
  disabled: !O,
169
168
  className: "h-8 rounded-md border border-border-default bg-bg-surface px-2 text-xs",
170
- children: [/* @__PURE__ */ x("option", {
169
+ children: [/* @__PURE__ */ S("option", {
171
170
  value: h.Member,
172
- children: A("admin.tenantMembers.role.member", "Member")
173
- }), /* @__PURE__ */ x("option", {
171
+ children: k("admin.tenantMembers.role.member", "Member")
172
+ }), /* @__PURE__ */ S("option", {
174
173
  value: h.Admin,
175
- children: A("admin.tenantMembers.role.admin", "Admin")
174
+ children: k("admin.tenantMembers.role.admin", "Admin")
176
175
  })]
177
- }), O && /* @__PURE__ */ x(t, {
176
+ }), O && /* @__PURE__ */ S(t, {
178
177
  variant: "ghost",
179
178
  size: "sm",
180
- onClick: () => F({ userId: e.userId }),
181
- disabled: K,
182
- "aria-label": A("admin.tenantMembers.remove", "Remove"),
183
- children: /* @__PURE__ */ x(v, { className: "h-4 w-4 text-status-error-text" })
179
+ onClick: () => P({ userId: e.userId }),
180
+ disabled: G,
181
+ "aria-label": k("admin.tenantMembers.remove", "Remove"),
182
+ children: /* @__PURE__ */ S(v, { className: "h-4 w-4 text-status-error-text" })
184
183
  })]
185
184
  })]
186
- }, e.id)), te && /* @__PURE__ */ S("div", {
185
+ }, e.id)), Q && /* @__PURE__ */ C("div", {
187
186
  className: "flex items-center justify-between gap-2 pt-2",
188
- children: [/* @__PURE__ */ x("span", {
187
+ children: [/* @__PURE__ */ S("span", {
189
188
  className: "text-xs text-text-secondary",
190
- children: A("admin.tenantMembers.shownOfTotal", "Showing {{shown}} of {{total}}", {
191
- shown: Z.length,
192
- total: Q
189
+ children: k("admin.tenantMembers.shownOfTotal", "Showing {{shown}} of {{total}}", {
190
+ shown: X.length,
191
+ total: Z
193
192
  })
194
- }), /* @__PURE__ */ S(t, {
193
+ }), /* @__PURE__ */ C(t, {
195
194
  variant: "outline",
196
195
  size: "sm",
197
- onClick: () => L((e) => e + T),
196
+ onClick: () => I((e) => e + T),
198
197
  disabled: $,
199
- children: [$ ? /* @__PURE__ */ x(_, { className: "mr-2 h-4 w-4 animate-spin" }) : null, A("admin.tenantMembers.loadMore", "Load more")]
198
+ children: [$ ? /* @__PURE__ */ S(_, { className: "mr-2 h-4 w-4 animate-spin" }) : null, k("admin.tenantMembers.loadMore", "Load more")]
200
199
  })]
201
200
  })]
202
201
  })
203
202
  ]
204
- })] }), /* @__PURE__ */ x(u, {
205
- open: !!P,
206
- title: A("admin.tenantMembers.confirmRemove.title", "Revoke tenant access?"),
207
- description: A("admin.tenantMembers.confirmRemove.description", "User {{userId}} will no longer be able to create organizations in this tenant. Existing organization and workspace memberships are not affected.", { userId: P?.userId ?? "" }),
208
- confirmLabel: A("admin.tenantMembers.confirmRemove.confirm", "Revoke"),
203
+ })] }), /* @__PURE__ */ S(l, {
204
+ open: !!N,
205
+ title: k("admin.tenantMembers.confirmRemove.title", "Revoke tenant access?"),
206
+ description: k("admin.tenantMembers.confirmRemove.description", "User {{userId}} will no longer be able to create organizations in this tenant. Existing organization and workspace memberships are not affected.", { userId: N?.userId ?? "" }),
207
+ confirmLabel: k("admin.tenantMembers.confirmRemove.confirm", "Revoke"),
209
208
  variant: "destructive",
210
- loading: K,
211
- onConfirm: Y,
212
- onClose: () => F(null)
209
+ loading: G,
210
+ onConfirm: J,
211
+ onClose: () => P(null)
213
212
  })] });
214
213
  }
215
214
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"TenantMembersPanel.js","names":[],"sources":["../../../src/admin/components/TenantMembersPanel.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Loader2, Trash2, UserPlus, Users } from 'lucide-react';\nimport {\n useListTenantMembersQuery,\n useAddTenantMemberMutation,\n useRemoveTenantMemberMutation,\n useUpdateTenantMemberRoleMutation,\n type TenantMemberFieldsFragment,\n} from '../../generated/global-operations';\nimport { TenantMemberRole } from '../../generated/global-types';\nimport { Card, CardHeader, CardTitle, CardContent, Button, Input, Separator } from './ui';\nimport { useToast } from './Toast';\nimport { ConfirmDialog } from './ConfirmDialog';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../utils/i18n';\nimport { formatDate } from '../utils/formatters';\n\ninterface TenantMembersPanelProps {\n tenantId: string;\n tenantType: string;\n canManage: boolean;\n}\n\nconst EMPTY_ADD_FORM = { userId: '', role: TenantMemberRole.Member };\nconst PAGE_SIZE = 100;\n\n/**\n * Tenant Members panel (BOFF-2652) — super_admin UI to grant non-super-admin\n * users explicit access to a DEDICATED tenant. SHARED-tenant access is\n * implicit, so the panel is hidden for SHARED tenants.\n */\nexport function TenantMembersPanel({ tenantId, tenantType, canManage }: TenantMembersPanelProps) {\n const { t } = useI18n();\n const tr = (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params);\n const toast = useToast();\n\n const [addForm, setAddForm] = useState(EMPTY_ADD_FORM);\n const [removeTarget, setRemoveTarget] = useState<{ userId: string } | null>(null);\n // Pagination: each \"Load more\" click widens the window by PAGE_SIZE rows;\n // Apollo re-fetches the bigger page rather than appending in JS so we\n // don't need a custom merge type policy. (BOFF-2652 review)\n const [pageSize, setPageSize] = useState(PAGE_SIZE);\n\n const isShared = tenantType?.toUpperCase() === 'SHARED';\n\n const { data, loading, error, refetch } = useListTenantMembersQuery({\n variables: { tenantId, limit: pageSize, offset: 0 },\n skip: !tenantId || isShared,\n fetchPolicy: 'cache-and-network',\n });\n\n const [addMember, { loading: adding }] = useAddTenantMemberMutation();\n const [removeMember, { loading: removing }] = useRemoveTenantMemberMutation();\n const [updateRole] = useUpdateTenantMemberRoleMutation();\n\n if (isShared) {\n return (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-lg\">\n <Users className=\"h-4 w-4\" />\n {tr('admin.tenantMembers.title', 'Tenant Members')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.tenantMembers.sharedImplicit',\n 'This is the shared tenant. Every authenticated user has implicit access — no explicit membership rows are stored.'\n )}\n </p>\n </CardContent>\n </Card>\n );\n }\n\n const handleAdd = async (e: React.FormEvent) => {\n e.preventDefault();\n const userId = addForm.userId.trim();\n if (!userId) return;\n try {\n await addMember({ variables: { input: { tenantId, userId, role: addForm.role } } });\n toast.success(\n tr('admin.tenantMembers.toast.addedTitle', 'Member added'),\n tr(\n 'admin.tenantMembers.toast.addedDescription',\n 'User {{userId}} can now access this tenant.',\n {\n userId,\n }\n )\n );\n setAddForm(EMPTY_ADD_FORM);\n await refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantMembers.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantMembers.toast.addFailed', 'Failed to add tenant member.')\n );\n }\n };\n\n const handleRemove = async () => {\n if (!removeTarget) return;\n const { userId } = removeTarget;\n try {\n await removeMember({ variables: { tenantId, userId } });\n toast.success(\n tr('admin.tenantMembers.toast.removedTitle', 'Member removed'),\n tr(\n 'admin.tenantMembers.toast.removedDescription',\n 'User {{userId}} no longer has access.',\n {\n userId,\n }\n )\n );\n setRemoveTarget(null);\n await refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantMembers.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantMembers.toast.removeFailed', 'Failed to remove tenant member.')\n );\n }\n };\n\n const handleRoleChange = async (userId: string, role: TenantMemberRole) => {\n try {\n await updateRole({ variables: { tenantId, userId, role } });\n await refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantMembers.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantMembers.toast.updateRoleFailed', 'Failed to update role.')\n );\n }\n };\n\n const members = data?.tenantMembers?.items ?? [];\n const total = data?.tenantMembers?.total ?? 0;\n const hasMore = data?.tenantMembers?.hasMore ?? false;\n const loadingMore = loading && members.length > 0;\n\n return (\n <>\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-lg\">\n <Users className=\"h-4 w-4\" />\n {tr('admin.tenantMembers.title', 'Tenant Members')}\n {total > 0 && <span className=\"text-sm text-text-secondary\">({total})</span>}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.tenantMembers.description',\n 'Grant a user explicit access to this dedicated tenant. They will be able to create organizations under it and see it in their tenant switcher.'\n )}\n </p>\n\n {canManage && (\n <>\n <form onSubmit={handleAdd} className=\"flex flex-wrap items-end gap-2\">\n <div className=\"flex-1 min-w-[240px]\">\n <label className=\"mb-1 block text-xs font-medium text-text-secondary\">\n {tr('admin.tenantMembers.form.userIdLabel', 'User ID')}\n </label>\n <Input\n value={addForm.userId}\n onChange={(e) => setAddForm({ ...addForm, userId: e.target.value })}\n placeholder={tr(\n 'admin.tenantMembers.form.userIdPlaceholder',\n 'UUID of the user to grant access'\n )}\n required\n disabled={adding}\n />\n </div>\n <div className=\"min-w-[140px]\">\n <label className=\"mb-1 block text-xs font-medium text-text-secondary\">\n {tr('admin.tenantMembers.form.roleLabel', 'Role')}\n </label>\n <select\n value={addForm.role}\n onChange={(e) =>\n setAddForm({ ...addForm, role: e.target.value as TenantMemberRole })\n }\n disabled={adding}\n className=\"h-9 w-full rounded-md border border-border-default bg-bg-surface px-3 text-sm\"\n >\n <option value={TenantMemberRole.Member}>\n {tr('admin.tenantMembers.role.member', 'Member')}\n </option>\n <option value={TenantMemberRole.Admin}>\n {tr('admin.tenantMembers.role.admin', 'Admin')}\n </option>\n </select>\n </div>\n <Button type=\"submit\" disabled={adding || !addForm.userId.trim()}>\n {adding ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <UserPlus className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantMembers.form.submit', 'Grant access')}\n </Button>\n </form>\n <Separator />\n </>\n )}\n\n {loading && members.length === 0 ? (\n <div className=\"flex h-24 items-center justify-center text-text-secondary\">\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n {tr('admin.tenantMembers.loading', 'Loading members...')}\n </div>\n ) : error ? (\n <div className=\"rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-3 text-sm text-status-error-text\">\n {error.message}\n </div>\n ) : members.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.tenantMembers.empty',\n 'No explicit members yet. Grant access above to add one.'\n )}\n </p>\n ) : (\n <div className=\"space-y-2\">\n {members.map((m: TenantMemberFieldsFragment) => (\n <div\n key={m.id}\n className=\"flex flex-wrap items-center justify-between gap-2 rounded-lg border border-border-subtle p-3\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate font-mono text-xs text-text-primary\">{m.userId}</p>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantMembers.addedAt', 'Added {{when}} by {{by}}', {\n when: formatDate(m.createdAt),\n by: m.addedBy,\n })}\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <select\n value={m.role}\n onChange={(e) =>\n handleRoleChange(m.userId, e.target.value as TenantMemberRole)\n }\n disabled={!canManage}\n className=\"h-8 rounded-md border border-border-default bg-bg-surface px-2 text-xs\"\n >\n <option value={TenantMemberRole.Member}>\n {tr('admin.tenantMembers.role.member', 'Member')}\n </option>\n <option value={TenantMemberRole.Admin}>\n {tr('admin.tenantMembers.role.admin', 'Admin')}\n </option>\n </select>\n {canManage && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setRemoveTarget({ userId: m.userId })}\n disabled={removing}\n aria-label={tr('admin.tenantMembers.remove', 'Remove')}\n >\n <Trash2 className=\"h-4 w-4 text-status-error-text\" />\n </Button>\n )}\n </div>\n </div>\n ))}\n {hasMore && (\n <div className=\"flex items-center justify-between gap-2 pt-2\">\n <span className=\"text-xs text-text-secondary\">\n {tr('admin.tenantMembers.shownOfTotal', 'Showing {{shown}} of {{total}}', {\n shown: members.length,\n total,\n })}\n </span>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setPageSize((prev) => prev + PAGE_SIZE)}\n disabled={loadingMore}\n >\n {loadingMore ? <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" /> : null}\n {tr('admin.tenantMembers.loadMore', 'Load more')}\n </Button>\n </div>\n )}\n </div>\n )}\n </CardContent>\n </Card>\n\n <ConfirmDialog\n open={Boolean(removeTarget)}\n title={tr('admin.tenantMembers.confirmRemove.title', 'Revoke tenant access?')}\n description={tr(\n 'admin.tenantMembers.confirmRemove.description',\n 'User {{userId}} will no longer be able to create organizations in this tenant. Existing organization and workspace memberships are not affected.',\n { userId: removeTarget?.userId ?? '' }\n )}\n confirmLabel={tr('admin.tenantMembers.confirmRemove.confirm', 'Revoke')}\n variant=\"destructive\"\n loading={removing}\n onConfirm={handleRemove}\n onClose={() => setRemoveTarget(null)}\n />\n </>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TenantMembersPanel.js","names":[],"sources":["../../../src/admin/components/TenantMembersPanel.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Loader2, Trash2, UserPlus, Users } from 'lucide-react';\nimport {\n useListTenantMembersQuery,\n useAddTenantMemberMutation,\n useRemoveTenantMemberMutation,\n useUpdateTenantMemberRoleMutation,\n type TenantMemberFieldsFragment,\n} from '../../generated/global-operations';\nimport { TenantMemberRole } from '../../generated/global-types';\nimport { Card, CardHeader, CardTitle, CardContent, Button, Input, Separator } from './ui';\nimport { useToast } from './Toast';\nimport { ConfirmDialog } from './ConfirmDialog';\nimport { useTr } from '../hooks/useTr';\nimport { formatDate } from '../utils/formatters';\n\ninterface TenantMembersPanelProps {\n tenantId: string;\n tenantType: string;\n canManage: boolean;\n}\n\nconst EMPTY_ADD_FORM = { userId: '', role: TenantMemberRole.Member };\nconst PAGE_SIZE = 100;\n\n/**\n * Tenant Members panel (BOFF-2652) — super_admin UI to grant non-super-admin\n * users explicit access to a DEDICATED tenant. SHARED-tenant access is\n * implicit, so the panel is hidden for SHARED tenants.\n */\nexport function TenantMembersPanel({ tenantId, tenantType, canManage }: TenantMembersPanelProps) {\n const tr = useTr();\n const toast = useToast();\n\n const [addForm, setAddForm] = useState(EMPTY_ADD_FORM);\n const [removeTarget, setRemoveTarget] = useState<{ userId: string } | null>(null);\n // Pagination: each \"Load more\" click widens the window by PAGE_SIZE rows;\n // Apollo re-fetches the bigger page rather than appending in JS so we\n // don't need a custom merge type policy. (BOFF-2652 review)\n const [pageSize, setPageSize] = useState(PAGE_SIZE);\n\n const isShared = tenantType?.toUpperCase() === 'SHARED';\n\n const { data, loading, error, refetch } = useListTenantMembersQuery({\n variables: { tenantId, limit: pageSize, offset: 0 },\n skip: !tenantId || isShared,\n fetchPolicy: 'cache-and-network',\n });\n\n const [addMember, { loading: adding }] = useAddTenantMemberMutation();\n const [removeMember, { loading: removing }] = useRemoveTenantMemberMutation();\n const [updateRole] = useUpdateTenantMemberRoleMutation();\n\n if (isShared) {\n return (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-lg\">\n <Users className=\"h-4 w-4\" />\n {tr('admin.tenantMembers.title', 'Tenant Members')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.tenantMembers.sharedImplicit',\n 'This is the shared tenant. Every authenticated user has implicit access — no explicit membership rows are stored.'\n )}\n </p>\n </CardContent>\n </Card>\n );\n }\n\n const handleAdd = async (e: React.FormEvent) => {\n e.preventDefault();\n const userId = addForm.userId.trim();\n if (!userId) return;\n try {\n await addMember({ variables: { input: { tenantId, userId, role: addForm.role } } });\n toast.success(\n tr('admin.tenantMembers.toast.addedTitle', 'Member added'),\n tr(\n 'admin.tenantMembers.toast.addedDescription',\n 'User {{userId}} can now access this tenant.',\n {\n userId,\n }\n )\n );\n setAddForm(EMPTY_ADD_FORM);\n await refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantMembers.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantMembers.toast.addFailed', 'Failed to add tenant member.')\n );\n }\n };\n\n const handleRemove = async () => {\n if (!removeTarget) return;\n const { userId } = removeTarget;\n try {\n await removeMember({ variables: { tenantId, userId } });\n toast.success(\n tr('admin.tenantMembers.toast.removedTitle', 'Member removed'),\n tr(\n 'admin.tenantMembers.toast.removedDescription',\n 'User {{userId}} no longer has access.',\n {\n userId,\n }\n )\n );\n setRemoveTarget(null);\n await refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantMembers.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantMembers.toast.removeFailed', 'Failed to remove tenant member.')\n );\n }\n };\n\n const handleRoleChange = async (userId: string, role: TenantMemberRole) => {\n try {\n await updateRole({ variables: { tenantId, userId, role } });\n await refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantMembers.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantMembers.toast.updateRoleFailed', 'Failed to update role.')\n );\n }\n };\n\n const members = data?.tenantMembers?.items ?? [];\n const total = data?.tenantMembers?.total ?? 0;\n const hasMore = data?.tenantMembers?.hasMore ?? false;\n const loadingMore = loading && members.length > 0;\n\n return (\n <>\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-lg\">\n <Users className=\"h-4 w-4\" />\n {tr('admin.tenantMembers.title', 'Tenant Members')}\n {total > 0 && <span className=\"text-sm text-text-secondary\">({total})</span>}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.tenantMembers.description',\n 'Grant a user explicit access to this dedicated tenant. They will be able to create organizations under it and see it in their tenant switcher.'\n )}\n </p>\n\n {canManage && (\n <>\n <form onSubmit={handleAdd} className=\"flex flex-wrap items-end gap-2\">\n <div className=\"flex-1 min-w-[240px]\">\n <label className=\"mb-1 block text-xs font-medium text-text-secondary\">\n {tr('admin.tenantMembers.form.userIdLabel', 'User ID')}\n </label>\n <Input\n value={addForm.userId}\n onChange={(e) => setAddForm({ ...addForm, userId: e.target.value })}\n placeholder={tr(\n 'admin.tenantMembers.form.userIdPlaceholder',\n 'UUID of the user to grant access'\n )}\n required\n disabled={adding}\n />\n </div>\n <div className=\"min-w-[140px]\">\n <label className=\"mb-1 block text-xs font-medium text-text-secondary\">\n {tr('admin.tenantMembers.form.roleLabel', 'Role')}\n </label>\n <select\n value={addForm.role}\n onChange={(e) =>\n setAddForm({ ...addForm, role: e.target.value as TenantMemberRole })\n }\n disabled={adding}\n className=\"h-9 w-full rounded-md border border-border-default bg-bg-surface px-3 text-sm\"\n >\n <option value={TenantMemberRole.Member}>\n {tr('admin.tenantMembers.role.member', 'Member')}\n </option>\n <option value={TenantMemberRole.Admin}>\n {tr('admin.tenantMembers.role.admin', 'Admin')}\n </option>\n </select>\n </div>\n <Button type=\"submit\" disabled={adding || !addForm.userId.trim()}>\n {adding ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <UserPlus className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantMembers.form.submit', 'Grant access')}\n </Button>\n </form>\n <Separator />\n </>\n )}\n\n {loading && members.length === 0 ? (\n <div className=\"flex h-24 items-center justify-center text-text-secondary\">\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n {tr('admin.tenantMembers.loading', 'Loading members...')}\n </div>\n ) : error ? (\n <div className=\"rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-3 text-sm text-status-error-text\">\n {error.message}\n </div>\n ) : members.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.tenantMembers.empty',\n 'No explicit members yet. Grant access above to add one.'\n )}\n </p>\n ) : (\n <div className=\"space-y-2\">\n {members.map((m: TenantMemberFieldsFragment) => (\n <div\n key={m.id}\n className=\"flex flex-wrap items-center justify-between gap-2 rounded-lg border border-border-subtle p-3\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate font-mono text-xs text-text-primary\">{m.userId}</p>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantMembers.addedAt', 'Added {{when}} by {{by}}', {\n when: formatDate(m.createdAt),\n by: m.addedBy,\n })}\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <select\n value={m.role}\n onChange={(e) =>\n handleRoleChange(m.userId, e.target.value as TenantMemberRole)\n }\n disabled={!canManage}\n className=\"h-8 rounded-md border border-border-default bg-bg-surface px-2 text-xs\"\n >\n <option value={TenantMemberRole.Member}>\n {tr('admin.tenantMembers.role.member', 'Member')}\n </option>\n <option value={TenantMemberRole.Admin}>\n {tr('admin.tenantMembers.role.admin', 'Admin')}\n </option>\n </select>\n {canManage && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setRemoveTarget({ userId: m.userId })}\n disabled={removing}\n aria-label={tr('admin.tenantMembers.remove', 'Remove')}\n >\n <Trash2 className=\"h-4 w-4 text-status-error-text\" />\n </Button>\n )}\n </div>\n </div>\n ))}\n {hasMore && (\n <div className=\"flex items-center justify-between gap-2 pt-2\">\n <span className=\"text-xs text-text-secondary\">\n {tr('admin.tenantMembers.shownOfTotal', 'Showing {{shown}} of {{total}}', {\n shown: members.length,\n total,\n })}\n </span>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setPageSize((prev) => prev + PAGE_SIZE)}\n disabled={loadingMore}\n >\n {loadingMore ? <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" /> : null}\n {tr('admin.tenantMembers.loadMore', 'Load more')}\n </Button>\n </div>\n )}\n </div>\n )}\n </CardContent>\n </Card>\n\n <ConfirmDialog\n open={Boolean(removeTarget)}\n title={tr('admin.tenantMembers.confirmRemove.title', 'Revoke tenant access?')}\n description={tr(\n 'admin.tenantMembers.confirmRemove.description',\n 'User {{userId}} will no longer be able to create organizations in this tenant. Existing organization and workspace memberships are not affected.',\n { userId: removeTarget?.userId ?? '' }\n )}\n confirmLabel={tr('admin.tenantMembers.confirmRemove.confirm', 'Revoke')}\n variant=\"destructive\"\n loading={removing}\n onConfirm={handleRemove}\n onClose={() => setRemoveTarget(null)}\n />\n </>\n );\n}\n"],"mappings":";;;;;;;;;;;AAsBA,IAAM,IAAiB;CAAE,QAAQ;CAAI,MAAM,EAAiB;CAAQ,EAC9D,IAAY;AAOlB,SAAgB,EAAmB,EAAE,aAAU,eAAY,gBAAsC;CAC/F,IAAM,IAAK,GAAO,EACZ,IAAQ,GAAU,EAElB,CAAC,GAAS,KAAc,EAAS,EAAe,EAChD,CAAC,GAAc,KAAmB,EAAoC,KAAK,EAI3E,CAAC,GAAU,KAAe,EAAS,EAAU,EAE7C,IAAW,GAAY,aAAa,KAAK,UAEzC,EAAE,SAAM,YAAS,UAAO,eAAY,EAA0B;EAClE,WAAW;GAAE;GAAU,OAAO;GAAU,QAAQ;GAAG;EACnD,MAAM,CAAC,KAAY;EACnB,aAAa;EACd,CAAC,EAEI,CAAC,GAAW,EAAE,SAAS,OAAY,GAA4B,EAC/D,CAAC,GAAc,EAAE,SAAS,OAAc,GAA+B,EACvE,CAAC,KAAc,GAAmC;AAExD,KAAI,EACF,QACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;EAAW,WAAU;YAArB,CACE,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA,EAC5B,EAAG,6BAA6B,iBAAiB,CACxC;KACD,CAAA,EACb,kBAAC,GAAD,EAAA,UACE,kBAAC,KAAD;EAAG,WAAU;YACV,EACC,sCACA,oHACD;EACC,CAAA,EACQ,CAAA,CACT,EAAA,CAAA;CAIX,IAAM,IAAY,OAAO,MAAuB;AAC9C,IAAE,gBAAgB;EAClB,IAAM,IAAS,EAAQ,OAAO,MAAM;AAC/B,QACL,KAAI;AAaF,GAZA,MAAM,EAAU,EAAE,WAAW,EAAE,OAAO;IAAE;IAAU;IAAQ,MAAM,EAAQ;IAAM,EAAE,EAAE,CAAC,EACnF,EAAM,QACJ,EAAG,wCAAwC,eAAe,EAC1D,EACE,8CACA,+CACA,EACE,WACD,CACF,CACF,EACD,EAAW,EAAe,EAC1B,MAAM,GAAS;WACR,GAAK;AACZ,KAAM,MACJ,EAAG,wCAAwC,QAAQ,EACnD,aAAe,QACX,EAAI,UACJ,EAAG,uCAAuC,+BAA+B,CAC9E;;IAIC,IAAe,YAAY;AAC/B,MAAI,CAAC,EAAc;EACnB,IAAM,EAAE,cAAW;AACnB,MAAI;AAaF,GAZA,MAAM,EAAa,EAAE,WAAW;IAAE;IAAU;IAAQ,EAAE,CAAC,EACvD,EAAM,QACJ,EAAG,0CAA0C,iBAAiB,EAC9D,EACE,gDACA,yCACA,EACE,WACD,CACF,CACF,EACD,EAAgB,KAAK,EACrB,MAAM,GAAS;WACR,GAAK;AACZ,KAAM,MACJ,EAAG,wCAAwC,QAAQ,EACnD,aAAe,QACX,EAAI,UACJ,EAAG,0CAA0C,kCAAkC,CACpF;;IAIC,IAAmB,OAAO,GAAgB,MAA2B;AACzE,MAAI;AAEF,GADA,MAAM,EAAW,EAAE,WAAW;IAAE;IAAU;IAAQ;IAAM,EAAE,CAAC,EAC3D,MAAM,GAAS;WACR,GAAK;AACZ,KAAM,MACJ,EAAG,wCAAwC,QAAQ,EACnD,aAAe,QACX,EAAI,UACJ,EAAG,8CAA8C,yBAAyB,CAC/E;;IAIC,IAAU,GAAM,eAAe,SAAS,EAAE,EAC1C,IAAQ,GAAM,eAAe,SAAS,GACtC,IAAU,GAAM,eAAe,WAAW,IAC1C,IAAc,KAAW,EAAQ,SAAS;AAEhD,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;EAAW,WAAU;YAArB;GACE,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;GAC5B,EAAG,6BAA6B,iBAAiB;GACjD,IAAQ,KAAK,kBAAC,QAAD;IAAM,WAAU;cAAhB;KAA8C;KAAE;KAAM;KAAQ;;GAClE;KACD,CAAA,EACb,kBAAC,GAAD;EAAa,WAAU;YAAvB;GACE,kBAAC,KAAD;IAAG,WAAU;cACV,EACC,mCACA,iJACD;IACC,CAAA;GAEH,KACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,QAAD;IAAM,UAAU;IAAW,WAAU;cAArC;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,SAAD;OAAO,WAAU;iBACd,EAAG,wCAAwC,UAAU;OAChD,CAAA,EACR,kBAAC,GAAD;OACE,OAAO,EAAQ;OACf,WAAW,MAAM,EAAW;QAAE,GAAG;QAAS,QAAQ,EAAE,OAAO;QAAO,CAAC;OACnE,aAAa,EACX,8CACA,mCACD;OACD,UAAA;OACA,UAAU;OACV,CAAA,CACE;;KACN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,SAAD;OAAO,WAAU;iBACd,EAAG,sCAAsC,OAAO;OAC3C,CAAA,EACR,kBAAC,UAAD;OACE,OAAO,EAAQ;OACf,WAAW,MACT,EAAW;QAAE,GAAG;QAAS,MAAM,EAAE,OAAO;QAA2B,CAAC;OAEtE,UAAU;OACV,WAAU;iBANZ,CAQE,kBAAC,UAAD;QAAQ,OAAO,EAAiB;kBAC7B,EAAG,mCAAmC,SAAS;QACzC,CAAA,EACT,kBAAC,UAAD;QAAQ,OAAO,EAAiB;kBAC7B,EAAG,kCAAkC,QAAQ;QACvC,CAAA,CACF;SACL;;KACN,kBAAC,GAAD;MAAQ,MAAK;MAAS,UAAU,KAAU,CAAC,EAAQ,OAAO,MAAM;gBAAhE,CACG,IACC,kBAAC,GAAD,EAAS,WAAU,6BAA8B,CAAA,GAEjD,kBAAC,GAAD,EAAU,WAAU,gBAAiB,CAAA,EAEtC,EAAG,mCAAmC,eAAe,CAC/C;;KACJ;OACP,kBAAC,GAAD,EAAa,CAAA,CACZ,EAAA,CAAA;GAGJ,KAAW,EAAQ,WAAW,IAC7B,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD,EAAS,WAAU,6BAA8B,CAAA,EAChD,EAAG,+BAA+B,qBAAqB,CACpD;QACJ,IACF,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAM;IACH,CAAA,GACJ,EAAQ,WAAW,IACrB,kBAAC,KAAD;IAAG,WAAU;cACV,EACC,6BACA,0DACD;IACC,CAAA,GAEJ,kBAAC,OAAD;IAAK,WAAU;cAAf,CACG,EAAQ,KAAK,MACZ,kBAAC,OAAD;KAEE,WAAU;eAFZ,CAIE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,KAAD;OAAG,WAAU;iBAAgD,EAAE;OAAW,CAAA,EAC1E,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAG,+BAA+B,4BAA4B;QAC7D,MAAM,EAAW,EAAE,UAAU;QAC7B,IAAI,EAAE;QACP,CAAC;OACA,CAAA,CACA;SACN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,UAAD;OACE,OAAO,EAAE;OACT,WAAW,MACT,EAAiB,EAAE,QAAQ,EAAE,OAAO,MAA0B;OAEhE,UAAU,CAAC;OACX,WAAU;iBANZ,CAQE,kBAAC,UAAD;QAAQ,OAAO,EAAiB;kBAC7B,EAAG,mCAAmC,SAAS;QACzC,CAAA,EACT,kBAAC,UAAD;QAAQ,OAAO,EAAiB;kBAC7B,EAAG,kCAAkC,QAAQ;QACvC,CAAA,CACF;UACR,KACC,kBAAC,GAAD;OACE,SAAQ;OACR,MAAK;OACL,eAAe,EAAgB,EAAE,QAAQ,EAAE,QAAQ,CAAC;OACpD,UAAU;OACV,cAAY,EAAG,8BAA8B,SAAS;iBAEtD,kBAAC,GAAD,EAAQ,WAAU,kCAAmC,CAAA;OAC9C,CAAA,CAEP;QACF;OAxCC,EAAE,GAwCH,CACN,EACD,KACC,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MAAM,WAAU;gBACb,EAAG,oCAAoC,kCAAkC;OACxE,OAAO,EAAQ;OACf;OACD,CAAC;MACG,CAAA,EACP,kBAAC,GAAD;MACE,SAAQ;MACR,MAAK;MACL,eAAe,GAAa,MAAS,IAAO,EAAU;MACtD,UAAU;gBAJZ,CAMG,IAAc,kBAAC,GAAD,EAAS,WAAU,6BAA8B,CAAA,GAAG,MAClE,EAAG,gCAAgC,YAAY,CACzC;QACL;OAEJ;;GAEI;IACT,EAAA,CAAA,EAEP,kBAAC,GAAD;EACE,MAAM,EAAQ;EACd,OAAO,EAAG,2CAA2C,wBAAwB;EAC7E,aAAa,EACX,iDACA,oJACA,EAAE,QAAQ,GAAc,UAAU,IAAI,CACvC;EACD,cAAc,EAAG,6CAA6C,SAAS;EACvE,SAAQ;EACR,SAAS;EACT,WAAW;EACX,eAAe,EAAgB,KAAK;EACpC,CAAA,CACD,EAAA,CAAA"}
@@ -49,6 +49,7 @@ function i() {
49
49
  canViewWaitlist: d,
50
50
  canManageWaitlist: l || i(e.WAITLIST_MANAGE) || i(e.TENANT_ADMIN),
51
51
  canViewDashboard: d,
52
+ canViewExploreConversations: l,
52
53
  isLoading: l ? !1 : a
53
54
  };
54
55
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useAdminPermissions.js","names":[],"sources":["../../../src/admin/hooks/useAdminPermissions.ts"],"sourcesContent":["import { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useAuth } from '@burdenoff/fe-libs/shared/providers/shell/AuthProvider';\nimport { ADMIN_PERMISSIONS } from '../constants/permissions';\n\ninterface JwtAccessClaims {\n role: string | null;\n actorType: string | null;\n}\n\nfunction decodeJwtAccessClaims(token: string | undefined): JwtAccessClaims {\n if (!token) return { role: null, actorType: null };\n const parts = token.split('.');\n if (parts.length !== 3) return { role: null, actorType: null };\n try {\n const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');\n const normalized = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');\n const payload = JSON.parse(atob(normalized)) as Record<string, unknown>;\n return {\n role: typeof payload.role === 'string' ? payload.role : null,\n actorType:\n typeof payload.actor_type === 'string'\n ? payload.actor_type\n : typeof payload.actorType === 'string'\n ? payload.actorType\n : null,\n };\n } catch {\n return { role: null, actorType: null };\n }\n}\n\n/**\n * Hook for admin console RBAC permissions.\n *\n * SUPER_ADMIN role / actor type (verified from profile or JWT) bypasses all\n * permission checks.\n * Other users require granular permissions from the global RBAC service.\n */\nexport function useAdminPermissions() {\n const { hasPermission, isLoading } = usePermissions();\n const auth = useAuth();\n\n const jwtClaims = decodeJwtAccessClaims(auth.tokens?.accessToken);\n const userActorType =\n typeof auth.user?.actorType === 'string'\n ? auth.user.actorType\n : typeof auth.user?.actor_type === 'string'\n ? auth.user.actor_type\n : null;\n const isSuperAdmin =\n auth.user?.role === 'SUPER_ADMIN' ||\n auth.user?.role === 'super_admin' ||\n userActorType === 'super_admin' ||\n jwtClaims.role === 'SUPER_ADMIN' ||\n jwtClaims.role === 'super_admin' ||\n jwtClaims.actorType === 'super_admin';\n\n const check = (permission: string) => isSuperAdmin || hasPermission(permission);\n\n // True if the user holds any admin-level permission (read or admin grants)\n const hasAnyAdminAccess =\n isSuperAdmin ||\n hasPermission(ADMIN_PERMISSIONS.TENANT_READ) ||\n hasPermission(ADMIN_PERMISSIONS.TENANT_ADMIN) ||\n hasPermission(ADMIN_PERMISSIONS.USER_READ) ||\n hasPermission(ADMIN_PERMISSIONS.USER_ADMIN) ||\n hasPermission(ADMIN_PERMISSIONS.BILLING_READ) ||\n hasPermission(ADMIN_PERMISSIONS.NOTIFICATION_READ) ||\n hasPermission(ADMIN_PERMISSIONS.RBAC_AUDIT_READ) ||\n hasPermission(ADMIN_PERMISSIONS.WORKSPACE_ADMIN);\n\n return {\n // Tenant management\n canViewTenants: check(ADMIN_PERMISSIONS.TENANT_READ),\n canManageTenants: check(ADMIN_PERMISSIONS.TENANT_WRITE),\n canDeleteTenants: check(ADMIN_PERMISSIONS.TENANT_DELETE),\n isTenantAdmin: check(ADMIN_PERMISSIONS.TENANT_ADMIN),\n\n // User management\n canViewUsers: check(ADMIN_PERMISSIONS.USER_READ),\n canUpdateUsers: check(ADMIN_PERMISSIONS.USER_UPDATE),\n canDeleteUsers: check(ADMIN_PERMISSIONS.USER_DELETE),\n isUserAdmin: check(ADMIN_PERMISSIONS.USER_ADMIN),\n\n // Billing\n canViewBilling: check(ADMIN_PERMISSIONS.BILLING_READ),\n\n // Communications\n canViewCommunications: check(ADMIN_PERMISSIONS.NOTIFICATION_READ),\n\n // Audit\n canViewAuditLogs: check(ADMIN_PERMISSIONS.RBAC_AUDIT_READ),\n\n // Platform settings / SRE\n canManageSettings: check(ADMIN_PERMISSIONS.TENANT_ADMIN),\n canViewSRE: check(ADMIN_PERMISSIONS.TENANT_ADMIN),\n\n // Cross-layer sync outbox monitoring (super-admin only)\n canViewSyncFailures: isSuperAdmin || hasPermission(ADMIN_PERMISSIONS.SYNC_OUTBOX_READ),\n canManageSyncFailures: isSuperAdmin || hasPermission(ADMIN_PERMISSIONS.SYNC_OUTBOX_MANAGE),\n\n // Workspace / Sandbox\n canManageSandbox: check(ADMIN_PERMISSIONS.WORKSPACE_ADMIN),\n\n // Products\n canViewProducts: hasAnyAdminAccess,\n canManageProducts: check(ADMIN_PERMISSIONS.PRODUCT_WRITE),\n\n // Waitlist\n canViewWaitlist: hasAnyAdminAccess,\n canManageWaitlist:\n isSuperAdmin ||\n hasPermission(ADMIN_PERMISSIONS.WAITLIST_MANAGE) ||\n hasPermission(ADMIN_PERMISSIONS.TENANT_ADMIN),\n\n // General pages — any admin role can access\n canViewDashboard: hasAnyAdminAccess,\n\n isLoading: isSuperAdmin ? false : isLoading,\n };\n}\n"],"mappings":";;;;AASA,SAAS,EAAsB,GAA4C;AACzE,KAAI,CAAC,EAAO,QAAO;EAAE,MAAM;EAAM,WAAW;EAAM;CAClD,IAAM,IAAQ,EAAM,MAAM,IAAI;AAC9B,KAAI,EAAM,WAAW,EAAG,QAAO;EAAE,MAAM;EAAM,WAAW;EAAM;AAC9D,KAAI;EACF,IAAM,IAAS,EAAM,GAAG,QAAQ,MAAM,IAAI,CAAC,QAAQ,MAAM,IAAI,EACvD,IAAa,EAAO,OAAO,KAAK,KAAK,EAAO,SAAS,EAAE,GAAG,GAAG,IAAI,EACjE,IAAU,KAAK,MAAM,KAAK,EAAW,CAAC;AAC5C,SAAO;GACL,MAAM,OAAO,EAAQ,QAAS,WAAW,EAAQ,OAAO;GACxD,WACE,OAAO,EAAQ,cAAe,WAC1B,EAAQ,aACR,OAAO,EAAQ,aAAc,WAC3B,EAAQ,YACR;GACT;SACK;AACN,SAAO;GAAE,MAAM;GAAM,WAAW;GAAM;;;AAW1C,SAAgB,IAAsB;CACpC,IAAM,EAAE,kBAAe,iBAAc,GAAgB,EAC/C,IAAO,GAAS,EAEhB,IAAY,EAAsB,EAAK,QAAQ,YAAY,EAC3D,IACJ,OAAO,EAAK,MAAM,aAAc,WAC5B,EAAK,KAAK,YACV,OAAO,EAAK,MAAM,cAAe,WAC/B,EAAK,KAAK,aACV,MACF,IACJ,EAAK,MAAM,SAAS,iBACpB,EAAK,MAAM,SAAS,iBACpB,MAAkB,iBAClB,EAAU,SAAS,iBACnB,EAAU,SAAS,iBACnB,EAAU,cAAc,eAEpB,KAAS,MAAuB,KAAgB,EAAc,EAAW,EAGzE,IACJ,KACA,EAAc,EAAkB,YAAY,IAC5C,EAAc,EAAkB,aAAa,IAC7C,EAAc,EAAkB,UAAU,IAC1C,EAAc,EAAkB,WAAW,IAC3C,EAAc,EAAkB,aAAa,IAC7C,EAAc,EAAkB,kBAAkB,IAClD,EAAc,EAAkB,gBAAgB,IAChD,EAAc,EAAkB,gBAAgB;AAElD,QAAO;EAEL,gBAAgB,EAAM,EAAkB,YAAY;EACpD,kBAAkB,EAAM,EAAkB,aAAa;EACvD,kBAAkB,EAAM,EAAkB,cAAc;EACxD,eAAe,EAAM,EAAkB,aAAa;EAGpD,cAAc,EAAM,EAAkB,UAAU;EAChD,gBAAgB,EAAM,EAAkB,YAAY;EACpD,gBAAgB,EAAM,EAAkB,YAAY;EACpD,aAAa,EAAM,EAAkB,WAAW;EAGhD,gBAAgB,EAAM,EAAkB,aAAa;EAGrD,uBAAuB,EAAM,EAAkB,kBAAkB;EAGjE,kBAAkB,EAAM,EAAkB,gBAAgB;EAG1D,mBAAmB,EAAM,EAAkB,aAAa;EACxD,YAAY,EAAM,EAAkB,aAAa;EAGjD,qBAAqB,KAAgB,EAAc,EAAkB,iBAAiB;EACtF,uBAAuB,KAAgB,EAAc,EAAkB,mBAAmB;EAG1F,kBAAkB,EAAM,EAAkB,gBAAgB;EAG1D,iBAAiB;EACjB,mBAAmB,EAAM,EAAkB,cAAc;EAGzD,iBAAiB;EACjB,mBACE,KACA,EAAc,EAAkB,gBAAgB,IAChD,EAAc,EAAkB,aAAa;EAG/C,kBAAkB;EAElB,WAAW,IAAe,KAAQ;EACnC"}
1
+ {"version":3,"file":"useAdminPermissions.js","names":[],"sources":["../../../src/admin/hooks/useAdminPermissions.ts"],"sourcesContent":["import { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useAuth } from '@burdenoff/fe-libs/shared/providers/shell/AuthProvider';\nimport { ADMIN_PERMISSIONS } from '../constants/permissions';\n\ninterface JwtAccessClaims {\n role: string | null;\n actorType: string | null;\n}\n\nfunction decodeJwtAccessClaims(token: string | undefined): JwtAccessClaims {\n if (!token) return { role: null, actorType: null };\n const parts = token.split('.');\n if (parts.length !== 3) return { role: null, actorType: null };\n try {\n const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');\n const normalized = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');\n const payload = JSON.parse(atob(normalized)) as Record<string, unknown>;\n return {\n role: typeof payload.role === 'string' ? payload.role : null,\n actorType:\n typeof payload.actor_type === 'string'\n ? payload.actor_type\n : typeof payload.actorType === 'string'\n ? payload.actorType\n : null,\n };\n } catch {\n return { role: null, actorType: null };\n }\n}\n\n/**\n * Hook for admin console RBAC permissions.\n *\n * SUPER_ADMIN role / actor type (verified from profile or JWT) bypasses all\n * permission checks.\n * Other users require granular permissions from the global RBAC service.\n */\nexport function useAdminPermissions() {\n const { hasPermission, isLoading } = usePermissions();\n const auth = useAuth();\n\n const jwtClaims = decodeJwtAccessClaims(auth.tokens?.accessToken);\n const userActorType =\n typeof auth.user?.actorType === 'string'\n ? auth.user.actorType\n : typeof auth.user?.actor_type === 'string'\n ? auth.user.actor_type\n : null;\n const isSuperAdmin =\n auth.user?.role === 'SUPER_ADMIN' ||\n auth.user?.role === 'super_admin' ||\n userActorType === 'super_admin' ||\n jwtClaims.role === 'SUPER_ADMIN' ||\n jwtClaims.role === 'super_admin' ||\n jwtClaims.actorType === 'super_admin';\n\n const check = (permission: string) => isSuperAdmin || hasPermission(permission);\n\n // True if the user holds any admin-level permission (read or admin grants)\n const hasAnyAdminAccess =\n isSuperAdmin ||\n hasPermission(ADMIN_PERMISSIONS.TENANT_READ) ||\n hasPermission(ADMIN_PERMISSIONS.TENANT_ADMIN) ||\n hasPermission(ADMIN_PERMISSIONS.USER_READ) ||\n hasPermission(ADMIN_PERMISSIONS.USER_ADMIN) ||\n hasPermission(ADMIN_PERMISSIONS.BILLING_READ) ||\n hasPermission(ADMIN_PERMISSIONS.NOTIFICATION_READ) ||\n hasPermission(ADMIN_PERMISSIONS.RBAC_AUDIT_READ) ||\n hasPermission(ADMIN_PERMISSIONS.WORKSPACE_ADMIN);\n\n return {\n // Tenant management\n canViewTenants: check(ADMIN_PERMISSIONS.TENANT_READ),\n canManageTenants: check(ADMIN_PERMISSIONS.TENANT_WRITE),\n canDeleteTenants: check(ADMIN_PERMISSIONS.TENANT_DELETE),\n isTenantAdmin: check(ADMIN_PERMISSIONS.TENANT_ADMIN),\n\n // User management\n canViewUsers: check(ADMIN_PERMISSIONS.USER_READ),\n canUpdateUsers: check(ADMIN_PERMISSIONS.USER_UPDATE),\n canDeleteUsers: check(ADMIN_PERMISSIONS.USER_DELETE),\n isUserAdmin: check(ADMIN_PERMISSIONS.USER_ADMIN),\n\n // Billing\n canViewBilling: check(ADMIN_PERMISSIONS.BILLING_READ),\n\n // Communications\n canViewCommunications: check(ADMIN_PERMISSIONS.NOTIFICATION_READ),\n\n // Audit\n canViewAuditLogs: check(ADMIN_PERMISSIONS.RBAC_AUDIT_READ),\n\n // Platform settings / SRE\n canManageSettings: check(ADMIN_PERMISSIONS.TENANT_ADMIN),\n canViewSRE: check(ADMIN_PERMISSIONS.TENANT_ADMIN),\n\n // Cross-layer sync outbox monitoring (super-admin only)\n canViewSyncFailures: isSuperAdmin || hasPermission(ADMIN_PERMISSIONS.SYNC_OUTBOX_READ),\n canManageSyncFailures: isSuperAdmin || hasPermission(ADMIN_PERMISSIONS.SYNC_OUTBOX_MANAGE),\n\n // Workspace / Sandbox\n canManageSandbox: check(ADMIN_PERMISSIONS.WORKSPACE_ADMIN),\n\n // Products\n canViewProducts: hasAnyAdminAccess,\n canManageProducts: check(ADMIN_PERMISSIONS.PRODUCT_WRITE),\n\n // Waitlist\n canViewWaitlist: hasAnyAdminAccess,\n canManageWaitlist:\n isSuperAdmin ||\n hasPermission(ADMIN_PERMISSIONS.WAITLIST_MANAGE) ||\n hasPermission(ADMIN_PERMISSIONS.TENANT_ADMIN),\n\n // General pages — any admin role can access\n canViewDashboard: hasAnyAdminAccess,\n\n /**\n * Explore conversations: superadmin only, matching the\n * `@rbac(actorTypes: [\"super_admin\"])` on the backing fields. Exposed as its own flag\n * rather than reusing a permission, because the backend gate is actor-type based and a\n * permission grant would not open it — the two must agree or the page renders and then\n * every query comes back FORBIDDEN.\n */\n canViewExploreConversations: isSuperAdmin,\n\n isLoading: isSuperAdmin ? false : isLoading,\n };\n}\n"],"mappings":";;;;AASA,SAAS,EAAsB,GAA4C;AACzE,KAAI,CAAC,EAAO,QAAO;EAAE,MAAM;EAAM,WAAW;EAAM;CAClD,IAAM,IAAQ,EAAM,MAAM,IAAI;AAC9B,KAAI,EAAM,WAAW,EAAG,QAAO;EAAE,MAAM;EAAM,WAAW;EAAM;AAC9D,KAAI;EACF,IAAM,IAAS,EAAM,GAAG,QAAQ,MAAM,IAAI,CAAC,QAAQ,MAAM,IAAI,EACvD,IAAa,EAAO,OAAO,KAAK,KAAK,EAAO,SAAS,EAAE,GAAG,GAAG,IAAI,EACjE,IAAU,KAAK,MAAM,KAAK,EAAW,CAAC;AAC5C,SAAO;GACL,MAAM,OAAO,EAAQ,QAAS,WAAW,EAAQ,OAAO;GACxD,WACE,OAAO,EAAQ,cAAe,WAC1B,EAAQ,aACR,OAAO,EAAQ,aAAc,WAC3B,EAAQ,YACR;GACT;SACK;AACN,SAAO;GAAE,MAAM;GAAM,WAAW;GAAM;;;AAW1C,SAAgB,IAAsB;CACpC,IAAM,EAAE,kBAAe,iBAAc,GAAgB,EAC/C,IAAO,GAAS,EAEhB,IAAY,EAAsB,EAAK,QAAQ,YAAY,EAC3D,IACJ,OAAO,EAAK,MAAM,aAAc,WAC5B,EAAK,KAAK,YACV,OAAO,EAAK,MAAM,cAAe,WAC/B,EAAK,KAAK,aACV,MACF,IACJ,EAAK,MAAM,SAAS,iBACpB,EAAK,MAAM,SAAS,iBACpB,MAAkB,iBAClB,EAAU,SAAS,iBACnB,EAAU,SAAS,iBACnB,EAAU,cAAc,eAEpB,KAAS,MAAuB,KAAgB,EAAc,EAAW,EAGzE,IACJ,KACA,EAAc,EAAkB,YAAY,IAC5C,EAAc,EAAkB,aAAa,IAC7C,EAAc,EAAkB,UAAU,IAC1C,EAAc,EAAkB,WAAW,IAC3C,EAAc,EAAkB,aAAa,IAC7C,EAAc,EAAkB,kBAAkB,IAClD,EAAc,EAAkB,gBAAgB,IAChD,EAAc,EAAkB,gBAAgB;AAElD,QAAO;EAEL,gBAAgB,EAAM,EAAkB,YAAY;EACpD,kBAAkB,EAAM,EAAkB,aAAa;EACvD,kBAAkB,EAAM,EAAkB,cAAc;EACxD,eAAe,EAAM,EAAkB,aAAa;EAGpD,cAAc,EAAM,EAAkB,UAAU;EAChD,gBAAgB,EAAM,EAAkB,YAAY;EACpD,gBAAgB,EAAM,EAAkB,YAAY;EACpD,aAAa,EAAM,EAAkB,WAAW;EAGhD,gBAAgB,EAAM,EAAkB,aAAa;EAGrD,uBAAuB,EAAM,EAAkB,kBAAkB;EAGjE,kBAAkB,EAAM,EAAkB,gBAAgB;EAG1D,mBAAmB,EAAM,EAAkB,aAAa;EACxD,YAAY,EAAM,EAAkB,aAAa;EAGjD,qBAAqB,KAAgB,EAAc,EAAkB,iBAAiB;EACtF,uBAAuB,KAAgB,EAAc,EAAkB,mBAAmB;EAG1F,kBAAkB,EAAM,EAAkB,gBAAgB;EAG1D,iBAAiB;EACjB,mBAAmB,EAAM,EAAkB,cAAc;EAGzD,iBAAiB;EACjB,mBACE,KACA,EAAc,EAAkB,gBAAgB,IAChD,EAAc,EAAkB,aAAa;EAG/C,kBAAkB;EASlB,6BAA6B;EAE7B,WAAW,IAAe,KAAQ;EACnC"}
@@ -0,0 +1,14 @@
1
+ import { useCallback as e } from "react";
2
+ import { useI18n as t } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
3
+ //#region src/admin/hooks/useTr.ts
4
+ function n() {
5
+ let { t: n } = t();
6
+ return e((e, t, r) => {
7
+ let i = n(e), a = i === e ? t : i;
8
+ return r ? Object.entries(r).reduce((e, [t, n]) => e.replace(RegExp(`\\{\\{${t}\\}\\}`, "g"), String(n)), a) : a;
9
+ }, [n]);
10
+ }
11
+ //#endregion
12
+ export { n as useTr };
13
+
14
+ //# sourceMappingURL=useTr.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useTr.js","names":[],"sources":["../../../src/admin/hooks/useTr.ts"],"sourcesContent":["import { useCallback } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\n/**\n * useTr — translation hook with fallback semantics.\n *\n * Wraps `useI18n().t()` so that when a key has no translation (i.e. the\n * provider returns the key itself), the caller's English fallback is used\n * instead. This keeps consumer call sites consistent and avoids leaking\n * untranslated keys into the UI.\n *\n * Supports optional interpolation params using `{{key}}` placeholders.\n *\n * @example\n * const tr = useTr();\n * return <h1>{tr('admin.dashboard.title', 'Dashboard')}</h1>;\n * return <p>{tr('admin.tenants.toast.createdDescription', '{{name}} has been created.', { name: 'Acme' })}</p>;\n */\nexport function useTr(): (\n key: string,\n fallback: string,\n params?: Record<string, string | number>\n) => string {\n const { t } = useI18n();\n return useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>): string => {\n const translated = t(key);\n const raw = translated === key ? fallback : translated;\n if (!params) return raw;\n return Object.entries(params).reduce(\n (str, [k, v]) => str.replace(new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}`, 'g'), String(v)),\n raw\n );\n },\n [t]\n );\n}\n"],"mappings":";;;AAkBA,SAAgB,IAIJ;CACV,IAAM,EAAE,SAAM,GAAS;AACvB,QAAO,GACJ,GAAa,GAAkB,MAAqD;EACnF,IAAM,IAAa,EAAE,EAAI,EACnB,IAAM,MAAe,IAAM,IAAW;AAE5C,SADK,IACE,OAAO,QAAQ,EAAO,CAAC,QAC3B,GAAK,CAAC,GAAG,OAAO,EAAI,QAAY,OAAO,SAAS,EAAE,SAAS,IAAI,EAAE,OAAO,EAAE,CAAC,EAC5E,EACD,GAJmB;IAMtB,CAAC,EAAE,CACJ"}