@10x-media/impersonation 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/access/filterTargets.js +51 -0
- package/dist/access/filterTargets.js.map +1 -0
- package/dist/auth/cookies.js +49 -0
- package/dist/auth/cookies.js.map +1 -0
- package/dist/auth/expiryCookies.js +59 -0
- package/dist/auth/expiryCookies.js.map +1 -0
- package/dist/auth/issue.js +126 -0
- package/dist/auth/issue.js.map +1 -0
- package/dist/auth/mode.js +69 -0
- package/dist/auth/mode.js.map +1 -0
- package/dist/auth/origin.js +55 -0
- package/dist/auth/origin.js.map +1 -0
- package/dist/auth/revoke.js +24 -0
- package/dist/auth/revoke.js.map +1 -0
- package/dist/auth/tenantCookie.js +74 -0
- package/dist/auth/tenantCookie.js.map +1 -0
- package/dist/endpoints/codes.d.ts +6 -0
- package/dist/endpoints/current.js +66 -0
- package/dist/endpoints/current.js.map +1 -0
- package/dist/endpoints/exit.js +242 -0
- package/dist/endpoints/exit.js.map +1 -0
- package/dist/endpoints/shared.js +76 -0
- package/dist/endpoints/shared.js.map +1 -0
- package/dist/endpoints/start.js +332 -0
- package/dist/endpoints/start.js.map +1 -0
- package/dist/endpoints/terminate.js +73 -0
- package/dist/endpoints/terminate.js.map +1 -0
- package/dist/exports/client.d.ts +9 -0
- package/dist/exports/client.js +10 -0
- package/dist/exports/i18n.d.ts +3 -0
- package/dist/exports/i18n.js +3 -0
- package/dist/exports/rsc.d.ts +4 -0
- package/dist/exports/rsc.js +4 -0
- package/dist/exports/types.d.ts +3 -0
- package/dist/exports/types.js +1 -0
- package/dist/getImpersonation.d.ts +49 -0
- package/dist/getImpersonation.js +78 -0
- package/dist/getImpersonation.js.map +1 -0
- package/dist/ids.js +27 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin/assertTargets.js +23 -0
- package/dist/plugin/assertTargets.js.map +1 -0
- package/dist/plugin/constants.d.ts +6 -0
- package/dist/plugin/constants.js +20 -0
- package/dist/plugin/constants.js.map +1 -0
- package/dist/plugin/decorateAuth.js +99 -0
- package/dist/plugin/decorateAuth.js.map +1 -0
- package/dist/plugin/lookup.js +23 -0
- package/dist/plugin/lookup.js.map +1 -0
- package/dist/plugin/normalizeOptions.js +83 -0
- package/dist/plugin/normalizeOptions.js.map +1 -0
- package/dist/plugin/registerAdmin.js +49 -0
- package/dist/plugin/registerAdmin.js.map +1 -0
- package/dist/plugin/registerCollection.js +183 -0
- package/dist/plugin/registerCollection.js.map +1 -0
- package/dist/plugin/registerEndpoints.js +19 -0
- package/dist/plugin/registerEndpoints.js.map +1 -0
- package/dist/plugin/registerHooks.js +59 -0
- package/dist/plugin/registerHooks.js.map +1 -0
- package/dist/plugin/registerRetention.js +57 -0
- package/dist/plugin/registerRetention.js.map +1 -0
- package/dist/plugin/registerTranslations.js +19 -0
- package/dist/plugin/registerTranslations.js.map +1 -0
- package/dist/plugin/registry.js +11 -0
- package/dist/plugin/registry.js.map +1 -0
- package/dist/plugin/startable.js +21 -0
- package/dist/plugin/startable.js.map +1 -0
- package/dist/session/close.js +65 -0
- package/dist/session/close.js.map +1 -0
- package/dist/session/closeStale.d.ts +11 -0
- package/dist/session/closeStale.js +65 -0
- package/dist/session/closeStale.js.map +1 -0
- package/dist/session/locks.js +32 -0
- package/dist/session/locks.js.map +1 -0
- package/dist/session/resolve.js +91 -0
- package/dist/session/resolve.js.map +1 -0
- package/dist/translations/ar.js +52 -0
- package/dist/translations/ar.js.map +1 -0
- package/dist/translations/de.js +52 -0
- package/dist/translations/de.js.map +1 -0
- package/dist/translations/en.js +52 -0
- package/dist/translations/en.js.map +1 -0
- package/dist/translations/es.js +52 -0
- package/dist/translations/es.js.map +1 -0
- package/dist/translations/fr.js +52 -0
- package/dist/translations/fr.js.map +1 -0
- package/dist/translations/id.js +52 -0
- package/dist/translations/id.js.map +1 -0
- package/dist/translations/index.d.ts +24 -0
- package/dist/translations/index.js +49 -0
- package/dist/translations/index.js.map +1 -0
- package/dist/translations/keys.d.ts +56 -0
- package/dist/translations/keys.js +56 -0
- package/dist/translations/keys.js.map +1 -0
- package/dist/translations/ko.js +52 -0
- package/dist/translations/ko.js.map +1 -0
- package/dist/translations/lookup.js +17 -0
- package/dist/translations/lookup.js.map +1 -0
- package/dist/translations/pt.js +52 -0
- package/dist/translations/pt.js.map +1 -0
- package/dist/translations/ru.js +52 -0
- package/dist/translations/ru.js.map +1 -0
- package/dist/translations/server.js +13 -0
- package/dist/translations/server.js.map +1 -0
- package/dist/translations/uk.js +52 -0
- package/dist/translations/uk.js.map +1 -0
- package/dist/translations/useTranslation.js +12 -0
- package/dist/translations/useTranslation.js.map +1 -0
- package/dist/translations/zh.js +52 -0
- package/dist/translations/zh.js.map +1 -0
- package/dist/types.d.ts +214 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/EndSessionMenuItem.d.ts +5 -0
- package/dist/ui/EndSessionMenuItem.js +45 -0
- package/dist/ui/EndSessionMenuItem.js.map +1 -0
- package/dist/ui/ImpersonateIcon.js +20 -0
- package/dist/ui/ImpersonateIcon.js.map +1 -0
- package/dist/ui/ImpersonationAction.d.ts +10 -0
- package/dist/ui/ImpersonationAction.js +48 -0
- package/dist/ui/ImpersonationAction.js.map +1 -0
- package/dist/ui/ImpersonationBar.d.ts +36 -0
- package/dist/ui/ImpersonationBar.js +357 -0
- package/dist/ui/ImpersonationBar.js.map +1 -0
- package/dist/ui/ImpersonationConfig.js +25 -0
- package/dist/ui/ImpersonationConfig.js.map +1 -0
- package/dist/ui/ImpersonationDocumentButton.d.ts +5 -0
- package/dist/ui/ImpersonationDocumentButton.js +85 -0
- package/dist/ui/ImpersonationDocumentButton.js.map +1 -0
- package/dist/ui/ImpersonationFrontendBanner.d.ts +41 -0
- package/dist/ui/ImpersonationFrontendBanner.js +58 -0
- package/dist/ui/ImpersonationFrontendBanner.js.map +1 -0
- package/dist/ui/ImpersonationFrontendExit.d.ts +17 -0
- package/dist/ui/ImpersonationFrontendExit.js +45 -0
- package/dist/ui/ImpersonationFrontendExit.js.map +1 -0
- package/dist/ui/ImpersonationProvider.d.ts +16 -0
- package/dist/ui/ImpersonationProvider.js +127 -0
- package/dist/ui/ImpersonationProvider.js.map +1 -0
- package/dist/ui/ImpersonationSwitcher.d.ts +21 -0
- package/dist/ui/ImpersonationSwitcher.js +251 -0
- package/dist/ui/ImpersonationSwitcher.js.map +1 -0
- package/dist/ui/ImpersonationUserCard.d.ts +31 -0
- package/dist/ui/ImpersonationUserCard.js +51 -0
- package/dist/ui/ImpersonationUserCard.js.map +1 -0
- package/dist/ui/StartConfirmModal.d.ts +28 -0
- package/dist/ui/StartConfirmModal.js +65 -0
- package/dist/ui/StartConfirmModal.js.map +1 -0
- package/dist/ui/api.js +66 -0
- package/dist/ui/api.js.map +1 -0
- package/dist/ui/barPlacement.js +107 -0
- package/dist/ui/barPlacement.js.map +1 -0
- package/dist/ui/frontendBanner.css +22 -0
- package/dist/ui/impersonation.css +244 -0
- package/dist/ui/useImpersonation.d.ts +43 -0
- package/dist/ui/useImpersonation.js +36 -0
- package/dist/ui/useImpersonation.js.map +1 -0
- package/package.json +126 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { collectionBySlug } from "../ids.js";
|
|
2
|
+
import { cachedFindOpenBySid } 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 { readHintCookie } from "../plugin/lookup.js";
|
|
7
|
+
import { isStartableAuthCollection } from "../plugin/startable.js";
|
|
8
|
+
import { ImpersonationSwitcher } from "./ImpersonationSwitcher.js";
|
|
9
|
+
import { getFromImportMap } from "payload/shared";
|
|
10
|
+
import { jsx } from "react/jsx-runtime";
|
|
11
|
+
//#region src/ui/ImpersonationAction.tsx
|
|
12
|
+
const ImpersonationAction = async ({ payload, user }) => {
|
|
13
|
+
const options = getRegistry(payload.config);
|
|
14
|
+
if (!options?.ui.headerAction || !user) return null;
|
|
15
|
+
if (user.collection !== payload.config.admin.user) return null;
|
|
16
|
+
if (asAuthUser(user)._impersonation) return null;
|
|
17
|
+
const sid = boundSid(user, options.session.binding);
|
|
18
|
+
if (!sid) return null;
|
|
19
|
+
const hint = await readHintCookie(options.hintCookieName);
|
|
20
|
+
if (Boolean(options.session.issue) || Boolean(hint) || sid.startsWith("imp_")) {
|
|
21
|
+
if (await cachedFindOpenBySid(sid, options, payload)) return null;
|
|
22
|
+
}
|
|
23
|
+
const collections = payload.config.collections.flatMap((collection) => {
|
|
24
|
+
if (!isStartableAuthCollection(collection, options)) return [];
|
|
25
|
+
const plural = collectionBySlug(payload, collection.slug)?.config.labels?.plural;
|
|
26
|
+
return [{
|
|
27
|
+
label: plural ? String(plural) : collection.slug,
|
|
28
|
+
slug: collection.slug,
|
|
29
|
+
useAsTitle: collection.admin?.useAsTitle ?? "email"
|
|
30
|
+
}];
|
|
31
|
+
});
|
|
32
|
+
if (collections.length === 0) return null;
|
|
33
|
+
const Card = options.ui.card ? getFromImportMap({
|
|
34
|
+
importMap: payload.importMap,
|
|
35
|
+
PayloadComponent: options.ui.card,
|
|
36
|
+
silent: true
|
|
37
|
+
}) : void 0;
|
|
38
|
+
if (options.ui.card && !Card) payload.logger.warn("@10x-media/impersonation: ui.card is not in the admin import map. Run generate:importmap. The default card is shown until then.");
|
|
39
|
+
return /* @__PURE__ */ jsx(ImpersonationSwitcher, {
|
|
40
|
+
Card,
|
|
41
|
+
collections,
|
|
42
|
+
viewerId: user.id
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
46
|
+
export { ImpersonationAction };
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=ImpersonationAction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImpersonationAction.js","names":[],"sources":["../../src/ui/ImpersonationAction.tsx"],"sourcesContent":["import type { ServerProps } from 'payload'\nimport { getFromImportMap } from 'payload/shared'\nimport type { ComponentType } from 'react'\n\nimport { collectionBySlug } from '../ids'\nimport { IMPERSONATION_SID_PREFIX } from '../plugin/constants'\nimport { readHintCookie } from '../plugin/lookup'\nimport { getRegistry } from '../plugin/registry'\nimport { isStartableAuthCollection } from '../plugin/startable'\nimport { cachedFindOpenBySid } from '../session/resolve'\nimport { asAuthUser, boundSid } from '../types'\nimport { ImpersonationSwitcher } from './ImpersonationSwitcher'\nimport type { ImpersonationUserCardProps } from './ImpersonationUserCard'\n\nexport const ImpersonationAction = async ({ payload, user }: ServerProps) => {\n\tconst options = getRegistry(payload.config)\n\tif (!options?.ui.headerAction || !user) {\n\t\treturn null\n\t}\n\n\tif (user.collection !== payload.config.admin.user) {\n\t\treturn null\n\t}\n\n\tif (asAuthUser(user)._impersonation) {\n\t\treturn null\n\t}\n\n\tconst sid = boundSid(user, options.session.binding)\n\tif (!sid) {\n\t\treturn null\n\t}\n\n\tconst hint = await readHintCookie(options.hintCookieName)\n\tconst shouldLookup =\n\t\tBoolean(options.session.issue) || Boolean(hint) || sid.startsWith(IMPERSONATION_SID_PREFIX)\n\tif (shouldLookup) {\n\t\tconst active = await cachedFindOpenBySid(sid, options, payload)\n\t\tif (active) {\n\t\t\treturn null\n\t\t}\n\t}\n\n\tconst collections = payload.config.collections.flatMap((collection) => {\n\t\tif (!isStartableAuthCollection(collection, options)) {\n\t\t\treturn []\n\t\t}\n\t\tconst registered = collectionBySlug(payload, collection.slug)\n\t\tconst plural = registered?.config.labels?.plural\n\t\treturn [\n\t\t\t{\n\t\t\t\tlabel: plural ? String(plural) : collection.slug,\n\t\t\t\tslug: collection.slug,\n\t\t\t\tuseAsTitle: collection.admin?.useAsTitle ?? 'email',\n\t\t\t},\n\t\t]\n\t})\n\n\tif (collections.length === 0) {\n\t\treturn null\n\t}\n\n\tconst Card = options.ui.card\n\t\t? getFromImportMap<ComponentType<ImpersonationUserCardProps>>({\n\t\t\t\timportMap: payload.importMap,\n\t\t\t\tPayloadComponent: options.ui.card,\n\t\t\t\tsilent: true,\n\t\t\t})\n\t\t: undefined\n\tif (options.ui.card && !Card) {\n\t\tpayload.logger.warn(\n\t\t\t'@10x-media/impersonation: ui.card is not in the admin import map. Run generate:importmap. The default card is shown until then.'\n\t\t)\n\t}\n\n\treturn <ImpersonationSwitcher Card={Card} collections={collections} viewerId={user.id} />\n}\n"],"mappings":";;;;;;;;;;;AAcA,MAAa,sBAAsB,OAAO,EAAE,SAAS,WAAwB;CAC5E,MAAM,UAAU,YAAY,QAAQ,MAAM;CAC1C,IAAI,CAAC,SAAS,GAAG,gBAAgB,CAAC,MACjC,OAAO;CAGR,IAAI,KAAK,eAAe,QAAQ,OAAO,MAAM,MAC5C,OAAO;CAGR,IAAI,WAAW,IAAI,EAAE,gBACpB,OAAO;CAGR,MAAM,MAAM,SAAS,MAAM,QAAQ,QAAQ,OAAO;CAClD,IAAI,CAAC,KACJ,OAAO;CAGR,MAAM,OAAO,MAAM,eAAe,QAAQ,cAAc;CAGxD,IADC,QAAQ,QAAQ,QAAQ,KAAK,KAAK,QAAQ,IAAI,KAAK,IAAI,WAAA,MAAmC;MAGtF,MADiB,oBAAoB,KAAK,SAAS,OAAO,GAE7D,OAAO;CAAA;CAIT,MAAM,cAAc,QAAQ,OAAO,YAAY,SAAS,eAAe;EACtE,IAAI,CAAC,0BAA0B,YAAY,OAAO,GACjD,OAAO,CAAC;EAGT,MAAM,SADa,iBAAiB,SAAS,WAAW,IAChC,GAAG,OAAO,QAAQ;EAC1C,OAAO,CACN;GACC,OAAO,SAAS,OAAO,MAAM,IAAI,WAAW;GAC5C,MAAM,WAAW;GACjB,YAAY,WAAW,OAAO,cAAc;EAC7C,CACD;CACD,CAAC;CAED,IAAI,YAAY,WAAW,GAC1B,OAAO;CAGR,MAAM,OAAO,QAAQ,GAAG,OACrB,iBAA4D;EAC5D,WAAW,QAAQ;EACnB,kBAAkB,QAAQ,GAAG;EAC7B,QAAQ;CACT,CAAC,IACA,KAAA;CACH,IAAI,QAAQ,GAAG,QAAQ,CAAC,MACvB,QAAQ,OAAO,KACd,iIACD;CAGD,OAAO,oBAAC,uBAAD;EAA6B;EAAmB;EAAa,UAAU,KAAK;CAAK,CAAA;AACzF"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/ui/ImpersonationBar.d.ts
|
|
2
|
+
type ImpersonationBarProps = {
|
|
3
|
+
actingAs: string;
|
|
4
|
+
adminRoute: string;
|
|
5
|
+
apiPath: string;
|
|
6
|
+
frontendUrl?: string;
|
|
7
|
+
impersonatorLocale?: null | string;
|
|
8
|
+
openWebsite: string;
|
|
9
|
+
pluginName: string;
|
|
10
|
+
returnTo: string;
|
|
11
|
+
sessionEndsAt?: null | string;
|
|
12
|
+
sessionEndsAtTemplate: string;
|
|
13
|
+
sessionEndsInTemplate: string;
|
|
14
|
+
showFrontendLink?: boolean; /** Collapsed chip label. */
|
|
15
|
+
targetName: string;
|
|
16
|
+
tone?: 'quiet' | 'warning';
|
|
17
|
+
};
|
|
18
|
+
declare const ImpersonationBar: ({
|
|
19
|
+
actingAs,
|
|
20
|
+
adminRoute,
|
|
21
|
+
apiPath,
|
|
22
|
+
frontendUrl,
|
|
23
|
+
impersonatorLocale,
|
|
24
|
+
openWebsite,
|
|
25
|
+
pluginName,
|
|
26
|
+
returnTo,
|
|
27
|
+
sessionEndsAt,
|
|
28
|
+
sessionEndsAtTemplate,
|
|
29
|
+
sessionEndsInTemplate,
|
|
30
|
+
showFrontendLink,
|
|
31
|
+
targetName,
|
|
32
|
+
tone
|
|
33
|
+
}: ImpersonationBarProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { ImpersonationBar };
|
|
36
|
+
//# sourceMappingURL=ImpersonationBar.d.ts.map
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { CLIENT_FETCH_TIMEOUT_MS } from "../plugin/constants.js";
|
|
3
|
+
import { keys } from "../translations/keys.js";
|
|
4
|
+
import { errorKey, goAfterSwitch, postImpersonation } from "./api.js";
|
|
5
|
+
import { useImpersonation } from "./useImpersonation.js";
|
|
6
|
+
import { fillTemplate, messageFor } from "../translations/lookup.js";
|
|
7
|
+
import { anchorsFor, nearestEdge, projectThrow, readBarPlacement, readViewport, writeBarPlacement } from "./barPlacement.js";
|
|
8
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
9
|
+
import { Button, Pill, toast } from "@payloadcms/ui";
|
|
10
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
import { usePathname } from "next/navigation";
|
|
12
|
+
import "./impersonation.css";
|
|
13
|
+
//#region src/ui/ImpersonationBar.tsx
|
|
14
|
+
const SESSION_GONE = new Set([
|
|
15
|
+
"forbidden",
|
|
16
|
+
"impersonatorGone",
|
|
17
|
+
"impersonatorSessionExpired",
|
|
18
|
+
"notImpersonating"
|
|
19
|
+
]);
|
|
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" }) => {
|
|
32
|
+
const { setStatus } = useImpersonation();
|
|
33
|
+
const [mounted, setMounted] = useState(false);
|
|
34
|
+
const onLogin = (usePathname() ?? "") === `${adminRoute}/login`;
|
|
35
|
+
const [busy, setBusy] = useState(false);
|
|
36
|
+
const [visible, setVisible] = useState(true);
|
|
37
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
38
|
+
const [edge, setEdge] = useState("right");
|
|
39
|
+
const [viewport, setViewport] = useState(() => typeof document === "undefined" ? {
|
|
40
|
+
height: 800,
|
|
41
|
+
width: 1200
|
|
42
|
+
} : readViewport());
|
|
43
|
+
const [dragPoint, setDragPoint] = useState(null);
|
|
44
|
+
const [now, setNow] = useState(() => Date.now());
|
|
45
|
+
const chipRef = useRef(null);
|
|
46
|
+
const expiredProbe = useRef(false);
|
|
47
|
+
const dragOffset = useRef(null);
|
|
48
|
+
const dragPointRef = useRef(null);
|
|
49
|
+
const dragOrigin = useRef(null);
|
|
50
|
+
const dragged = useRef(false);
|
|
51
|
+
const lastSample = useRef(null);
|
|
52
|
+
const velocity = useRef({
|
|
53
|
+
x: 0,
|
|
54
|
+
y: 0
|
|
55
|
+
});
|
|
56
|
+
const endChipDragRef = useRef(() => {});
|
|
57
|
+
const onWindowPointerEnd = useRef(() => {
|
|
58
|
+
endChipDragRef.current();
|
|
59
|
+
});
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const stored = readBarPlacement();
|
|
62
|
+
setCollapsed(stored.collapsed);
|
|
63
|
+
setEdge(stored.edge);
|
|
64
|
+
setMounted(true);
|
|
65
|
+
}, []);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!collapsed) return;
|
|
68
|
+
const update = () => setViewport(readViewport());
|
|
69
|
+
update();
|
|
70
|
+
window.addEventListener("resize", update);
|
|
71
|
+
return () => window.removeEventListener("resize", update);
|
|
72
|
+
}, [collapsed]);
|
|
73
|
+
const probe = useCallback(async () => {
|
|
74
|
+
try {
|
|
75
|
+
const body = await (await fetch(apiPath, {
|
|
76
|
+
credentials: "include",
|
|
77
|
+
signal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS)
|
|
78
|
+
})).json();
|
|
79
|
+
if (typeof body.active !== "boolean") return;
|
|
80
|
+
setStatus(body.active ? body : { active: false });
|
|
81
|
+
setVisible(body.active);
|
|
82
|
+
} catch {}
|
|
83
|
+
}, [apiPath, setStatus]);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (onLogin) {
|
|
86
|
+
setVisible(false);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
probe();
|
|
90
|
+
}, [onLogin, probe]);
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!sessionEndsAt || !visible || onLogin) return;
|
|
93
|
+
expiredProbe.current = false;
|
|
94
|
+
const timer = window.setInterval(() => {
|
|
95
|
+
const next = Date.now();
|
|
96
|
+
setNow(next);
|
|
97
|
+
if (expiredProbe.current) return;
|
|
98
|
+
if (new Date(sessionEndsAt).getTime() - next <= 0) {
|
|
99
|
+
expiredProbe.current = true;
|
|
100
|
+
probe();
|
|
101
|
+
}
|
|
102
|
+
}, 1e3);
|
|
103
|
+
return () => window.clearInterval(timer);
|
|
104
|
+
}, [
|
|
105
|
+
onLogin,
|
|
106
|
+
probe,
|
|
107
|
+
sessionEndsAt,
|
|
108
|
+
visible
|
|
109
|
+
]);
|
|
110
|
+
const onExit = async () => {
|
|
111
|
+
if (busy) return;
|
|
112
|
+
setBusy(true);
|
|
113
|
+
try {
|
|
114
|
+
const result = await postImpersonation(`${apiPath}/exit`, {});
|
|
115
|
+
if (!result.ok) {
|
|
116
|
+
toast.error(messageFor(impersonatorLocale, errorKey(result.error ?? "failed")));
|
|
117
|
+
if (result.error && SESSION_GONE.has(result.error)) {
|
|
118
|
+
setStatus({ active: false });
|
|
119
|
+
setVisible(false);
|
|
120
|
+
goAfterSwitch(adminRoute);
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
goAfterSwitch(adminRoute);
|
|
125
|
+
} finally {
|
|
126
|
+
setBusy(false);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const collapse = () => {
|
|
130
|
+
const next = {
|
|
131
|
+
collapsed: true,
|
|
132
|
+
edge
|
|
133
|
+
};
|
|
134
|
+
setCollapsed(true);
|
|
135
|
+
writeBarPlacement(next);
|
|
136
|
+
};
|
|
137
|
+
const expand = () => {
|
|
138
|
+
const next = {
|
|
139
|
+
collapsed: false,
|
|
140
|
+
edge
|
|
141
|
+
};
|
|
142
|
+
setCollapsed(false);
|
|
143
|
+
setDragPoint(null);
|
|
144
|
+
writeBarPlacement(next);
|
|
145
|
+
};
|
|
146
|
+
const endChipDrag = () => {
|
|
147
|
+
if (!dragOffset.current && !dragOrigin.current) return;
|
|
148
|
+
window.removeEventListener("pointerup", onWindowPointerEnd.current);
|
|
149
|
+
window.removeEventListener("pointercancel", onWindowPointerEnd.current);
|
|
150
|
+
const point = dragPointRef.current;
|
|
151
|
+
const didDrag = dragged.current;
|
|
152
|
+
dragOffset.current = null;
|
|
153
|
+
dragOrigin.current = null;
|
|
154
|
+
dragPointRef.current = null;
|
|
155
|
+
if (!didDrag) {
|
|
156
|
+
expand();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const rect = chipRef.current?.getBoundingClientRect();
|
|
160
|
+
if (!point || !rect) {
|
|
161
|
+
setDragPoint(null);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const header = Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--app-header-height"));
|
|
165
|
+
const anchors = anchorsFor({
|
|
166
|
+
header: Number.isFinite(header) ? header : 48,
|
|
167
|
+
size: {
|
|
168
|
+
height: rect.height,
|
|
169
|
+
width: rect.width
|
|
170
|
+
},
|
|
171
|
+
viewport: readViewport()
|
|
172
|
+
});
|
|
173
|
+
const nextEdge = nearestEdge(projectThrow(point, velocity.current), anchors);
|
|
174
|
+
setEdge(nextEdge);
|
|
175
|
+
setDragPoint(null);
|
|
176
|
+
writeBarPlacement({
|
|
177
|
+
collapsed: true,
|
|
178
|
+
edge: nextEdge
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
endChipDragRef.current = endChipDrag;
|
|
182
|
+
const onChipPointerDown = (event) => {
|
|
183
|
+
if (event.button !== 0) return;
|
|
184
|
+
const rect = chipRef.current?.getBoundingClientRect();
|
|
185
|
+
if (!rect) return;
|
|
186
|
+
dragged.current = false;
|
|
187
|
+
dragOrigin.current = {
|
|
188
|
+
x: event.clientX,
|
|
189
|
+
y: event.clientY
|
|
190
|
+
};
|
|
191
|
+
dragOffset.current = {
|
|
192
|
+
x: event.clientX - rect.left,
|
|
193
|
+
y: event.clientY - rect.top
|
|
194
|
+
};
|
|
195
|
+
lastSample.current = {
|
|
196
|
+
t: event.timeStamp,
|
|
197
|
+
x: event.clientX,
|
|
198
|
+
y: event.clientY
|
|
199
|
+
};
|
|
200
|
+
velocity.current = {
|
|
201
|
+
x: 0,
|
|
202
|
+
y: 0
|
|
203
|
+
};
|
|
204
|
+
window.addEventListener("pointerup", onWindowPointerEnd.current);
|
|
205
|
+
window.addEventListener("pointercancel", onWindowPointerEnd.current);
|
|
206
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
207
|
+
};
|
|
208
|
+
const onChipPointerMove = (event) => {
|
|
209
|
+
if (!dragOffset.current || !dragOrigin.current) return;
|
|
210
|
+
if (event.pointerType !== "touch" && event.buttons === 0) {
|
|
211
|
+
endChipDrag();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const travel = Math.hypot(event.clientX - dragOrigin.current.x, event.clientY - dragOrigin.current.y);
|
|
215
|
+
if (!dragged.current && travel < 4) return;
|
|
216
|
+
dragged.current = true;
|
|
217
|
+
const next = {
|
|
218
|
+
x: event.clientX - dragOffset.current.x,
|
|
219
|
+
y: event.clientY - dragOffset.current.y
|
|
220
|
+
};
|
|
221
|
+
const previous = lastSample.current;
|
|
222
|
+
const dt = previous ? event.timeStamp - previous.t : 0;
|
|
223
|
+
if (previous && dt > 0) velocity.current = {
|
|
224
|
+
x: (event.clientX - previous.x) / dt,
|
|
225
|
+
y: (event.clientY - previous.y) / dt
|
|
226
|
+
};
|
|
227
|
+
lastSample.current = {
|
|
228
|
+
t: event.timeStamp,
|
|
229
|
+
x: event.clientX,
|
|
230
|
+
y: event.clientY
|
|
231
|
+
};
|
|
232
|
+
dragPointRef.current = next;
|
|
233
|
+
setDragPoint(next);
|
|
234
|
+
};
|
|
235
|
+
if (!visible || onLogin) return null;
|
|
236
|
+
if (collapsed) {
|
|
237
|
+
const rect = chipRef.current?.getBoundingClientRect();
|
|
238
|
+
const header = mounted ? Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--app-header-height")) : 48;
|
|
239
|
+
const anchors = anchorsFor({
|
|
240
|
+
header: Number.isFinite(header) ? header : 48,
|
|
241
|
+
size: {
|
|
242
|
+
height: rect?.height ?? 36,
|
|
243
|
+
width: rect?.width ?? 168
|
|
244
|
+
},
|
|
245
|
+
viewport
|
|
246
|
+
});
|
|
247
|
+
const point = dragPoint ?? anchors[edge];
|
|
248
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
249
|
+
"aria-label": actingAs,
|
|
250
|
+
"aria-live": "polite",
|
|
251
|
+
className: [
|
|
252
|
+
"impersonation-chip",
|
|
253
|
+
tone === "quiet" ? "impersonation-chip--quiet" : null,
|
|
254
|
+
dragPoint ? "impersonation-chip--dragging" : null
|
|
255
|
+
].filter(Boolean).join(" "),
|
|
256
|
+
onPointerCancel: endChipDrag,
|
|
257
|
+
onPointerDown: onChipPointerDown,
|
|
258
|
+
onPointerMove: onChipPointerMove,
|
|
259
|
+
onPointerUp: endChipDrag,
|
|
260
|
+
ref: chipRef,
|
|
261
|
+
role: "status",
|
|
262
|
+
style: {
|
|
263
|
+
left: point.x,
|
|
264
|
+
top: point.y
|
|
265
|
+
},
|
|
266
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
267
|
+
className: "impersonation-chip__name",
|
|
268
|
+
children: targetName
|
|
269
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
270
|
+
"aria-label": messageFor(impersonatorLocale, keys.expandBar),
|
|
271
|
+
className: "impersonation-bar__icon",
|
|
272
|
+
onClick: (event) => {
|
|
273
|
+
if (dragged.current) {
|
|
274
|
+
event.preventDefault();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
expand();
|
|
278
|
+
},
|
|
279
|
+
type: "button",
|
|
280
|
+
children: /* @__PURE__ */ jsx(ExpandIcon, {})
|
|
281
|
+
})]
|
|
282
|
+
});
|
|
283
|
+
}
|
|
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;
|
|
286
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
287
|
+
"aria-live": "polite",
|
|
288
|
+
className: tone === "quiet" ? "impersonation-bar impersonation-bar--quiet" : "impersonation-bar",
|
|
289
|
+
role: "status",
|
|
290
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
291
|
+
className: "impersonation-bar__meta",
|
|
292
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
293
|
+
className: "impersonation-bar__copy",
|
|
294
|
+
children: [/* @__PURE__ */ jsx(Pill, {
|
|
295
|
+
pillStyle: tone === "quiet" ? "success" : "warning",
|
|
296
|
+
size: "small",
|
|
297
|
+
children: pluginName
|
|
298
|
+
}), /* @__PURE__ */ jsx("span", { children: actingAs })]
|
|
299
|
+
}), endsLabel ? /* @__PURE__ */ jsx("div", {
|
|
300
|
+
className: "impersonation-bar__time",
|
|
301
|
+
children: endsLabel
|
|
302
|
+
}) : null]
|
|
303
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
304
|
+
className: "impersonation-bar__actions",
|
|
305
|
+
children: [
|
|
306
|
+
showFrontendLink && frontendUrl ? /* @__PURE__ */ jsx("a", {
|
|
307
|
+
className: "impersonation-bar__link",
|
|
308
|
+
href: frontendUrl,
|
|
309
|
+
children: openWebsite
|
|
310
|
+
}) : null,
|
|
311
|
+
/* @__PURE__ */ jsx(Button, {
|
|
312
|
+
buttonStyle: "pill",
|
|
313
|
+
disabled: busy,
|
|
314
|
+
margin: false,
|
|
315
|
+
onClick: () => void onExit(),
|
|
316
|
+
size: "small",
|
|
317
|
+
children: returnTo
|
|
318
|
+
}),
|
|
319
|
+
/* @__PURE__ */ jsx("button", {
|
|
320
|
+
"aria-label": messageFor(impersonatorLocale, keys.collapseBar),
|
|
321
|
+
className: "impersonation-bar__icon",
|
|
322
|
+
onClick: collapse,
|
|
323
|
+
type: "button",
|
|
324
|
+
children: /* @__PURE__ */ jsx(CollapseIcon, {})
|
|
325
|
+
})
|
|
326
|
+
]
|
|
327
|
+
})]
|
|
328
|
+
});
|
|
329
|
+
};
|
|
330
|
+
const CollapseIcon = () => /* @__PURE__ */ jsx("svg", {
|
|
331
|
+
"aria-hidden": "true",
|
|
332
|
+
fill: "none",
|
|
333
|
+
height: "16",
|
|
334
|
+
viewBox: "0 0 16 16",
|
|
335
|
+
width: "16",
|
|
336
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
337
|
+
d: "M3.5 6 8 10.5 12.5 6",
|
|
338
|
+
stroke: "currentColor",
|
|
339
|
+
strokeWidth: "1.5"
|
|
340
|
+
})
|
|
341
|
+
});
|
|
342
|
+
const ExpandIcon = () => /* @__PURE__ */ jsx("svg", {
|
|
343
|
+
"aria-hidden": "true",
|
|
344
|
+
fill: "none",
|
|
345
|
+
height: "16",
|
|
346
|
+
viewBox: "0 0 16 16",
|
|
347
|
+
width: "16",
|
|
348
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
349
|
+
d: "M3 6h10M3 10h10",
|
|
350
|
+
stroke: "currentColor",
|
|
351
|
+
strokeWidth: "1.5"
|
|
352
|
+
})
|
|
353
|
+
});
|
|
354
|
+
//#endregion
|
|
355
|
+
export { ImpersonationBar };
|
|
356
|
+
|
|
357
|
+
//# sourceMappingURL=ImpersonationBar.js.map
|
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
//#region src/ui/ImpersonationConfig.tsx
|
|
5
|
+
const Context = createContext(null);
|
|
6
|
+
const ImpersonationClientConfig = ({ children, value }) => {
|
|
7
|
+
const [status, setStatus] = useState(value.status);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
setStatus(value.status);
|
|
10
|
+
}, [value.status]);
|
|
11
|
+
const live = useMemo(() => ({
|
|
12
|
+
...value,
|
|
13
|
+
setStatus,
|
|
14
|
+
status
|
|
15
|
+
}), [status, value]);
|
|
16
|
+
return /* @__PURE__ */ jsx(Context.Provider, {
|
|
17
|
+
value: live,
|
|
18
|
+
children
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
const useImpersonationClient = () => useContext(Context);
|
|
22
|
+
//#endregion
|
|
23
|
+
export { ImpersonationClientConfig, useImpersonationClient };
|
|
24
|
+
|
|
25
|
+
//# sourceMappingURL=ImpersonationConfig.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImpersonationConfig.js","names":[],"sources":["../../src/ui/ImpersonationConfig.tsx"],"sourcesContent":["'use client'\n\nimport type { Where } from 'payload'\nimport { createContext, type ReactNode, useContext, useEffect, useMemo, useState } from 'react'\n\nimport type { ImpersonationStatus } from '../getImpersonation'\n\nexport type TargetFilterMap = Record<string, true | Where>\n\nexport type ImpersonationClientOptions = {\n\tapiPath: string\n\tcardEmail: boolean\n\treasonMode: 'off' | 'optional' | 'required'\n\tsessionCollection: string\n\tsetStatus: (status: ImpersonationStatus) => void\n\tstatus: ImpersonationStatus\n\ttargetFilters: TargetFilterMap\n}\n\ntype ServerClientOptions = Omit<ImpersonationClientOptions, 'setStatus'>\n\nconst Context = createContext<ImpersonationClientOptions | null>(null)\n\nexport const ImpersonationClientConfig = ({\n\tchildren,\n\tvalue,\n}: {\n\tchildren: ReactNode\n\tvalue: ServerClientOptions\n}) => {\n\tconst [status, setStatus] = useState(value.status)\n\tuseEffect(() => {\n\t\tsetStatus(value.status)\n\t}, [value.status])\n\tconst live = useMemo(() => ({ ...value, setStatus, status }), [status, value])\n\treturn <Context.Provider value={live}>{children}</Context.Provider>\n}\n\nexport const useImpersonationClient = () => useContext(Context)\n"],"mappings":";;;;AAqBA,MAAM,UAAU,cAAiD,IAAI;AAErE,MAAa,6BAA6B,EACzC,UACA,YAIK;CACL,MAAM,CAAC,QAAQ,aAAa,SAAS,MAAM,MAAM;CACjD,gBAAgB;EACf,UAAU,MAAM,MAAM;CACvB,GAAG,CAAC,MAAM,MAAM,CAAC;CACjB,MAAM,OAAO,eAAe;EAAE,GAAG;EAAO;EAAW;CAAO,IAAI,CAAC,QAAQ,KAAK,CAAC;CAC7E,OAAO,oBAAC,QAAQ,UAAT;EAAkB,OAAO;EAAO;CAA2B,CAAA;AACnE;AAEA,MAAa,+BAA+B,WAAW,OAAO"}
|
|
@@ -0,0 +1,85 @@
|
|
|
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 { useEffect, useMemo, useState } from "react";
|
|
8
|
+
import { Button, useAuth, useConfig, useDocumentInfo, useDocumentTitle, useModal } from "@payloadcms/ui";
|
|
9
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
//#region src/ui/ImpersonationDocumentButton.tsx
|
|
11
|
+
const CONFIRM_SLUG = "impersonation-confirm-document";
|
|
12
|
+
const ImpersonationDocumentButton = () => {
|
|
13
|
+
const { collectionSlug, data, id } = useDocumentInfo();
|
|
14
|
+
const { title } = useDocumentTitle();
|
|
15
|
+
const { user } = useAuth();
|
|
16
|
+
const { config } = useConfig();
|
|
17
|
+
const { t } = useTranslation$1();
|
|
18
|
+
const { openModal } = useModal();
|
|
19
|
+
const { apiPath, reasonMode, status, targetFilters } = useImpersonation();
|
|
20
|
+
const filter = collectionSlug ? targetFilters[collectionSlug] : void 0;
|
|
21
|
+
const [matchesFilter, setMatchesFilter] = useState(filter === true);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!collectionSlug || id === void 0 || filter === void 0 || filter === true) {
|
|
24
|
+
setMatchesFilter(filter === true);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
const params = new URLSearchParams({
|
|
29
|
+
depth: "0",
|
|
30
|
+
limit: "1",
|
|
31
|
+
where: JSON.stringify({ and: [{ id: { equals: id } }, filter] })
|
|
32
|
+
});
|
|
33
|
+
fetch(`${config.routes.api}/${collectionSlug}?${params.toString()}`, {
|
|
34
|
+
credentials: "include",
|
|
35
|
+
signal: controller.signal
|
|
36
|
+
}).then(async (response) => {
|
|
37
|
+
if (!response.ok) return { docs: [] };
|
|
38
|
+
return await response.json();
|
|
39
|
+
}).then((body) => {
|
|
40
|
+
if (!controller.signal.aborted) setMatchesFilter((body.docs?.length ?? 0) > 0);
|
|
41
|
+
}).catch(() => {
|
|
42
|
+
if (!controller.signal.aborted) setMatchesFilter(false);
|
|
43
|
+
});
|
|
44
|
+
return () => controller.abort();
|
|
45
|
+
}, [
|
|
46
|
+
collectionSlug,
|
|
47
|
+
config.routes.api,
|
|
48
|
+
filter,
|
|
49
|
+
id
|
|
50
|
+
]);
|
|
51
|
+
const target = useMemo(() => {
|
|
52
|
+
if (!collectionSlug || id === void 0 || filter === void 0) return null;
|
|
53
|
+
return {
|
|
54
|
+
collection: collectionSlug,
|
|
55
|
+
id,
|
|
56
|
+
label: (data && typeof data === "object" ? String(data.name ?? data.email ?? "") : "") || title || String(id)
|
|
57
|
+
};
|
|
58
|
+
}, [
|
|
59
|
+
collectionSlug,
|
|
60
|
+
data,
|
|
61
|
+
filter,
|
|
62
|
+
id,
|
|
63
|
+
title
|
|
64
|
+
]);
|
|
65
|
+
const self = user && collectionSlug === user.collection && id !== void 0 && String(user.id) === String(id);
|
|
66
|
+
if (!target || !matchesFilter || status.active || self) return null;
|
|
67
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Button, {
|
|
68
|
+
buttonStyle: "subtle",
|
|
69
|
+
className: "impersonation-document-button",
|
|
70
|
+
icon: /* @__PURE__ */ jsx(ImpersonateIcon, {}),
|
|
71
|
+
iconPosition: "left",
|
|
72
|
+
margin: false,
|
|
73
|
+
onClick: () => openModal(CONFIRM_SLUG),
|
|
74
|
+
children: t(keys.switchToUser)
|
|
75
|
+
}), /* @__PURE__ */ jsx(StartConfirmModal, {
|
|
76
|
+
apiPath,
|
|
77
|
+
modalSlug: CONFIRM_SLUG,
|
|
78
|
+
reasonMode,
|
|
79
|
+
target
|
|
80
|
+
}, `${target.collection}:${target.id}`)] });
|
|
81
|
+
};
|
|
82
|
+
//#endregion
|
|
83
|
+
export { ImpersonationDocumentButton };
|
|
84
|
+
|
|
85
|
+
//# sourceMappingURL=ImpersonationDocumentButton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImpersonationDocumentButton.js","names":["useTranslation"],"sources":["../../src/ui/ImpersonationDocumentButton.tsx"],"sourcesContent":["'use client'\n\nimport {\n\tButton,\n\tuseAuth,\n\tuseConfig,\n\tuseDocumentInfo,\n\tuseDocumentTitle,\n\tuseModal,\n} from '@payloadcms/ui'\nimport { useEffect, useMemo, useState } from 'react'\n\nimport { keys } from '../translations/keys'\nimport { useTranslation } from '../translations/useTranslation'\nimport { ImpersonateIcon } from './ImpersonateIcon'\nimport { StartConfirmModal, type StartTarget } from './StartConfirmModal'\nimport { useImpersonation } from './useImpersonation'\n\nconst CONFIRM_SLUG = 'impersonation-confirm-document'\n\nexport const ImpersonationDocumentButton = () => {\n\tconst { collectionSlug, data, id } = useDocumentInfo()\n\tconst { title } = useDocumentTitle()\n\tconst { user } = useAuth()\n\tconst { config } = useConfig()\n\tconst { t } = useTranslation()\n\tconst { openModal } = useModal()\n\tconst { apiPath, reasonMode, status, targetFilters } = useImpersonation()\n\tconst filter = collectionSlug ? targetFilters[collectionSlug] : undefined\n\tconst [matchesFilter, setMatchesFilter] = useState(filter === true)\n\n\tuseEffect(() => {\n\t\tif (!collectionSlug || id === undefined || filter === undefined || filter === true) {\n\t\t\tsetMatchesFilter(filter === true)\n\t\t\treturn\n\t\t}\n\t\tconst controller = new AbortController()\n\t\tconst params = new URLSearchParams({\n\t\t\tdepth: '0',\n\t\t\tlimit: '1',\n\t\t\twhere: JSON.stringify({ and: [{ id: { equals: id } }, filter] }),\n\t\t})\n\t\tvoid fetch(`${config.routes.api}/${collectionSlug}?${params.toString()}`, {\n\t\t\tcredentials: 'include',\n\t\t\tsignal: controller.signal,\n\t\t})\n\t\t\t.then(async (response) => {\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\treturn { docs: [] }\n\t\t\t\t}\n\t\t\t\treturn (await response.json()) as { docs?: unknown[] }\n\t\t\t})\n\t\t\t.then((body) => {\n\t\t\t\tif (!controller.signal.aborted) {\n\t\t\t\t\tsetMatchesFilter((body.docs?.length ?? 0) > 0)\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch(() => {\n\t\t\t\tif (!controller.signal.aborted) {\n\t\t\t\t\tsetMatchesFilter(false)\n\t\t\t\t}\n\t\t\t})\n\t\treturn () => controller.abort()\n\t}, [collectionSlug, config.routes.api, filter, id])\n\n\tconst target = useMemo<null | StartTarget>(() => {\n\t\tif (!collectionSlug || id === undefined || filter === undefined) {\n\t\t\treturn null\n\t\t}\n\t\tconst fromData =\n\t\t\tdata && typeof data === 'object'\n\t\t\t\t? String(\n\t\t\t\t\t\t(data as { email?: string; name?: string }).name ??\n\t\t\t\t\t\t\t(data as { email?: string }).email ??\n\t\t\t\t\t\t\t''\n\t\t\t\t\t)\n\t\t\t\t: ''\n\t\treturn {\n\t\t\tcollection: collectionSlug,\n\t\t\tid,\n\t\t\tlabel: fromData || title || String(id),\n\t\t}\n\t}, [collectionSlug, data, filter, id, title])\n\n\tconst self =\n\t\tuser && collectionSlug === user.collection && id !== undefined && String(user.id) === String(id)\n\n\tif (!target || !matchesFilter || status.active || self) {\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-document-button\"\n\t\t\t\ticon={<ImpersonateIcon />}\n\t\t\t\ticonPosition=\"left\"\n\t\t\t\tmargin={false}\n\t\t\t\tonClick={() => openModal(CONFIRM_SLUG)}\n\t\t\t>\n\t\t\t\t{t(keys.switchToUser)}\n\t\t\t</Button>\n\t\t\t<StartConfirmModal\n\t\t\t\tapiPath={apiPath}\n\t\t\t\tkey={`${target.collection}:${target.id}`}\n\t\t\t\tmodalSlug={CONFIRM_SLUG}\n\t\t\t\treasonMode={reasonMode}\n\t\t\t\ttarget={target}\n\t\t\t/>\n\t\t</>\n\t)\n}\n"],"mappings":";;;;;;;;;;AAkBA,MAAM,eAAe;AAErB,MAAa,oCAAoC;CAChD,MAAM,EAAE,gBAAgB,MAAM,OAAO,gBAAgB;CACrD,MAAM,EAAE,UAAU,iBAAiB;CACnC,MAAM,EAAE,SAAS,QAAQ;CACzB,MAAM,EAAE,WAAW,UAAU;CAC7B,MAAM,EAAE,MAAMA,iBAAe;CAC7B,MAAM,EAAE,cAAc,SAAS;CAC/B,MAAM,EAAE,SAAS,YAAY,QAAQ,kBAAkB,iBAAiB;CACxE,MAAM,SAAS,iBAAiB,cAAc,kBAAkB,KAAA;CAChE,MAAM,CAAC,eAAe,oBAAoB,SAAS,WAAW,IAAI;CAElE,gBAAgB;EACf,IAAI,CAAC,kBAAkB,OAAO,KAAA,KAAa,WAAW,KAAA,KAAa,WAAW,MAAM;GACnF,iBAAiB,WAAW,IAAI;GAChC;EACD;EACA,MAAM,aAAa,IAAI,gBAAgB;EACvC,MAAM,SAAS,IAAI,gBAAgB;GAClC,OAAO;GACP,OAAO;GACP,OAAO,KAAK,UAAU,EAAE,KAAK,CAAC,EAAE,IAAI,EAAE,QAAQ,GAAG,EAAE,GAAG,MAAM,EAAE,CAAC;EAChE,CAAC;EACD,MAAW,GAAG,OAAO,OAAO,IAAI,GAAG,eAAe,GAAG,OAAO,SAAS,KAAK;GACzE,aAAa;GACb,QAAQ,WAAW;EACpB,CAAC,EACC,KAAK,OAAO,aAAa;GACzB,IAAI,CAAC,SAAS,IACb,OAAO,EAAE,MAAM,CAAC,EAAE;GAEnB,OAAQ,MAAM,SAAS,KAAK;EAC7B,CAAC,EACA,MAAM,SAAS;GACf,IAAI,CAAC,WAAW,OAAO,SACtB,kBAAkB,KAAK,MAAM,UAAU,KAAK,CAAC;EAE/C,CAAC,EACA,YAAY;GACZ,IAAI,CAAC,WAAW,OAAO,SACtB,iBAAiB,KAAK;EAExB,CAAC;EACF,aAAa,WAAW,MAAM;CAC/B,GAAG;EAAC;EAAgB,OAAO,OAAO;EAAK;EAAQ;CAAE,CAAC;CAElD,MAAM,SAAS,cAAkC;EAChD,IAAI,CAAC,kBAAkB,OAAO,KAAA,KAAa,WAAW,KAAA,GACrD,OAAO;EAUR,OAAO;GACN,YAAY;GACZ;GACA,QAVA,QAAQ,OAAO,SAAS,WACrB,OACC,KAA2C,QAC1C,KAA4B,SAC7B,EACF,IACC,OAIgB,SAAS,OAAO,EAAE;EACtC;CACD,GAAG;EAAC;EAAgB;EAAM;EAAQ;EAAI;CAAK,CAAC;CAE5C,MAAM,OACL,QAAQ,mBAAmB,KAAK,cAAc,OAAO,KAAA,KAAa,OAAO,KAAK,EAAE,MAAM,OAAO,EAAE;CAEhG,IAAI,CAAC,UAAU,CAAC,iBAAiB,OAAO,UAAU,MACjD,OAAO;CAGR,OACC,qBAAA,UAAA,EAAA,UAAA,CACC,oBAAC,QAAD;EACC,aAAY;EACZ,WAAU;EACV,MAAM,oBAAC,iBAAD,CAAkB,CAAA;EACxB,cAAa;EACb,QAAQ;EACR,eAAe,UAAU,YAAY;YAEpC,EAAE,KAAK,YAAY;CACb,CAAA,GACR,oBAAC,mBAAD;EACU;EAET,WAAW;EACC;EACJ;CACR,GAJK,GAAG,OAAO,WAAW,GAAG,OAAO,IAIpC,CACA,EAAA,CAAA;AAEJ"}
|