@10x-media/impersonation 0.1.0-beta.1 → 0.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @10x-media/impersonation
2
2
 
3
+ ## 0.1.0-beta.2
4
+
5
+ ### Patch Changes
6
+
7
+ - The admin bar no longer throws a hydration error on page load: its session countdown appears once the bar has mounted. The bar's text, button labels and exit errors now take overrides from the `translations` option and from `i18n.translations`, as the plugin's other admin strings do.
8
+
3
9
  ## 0.1.0-beta.1
4
10
 
5
11
  ### Patch Changes
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 10x Media GmbH
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 10x Media GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,22 @@
1
+ //#region src/endpoints/codes.ts
2
+ const FAILURE_CODES = [
3
+ "alreadyImpersonating",
4
+ "failed",
5
+ "forbidden",
6
+ "impersonatorGone",
7
+ "impersonatorSessionExpired",
8
+ "invalidBody",
9
+ "notImpersonating",
10
+ "origin",
11
+ "reasonRequired",
12
+ "selfTarget",
13
+ "targetNotFound",
14
+ "targetTrashed",
15
+ "targetUnverified",
16
+ "unsupportedAuth",
17
+ "unsupportedCollection"
18
+ ];
19
+ //#endregion
20
+ export { FAILURE_CODES };
21
+
22
+ //# sourceMappingURL=codes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codes.js","names":[],"sources":["../../src/endpoints/codes.ts"],"sourcesContent":["export const FAILURE_CODES = [\n\t'alreadyImpersonating',\n\t'failed',\n\t'forbidden',\n\t'impersonatorGone',\n\t'impersonatorSessionExpired',\n\t'invalidBody',\n\t'notImpersonating',\n\t'origin',\n\t'reasonRequired',\n\t'selfTarget',\n\t'targetNotFound',\n\t'targetTrashed',\n\t'targetUnverified',\n\t'unsupportedAuth',\n\t'unsupportedCollection',\n] as const\n\nexport type FailureCode = (typeof FAILURE_CODES)[number]\n"],"mappings":";AAAA,MAAa,gBAAgB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD"}
@@ -0,0 +1,26 @@
1
+ import { keys } from "./keys.js";
2
+ //#region src/translations/errorKey.ts
3
+ /** The message key for a failure code an endpoint answers with. */
4
+ const errorKey = (code) => {
5
+ return {
6
+ alreadyImpersonating: keys.errorAlreadyImpersonating,
7
+ failed: keys.errorFailed,
8
+ forbidden: keys.errorForbidden,
9
+ impersonatorGone: keys.errorImpersonatorGone,
10
+ impersonatorSessionExpired: keys.errorImpersonatorSessionExpired,
11
+ invalidBody: keys.errorInvalidBody,
12
+ notImpersonating: keys.errorNotImpersonating,
13
+ origin: keys.errorOrigin,
14
+ reasonRequired: keys.errorReasonRequired,
15
+ selfTarget: keys.errorSelfTarget,
16
+ targetNotFound: keys.errorTargetNotFound,
17
+ targetTrashed: keys.errorTargetTrashed,
18
+ targetUnverified: keys.errorTargetUnverified,
19
+ unsupportedAuth: keys.errorUnsupportedAuth,
20
+ unsupportedCollection: keys.errorUnsupportedCollection
21
+ }[code] ?? keys.errorFailed;
22
+ };
23
+ //#endregion
24
+ export { errorKey };
25
+
26
+ //# sourceMappingURL=errorKey.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errorKey.js","names":[],"sources":["../../src/translations/errorKey.ts"],"sourcesContent":["import { keys, type TranslationKey } from './keys'\n\n/** The message key for a failure code an endpoint answers with. */\nexport const errorKey = (code: string) => {\n\tconst map: Record<string, TranslationKey> = {\n\t\talreadyImpersonating: keys.errorAlreadyImpersonating,\n\t\tfailed: keys.errorFailed,\n\t\tforbidden: keys.errorForbidden,\n\t\timpersonatorGone: keys.errorImpersonatorGone,\n\t\timpersonatorSessionExpired: keys.errorImpersonatorSessionExpired,\n\t\tinvalidBody: keys.errorInvalidBody,\n\t\tnotImpersonating: keys.errorNotImpersonating,\n\t\torigin: keys.errorOrigin,\n\t\treasonRequired: keys.errorReasonRequired,\n\t\tselfTarget: keys.errorSelfTarget,\n\t\ttargetNotFound: keys.errorTargetNotFound,\n\t\ttargetTrashed: keys.errorTargetTrashed,\n\t\ttargetUnverified: keys.errorTargetUnverified,\n\t\tunsupportedAuth: keys.errorUnsupportedAuth,\n\t\tunsupportedCollection: keys.errorUnsupportedCollection,\n\t}\n\treturn map[code] ?? keys.errorFailed\n}\n"],"mappings":";;;AAGA,MAAa,YAAY,SAAiB;CAkBzC,OAAO;EAhBN,sBAAsB,KAAK;EAC3B,QAAQ,KAAK;EACb,WAAW,KAAK;EAChB,kBAAkB,KAAK;EACvB,4BAA4B,KAAK;EACjC,aAAa,KAAK;EAClB,kBAAkB,KAAK;EACvB,QAAQ,KAAK;EACb,gBAAgB,KAAK;EACrB,YAAY,KAAK;EACjB,gBAAgB,KAAK;EACrB,eAAe,KAAK;EACpB,kBAAkB,KAAK;EACvB,iBAAiB,KAAK;EACtB,uBAAuB,KAAK;CAEpB,EAAE,SAAS,KAAK;AAC1B"}
@@ -1,13 +1,18 @@
1
1
  import { translations } from "./index.js";
2
2
  //#region src/translations/lookup.ts
