@jini-ai/user-management 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/dist/core/principal-kind-policy.d.ts +1 -1
- package/dist/core/principal-kind-policy.js +1 -1
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.js +2 -0
- package/dist/react/members/Members.d.ts +28 -1
- package/dist/react/members/Members.js +14 -10
- package/dist/react/members/hooks/use-members.hooks.d.ts +43 -1
- package/dist/react/members/hooks/use-members.hooks.js +37 -9
- package/dist/react/members/rules.d.ts +31 -4
- package/dist/react/members/rules.js +17 -5
- package/dist/react/models.d.ts +3 -0
- package/dist/react/users/Users.d.ts +105 -6
- package/dist/react/users/Users.js +105 -43
- package/dist/react/users/hooks/use-reset-password-fields.hooks.d.ts +33 -1
- package/dist/react/users/hooks/use-reset-password-fields.hooks.js +16 -1
- package/dist/react/users/hooks/use-users.hooks.d.ts +120 -3
- package/dist/react/users/hooks/use-users.hooks.js +192 -23
- package/dist/react/users/rules.d.ts +115 -8
- package/dist/react/users/rules.js +109 -6
- package/package.json +15 -10
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,7 @@ import type { PrincipalKind } from "./types.js";
|
|
|
6
6
|
* The one place that decides whether a principal's KIND lets it use the operator RBAC axis
|
|
7
7
|
* (`authorize()`'s permissions). Today the answer is: every kind except `member`. A `member` is a
|
|
8
8
|
* public-site sign-up — a host's front-end account, governed by that host's own entitlement axis
|
|
9
|
-
* (tiers, gated content), never by operator RBAC. OWNER DECISION 2026-10-04 (
|
|
9
|
+
* (tiers, gated content), never by operator RBAC. OWNER DECISION 2026-10-04 (host decision F3144): site
|
|
10
10
|
* members must not be able to use admin features, at least for now.
|
|
11
11
|
*
|
|
12
12
|
* Why a function and not a check at each call site:
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* The one place that decides whether a principal's KIND lets it use the operator RBAC axis
|
|
6
6
|
* (`authorize()`'s permissions). Today the answer is: every kind except `member`. A `member` is a
|
|
7
7
|
* public-site sign-up — a host's front-end account, governed by that host's own entitlement axis
|
|
8
|
-
* (tiers, gated content), never by operator RBAC. OWNER DECISION 2026-10-04 (
|
|
8
|
+
* (tiers, gated content), never by operator RBAC. OWNER DECISION 2026-10-04 (host decision F3144): site
|
|
9
9
|
* members must not be able to use admin features, at least for now.
|
|
10
10
|
*
|
|
11
11
|
* Why a function and not a check at each call site:
|
package/dist/react/index.d.ts
CHANGED
|
@@ -15,4 +15,8 @@ export type { MembersProps } from './members/Members.js';
|
|
|
15
15
|
export { useMembers } from './members/hooks/use-members.hooks.js';
|
|
16
16
|
export type { MembersController, MembersDependencies, MembersOptions } from './members/hooks/use-members.hooks.js';
|
|
17
17
|
export { describeIdentityError } from './errors.js';
|
|
18
|
+
export { userAccountCapabilities, isOwnerAccount, grantOptionLabel, formatGrantLabel, userRowMenuItems, describeUsersError } from './users/rules.js';
|
|
19
|
+
export type { UserAccountCapabilities, UserRowMenuHandlers } from './users/rules.js';
|
|
20
|
+
export { emptyRowState, memberRowMenuItems, describeMembersError } from './members/rules.js';
|
|
21
|
+
export type { RowActionState, MemberRowMenuHandlers } from './members/rules.js';
|
|
18
22
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/react/index.js
CHANGED
|
@@ -6,4 +6,6 @@ export { useResetPasswordFields } from './users/hooks/use-reset-password-fields.
|
|
|
6
6
|
export { Members } from './members/Members.js';
|
|
7
7
|
export { useMembers } from './members/hooks/use-members.hooks.js';
|
|
8
8
|
export { describeIdentityError } from './errors.js';
|
|
9
|
+
export { userAccountCapabilities, isOwnerAccount, grantOptionLabel, formatGrantLabel, userRowMenuItems, describeUsersError } from './users/rules.js';
|
|
10
|
+
export { emptyRowState, memberRowMenuItems, describeMembersError } from './members/rules.js';
|
|
9
11
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
import { useMembers, type MembersDependencies, type MembersOptions } from "./hooks/use-members.hooks.js";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* @file Admin "Members" screen (ADR-030, ADR-PIPE-013 Decision §7) — markup only.
|
|
5
|
+
*
|
|
6
|
+
* State and API calls live in `hooks/use-members.hooks.ts`; the row-menu logic, per-row action
|
|
7
|
+
* state shape, and the server-error-message override live in `rules.ts`. What stays here is what
|
|
8
|
+
* actually renders: the table and the confirm dialog.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors `features/posts/Posts.tsx`'s fetch/loading/error/table shape. Adds the
|
|
11
|
+
* three row-level actions this remediation wires up (T039): disable, resend
|
|
12
|
+
* sign-in link, and a click-to-expand detail panel — all calling the 3
|
|
13
|
+
* already-existing, already-unused the host admin API client client methods
|
|
14
|
+
* (`disableMember`, `requestMemberMagicLink`, `getMember`). No new backend
|
|
15
|
+
* contract needed. Pagination is explicitly deferred (ADR-PIPE-013 Decision
|
|
16
|
+
* §7) — not part of this screen yet.
|
|
17
|
+
*
|
|
18
|
+
* Per-row in-flight state disables only the clicked control (not the whole
|
|
19
|
+
* table), and errors surface via the existing `notice error` convention
|
|
20
|
+
* (inline per row for actions; a full-width banner for the initial load).
|
|
21
|
+
* Stays a single flat file, matching every other admin section's convention.
|
|
22
|
+
*/
|
|
2
23
|
export interface MembersProps extends MembersDependencies, MembersOptions {
|
|
3
24
|
useMembersHook?: typeof useMembers | undefined;
|
|
25
|
+
/** Host terminology is copy, not a second members lifecycle. */
|
|
26
|
+
description?: string | undefined;
|
|
27
|
+
emptyDescription?: string | undefined;
|
|
28
|
+
renderServerLabel?: ((required: {
|
|
29
|
+
value: string;
|
|
30
|
+
}) => ReactNode) | undefined;
|
|
4
31
|
}
|
|
5
|
-
export declare function Members({ useMembersHook, port, translate: injectedTranslate, refresh }: MembersProps): import("react").JSX.Element;
|
|
32
|
+
export declare function Members({ useMembersHook, port, translate: injectedTranslate, refresh, description, emptyDescription, renderServerLabel }: MembersProps): import("react").JSX.Element;
|
|
6
33
|
//# sourceMappingURL=Members.d.ts.map
|
|
@@ -6,6 +6,10 @@ import { formatTimestamp } from "@jini-ai/ui/panel-kit";
|
|
|
6
6
|
import { buildAgentListHandles } from "@jini-ai/ui/panel-kit";
|
|
7
7
|
import { memberRowMenuItems } from "./rules.js";
|
|
8
8
|
import { useMembers } from "./hooks/use-members.hooks.js";
|
|
9
|
+
/** The expanded row's detail panel — one of "loading" / "error" / the fetched fields / nothing
|
|
10
|
+
* yet, extracted out of `MemberRow` so its three-way branch isn't counted in `MemberRow`'s own
|
|
11
|
+
* scope. Same "the panel, not the row, was the actual size" split `Users.tsx`'s
|
|
12
|
+
* `UserRow` -> `UserManagePanel` and `Roles.tsx`'s `PolicyRow` -> `PolicyRowActions` already use. */
|
|
9
13
|
function MemberDetailPanel({ memberId, detailLoadingId, detailError, detail, t }) {
|
|
10
14
|
if (detailLoadingId === memberId)
|
|
11
15
|
return _jsx("div", { className: "notice", children: t("Loading detail…") });
|
|
@@ -15,24 +19,24 @@ function MemberDetailPanel({ memberId, detailLoadingId, detailError, detail, t }
|
|
|
15
19
|
return null;
|
|
16
20
|
return (_jsxs("dl", { className: "member-detail", children: [_jsx("dt", { children: t("ID") }), _jsx("dd", { children: detail.id }), _jsx("dt", { children: t("Email verified") }), _jsx("dd", { children: detail.emailVerifiedAt ?? t("not verified") }), _jsx("dt", { children: t("Updated") }), _jsx("dd", { children: detail.updatedAt }), _jsx("dt", { children: t("Version") }), _jsx("dd", { children: detail.version })] }));
|
|
17
21
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
/** One member's row plus its optional expanded detail row — extracted from `Members`'s
|
|
23
|
+
* `.map()` body verbatim, same convention `Users.tsx`'s `UserRow`/`Roles.tsx`'s `PolicyRow` use.
|
|
24
|
+
* `key` lives on the `<MemberRow>` element at the call site. */
|
|
25
|
+
function MemberRow({ member, rowState, isExpanded, detail, detailError, detailLoadingId, onToggleDetail, onResendSignInLink, setConfirmingDisable, agentBase, t, translate, renderServerLabel, }) {
|
|
26
|
+
return (_jsxs(Fragment, { children: [_jsxs("tr", { children: [_jsx("td", { children: _jsx("button", { type: "button", className: "link-button", onClick: () => void onToggleDetail(member), "aria-expanded": isExpanded, ...agentHandle({ handle: `${agentBase}-toggle-detail` }, { role: "button", label: `Expand or collapse ${member.email}'s detail panel` }), children: member.email }) }), _jsx("td", { children: member.name ?? "—" }), _jsx("td", { children: _jsx("span", { className: `status status-${member.status}`, children: renderServerLabel?.({ value: member.status }) ?? t(member.status) }) }), _jsx("td", { children: formatTimestamp({ iso: member.createdAt }) }), _jsxs("td", { children: [_jsx(RowMenu, { triggerLabel: `${t("Actions for member")} "${member.email}"`, agentHandle: `${agentBase}-menu`, items: memberRowMenuItems({ member, rs: rowState, handlers: {
|
|
23
27
|
onResendSignInLink: (m) => void onResendSignInLink(m),
|
|
24
28
|
onRequestDisable: setConfirmingDisable,
|
|
25
|
-
},
|
|
26
|
-
translate,
|
|
27
|
-
}) }), rowState.error ? (_jsx("div", { className: "notice error", role: "alert", children: rowState.error })) : null, rowState.notice ? _jsx("div", { className: "notice", children: rowState.notice }) : null] })] }), isExpanded ? (_jsx("tr", { children: _jsx("td", { colSpan: 5, children: _jsx(MemberDetailPanel, { memberId: member.id, detailLoadingId: detailLoadingId, detailError: detailError, detail: detail, t: t }) }) })) : null] }, member.id));
|
|
29
|
+
}, translate }) }), rowState.error ? (_jsx("div", { className: "notice error", role: "alert", children: rowState.error })) : null, rowState.notice ? _jsx("div", { className: "notice", children: rowState.notice }) : null] })] }), isExpanded ? (_jsx("tr", { children: _jsx("td", { colSpan: 5, children: _jsx(MemberDetailPanel, { memberId: member.id, detailLoadingId: detailLoadingId, detailError: detailError, detail: detail, t: t }) }) })) : null] }, member.id));
|
|
28
30
|
}
|
|
29
|
-
export function Members({ useMembersHook = useMembers, port, translate: injectedTranslate, refresh }) {
|
|
31
|
+
export function Members({ useMembersHook = useMembers, port, translate: injectedTranslate, refresh, description = "People who have registered an account — review status, resend a sign-in link, or disable access.", emptyDescription = "Registered members will show up here.", renderServerLabel }) {
|
|
30
32
|
const { members, error, stateFor, onResendSignInLink, expandedId, detailById, detailError, detailLoadingId, onToggleDetail, confirmingDisable, setConfirmingDisable, confirmDisable, t, translate, } = useMembersHook({ port, translate: injectedTranslate }, { refresh });
|
|
31
33
|
if (error)
|
|
32
34
|
return _jsx("div", { className: "notice error", children: error });
|
|
33
35
|
if (!members)
|
|
34
36
|
return _jsx("div", { className: "notice", children: t("Loading members…") });
|
|
37
|
+
// Member ids are stable and unique, so they disambiguate one row's menu from another's — same
|
|
38
|
+
// reasoning as every other list on this workstream.
|
|
35
39
|
const memberMenuBases = buildAgentListHandles({ prefix: "members-row", ids: members.map((member) => member.id) });
|
|
36
|
-
return (_jsxs("div", { className: "page", children: [_jsx("div", { className: "page-header", children: _jsxs("div", { className: "page-header-text", children: [_jsx("p", { className: "page-kicker", children: t("People") }), _jsx("h1", { className: "page-title", children: t("Members") }), _jsx("p", { className: "page-description", children: t(
|
|
40
|
+
return (_jsxs("div", { className: "page", children: [_jsx("div", { className: "page-header", children: _jsxs("div", { className: "page-header-text", children: [_jsx("p", { className: "page-kicker", children: t("People") }), _jsx("h1", { className: "page-title", children: t("Members") }), _jsx("p", { className: "page-description", children: t(description) })] }) }), members.length === 0 ? (_jsx("div", { className: "card", children: _jsxs("div", { className: "empty-state", children: [_jsx("p", { children: t("No members yet.") }), _jsx("p", { className: "page-description", children: t(emptyDescription) })] }) })) : (_jsx("div", { className: "table-scroll", children: _jsxs("table", { className: "list-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: t("Email") }), _jsx("th", { children: t("Name") }), _jsx("th", { children: t("Status") }), _jsx("th", { children: t("Created") }), _jsx("th", { children: t("Actions") })] }) }), _jsx("tbody", { children: members.map((member, index) => (_jsx(MemberRow, { member: member, rowState: stateFor({ id: member.id }), isExpanded: expandedId === member.id, detail: detailById[member.id], detailError: detailError, detailLoadingId: detailLoadingId, onToggleDetail: onToggleDetail, onResendSignInLink: onResendSignInLink, setConfirmingDisable: setConfirmingDisable, agentBase: memberMenuBases[index], t: t, translate: translate, renderServerLabel: renderServerLabel }, member.id))) })] }) })), _jsx(ConfirmDialog, { cancelLabel: t("Cancel"), open: confirmingDisable !== null, agentHandle: "members-disable", title: t("Disable this member?"), body: confirmingDisable ? (_jsxs("p", { children: [t("Disable"), " \"", confirmingDisable.email, "\"? ", t("They will no longer be able to sign in.")] })) : null, confirmLabel: t("Disable"), tone: "warning", pending: confirmingDisable !== null && stateFor({ id: confirmingDisable.id }).disabling, onConfirm: confirmDisable, onCancel: () => setConfirmingDisable(null) })] }));
|
|
37
41
|
}
|
|
38
42
|
//# sourceMappingURL=Members.js.map
|
|
@@ -1,28 +1,70 @@
|
|
|
1
|
+
import type { IdentityRefreshPort } from "../../ports.js";
|
|
1
2
|
import { type Dispatch, type SetStateAction } from "react";
|
|
2
3
|
import type { AdminMember } from "../../models.js";
|
|
3
4
|
import { type RowActionState } from "../rules.js";
|
|
4
|
-
import type { MembersPort
|
|
5
|
+
import type { MembersPort } from "../../ports.js";
|
|
5
6
|
import type { Translate } from "@jini-ai/ui/panel-kit";
|
|
7
|
+
/**
|
|
8
|
+
* @file Everything the Members screen does, so `Members.tsx` is only markup.
|
|
9
|
+
*
|
|
10
|
+
* Extracted verbatim — same state, same declaration order, same effect bodies, same error strings.
|
|
11
|
+
* `RowActionState`/`emptyRowState`/`describeApiError` moved to `rules.ts` (they were already
|
|
12
|
+
* module-scope free functions in the original, just private and untested); this hook imports them
|
|
13
|
+
* back for `stateFor` and its own async handlers.
|
|
14
|
+
*
|
|
15
|
+
* Naming follows `hooks/use-settings-slice.hooks.ts` and `posts/hooks/use-posts.hooks.ts`:
|
|
16
|
+
* `use-<thing>.hooks.ts`. Feature-local because nothing outside `features/members` needs it.
|
|
17
|
+
*
|
|
18
|
+
* `deps.port` is injected (see `members-port.hooks.ts`) rather than reaching for `lib/api`'s `api`
|
|
19
|
+
* directly — the same `useX(dependencies)` / `useWiredX()` split `redirects`/`widgets`/`plugins` use.
|
|
20
|
+
*
|
|
21
|
+
* `t`/`locale` (2026-08-11, standing i18n rule — a component with a hook gets a BOUND `t` from that
|
|
22
|
+
* hook, not its own `useAdminLocale()`/dictionary import): this hook already called
|
|
23
|
+
* `useAdminLocale()` for its own error-string translations, so exposing that SAME already-resolved
|
|
24
|
+
* `locale` as a bound `t` (plus the raw value, still needed for `rules.ts`'s `memberRowMenuItems`,
|
|
25
|
+
* which takes `locale` directly) on the return value adds no new fetch — `Members.tsx` used to call
|
|
26
|
+
* `useAdminLocale()` a second time and rebuild its own `translateMembers(locale, key)` closure,
|
|
27
|
+
* entirely redundant with the resolution this hook was already doing internally.
|
|
28
|
+
*
|
|
29
|
+
* `useContentRefreshSubscription` (staleness-bug generalization pass — see that hook's own header):
|
|
30
|
+
* `load` is pulled into a `useCallback` so it can also be handed to that hook, which re-runs it
|
|
31
|
+
* whenever `members_disable` (the host member tool contributor) changes a
|
|
32
|
+
* member's status from an assistant run this screen otherwise has no way to learn about. No draft
|
|
33
|
+
* to protect — every row's own edit state is per-action busy/error tracking, not typed text.
|
|
34
|
+
*/
|
|
6
35
|
export interface MembersDependencies {
|
|
7
36
|
port: MembersPort;
|
|
8
37
|
translate: Translate;
|
|
9
38
|
}
|
|
10
39
|
export interface MembersController {
|
|
40
|
+
/** `null` until the initial load settles — the caller renders a loading state. */
|
|
11
41
|
members: AdminMember[] | null;
|
|
12
42
|
error: string | null;
|
|
43
|
+
/** Per-row action state (Disable/Resend in-flight, error, notice), falling back to the empty
|
|
44
|
+
* state for a row with no action taken yet. */
|
|
13
45
|
stateFor: (required: {
|
|
14
46
|
id: string;
|
|
15
47
|
}) => RowActionState;
|
|
16
48
|
onResendSignInLink: (member: AdminMember) => Promise<void>;
|
|
49
|
+
/** The row whose detail panel is expanded — `null` when every row is collapsed. */
|
|
17
50
|
expandedId: string | null;
|
|
51
|
+
/** Detail already fetched for an expanded row, keyed by member id — a cache so re-expanding a
|
|
52
|
+
* row already visited this session doesn't re-fetch. */
|
|
18
53
|
detailById: Record<string, AdminMember>;
|
|
19
54
|
detailError: string | null;
|
|
20
55
|
detailLoadingId: string | null;
|
|
21
56
|
onToggleDetail: (member: AdminMember) => Promise<void>;
|
|
57
|
+
/** The member a `RowMenu` "Disable" selection is asking to confirm; `null` when the dialog is
|
|
58
|
+
* shut. `ConfirmDialog` stays mounted unconditionally in the view (see its own doc comment on
|
|
59
|
+
* why); this is what drives its `open` prop. */
|
|
22
60
|
confirmingDisable: AdminMember | null;
|
|
23
61
|
setConfirmingDisable: Dispatch<SetStateAction<AdminMember | null>>;
|
|
24
62
|
confirmDisable: () => Promise<void>;
|
|
63
|
+
/** Bound translator — `key` already resolved against the caller's locale, so `Members.tsx` never
|
|
64
|
+
* imports `useAdminLocale`/`members-i18n` itself. See this file's header. */
|
|
25
65
|
t: Translate;
|
|
66
|
+
/** Raw resolved locale — `rules.ts`'s `memberRowMenuItems` takes `locale` directly rather than a
|
|
67
|
+
* bound translator. See this file's header. */
|
|
26
68
|
translate: Translate;
|
|
27
69
|
}
|
|
28
70
|
export interface MembersOptions {
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
-
import { describeApiError
|
|
2
|
+
import { describeApiError, emptyRowState } from "../rules.js";
|
|
3
3
|
import { useSettlementGeneration } from "@jini-ai/ui/panel-kit";
|
|
4
4
|
export function useMembers({ port, translate }, { refresh } = {}) {
|
|
5
|
-
const describeApiError = (error, fallback) => describeFeatureError({ error, fallback, translate });
|
|
6
|
-
const t = translate;
|
|
7
5
|
const boundT = translate;
|
|
6
|
+
const describeError = (error, fallback) => describeApiError({ error, fallback, translate });
|
|
8
7
|
const [members, setMembers] = useState(null);
|
|
9
8
|
const [error, setError] = useState(null);
|
|
10
9
|
const [rowState, setRowState] = useState({});
|
|
@@ -12,11 +11,27 @@ export function useMembers({ port, translate }, { refresh } = {}) {
|
|
|
12
11
|
const [detailById, setDetailById] = useState({});
|
|
13
12
|
const [detailError, setDetailError] = useState(null);
|
|
14
13
|
const [detailLoadingId, setDetailLoadingId] = useState(null);
|
|
14
|
+
// Disable now confirms via a `RowMenu` item -> `ConfirmDialog` modal (replacing the in-place
|
|
15
|
+
// two-click `ConfirmButton`, which has no menu-item equivalent — same migration Posts.tsx/
|
|
16
|
+
// Redirects.tsx/Users.tsx already made). `null` when the dialog is closed.
|
|
15
17
|
const [confirmingDisable, setConfirmingDisable] = useState(null);
|
|
16
18
|
const settlement = useSettlementGeneration();
|
|
19
|
+
// Which row's detail panel is actually open (C7, plan-access.md §8, N2) — a ref, not just
|
|
20
|
+
// `expandedId` state, because `onToggleDetail`'s `catch`/`finally` read it after an `await`, where
|
|
21
|
+
// a state read would see the closure's stale value. Set by `onToggleDetail` alone. Without this,
|
|
22
|
+
// expanding row A (slow, then failing), then row B, lets A's `finally` blank B's loading indicator
|
|
23
|
+
// and lets A's failure paint onto B's now-open panel — the same "entity-scoped panel, unkeyed
|
|
24
|
+
// settle" shape `use-roles.hooks.ts`'s `permissionPolicyIdRef` fixes for `loadPermissions`.
|
|
17
25
|
const expandedIdRef = useRef(null);
|
|
26
|
+
// Latest-wins for `onToggleDetail`'s detail loads, a separate instance from `settlement` (which
|
|
27
|
+
// tracks `load()`'s list reads).
|
|
18
28
|
const detailSettlement = useSettlementGeneration();
|
|
19
29
|
const load = useCallback(() => {
|
|
30
|
+
// Claim this call's generation BEFORE the request starts — see `useSettlementGeneration`'s own
|
|
31
|
+
// doc for why a synchronous ref bump, not `useState`, is what makes two overlapping calls each
|
|
32
|
+
// see the other's claim. Needed now that a content refresh can fire more than once per run
|
|
33
|
+
// (mid-run tool progress, see `AssistantDock.hooks.tsx`), so two overlapping `load()` calls have
|
|
34
|
+
// no ordering guarantee on their responses.
|
|
20
35
|
const generation = settlement.next();
|
|
21
36
|
port
|
|
22
37
|
.listMembers({})
|
|
@@ -29,8 +44,12 @@ export function useMembers({ port, translate }, { refresh } = {}) {
|
|
|
29
44
|
.catch((e) => {
|
|
30
45
|
if (!settlement.isCurrent({ generation }))
|
|
31
46
|
return;
|
|
32
|
-
setError(e instanceof Error ? e.message :
|
|
47
|
+
setError(e instanceof Error ? e.message : translate("failed to load members"));
|
|
33
48
|
});
|
|
49
|
+
// `port`/`locale`/`settlement` are added — see `use-page-editor.hooks.ts`'s identical note:
|
|
50
|
+
// function-scoped values ESLint's exhaustive-deps rule can see, referentially stable in
|
|
51
|
+
// production, so this changes nothing about when this callback's identity changes.
|
|
52
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
34
53
|
}, [port, settlement, translate]);
|
|
35
54
|
useEffect(() => {
|
|
36
55
|
load();
|
|
@@ -49,10 +68,10 @@ export function useMembers({ port, translate }, { refresh } = {}) {
|
|
|
49
68
|
try {
|
|
50
69
|
const result = await port.disableMember({ id: member.id });
|
|
51
70
|
setMembers((current) => (current ? current.map((m) => (m.id === member.id ? result.member : m)) : current));
|
|
52
|
-
patchRowState(member.id, { disabling: false, notice:
|
|
71
|
+
patchRowState(member.id, { disabling: false, notice: translate("Member disabled.") });
|
|
53
72
|
}
|
|
54
73
|
catch (e) {
|
|
55
|
-
patchRowState(member.id, { disabling: false, error:
|
|
74
|
+
patchRowState(member.id, { disabling: false, error: describeError(e, translate("Failed to disable member.")) });
|
|
56
75
|
}
|
|
57
76
|
}
|
|
58
77
|
async function onResendSignInLink(member) {
|
|
@@ -61,12 +80,15 @@ export function useMembers({ port, translate }, { refresh } = {}) {
|
|
|
61
80
|
patchRowState(member.id, { resending: true, error: null, notice: null });
|
|
62
81
|
try {
|
|
63
82
|
await port.requestMemberMagicLink({ email: member.email });
|
|
64
|
-
patchRowState(member.id, { resending: false, notice:
|
|
83
|
+
patchRowState(member.id, { resending: false, notice: translate("Sign-in link sent.") });
|
|
65
84
|
}
|
|
66
85
|
catch (e) {
|
|
67
|
-
patchRowState(member.id, { resending: false, error:
|
|
86
|
+
patchRowState(member.id, { resending: false, error: describeError(e, translate("Failed to send sign-in link.")) });
|
|
68
87
|
}
|
|
69
88
|
}
|
|
89
|
+
/** Confirms the Disable that `RowMenu`'s "Disable" item asked about. Closes the dialog either
|
|
90
|
+
* way (matching Posts.tsx/Redirects.tsx/Users.tsx's own Disable/Delete `ConfirmDialog`
|
|
91
|
+
* convention) — a failure surfaces via the row's own `rs.error`, not by leaving the modal open. */
|
|
70
92
|
async function confirmDisable() {
|
|
71
93
|
if (!confirmingDisable)
|
|
72
94
|
return;
|
|
@@ -85,17 +107,23 @@ export function useMembers({ port, translate }, { refresh } = {}) {
|
|
|
85
107
|
if (detailById[member.id])
|
|
86
108
|
return;
|
|
87
109
|
setDetailLoadingId(member.id);
|
|
110
|
+
// Claimed before the `await`, so a later detail load (another row's, or this same row's after
|
|
111
|
+
// a close and reopen) supersedes this one. The row-id key alone cannot tell two loads of the
|
|
112
|
+
// SAME row apart.
|
|
88
113
|
const generation = detailSettlement.next();
|
|
89
114
|
try {
|
|
90
115
|
const result = await port.getMember({ id: member.id });
|
|
91
116
|
setDetailById((current) => ({ ...current, [member.id]: result.member }));
|
|
92
117
|
}
|
|
93
118
|
catch (e) {
|
|
119
|
+
// Only paint the failure onto the panel this load was actually for — a different row may
|
|
120
|
+
// already be open by the time this settles, or a newer load of this row may be running.
|
|
94
121
|
if (detailSettlement.isCurrent({ generation }) && expandedIdRef.current === member.id) {
|
|
95
|
-
setDetailError(
|
|
122
|
+
setDetailError(describeError(e, translate("Failed to load member detail.")));
|
|
96
123
|
}
|
|
97
124
|
}
|
|
98
125
|
finally {
|
|
126
|
+
// A superseded load leaves the spinner to the load that superseded it.
|
|
99
127
|
if (detailSettlement.isCurrent({ generation }))
|
|
100
128
|
setDetailLoadingId(null);
|
|
101
129
|
}
|
|
@@ -1,26 +1,53 @@
|
|
|
1
1
|
import type { AdminMember } from "../models.js";
|
|
2
2
|
import type { Translate } from "@jini-ai/ui/panel-kit";
|
|
3
3
|
import type { RowMenuItem } from "@jini-ai/admin/react";
|
|
4
|
+
/**
|
|
5
|
+
* @file Pure logic for the `members` feature — everything that computes a value rather than
|
|
6
|
+
* rendering one. Follows the `posts/rules.ts` convention: no React import, no hooks, directly
|
|
7
|
+
* testable.
|
|
8
|
+
*
|
|
9
|
+
* Moved here from `Members.tsx`: `RowActionState`/`emptyRowState` (already module-scope free
|
|
10
|
+
* functions in the original, just not exported or testable), the screen's `describeApiError`
|
|
11
|
+
* override (a `FORBIDDEN`-code branch), and the row-menu item builder (a branch on
|
|
12
|
+
* `member.status !== "disabled"`).
|
|
13
|
+
*/
|
|
14
|
+
/** Per-row in-flight/result state for the two row actions (Disable, Resend sign-in link). */
|
|
4
15
|
export interface RowActionState {
|
|
5
16
|
disabling: boolean;
|
|
6
17
|
resending: boolean;
|
|
7
18
|
error: string | null;
|
|
8
19
|
notice: string | null;
|
|
9
20
|
}
|
|
10
|
-
export declare function emptyRowState(_required: Record<string, never>): RowActionState;
|
|
11
|
-
|
|
21
|
+
export declare function emptyRowState(_required: Record<string, never>, _optional?: Record<string, never>): RowActionState;
|
|
22
|
+
/** Overrides layered on the shared default (`lib/api.ts`'s `describeApiError`).
|
|
23
|
+
*
|
|
24
|
+
* `locale` (C4 fix, 2026-09-20): the FORBIDDEN override used to return its English literal
|
|
25
|
+
* directly, leaking English into every non-`en` locale — see `users/rules.ts`'s identical fix for
|
|
26
|
+
* the full reasoning. The literal is now also a key into `members-i18n.ts`'s `MEMBERS_DICT`. */
|
|
27
|
+
export declare function describeMembersError({ error, fallback, translate }: {
|
|
12
28
|
error: unknown;
|
|
13
29
|
fallback: string;
|
|
14
30
|
translate: Translate;
|
|
15
|
-
}): string;
|
|
31
|
+
}, _optional?: Record<string, never>): string;
|
|
32
|
+
export { describeMembersError as describeApiError };
|
|
33
|
+
/** The callbacks a row menu needs. Passed in rather than imported, so this module stays free of
|
|
34
|
+
* state (same convention as `posts/rules.ts`'s `PostRowMenuHandlers`). */
|
|
16
35
|
export interface MemberRowMenuHandlers {
|
|
17
36
|
onResendSignInLink: (member: AdminMember) => void;
|
|
37
|
+
/** Opens the confirm dialog; only offered when `member.status !== "disabled"`. */
|
|
18
38
|
onRequestDisable: (member: AdminMember) => void;
|
|
19
39
|
}
|
|
40
|
+
/** `RowMenu` items for one member row. "Disable" is omitted once the member is already disabled
|
|
41
|
+
* — `RowMenu` has no per-item `disabled`, and `Posts.tsx`'s own precedent (omitting "Disable"
|
|
42
|
+
* entirely for an already-draft row rather than showing it disabled) is to omit an inapplicable
|
|
43
|
+
* action rather than show it as a no-op.
|
|
44
|
+
*
|
|
45
|
+
* @complexity Time/space: O(1) — at most two entries, no iteration.
|
|
46
|
+
*/
|
|
20
47
|
export declare function memberRowMenuItems({ member, rs, handlers, translate }: {
|
|
21
48
|
member: AdminMember;
|
|
22
49
|
rs: RowActionState;
|
|
23
50
|
handlers: MemberRowMenuHandlers;
|
|
24
51
|
translate: Translate;
|
|
25
|
-
}): RowMenuItem[];
|
|
52
|
+
}, _optional?: Record<string, never>): RowMenuItem[];
|
|
26
53
|
//# sourceMappingURL=rules.d.ts.map
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
import { describeIdentityError } from "../errors.js";
|
|
2
|
-
|
|
3
|
-
export function emptyRowState(_required) {
|
|
2
|
+
export function emptyRowState(_required, _optional = {}) {
|
|
4
3
|
return { disabling: false, resending: false, error: null, notice: null };
|
|
5
4
|
}
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/** Overrides layered on the shared default (`lib/api.ts`'s `describeApiError`).
|
|
6
|
+
*
|
|
7
|
+
* `locale` (C4 fix, 2026-09-20): the FORBIDDEN override used to return its English literal
|
|
8
|
+
* directly, leaking English into every non-`en` locale — see `users/rules.ts`'s identical fix for
|
|
9
|
+
* the full reasoning. The literal is now also a key into `members-i18n.ts`'s `MEMBERS_DICT`. */
|
|
10
|
+
export function describeMembersError({ error, fallback, translate }, _optional = {}) {
|
|
11
|
+
return describeIdentityError({ error, fallback, translate }, { messages: { FORBIDDEN: "You do not have permission to do that." } });
|
|
8
12
|
}
|
|
9
|
-
export
|
|
13
|
+
export { describeMembersError as describeApiError };
|
|
14
|
+
/** `RowMenu` items for one member row. "Disable" is omitted once the member is already disabled
|
|
15
|
+
* — `RowMenu` has no per-item `disabled`, and `Posts.tsx`'s own precedent (omitting "Disable"
|
|
16
|
+
* entirely for an already-draft row rather than showing it disabled) is to omit an inapplicable
|
|
17
|
+
* action rather than show it as a no-op.
|
|
18
|
+
*
|
|
19
|
+
* @complexity Time/space: O(1) — at most two entries, no iteration.
|
|
20
|
+
*/
|
|
21
|
+
export function memberRowMenuItems({ member, rs, handlers, translate }, _optional = {}) {
|
|
10
22
|
const items = [
|
|
11
23
|
{
|
|
12
24
|
key: "resend",
|
package/dist/react/models.d.ts
CHANGED
|
@@ -11,6 +11,9 @@ export interface AdminIdentityUser {
|
|
|
11
11
|
status: "active" | "disabled";
|
|
12
12
|
createdAt: string;
|
|
13
13
|
lastLoginAt?: string | undefined;
|
|
14
|
+
/** Server classification covers ownership granted by policy, not only named roles. */
|
|
15
|
+
isOwner?: boolean | undefined;
|
|
16
|
+
isProtectedAccount?: boolean | undefined;
|
|
14
17
|
roleIds: string[];
|
|
15
18
|
policyIds: string[];
|
|
16
19
|
}
|
|
@@ -1,26 +1,100 @@
|
|
|
1
|
-
import type { Translate } from "@jini-ai/ui/panel-kit";
|
|
2
1
|
import { type Dispatch, type SetStateAction } from "react";
|
|
3
2
|
import type { AdminIdentityUser, AdminPolicy, AdminRole } from "../models.js";
|
|
3
|
+
import { type UserAccountCapabilities } from "./rules.js";
|
|
4
4
|
import { useUsers, type UsersDependencies, type UsersOptions } from "./hooks/use-users.hooks.js";
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
import type { Translate } from "@jini-ai/ui/panel-kit";
|
|
7
|
+
/**
|
|
8
|
+
* @file Admin "Users" screen (SPEC-006 §3 human grant-writing transitions + 0.6.0 CRUD-completion
|
|
9
|
+
* amendment) — markup only.
|
|
10
|
+
*
|
|
11
|
+
* State and API calls live in `hooks/use-users.hooks.ts`; the row-menu logic and the
|
|
12
|
+
* server-error-message overrides live in `rules.ts`. What stays here is what actually renders: the
|
|
13
|
+
* form, the table, and the two dialogs.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors `features/integrations/Integrations.tsx`'s fetch/loading/error/form/table shape.
|
|
16
|
+
* Lists operator users, creates new ones (`CREATE_USER`), and grants roles/
|
|
17
|
+
* policies to an existing user (`ASSIGN_ROLE`/`ATTACH_POLICY`) via an
|
|
18
|
+
* inline expandable "Manage" row. Role/policy *creation* is out of
|
|
19
|
+
* this screen's UI (no `CREATE_ROLE`/`CREATE_POLICY` form here) — the API
|
|
20
|
+
* client exposes `api.createRole`/`api.createPolicy` for the Roles &
|
|
21
|
+
* Permissions screen to reuse.
|
|
22
|
+
*
|
|
23
|
+
* 0.6.0: the same expandable row also carries `UPDATE_USER` (email),
|
|
24
|
+
* `RESET_USER_PASSWORD`, and a quick `DISABLE_PRINCIPAL`/`ENABLE_PRINCIPAL` toggle.
|
|
25
|
+
*
|
|
26
|
+
* Row actions (audit follow-up): all three of the above — Disable/Enable, Manage (opens the
|
|
27
|
+
* expandable row), Reset password — now live behind a single three-dot `RowMenu`, matching
|
|
28
|
+
* `Posts.tsx`/`Pages.tsx`'s row-action shape rather than a row of separate buttons.
|
|
29
|
+
*
|
|
30
|
+
* Delete-user plan v2 (2026-09-24): a fourth "Delete" item joins the menu, shown only when the
|
|
31
|
+
* signed-in caller may manage the user Trash (owner or the built-in `admin` role — OWNER DECISION
|
|
32
|
+
* 2026-09-24, `use-users.hooks.ts`'s `canManageUserTrash`). It moves the target to the Trash
|
|
33
|
+
* (disabled, sessions revoked, restorable there for 60 days) rather than hard-deleting it — see
|
|
34
|
+
* `UserDeleteDialog`'s own doc comment.
|
|
35
|
+
*
|
|
36
|
+
* Complexity-ceiling pass (2026-08-06): `Users` and its row-map closure both scored over the
|
|
37
|
+
* ceiling (19/16 and 11/11 respectively). Split into four top-level, individually-testable
|
|
38
|
+
* functions below — `NewUserForm`, `UsersTable`/`UserRow`, `UserDisableDialog`/`UserDeleteDialog`,
|
|
39
|
+
* `UserResetPasswordDialog` — following `Taxonomy.tsx`'s existing convention of sibling top-level
|
|
40
|
+
* function components in the same file rather than nested closures (a nested `const` inside
|
|
41
|
+
* `Users` would not have moved any branching out of `Users`' own scope). `Users` itself is now a
|
|
42
|
+
* thin composition of those, none of them exported, so no test does anything `Users.tsx`
|
|
43
|
+
* didn't already support — see `__tests__/Users.unit.test.tsx`/`Users.crud.unit.test.tsx` for the
|
|
44
|
+
* full-render tests these were extracted underneath (unchanged), plus new
|
|
45
|
+
* `__tests__/users-components.unit.test.tsx` for direct component-level tests of each piece.
|
|
46
|
+
*
|
|
47
|
+
* Reset-password confirm + reveal (typo-catching pass): `UserResetPasswordDialog` gained a second
|
|
48
|
+
* field ("Confirm new password") and an independent show/hide toggle per field, both driven by the
|
|
49
|
+
* new `hooks/use-reset-password-fields.hooks.ts` (confirm-field state and both reveal flags are
|
|
50
|
+
* purely local/ephemeral — never sent to the server, so they don't belong in `use-users.hooks.ts`'s
|
|
51
|
+
* screen-wide `UsersController`). The confirm-vs-server-error display conflict is resolved by giving
|
|
52
|
+
* the two states one shared slot with a fixed precedence — a live mismatch always wins over a stale
|
|
53
|
+
* `passwordError` from a previous attempt — rather than trying to show both at once or inventing a
|
|
54
|
+
* second error line; see `UserResetPasswordDialog`'s own body for the exact ternary. `RevealablePasswordField`
|
|
55
|
+
* is the shared field+toggle shape both new inputs use, following this file's own `GrantSelect`
|
|
56
|
+
* precedent for a shape used twice.
|
|
57
|
+
*/
|
|
5
58
|
export interface UsersProps extends UsersDependencies, UsersOptions {
|
|
6
59
|
useUsersHook?: typeof useUsers | undefined;
|
|
7
|
-
|
|
60
|
+
/** The host owns localization of protocol status/role labels. */
|
|
61
|
+
renderServerLabel?: ((required: {
|
|
62
|
+
value: string;
|
|
63
|
+
}) => ReactNode) | undefined;
|
|
8
64
|
}
|
|
65
|
+
/** The subset of `AdminRole`/`AdminPolicy` a grant `<select>` actually renders. Named rather than
|
|
66
|
+
* taking the full records, so the shared control below cannot come to depend on anything a role has
|
|
67
|
+
* and a policy does not. */
|
|
9
68
|
export interface GrantOption {
|
|
10
69
|
id: string;
|
|
11
70
|
name: string;
|
|
12
71
|
isBuiltin: boolean;
|
|
13
72
|
}
|
|
73
|
+
/** One "pick something, then grant it to this user" control. Both grants on the Manage panel are
|
|
74
|
+
* this same shape, which is what lets them share {@link GrantSelect} instead of being two
|
|
75
|
+
* near-identical blocks whose props had to be threaded separately. */
|
|
14
76
|
export interface GrantSelectController {
|
|
15
77
|
options: GrantOption[];
|
|
78
|
+
/** The id currently selected in the `<select>`, or `""` for the placeholder row. */
|
|
16
79
|
pendingId: string;
|
|
17
80
|
setPendingId: Dispatch<SetStateAction<string>>;
|
|
18
81
|
submit: (required: {
|
|
19
82
|
principalId: string;
|
|
20
83
|
}) => Promise<void>;
|
|
21
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Everything the expanded "Manage" panel needs, as one object.
|
|
87
|
+
*
|
|
88
|
+
* Replaces the fifteen loose state/setter/callback props this panel used to take and `UserRow` used
|
|
89
|
+
* to relay through an `Omit<UserManagePanelProps, "principalId">` rest spread. The grouping is by
|
|
90
|
+
* interaction, not by convenience: the email editor and the two grants are three independent
|
|
91
|
+
* controls that happen to share one error line and one in-flight flag, and saying so in the type is
|
|
92
|
+
* what lets a test build a Manage panel without also standing up a row, a table, and a page.
|
|
93
|
+
*/
|
|
22
94
|
export interface UserManageController {
|
|
95
|
+
/** The one error line the whole panel shares — any of the three writes can set it. */
|
|
23
96
|
error: string | null;
|
|
97
|
+
/** Shared by both grants: the panel issues one grant at a time. */
|
|
24
98
|
saving: boolean;
|
|
25
99
|
email: {
|
|
26
100
|
value: string;
|
|
@@ -30,43 +104,68 @@ export interface UserManageController {
|
|
|
30
104
|
principalId: string;
|
|
31
105
|
}) => Promise<void>;
|
|
32
106
|
};
|
|
33
|
-
|
|
34
|
-
|
|
107
|
+
/** `null` when the caller may not read roles/policies (`UsersController.canGrant`): no grant controls. */
|
|
108
|
+
roleGrant: GrantSelectController | null;
|
|
109
|
+
policyGrant: GrantSelectController | null;
|
|
35
110
|
}
|
|
36
111
|
export interface UserManagePanelProps {
|
|
37
112
|
principalId: string;
|
|
38
113
|
manage: UserManageController;
|
|
39
114
|
t: (key: string) => string;
|
|
115
|
+
/** The table's column count, so the panel row spans it exactly (fewer without Roles/Policies). */
|
|
116
|
+
colSpan?: number;
|
|
40
117
|
}
|
|
41
|
-
|
|
118
|
+
/** The expanded "Manage" row's contents (email edit, role/policy grant forms).
|
|
119
|
+
*
|
|
120
|
+
* Exported so a test can drive it with a hand-built {@link UserManageController} — the seam only
|
|
121
|
+
* counts as one if something other than `Users` can supply it. */
|
|
122
|
+
export declare function UserManagePanel({ principalId, manage, t, colSpan }: UserManagePanelProps): import("react").JSX.Element;
|
|
123
|
+
/** A user row's own affordances — expand/collapse and the three menu actions. Distinct from
|
|
124
|
+
* {@link UserManageController}, which describes what the expanded panel does once it is open. */
|
|
42
125
|
export interface UserRowActionsController {
|
|
43
126
|
expandedId: string | null;
|
|
44
127
|
toggleExpanded: (user: AdminIdentityUser) => void;
|
|
128
|
+
/** Which row has a status write in flight — drives that row's disabled menu item only. */
|
|
45
129
|
savingId: string | null;
|
|
46
130
|
requestDisable: (user: AdminIdentityUser) => void;
|
|
47
131
|
toggleStatus: (user: AdminIdentityUser) => Promise<void>;
|
|
48
132
|
openResetPassword: (user: AdminIdentityUser) => void;
|
|
133
|
+
/** Delete-user plan v2 (2026-09-24): whether the row's `RowMenu` should carry a "Delete" item at
|
|
134
|
+
* all — see `use-users.hooks.ts`'s `canManageUserTrash` doc comment for where this comes from. */
|
|
49
135
|
canDelete: boolean;
|
|
136
|
+
rowCapabilities?: (user: AdminIdentityUser) => UserAccountCapabilities;
|
|
50
137
|
requestDelete: (user: AdminIdentityUser) => void;
|
|
51
138
|
}
|
|
52
139
|
export interface UserRowProps {
|
|
53
140
|
user: AdminIdentityUser;
|
|
54
141
|
roleById: ReadonlyMap<string, AdminRole>;
|
|
55
142
|
policyById: ReadonlyMap<string, AdminPolicy>;
|
|
143
|
+
/** Whether the Roles/Policies cells render — see {@link UsersTableProps.showGrants}. */
|
|
144
|
+
showGrants: boolean;
|
|
56
145
|
actions: UserRowActionsController;
|
|
57
146
|
manage: UserManageController;
|
|
147
|
+
/** This row's own distinct handle base — computed once, across every rendered row, by
|
|
148
|
+
* `UsersTable` (via `buildAgentListHandles`); see `Taxonomy.tsx`'s `NewTermForm.agentBase` for
|
|
149
|
+
* why a per-instance uniqueness search does not work here. */
|
|
58
150
|
agentBase: string;
|
|
59
151
|
t: (key: string) => string;
|
|
60
152
|
translate: Translate;
|
|
153
|
+
renderServerLabel?: UsersProps["renderServerLabel"];
|
|
61
154
|
}
|
|
62
155
|
export interface UsersTableProps {
|
|
63
156
|
users: AdminIdentityUser[];
|
|
157
|
+
/** The full records, for the Roles/Policies label columns. The grant selects get their own
|
|
158
|
+
* `options` through `manage`, so neither consumer has to reach into the other's props. */
|
|
64
159
|
roles: AdminRole[];
|
|
65
160
|
policies: AdminPolicy[];
|
|
161
|
+
/** `UsersController.canGrant`: without `role.manage` there are no names to resolve grant ids
|
|
162
|
+
* against, so the Roles/Policies columns are left out rather than showing raw ids. */
|
|
163
|
+
showGrants: boolean;
|
|
66
164
|
actions: UserRowActionsController;
|
|
67
165
|
manage: UserManageController;
|
|
68
166
|
t: (key: string) => string;
|
|
69
167
|
translate: Translate;
|
|
168
|
+
renderServerLabel?: UsersProps["renderServerLabel"];
|
|
70
169
|
}
|
|
71
|
-
export declare function Users({ useUsersHook, port, translate: injectedTranslate, queryScope, openOwnPasswordReset, onOwnPasswordResetClosed }: UsersProps): import("react").JSX.Element;
|
|
170
|
+
export declare function Users({ useUsersHook, port, translate: injectedTranslate, queryScope, openOwnPasswordReset, onOwnPasswordResetClosed, passwordResetNotice, renderServerLabel }: UsersProps): import("react").JSX.Element;
|
|
72
171
|
//# sourceMappingURL=Users.d.ts.map
|