@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.
Files changed (163) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +49 -0
  4. package/dist/access/filterTargets.js +51 -0
  5. package/dist/access/filterTargets.js.map +1 -0
  6. package/dist/auth/cookies.js +49 -0
  7. package/dist/auth/cookies.js.map +1 -0
  8. package/dist/auth/expiryCookies.js +59 -0
  9. package/dist/auth/expiryCookies.js.map +1 -0
  10. package/dist/auth/issue.js +126 -0
  11. package/dist/auth/issue.js.map +1 -0
  12. package/dist/auth/mode.js +69 -0
  13. package/dist/auth/mode.js.map +1 -0
  14. package/dist/auth/origin.js +55 -0
  15. package/dist/auth/origin.js.map +1 -0
  16. package/dist/auth/revoke.js +24 -0
  17. package/dist/auth/revoke.js.map +1 -0
  18. package/dist/auth/tenantCookie.js +74 -0
  19. package/dist/auth/tenantCookie.js.map +1 -0
  20. package/dist/endpoints/codes.d.ts +6 -0
  21. package/dist/endpoints/current.js +66 -0
  22. package/dist/endpoints/current.js.map +1 -0
  23. package/dist/endpoints/exit.js +242 -0
  24. package/dist/endpoints/exit.js.map +1 -0
  25. package/dist/endpoints/shared.js +76 -0
  26. package/dist/endpoints/shared.js.map +1 -0
  27. package/dist/endpoints/start.js +332 -0
  28. package/dist/endpoints/start.js.map +1 -0
  29. package/dist/endpoints/terminate.js +73 -0
  30. package/dist/endpoints/terminate.js.map +1 -0
  31. package/dist/exports/client.d.ts +9 -0
  32. package/dist/exports/client.js +10 -0
  33. package/dist/exports/i18n.d.ts +3 -0
  34. package/dist/exports/i18n.js +3 -0
  35. package/dist/exports/rsc.d.ts +4 -0
  36. package/dist/exports/rsc.js +4 -0
  37. package/dist/exports/types.d.ts +3 -0
  38. package/dist/exports/types.js +1 -0
  39. package/dist/getImpersonation.d.ts +49 -0
  40. package/dist/getImpersonation.js +78 -0
  41. package/dist/getImpersonation.js.map +1 -0
  42. package/dist/ids.js +27 -0
  43. package/dist/ids.js.map +1 -0
  44. package/dist/index.d.ts +20 -0
  45. package/dist/index.js +55 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/plugin/assertTargets.js +23 -0
  48. package/dist/plugin/assertTargets.js.map +1 -0
  49. package/dist/plugin/constants.d.ts +6 -0
  50. package/dist/plugin/constants.js +20 -0
  51. package/dist/plugin/constants.js.map +1 -0
  52. package/dist/plugin/decorateAuth.js +99 -0
  53. package/dist/plugin/decorateAuth.js.map +1 -0
  54. package/dist/plugin/lookup.js +23 -0
  55. package/dist/plugin/lookup.js.map +1 -0
  56. package/dist/plugin/normalizeOptions.js +83 -0
  57. package/dist/plugin/normalizeOptions.js.map +1 -0
  58. package/dist/plugin/registerAdmin.js +49 -0
  59. package/dist/plugin/registerAdmin.js.map +1 -0
  60. package/dist/plugin/registerCollection.js +183 -0
  61. package/dist/plugin/registerCollection.js.map +1 -0
  62. package/dist/plugin/registerEndpoints.js +19 -0
  63. package/dist/plugin/registerEndpoints.js.map +1 -0
  64. package/dist/plugin/registerHooks.js +59 -0
  65. package/dist/plugin/registerHooks.js.map +1 -0
  66. package/dist/plugin/registerRetention.js +57 -0
  67. package/dist/plugin/registerRetention.js.map +1 -0
  68. package/dist/plugin/registerTranslations.js +19 -0
  69. package/dist/plugin/registerTranslations.js.map +1 -0
  70. package/dist/plugin/registry.js +11 -0
  71. package/dist/plugin/registry.js.map +1 -0
  72. package/dist/plugin/startable.js +21 -0
  73. package/dist/plugin/startable.js.map +1 -0
  74. package/dist/session/close.js +65 -0
  75. package/dist/session/close.js.map +1 -0
  76. package/dist/session/closeStale.d.ts +11 -0
  77. package/dist/session/closeStale.js +65 -0
  78. package/dist/session/closeStale.js.map +1 -0
  79. package/dist/session/locks.js +32 -0
  80. package/dist/session/locks.js.map +1 -0
  81. package/dist/session/resolve.js +91 -0
  82. package/dist/session/resolve.js.map +1 -0
  83. package/dist/translations/ar.js +52 -0
  84. package/dist/translations/ar.js.map +1 -0
  85. package/dist/translations/de.js +52 -0
  86. package/dist/translations/de.js.map +1 -0
  87. package/dist/translations/en.js +52 -0
  88. package/dist/translations/en.js.map +1 -0
  89. package/dist/translations/es.js +52 -0
  90. package/dist/translations/es.js.map +1 -0
  91. package/dist/translations/fr.js +52 -0
  92. package/dist/translations/fr.js.map +1 -0
  93. package/dist/translations/id.js +52 -0
  94. package/dist/translations/id.js.map +1 -0
  95. package/dist/translations/index.d.ts +24 -0
  96. package/dist/translations/index.js +49 -0
  97. package/dist/translations/index.js.map +1 -0
  98. package/dist/translations/keys.d.ts +56 -0
  99. package/dist/translations/keys.js +56 -0
  100. package/dist/translations/keys.js.map +1 -0
  101. package/dist/translations/ko.js +52 -0
  102. package/dist/translations/ko.js.map +1 -0
  103. package/dist/translations/lookup.js +17 -0
  104. package/dist/translations/lookup.js.map +1 -0
  105. package/dist/translations/pt.js +52 -0
  106. package/dist/translations/pt.js.map +1 -0
  107. package/dist/translations/ru.js +52 -0
  108. package/dist/translations/ru.js.map +1 -0
  109. package/dist/translations/server.js +13 -0
  110. package/dist/translations/server.js.map +1 -0
  111. package/dist/translations/uk.js +52 -0
  112. package/dist/translations/uk.js.map +1 -0
  113. package/dist/translations/useTranslation.js +12 -0
  114. package/dist/translations/useTranslation.js.map +1 -0
  115. package/dist/translations/zh.js +52 -0
  116. package/dist/translations/zh.js.map +1 -0
  117. package/dist/types.d.ts +214 -0
  118. package/dist/types.js +11 -0
  119. package/dist/types.js.map +1 -0
  120. package/dist/ui/EndSessionMenuItem.d.ts +5 -0
  121. package/dist/ui/EndSessionMenuItem.js +45 -0
  122. package/dist/ui/EndSessionMenuItem.js.map +1 -0
  123. package/dist/ui/ImpersonateIcon.js +20 -0
  124. package/dist/ui/ImpersonateIcon.js.map +1 -0
  125. package/dist/ui/ImpersonationAction.d.ts +10 -0
  126. package/dist/ui/ImpersonationAction.js +48 -0
  127. package/dist/ui/ImpersonationAction.js.map +1 -0
  128. package/dist/ui/ImpersonationBar.d.ts +36 -0
  129. package/dist/ui/ImpersonationBar.js +357 -0
  130. package/dist/ui/ImpersonationBar.js.map +1 -0
  131. package/dist/ui/ImpersonationConfig.js +25 -0
  132. package/dist/ui/ImpersonationConfig.js.map +1 -0
  133. package/dist/ui/ImpersonationDocumentButton.d.ts +5 -0
  134. package/dist/ui/ImpersonationDocumentButton.js +85 -0
  135. package/dist/ui/ImpersonationDocumentButton.js.map +1 -0
  136. package/dist/ui/ImpersonationFrontendBanner.d.ts +41 -0
  137. package/dist/ui/ImpersonationFrontendBanner.js +58 -0
  138. package/dist/ui/ImpersonationFrontendBanner.js.map +1 -0
  139. package/dist/ui/ImpersonationFrontendExit.d.ts +17 -0
  140. package/dist/ui/ImpersonationFrontendExit.js +45 -0
  141. package/dist/ui/ImpersonationFrontendExit.js.map +1 -0
  142. package/dist/ui/ImpersonationProvider.d.ts +16 -0
  143. package/dist/ui/ImpersonationProvider.js +127 -0
  144. package/dist/ui/ImpersonationProvider.js.map +1 -0
  145. package/dist/ui/ImpersonationSwitcher.d.ts +21 -0
  146. package/dist/ui/ImpersonationSwitcher.js +251 -0
  147. package/dist/ui/ImpersonationSwitcher.js.map +1 -0
  148. package/dist/ui/ImpersonationUserCard.d.ts +31 -0
  149. package/dist/ui/ImpersonationUserCard.js +51 -0
  150. package/dist/ui/ImpersonationUserCard.js.map +1 -0
  151. package/dist/ui/StartConfirmModal.d.ts +28 -0
  152. package/dist/ui/StartConfirmModal.js +65 -0
  153. package/dist/ui/StartConfirmModal.js.map +1 -0
  154. package/dist/ui/api.js +66 -0
  155. package/dist/ui/api.js.map +1 -0
  156. package/dist/ui/barPlacement.js +107 -0
  157. package/dist/ui/barPlacement.js.map +1 -0
  158. package/dist/ui/frontendBanner.css +22 -0
  159. package/dist/ui/impersonation.css +244 -0
  160. package/dist/ui/useImpersonation.d.ts +43 -0
  161. package/dist/ui/useImpersonation.js +36 -0
  162. package/dist/ui/useImpersonation.js.map +1 -0
  163. 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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"}