3
- const nestedOf = (locale) => translations[locale];
4
- /** Resolve a plugin string in `locale`, falling back to English. */
5
- const messageFor = (locale, key) => {
6
- const code = (locale ?? "en").split("-")[0] ?? "en";
3
+ const valueIn = (source, locale, key) => {
7
4
  const separator = key.indexOf(":");
8
- const namespace = key.slice(0, separator);
9
- const rest = key.slice(separator + 1);
10
- return nestedOf(code)?.[namespace]?.[rest] ?? nestedOf("en")?.[namespace]?.[rest] ?? key;
5
+ const value = source?.[locale]?.[key.slice(0, separator)]?.[key.slice(separator + 1)];
6
+ return typeof value === "string" ? value : void 0;
7
+ };
8
+ /**
9
+ * Resolve a plugin string in `locale`, falling back to English. Pass the config's
10
+ * `i18n.translations` as `source` so host and `translations` option overrides apply; the plugin's
11
+ * bundled strings back it.
12
+ */
13
+ const messageFor = (locale, key, source) => {
14
+ const code = (locale ?? "en").split("-")[0] ?? "en";
15
+ return valueIn(source, code, key) ?? valueIn(translations, code, key) ?? valueIn(source, "en", key) ?? valueIn(translations, "en", key) ?? key;
11
16
  };
12
17
  /** Replace every `token` without interpreting `$` in `value`. */
13
18
  const fillTemplate = (template, token, value) => template.split(token).join(value);
@@ -1 +1 @@
1
- {"version":3,"file":"lookup.js","names":[],"sources":["../../src/translations/lookup.ts"],"sourcesContent":["import { translations } from './index'\nimport type { TranslationKey } from './keys'\n\nconst nestedOf = (locale: string) => translations[locale as keyof typeof translations]\n\n/** Resolve a plugin string in `locale`, falling back to English. */\nexport const messageFor = (locale: null | string | undefined, key: TranslationKey): string => {\n\tconst code = (locale ?? 'en').split('-')[0] ?? 'en'\n\tconst separator = key.indexOf(':')\n\tconst namespace = key.slice(0, separator)\n\tconst rest = key.slice(separator + 1)\n\treturn nestedOf(code)?.[namespace]?.[rest] ?? nestedOf('en')?.[namespace]?.[rest] ?? key\n}\n\n/** Replace every `token` without interpreting `$` in `value`. */\nexport const fillTemplate = (template: string, token: string, value: string): string =>\n\ttemplate.split(token).join(value)\n"],"mappings":";;AAGA,MAAM,YAAY,WAAmB,aAAa;;AAGlD,MAAa,cAAc,QAAmC,QAAgC;CAC7F,MAAM,QAAQ,UAAU,MAAM,MAAM,GAAG,EAAE,MAAM;CAC/C,MAAM,YAAY,IAAI,QAAQ,GAAG;CACjC,MAAM,YAAY,IAAI,MAAM,GAAG,SAAS;CACxC,MAAM,OAAO,IAAI,MAAM,YAAY,CAAC;CACpC,OAAO,SAAS,IAAI,IAAI,aAAa,SAAS,SAAS,IAAI,IAAI,aAAa,SAAS;AACtF;;AAGA,MAAa,gBAAgB,UAAkB,OAAe,UAC7D,SAAS,MAAM,KAAK,EAAE,KAAK,KAAK"}
1
+ {"version":3,"file":"lookup.js","names":[],"sources":["../../src/translations/lookup.ts"],"sourcesContent":["import { translations } from './index'\nimport type { TranslationKey } from './keys'\n\ntype Nested = Record<string, Record<string, Record<string, unknown> | undefined> | undefined>\n\nconst valueIn = (source: unknown, locale: string, key: TranslationKey): string | undefined => {\n\tconst separator = key.indexOf(':')\n\tconst value = (source as Nested | null | undefined)?.[locale]?.[key.slice(0, separator)]?.[\n\t\tkey.slice(separator + 1)\n\t]\n\treturn typeof value === 'string' ? value : undefined\n}\n\n/**\n * Resolve a plugin string in `locale`, falling back to English. Pass the config's\n * `i18n.translations` as `source` so host and `translations` option overrides apply; the plugin's\n * bundled strings back it.\n */\nexport const messageFor = (\n\tlocale: null | string | undefined,\n\tkey: TranslationKey,\n\tsource?: unknown\n): string => {\n\tconst code = (locale ?? 'en').split('-')[0] ?? 'en'\n\treturn (\n\t\tvalueIn(source, code, key) ??\n\t\tvalueIn(translations, code, key) ??\n\t\tvalueIn(source, 'en', key) ??\n\t\tvalueIn(translations, 'en', key) ??\n\t\tkey\n\t)\n}\n\n/** Replace every `token` without interpreting `$` in `value`. */\nexport const fillTemplate = (template: string, token: string, value: string): string =>\n\ttemplate.split(token).join(value)\n"],"mappings":";;AAKA,MAAM,WAAW,QAAiB,QAAgB,QAA4C;CAC7F,MAAM,YAAY,IAAI,QAAQ,GAAG;CACjC,MAAM,QAAS,SAAuC,UAAU,IAAI,MAAM,GAAG,SAAS,KACrF,IAAI,MAAM,YAAY,CAAC;CAExB,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;AAC5C;;;;;;AAOA,MAAa,cACZ,QACA,KACA,WACY;CACZ,MAAM,QAAQ,UAAU,MAAM,MAAM,GAAG,EAAE,MAAM;CAC/C,OACC,QAAQ,QAAQ,MAAM,GAAG,KACzB,QAAQ,cAAc,MAAM,GAAG,KAC/B,QAAQ,QAAQ,MAAM,GAAG,KACzB,QAAQ,cAAc,MAAM,GAAG,KAC/B;AAEF;;AAGA,MAAa,gBAAgB,UAAkB,OAAe,UAC7D,SAAS,MAAM,KAAK,EAAE,KAAK,KAAK"}
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { keys } from "../translations/keys.js";
3
3
  import { useTranslation as useTranslation$1 } from "../translations/useTranslation.js";
4
- import { errorKey } from "./api.js";
4
+ import { errorKey } from "../translations/errorKey.js";
5
5
  import { useImpersonation } from "./useImpersonation.js";
6
6
  import { useState } from "react";
7
7
  import { Button, ConfirmationModal, toast, useDocumentInfo, useModal } from "@payloadcms/ui";
@@ -1,10 +1,16 @@
1
+ import { FailureCode } from "../endpoints/codes.js";
1
2
  //#region src/ui/ImpersonationBar.d.ts
2
3
  type ImpersonationBarProps = {
3
4
  actingAs: string;
4
5
  adminRoute: string;
5
6
  apiPath: string;
6
7
  frontendUrl?: string;
7
- impersonatorLocale?: null | string;
8
+ impersonatorLocale?: null | string; /** Resolved by the provider from the config's translations, so overrides reach the client. */
9
+ messages?: {
10
+ collapseBar: string;
11
+ errors: Partial<Record<FailureCode, string>>;
12
+ expandBar: string;
13
+ };
8
14
  openWebsite: string;
9
15
  pluginName: string;
10
16
  returnTo: string;
@@ -21,6 +27,7 @@ declare const ImpersonationBar: ({
21
27
  apiPath,
22
28
  frontendUrl,
23
29
  impersonatorLocale,
30
+ messages,
24
31
  openWebsite,
25
32
  pluginName,
26
33
  returnTo,
@@ -1,10 +1,12 @@
1
1
  "use client";
2
2
  import { CLIENT_FETCH_TIMEOUT_MS } from "../plugin/constants.js";
3
3
  import { keys } from "../translations/keys.js";
4
- import { errorKey, goAfterSwitch, postImpersonation } from "./api.js";
4
+ import { errorKey } from "../translations/errorKey.js";
5
+ import { goAfterSwitch, postImpersonation } from "./api.js";
5
6
  import { useImpersonation } from "./useImpersonation.js";
6
- import { fillTemplate, messageFor } from "../translations/lookup.js";
7
+ import { messageFor } from "../translations/lookup.js";
7
8
  import { anchorsFor, nearestEdge, projectThrow, readBarPlacement, readViewport, writeBarPlacement } from "./barPlacement.js";
9
+ import { sessionEndsLabel } from "./sessionEnds.js";
8
10
  import { useCallback, useEffect, useRef, useState } from "react";
9
11
  import { Button, Pill, toast } from "@payloadcms/ui";
10
12
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -17,18 +19,7 @@ const SESSION_GONE = new Set([
17
19
  "impersonatorSessionExpired",
18
20
  "notImpersonating"
19
21
  ]);
20
- const formatCountdown = (expiresAt, now) => {
21
- const remaining = new Date(expiresAt).getTime() - now;
22
- if (!Number.isFinite(remaining) || remaining <= 0) return "0s";
23
- const totalSeconds = Math.floor(remaining / 1e3);
24
- const hours = Math.floor(totalSeconds / 3600);
25
- const minutes = Math.floor(totalSeconds % 3600 / 60);
26
- const seconds = totalSeconds % 60;
27
- if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m ${String(seconds).padStart(2, "0")}s`;
28
- if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
29
- return `${seconds}s`;
30
- };
31
- const ImpersonationBar = ({ actingAs, adminRoute, apiPath, frontendUrl, impersonatorLocale, openWebsite, pluginName, returnTo, sessionEndsAt, sessionEndsAtTemplate, sessionEndsInTemplate, showFrontendLink, targetName, tone = "warning" }) => {
22
+ const ImpersonationBar = ({ actingAs, adminRoute, apiPath, frontendUrl, impersonatorLocale, messages, openWebsite, pluginName, returnTo, sessionEndsAt, sessionEndsAtTemplate, sessionEndsInTemplate, showFrontendLink, targetName, tone = "warning" }) => {
32
23
  const { setStatus } = useImpersonation();
33
24
  const [mounted, setMounted] = useState(false);
34
25
  const onLogin = (usePathname() ?? "") === `${adminRoute}/login`;
@@ -41,7 +32,7 @@ const ImpersonationBar = ({ actingAs, adminRoute, apiPath, frontendUrl, imperson
41
32
  width: 1200
42
33
  } : readViewport());
43
34
  const [dragPoint, setDragPoint] = useState(null);
44
- const [now, setNow] = useState(() => Date.now());
35
+ const [now, setNow] = useState(null);
45
36
  const chipRef = useRef(null);
46
37
  const expiredProbe = useRef(false);
47
38
  const dragOffset = useRef(null);
@@ -91,6 +82,7 @@ const ImpersonationBar = ({ actingAs, adminRoute, apiPath, frontendUrl, imperson
91
82
  useEffect(() => {
92
83
  if (!sessionEndsAt || !visible || onLogin) return;
93
84
  expiredProbe.current = false;
85
+ setNow(Date.now());
94
86
  const timer = window.setInterval(() => {
95
87
  const next = Date.now();
96
88
  setNow(next);
@@ -113,7 +105,8 @@ const ImpersonationBar = ({ actingAs, adminRoute, apiPath, frontendUrl, imperson
113
105
  try {
114
106
  const result = await postImpersonation(`${apiPath}/exit`, {});
115
107
  if (!result.ok) {
116
- toast.error(messageFor(impersonatorLocale, errorKey(result.error ?? "failed")));
108
+ const code = result.error ?? "failed";
109
+ toast.error(messages?.errors[code] ?? messageFor(impersonatorLocale, errorKey(code)));
117
110
  if (result.error && SESSION_GONE.has(result.error)) {
118
111
  setStatus({ active: false });
119
112
  setVisible(false);
@@ -267,7 +260,7 @@ const ImpersonationBar = ({ actingAs, adminRoute, apiPath, frontendUrl, imperson
267
260
  className: "impersonation-chip__name",
268
261
  children: targetName
269
262
  }), /* @__PURE__ */ jsx("button", {
270
- "aria-label": messageFor(impersonatorLocale, keys.expandBar),
263
+ "aria-label": messages?.expandBar ?? messageFor(impersonatorLocale, keys.expandBar),
271
264
  className: "impersonation-bar__icon",
272
265
  onClick: (event) => {
273
266
  if (dragged.current) {
@@ -281,8 +274,15 @@ const ImpersonationBar = ({ actingAs, adminRoute, apiPath, frontendUrl, imperson
281
274
  })]
282
275
  });
283
276
  }
284
- const countdown = sessionEndsAt ? formatCountdown(sessionEndsAt, now) : null;
285
- const endsLabel = countdown && countdown !== "0s" ? fillTemplate(sessionEndsInTemplate, "{{countdown}}", countdown) : sessionEndsAt && countdown !== "0s" ? fillTemplate(sessionEndsAtTemplate, "{{time}}", new Date(sessionEndsAt).toLocaleTimeString(impersonatorLocale ?? void 0)) : null;
277
+ const endsLabel = sessionEndsLabel({
278
+ endsAt: sessionEndsAt,
279
+ locale: impersonatorLocale,
280
+ now,
281
+ templates: {
282
+ endsAt: sessionEndsAtTemplate,
283
+ endsIn: sessionEndsInTemplate
284
+ }
285
+ });
286
286
  return /* @__PURE__ */ jsxs("div", {
287
287
  "aria-live": "polite",
288
288
  className: tone === "quiet" ? "impersonation-bar impersonation-bar--quiet" : "impersonation-bar",
@@ -296,9 +296,9 @@ const ImpersonationBar = ({ actingAs, adminRoute, apiPath, frontendUrl, imperson
296
296
  size: "small",
297
297
  children: pluginName
298
298
  }), /* @__PURE__ */ jsx("span", { children: actingAs })]
299
- }), endsLabel ? /* @__PURE__ */ jsx("div", {
299
+ }), sessionEndsAt ? /* @__PURE__ */ jsx("div", {
300
300
  className: "impersonation-bar__time",
301
- children: endsLabel
301
+ children: endsLabel ?? "\xA0"
302
302
  }) : null]
303
303
  }), /* @__PURE__ */ jsxs("div", {
304
304
  className: "impersonation-bar__actions",
@@ -317,7 +317,7 @@ const ImpersonationBar = ({ actingAs, adminRoute, apiPath, frontendUrl, imperson
317
317
  children: returnTo
318
318
  }),
319
319
  /* @__PURE__ */ jsx("button", {
320
- "aria-label": messageFor(impersonatorLocale, keys.collapseBar),
320
+ "aria-label": messages?.collapseBar ?? messageFor(impersonatorLocale, keys.collapseBar),
321
321
  className: "impersonation-bar__icon",
322
322
  onClick: collapse,
323
323
  type: "button",
@@ -1 +1 @@
1
- {"version":3,"file":"ImpersonationBar.js","names":[],"sources":["../../src/ui/ImpersonationBar.tsx"],"sourcesContent":["'use client'\n\nimport { Button, Pill, toast } from '@payloadcms/ui'\nimport { usePathname } from 'next/navigation'\nimport {\n\ttype PointerEvent as ReactPointerEvent,\n\tuseCallback,\n\tuseEffect,\n\tuseRef,\n\tuseState,\n} from 'react'\n\nimport type { ImpersonationStatus } from '../getImpersonation'\nimport { CLIENT_FETCH_TIMEOUT_MS } from '../plugin/constants'\nimport { keys } from '../translations/keys'\nimport { fillTemplate, messageFor } from '../translations/lookup'\nimport { errorKey, goAfterSwitch, postImpersonation } from './api'\nimport {\n\tanchorsFor,\n\ttype BarEdge,\n\tnearestEdge,\n\ttype Point,\n\tprojectThrow,\n\treadBarPlacement,\n\treadViewport,\n\twriteBarPlacement,\n} from './barPlacement'\nimport { useImpersonation } from './useImpersonation'\nimport './impersonation.css'\n\nconst SESSION_GONE = new Set([\n\t'forbidden',\n\t'impersonatorGone',\n\t'impersonatorSessionExpired',\n\t'notImpersonating',\n])\n\nconst formatCountdown = (expiresAt: string, now: number): string | null => {\n\tconst remaining = new Date(expiresAt).getTime() - now\n\tif (!Number.isFinite(remaining) || remaining <= 0) {\n\t\treturn '0s'\n\t}\n\tconst totalSeconds = Math.floor(remaining / 1000)\n\tconst hours = Math.floor(totalSeconds / 3600)\n\tconst minutes = Math.floor((totalSeconds % 3600) / 60)\n\tconst seconds = totalSeconds % 60\n\tif (hours > 0) {\n\t\treturn `${hours}h ${String(minutes).padStart(2, '0')}m ${String(seconds).padStart(2, '0')}s`\n\t}\n\tif (minutes > 0) {\n\t\treturn `${minutes}m ${String(seconds).padStart(2, '0')}s`\n\t}\n\treturn `${seconds}s`\n}\n\nexport type ImpersonationBarProps = {\n\tactingAs: string\n\tadminRoute: string\n\tapiPath: string\n\tfrontendUrl?: string\n\timpersonatorLocale?: null | string\n\topenWebsite: string\n\tpluginName: string\n\treturnTo: string\n\tsessionEndsAt?: null | string\n\tsessionEndsAtTemplate: string\n\tsessionEndsInTemplate: string\n\tshowFrontendLink?: boolean\n\t/** Collapsed chip label. */\n\ttargetName: string\n\ttone?: 'quiet' | 'warning'\n}\n\nexport const ImpersonationBar = ({\n\tactingAs,\n\tadminRoute,\n\tapiPath,\n\tfrontendUrl,\n\timpersonatorLocale,\n\topenWebsite,\n\tpluginName,\n\treturnTo,\n\tsessionEndsAt,\n\tsessionEndsAtTemplate,\n\tsessionEndsInTemplate,\n\tshowFrontendLink,\n\ttargetName,\n\ttone = 'warning',\n}: ImpersonationBarProps) => {\n\tconst { setStatus } = useImpersonation()\n\tconst [mounted, setMounted] = useState(false)\n\tconst pathname = usePathname() ?? ''\n\tconst onLogin = pathname === `${adminRoute}/login`\n\tconst [busy, setBusy] = useState(false)\n\tconst [visible, setVisible] = useState(true)\n\tconst [collapsed, setCollapsed] = useState(false)\n\tconst [edge, setEdge] = useState<BarEdge>('right')\n\tconst [viewport, setViewport] = useState(() =>\n\t\ttypeof document === 'undefined' ? { height: 800, width: 1200 } : readViewport()\n\t)\n\tconst [dragPoint, setDragPoint] = useState<null | Point>(null)\n\tconst [now, setNow] = useState(() => Date.now())\n\tconst chipRef = useRef<HTMLDivElement>(null)\n\tconst expiredProbe = useRef(false)\n\tconst dragOffset = useRef<null | Point>(null)\n\tconst dragPointRef = useRef<null | Point>(null)\n\tconst dragOrigin = useRef<null | Point>(null)\n\tconst dragged = useRef(false)\n\tconst lastSample = useRef<null | { t: number; x: number; y: number }>(null)\n\tconst velocity = useRef<Point>({ x: 0, y: 0 })\n\tconst endChipDragRef = useRef<() => void>(() => {})\n\tconst onWindowPointerEnd = useRef(() => {\n\t\tendChipDragRef.current()\n\t})\n\n\tuseEffect(() => {\n\t\tconst stored = readBarPlacement()\n\t\tsetCollapsed(stored.collapsed)\n\t\tsetEdge(stored.edge)\n\t\tsetMounted(true)\n\t}, [])\n\n\tuseEffect(() => {\n\t\tif (!collapsed) {\n\t\t\treturn\n\t\t}\n\t\tconst update = () => setViewport(readViewport())\n\t\tupdate()\n\t\twindow.addEventListener('resize', update)\n\t\treturn () => window.removeEventListener('resize', update)\n\t}, [collapsed])\n\n\tconst probe = useCallback(async () => {\n\t\ttry {\n\t\t\tconst response = await fetch(apiPath, {\n\t\t\t\tcredentials: 'include',\n\t\t\t\tsignal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS),\n\t\t\t})\n\t\t\tconst body = (await response.json()) as { active?: boolean }\n\t\t\tif (typeof body.active !== 'boolean') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tsetStatus(body.active ? (body as ImpersonationStatus) : { active: false })\n\t\t\tsetVisible(body.active)\n\t\t} catch {\n\t\t\t// A timeout or a non-JSON body leaves the painted bar in place.\n\t\t}\n\t}, [apiPath, setStatus])\n\n\tuseEffect(() => {\n\t\tif (onLogin) {\n\t\t\tsetVisible(false)\n\t\t\treturn\n\t\t}\n\t\tvoid probe()\n\t}, [onLogin, probe])\n\n\tuseEffect(() => {\n\t\tif (!sessionEndsAt || !visible || onLogin) {\n\t\t\treturn\n\t\t}\n\t\texpiredProbe.current = false\n\t\tconst timer = window.setInterval(() => {\n\t\t\tconst next = Date.now()\n\t\t\tsetNow(next)\n\t\t\tif (expiredProbe.current) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (new Date(sessionEndsAt).getTime() - next <= 0) {\n\t\t\t\texpiredProbe.current = true\n\t\t\t\tvoid probe()\n\t\t\t}\n\t\t}, 1000)\n\t\treturn () => window.clearInterval(timer)\n\t}, [onLogin, probe, sessionEndsAt, visible])\n\n\tconst onExit = async () => {\n\t\tif (busy) {\n\t\t\treturn\n\t\t}\n\t\tsetBusy(true)\n\t\ttry {\n\t\t\tconst result = await postImpersonation(`${apiPath}/exit`, {})\n\t\t\tif (!result.ok) {\n\t\t\t\ttoast.error(messageFor(impersonatorLocale, errorKey(result.error ?? 'failed')))\n\t\t\t\tif (result.error && SESSION_GONE.has(result.error)) {\n\t\t\t\t\tsetStatus({ active: false })\n\t\t\t\t\tsetVisible(false)\n\t\t\t\t\tgoAfterSwitch(adminRoute)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tgoAfterSwitch(adminRoute)\n\t\t} finally {\n\t\t\tsetBusy(false)\n\t\t}\n\t}\n\n\tconst collapse = () => {\n\t\tconst next = { collapsed: true, edge }\n\t\tsetCollapsed(true)\n\t\twriteBarPlacement(next)\n\t}\n\n\tconst expand = () => {\n\t\tconst next = { collapsed: false, edge }\n\t\tsetCollapsed(false)\n\t\tsetDragPoint(null)\n\t\twriteBarPlacement(next)\n\t}\n\n\tconst endChipDrag = () => {\n\t\tif (!dragOffset.current && !dragOrigin.current) {\n\t\t\treturn\n\t\t}\n\t\twindow.removeEventListener('pointerup', onWindowPointerEnd.current)\n\t\twindow.removeEventListener('pointercancel', onWindowPointerEnd.current)\n\t\tconst point = dragPointRef.current\n\t\tconst didDrag = dragged.current\n\t\tdragOffset.current = null\n\t\tdragOrigin.current = null\n\t\tdragPointRef.current = null\n\t\tif (!didDrag) {\n\t\t\texpand()\n\t\t\treturn\n\t\t}\n\t\tconst rect = chipRef.current?.getBoundingClientRect()\n\t\tif (!point || !rect) {\n\t\t\tsetDragPoint(null)\n\t\t\treturn\n\t\t}\n\t\tconst header = Number.parseFloat(\n\t\t\tgetComputedStyle(document.documentElement).getPropertyValue('--app-header-height')\n\t\t)\n\t\tconst anchors = anchorsFor({\n\t\t\theader: Number.isFinite(header) ? header : 48,\n\t\t\tsize: { height: rect.height, width: rect.width },\n\t\t\tviewport: readViewport(),\n\t\t})\n\t\tconst nextEdge = nearestEdge(projectThrow(point, velocity.current), anchors)\n\t\tsetEdge(nextEdge)\n\t\tsetDragPoint(null)\n\t\twriteBarPlacement({ collapsed: true, edge: nextEdge })\n\t}\n\tendChipDragRef.current = endChipDrag\n\n\tconst onChipPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {\n\t\tif (event.button !== 0) {\n\t\t\treturn\n\t\t}\n\t\tconst rect = chipRef.current?.getBoundingClientRect()\n\t\tif (!rect) {\n\t\t\treturn\n\t\t}\n\t\tdragged.current = false\n\t\tdragOrigin.current = { x: event.clientX, y: event.clientY }\n\t\tdragOffset.current = { x: event.clientX - rect.left, y: event.clientY - rect.top }\n\t\tlastSample.current = { t: event.timeStamp, x: event.clientX, y: event.clientY }\n\t\tvelocity.current = { x: 0, y: 0 }\n\t\twindow.addEventListener('pointerup', onWindowPointerEnd.current)\n\t\twindow.addEventListener('pointercancel', onWindowPointerEnd.current)\n\t\tevent.currentTarget.setPointerCapture(event.pointerId)\n\t}\n\n\tconst onChipPointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {\n\t\tif (!dragOffset.current || !dragOrigin.current) {\n\t\t\treturn\n\t\t}\n\t\tif (event.pointerType !== 'touch' && event.buttons === 0) {\n\t\t\tendChipDrag()\n\t\t\treturn\n\t\t}\n\t\tconst travel = Math.hypot(\n\t\t\tevent.clientX - dragOrigin.current.x,\n\t\t\tevent.clientY - dragOrigin.current.y\n\t\t)\n\t\tif (!dragged.current && travel < 4) {\n\t\t\treturn\n\t\t}\n\t\tdragged.current = true\n\t\tconst next = {\n\t\t\tx: event.clientX - dragOffset.current.x,\n\t\t\ty: event.clientY - dragOffset.current.y,\n\t\t}\n\t\tconst previous = lastSample.current\n\t\tconst dt = previous ? event.timeStamp - previous.t : 0\n\t\tif (previous && dt > 0) {\n\t\t\tvelocity.current = {\n\t\t\t\tx: (event.clientX - previous.x) / dt,\n\t\t\t\ty: (event.clientY - previous.y) / dt,\n\t\t\t}\n\t\t}\n\t\tlastSample.current = { t: event.timeStamp, x: event.clientX, y: event.clientY }\n\t\tdragPointRef.current = next\n\t\tsetDragPoint(next)\n\t}\n\n\tif (!visible || onLogin) {\n\t\treturn null\n\t}\n\n\tif (collapsed) {\n\t\tconst rect = chipRef.current?.getBoundingClientRect()\n\t\tconst header = mounted\n\t\t\t? Number.parseFloat(\n\t\t\t\t\tgetComputedStyle(document.documentElement).getPropertyValue('--app-header-height')\n\t\t\t\t)\n\t\t\t: 48\n\t\tconst anchors = anchorsFor({\n\t\t\theader: Number.isFinite(header) ? header : 48,\n\t\t\tsize: { height: rect?.height ?? 36, width: rect?.width ?? 168 },\n\t\t\tviewport,\n\t\t})\n\t\tconst point = dragPoint ?? anchors[edge]\n\t\treturn (\n\t\t\t<div\n\t\t\t\taria-label={actingAs}\n\t\t\t\taria-live=\"polite\"\n\t\t\t\tclassName={[\n\t\t\t\t\t'impersonation-chip',\n\t\t\t\t\ttone === 'quiet' ? 'impersonation-chip--quiet' : null,\n\t\t\t\t\tdragPoint ? 'impersonation-chip--dragging' : null,\n\t\t\t\t]\n\t\t\t\t\t.filter(Boolean)\n\t\t\t\t\t.join(' ')}\n\t\t\t\tonPointerCancel={endChipDrag}\n\t\t\t\tonPointerDown={onChipPointerDown}\n\t\t\t\tonPointerMove={onChipPointerMove}\n\t\t\t\tonPointerUp={endChipDrag}\n\t\t\t\tref={chipRef}\n\t\t\t\trole=\"status\"\n\t\t\t\tstyle={{ left: point.x, top: point.y }}\n\t\t\t>\n\t\t\t\t<span className=\"impersonation-chip__name\">{targetName}</span>\n\t\t\t\t<button\n\t\t\t\t\taria-label={messageFor(impersonatorLocale, keys.expandBar)}\n\t\t\t\t\tclassName=\"impersonation-bar__icon\"\n\t\t\t\t\tonClick={(event) => {\n\t\t\t\t\t\tif (dragged.current) {\n\t\t\t\t\t\t\tevent.preventDefault()\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\texpand()\n\t\t\t\t\t}}\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t>\n\t\t\t\t\t<ExpandIcon />\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t)\n\t}\n\n\tconst countdown = sessionEndsAt ? formatCountdown(sessionEndsAt, now) : null\n\tconst endsLabel =\n\t\tcountdown && countdown !== '0s'\n\t\t\t? fillTemplate(sessionEndsInTemplate, '{{countdown}}', countdown)\n\t\t\t: sessionEndsAt && countdown !== '0s'\n\t\t\t\t? fillTemplate(\n\t\t\t\t\t\tsessionEndsAtTemplate,\n\t\t\t\t\t\t'{{time}}',\n\t\t\t\t\t\tnew Date(sessionEndsAt).toLocaleTimeString(impersonatorLocale ?? undefined)\n\t\t\t\t\t)\n\t\t\t\t: null\n\n\treturn (\n\t\t<div\n\t\t\taria-live=\"polite\"\n\t\t\tclassName={\n\t\t\t\ttone === 'quiet' ? 'impersonation-bar impersonation-bar--quiet' : 'impersonation-bar'\n\t\t\t}\n\t\t\trole=\"status\"\n\t\t>\n\t\t\t<div className=\"impersonation-bar__meta\">\n\t\t\t\t<div className=\"impersonation-bar__copy\">\n\t\t\t\t\t<Pill pillStyle={tone === 'quiet' ? 'success' : 'warning'} size=\"small\">\n\t\t\t\t\t\t{pluginName}\n\t\t\t\t\t</Pill>\n\t\t\t\t\t<span>{actingAs}</span>\n\t\t\t\t</div>\n\t\t\t\t{endsLabel ? <div className=\"impersonation-bar__time\">{endsLabel}</div> : null}\n\t\t\t</div>\n\t\t\t<div className=\"impersonation-bar__actions\">\n\t\t\t\t{showFrontendLink && frontendUrl ? (\n\t\t\t\t\t<a className=\"impersonation-bar__link\" href={frontendUrl}>\n\t\t\t\t\t\t{openWebsite}\n\t\t\t\t\t</a>\n\t\t\t\t) : null}\n\t\t\t\t<Button\n\t\t\t\t\tbuttonStyle=\"pill\"\n\t\t\t\t\tdisabled={busy}\n\t\t\t\t\tmargin={false}\n\t\t\t\t\tonClick={() => void onExit()}\n\t\t\t\t\tsize=\"small\"\n\t\t\t\t>\n\t\t\t\t\t{returnTo}\n\t\t\t\t</Button>\n\t\t\t\t<button\n\t\t\t\t\taria-label={messageFor(impersonatorLocale, keys.collapseBar)}\n\t\t\t\t\tclassName=\"impersonation-bar__icon\"\n\t\t\t\t\tonClick={collapse}\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t>\n\t\t\t\t\t<CollapseIcon />\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n\nconst CollapseIcon = () => (\n\t<svg aria-hidden=\"true\" fill=\"none\" height=\"16\" viewBox=\"0 0 16 16\" width=\"16\">\n\t\t<path d=\"M3.5 6 8 10.5 12.5 6\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\n\t</svg>\n)\n\nconst ExpandIcon = () => (\n\t<svg aria-hidden=\"true\" fill=\"none\" height=\"16\" viewBox=\"0 0 16 16\" width=\"16\">\n\t\t<path d=\"M3 6h10M3 10h10\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\n\t</svg>\n)\n"],"mappings":"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"}
1
+ {"version":3,"file":"ImpersonationBar.js","names":[],"sources":["../../src/ui/ImpersonationBar.tsx"],"sourcesContent":["'use client'\n\nimport { Button, Pill, toast } from '@payloadcms/ui'\nimport { usePathname } from 'next/navigation'\nimport {\n\ttype PointerEvent as ReactPointerEvent,\n\tuseCallback,\n\tuseEffect,\n\tuseRef,\n\tuseState,\n} from 'react'\n\nimport type { ImpersonationStatus } from '../getImpersonation'\nimport { CLIENT_FETCH_TIMEOUT_MS } from '../plugin/constants'\nimport { keys } from '../translations/keys'\nimport { messageFor } from '../translations/lookup'\nimport { errorKey, type FailureCode, goAfterSwitch, postImpersonation } from './api'\nimport {\n\tanchorsFor,\n\ttype BarEdge,\n\tnearestEdge,\n\ttype Point,\n\tprojectThrow,\n\treadBarPlacement,\n\treadViewport,\n\twriteBarPlacement,\n} from './barPlacement'\nimport { sessionEndsLabel } from './sessionEnds'\nimport { useImpersonation } from './useImpersonation'\nimport './impersonation.css'\n\nconst SESSION_GONE = new Set([\n\t'forbidden',\n\t'impersonatorGone',\n\t'impersonatorSessionExpired',\n\t'notImpersonating',\n])\n\nexport type ImpersonationBarProps = {\n\tactingAs: string\n\tadminRoute: string\n\tapiPath: string\n\tfrontendUrl?: string\n\timpersonatorLocale?: null | string\n\t/** Resolved by the provider from the config's translations, so overrides reach the client. */\n\tmessages?: {\n\t\tcollapseBar: string\n\t\terrors: Partial<Record<FailureCode, string>>\n\t\texpandBar: string\n\t}\n\topenWebsite: string\n\tpluginName: string\n\treturnTo: string\n\tsessionEndsAt?: null | string\n\tsessionEndsAtTemplate: string\n\tsessionEndsInTemplate: string\n\tshowFrontendLink?: boolean\n\t/** Collapsed chip label. */\n\ttargetName: string\n\ttone?: 'quiet' | 'warning'\n}\n\nexport const ImpersonationBar = ({\n\tactingAs,\n\tadminRoute,\n\tapiPath,\n\tfrontendUrl,\n\timpersonatorLocale,\n\tmessages,\n\topenWebsite,\n\tpluginName,\n\treturnTo,\n\tsessionEndsAt,\n\tsessionEndsAtTemplate,\n\tsessionEndsInTemplate,\n\tshowFrontendLink,\n\ttargetName,\n\ttone = 'warning',\n}: ImpersonationBarProps) => {\n\tconst { setStatus } = useImpersonation()\n\tconst [mounted, setMounted] = useState(false)\n\tconst pathname = usePathname() ?? ''\n\tconst onLogin = pathname === `${adminRoute}/login`\n\tconst [busy, setBusy] = useState(false)\n\tconst [visible, setVisible] = useState(true)\n\tconst [collapsed, setCollapsed] = useState(false)\n\tconst [edge, setEdge] = useState<BarEdge>('right')\n\tconst [viewport, setViewport] = useState(() =>\n\t\ttypeof document === 'undefined' ? { height: 800, width: 1200 } : readViewport()\n\t)\n\tconst [dragPoint, setDragPoint] = useState<null | Point>(null)\n\tconst [now, setNow] = useState<null | number>(null)\n\tconst chipRef = useRef<HTMLDivElement>(null)\n\tconst expiredProbe = useRef(false)\n\tconst dragOffset = useRef<null | Point>(null)\n\tconst dragPointRef = useRef<null | Point>(null)\n\tconst dragOrigin = useRef<null | Point>(null)\n\tconst dragged = useRef(false)\n\tconst lastSample = useRef<null | { t: number; x: number; y: number }>(null)\n\tconst velocity = useRef<Point>({ x: 0, y: 0 })\n\tconst endChipDragRef = useRef<() => void>(() => {})\n\tconst onWindowPointerEnd = useRef(() => {\n\t\tendChipDragRef.current()\n\t})\n\n\tuseEffect(() => {\n\t\tconst stored = readBarPlacement()\n\t\tsetCollapsed(stored.collapsed)\n\t\tsetEdge(stored.edge)\n\t\tsetMounted(true)\n\t}, [])\n\n\tuseEffect(() => {\n\t\tif (!collapsed) {\n\t\t\treturn\n\t\t}\n\t\tconst update = () => setViewport(readViewport())\n\t\tupdate()\n\t\twindow.addEventListener('resize', update)\n\t\treturn () => window.removeEventListener('resize', update)\n\t}, [collapsed])\n\n\tconst probe = useCallback(async () => {\n\t\ttry {\n\t\t\tconst response = await fetch(apiPath, {\n\t\t\t\tcredentials: 'include',\n\t\t\t\tsignal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS),\n\t\t\t})\n\t\t\tconst body = (await response.json()) as { active?: boolean }\n\t\t\tif (typeof body.active !== 'boolean') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tsetStatus(body.active ? (body as ImpersonationStatus) : { active: false })\n\t\t\tsetVisible(body.active)\n\t\t} catch {\n\t\t\t// A timeout or a non-JSON body leaves the painted bar in place.\n\t\t}\n\t}, [apiPath, setStatus])\n\n\tuseEffect(() => {\n\t\tif (onLogin) {\n\t\t\tsetVisible(false)\n\t\t\treturn\n\t\t}\n\t\tvoid probe()\n\t}, [onLogin, probe])\n\n\tuseEffect(() => {\n\t\tif (!sessionEndsAt || !visible || onLogin) {\n\t\t\treturn\n\t\t}\n\t\texpiredProbe.current = false\n\t\tsetNow(Date.now())\n\t\tconst timer = window.setInterval(() => {\n\t\t\tconst next = Date.now()\n\t\t\tsetNow(next)\n\t\t\tif (expiredProbe.current) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (new Date(sessionEndsAt).getTime() - next <= 0) {\n\t\t\t\texpiredProbe.current = true\n\t\t\t\tvoid probe()\n\t\t\t}\n\t\t}, 1000)\n\t\treturn () => window.clearInterval(timer)\n\t}, [onLogin, probe, sessionEndsAt, visible])\n\n\tconst onExit = async () => {\n\t\tif (busy) {\n\t\t\treturn\n\t\t}\n\t\tsetBusy(true)\n\t\ttry {\n\t\t\tconst result = await postImpersonation(`${apiPath}/exit`, {})\n\t\t\tif (!result.ok) {\n\t\t\t\tconst code = result.error ?? 'failed'\n\t\t\t\ttoast.error(messages?.errors[code] ?? messageFor(impersonatorLocale, errorKey(code)))\n\t\t\t\tif (result.error && SESSION_GONE.has(result.error)) {\n\t\t\t\t\tsetStatus({ active: false })\n\t\t\t\t\tsetVisible(false)\n\t\t\t\t\tgoAfterSwitch(adminRoute)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tgoAfterSwitch(adminRoute)\n\t\t} finally {\n\t\t\tsetBusy(false)\n\t\t}\n\t}\n\n\tconst collapse = () => {\n\t\tconst next = { collapsed: true, edge }\n\t\tsetCollapsed(true)\n\t\twriteBarPlacement(next)\n\t}\n\n\tconst expand = () => {\n\t\tconst next = { collapsed: false, edge }\n\t\tsetCollapsed(false)\n\t\tsetDragPoint(null)\n\t\twriteBarPlacement(next)\n\t}\n\n\tconst endChipDrag = () => {\n\t\tif (!dragOffset.current && !dragOrigin.current) {\n\t\t\treturn\n\t\t}\n\t\twindow.removeEventListener('pointerup', onWindowPointerEnd.current)\n\t\twindow.removeEventListener('pointercancel', onWindowPointerEnd.current)\n\t\tconst point = dragPointRef.current\n\t\tconst didDrag = dragged.current\n\t\tdragOffset.current = null\n\t\tdragOrigin.current = null\n\t\tdragPointRef.current = null\n\t\tif (!didDrag) {\n\t\t\texpand()\n\t\t\treturn\n\t\t}\n\t\tconst rect = chipRef.current?.getBoundingClientRect()\n\t\tif (!point || !rect) {\n\t\t\tsetDragPoint(null)\n\t\t\treturn\n\t\t}\n\t\tconst header = Number.parseFloat(\n\t\t\tgetComputedStyle(document.documentElement).getPropertyValue('--app-header-height')\n\t\t)\n\t\tconst anchors = anchorsFor({\n\t\t\theader: Number.isFinite(header) ? header : 48,\n\t\t\tsize: { height: rect.height, width: rect.width },\n\t\t\tviewport: readViewport(),\n\t\t})\n\t\tconst nextEdge = nearestEdge(projectThrow(point, velocity.current), anchors)\n\t\tsetEdge(nextEdge)\n\t\tsetDragPoint(null)\n\t\twriteBarPlacement({ collapsed: true, edge: nextEdge })\n\t}\n\tendChipDragRef.current = endChipDrag\n\n\tconst onChipPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {\n\t\tif (event.button !== 0) {\n\t\t\treturn\n\t\t}\n\t\tconst rect = chipRef.current?.getBoundingClientRect()\n\t\tif (!rect) {\n\t\t\treturn\n\t\t}\n\t\tdragged.current = false\n\t\tdragOrigin.current = { x: event.clientX, y: event.clientY }\n\t\tdragOffset.current = { x: event.clientX - rect.left, y: event.clientY - rect.top }\n\t\tlastSample.current = { t: event.timeStamp, x: event.clientX, y: event.clientY }\n\t\tvelocity.current = { x: 0, y: 0 }\n\t\twindow.addEventListener('pointerup', onWindowPointerEnd.current)\n\t\twindow.addEventListener('pointercancel', onWindowPointerEnd.current)\n\t\tevent.currentTarget.setPointerCapture(event.pointerId)\n\t}\n\n\tconst onChipPointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {\n\t\tif (!dragOffset.current || !dragOrigin.current) {\n\t\t\treturn\n\t\t}\n\t\tif (event.pointerType !== 'touch' && event.buttons === 0) {\n\t\t\tendChipDrag()\n\t\t\treturn\n\t\t}\n\t\tconst travel = Math.hypot(\n\t\t\tevent.clientX - dragOrigin.current.x,\n\t\t\tevent.clientY - dragOrigin.current.y\n\t\t)\n\t\tif (!dragged.current && travel < 4) {\n\t\t\treturn\n\t\t}\n\t\tdragged.current = true\n\t\tconst next = {\n\t\t\tx: event.clientX - dragOffset.current.x,\n\t\t\ty: event.clientY - dragOffset.current.y,\n\t\t}\n\t\tconst previous = lastSample.current\n\t\tconst dt = previous ? event.timeStamp - previous.t : 0\n\t\tif (previous && dt > 0) {\n\t\t\tvelocity.current = {\n\t\t\t\tx: (event.clientX - previous.x) / dt,\n\t\t\t\ty: (event.clientY - previous.y) / dt,\n\t\t\t}\n\t\t}\n\t\tlastSample.current = { t: event.timeStamp, x: event.clientX, y: event.clientY }\n\t\tdragPointRef.current = next\n\t\tsetDragPoint(next)\n\t}\n\n\tif (!visible || onLogin) {\n\t\treturn null\n\t}\n\n\tif (collapsed) {\n\t\tconst rect = chipRef.current?.getBoundingClientRect()\n\t\tconst header = mounted\n\t\t\t? Number.parseFloat(\n\t\t\t\t\tgetComputedStyle(document.documentElement).getPropertyValue('--app-header-height')\n\t\t\t\t)\n\t\t\t: 48\n\t\tconst anchors = anchorsFor({\n\t\t\theader: Number.isFinite(header) ? header : 48,\n\t\t\tsize: { height: rect?.height ?? 36, width: rect?.width ?? 168 },\n\t\t\tviewport,\n\t\t})\n\t\tconst point = dragPoint ?? anchors[edge]\n\t\treturn (\n\t\t\t<div\n\t\t\t\taria-label={actingAs}\n\t\t\t\taria-live=\"polite\"\n\t\t\t\tclassName={[\n\t\t\t\t\t'impersonation-chip',\n\t\t\t\t\ttone === 'quiet' ? 'impersonation-chip--quiet' : null,\n\t\t\t\t\tdragPoint ? 'impersonation-chip--dragging' : null,\n\t\t\t\t]\n\t\t\t\t\t.filter(Boolean)\n\t\t\t\t\t.join(' ')}\n\t\t\t\tonPointerCancel={endChipDrag}\n\t\t\t\tonPointerDown={onChipPointerDown}\n\t\t\t\tonPointerMove={onChipPointerMove}\n\t\t\t\tonPointerUp={endChipDrag}\n\t\t\t\tref={chipRef}\n\t\t\t\trole=\"status\"\n\t\t\t\tstyle={{ left: point.x, top: point.y }}\n\t\t\t>\n\t\t\t\t<span className=\"impersonation-chip__name\">{targetName}</span>\n\t\t\t\t<button\n\t\t\t\t\taria-label={messages?.expandBar ?? messageFor(impersonatorLocale, keys.expandBar)}\n\t\t\t\t\tclassName=\"impersonation-bar__icon\"\n\t\t\t\t\tonClick={(event) => {\n\t\t\t\t\t\tif (dragged.current) {\n\t\t\t\t\t\t\tevent.preventDefault()\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\texpand()\n\t\t\t\t\t}}\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t>\n\t\t\t\t\t<ExpandIcon />\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t)\n\t}\n\n\tconst endsLabel = sessionEndsLabel({\n\t\tendsAt: sessionEndsAt,\n\t\tlocale: impersonatorLocale,\n\t\tnow,\n\t\ttemplates: { endsAt: sessionEndsAtTemplate, endsIn: sessionEndsInTemplate },\n\t})\n\n\treturn (\n\t\t<div\n\t\t\taria-live=\"polite\"\n\t\t\tclassName={\n\t\t\t\ttone === 'quiet' ? 'impersonation-bar impersonation-bar--quiet' : 'impersonation-bar'\n\t\t\t}\n\t\t\trole=\"status\"\n\t\t>\n\t\t\t<div className=\"impersonation-bar__meta\">\n\t\t\t\t<div className=\"impersonation-bar__copy\">\n\t\t\t\t\t<Pill pillStyle={tone === 'quiet' ? 'success' : 'warning'} size=\"small\">\n\t\t\t\t\t\t{pluginName}\n\t\t\t\t\t</Pill>\n\t\t\t\t\t<span>{actingAs}</span>\n\t\t\t\t</div>\n\t\t\t\t{sessionEndsAt ? (\n\t\t\t\t\t<div className=\"impersonation-bar__time\">{endsLabel ?? '\\u00a0'}</div>\n\t\t\t\t) : null}\n\t\t\t</div>\n\t\t\t<div className=\"impersonation-bar__actions\">\n\t\t\t\t{showFrontendLink && frontendUrl ? (\n\t\t\t\t\t<a className=\"impersonation-bar__link\" href={frontendUrl}>\n\t\t\t\t\t\t{openWebsite}\n\t\t\t\t\t</a>\n\t\t\t\t) : null}\n\t\t\t\t<Button\n\t\t\t\t\tbuttonStyle=\"pill\"\n\t\t\t\t\tdisabled={busy}\n\t\t\t\t\tmargin={false}\n\t\t\t\t\tonClick={() => void onExit()}\n\t\t\t\t\tsize=\"small\"\n\t\t\t\t>\n\t\t\t\t\t{returnTo}\n\t\t\t\t</Button>\n\t\t\t\t<button\n\t\t\t\t\taria-label={messages?.collapseBar ?? messageFor(impersonatorLocale, keys.collapseBar)}\n\t\t\t\t\tclassName=\"impersonation-bar__icon\"\n\t\t\t\t\tonClick={collapse}\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t>\n\t\t\t\t\t<CollapseIcon />\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n\nconst CollapseIcon = () => (\n\t<svg aria-hidden=\"true\" fill=\"none\" height=\"16\" viewBox=\"0 0 16 16\" width=\"16\">\n\t\t<path d=\"M3.5 6 8 10.5 12.5 6\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\n\t</svg>\n)\n\nconst ExpandIcon = () => (\n\t<svg aria-hidden=\"true\" fill=\"none\" height=\"16\" viewBox=\"0 0 16 16\" width=\"16\">\n\t\t<path d=\"M3 6h10M3 10h10\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\n\t</svg>\n)\n"],"mappings":"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"}
@@ -8,9 +8,11 @@ import { readHintCookie } from "../plugin/lookup.js";
8
8
  import { startableCollectionSlugs } from "../plugin/startable.js";
9
9
  import { keys } from "../translations/keys.js";
10
10
  import { resolveTargetFilters } from "../access/filterTargets.js";
11
+ import { errorKey } from "../translations/errorKey.js";
11
12
  import { ImpersonationClientConfig } from "./ImpersonationConfig.js";
12
13
  import { fillTemplate, messageFor } from "../translations/lookup.js";
13
14
  import { ImpersonationBar } from "./ImpersonationBar.js";
15
+ import { FAILURE_CODES } from "../endpoints/codes.js";
14
16
  import { createLocalReq } from "payload";
15
17
  import { jsx, jsxs } from "react/jsx-runtime";
16
18
  import { headers } from "next/headers";
@@ -90,6 +92,7 @@ const ImpersonationProvider = async ({ children, i18n, payload, user }) => {
90
92
  children
91
93
  });
92
94
  const locale = barSource.impersonatorLocale;
95
+ const message = (key) => messageFor(locale, key, payload.config.i18n.translations);
93
96
  const targetName = labelUser(options.ui.userLabel, {
94
97
  email: barSource.targetEmail,
95
98
  title: barSource.targetTitle
@@ -99,8 +102,8 @@ const ImpersonationProvider = async ({ children, i18n, payload, user }) => {
99
102
  title: barSource.impersonatorTitle
100
103
  }) ?? String(barSource.impersonatorId ?? "");
101
104
  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));
105
+ const actingAs = isImpersonatorSide ? fillTemplate(message(keys.sessionActiveOnSite), "{{name}}", String(targetName)) : fillTemplate(message(keys.actingAs), "{{name}}", String(targetName));
106
+ const returnTo = isImpersonatorSide ? message(keys.endSession) : fillTemplate(message(keys.returnTo), "{{name}}", String(impersonatorLabel));
104
107
  return /* @__PURE__ */ jsxs(ImpersonationClientConfig, {
105
108
  value: clientValue,
106
109
  children: [children, /* @__PURE__ */ jsx(ImpersonationBar, {
@@ -109,12 +112,17 @@ const ImpersonationProvider = async ({ children, i18n, payload, user }) => {
109
112
  apiPath,
110
113
  frontendUrl: payload.config.serverURL || void 0,
111
114
  impersonatorLocale: locale,
112
- openWebsite: messageFor(locale, keys.openWebsite),
113
- pluginName: messageFor(locale, keys.pluginName),
115
+ messages: {
116
+ collapseBar: message(keys.collapseBar),
117
+ errors: Object.fromEntries(FAILURE_CODES.map((code) => [code, message(errorKey(code))])),
118
+ expandBar: message(keys.expandBar)
119
+ },
120
+ openWebsite: message(keys.openWebsite),
121
+ pluginName: message(keys.pluginName),
114
122
  returnTo,
115
123
  sessionEndsAt: barSource.absoluteExpiresAt,
116
- sessionEndsAtTemplate: messageFor(locale, keys.sessionEndsAt),
117
- sessionEndsInTemplate: messageFor(locale, keys.sessionEndsIn),
124
+ sessionEndsAtTemplate: message(keys.sessionEndsAt),
125
+ sessionEndsInTemplate: message(keys.sessionEndsIn),
118
126
  showFrontendLink: isImpersonatorSide,
119
127
  targetName,
120
128
  tone: isImpersonatorSide ? "quiet" : "warning"
@@ -1 +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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"}
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 { FAILURE_CODES } from '../endpoints/codes'\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 { errorKey } from '../translations/errorKey'\nimport { keys, type TranslationKey } 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 message = (key: TranslationKey) => messageFor(locale, key, payload.config.i18n.translations)\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(message(keys.sessionActiveOnSite), '{{name}}', String(targetName))\n\t\t: fillTemplate(message(keys.actingAs), '{{name}}', String(targetName))\n\tconst returnTo = isImpersonatorSide\n\t\t? message(keys.endSession)\n\t\t: fillTemplate(message(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\tmessages={{\n\t\t\t\t\tcollapseBar: message(keys.collapseBar),\n\t\t\t\t\terrors: Object.fromEntries(FAILURE_CODES.map((code) => [code, message(errorKey(code))])),\n\t\t\t\t\texpandBar: message(keys.expandBar),\n\t\t\t\t}}\n\t\t\t\topenWebsite={message(keys.openWebsite)}\n\t\t\t\tpluginName={message(keys.pluginName)}\n\t\t\t\treturnTo={returnTo}\n\t\t\t\tsessionEndsAt={barSource.absoluteExpiresAt}\n\t\t\t\tsessionEndsAtTemplate={message(keys.sessionEndsAt)}\n\t\t\t\tsessionEndsInTemplate={message(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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"}
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import { keys } from "../translations/keys.js";
3
3
  import { useTranslation as useTranslation$1 } from "../translations/useTranslation.js";
4
- import { errorKey, goAfterSwitch, postImpersonation } from "./api.js";
4
+ import { errorKey } from "../translations/errorKey.js";
5
+ import { goAfterSwitch, postImpersonation } from "./api.js";
5
6
  import { useRef, useState } from "react";
6
7
  import { ConfirmationModal, TextInput, toast } from "@payloadcms/ui";
7
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -1 +1 @@
1
- {"version":3,"file":"StartConfirmModal.js","names":["useTranslation"],"sources":["../../src/ui/StartConfirmModal.tsx"],"sourcesContent":["'use client'\n\nimport { ConfirmationModal, TextInput, toast } from '@payloadcms/ui'\nimport { type ChangeEvent, useRef, useState } from 'react'\n\nimport { keys } from '../translations/keys'\nimport { useTranslation } from '../translations/useTranslation'\nimport { errorKey, goAfterSwitch, postImpersonation } from './api'\n\nexport type StartTarget = {\n\tcollection: string\n\tid: number | string\n\tlabel: string\n}\n\nexport type StartConfirmModalProps = {\n\tapiPath: string\n\tmodalSlug: string\n\treasonMode: 'off' | 'optional' | 'required'\n\ttarget: null | StartTarget\n}\n\n/**\n * Payload `ConfirmationModal` for starting a session. Reason uses `TextInput`\n * in the body when the host asked for optional or required notes.\n *\n * `key={generation}` remounts after a failed confirm so the native Loading\n * state resets. Throw still keeps the modal open.\n */\nexport const StartConfirmModal = ({\n\tapiPath,\n\tmodalSlug,\n\treasonMode,\n\ttarget,\n}: StartConfirmModalProps) => {\n\tconst { t } = useTranslation()\n\tconst [reason, setReason] = useState('')\n\tconst [generation, setGeneration] = useState(0)\n\tconst reasonRef = useRef(reason)\n\treasonRef.current = reason\n\tconst targetRef = useRef(target)\n\ttargetRef.current = target\n\n\tconst failConfirm = (message: string): never => {\n\t\tsetGeneration((current) => current + 1)\n\t\tthrow new Error(message)\n\t}\n\n\tconst onConfirm = async () => {\n\t\tconst next = targetRef.current\n\t\tif (!next) {\n\t\t\treturn\n\t\t}\n\t\tif (reasonMode === 'required' && !reasonRef.current.trim()) {\n\t\t\ttoast.error(t(keys.errorReasonRequired))\n\t\t\tfailConfirm('reason required')\n\t\t}\n\t\tconst result = await postImpersonation(`${apiPath}/start`, {\n\t\t\tcollection: next.collection,\n\t\t\tid: next.id,\n\t\t\treason: reasonMode === 'off' ? undefined : reasonRef.current,\n\t\t})\n\t\tif (!result.ok) {\n\t\t\ttoast.error(result.error ? t(errorKey(result.error)) : t(keys.errorFailed))\n\t\t\tfailConfirm(result.error ?? 'failed')\n\t\t}\n\t\tgoAfterSwitch(result.redirect)\n\t}\n\n\treturn (\n\t\t<ConfirmationModal\n\t\t\tbody={\n\t\t\t\t<>\n\t\t\t\t\t<p>{target?.label}</p>\n\t\t\t\t\t{reasonMode === 'off' ? null : (\n\t\t\t\t\t\t<TextInput\n\t\t\t\t\t\t\tlabel={t(keys.reasonLabel)}\n\t\t\t\t\t\t\tonChange={(event: ChangeEvent<HTMLInputElement>) => setReason(event.target.value)}\n\t\t\t\t\t\t\tpath={`${modalSlug}-reason`}\n\t\t\t\t\t\t\tplaceholder={reasonMode === 'required' ? undefined : t(keys.reasonPlaceholder)}\n\t\t\t\t\t\t\trequired={reasonMode === 'required'}\n\t\t\t\t\t\t\tvalue={reason}\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t\t</>\n\t\t\t}\n\t\t\tclassName=\"impersonation-confirm-modal\"\n\t\t\tconfirmLabel={t(keys.confirm)}\n\t\t\theading={t(keys.confirmTitle)}\n\t\t\tkey={generation}\n\t\t\tmodalSlug={modalSlug}\n\t\t\tonConfirm={onConfirm}\n\t\t/>\n\t)\n}\n"],"mappings":";;;;;;;;;;;;;;;AA6BA,MAAa,qBAAqB,EACjC,SACA,WACA,YACA,aAC6B;CAC7B,MAAM,EAAE,MAAMA,iBAAe;CAC7B,MAAM,CAAC,QAAQ,aAAa,SAAS,EAAE;CACvC,MAAM,CAAC,YAAY,iBAAiB,SAAS,CAAC;CAC9C,MAAM,YAAY,OAAO,MAAM;CAC/B,UAAU,UAAU;CACpB,MAAM,YAAY,OAAO,MAAM;CAC/B,UAAU,UAAU;CAEpB,MAAM,eAAe,YAA2B;EAC/C,eAAe,YAAY,UAAU,CAAC;EACtC,MAAM,IAAI,MAAM,OAAO;CACxB;CAEA,MAAM,YAAY,YAAY;EAC7B,MAAM,OAAO,UAAU;EACvB,IAAI,CAAC,MACJ;EAED,IAAI,eAAe,cAAc,CAAC,UAAU,QAAQ,KAAK,GAAG;GAC3D,MAAM,MAAM,EAAE,KAAK,mBAAmB,CAAC;GACvC,YAAY,iBAAiB;EAC9B;EACA,MAAM,SAAS,MAAM,kBAAkB,GAAG,QAAQ,SAAS;GAC1D,YAAY,KAAK;GACjB,IAAI,KAAK;GACT,QAAQ,eAAe,QAAQ,KAAA,IAAY,UAAU;EACtD,CAAC;EACD,IAAI,CAAC,OAAO,IAAI;GACf,MAAM,MAAM,OAAO,QAAQ,EAAE,SAAS,OAAO,KAAK,CAAC,IAAI,EAAE,KAAK,WAAW,CAAC;GAC1E,YAAY,OAAO,SAAS,QAAQ;EACrC;EACA,cAAc,OAAO,QAAQ;CAC9B;CAEA,OACC,oBAAC,mBAAD;EACC,MACC,qBAAA,UAAA,EAAA,UAAA,CACC,oBAAC,KAAD,EAAA,UAAI,QAAQ,MAAS,CAAA,GACpB,eAAe,QAAQ,OACvB,oBAAC,WAAD;GACC,OAAO,EAAE,KAAK,WAAW;GACzB,WAAW,UAAyC,UAAU,MAAM,OAAO,KAAK;GAChF,MAAM,GAAG,UAAU;GACnB,aAAa,eAAe,aAAa,KAAA,IAAY,EAAE,KAAK,iBAAiB;GAC7E,UAAU,eAAe;GACzB,OAAO;EACP,CAAA,CAED,EAAA,CAAA;EAEH,WAAU;EACV,cAAc,EAAE,KAAK,OAAO;EAC5B,SAAS,EAAE,KAAK,YAAY;EAEjB;EACA;CACX,GAHK,UAGL;AAEH"}
1
+ {"version":3,"file":"StartConfirmModal.js","names":["useTranslation"],"sources":["../../src/ui/StartConfirmModal.tsx"],"sourcesContent":["'use client'\n\nimport { ConfirmationModal, TextInput, toast } from '@payloadcms/ui'\nimport { type ChangeEvent, useRef, useState } from 'react'\n\nimport { keys } from '../translations/keys'\nimport { useTranslation } from '../translations/useTranslation'\nimport { errorKey, goAfterSwitch, postImpersonation } from './api'\n\nexport type StartTarget = {\n\tcollection: string\n\tid: number | string\n\tlabel: string\n}\n\nexport type StartConfirmModalProps = {\n\tapiPath: string\n\tmodalSlug: string\n\treasonMode: 'off' | 'optional' | 'required'\n\ttarget: null | StartTarget\n}\n\n/**\n * Payload `ConfirmationModal` for starting a session. Reason uses `TextInput`\n * in the body when the host asked for optional or required notes.\n *\n * `key={generation}` remounts after a failed confirm so the native Loading\n * state resets. Throw still keeps the modal open.\n */\nexport const StartConfirmModal = ({\n\tapiPath,\n\tmodalSlug,\n\treasonMode,\n\ttarget,\n}: StartConfirmModalProps) => {\n\tconst { t } = useTranslation()\n\tconst [reason, setReason] = useState('')\n\tconst [generation, setGeneration] = useState(0)\n\tconst reasonRef = useRef(reason)\n\treasonRef.current = reason\n\tconst targetRef = useRef(target)\n\ttargetRef.current = target\n\n\tconst failConfirm = (message: string): never => {\n\t\tsetGeneration((current) => current + 1)\n\t\tthrow new Error(message)\n\t}\n\n\tconst onConfirm = async () => {\n\t\tconst next = targetRef.current\n\t\tif (!next) {\n\t\t\treturn\n\t\t}\n\t\tif (reasonMode === 'required' && !reasonRef.current.trim()) {\n\t\t\ttoast.error(t(keys.errorReasonRequired))\n\t\t\tfailConfirm('reason required')\n\t\t}\n\t\tconst result = await postImpersonation(`${apiPath}/start`, {\n\t\t\tcollection: next.collection,\n\t\t\tid: next.id,\n\t\t\treason: reasonMode === 'off' ? undefined : reasonRef.current,\n\t\t})\n\t\tif (!result.ok) {\n\t\t\ttoast.error(result.error ? t(errorKey(result.error)) : t(keys.errorFailed))\n\t\t\tfailConfirm(result.error ?? 'failed')\n\t\t}\n\t\tgoAfterSwitch(result.redirect)\n\t}\n\n\treturn (\n\t\t<ConfirmationModal\n\t\t\tbody={\n\t\t\t\t<>\n\t\t\t\t\t<p>{target?.label}</p>\n\t\t\t\t\t{reasonMode === 'off' ? null : (\n\t\t\t\t\t\t<TextInput\n\t\t\t\t\t\t\tlabel={t(keys.reasonLabel)}\n\t\t\t\t\t\t\tonChange={(event: ChangeEvent<HTMLInputElement>) => setReason(event.target.value)}\n\t\t\t\t\t\t\tpath={`${modalSlug}-reason`}\n\t\t\t\t\t\t\tplaceholder={reasonMode === 'required' ? undefined : t(keys.reasonPlaceholder)}\n\t\t\t\t\t\t\trequired={reasonMode === 'required'}\n\t\t\t\t\t\t\tvalue={reason}\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t\t</>\n\t\t\t}\n\t\t\tclassName=\"impersonation-confirm-modal\"\n\t\t\tconfirmLabel={t(keys.confirm)}\n\t\t\theading={t(keys.confirmTitle)}\n\t\t\tkey={generation}\n\t\t\tmodalSlug={modalSlug}\n\t\t\tonConfirm={onConfirm}\n\t\t/>\n\t)\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA6BA,MAAa,qBAAqB,EACjC,SACA,WACA,YACA,aAC6B;CAC7B,MAAM,EAAE,MAAMA,iBAAe;CAC7B,MAAM,CAAC,QAAQ,aAAa,SAAS,EAAE;CACvC,MAAM,CAAC,YAAY,iBAAiB,SAAS,CAAC;CAC9C,MAAM,YAAY,OAAO,MAAM;CAC/B,UAAU,UAAU;CACpB,MAAM,YAAY,OAAO,MAAM;CAC/B,UAAU,UAAU;CAEpB,MAAM,eAAe,YAA2B;EAC/C,eAAe,YAAY,UAAU,CAAC;EACtC,MAAM,IAAI,MAAM,OAAO;CACxB;CAEA,MAAM,YAAY,YAAY;EAC7B,MAAM,OAAO,UAAU;EACvB,IAAI,CAAC,MACJ;EAED,IAAI,eAAe,cAAc,CAAC,UAAU,QAAQ,KAAK,GAAG;GAC3D,MAAM,MAAM,EAAE,KAAK,mBAAmB,CAAC;GACvC,YAAY,iBAAiB;EAC9B;EACA,MAAM,SAAS,MAAM,kBAAkB,GAAG,QAAQ,SAAS;GAC1D,YAAY,KAAK;GACjB,IAAI,KAAK;GACT,QAAQ,eAAe,QAAQ,KAAA,IAAY,UAAU;EACtD,CAAC;EACD,IAAI,CAAC,OAAO,IAAI;GACf,MAAM,MAAM,OAAO,QAAQ,EAAE,SAAS,OAAO,KAAK,CAAC,IAAI,EAAE,KAAK,WAAW,CAAC;GAC1E,YAAY,OAAO,SAAS,QAAQ;EACrC;EACA,cAAc,OAAO,QAAQ;CAC9B;CAEA,OACC,oBAAC,mBAAD;EACC,MACC,qBAAA,UAAA,EAAA,UAAA,CACC,oBAAC,KAAD,EAAA,UAAI,QAAQ,MAAS,CAAA,GACpB,eAAe,QAAQ,OACvB,oBAAC,WAAD;GACC,OAAO,EAAE,KAAK,WAAW;GACzB,WAAW,UAAyC,UAAU,MAAM,OAAO,KAAK;GAChF,MAAM,GAAG,UAAU;GACnB,aAAa,eAAe,aAAa,KAAA,IAAY,EAAE,KAAK,iBAAiB;GAC7E,UAAU,eAAe;GACzB,OAAO;EACP,CAAA,CAED,EAAA,CAAA;EAEH,WAAU;EACV,cAAc,EAAE,KAAK,OAAO;EAC5B,SAAS,EAAE,KAAK,YAAY;EAEjB;EACA;CACX,GAHK,UAGL;AAEH"}
package/dist/ui/api.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { CLIENT_FETCH_TIMEOUT_MS } from "../plugin/constants.js";
3
- import { keys } from "../translations/keys.js";
3
+ import "../translations/errorKey.js";
4
4
  //#region src/ui/api.ts
5
5
  const jsonHeaders = { "Content-Type": "application/json" };
6
6
  const postImpersonation = async (path, body) => {
@@ -41,26 +41,7 @@ const goAfterSwitch = (redirect) => {
41
41
  }
42
42
  window.location.assign(next);
43
43
  };
44
- const errorKey = (code) => {
45
- return {
46
- alreadyImpersonating: keys.errorAlreadyImpersonating,
47
- failed: keys.errorFailed,
48
- forbidden: keys.errorForbidden,
49
- impersonatorGone: keys.errorImpersonatorGone,
50
- impersonatorSessionExpired: keys.errorImpersonatorSessionExpired,
51
- invalidBody: keys.errorInvalidBody,
52
- notImpersonating: keys.errorNotImpersonating,
53
- origin: keys.errorOrigin,
54
- reasonRequired: keys.errorReasonRequired,
55
- selfTarget: keys.errorSelfTarget,
56
- targetNotFound: keys.errorTargetNotFound,
57
- targetTrashed: keys.errorTargetTrashed,
58
- targetUnverified: keys.errorTargetUnverified,
59
- unsupportedAuth: keys.errorUnsupportedAuth,
60
- unsupportedCollection: keys.errorUnsupportedCollection
61
- }[code] ?? keys.errorFailed;
62
- };
63
44
  //#endregion
64
- export { errorKey, goAfterSwitch, postImpersonation };
45
+ export { goAfterSwitch, postImpersonation };
65
46
 
66
47
  //# sourceMappingURL=api.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"api.js","names":[],"sources":["../../src/ui/api.ts"],"sourcesContent":["'use client'\n\nimport type { FailureCode } from '../endpoints/codes'\nimport { CLIENT_FETCH_TIMEOUT_MS } from '../plugin/constants'\nimport { keys } from '../translations/keys'\n\nexport type { FailureCode }\n\nconst jsonHeaders = { 'Content-Type': 'application/json' }\n\nexport const postImpersonation = async (\n\tpath: string,\n\tbody: unknown\n): Promise<{ error?: FailureCode; ok: boolean; redirect?: string; status: number }> => {\n\ttry {\n\t\tconst response = await fetch(path, {\n\t\t\tbody: JSON.stringify(body ?? {}),\n\t\t\tcredentials: 'include',\n\t\t\theaders: jsonHeaders,\n\t\t\tmethod: 'POST',\n\t\t\tsignal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS),\n\t\t})\n\t\tlet parsed: { error?: FailureCode; redirect?: string } = {}\n\t\ttry {\n\t\t\tparsed = (await response.json()) as { error?: FailureCode; redirect?: string }\n\t\t} catch {\n\t\t\tparsed = {}\n\t\t}\n\t\treturn {\n\t\t\terror: parsed.error,\n\t\t\tok: response.ok,\n\t\t\tredirect: parsed.redirect,\n\t\t\tstatus: response.status,\n\t\t}\n\t} catch {\n\t\treturn { error: 'failed', ok: false, status: 0 }\n\t}\n}\n\n/** Reload when the redirect is the current path; `assign` would be a no-op. */\nexport const goAfterSwitch = (redirect?: string) => {\n\tconst next = redirect ?? '/'\n\tconst path = new URL(next, window.location.origin).pathname\n\tif (path === window.location.pathname) {\n\t\twindow.location.reload()\n\t\treturn\n\t}\n\twindow.location.assign(next)\n}\n\nexport const errorKey = (code: string) => {\n\tconst map: Record<string, (typeof keys)[keyof typeof keys]> = {\n\t\talreadyImpersonating: keys.errorAlreadyImpersonating,\n\t\tfailed: keys.errorFailed,\n\t\tforbidden: keys.errorForbidden,\n\t\timpersonatorGone: keys.errorImpersonatorGone,\n\t\timpersonatorSessionExpired: keys.errorImpersonatorSessionExpired,\n\t\tinvalidBody: keys.errorInvalidBody,\n\t\tnotImpersonating: keys.errorNotImpersonating,\n\t\torigin: keys.errorOrigin,\n\t\treasonRequired: keys.errorReasonRequired,\n\t\tselfTarget: keys.errorSelfTarget,\n\t\ttargetNotFound: keys.errorTargetNotFound,\n\t\ttargetTrashed: keys.errorTargetTrashed,\n\t\ttargetUnverified: keys.errorTargetUnverified,\n\t\tunsupportedAuth: keys.errorUnsupportedAuth,\n\t\tunsupportedCollection: keys.errorUnsupportedCollection,\n\t}\n\treturn map[code] ?? keys.errorFailed\n}\n"],"mappings":";;;;AAQA,MAAM,cAAc,EAAE,gBAAgB,mBAAmB;AAEzD,MAAa,oBAAoB,OAChC,MACA,SACsF;CACtF,IAAI;EACH,MAAM,WAAW,MAAM,MAAM,MAAM;GAClC,MAAM,KAAK,UAAU,QAAQ,CAAC,CAAC;GAC/B,aAAa;GACb,SAAS;GACT,QAAQ;GACR,QAAQ,YAAY,QAAQ,uBAAuB;EACpD,CAAC;EACD,IAAI,SAAqD,CAAC;EAC1D,IAAI;GACH,SAAU,MAAM,SAAS,KAAK;EAC/B,QAAQ;GACP,SAAS,CAAC;EACX;EACA,OAAO;GACN,OAAO,OAAO;GACd,IAAI,SAAS;GACb,UAAU,OAAO;GACjB,QAAQ,SAAS;EAClB;CACD,QAAQ;EACP,OAAO;GAAE,OAAO;GAAU,IAAI;GAAO,QAAQ;EAAE;CAChD;AACD;;AAGA,MAAa,iBAAiB,aAAsB;CACnD,MAAM,OAAO,YAAY;CAEzB,IADa,IAAI,IAAI,MAAM,OAAO,SAAS,MAAM,EAAE,aACtC,OAAO,SAAS,UAAU;EACtC,OAAO,SAAS,OAAO;EACvB;CACD;CACA,OAAO,SAAS,OAAO,IAAI;AAC5B;AAEA,MAAa,YAAY,SAAiB;CAkBzC,OAAO;EAhBN,sBAAsB,KAAK;EAC3B,QAAQ,KAAK;EACb,WAAW,KAAK;EAChB,kBAAkB,KAAK;EACvB,4BAA4B,KAAK;EACjC,aAAa,KAAK;EAClB,kBAAkB,KAAK;EACvB,QAAQ,KAAK;EACb,gBAAgB,KAAK;EACrB,YAAY,KAAK;EACjB,gBAAgB,KAAK;EACrB,eAAe,KAAK;EACpB,kBAAkB,KAAK;EACvB,iBAAiB,KAAK;EACtB,uBAAuB,KAAK;CAEpB,EAAE,SAAS,KAAK;AAC1B"}
1
+ {"version":3,"file":"api.js","names":[],"sources":["../../src/ui/api.ts"],"sourcesContent":["'use client'\n\nimport type { FailureCode } from '../endpoints/codes'\nimport { CLIENT_FETCH_TIMEOUT_MS } from '../plugin/constants'\n\nexport { errorKey } from '../translations/errorKey'\nexport type { FailureCode }\n\nconst jsonHeaders = { 'Content-Type': 'application/json' }\n\nexport const postImpersonation = async (\n\tpath: string,\n\tbody: unknown\n): Promise<{ error?: FailureCode; ok: boolean; redirect?: string; status: number }> => {\n\ttry {\n\t\tconst response = await fetch(path, {\n\t\t\tbody: JSON.stringify(body ?? {}),\n\t\t\tcredentials: 'include',\n\t\t\theaders: jsonHeaders,\n\t\t\tmethod: 'POST',\n\t\t\tsignal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS),\n\t\t})\n\t\tlet parsed: { error?: FailureCode; redirect?: string } = {}\n\t\ttry {\n\t\t\tparsed = (await response.json()) as { error?: FailureCode; redirect?: string }\n\t\t} catch {\n\t\t\tparsed = {}\n\t\t}\n\t\treturn {\n\t\t\terror: parsed.error,\n\t\t\tok: response.ok,\n\t\t\tredirect: parsed.redirect,\n\t\t\tstatus: response.status,\n\t\t}\n\t} catch {\n\t\treturn { error: 'failed', ok: false, status: 0 }\n\t}\n}\n\n/** Reload when the redirect is the current path; `assign` would be a no-op. */\nexport const goAfterSwitch = (redirect?: string) => {\n\tconst next = redirect ?? '/'\n\tconst path = new URL(next, window.location.origin).pathname\n\tif (path === window.location.pathname) {\n\t\twindow.location.reload()\n\t\treturn\n\t}\n\twindow.location.assign(next)\n}\n"],"mappings":";;;;AAQA,MAAM,cAAc,EAAE,gBAAgB,mBAAmB;AAEzD,MAAa,oBAAoB,OAChC,MACA,SACsF;CACtF,IAAI;EACH,MAAM,WAAW,MAAM,MAAM,MAAM;GAClC,MAAM,KAAK,UAAU,QAAQ,CAAC,CAAC;GAC/B,aAAa;GACb,SAAS;GACT,QAAQ;GACR,QAAQ,YAAY,QAAQ,uBAAuB;EACpD,CAAC;EACD,IAAI,SAAqD,CAAC;EAC1D,IAAI;GACH,SAAU,MAAM,SAAS,KAAK;EAC/B,QAAQ;GACP,SAAS,CAAC;EACX;EACA,OAAO;GACN,OAAO,OAAO;GACd,IAAI,SAAS;GACb,UAAU,OAAO;GACjB,QAAQ,SAAS;EAClB;CACD,QAAQ;EACP,OAAO;GAAE,OAAO;GAAU,IAAI;GAAO,QAAQ;EAAE;CAChD;AACD;;AAGA,MAAa,iBAAiB,aAAsB;CACnD,MAAM,OAAO,YAAY;CAEzB,IADa,IAAI,IAAI,MAAM,OAAO,SAAS,MAAM,EAAE,aACtC,OAAO,SAAS,UAAU;EACtC,OAAO,SAAS,OAAO;EACvB;CACD;CACA,OAAO,SAAS,OAAO,IAAI;AAC5B"}
@@ -0,0 +1,28 @@
1
+ import { fillTemplate } from "../translations/lookup.js";
2
+ //#region src/ui/sessionEnds.ts
3
+ const formatCountdown = (expiresAt, now) => {
4
+ const remaining = new Date(expiresAt).getTime() - now;
5
+ if (!Number.isFinite(remaining) || remaining <= 0) return "0s";
6
+ const totalSeconds = Math.floor(remaining / 1e3);
7
+ const hours = Math.floor(totalSeconds / 3600);
8
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
9
+ const seconds = totalSeconds % 60;
10
+ if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m ${String(seconds).padStart(2, "0")}s`;
11
+ if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
12
+ return `${seconds}s`;
13
+ };
14
+ /**
15
+ * The bar's "session ends" line, or null while `now` is null. The server and the browser read
16
+ * different clocks and time zones, so the bar renders no time until it has mounted, or it would not
17
+ * hydrate.
18
+ */
19
+ const sessionEndsLabel = ({ endsAt, locale, now, templates }) => {
20
+ if (!endsAt || now === null) return null;
21
+ const countdown = formatCountdown(endsAt, now);
22
+ if (countdown === "0s") return null;
23
+ return countdown ? fillTemplate(templates.endsIn, "{{countdown}}", countdown) : fillTemplate(templates.endsAt, "{{time}}", new Date(endsAt).toLocaleTimeString(locale ?? void 0));
24
+ };
25
+ //#endregion
26
+ export { sessionEndsLabel };
27
+
28
+ //# sourceMappingURL=sessionEnds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionEnds.js","names":[],"sources":["../../src/ui/sessionEnds.ts"],"sourcesContent":["import { fillTemplate } from '../translations/lookup'\n\nconst formatCountdown = (expiresAt: string, now: number): string | null => {\n\tconst remaining = new Date(expiresAt).getTime() - now\n\tif (!Number.isFinite(remaining) || remaining <= 0) {\n\t\treturn '0s'\n\t}\n\tconst totalSeconds = Math.floor(remaining / 1000)\n\tconst hours = Math.floor(totalSeconds / 3600)\n\tconst minutes = Math.floor((totalSeconds % 3600) / 60)\n\tconst seconds = totalSeconds % 60\n\tif (hours > 0) {\n\t\treturn `${hours}h ${String(minutes).padStart(2, '0')}m ${String(seconds).padStart(2, '0')}s`\n\t}\n\tif (minutes > 0) {\n\t\treturn `${minutes}m ${String(seconds).padStart(2, '0')}s`\n\t}\n\treturn `${seconds}s`\n}\n\n/**\n * The bar's \"session ends\" line, or null while `now` is null. The server and the browser read\n * different clocks and time zones, so the bar renders no time until it has mounted, or it would not\n * hydrate.\n */\nexport const sessionEndsLabel = ({\n\tendsAt,\n\tlocale,\n\tnow,\n\ttemplates,\n}: {\n\tendsAt?: null | string\n\tlocale?: null | string\n\tnow: null | number\n\ttemplates: { endsAt: string; endsIn: string }\n}): null | string => {\n\tif (!endsAt || now === null) {\n\t\treturn null\n\t}\n\tconst countdown = formatCountdown(endsAt, now)\n\tif (countdown === '0s') {\n\t\treturn null\n\t}\n\treturn countdown\n\t\t? fillTemplate(templates.endsIn, '{{countdown}}', countdown)\n\t\t: fillTemplate(\n\t\t\t\ttemplates.endsAt,\n\t\t\t\t'{{time}}',\n\t\t\t\tnew Date(endsAt).toLocaleTimeString(locale ?? undefined)\n\t\t\t)\n}\n"],"mappings":";;AAEA,MAAM,mBAAmB,WAAmB,QAA+B;CAC1E,MAAM,YAAY,IAAI,KAAK,SAAS,EAAE,QAAQ,IAAI;CAClD,IAAI,CAAC,OAAO,SAAS,SAAS,KAAK,aAAa,GAC/C,OAAO;CAER,MAAM,eAAe,KAAK,MAAM,YAAY,GAAI;CAChD,MAAM,QAAQ,KAAK,MAAM,eAAe,IAAI;CAC5C,MAAM,UAAU,KAAK,MAAO,eAAe,OAAQ,EAAE;CACrD,MAAM,UAAU,eAAe;CAC/B,IAAI,QAAQ,GACX,OAAO,GAAG,MAAM,IAAI,OAAO,OAAO,EAAE,SAAS,GAAG,GAAG,EAAE,IAAI,OAAO,OAAO,EAAE,SAAS,GAAG,GAAG,EAAE;CAE3F,IAAI,UAAU,GACb,OAAO,GAAG,QAAQ,IAAI,OAAO,OAAO,EAAE,SAAS,GAAG,GAAG,EAAE;CAExD,OAAO,GAAG,QAAQ;AACnB;;;;;;AAOA,MAAa,oBAAoB,EAChC,QACA,QACA,KACA,gBAMoB;CACpB,IAAI,CAAC,UAAU,QAAQ,MACtB,OAAO;CAER,MAAM,YAAY,gBAAgB,QAAQ,GAAG;CAC7C,IAAI,cAAc,MACjB,OAAO;CAER,OAAO,YACJ,aAAa,UAAU,QAAQ,iBAAiB,SAAS,IACzD,aACA,UAAU,QACV,YACA,IAAI,KAAK,MAAM,EAAE,mBAAmB,UAAU,KAAA,CAAS,CACxD;AACH"}
@@ -1,4 +1,5 @@
1
1
  "use client";
2
+ import "../translations/errorKey.js";
2
3
  import { postImpersonation } from "./api.js";
3
4
  import { useImpersonationClient } from "./ImpersonationConfig.js";
4
5
  import { useConfig } from "@payloadcms/ui";
@@ -1 +1 @@
1
- {"version":3,"file":"useImpersonation.js","names":[],"sources":["../../src/ui/useImpersonation.ts"],"sourcesContent":["'use client'\n\nimport { useConfig } from '@payloadcms/ui'\nimport type { Where } from 'payload'\n\nimport type { FailureCode } from '../endpoints/codes'\nimport type { ImpersonationStatus } from '../getImpersonation'\nimport { errorKey, goAfterSwitch, postImpersonation } from './api'\nimport { useImpersonationClient } from './ImpersonationConfig'\n\nexport type UseImpersonation = {\n\tapiPath: string\n\tcardEmail: boolean\n\tend: (id: number | string) => Promise<{ error?: FailureCode; ok: boolean; status: number }>\n\texit: () => Promise<{ error?: FailureCode; ok: boolean; status: number }>\n\treasonMode: 'off' | 'optional' | 'required'\n\tsessionCollection: string\n\tsetStatus: (status: ImpersonationStatus) => void\n\tstart: (args: {\n\t\tcollection: string\n\t\tid: number | string\n\t\treason?: string\n\t\tredirect?: string\n\t}) => Promise<{ error?: FailureCode; ok: boolean; redirect?: string; status: number }>\n\tstatus: ImpersonationStatus\n\ttargetFilters: Record<string, true | Where>\n}\n\nconst noopSetStatus = (_status: ImpersonationStatus): void => {}\n\n/**\n * Headless client API. The provider is always registered, so this works from\n * any admin component. Built-in switcher, bar, and document button use it.\n */\nexport const useImpersonation = (): UseImpersonation => {\n\tconst plugin = useImpersonationClient()\n\tconst { config } = useConfig()\n\tconst apiPath = plugin?.apiPath ?? `${config.routes.api}/impersonation`\n\tconst reasonMode = plugin?.reasonMode ?? 'off'\n\tconst sessionCollection = plugin?.sessionCollection ?? 'impersonation-sessions'\n\tconst targetFilters = plugin?.targetFilters ?? {}\n\tconst cardEmail = plugin?.cardEmail ?? true\n\tconst status = plugin?.status ?? { active: false }\n\tconst setStatus = plugin?.setStatus ?? noopSetStatus\n\n\treturn {\n\t\tapiPath,\n\t\tcardEmail,\n\t\tend: (id) => postImpersonation(`${apiPath}/${encodeURIComponent(String(id))}/end`, {}),\n\t\texit: () => postImpersonation(`${apiPath}/exit`, {}),\n\t\treasonMode,\n\t\tsessionCollection,\n\t\tsetStatus,\n\t\tstart: (args) => postImpersonation(`${apiPath}/start`, args),\n\t\tstatus,\n\t\ttargetFilters,\n\t}\n}\n\nexport { errorKey, goAfterSwitch, postImpersonation }\n"],"mappings":";;;;;AA4BA,MAAM,iBAAiB,YAAuC,CAAC;;;;;AAM/D,MAAa,yBAA2C;CACvD,MAAM,SAAS,uBAAuB;CACtC,MAAM,EAAE,WAAW,UAAU;CAC7B,MAAM,UAAU,QAAQ,WAAW,GAAG,OAAO,OAAO,IAAI;CACxD,MAAM,aAAa,QAAQ,cAAc;CACzC,MAAM,oBAAoB,QAAQ,qBAAqB;CACvD,MAAM,gBAAgB,QAAQ,iBAAiB,CAAC;CAChD,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,SAAS,QAAQ,UAAU,EAAE,QAAQ,MAAM;CAGjD,OAAO;EACN;EACA;EACA,MAAM,OAAO,kBAAkB,GAAG,QAAQ,GAAG,mBAAmB,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;EACrF,YAAY,kBAAkB,GAAG,QAAQ,QAAQ,CAAC,CAAC;EACnD;EACA;EACA,WATiB,QAAQ,aAAa;EAUtC,QAAQ,SAAS,kBAAkB,GAAG,QAAQ,SAAS,IAAI;EAC3D;EACA;CACD;AACD"}
1
+ {"version":3,"file":"useImpersonation.js","names":[],"sources":["../../src/ui/useImpersonation.ts"],"sourcesContent":["'use client'\n\nimport { useConfig } from '@payloadcms/ui'\nimport type { Where } from 'payload'\n\nimport type { FailureCode } from '../endpoints/codes'\nimport type { ImpersonationStatus } from '../getImpersonation'\nimport { errorKey, goAfterSwitch, postImpersonation } from './api'\nimport { useImpersonationClient } from './ImpersonationConfig'\n\nexport type UseImpersonation = {\n\tapiPath: string\n\tcardEmail: boolean\n\tend: (id: number | string) => Promise<{ error?: FailureCode; ok: boolean; status: number }>\n\texit: () => Promise<{ error?: FailureCode; ok: boolean; status: number }>\n\treasonMode: 'off' | 'optional' | 'required'\n\tsessionCollection: string\n\tsetStatus: (status: ImpersonationStatus) => void\n\tstart: (args: {\n\t\tcollection: string\n\t\tid: number | string\n\t\treason?: string\n\t\tredirect?: string\n\t}) => Promise<{ error?: FailureCode; ok: boolean; redirect?: string; status: number }>\n\tstatus: ImpersonationStatus\n\ttargetFilters: Record<string, true | Where>\n}\n\nconst noopSetStatus = (_status: ImpersonationStatus): void => {}\n\n/**\n * Headless client API. The provider is always registered, so this works from\n * any admin component. Built-in switcher, bar, and document button use it.\n */\nexport const useImpersonation = (): UseImpersonation => {\n\tconst plugin = useImpersonationClient()\n\tconst { config } = useConfig()\n\tconst apiPath = plugin?.apiPath ?? `${config.routes.api}/impersonation`\n\tconst reasonMode = plugin?.reasonMode ?? 'off'\n\tconst sessionCollection = plugin?.sessionCollection ?? 'impersonation-sessions'\n\tconst targetFilters = plugin?.targetFilters ?? {}\n\tconst cardEmail = plugin?.cardEmail ?? true\n\tconst status = plugin?.status ?? { active: false }\n\tconst setStatus = plugin?.setStatus ?? noopSetStatus\n\n\treturn {\n\t\tapiPath,\n\t\tcardEmail,\n\t\tend: (id) => postImpersonation(`${apiPath}/${encodeURIComponent(String(id))}/end`, {}),\n\t\texit: () => postImpersonation(`${apiPath}/exit`, {}),\n\t\treasonMode,\n\t\tsessionCollection,\n\t\tsetStatus,\n\t\tstart: (args) => postImpersonation(`${apiPath}/start`, args),\n\t\tstatus,\n\t\ttargetFilters,\n\t}\n}\n\nexport { errorKey, goAfterSwitch, postImpersonation }\n"],"mappings":";;;;;;AA4BA,MAAM,iBAAiB,YAAuC,CAAC;;;;;AAM/D,MAAa,yBAA2C;CACvD,MAAM,SAAS,uBAAuB;CACtC,MAAM,EAAE,WAAW,UAAU;CAC7B,MAAM,UAAU,QAAQ,WAAW,GAAG,OAAO,OAAO,IAAI;CACxD,MAAM,aAAa,QAAQ,cAAc;CACzC,MAAM,oBAAoB,QAAQ,qBAAqB;CACvD,MAAM,gBAAgB,QAAQ,iBAAiB,CAAC;CAChD,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,SAAS,QAAQ,UAAU,EAAE,QAAQ,MAAM;CAGjD,OAAO;EACN;EACA;EACA,MAAM,OAAO,kBAAkB,GAAG,QAAQ,GAAG,mBAAmB,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;EACrF,YAAY,kBAAkB,GAAG,QAAQ,QAAQ,CAAC,CAAC;EACnD;EACA;EACA,WATiB,QAAQ,aAAa;EAUtC,QAAQ,SAAS,kBAAkB,GAAG,QAAQ,SAAS,IAAI;EAC3D;EACA;CACD;AACD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@10x-media/impersonation",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-beta.2",
4
4
  "description": "Let an authorised account sign in as another user without their password, then return. Every session is recorded.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -89,11 +89,11 @@
89
89
  "tsdown": "0.22.1",
90
90
  "typescript": "5.9.3",
91
91
  "vitest": "4.1.7",
92
+ "@10x-media/tsconfig": "0.0.0",
92
93
  "@10x-media/tsdown-config": "0.0.0",
93
94
  "@10x-media/dual-session": "0.1.0-beta.2",
94
- "@10x-media/tsconfig": "0.0.0",
95
- "@10x-media/payload-test-harness": "0.0.0",
96
- "@10x-media/vitest-config": "0.0.0"
95
+ "@10x-media/vitest-config": "0.0.0",
96
+ "@10x-media/payload-test-harness": "0.0.0"
97
97
  },
98
98
  "publishConfig": {
99
99
  "access": "public"