@10x-media/impersonation 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/access/filterTargets.js +51 -0
- package/dist/access/filterTargets.js.map +1 -0
- package/dist/auth/cookies.js +49 -0
- package/dist/auth/cookies.js.map +1 -0
- package/dist/auth/expiryCookies.js +59 -0
- package/dist/auth/expiryCookies.js.map +1 -0
- package/dist/auth/issue.js +126 -0
- package/dist/auth/issue.js.map +1 -0
- package/dist/auth/mode.js +69 -0
- package/dist/auth/mode.js.map +1 -0
- package/dist/auth/origin.js +55 -0
- package/dist/auth/origin.js.map +1 -0
- package/dist/auth/revoke.js +24 -0
- package/dist/auth/revoke.js.map +1 -0
- package/dist/auth/tenantCookie.js +74 -0
- package/dist/auth/tenantCookie.js.map +1 -0
- package/dist/endpoints/codes.d.ts +6 -0
- package/dist/endpoints/current.js +66 -0
- package/dist/endpoints/current.js.map +1 -0
- package/dist/endpoints/exit.js +242 -0
- package/dist/endpoints/exit.js.map +1 -0
- package/dist/endpoints/shared.js +76 -0
- package/dist/endpoints/shared.js.map +1 -0
- package/dist/endpoints/start.js +332 -0
- package/dist/endpoints/start.js.map +1 -0
- package/dist/endpoints/terminate.js +73 -0
- package/dist/endpoints/terminate.js.map +1 -0
- package/dist/exports/client.d.ts +9 -0
- package/dist/exports/client.js +10 -0
- package/dist/exports/i18n.d.ts +3 -0
- package/dist/exports/i18n.js +3 -0
- package/dist/exports/rsc.d.ts +4 -0
- package/dist/exports/rsc.js +4 -0
- package/dist/exports/types.d.ts +3 -0
- package/dist/exports/types.js +1 -0
- package/dist/getImpersonation.d.ts +49 -0
- package/dist/getImpersonation.js +78 -0
- package/dist/getImpersonation.js.map +1 -0
- package/dist/ids.js +27 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin/assertTargets.js +23 -0
- package/dist/plugin/assertTargets.js.map +1 -0
- package/dist/plugin/constants.d.ts +6 -0
- package/dist/plugin/constants.js +20 -0
- package/dist/plugin/constants.js.map +1 -0
- package/dist/plugin/decorateAuth.js +99 -0
- package/dist/plugin/decorateAuth.js.map +1 -0
- package/dist/plugin/lookup.js +23 -0
- package/dist/plugin/lookup.js.map +1 -0
- package/dist/plugin/normalizeOptions.js +83 -0
- package/dist/plugin/normalizeOptions.js.map +1 -0
- package/dist/plugin/registerAdmin.js +49 -0
- package/dist/plugin/registerAdmin.js.map +1 -0
- package/dist/plugin/registerCollection.js +183 -0
- package/dist/plugin/registerCollection.js.map +1 -0
- package/dist/plugin/registerEndpoints.js +19 -0
- package/dist/plugin/registerEndpoints.js.map +1 -0
- package/dist/plugin/registerHooks.js +59 -0
- package/dist/plugin/registerHooks.js.map +1 -0
- package/dist/plugin/registerRetention.js +57 -0
- package/dist/plugin/registerRetention.js.map +1 -0
- package/dist/plugin/registerTranslations.js +19 -0
- package/dist/plugin/registerTranslations.js.map +1 -0
- package/dist/plugin/registry.js +11 -0
- package/dist/plugin/registry.js.map +1 -0
- package/dist/plugin/startable.js +21 -0
- package/dist/plugin/startable.js.map +1 -0
- package/dist/session/close.js +65 -0
- package/dist/session/close.js.map +1 -0
- package/dist/session/closeStale.d.ts +11 -0
- package/dist/session/closeStale.js +65 -0
- package/dist/session/closeStale.js.map +1 -0
- package/dist/session/locks.js +32 -0
- package/dist/session/locks.js.map +1 -0
- package/dist/session/resolve.js +91 -0
- package/dist/session/resolve.js.map +1 -0
- package/dist/translations/ar.js +52 -0
- package/dist/translations/ar.js.map +1 -0
- package/dist/translations/de.js +52 -0
- package/dist/translations/de.js.map +1 -0
- package/dist/translations/en.js +52 -0
- package/dist/translations/en.js.map +1 -0
- package/dist/translations/es.js +52 -0
- package/dist/translations/es.js.map +1 -0
- package/dist/translations/fr.js +52 -0
- package/dist/translations/fr.js.map +1 -0
- package/dist/translations/id.js +52 -0
- package/dist/translations/id.js.map +1 -0
- package/dist/translations/index.d.ts +24 -0
- package/dist/translations/index.js +49 -0
- package/dist/translations/index.js.map +1 -0
- package/dist/translations/keys.d.ts +56 -0
- package/dist/translations/keys.js +56 -0
- package/dist/translations/keys.js.map +1 -0
- package/dist/translations/ko.js +52 -0
- package/dist/translations/ko.js.map +1 -0
- package/dist/translations/lookup.js +17 -0
- package/dist/translations/lookup.js.map +1 -0
- package/dist/translations/pt.js +52 -0
- package/dist/translations/pt.js.map +1 -0
- package/dist/translations/ru.js +52 -0
- package/dist/translations/ru.js.map +1 -0
- package/dist/translations/server.js +13 -0
- package/dist/translations/server.js.map +1 -0
- package/dist/translations/uk.js +52 -0
- package/dist/translations/uk.js.map +1 -0
- package/dist/translations/useTranslation.js +12 -0
- package/dist/translations/useTranslation.js.map +1 -0
- package/dist/translations/zh.js +52 -0
- package/dist/translations/zh.js.map +1 -0
- package/dist/types.d.ts +214 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/EndSessionMenuItem.d.ts +5 -0
- package/dist/ui/EndSessionMenuItem.js +45 -0
- package/dist/ui/EndSessionMenuItem.js.map +1 -0
- package/dist/ui/ImpersonateIcon.js +20 -0
- package/dist/ui/ImpersonateIcon.js.map +1 -0
- package/dist/ui/ImpersonationAction.d.ts +10 -0
- package/dist/ui/ImpersonationAction.js +48 -0
- package/dist/ui/ImpersonationAction.js.map +1 -0
- package/dist/ui/ImpersonationBar.d.ts +36 -0
- package/dist/ui/ImpersonationBar.js +357 -0
- package/dist/ui/ImpersonationBar.js.map +1 -0
- package/dist/ui/ImpersonationConfig.js +25 -0
- package/dist/ui/ImpersonationConfig.js.map +1 -0
- package/dist/ui/ImpersonationDocumentButton.d.ts +5 -0
- package/dist/ui/ImpersonationDocumentButton.js +85 -0
- package/dist/ui/ImpersonationDocumentButton.js.map +1 -0
- package/dist/ui/ImpersonationFrontendBanner.d.ts +41 -0
- package/dist/ui/ImpersonationFrontendBanner.js +58 -0
- package/dist/ui/ImpersonationFrontendBanner.js.map +1 -0
- package/dist/ui/ImpersonationFrontendExit.d.ts +17 -0
- package/dist/ui/ImpersonationFrontendExit.js +45 -0
- package/dist/ui/ImpersonationFrontendExit.js.map +1 -0
- package/dist/ui/ImpersonationProvider.d.ts +16 -0
- package/dist/ui/ImpersonationProvider.js +127 -0
- package/dist/ui/ImpersonationProvider.js.map +1 -0
- package/dist/ui/ImpersonationSwitcher.d.ts +21 -0
- package/dist/ui/ImpersonationSwitcher.js +251 -0
- package/dist/ui/ImpersonationSwitcher.js.map +1 -0
- package/dist/ui/ImpersonationUserCard.d.ts +31 -0
- package/dist/ui/ImpersonationUserCard.js +51 -0
- package/dist/ui/ImpersonationUserCard.js.map +1 -0
- package/dist/ui/StartConfirmModal.d.ts +28 -0
- package/dist/ui/StartConfirmModal.js +65 -0
- package/dist/ui/StartConfirmModal.js.map +1 -0
- package/dist/ui/api.js +66 -0
- package/dist/ui/api.js.map +1 -0
- package/dist/ui/barPlacement.js +107 -0
- package/dist/ui/barPlacement.js.map +1 -0
- package/dist/ui/frontendBanner.css +22 -0
- package/dist/ui/impersonation.css +244 -0
- package/dist/ui/useImpersonation.d.ts +43 -0
- package/dist/ui/useImpersonation.js +36 -0
- package/dist/ui/useImpersonation.js.map +1 -0
- package/package.json +126 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { UserLabel } from "../types.js";
|
|
2
|
+
import { Payload } from "payload";
|
|
3
|
+
//#region src/ui/ImpersonationFrontendBanner.d.ts
|
|
4
|
+
type ImpersonationFrontendBannerLabels = {
|
|
5
|
+
/** `{{name}}` is the target per `userLabel`, else collection and id. */actingAs?: string;
|
|
6
|
+
exit?: string;
|
|
7
|
+
};
|
|
8
|
+
type ImpersonationFrontendBannerProps = {
|
|
9
|
+
className?: string;
|
|
10
|
+
headers: Headers;
|
|
11
|
+
labels?: ImpersonationFrontendBannerLabels;
|
|
12
|
+
payload: Payload; /** Skip the default look. Class names stay, so the project styles everything. */
|
|
13
|
+
unstyled?: boolean; /** Name the target by `useAsTitle` or by email. Defaults to the plugin's `ui.userLabel`. */
|
|
14
|
+
userLabel?: UserLabel;
|
|
15
|
+
user?: null | {
|
|
16
|
+
collection?: string;
|
|
17
|
+
id?: number | string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Default frontend banner. Renders nothing when this request is not the
|
|
22
|
+
* impersonated session. Class names are the styling hook. The default look
|
|
23
|
+
* has zero specificity, so a project class overrides it without `!important`;
|
|
24
|
+
* `unstyled` drops it.
|
|
25
|
+
*
|
|
26
|
+
* ```tsx
|
|
27
|
+
* <ImpersonationFrontendBanner payload={payload} headers={await headers()} />
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
declare const ImpersonationFrontendBanner: ({
|
|
31
|
+
className,
|
|
32
|
+
headers,
|
|
33
|
+
labels,
|
|
34
|
+
payload,
|
|
35
|
+
unstyled,
|
|
36
|
+
user,
|
|
37
|
+
userLabel
|
|
38
|
+
}: ImpersonationFrontendBannerProps) => Promise<import("react/jsx-runtime").JSX.Element | null>;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { ImpersonationFrontendBanner };
|
|
41
|
+
//# sourceMappingURL=ImpersonationFrontendBanner.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { labelUser } from "../ids.js";
|
|
2
|
+
import { getRegistry } from "../plugin/registry.js";
|
|
3
|
+
import { getImpersonation } from "../getImpersonation.js";
|
|
4
|
+
import { ImpersonationFrontendExit } from "./ImpersonationFrontendExit.js";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import "./frontendBanner.css";
|
|
7
|
+
//#region src/ui/ImpersonationFrontendBanner.tsx
|
|
8
|
+
const fillName = (template, name) => template.split("{{name}}").join(name);
|
|
9
|
+
/**
|
|
10
|
+
* Default frontend banner. Renders nothing when this request is not the
|
|
11
|
+
* impersonated session. Class names are the styling hook. The default look
|
|
12
|
+
* has zero specificity, so a project class overrides it without `!important`;
|
|
13
|
+
* `unstyled` drops it.
|
|
14
|
+
*
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <ImpersonationFrontendBanner payload={payload} headers={await headers()} />
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
const ImpersonationFrontendBanner = async ({ className, headers, labels, payload, unstyled, user, userLabel }) => {
|
|
20
|
+
const options = getRegistry(payload.config);
|
|
21
|
+
if (!options) return null;
|
|
22
|
+
const status = await getImpersonation(user !== void 0 ? {
|
|
23
|
+
headers,
|
|
24
|
+
payload,
|
|
25
|
+
user
|
|
26
|
+
} : {
|
|
27
|
+
headers,
|
|
28
|
+
payload
|
|
29
|
+
});
|
|
30
|
+
if (!status.active || status.side !== "target") return null;
|
|
31
|
+
const fallback = status.target ? `${status.target.collection}/${status.target.id}` : "user";
|
|
32
|
+
const name = labelUser(userLabel ?? options.ui.userLabel, {
|
|
33
|
+
email: status.targetEmail,
|
|
34
|
+
title: status.targetTitle
|
|
35
|
+
}) ?? fallback;
|
|
36
|
+
const actingAs = fillName(labels?.actingAs ?? "Acting as {{name}}", name);
|
|
37
|
+
const apiPath = `${payload.config.routes.api}${options.apiPath}`;
|
|
38
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
39
|
+
className: [
|
|
40
|
+
"impersonation-frontend-banner",
|
|
41
|
+
unstyled ? null : "impersonation-frontend-banner--default",
|
|
42
|
+
className
|
|
43
|
+
].filter(Boolean).join(" "),
|
|
44
|
+
role: "status",
|
|
45
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
46
|
+
className: "impersonation-frontend-banner__text",
|
|
47
|
+
children: actingAs
|
|
48
|
+
}), /* @__PURE__ */ jsx(ImpersonationFrontendExit, {
|
|
49
|
+
apiPath,
|
|
50
|
+
className: "impersonation-frontend-banner__exit",
|
|
51
|
+
label: labels?.exit ?? "Exit"
|
|
52
|
+
})]
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
//#endregion
|
|
56
|
+
export { ImpersonationFrontendBanner };
|
|
57
|
+
|
|
58
|
+
//# sourceMappingURL=ImpersonationFrontendBanner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImpersonationFrontendBanner.js","names":[],"sources":["../../src/ui/ImpersonationFrontendBanner.tsx"],"sourcesContent":["import type { Payload } from 'payload'\n\nimport { getImpersonation } from '../getImpersonation'\nimport { labelUser } from '../ids'\nimport { getRegistry } from '../plugin/registry'\nimport type { UserLabel } from '../types'\nimport { ImpersonationFrontendExit } from '../ui/ImpersonationFrontendExit'\nimport './frontendBanner.css'\n\nexport type ImpersonationFrontendBannerLabels = {\n\t/** `{{name}}` is the target per `userLabel`, else collection and id. */\n\tactingAs?: string\n\texit?: string\n}\n\nexport type ImpersonationFrontendBannerProps = {\n\tclassName?: string\n\theaders: Headers\n\tlabels?: ImpersonationFrontendBannerLabels\n\tpayload: Payload\n\t/** Skip the default look. Class names stay, so the project styles everything. */\n\tunstyled?: boolean\n\t/** Name the target by `useAsTitle` or by email. Defaults to the plugin's `ui.userLabel`. */\n\tuserLabel?: UserLabel\n\tuser?: null | { collection?: string; id?: number | string }\n}\n\nconst fillName = (template: string, name: string): string => template.split('{{name}}').join(name)\n\n/**\n * Default frontend banner. Renders nothing when this request is not the\n * impersonated session. Class names are the styling hook. The default look\n * has zero specificity, so a project class overrides it without `!important`;\n * `unstyled` drops it.\n *\n * ```tsx\n * <ImpersonationFrontendBanner payload={payload} headers={await headers()} />\n * ```\n */\nexport const ImpersonationFrontendBanner = async ({\n\tclassName,\n\theaders,\n\tlabels,\n\tpayload,\n\tunstyled,\n\tuser,\n\tuserLabel,\n}: ImpersonationFrontendBannerProps) => {\n\tconst options = getRegistry(payload.config)\n\tif (!options) {\n\t\treturn null\n\t}\n\n\tconst status = await getImpersonation(\n\t\tuser !== undefined ? { headers, payload, user: user as never } : { headers, payload }\n\t)\n\tif (!status.active || status.side !== 'target') {\n\t\treturn null\n\t}\n\n\tconst fallback = status.target ? `${status.target.collection}/${status.target.id}` : 'user'\n\tconst name =\n\t\tlabelUser(userLabel ?? options.ui.userLabel, {\n\t\t\temail: status.targetEmail,\n\t\t\ttitle: status.targetTitle,\n\t\t}) ?? fallback\n\tconst actingAs = fillName(labels?.actingAs ?? 'Acting as {{name}}', name)\n\tconst apiPath = `${payload.config.routes.api}${options.apiPath}`\n\tconst rootClass = [\n\t\t'impersonation-frontend-banner',\n\t\tunstyled ? null : 'impersonation-frontend-banner--default',\n\t\tclassName,\n\t]\n\t\t.filter(Boolean)\n\t\t.join(' ')\n\n\treturn (\n\t\t<div className={rootClass} role=\"status\">\n\t\t\t<span className=\"impersonation-frontend-banner__text\">{actingAs}</span>\n\t\t\t<ImpersonationFrontendExit\n\t\t\t\tapiPath={apiPath}\n\t\t\t\tclassName=\"impersonation-frontend-banner__exit\"\n\t\t\t\tlabel={labels?.exit ?? 'Exit'}\n\t\t\t/>\n\t\t</div>\n\t)\n}\n"],"mappings":";;;;;;;AA2BA,MAAM,YAAY,UAAkB,SAAyB,SAAS,MAAM,UAAU,EAAE,KAAK,IAAI;;;;;;;;;;;AAYjG,MAAa,8BAA8B,OAAO,EACjD,WACA,SACA,QACA,SACA,UACA,MACA,gBACuC;CACvC,MAAM,UAAU,YAAY,QAAQ,MAAM;CAC1C,IAAI,CAAC,SACJ,OAAO;CAGR,MAAM,SAAS,MAAM,iBACpB,SAAS,KAAA,IAAY;EAAE;EAAS;EAAe;CAAc,IAAI;EAAE;EAAS;CAAQ,CACrF;CACA,IAAI,CAAC,OAAO,UAAU,OAAO,SAAS,UACrC,OAAO;CAGR,MAAM,WAAW,OAAO,SAAS,GAAG,OAAO,OAAO,WAAW,GAAG,OAAO,OAAO,OAAO;CACrF,MAAM,OACL,UAAU,aAAa,QAAQ,GAAG,WAAW;EAC5C,OAAO,OAAO;EACd,OAAO,OAAO;CACf,CAAC,KAAK;CACP,MAAM,WAAW,SAAS,QAAQ,YAAY,sBAAsB,IAAI;CACxE,MAAM,UAAU,GAAG,QAAQ,OAAO,OAAO,MAAM,QAAQ;CASvD,OACC,qBAAC,OAAD;EAAK,WATY;GACjB;GACA,WAAW,OAAO;GAClB;EACD,EACE,OAAO,OAAO,EACd,KAAK,GAGkB;EAAG,MAAK;YAAhC,CACC,oBAAC,QAAD;GAAM,WAAU;aAAuC;EAAe,CAAA,GACtE,oBAAC,2BAAD;GACU;GACT,WAAU;GACV,OAAO,QAAQ,QAAQ;EACvB,CAAA,CACG;;AAEP"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/ui/ImpersonationFrontendExit.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Posts to exit and reloads on success. A failed or timed-out request logs the
|
|
4
|
+
* reason and re-enables the button, so a second click can retry.
|
|
5
|
+
*/
|
|
6
|
+
declare const ImpersonationFrontendExit: ({
|
|
7
|
+
apiPath,
|
|
8
|
+
className,
|
|
9
|
+
label
|
|
10
|
+
}: {
|
|
11
|
+
apiPath: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
label: string;
|
|
14
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { ImpersonationFrontendExit };
|
|
17
|
+
//# sourceMappingURL=ImpersonationFrontendExit.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { CLIENT_FETCH_TIMEOUT_MS } from "../plugin/constants.js";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
//#region src/ui/ImpersonationFrontendExit.tsx
|
|
6
|
+
/**
|
|
7
|
+
* Posts to exit and reloads on success. A failed or timed-out request logs the
|
|
8
|
+
* reason and re-enables the button, so a second click can retry.
|
|
9
|
+
*/
|
|
10
|
+
const ImpersonationFrontendExit = ({ apiPath, className, label }) => {
|
|
11
|
+
const [busy, setBusy] = useState(false);
|
|
12
|
+
const onClick = async () => {
|
|
13
|
+
setBusy(true);
|
|
14
|
+
try {
|
|
15
|
+
const response = await fetch(`${apiPath}/exit`, {
|
|
16
|
+
body: "{}",
|
|
17
|
+
credentials: "include",
|
|
18
|
+
headers: { "Content-Type": "application/json" },
|
|
19
|
+
method: "POST",
|
|
20
|
+
signal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS)
|
|
21
|
+
});
|
|
22
|
+
if (response.ok) {
|
|
23
|
+
window.location.reload();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const body = await response.json().catch(() => null);
|
|
27
|
+
console.error(`@10x-media/impersonation: exit failed (${response.status}${body?.error ? `, ${body.error}` : ""})`);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error("@10x-media/impersonation: exit request failed", error);
|
|
30
|
+
}
|
|
31
|
+
setBusy(false);
|
|
32
|
+
};
|
|
33
|
+
return /* @__PURE__ */ jsx("button", {
|
|
34
|
+
"aria-busy": busy,
|
|
35
|
+
className,
|
|
36
|
+
disabled: busy,
|
|
37
|
+
onClick: () => void onClick(),
|
|
38
|
+
type: "button",
|
|
39
|
+
children: label
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
export { ImpersonationFrontendExit };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=ImpersonationFrontendExit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImpersonationFrontendExit.js","names":[],"sources":["../../src/ui/ImpersonationFrontendExit.tsx"],"sourcesContent":["'use client'\n\nimport { useState } from 'react'\n\nimport { CLIENT_FETCH_TIMEOUT_MS } from '../plugin/constants'\n\n/**\n * Posts to exit and reloads on success. A failed or timed-out request logs the\n * reason and re-enables the button, so a second click can retry.\n */\nexport const ImpersonationFrontendExit = ({\n\tapiPath,\n\tclassName,\n\tlabel,\n}: {\n\tapiPath: string\n\tclassName?: string\n\tlabel: string\n}) => {\n\tconst [busy, setBusy] = useState(false)\n\n\tconst onClick = async () => {\n\t\tsetBusy(true)\n\t\ttry {\n\t\t\tconst response = await fetch(`${apiPath}/exit`, {\n\t\t\t\tbody: '{}',\n\t\t\t\tcredentials: 'include',\n\t\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\t\tmethod: 'POST',\n\t\t\t\tsignal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS),\n\t\t\t})\n\t\t\tif (response.ok) {\n\t\t\t\twindow.location.reload()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst body = (await response.json().catch(() => null)) as { error?: string } | null\n\t\t\tconsole.error(\n\t\t\t\t`@10x-media/impersonation: exit failed (${response.status}${body?.error ? `, ${body.error}` : ''})`\n\t\t\t)\n\t\t} catch (error) {\n\t\t\tconsole.error('@10x-media/impersonation: exit request failed', error)\n\t\t}\n\t\tsetBusy(false)\n\t}\n\n\treturn (\n\t\t<button\n\t\t\taria-busy={busy}\n\t\t\tclassName={className}\n\t\t\tdisabled={busy}\n\t\t\tonClick={() => void onClick()}\n\t\t\ttype=\"button\"\n\t\t>\n\t\t\t{label}\n\t\t</button>\n\t)\n}\n"],"mappings":";;;;;;;;;AAUA,MAAa,6BAA6B,EACzC,SACA,WACA,YAKK;CACL,MAAM,CAAC,MAAM,WAAW,SAAS,KAAK;CAEtC,MAAM,UAAU,YAAY;EAC3B,QAAQ,IAAI;EACZ,IAAI;GACH,MAAM,WAAW,MAAM,MAAM,GAAG,QAAQ,QAAQ;IAC/C,MAAM;IACN,aAAa;IACb,SAAS,EAAE,gBAAgB,mBAAmB;IAC9C,QAAQ;IACR,QAAQ,YAAY,QAAQ,uBAAuB;GACpD,CAAC;GACD,IAAI,SAAS,IAAI;IAChB,OAAO,SAAS,OAAO;IACvB;GACD;GACA,MAAM,OAAQ,MAAM,SAAS,KAAK,EAAE,YAAY,IAAI;GACpD,QAAQ,MACP,0CAA0C,SAAS,SAAS,MAAM,QAAQ,KAAK,KAAK,UAAU,GAAG,EAClG;EACD,SAAS,OAAO;GACf,QAAQ,MAAM,iDAAiD,KAAK;EACrE;EACA,QAAQ,KAAK;CACd;CAEA,OACC,oBAAC,UAAD;EACC,aAAW;EACA;EACX,UAAU;EACV,eAAe,KAAK,QAAQ;EAC5B,MAAK;YAEJ;CACM,CAAA;AAEV"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ServerProps } from "payload";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/ui/ImpersonationProvider.d.ts
|
|
5
|
+
type ImpersonationProviderProps = ServerProps & {
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
};
|
|
8
|
+
declare const ImpersonationProvider: ({
|
|
9
|
+
children,
|
|
10
|
+
i18n,
|
|
11
|
+
payload,
|
|
12
|
+
user
|
|
13
|
+
}: ImpersonationProviderProps) => Promise<string | number | bigint | boolean | Iterable<ReactNode> | import("react/jsx-runtime").JSX.Element | null | undefined>;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { ImpersonationProvider };
|
|
16
|
+
//# sourceMappingURL=ImpersonationProvider.d.ts.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { labelUser } from "../ids.js";
|
|
2
|
+
import { cachedFindOpenBySid, impersonationSide, isPastAbsoluteExpiry, relationOf } from "../session/resolve.js";
|
|
3
|
+
import "../plugin/constants.js";
|
|
4
|
+
import { getRegistry } from "../plugin/registry.js";
|
|
5
|
+
import { asAuthUser, boundSid } from "../types.js";
|
|
6
|
+
import { statusFromRow } from "../getImpersonation.js";
|
|
7
|
+
import { readHintCookie } from "../plugin/lookup.js";
|
|
8
|
+
import { startableCollectionSlugs } from "../plugin/startable.js";
|
|
9
|
+
import { keys } from "../translations/keys.js";
|
|
10
|
+
import { resolveTargetFilters } from "../access/filterTargets.js";
|
|
11
|
+
import { ImpersonationClientConfig } from "./ImpersonationConfig.js";
|
|
12
|
+
import { fillTemplate, messageFor } from "../translations/lookup.js";
|
|
13
|
+
import { ImpersonationBar } from "./ImpersonationBar.js";
|
|
14
|
+
import { createLocalReq } from "payload";
|
|
15
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
16
|
+
import { headers } from "next/headers";
|
|
17
|
+
//#region src/ui/ImpersonationProvider.tsx
|
|
18
|
+
const ImpersonationProvider = async ({ children, i18n, payload, user }) => {
|
|
19
|
+
const options = getRegistry(payload.config);
|
|
20
|
+
if (!options) return children;
|
|
21
|
+
const startable = startableCollectionSlugs(payload.config.collections, options);
|
|
22
|
+
let targets = Object.fromEntries(startable.map((slug) => [slug, true]));
|
|
23
|
+
if (user && options.access.filterTargets) targets = await resolveTargetFilters({
|
|
24
|
+
collections: startable,
|
|
25
|
+
options,
|
|
26
|
+
req: await createLocalReq({
|
|
27
|
+
req: {
|
|
28
|
+
headers: await headers(),
|
|
29
|
+
i18n
|
|
30
|
+
},
|
|
31
|
+
user
|
|
32
|
+
}, payload)
|
|
33
|
+
});
|
|
34
|
+
const apiPath = `${payload.config.routes.api}${options.apiPath}`;
|
|
35
|
+
const sid = boundSid(user, options.session.binding);
|
|
36
|
+
const painted = user ? asAuthUser(user)._impersonation : void 0;
|
|
37
|
+
const paintedLive = painted && !isPastAbsoluteExpiry(painted) ? painted : void 0;
|
|
38
|
+
const hint = await readHintCookie(options.hintCookieName);
|
|
39
|
+
const shouldLookup = !painted && (Boolean(options.session.issue) || Boolean(hint) || Boolean(sid?.startsWith("imp_")));
|
|
40
|
+
let row = null;
|
|
41
|
+
if (!painted && user && sid && shouldLookup) {
|
|
42
|
+
row = await cachedFindOpenBySid(sid, options, payload);
|
|
43
|
+
if (row && isPastAbsoluteExpiry(row)) row = null;
|
|
44
|
+
}
|
|
45
|
+
const status = paintedLive ? {
|
|
46
|
+
absoluteExpiresAt: paintedLive.absoluteExpiresAt ?? null,
|
|
47
|
+
active: true,
|
|
48
|
+
impersonator: paintedLive.impersonator,
|
|
49
|
+
impersonatorLocale: paintedLive.impersonatorLocale ?? null,
|
|
50
|
+
mode: paintedLive.mode,
|
|
51
|
+
side: "target",
|
|
52
|
+
startedAt: paintedLive.startedAt,
|
|
53
|
+
target: paintedLive.target,
|
|
54
|
+
targetEmail: paintedLive.targetEmail ?? null,
|
|
55
|
+
targetTitle: paintedLive.targetTitle ?? paintedLive.targetEmail ?? null
|
|
56
|
+
} : row && sid ? statusFromRow(row, sid) : { active: false };
|
|
57
|
+
const barSource = paintedLive ? {
|
|
58
|
+
absoluteExpiresAt: paintedLive.absoluteExpiresAt,
|
|
59
|
+
impersonatorEmail: paintedLive.impersonatorEmail,
|
|
60
|
+
impersonatorId: paintedLive.impersonator.id,
|
|
61
|
+
impersonatorTitle: paintedLive.impersonatorTitle,
|
|
62
|
+
impersonatorLocale: paintedLive.impersonatorLocale,
|
|
63
|
+
mode: paintedLive.mode,
|
|
64
|
+
side: "target",
|
|
65
|
+
targetEmail: paintedLive.targetEmail,
|
|
66
|
+
targetId: paintedLive.target.id,
|
|
67
|
+
targetTitle: paintedLive.targetTitle
|
|
68
|
+
} : row && sid ? {
|
|
69
|
+
absoluteExpiresAt: row.absoluteExpiresAt,
|
|
70
|
+
impersonatorEmail: row.impersonatorEmail,
|
|
71
|
+
impersonatorId: relationOf(row.impersonator)?.id,
|
|
72
|
+
impersonatorTitle: row.impersonatorTitle,
|
|
73
|
+
impersonatorLocale: row.impersonatorLocale,
|
|
74
|
+
mode: row.mode,
|
|
75
|
+
side: impersonationSide(row, sid),
|
|
76
|
+
targetEmail: row.targetEmail,
|
|
77
|
+
targetId: relationOf(row.target)?.id,
|
|
78
|
+
targetTitle: row.targetTitle
|
|
79
|
+
} : null;
|
|
80
|
+
const clientValue = {
|
|
81
|
+
apiPath,
|
|
82
|
+
cardEmail: options.ui.cardEmail,
|
|
83
|
+
reasonMode: options.reason,
|
|
84
|
+
sessionCollection: options.collectionSlug,
|
|
85
|
+
status,
|
|
86
|
+
targetFilters: targets
|
|
87
|
+
};
|
|
88
|
+
if (!options.ui.bar || !barSource) return /* @__PURE__ */ jsx(ImpersonationClientConfig, {
|
|
89
|
+
value: clientValue,
|
|
90
|
+
children
|
|
91
|
+
});
|
|
92
|
+
const locale = barSource.impersonatorLocale;
|
|
93
|
+
const targetName = labelUser(options.ui.userLabel, {
|
|
94
|
+
email: barSource.targetEmail,
|
|
95
|
+
title: barSource.targetTitle
|
|
96
|
+
}) ?? String(barSource.targetId ?? "");
|
|
97
|
+
const impersonatorLabel = labelUser(options.ui.userLabel, {
|
|
98
|
+
email: barSource.impersonatorEmail,
|
|
99
|
+
title: barSource.impersonatorTitle
|
|
100
|
+
}) ?? String(barSource.impersonatorId ?? "");
|
|
101
|
+
const isImpersonatorSide = barSource.side === "impersonator" && barSource.mode === "parallel";
|
|
102
|
+
const actingAs = isImpersonatorSide ? fillTemplate(messageFor(locale, keys.sessionActiveOnSite), "{{name}}", String(targetName)) : fillTemplate(messageFor(locale, keys.actingAs), "{{name}}", String(targetName));
|
|
103
|
+
const returnTo = isImpersonatorSide ? messageFor(locale, keys.endSession) : fillTemplate(messageFor(locale, keys.returnTo), "{{name}}", String(impersonatorLabel));
|
|
104
|
+
return /* @__PURE__ */ jsxs(ImpersonationClientConfig, {
|
|
105
|
+
value: clientValue,
|
|
106
|
+
children: [children, /* @__PURE__ */ jsx(ImpersonationBar, {
|
|
107
|
+
actingAs,
|
|
108
|
+
adminRoute: payload.config.routes.admin,
|
|
109
|
+
apiPath,
|
|
110
|
+
frontendUrl: payload.config.serverURL || void 0,
|
|
111
|
+
impersonatorLocale: locale,
|
|
112
|
+
openWebsite: messageFor(locale, keys.openWebsite),
|
|
113
|
+
pluginName: messageFor(locale, keys.pluginName),
|
|
114
|
+
returnTo,
|
|
115
|
+
sessionEndsAt: barSource.absoluteExpiresAt,
|
|
116
|
+
sessionEndsAtTemplate: messageFor(locale, keys.sessionEndsAt),
|
|
117
|
+
sessionEndsInTemplate: messageFor(locale, keys.sessionEndsIn),
|
|
118
|
+
showFrontendLink: isImpersonatorSide,
|
|
119
|
+
targetName,
|
|
120
|
+
tone: isImpersonatorSide ? "quiet" : "warning"
|
|
121
|
+
})]
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
//#endregion
|
|
125
|
+
export { ImpersonationProvider };
|
|
126
|
+
|
|
127
|
+
//# sourceMappingURL=ImpersonationProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImpersonationProvider.js","names":["nextHeaders"],"sources":["../../src/ui/ImpersonationProvider.tsx"],"sourcesContent":["import { headers as nextHeaders } from 'next/headers'\nimport type { ServerProps } from 'payload'\nimport { createLocalReq } from 'payload'\nimport type { ReactNode } from 'react'\n\nimport { resolveTargetFilters } from '../access/filterTargets'\nimport { statusFromRow } from '../getImpersonation'\nimport { labelUser } from '../ids'\nimport { IMPERSONATION_SID_PREFIX } from '../plugin/constants'\nimport { readHintCookie } from '../plugin/lookup'\nimport { getRegistry } from '../plugin/registry'\nimport { startableCollectionSlugs } from '../plugin/startable'\nimport {\n\tcachedFindOpenBySid,\n\timpersonationSide,\n\tisPastAbsoluteExpiry,\n\trelationOf,\n} from '../session/resolve'\nimport { keys } from '../translations/keys'\nimport { fillTemplate, messageFor } from '../translations/lookup'\nimport { asAuthUser, boundSid } from '../types'\nimport { ImpersonationBar } from './ImpersonationBar'\nimport { ImpersonationClientConfig } from './ImpersonationConfig'\n\nexport type ImpersonationProviderProps = ServerProps & { children?: ReactNode }\n\nexport const ImpersonationProvider = async ({\n\tchildren,\n\ti18n,\n\tpayload,\n\tuser,\n}: ImpersonationProviderProps) => {\n\tconst options = getRegistry(payload.config)\n\tif (!options) {\n\t\treturn children\n\t}\n\n\tconst startable = startableCollectionSlugs(payload.config.collections, options)\n\tlet targets: Awaited<ReturnType<typeof resolveTargetFilters>> = Object.fromEntries(\n\t\tstartable.map((slug) => [slug, true as const])\n\t)\n\tif (user && options.access.filterTargets) {\n\t\tconst headerList = await nextHeaders()\n\t\tconst req = await createLocalReq({ req: { headers: headerList, i18n } as never, user }, payload)\n\t\ttargets = await resolveTargetFilters({ collections: startable, options, req })\n\t}\n\n\tconst apiPath = `${payload.config.routes.api}${options.apiPath}`\n\tconst sid = boundSid(user, options.session.binding)\n\tconst painted = user ? asAuthUser(user)._impersonation : undefined\n\tconst paintedLive = painted && !isPastAbsoluteExpiry(painted) ? painted : undefined\n\tconst hint = await readHintCookie(options.hintCookieName)\n\tconst shouldLookup =\n\t\t!painted &&\n\t\t(Boolean(options.session.issue) ||\n\t\t\tBoolean(hint) ||\n\t\t\tBoolean(sid?.startsWith(IMPERSONATION_SID_PREFIX)))\n\n\tlet row = null\n\tif (!painted && user && sid && shouldLookup) {\n\t\trow = await cachedFindOpenBySid(sid, options, payload)\n\t\tif (row && isPastAbsoluteExpiry(row)) {\n\t\t\trow = null\n\t\t}\n\t}\n\n\tconst status = paintedLive\n\t\t? {\n\t\t\t\tabsoluteExpiresAt: paintedLive.absoluteExpiresAt ?? null,\n\t\t\t\tactive: true as const,\n\t\t\t\timpersonator: paintedLive.impersonator,\n\t\t\t\timpersonatorLocale: paintedLive.impersonatorLocale ?? null,\n\t\t\t\tmode: paintedLive.mode,\n\t\t\t\tside: 'target' as const,\n\t\t\t\tstartedAt: paintedLive.startedAt,\n\t\t\t\ttarget: paintedLive.target,\n\t\t\t\ttargetEmail: paintedLive.targetEmail ?? null,\n\t\t\t\ttargetTitle: paintedLive.targetTitle ?? paintedLive.targetEmail ?? null,\n\t\t\t}\n\t\t: row && sid\n\t\t\t? statusFromRow(row, sid)\n\t\t\t: { active: false as const }\n\n\tconst barSource = paintedLive\n\t\t? {\n\t\t\t\tabsoluteExpiresAt: paintedLive.absoluteExpiresAt,\n\t\t\t\timpersonatorEmail: paintedLive.impersonatorEmail,\n\t\t\t\timpersonatorId: paintedLive.impersonator.id,\n\t\t\t\timpersonatorTitle: paintedLive.impersonatorTitle,\n\t\t\t\timpersonatorLocale: paintedLive.impersonatorLocale,\n\t\t\t\tmode: paintedLive.mode,\n\t\t\t\tside: 'target' as const,\n\t\t\t\ttargetEmail: paintedLive.targetEmail,\n\t\t\t\ttargetId: paintedLive.target.id,\n\t\t\t\ttargetTitle: paintedLive.targetTitle,\n\t\t\t}\n\t\t: row && sid\n\t\t\t? {\n\t\t\t\t\tabsoluteExpiresAt: row.absoluteExpiresAt,\n\t\t\t\t\timpersonatorEmail: row.impersonatorEmail,\n\t\t\t\t\timpersonatorId: relationOf(row.impersonator)?.id,\n\t\t\t\t\timpersonatorTitle: row.impersonatorTitle,\n\t\t\t\t\timpersonatorLocale: row.impersonatorLocale,\n\t\t\t\t\tmode: row.mode,\n\t\t\t\t\tside: impersonationSide(row, sid),\n\t\t\t\t\ttargetEmail: row.targetEmail,\n\t\t\t\t\ttargetId: relationOf(row.target)?.id,\n\t\t\t\t\ttargetTitle: row.targetTitle,\n\t\t\t\t}\n\t\t\t: null\n\n\tconst clientValue = {\n\t\tapiPath,\n\t\tcardEmail: options.ui.cardEmail,\n\t\treasonMode: options.reason,\n\t\tsessionCollection: options.collectionSlug,\n\t\tstatus,\n\t\ttargetFilters: targets,\n\t}\n\n\tif (!options.ui.bar || !barSource) {\n\t\treturn <ImpersonationClientConfig value={clientValue}>{children}</ImpersonationClientConfig>\n\t}\n\n\tconst locale = barSource.impersonatorLocale\n\tconst targetName =\n\t\tlabelUser(options.ui.userLabel, {\n\t\t\temail: barSource.targetEmail,\n\t\t\ttitle: barSource.targetTitle,\n\t\t}) ?? String(barSource.targetId ?? '')\n\tconst impersonatorLabel =\n\t\tlabelUser(options.ui.userLabel, {\n\t\t\temail: barSource.impersonatorEmail,\n\t\t\ttitle: barSource.impersonatorTitle,\n\t\t}) ?? String(barSource.impersonatorId ?? '')\n\tconst isImpersonatorSide = barSource.side === 'impersonator' && barSource.mode === 'parallel'\n\tconst actingAs = isImpersonatorSide\n\t\t? fillTemplate(messageFor(locale, keys.sessionActiveOnSite), '{{name}}', String(targetName))\n\t\t: fillTemplate(messageFor(locale, keys.actingAs), '{{name}}', String(targetName))\n\tconst returnTo = isImpersonatorSide\n\t\t? messageFor(locale, keys.endSession)\n\t\t: fillTemplate(messageFor(locale, keys.returnTo), '{{name}}', String(impersonatorLabel))\n\n\treturn (\n\t\t<ImpersonationClientConfig value={clientValue}>\n\t\t\t{children}\n\t\t\t<ImpersonationBar\n\t\t\t\tactingAs={actingAs}\n\t\t\t\tadminRoute={payload.config.routes.admin}\n\t\t\t\tapiPath={apiPath}\n\t\t\t\tfrontendUrl={payload.config.serverURL || undefined}\n\t\t\t\timpersonatorLocale={locale}\n\t\t\t\topenWebsite={messageFor(locale, keys.openWebsite)}\n\t\t\t\tpluginName={messageFor(locale, keys.pluginName)}\n\t\t\t\treturnTo={returnTo}\n\t\t\t\tsessionEndsAt={barSource.absoluteExpiresAt}\n\t\t\t\tsessionEndsAtTemplate={messageFor(locale, keys.sessionEndsAt)}\n\t\t\t\tsessionEndsInTemplate={messageFor(locale, keys.sessionEndsIn)}\n\t\t\t\tshowFrontendLink={isImpersonatorSide}\n\t\t\t\ttargetName={targetName}\n\t\t\t\ttone={isImpersonatorSide ? 'quiet' : 'warning'}\n\t\t\t/>\n\t\t</ImpersonationClientConfig>\n\t)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ImpersonationUserCardProps } from "./ImpersonationUserCard.js";
|
|
2
|
+
import { ComponentType } from "react";
|
|
3
|
+
//#region src/ui/ImpersonationSwitcher.d.ts
|
|
4
|
+
type SwitcherCollection = {
|
|
5
|
+
label: string;
|
|
6
|
+
slug: string;
|
|
7
|
+
useAsTitle: string;
|
|
8
|
+
};
|
|
9
|
+
type ImpersonationSwitcherProps = {
|
|
10
|
+
Card?: ComponentType<ImpersonationUserCardProps>;
|
|
11
|
+
collections: SwitcherCollection[];
|
|
12
|
+
viewerId: number | string;
|
|
13
|
+
};
|
|
14
|
+
declare const ImpersonationSwitcher: ({
|
|
15
|
+
Card,
|
|
16
|
+
collections,
|
|
17
|
+
viewerId
|
|
18
|
+
}: ImpersonationSwitcherProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { ImpersonationSwitcher };
|
|
21
|
+
//# sourceMappingURL=ImpersonationSwitcher.d.ts.map
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { keys } from "../translations/keys.js";
|
|
3
|
+
import { useTranslation as useTranslation$1 } from "../translations/useTranslation.js";
|
|
4
|
+
import { useImpersonation } from "./useImpersonation.js";
|
|
5
|
+
import { ImpersonateIcon } from "./ImpersonateIcon.js";
|
|
6
|
+
import { StartConfirmModal } from "./StartConfirmModal.js";
|
|
7
|
+
import { ImpersonationUserCard } from "./ImpersonationUserCard.js";
|
|
8
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
9
|
+
import { Button, ChevronIcon, Drawer, Pagination, Pill, Popup, PopupList, RenderCustomComponent, SearchFilter, SearchIcon, toast, useConfig, useDocumentDrawer, useModal } from "@payloadcms/ui";
|
|
10
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
import "./impersonation.css";
|
|
12
|
+
//#region src/ui/ImpersonationSwitcher.tsx
|
|
13
|
+
const drawerSlug = "impersonation-switcher";
|
|
14
|
+
const confirmSlug = "impersonation-confirm-start";
|
|
15
|
+
const andWhere = (clauses) => {
|
|
16
|
+
const compact = clauses.filter((clause) => Object.keys(clause).length > 0);
|
|
17
|
+
if (compact.length === 0) return {};
|
|
18
|
+
if (compact.length === 1) return compact[0];
|
|
19
|
+
return { and: compact };
|
|
20
|
+
};
|
|
21
|
+
const PreviewDrawer = ({ collectionSlug, id }) => {
|
|
22
|
+
const [DocumentDrawer, , { openDrawer }] = useDocumentDrawer({
|
|
23
|
+
collectionSlug,
|
|
24
|
+
id: String(id)
|
|
25
|
+
});
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
openDrawer();
|
|
28
|
+
}, [openDrawer]);
|
|
29
|
+
return /* @__PURE__ */ jsx(DocumentDrawer, {});
|
|
30
|
+
};
|
|
31
|
+
const ImpersonationSwitcher = ({ Card, collections, viewerId }) => {
|
|
32
|
+
const { config } = useConfig();
|
|
33
|
+
const { t } = useTranslation$1();
|
|
34
|
+
const { closeModal, isModalOpen, openModal } = useModal();
|
|
35
|
+
const { apiPath, cardEmail, reasonMode, targetFilters } = useImpersonation();
|
|
36
|
+
const [collection, setCollection] = useState(collections[0]?.slug ?? "");
|
|
37
|
+
const [collectionMenuOpen, setCollectionMenuOpen] = useState(false);
|
|
38
|
+
const [search, setSearch] = useState("");
|
|
39
|
+
const [page, setPage] = useState(1);
|
|
40
|
+
const [docs, setDocs] = useState([]);
|
|
41
|
+
const [hasNext, setHasNext] = useState(false);
|
|
42
|
+
const [hasPrev, setHasPrev] = useState(false);
|
|
43
|
+
const [totalPages, setTotalPages] = useState(1);
|
|
44
|
+
const [loading, setLoading] = useState(false);
|
|
45
|
+
const [target, setTarget] = useState(null);
|
|
46
|
+
const [preview, setPreview] = useState(null);
|
|
47
|
+
const visibleCollections = useMemo(() => collections.filter((entry) => targetFilters[entry.slug] !== void 0), [collections, targetFilters]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (visibleCollections.some((entry) => entry.slug === collection)) return;
|
|
50
|
+
setCollection(visibleCollections[0]?.slug ?? "");
|
|
51
|
+
}, [collection, visibleCollections]);
|
|
52
|
+
const useAsTitle = visibleCollections.find((entry) => entry.slug === collection)?.useAsTitle ?? "email";
|
|
53
|
+
const onSearch = useCallback((value) => {
|
|
54
|
+
setPage(1);
|
|
55
|
+
setSearch(value ?? "");
|
|
56
|
+
}, []);
|
|
57
|
+
const listAbort = useRef(null);
|
|
58
|
+
const switcherRef = useRef(null);
|
|
59
|
+
const resultsRef = useRef(null);
|
|
60
|
+
const changePage = (next) => {
|
|
61
|
+
setPage(next);
|
|
62
|
+
switcherRef.current?.closest(".drawer__content-children")?.scrollTo({ top: 0 });
|
|
63
|
+
resultsRef.current?.scrollTo({ top: 0 });
|
|
64
|
+
};
|
|
65
|
+
const load = useCallback(async () => {
|
|
66
|
+
if (!collection || !isModalOpen(drawerSlug)) return;
|
|
67
|
+
listAbort.current?.abort();
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
listAbort.current = controller;
|
|
70
|
+
setLoading(true);
|
|
71
|
+
try {
|
|
72
|
+
const filter = targetFilters[collection];
|
|
73
|
+
const clauses = [];
|
|
74
|
+
if (filter && filter !== true) clauses.push(filter);
|
|
75
|
+
if (collection === config.admin.user && viewerId !== void 0 && viewerId !== "") clauses.push({ id: { not_equals: viewerId } });
|
|
76
|
+
const trimmed = search.trim();
|
|
77
|
+
if (trimmed) {
|
|
78
|
+
const fields = new Set(["email", useAsTitle]);
|
|
79
|
+
clauses.push({ or: [...fields].map((field) => ({ [field]: { like: trimmed } })) });
|
|
80
|
+
}
|
|
81
|
+
const params = new URLSearchParams({
|
|
82
|
+
depth: "0",
|
|
83
|
+
limit: String(20),
|
|
84
|
+
page: String(page),
|
|
85
|
+
sort: useAsTitle,
|
|
86
|
+
where: JSON.stringify(andWhere(clauses))
|
|
87
|
+
});
|
|
88
|
+
const response = await fetch(`${config.routes.api}/${collection}?${params.toString()}`, {
|
|
89
|
+
credentials: "include",
|
|
90
|
+
signal: controller.signal
|
|
91
|
+
});
|
|
92
|
+
if (!response.ok) throw new Error(String(response.status));
|
|
93
|
+
const body = await response.json();
|
|
94
|
+
setDocs(body.docs ?? []);
|
|
95
|
+
setHasNext(Boolean(body.hasNextPage));
|
|
96
|
+
setHasPrev(Boolean(body.hasPrevPage));
|
|
97
|
+
setTotalPages(body.totalPages ?? 1);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (controller.signal.aborted || error instanceof DOMException && error.name === "AbortError") return;
|
|
100
|
+
toast.error(t(keys.errorFailed));
|
|
101
|
+
setDocs([]);
|
|
102
|
+
setHasNext(false);
|
|
103
|
+
setHasPrev(false);
|
|
104
|
+
setTotalPages(1);
|
|
105
|
+
} finally {
|
|
106
|
+
if (!controller.signal.aborted) setLoading(false);
|
|
107
|
+
}
|
|
108
|
+
}, [
|
|
109
|
+
collection,
|
|
110
|
+
config.admin.user,
|
|
111
|
+
config.routes.api,
|
|
112
|
+
isModalOpen,
|
|
113
|
+
page,
|
|
114
|
+
search,
|
|
115
|
+
t,
|
|
116
|
+
targetFilters,
|
|
117
|
+
useAsTitle,
|
|
118
|
+
viewerId
|
|
119
|
+
]);
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
load();
|
|
122
|
+
}, [load]);
|
|
123
|
+
const pick = (doc) => {
|
|
124
|
+
const title = String(doc[useAsTitle] ?? doc.email ?? doc.id);
|
|
125
|
+
setTarget({
|
|
126
|
+
collection,
|
|
127
|
+
id: doc.id,
|
|
128
|
+
label: title
|
|
129
|
+
});
|
|
130
|
+
closeModal(drawerSlug);
|
|
131
|
+
openModal(confirmSlug);
|
|
132
|
+
};
|
|
133
|
+
if (visibleCollections.length === 0) return null;
|
|
134
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
135
|
+
/* @__PURE__ */ jsx(Button, {
|
|
136
|
+
buttonStyle: "subtle",
|
|
137
|
+
className: "impersonation-switcher-button",
|
|
138
|
+
margin: false,
|
|
139
|
+
onClick: () => {
|
|
140
|
+
setPage(1);
|
|
141
|
+
openModal(drawerSlug);
|
|
142
|
+
},
|
|
143
|
+
size: "small",
|
|
144
|
+
icon: /* @__PURE__ */ jsx(ImpersonateIcon, {}),
|
|
145
|
+
iconPosition: "left",
|
|
146
|
+
children: t(keys.switchToUser)
|
|
147
|
+
}),
|
|
148
|
+
/* @__PURE__ */ jsx(Drawer, {
|
|
149
|
+
slug: drawerSlug,
|
|
150
|
+
title: t(keys.switchToUser),
|
|
151
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
152
|
+
className: "impersonation-switcher",
|
|
153
|
+
ref: switcherRef,
|
|
154
|
+
children: [
|
|
155
|
+
/* @__PURE__ */ jsxs("div", {
|
|
156
|
+
className: "search-bar",
|
|
157
|
+
children: [
|
|
158
|
+
/* @__PURE__ */ jsx(SearchIcon, {}),
|
|
159
|
+
/* @__PURE__ */ jsx(SearchFilter, {
|
|
160
|
+
handleChange: onSearch,
|
|
161
|
+
label: t(keys.searchByName)
|
|
162
|
+
}),
|
|
163
|
+
visibleCollections.length > 1 ? /* @__PURE__ */ jsx("div", {
|
|
164
|
+
className: "search-bar__actions",
|
|
165
|
+
children: /* @__PURE__ */ jsx(Popup, {
|
|
166
|
+
button: /* @__PURE__ */ jsx(Pill, {
|
|
167
|
+
className: "impersonation-collection",
|
|
168
|
+
icon: /* @__PURE__ */ jsx(ChevronIcon, { direction: collectionMenuOpen ? "up" : "down" }),
|
|
169
|
+
pillStyle: "light",
|
|
170
|
+
size: "small",
|
|
171
|
+
children: visibleCollections.find((entry) => entry.slug === collection)?.label ?? collection
|
|
172
|
+
}),
|
|
173
|
+
buttonType: "custom",
|
|
174
|
+
caret: false,
|
|
175
|
+
horizontalAlign: "right",
|
|
176
|
+
onToggleOpen: setCollectionMenuOpen,
|
|
177
|
+
render: ({ close }) => /* @__PURE__ */ jsx(PopupList.ButtonGroup, { children: visibleCollections.map((entry) => /* @__PURE__ */ jsx(PopupList.Button, {
|
|
178
|
+
active: entry.slug === collection,
|
|
179
|
+
onClick: () => {
|
|
180
|
+
setCollection(entry.slug);
|
|
181
|
+
setPage(1);
|
|
182
|
+
close();
|
|
183
|
+
},
|
|
184
|
+
children: entry.label
|
|
185
|
+
}, entry.slug)) }),
|
|
186
|
+
size: "fit-content"
|
|
187
|
+
})
|
|
188
|
+
}) : null
|
|
189
|
+
]
|
|
190
|
+
}),
|
|
191
|
+
/* @__PURE__ */ jsxs("div", {
|
|
192
|
+
className: "impersonation-switcher__results",
|
|
193
|
+
ref: resultsRef,
|
|
194
|
+
children: [docs.length === 0 && !loading ? /* @__PURE__ */ jsx("p", {
|
|
195
|
+
className: "impersonation-switcher__empty",
|
|
196
|
+
children: t(keys.noResults)
|
|
197
|
+
}) : null, docs.map((doc) => {
|
|
198
|
+
const title = String(doc[useAsTitle] ?? doc.email ?? doc.id);
|
|
199
|
+
const email = typeof doc.email === "string" ? doc.email : void 0;
|
|
200
|
+
const showEmail = cardEmail && email && email !== title;
|
|
201
|
+
const cardProps = {
|
|
202
|
+
collectionSlug: collection,
|
|
203
|
+
doc,
|
|
204
|
+
documentHref: `${config.routes.admin}/collections/${collection}/${doc.id}`,
|
|
205
|
+
email: showEmail ? email : void 0,
|
|
206
|
+
onOpenDrawer: () => setPreview({
|
|
207
|
+
collection,
|
|
208
|
+
id: doc.id
|
|
209
|
+
}),
|
|
210
|
+
onSelect: () => pick(doc),
|
|
211
|
+
openDocumentLabel: t(keys.openDocument),
|
|
212
|
+
openDrawerLabel: t(keys.openDrawer),
|
|
213
|
+
title
|
|
214
|
+
};
|
|
215
|
+
return /* @__PURE__ */ jsx(RenderCustomComponent, {
|
|
216
|
+
CustomComponent: Card ? /* @__PURE__ */ jsx(Card, { ...cardProps }) : void 0,
|
|
217
|
+
Fallback: /* @__PURE__ */ jsx(ImpersonationUserCard, { ...cardProps })
|
|
218
|
+
}, String(doc.id));
|
|
219
|
+
})]
|
|
220
|
+
}),
|
|
221
|
+
totalPages > 1 ? /* @__PURE__ */ jsx("div", {
|
|
222
|
+
className: "impersonation-switcher__pager",
|
|
223
|
+
children: /* @__PURE__ */ jsx(Pagination, {
|
|
224
|
+
hasNextPage: hasNext,
|
|
225
|
+
hasPrevPage: hasPrev,
|
|
226
|
+
nextPage: page + 1,
|
|
227
|
+
onChange: changePage,
|
|
228
|
+
page,
|
|
229
|
+
prevPage: Math.max(1, page - 1),
|
|
230
|
+
totalPages
|
|
231
|
+
})
|
|
232
|
+
}) : null
|
|
233
|
+
]
|
|
234
|
+
})
|
|
235
|
+
}),
|
|
236
|
+
preview ? /* @__PURE__ */ jsx(PreviewDrawer, {
|
|
237
|
+
collectionSlug: preview.collection,
|
|
238
|
+
id: preview.id
|
|
239
|
+
}, `${preview.collection}:${preview.id}`) : null,
|
|
240
|
+
/* @__PURE__ */ jsx(StartConfirmModal, {
|
|
241
|
+
apiPath,
|
|
242
|
+
modalSlug: confirmSlug,
|
|
243
|
+
reasonMode,
|
|
244
|
+
target
|
|
245
|
+
}, target ? `${target.collection}:${target.id}` : "idle")
|
|
246
|
+
] });
|
|
247
|
+
};
|
|
248
|
+
//#endregion
|
|
249
|
+
export { ImpersonationSwitcher };
|
|
250
|
+
|
|
251
|
+
//# sourceMappingURL=ImpersonationSwitcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImpersonationSwitcher.js","names":["useTranslation"],"sources":["../../src/ui/ImpersonationSwitcher.tsx"],"sourcesContent":["'use client'\n\nimport {\n\tButton,\n\tChevronIcon,\n\tDrawer,\n\tPagination,\n\tPill,\n\tPopup,\n\tPopupList,\n\tRenderCustomComponent,\n\tSearchFilter,\n\tSearchIcon,\n\ttoast,\n\tuseConfig,\n\tuseDocumentDrawer,\n\tuseModal,\n} from '@payloadcms/ui'\nimport type { CollectionSlug, Where } from 'payload'\nimport { type ComponentType, useCallback, useEffect, useMemo, useRef, useState } from 'react'\n\nimport { SWITCHER_PAGE_SIZE } from '../plugin/constants'\nimport { keys } from '../translations/keys'\nimport { useTranslation } from '../translations/useTranslation'\nimport { ImpersonateIcon } from './ImpersonateIcon'\nimport { ImpersonationUserCard, type ImpersonationUserCardProps } from './ImpersonationUserCard'\nimport { StartConfirmModal, type StartTarget } from './StartConfirmModal'\nimport { useImpersonation } from './useImpersonation'\nimport './impersonation.css'\n\nexport type SwitcherCollection = {\n\tlabel: string\n\tslug: string\n\tuseAsTitle: string\n}\n\nexport type ImpersonationSwitcherProps = {\n\tCard?: ComponentType<ImpersonationUserCardProps>\n\tcollections: SwitcherCollection[]\n\tviewerId: number | string\n}\n\ntype ListedUser = Record<string, unknown> & { id: number | string }\n\nconst drawerSlug = 'impersonation-switcher'\nconst confirmSlug = 'impersonation-confirm-start'\n\nconst andWhere = (clauses: Where[]): Where => {\n\tconst compact = clauses.filter((clause) => Object.keys(clause).length > 0)\n\tif (compact.length === 0) {\n\t\treturn {}\n\t}\n\tif (compact.length === 1) {\n\t\treturn compact[0] as Where\n\t}\n\treturn { and: compact }\n}\n\nconst PreviewDrawer = ({ collectionSlug, id }: { collectionSlug: string; id: number | string }) => {\n\tconst [DocumentDrawer, , { openDrawer }] = useDocumentDrawer({\n\t\tcollectionSlug: collectionSlug as CollectionSlug,\n\t\tid: String(id),\n\t})\n\n\tuseEffect(() => {\n\t\topenDrawer()\n\t}, [openDrawer])\n\n\treturn <DocumentDrawer />\n}\n\nexport const ImpersonationSwitcher = ({\n\tCard,\n\tcollections,\n\tviewerId,\n}: ImpersonationSwitcherProps) => {\n\tconst { config } = useConfig()\n\tconst { t } = useTranslation()\n\tconst { closeModal, isModalOpen, openModal } = useModal()\n\tconst { apiPath, cardEmail, reasonMode, targetFilters } = useImpersonation()\n\tconst [collection, setCollection] = useState(collections[0]?.slug ?? '')\n\tconst [collectionMenuOpen, setCollectionMenuOpen] = useState(false)\n\tconst [search, setSearch] = useState('')\n\tconst [page, setPage] = useState(1)\n\tconst [docs, setDocs] = useState<ListedUser[]>([])\n\tconst [hasNext, setHasNext] = useState(false)\n\tconst [hasPrev, setHasPrev] = useState(false)\n\tconst [totalPages, setTotalPages] = useState(1)\n\tconst [loading, setLoading] = useState(false)\n\tconst [target, setTarget] = useState<null | StartTarget>(null)\n\tconst [preview, setPreview] = useState<null | { collection: string; id: number | string }>(null)\n\n\tconst visibleCollections = useMemo(\n\t\t() => collections.filter((entry) => targetFilters[entry.slug] !== undefined),\n\t\t[collections, targetFilters]\n\t)\n\n\tuseEffect(() => {\n\t\tif (visibleCollections.some((entry) => entry.slug === collection)) {\n\t\t\treturn\n\t\t}\n\t\tsetCollection(visibleCollections[0]?.slug ?? '')\n\t}, [collection, visibleCollections])\n\n\tconst collectionMeta = visibleCollections.find((entry) => entry.slug === collection)\n\tconst useAsTitle = collectionMeta?.useAsTitle ?? 'email'\n\n\tconst onSearch = useCallback((value: string) => {\n\t\tsetPage(1)\n\t\tsetSearch(value ?? '')\n\t}, [])\n\n\tconst listAbort = useRef<AbortController | null>(null)\n\tconst switcherRef = useRef<HTMLDivElement>(null)\n\tconst resultsRef = useRef<HTMLDivElement>(null)\n\n\tconst changePage = (next: number) => {\n\t\tsetPage(next)\n\t\tswitcherRef.current?.closest('.drawer__content-children')?.scrollTo({ top: 0 })\n\t\tresultsRef.current?.scrollTo({ top: 0 })\n\t}\n\n\tconst load = useCallback(async () => {\n\t\tif (!collection || !isModalOpen(drawerSlug)) {\n\t\t\treturn\n\t\t}\n\t\tlistAbort.current?.abort()\n\t\tconst controller = new AbortController()\n\t\tlistAbort.current = controller\n\t\tsetLoading(true)\n\t\ttry {\n\t\t\tconst filter = targetFilters[collection]\n\t\t\tconst clauses: Where[] = []\n\t\t\tif (filter && filter !== true) {\n\t\t\t\tclauses.push(filter)\n\t\t\t}\n\t\t\tif (collection === config.admin.user && viewerId !== undefined && viewerId !== '') {\n\t\t\t\tclauses.push({ id: { not_equals: viewerId } })\n\t\t\t}\n\t\t\tconst trimmed = search.trim()\n\t\t\tif (trimmed) {\n\t\t\t\tconst fields = new Set(['email', useAsTitle])\n\t\t\t\tclauses.push({\n\t\t\t\t\tor: [...fields].map((field) => ({ [field]: { like: trimmed } })),\n\t\t\t\t})\n\t\t\t}\n\t\t\tconst params = new URLSearchParams({\n\t\t\t\tdepth: '0',\n\t\t\t\tlimit: String(SWITCHER_PAGE_SIZE),\n\t\t\t\tpage: String(page),\n\t\t\t\tsort: useAsTitle,\n\t\t\t\twhere: JSON.stringify(andWhere(clauses)),\n\t\t\t})\n\t\t\tconst response = await fetch(`${config.routes.api}/${collection}?${params.toString()}`, {\n\t\t\t\tcredentials: 'include',\n\t\t\t\tsignal: controller.signal,\n\t\t\t})\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(String(response.status))\n\t\t\t}\n\t\t\tconst body = (await response.json()) as {\n\t\t\t\tdocs?: ListedUser[]\n\t\t\t\thasNextPage?: boolean\n\t\t\t\thasPrevPage?: boolean\n\t\t\t\ttotalPages?: number\n\t\t\t}\n\t\t\tsetDocs(body.docs ?? [])\n\t\t\tsetHasNext(Boolean(body.hasNextPage))\n\t\t\tsetHasPrev(Boolean(body.hasPrevPage))\n\t\t\tsetTotalPages(body.totalPages ?? 1)\n\t\t} catch (error) {\n\t\t\tif (\n\t\t\t\tcontroller.signal.aborted ||\n\t\t\t\t(error instanceof DOMException && error.name === 'AbortError')\n\t\t\t) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttoast.error(t(keys.errorFailed))\n\t\t\tsetDocs([])\n\t\t\tsetHasNext(false)\n\t\t\tsetHasPrev(false)\n\t\t\tsetTotalPages(1)\n\t\t} finally {\n\t\t\tif (!controller.signal.aborted) {\n\t\t\t\tsetLoading(false)\n\t\t\t}\n\t\t}\n\t}, [\n\t\tcollection,\n\t\tconfig.admin.user,\n\t\tconfig.routes.api,\n\t\tisModalOpen,\n\t\tpage,\n\t\tsearch,\n\t\tt,\n\t\ttargetFilters,\n\t\tuseAsTitle,\n\t\tviewerId,\n\t])\n\n\tuseEffect(() => {\n\t\tvoid load()\n\t}, [load])\n\n\tconst pick = (doc: ListedUser) => {\n\t\tconst title = String(doc[useAsTitle] ?? doc.email ?? doc.id)\n\t\tsetTarget({ collection, id: doc.id, label: title })\n\t\tcloseModal(drawerSlug)\n\t\topenModal(confirmSlug)\n\t}\n\n\tif (visibleCollections.length === 0) {\n\t\treturn null\n\t}\n\n\treturn (\n\t\t<>\n\t\t\t<Button\n\t\t\t\tbuttonStyle=\"subtle\"\n\t\t\t\tclassName=\"impersonation-switcher-button\"\n\t\t\t\tmargin={false}\n\t\t\t\tonClick={() => {\n\t\t\t\t\tsetPage(1)\n\t\t\t\t\topenModal(drawerSlug)\n\t\t\t\t}}\n\t\t\t\tsize=\"small\"\n\t\t\t\ticon={<ImpersonateIcon />}\n\t\t\t\ticonPosition=\"left\"\n\t\t\t>\n\t\t\t\t{t(keys.switchToUser)}\n\t\t\t</Button>\n\t\t\t<Drawer slug={drawerSlug} title={t(keys.switchToUser)}>\n\t\t\t\t<div className=\"impersonation-switcher\" ref={switcherRef}>\n\t\t\t\t\t<div className=\"search-bar\">\n\t\t\t\t\t\t<SearchIcon />\n\t\t\t\t\t\t<SearchFilter handleChange={onSearch} label={t(keys.searchByName)} />\n\t\t\t\t\t\t{visibleCollections.length > 1 ? (\n\t\t\t\t\t\t\t<div className=\"search-bar__actions\">\n\t\t\t\t\t\t\t\t<Popup\n\t\t\t\t\t\t\t\t\tbutton={\n\t\t\t\t\t\t\t\t\t\t<Pill\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"impersonation-collection\"\n\t\t\t\t\t\t\t\t\t\t\ticon={<ChevronIcon direction={collectionMenuOpen ? 'up' : 'down'} />}\n\t\t\t\t\t\t\t\t\t\t\tpillStyle=\"light\"\n\t\t\t\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{visibleCollections.find((entry) => entry.slug === collection)?.label ??\n\t\t\t\t\t\t\t\t\t\t\t\tcollection}\n\t\t\t\t\t\t\t\t\t\t</Pill>\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tbuttonType=\"custom\"\n\t\t\t\t\t\t\t\t\tcaret={false}\n\t\t\t\t\t\t\t\t\thorizontalAlign=\"right\"\n\t\t\t\t\t\t\t\t\tonToggleOpen={setCollectionMenuOpen}\n\t\t\t\t\t\t\t\t\trender={({ close }) => (\n\t\t\t\t\t\t\t\t\t\t<PopupList.ButtonGroup>\n\t\t\t\t\t\t\t\t\t\t\t{visibleCollections.map((entry) => (\n\t\t\t\t\t\t\t\t\t\t\t\t<PopupList.Button\n\t\t\t\t\t\t\t\t\t\t\t\t\tactive={entry.slug === collection}\n\t\t\t\t\t\t\t\t\t\t\t\t\tkey={entry.slug}\n\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetCollection(entry.slug)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetPage(1)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclose()\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{entry.label}\n\t\t\t\t\t\t\t\t\t\t\t\t</PopupList.Button>\n\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t</PopupList.ButtonGroup>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\tsize=\"fit-content\"\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"impersonation-switcher__results\" ref={resultsRef}>\n\t\t\t\t\t\t{docs.length === 0 && !loading ? (\n\t\t\t\t\t\t\t<p className=\"impersonation-switcher__empty\">{t(keys.noResults)}</p>\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t{docs.map((doc) => {\n\t\t\t\t\t\t\tconst title = String(doc[useAsTitle] ?? doc.email ?? doc.id)\n\t\t\t\t\t\t\tconst email = typeof doc.email === 'string' ? doc.email : undefined\n\t\t\t\t\t\t\tconst showEmail = cardEmail && email && email !== title\n\t\t\t\t\t\t\tconst cardProps: ImpersonationUserCardProps = {\n\t\t\t\t\t\t\t\tcollectionSlug: collection,\n\t\t\t\t\t\t\t\tdoc,\n\t\t\t\t\t\t\t\tdocumentHref: `${config.routes.admin}/collections/${collection}/${doc.id}`,\n\t\t\t\t\t\t\t\temail: showEmail ? email : undefined,\n\t\t\t\t\t\t\t\tonOpenDrawer: () => setPreview({ collection, id: doc.id }),\n\t\t\t\t\t\t\t\tonSelect: () => pick(doc),\n\t\t\t\t\t\t\t\topenDocumentLabel: t(keys.openDocument),\n\t\t\t\t\t\t\t\topenDrawerLabel: t(keys.openDrawer),\n\t\t\t\t\t\t\t\ttitle,\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<RenderCustomComponent\n\t\t\t\t\t\t\t\t\tCustomComponent={Card ? <Card {...cardProps} /> : undefined}\n\t\t\t\t\t\t\t\t\tFallback={<ImpersonationUserCard {...cardProps} />}\n\t\t\t\t\t\t\t\t\tkey={String(doc.id)}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t})}\n\t\t\t\t\t</div>\n\t\t\t\t\t{totalPages > 1 ? (\n\t\t\t\t\t\t<div className=\"impersonation-switcher__pager\">\n\t\t\t\t\t\t\t<Pagination\n\t\t\t\t\t\t\t\thasNextPage={hasNext}\n\t\t\t\t\t\t\t\thasPrevPage={hasPrev}\n\t\t\t\t\t\t\t\tnextPage={page + 1}\n\t\t\t\t\t\t\t\tonChange={changePage}\n\t\t\t\t\t\t\t\tpage={page}\n\t\t\t\t\t\t\t\tprevPage={Math.max(1, page - 1)}\n\t\t\t\t\t\t\t\ttotalPages={totalPages}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t) : null}\n\t\t\t\t</div>\n\t\t\t</Drawer>\n\t\t\t{preview ? (\n\t\t\t\t<PreviewDrawer\n\t\t\t\t\tcollectionSlug={preview.collection}\n\t\t\t\t\tid={preview.id}\n\t\t\t\t\tkey={`${preview.collection}:${preview.id}`}\n\t\t\t\t/>\n\t\t\t) : null}\n\t\t\t<StartConfirmModal\n\t\t\t\tapiPath={apiPath}\n\t\t\t\tkey={target ? `${target.collection}:${target.id}` : 'idle'}\n\t\t\t\tmodalSlug={confirmSlug}\n\t\t\t\treasonMode={reasonMode}\n\t\t\t\ttarget={target}\n\t\t\t/>\n\t\t</>\n\t)\n}\n"],"mappings":"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"}
|