@planningcenter/topbar 12.3.0 → 12.4.0-rc.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/dist/topbar.css +7 -7
- package/dist/topbar.css.map +1 -1
- package/dist/topbar.js +1303 -425
- package/dist/topbar.js.map +1 -1
- package/dist/topbar.modern.js +1308 -430
- package/dist/topbar.modern.js.map +1 -1
- package/dist/topbar.module.js +1308 -430
- package/dist/topbar.module.js.map +1 -1
- package/dist/topbar.umd.js +1303 -425
- package/dist/topbar.umd.js.map +1 -1
- package/package.json +4 -3
package/dist/topbar.js
CHANGED
|
@@ -180,12 +180,14 @@ function inferRailsCsrfToken() {
|
|
|
180
180
|
}
|
|
181
181
|
function apiRequest(fetch, url, {
|
|
182
182
|
method = "GET",
|
|
183
|
-
data = null
|
|
183
|
+
data = null,
|
|
184
|
+
headers = {}
|
|
184
185
|
} = {}) {
|
|
185
186
|
const options = {
|
|
186
187
|
headers: {
|
|
187
188
|
Accept: "application/json",
|
|
188
|
-
"Content-Type": "application/json"
|
|
189
|
+
"Content-Type": "application/json",
|
|
190
|
+
...headers
|
|
189
191
|
},
|
|
190
192
|
method,
|
|
191
193
|
redirect: "follow"
|
|
@@ -805,7 +807,7 @@ function useMediaQuery(query) {
|
|
|
805
807
|
return matches;
|
|
806
808
|
}
|
|
807
809
|
|
|
808
|
-
const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1);
|
|
810
|
+
const capitalize$1 = s => s.charAt(0).toUpperCase() + s.slice(1);
|
|
809
811
|
const RESOLVED_COLOR_MODE_ATTRIBUTE = "data-resolved-color-mode";
|
|
810
812
|
function resolveColorMode(colorMode) {
|
|
811
813
|
if (colorMode === "system") {
|
|
@@ -3329,19 +3331,513 @@ function ProductTabs({
|
|
|
3329
3331
|
}, children));
|
|
3330
3332
|
}
|
|
3331
3333
|
|
|
3334
|
+
function parseValue(element) {
|
|
3335
|
+
const datatype = element.getAttribute("datatype");
|
|
3336
|
+
switch (datatype) {
|
|
3337
|
+
case "boolean":
|
|
3338
|
+
return element.content === "true" || element.content === "1";
|
|
3339
|
+
default:
|
|
3340
|
+
return element.content;
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
function getAllPcoMeta() {
|
|
3344
|
+
const elements = document.head.querySelectorAll(`meta[property^="pco:"]`);
|
|
3345
|
+
const meta = {};
|
|
3346
|
+
elements.forEach(e => {
|
|
3347
|
+
const property = e.getAttribute("property");
|
|
3348
|
+
meta[property.substring("pco:".length)] = parseValue(e);
|
|
3349
|
+
});
|
|
3350
|
+
return meta;
|
|
3351
|
+
}
|
|
3352
|
+
// usePcoMeta only reads each tag's content. Recently Visited also needs the
|
|
3353
|
+
// pco:recently_visitable tag's `metadata` attribute, which can change while
|
|
3354
|
+
// the content stays the same, so it is tracked in its own store.
|
|
3355
|
+
function getRecentlyVisitableMetadata() {
|
|
3356
|
+
var _a, _b;
|
|
3357
|
+
return (_b = (_a = document.head.querySelector(`meta[property="pco:recently_visitable"]`)) === null || _a === void 0 ? void 0 : _a.getAttribute("metadata")) !== null && _b !== void 0 ? _b : null;
|
|
3358
|
+
}
|
|
3359
|
+
const store = createStore(getAllPcoMeta());
|
|
3360
|
+
const recentlyVisitableMetadataStore = createStore({
|
|
3361
|
+
metadata: getRecentlyVisitableMetadata()
|
|
3362
|
+
});
|
|
3363
|
+
function sync() {
|
|
3364
|
+
const nextState = getAllPcoMeta();
|
|
3365
|
+
if (!shallowCompare(store.getState(), nextState)) store.setState(nextState);
|
|
3366
|
+
const metadata = getRecentlyVisitableMetadata();
|
|
3367
|
+
if (recentlyVisitableMetadataStore.getState().metadata !== metadata) {
|
|
3368
|
+
recentlyVisitableMetadataStore.setState({
|
|
3369
|
+
metadata
|
|
3370
|
+
});
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
const observer = new MutationObserver(sync);
|
|
3374
|
+
let hasTriggeredObserve = false;
|
|
3375
|
+
function observe() {
|
|
3376
|
+
if (hasTriggeredObserve) return;
|
|
3377
|
+
hasTriggeredObserve = true;
|
|
3378
|
+
// The module-load snapshot above can predate meta tags that appear later
|
|
3379
|
+
// in <head> (e.g. pco:resource, yielded right before </head>) if this
|
|
3380
|
+
// module's own script tag isn't deferred and runs mid-parse. Those tags
|
|
3381
|
+
// never mutate afterward, so the observer alone would never catch them.
|
|
3382
|
+
// Re-read once now, since observe() is only called once a consumer is
|
|
3383
|
+
// actually mounting, which is always after the document has fully parsed.
|
|
3384
|
+
sync();
|
|
3385
|
+
observer.observe(document.head, {
|
|
3386
|
+
attributes: true,
|
|
3387
|
+
childList: true,
|
|
3388
|
+
subtree: true
|
|
3389
|
+
});
|
|
3390
|
+
}
|
|
3391
|
+
function usePcoMeta() {
|
|
3392
|
+
observe();
|
|
3393
|
+
return shim.useSyncExternalStore(store.subscribe, () => store.getState());
|
|
3394
|
+
}
|
|
3395
|
+
function useRecentlyVisitableMetadata() {
|
|
3396
|
+
observe();
|
|
3397
|
+
return shim.useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3400
|
+
const MAX_ENTRIES$1 = 20;
|
|
3401
|
+
const FORMAT_VERSION = 1;
|
|
3402
|
+
const MAX_TEXT_LENGTH = 200;
|
|
3403
|
+
const PRODUCTS = ["accounts", "calendar", "check-ins", "giving", "groups", "home", "login", "people", "publishing", "registrations", "services"];
|
|
3404
|
+
function isId(value) {
|
|
3405
|
+
return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
|
|
3406
|
+
}
|
|
3407
|
+
// A PCRL looks like `<product>/v2/<resource_type>/<id>`.
|
|
3408
|
+
function isPcrl(value) {
|
|
3409
|
+
const segments = value.split("/");
|
|
3410
|
+
return segments.length === 4 && PRODUCTS.some(product => product === segments[0]) && isId(segments[3]);
|
|
3411
|
+
}
|
|
3412
|
+
function isNonBlankString(value) {
|
|
3413
|
+
return typeof value === "string" && value.trim() !== "";
|
|
3414
|
+
}
|
|
3415
|
+
function isRecord(value) {
|
|
3416
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3417
|
+
}
|
|
3418
|
+
function entryKey(entry) {
|
|
3419
|
+
if (entry.type === "resource") {
|
|
3420
|
+
return `resource:${entry.pcrl}`;
|
|
3421
|
+
} else {
|
|
3422
|
+
return `page:${entry.product}:${entry.path}`;
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
function isEntry(value) {
|
|
3426
|
+
if (!isRecord(value)) {
|
|
3427
|
+
return false;
|
|
3428
|
+
} else if (value.type === "resource") {
|
|
3429
|
+
return typeof value.pcrl === "string" && isPcrl(value.pcrl);
|
|
3430
|
+
} else if (value.type === "page") {
|
|
3431
|
+
return isNonBlankString(value.product) && isNonBlankString(value.title) && isNonBlankString(value.path) && typeof value.details === "string";
|
|
3432
|
+
} else {
|
|
3433
|
+
return false;
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
// Cuts on code points so a surrogate pair is never split.
|
|
3437
|
+
function clampText(value) {
|
|
3438
|
+
return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
|
|
3439
|
+
}
|
|
3440
|
+
function clampEntry(entry) {
|
|
3441
|
+
if (entry.type === "resource") {
|
|
3442
|
+
return entry;
|
|
3443
|
+
} else {
|
|
3444
|
+
return {
|
|
3445
|
+
...entry,
|
|
3446
|
+
details: clampText(entry.details),
|
|
3447
|
+
title: clampText(entry.title)
|
|
3448
|
+
};
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
// Recents are disposable: anything unexpected reads as an empty list (or, for
|
|
3452
|
+
// a single bad entry, a list without it) rather than throwing.
|
|
3453
|
+
function parseRecentlyVisited(value, personId) {
|
|
3454
|
+
let parsed = null;
|
|
3455
|
+
try {
|
|
3456
|
+
parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
|
|
3457
|
+
} catch {
|
|
3458
|
+
parsed = null;
|
|
3459
|
+
}
|
|
3460
|
+
if (isRecord(parsed) && parsed.v === FORMAT_VERSION && parsed.person === personId && Array.isArray(parsed.entries)) {
|
|
3461
|
+
return parsed.entries.filter(isEntry);
|
|
3462
|
+
} else {
|
|
3463
|
+
return [];
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
function addVisit(entries, visit) {
|
|
3467
|
+
const key = entryKey(visit);
|
|
3468
|
+
return [clampEntry(visit), ...entries.filter(entry => entryKey(entry) !== key)].slice(0, MAX_ENTRIES$1);
|
|
3469
|
+
}
|
|
3470
|
+
function serializeRecentlyVisited(entries, personId) {
|
|
3471
|
+
return JSON.stringify({
|
|
3472
|
+
entries,
|
|
3473
|
+
person: personId,
|
|
3474
|
+
v: FORMAT_VERSION
|
|
3475
|
+
});
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
// The product whose URL, in this environment, has the current page's host.
|
|
3479
|
+
// Reversing pcoUrl handles every environment @planningcenter/url knows about,
|
|
3480
|
+
// and guarantees the stored product rebuilds a working link.
|
|
3481
|
+
function currentProduct() {
|
|
3482
|
+
var _a;
|
|
3483
|
+
return (_a = PRODUCTS.find(product => new URL(pcoUrl$1.pcoUrl(product)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
|
|
3484
|
+
}
|
|
3485
|
+
function detailsFromMetadata(metadata) {
|
|
3486
|
+
var _a;
|
|
3487
|
+
try {
|
|
3488
|
+
const details = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) === null || _a === void 0 ? void 0 : _a.details;
|
|
3489
|
+
return typeof details === "string" ? details : "";
|
|
3490
|
+
} catch {
|
|
3491
|
+
return "";
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
// pco:resource is the more specific signal, so it wins when both tags are
|
|
3495
|
+
// present. A resource that isn't a PCRL is ignored rather than recorded.
|
|
3496
|
+
function visitFromPage({
|
|
3497
|
+
metadata,
|
|
3498
|
+
resource,
|
|
3499
|
+
title
|
|
3500
|
+
}) {
|
|
3501
|
+
const product = currentProduct();
|
|
3502
|
+
if (resource && isPcrl(resource)) {
|
|
3503
|
+
return {
|
|
3504
|
+
pcrl: resource,
|
|
3505
|
+
type: "resource"
|
|
3506
|
+
};
|
|
3507
|
+
} else if (title && title.trim() !== "" && product) {
|
|
3508
|
+
return {
|
|
3509
|
+
details: detailsFromMetadata(metadata),
|
|
3510
|
+
path: window.location.pathname,
|
|
3511
|
+
product,
|
|
3512
|
+
title,
|
|
3513
|
+
type: "page"
|
|
3514
|
+
};
|
|
3515
|
+
} else {
|
|
3516
|
+
return null;
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
// The entry Recently Visited would record for the current page, or null when
|
|
3520
|
+
// the page isn't eligible. Shared by the recorder and the debug panel so the
|
|
3521
|
+
// panel always shows what actually gets stored.
|
|
3522
|
+
function useCurrentVisit() {
|
|
3523
|
+
const {
|
|
3524
|
+
recently_visitable: title,
|
|
3525
|
+
resource
|
|
3526
|
+
} = usePcoMeta();
|
|
3527
|
+
const metadata = useRecentlyVisitableMetadata();
|
|
3528
|
+
return React.useMemo(() => visitFromPage({
|
|
3529
|
+
metadata,
|
|
3530
|
+
resource,
|
|
3531
|
+
title
|
|
3532
|
+
}), [metadata, resource, title]);
|
|
3533
|
+
}
|
|
3534
|
+
// Products hand the person's ID to AppsProvider, not to the topbar itself,
|
|
3535
|
+
// and several pass it as a number.
|
|
3536
|
+
function useCurrentPersonId() {
|
|
3537
|
+
const {
|
|
3538
|
+
userId
|
|
3539
|
+
} = useStore();
|
|
3540
|
+
return userId == null ? undefined : String(userId);
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
// Per-origin localStorage is a stopgap: each product keeps its own list until
|
|
3544
|
+
// PCA-409 moves this to storage shared across product subdomains. Only this
|
|
3545
|
+
// file should need to change when it does.
|
|
3546
|
+
function storageKey({
|
|
3547
|
+
env,
|
|
3548
|
+
personId
|
|
3549
|
+
}) {
|
|
3550
|
+
return `topbar.recently_visited.${env}.${personId}`;
|
|
3551
|
+
}
|
|
3552
|
+
// localStorage doesn't notify the tab that wrote it, so readers in this tab
|
|
3553
|
+
// learn about new visits from this counter instead.
|
|
3554
|
+
const changes = createStore({
|
|
3555
|
+
version: 0
|
|
3556
|
+
});
|
|
3557
|
+
function readRecentlyVisited(scope) {
|
|
3558
|
+
try {
|
|
3559
|
+
return parseRecentlyVisited(localStorage.getItem(storageKey(scope)), scope.personId);
|
|
3560
|
+
} catch {
|
|
3561
|
+
return [];
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
function recordVisit(visit, scope) {
|
|
3565
|
+
if (isEntry(visit)) {
|
|
3566
|
+
try {
|
|
3567
|
+
localStorage.setItem(storageKey(scope), serializeRecentlyVisited(addVisit(readRecentlyVisited(scope), visit), scope.personId));
|
|
3568
|
+
changes.setState({
|
|
3569
|
+
version: changes.getState().version + 1
|
|
3570
|
+
});
|
|
3571
|
+
} catch {
|
|
3572
|
+
// Storage can be full or blocked; recents are disposable.
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
function useRecentlyVisited({
|
|
3577
|
+
env,
|
|
3578
|
+
personId
|
|
3579
|
+
}) {
|
|
3580
|
+
const version = shim.useSyncExternalStore(changes.subscribe, () => changes.getState().version);
|
|
3581
|
+
// `version` isn't read, but listing it re-reads storage after each visit.
|
|
3582
|
+
return React.useMemo(() => personId ? readRecentlyVisited({
|
|
3583
|
+
env,
|
|
3584
|
+
personId
|
|
3585
|
+
}) : [], [env, personId, version]);
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
// The versions home-mobile pins, so web and mobile read the same response
|
|
3589
|
+
// shapes. Some endpoints only exist from a version on: Registrations' me,
|
|
3590
|
+
// product_search_results, and pcrl_lookups need 2025-05-01.
|
|
3591
|
+
const API_VERSIONS = {
|
|
3592
|
+
calendar: "2022-07-07",
|
|
3593
|
+
"check-ins": "2025-05-28",
|
|
3594
|
+
giving: "2019-10-18",
|
|
3595
|
+
groups: "2023-07-10",
|
|
3596
|
+
home: "2018-08-01",
|
|
3597
|
+
people: "2023-03-21",
|
|
3598
|
+
publishing: "2024-03-25",
|
|
3599
|
+
registrations: "2025-05-01",
|
|
3600
|
+
services: "2018-11-01"
|
|
3601
|
+
};
|
|
3602
|
+
// For a `/~api/{product}/v2/...` URL, the header that pins its product's
|
|
3603
|
+
// version.
|
|
3604
|
+
function apiVersionHeaders(url) {
|
|
3605
|
+
const [,, product] = new URL(url).pathname.split("/");
|
|
3606
|
+
const version = API_VERSIONS[product];
|
|
3607
|
+
if (version) {
|
|
3608
|
+
return {
|
|
3609
|
+
"X-PCO-API-Version": version
|
|
3610
|
+
};
|
|
3611
|
+
} else {
|
|
3612
|
+
return {};
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
const CACHE_TTL_MS = 60000;
|
|
3617
|
+
const MAX_ENTRIES = 50;
|
|
3618
|
+
const cache = new Map();
|
|
3619
|
+
function cachedRequest(url) {
|
|
3620
|
+
const cached = cache.get(url);
|
|
3621
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
3622
|
+
// Re-inserting keeps the Map in recently-used order for eviction.
|
|
3623
|
+
cache.delete(url);
|
|
3624
|
+
cache.set(url, cached);
|
|
3625
|
+
return cached.promise;
|
|
3626
|
+
} else {
|
|
3627
|
+
return fetchIntoCache(url);
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
function fetchIntoCache(url) {
|
|
3631
|
+
const promise = apiRequest(defaultFetch, url, {
|
|
3632
|
+
headers: apiVersionHeaders(url)
|
|
3633
|
+
}).then(({
|
|
3634
|
+
json,
|
|
3635
|
+
ok
|
|
3636
|
+
}) => {
|
|
3637
|
+
if (!ok || json === null) throw new Error(`Request failed: ${url}`);
|
|
3638
|
+
return json;
|
|
3639
|
+
});
|
|
3640
|
+
// In-flight entries never expire, so every caller shares the one fetch.
|
|
3641
|
+
const entry = {
|
|
3642
|
+
expiresAt: Infinity,
|
|
3643
|
+
promise
|
|
3644
|
+
};
|
|
3645
|
+
cache.set(url, entry);
|
|
3646
|
+
promise.then(() => entry.expiresAt = Date.now() + CACHE_TTL_MS, () => cache.get(url) === entry && cache.delete(url));
|
|
3647
|
+
if (cache.size > MAX_ENTRIES) cache.delete(cache.keys().next().value);
|
|
3648
|
+
return promise;
|
|
3649
|
+
}
|
|
3650
|
+
function useCachedRequest(url) {
|
|
3651
|
+
const [settled, setSettled] = React.useState();
|
|
3652
|
+
React.useEffect(() => {
|
|
3653
|
+
if (url) {
|
|
3654
|
+
let current = true;
|
|
3655
|
+
cachedRequest(url).then(data => current && setSettled({
|
|
3656
|
+
data,
|
|
3657
|
+
url
|
|
3658
|
+
}), () => current && setSettled({
|
|
3659
|
+
failed: true,
|
|
3660
|
+
url
|
|
3661
|
+
}));
|
|
3662
|
+
return () => {
|
|
3663
|
+
current = false;
|
|
3664
|
+
};
|
|
3665
|
+
} else {
|
|
3666
|
+
return undefined;
|
|
3667
|
+
}
|
|
3668
|
+
}, [url]);
|
|
3669
|
+
if (!url) {
|
|
3670
|
+
return {
|
|
3671
|
+
status: "idle"
|
|
3672
|
+
};
|
|
3673
|
+
} else if ((settled === null || settled === void 0 ? void 0 : settled.url) !== url) {
|
|
3674
|
+
return {
|
|
3675
|
+
data: settled && "data" in settled ? settled.data : undefined,
|
|
3676
|
+
status: "loading"
|
|
3677
|
+
};
|
|
3678
|
+
} else if ("data" in settled) {
|
|
3679
|
+
return {
|
|
3680
|
+
data: settled.data,
|
|
3681
|
+
status: "done"
|
|
3682
|
+
};
|
|
3683
|
+
} else {
|
|
3684
|
+
return {
|
|
3685
|
+
status: "error"
|
|
3686
|
+
};
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
// One request per URL, for callers that fan out to a varying number of
|
|
3690
|
+
// endpoints. Unlike `useCachedRequest`, a URL that hasn't settled yet has no
|
|
3691
|
+
// earlier data to fall back on.
|
|
3692
|
+
function useCachedRequests(urls) {
|
|
3693
|
+
const [settled, setSettled] = React.useState({});
|
|
3694
|
+
// Keyed by value because each settle re-renders the caller, and a caller
|
|
3695
|
+
// that builds a fresh `urls` array on render would otherwise loop forever.
|
|
3696
|
+
const key = urls.join("\n");
|
|
3697
|
+
React.useEffect(() => {
|
|
3698
|
+
let current = true;
|
|
3699
|
+
const settle = result => current && setSettled(prev => ({
|
|
3700
|
+
...prev,
|
|
3701
|
+
[result.url]: result
|
|
3702
|
+
}));
|
|
3703
|
+
for (const url of key.split("\n").filter(Boolean)) {
|
|
3704
|
+
cachedRequest(url).then(data => settle({
|
|
3705
|
+
data,
|
|
3706
|
+
url
|
|
3707
|
+
}), () => settle({
|
|
3708
|
+
failed: true,
|
|
3709
|
+
url
|
|
3710
|
+
}));
|
|
3711
|
+
}
|
|
3712
|
+
return () => {
|
|
3713
|
+
current = false;
|
|
3714
|
+
};
|
|
3715
|
+
}, [key]);
|
|
3716
|
+
return urls.map(url => {
|
|
3717
|
+
const result = settled[url];
|
|
3718
|
+
if (!result) {
|
|
3719
|
+
return {
|
|
3720
|
+
status: "loading"
|
|
3721
|
+
};
|
|
3722
|
+
} else if ("data" in result) {
|
|
3723
|
+
return {
|
|
3724
|
+
data: result.data,
|
|
3725
|
+
status: "done"
|
|
3726
|
+
};
|
|
3727
|
+
} else {
|
|
3728
|
+
return {
|
|
3729
|
+
status: "error"
|
|
3730
|
+
};
|
|
3731
|
+
}
|
|
3732
|
+
});
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
const pcoUrl = pcoUrl$1;
|
|
3736
|
+
|
|
3737
|
+
// Resolves resource entries with one pcrl_lookups request per product.
|
|
3738
|
+
// pcrl_lookups reports forbidden and deleted resources the same way, so both
|
|
3739
|
+
// become `unresolved`, as does every entry in a batch whose request failed.
|
|
3740
|
+
function useHydratedRecentlyVisited(entries, env) {
|
|
3741
|
+
const batches = React.useMemo(() => batchByProduct(entries), [entries]);
|
|
3742
|
+
const urls = React.useMemo(() => batches.map(([product, pcrls]) => pcrlLookupsUrl(product, pcrls)), [batches]);
|
|
3743
|
+
const requests = useCachedRequests(urls);
|
|
3744
|
+
const lookups = new Map();
|
|
3745
|
+
batches.forEach(([, pcrls], index) => {
|
|
3746
|
+
const request = requests[index];
|
|
3747
|
+
for (const pcrl of pcrls) lookups.set(pcrl, lookupFor(pcrl, request));
|
|
3748
|
+
});
|
|
3749
|
+
return entries.map(entry => {
|
|
3750
|
+
var _a;
|
|
3751
|
+
if (entry.type === "page") {
|
|
3752
|
+
return {
|
|
3753
|
+
entry,
|
|
3754
|
+
status: "page",
|
|
3755
|
+
url: pageUrl(entry, env)
|
|
3756
|
+
};
|
|
3757
|
+
} else {
|
|
3758
|
+
const lookup = (_a = lookups.get(entry.pcrl)) !== null && _a !== void 0 ? _a : {
|
|
3759
|
+
status: "unresolved"
|
|
3760
|
+
};
|
|
3761
|
+
if (lookup.status === "resolved") {
|
|
3762
|
+
return {
|
|
3763
|
+
...lookup,
|
|
3764
|
+
entry,
|
|
3765
|
+
url: lookup.result.attributes.url
|
|
3766
|
+
};
|
|
3767
|
+
} else {
|
|
3768
|
+
return {
|
|
3769
|
+
entry,
|
|
3770
|
+
status: lookup.status
|
|
3771
|
+
};
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
});
|
|
3775
|
+
}
|
|
3776
|
+
function lookupFor(pcrl, request) {
|
|
3777
|
+
if (request.status === "done") {
|
|
3778
|
+
const result = request.data.data.find(({
|
|
3779
|
+
attributes
|
|
3780
|
+
}) => attributes.pcrl === pcrl);
|
|
3781
|
+
return result ? {
|
|
3782
|
+
result,
|
|
3783
|
+
status: "resolved"
|
|
3784
|
+
} : {
|
|
3785
|
+
status: "unresolved"
|
|
3786
|
+
};
|
|
3787
|
+
} else if (request.status === "error") {
|
|
3788
|
+
return {
|
|
3789
|
+
status: "unresolved"
|
|
3790
|
+
};
|
|
3791
|
+
} else {
|
|
3792
|
+
return {
|
|
3793
|
+
status: "loading"
|
|
3794
|
+
};
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
const RECENTLY_VISITED_PAGE_SIZE = 10;
|
|
3798
|
+
// A PCRL can only be looked up through its own product's API, which may not be
|
|
3799
|
+
// the product it was visited in. Each page of entries is batched on its own,
|
|
3800
|
+
// so loading the next page leaves earlier pages' URLs, and so their cache
|
|
3801
|
+
// keys, alone. Sorting keeps them stable as a page reorders.
|
|
3802
|
+
function batchByProduct(entries) {
|
|
3803
|
+
var _a;
|
|
3804
|
+
const batches = [];
|
|
3805
|
+
for (let start = 0; start < entries.length; start += RECENTLY_VISITED_PAGE_SIZE) {
|
|
3806
|
+
const page = new Map();
|
|
3807
|
+
for (const entry of entries.slice(start, start + RECENTLY_VISITED_PAGE_SIZE)) {
|
|
3808
|
+
if (entry.type === "resource") {
|
|
3809
|
+
const [product] = entry.pcrl.split("/");
|
|
3810
|
+
page.set(product, [...((_a = page.get(product)) !== null && _a !== void 0 ? _a : []), entry.pcrl]);
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
for (const [product, pcrls] of page) {
|
|
3814
|
+
batches.push([product, [...pcrls].sort()]);
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
return batches;
|
|
3818
|
+
}
|
|
3819
|
+
function pcrlLookupsUrl(product, pcrls) {
|
|
3820
|
+
const params = new URLSearchParams({
|
|
3821
|
+
"where[pcrl]": pcrls.join(",")
|
|
3822
|
+
});
|
|
3823
|
+
return `${window.location.origin}/~api/${product}/v2/pcrl_lookups?${params}`;
|
|
3824
|
+
}
|
|
3825
|
+
// Built from the product rather than the current origin, so it stays right
|
|
3826
|
+
// once PCA-409 shares one list across product subdomains.
|
|
3827
|
+
function pageUrl(entry, env) {
|
|
3828
|
+
return `${pcoUrl(env)(entry.product)}${entry.path}`;
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3332
3831
|
var styles$a = {"result":"result-WCnxr"};
|
|
3333
3832
|
|
|
3334
3833
|
function SearchResult({
|
|
3335
3834
|
highlighted,
|
|
3336
3835
|
id,
|
|
3337
3836
|
onHighlight,
|
|
3338
|
-
|
|
3837
|
+
subtitle,
|
|
3838
|
+
title,
|
|
3839
|
+
url
|
|
3339
3840
|
}) {
|
|
3340
|
-
const {
|
|
3341
|
-
metadata,
|
|
3342
|
-
title,
|
|
3343
|
-
url
|
|
3344
|
-
} = result.attributes;
|
|
3345
3841
|
const ref = React.useRef(null);
|
|
3346
3842
|
React.useEffect(() => {
|
|
3347
3843
|
var _a;
|
|
@@ -3362,10 +3858,7 @@ function SearchResult({
|
|
|
3362
3858
|
ref: ref,
|
|
3363
3859
|
role: "option",
|
|
3364
3860
|
tabIndex: -1
|
|
3365
|
-
}, /*#__PURE__*/React.createElement("div", null, title), /*#__PURE__*/React.createElement("div", null,
|
|
3366
|
-
}
|
|
3367
|
-
function summarize(metadata) {
|
|
3368
|
-
return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
|
|
3861
|
+
}, /*#__PURE__*/React.createElement("div", null, title), /*#__PURE__*/React.createElement("div", null, subtitle));
|
|
3369
3862
|
}
|
|
3370
3863
|
|
|
3371
3864
|
const resultId = (listboxId, index) => `${listboxId}-result-${index}`;
|
|
@@ -3437,83 +3930,127 @@ function ResultGroups({
|
|
|
3437
3930
|
id: resultId(id, resultIndex),
|
|
3438
3931
|
key: result.id,
|
|
3439
3932
|
onHighlight: () => onHighlight(resultIndex),
|
|
3440
|
-
|
|
3933
|
+
subtitle: summarize(result.attributes.metadata),
|
|
3934
|
+
title: result.attributes.title,
|
|
3935
|
+
url: result.attributes.url
|
|
3441
3936
|
});
|
|
3442
3937
|
}));
|
|
3443
3938
|
}));
|
|
3444
3939
|
}
|
|
3940
|
+
function summarize(metadata) {
|
|
3941
|
+
return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
|
|
3942
|
+
}
|
|
3445
3943
|
|
|
3446
|
-
|
|
3447
|
-
const
|
|
3448
|
-
const
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3944
|
+
function useRecentVisits(env) {
|
|
3945
|
+
const personId = useCurrentPersonId();
|
|
3946
|
+
const entries = useRecentlyVisited({
|
|
3947
|
+
env,
|
|
3948
|
+
personId
|
|
3949
|
+
});
|
|
3950
|
+
const [count, setCount] = React.useState(RECENTLY_VISITED_PAGE_SIZE);
|
|
3951
|
+
const shown = React.useMemo(() => entries.slice(0, count), [entries, count]);
|
|
3952
|
+
const hydrated = useHydratedRecentlyVisited(shown, env);
|
|
3953
|
+
// Resources show up once they're found. Ones that can't be found anymore,
|
|
3954
|
+
// or that the person can no longer see, are left out.
|
|
3955
|
+
const visits = hydrated.filter(isShown);
|
|
3956
|
+
const loading = hydrated.some(({
|
|
3957
|
+
status
|
|
3958
|
+
}) => status === "loading");
|
|
3959
|
+
const showMore = React.useCallback(() => setCount(current => current + RECENTLY_VISITED_PAGE_SIZE), []);
|
|
3960
|
+
return {
|
|
3961
|
+
count,
|
|
3962
|
+
hasMore: entries.length > count,
|
|
3963
|
+
loading,
|
|
3964
|
+
showMore,
|
|
3965
|
+
visits
|
|
3966
|
+
};
|
|
3967
|
+
}
|
|
3968
|
+
function isShown(visit) {
|
|
3969
|
+
return visit.status === "page" || visit.status === "resolved";
|
|
3970
|
+
}
|
|
3971
|
+
function RecentlyVisited({
|
|
3972
|
+
count,
|
|
3973
|
+
hasMore,
|
|
3974
|
+
highlightedIndex,
|
|
3975
|
+
id,
|
|
3976
|
+
loading,
|
|
3977
|
+
onHighlight,
|
|
3978
|
+
showMore,
|
|
3979
|
+
visits
|
|
3980
|
+
}) {
|
|
3981
|
+
const headingId = `${id}-heading`;
|
|
3982
|
+
if (visits.length === 0 && !hasMore && !loading) {
|
|
3983
|
+
return /*#__PURE__*/React.createElement("p", null, "Pages you visit will show up here.");
|
|
3456
3984
|
} else {
|
|
3457
|
-
return
|
|
3985
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
|
|
3986
|
+
id: headingId
|
|
3987
|
+
}, "Recently visited"), /*#__PURE__*/React.createElement("div", {
|
|
3988
|
+
"aria-labelledby": headingId,
|
|
3989
|
+
id: id,
|
|
3990
|
+
role: "listbox"
|
|
3991
|
+
}, visits.map((visit, index) => (/*#__PURE__*/React.createElement(SearchResult, {
|
|
3992
|
+
highlighted: index === highlightedIndex,
|
|
3993
|
+
id: resultId(id, index),
|
|
3994
|
+
key: entryKey(visit.entry),
|
|
3995
|
+
onHighlight: () => onHighlight(index),
|
|
3996
|
+
...describeVisit(visit)
|
|
3997
|
+
})))), hasMore && /*#__PURE__*/React.createElement(ShowMoreWhenVisible, {
|
|
3998
|
+
key: count,
|
|
3999
|
+
onVisible: showMore
|
|
4000
|
+
}));
|
|
3458
4001
|
}
|
|
3459
4002
|
}
|
|
3460
|
-
function
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
4003
|
+
function ShowMoreWhenVisible({
|
|
4004
|
+
onVisible
|
|
4005
|
+
}) {
|
|
4006
|
+
const ref = React.useRef(null);
|
|
4007
|
+
React.useEffect(() => {
|
|
4008
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
4009
|
+
if (entry.isIntersecting) onVisible();
|
|
4010
|
+
});
|
|
4011
|
+
if (ref.current) observer.observe(ref.current);
|
|
4012
|
+
return () => observer.disconnect();
|
|
4013
|
+
}, [onVisible]);
|
|
4014
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
4015
|
+
ref: ref
|
|
3467
4016
|
});
|
|
3468
|
-
// In-flight entries never expire, so every caller shares the one fetch.
|
|
3469
|
-
const entry = {
|
|
3470
|
-
expiresAt: Infinity,
|
|
3471
|
-
promise
|
|
3472
|
-
};
|
|
3473
|
-
cache.set(url, entry);
|
|
3474
|
-
promise.then(() => entry.expiresAt = Date.now() + CACHE_TTL_MS, () => cache.get(url) === entry && cache.delete(url));
|
|
3475
|
-
if (cache.size > MAX_ENTRIES$1) cache.delete(cache.keys().next().value);
|
|
3476
|
-
return promise;
|
|
3477
4017
|
}
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
url
|
|
3489
|
-
}));
|
|
3490
|
-
return () => {
|
|
3491
|
-
current = false;
|
|
3492
|
-
};
|
|
3493
|
-
} else {
|
|
3494
|
-
return undefined;
|
|
3495
|
-
}
|
|
3496
|
-
}, [url]);
|
|
3497
|
-
if (!url) {
|
|
3498
|
-
return {
|
|
3499
|
-
status: "idle"
|
|
3500
|
-
};
|
|
3501
|
-
} else if ((settled === null || settled === void 0 ? void 0 : settled.url) !== url) {
|
|
3502
|
-
return {
|
|
3503
|
-
data: settled && "data" in settled ? settled.data : undefined,
|
|
3504
|
-
status: "loading"
|
|
3505
|
-
};
|
|
3506
|
-
} else if ("data" in settled) {
|
|
4018
|
+
// Unlike search results, recents come from every product, so each one names
|
|
4019
|
+
// its product and type.
|
|
4020
|
+
function describeVisit(visit) {
|
|
4021
|
+
const subtitle = (...parts) => parts.filter(Boolean).join(" · ");
|
|
4022
|
+
if (visit.status === "page") {
|
|
4023
|
+
const {
|
|
4024
|
+
details,
|
|
4025
|
+
product,
|
|
4026
|
+
title
|
|
4027
|
+
} = visit.entry;
|
|
3507
4028
|
return {
|
|
3508
|
-
|
|
3509
|
-
|
|
4029
|
+
subtitle: subtitle(productName(product), details),
|
|
4030
|
+
title,
|
|
4031
|
+
url: visit.url
|
|
3510
4032
|
};
|
|
3511
4033
|
} else {
|
|
4034
|
+
const {
|
|
4035
|
+
metadata,
|
|
4036
|
+
product,
|
|
4037
|
+
title,
|
|
4038
|
+
type
|
|
4039
|
+
} = visit.result.attributes;
|
|
3512
4040
|
return {
|
|
3513
|
-
|
|
4041
|
+
subtitle: subtitle(productName(product), type, summarize(metadata)),
|
|
4042
|
+
title,
|
|
4043
|
+
url: visit.url
|
|
3514
4044
|
};
|
|
3515
4045
|
}
|
|
3516
4046
|
}
|
|
4047
|
+
// check-ins → Check-Ins
|
|
4048
|
+
function productName(slug) {
|
|
4049
|
+
return slug.split("-").map(capitalize).join("-");
|
|
4050
|
+
}
|
|
4051
|
+
function capitalize(word) {
|
|
4052
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
4053
|
+
}
|
|
3517
4054
|
|
|
3518
4055
|
const SEARCH_DEBOUNCE_MS = 300;
|
|
3519
4056
|
function useProductSearch(product, term) {
|
|
@@ -3604,15 +4141,16 @@ const verticalStep = key => ({
|
|
|
3604
4141
|
ArrowDown: 1,
|
|
3605
4142
|
ArrowUp: -1
|
|
3606
4143
|
})[key];
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
4144
|
+
// Remembering which list the index belongs to puts the highlight back on the
|
|
4145
|
+
// first option whenever a new list replaces it, without an effect to reset it.
|
|
4146
|
+
// A list that grows or fills in under the same `list` keeps its highlight.
|
|
4147
|
+
function useResultNavigation(options, list) {
|
|
3610
4148
|
const [highlight, setHighlight] = React.useState({
|
|
3611
4149
|
index: 0,
|
|
3612
|
-
|
|
4150
|
+
list
|
|
3613
4151
|
});
|
|
3614
|
-
const highlightedIndex = highlight.
|
|
3615
|
-
const highlighted =
|
|
4152
|
+
const highlightedIndex = highlight.list === list ? highlight.index : 0;
|
|
4153
|
+
const highlighted = options[highlightedIndex];
|
|
3616
4154
|
const [copiedUrl, setCopiedUrl] = React.useState(null);
|
|
3617
4155
|
React.useEffect(() => {
|
|
3618
4156
|
if (copiedUrl) {
|
|
@@ -3622,30 +4160,30 @@ function useResultNavigation(results) {
|
|
|
3622
4160
|
return undefined;
|
|
3623
4161
|
}
|
|
3624
4162
|
}, [copiedUrl]);
|
|
3625
|
-
const highlightIndex = React.useCallback(index => setHighlight(current => current.
|
|
4163
|
+
const highlightIndex = React.useCallback(index => setHighlight(current => current.list === list && current.index === index ? current : {
|
|
3626
4164
|
index,
|
|
3627
|
-
|
|
3628
|
-
}), [
|
|
4165
|
+
list
|
|
4166
|
+
}), [list]);
|
|
3629
4167
|
const handleKeyDown = event => {
|
|
3630
4168
|
const step = verticalStep(event.key);
|
|
3631
|
-
if (step &&
|
|
4169
|
+
if (step && options.length > 0) {
|
|
3632
4170
|
event.preventDefault();
|
|
3633
|
-
highlightIndex((highlightedIndex + step +
|
|
4171
|
+
highlightIndex((highlightedIndex + step + options.length) % options.length);
|
|
3634
4172
|
} else if (event.key === "Enter" && highlighted && !event.nativeEvent.isComposing) {
|
|
3635
4173
|
event.preventDefault();
|
|
3636
|
-
open(highlighted.
|
|
4174
|
+
open(highlighted.url, {
|
|
3637
4175
|
newTab: hasCommandKey(event)
|
|
3638
4176
|
});
|
|
3639
4177
|
} else if (isCopyShortcut(event) && highlighted && !hasSelectedText(event.currentTarget)) {
|
|
3640
4178
|
event.preventDefault();
|
|
3641
4179
|
const {
|
|
3642
4180
|
url
|
|
3643
|
-
} = highlighted
|
|
4181
|
+
} = highlighted;
|
|
3644
4182
|
navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
|
|
3645
4183
|
}
|
|
3646
4184
|
};
|
|
3647
4185
|
return {
|
|
3648
|
-
copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.
|
|
4186
|
+
copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.url),
|
|
3649
4187
|
handleKeyDown,
|
|
3650
4188
|
highlightIndex,
|
|
3651
4189
|
highlighted,
|
|
@@ -3670,29 +4208,66 @@ function hasSelectedText(element) {
|
|
|
3670
4208
|
}
|
|
3671
4209
|
|
|
3672
4210
|
const SEARCHABLE_PRODUCTS = [{
|
|
4211
|
+
appName: "Calendar",
|
|
4212
|
+
slug: "calendar"
|
|
4213
|
+
}, {
|
|
4214
|
+
appName: "Check-Ins",
|
|
4215
|
+
slug: "check-ins"
|
|
4216
|
+
}, {
|
|
4217
|
+
appName: "Giving",
|
|
4218
|
+
slug: "giving"
|
|
4219
|
+
}, {
|
|
3673
4220
|
appName: "Groups",
|
|
3674
4221
|
slug: "groups"
|
|
3675
4222
|
}, {
|
|
3676
4223
|
appName: "People",
|
|
3677
4224
|
slug: "people"
|
|
4225
|
+
}, {
|
|
4226
|
+
appName: "Publishing",
|
|
4227
|
+
slug: "publishing"
|
|
4228
|
+
}, {
|
|
4229
|
+
appName: "Registrations",
|
|
4230
|
+
slug: "registrations"
|
|
3678
4231
|
}, {
|
|
3679
4232
|
appName: "Services",
|
|
3680
4233
|
slug: "services"
|
|
3681
4234
|
}];
|
|
3682
4235
|
const PERMISSION_CHECKS = {
|
|
4236
|
+
"check-ins": () => fetchMe("check-ins", "permission").then(({
|
|
4237
|
+
permission
|
|
4238
|
+
}) => permission !== null),
|
|
4239
|
+
giving: personId => {
|
|
4240
|
+
if (personId === undefined) {
|
|
4241
|
+
return Promise.reject(new Error("No person to check Giving access for"));
|
|
4242
|
+
} else {
|
|
4243
|
+
return fetchAttributes(`giving/v2/person_permissions/${personId}`, {
|
|
4244
|
+
"fields[PersonPermissions]": "can_search"
|
|
4245
|
+
}).then(({
|
|
4246
|
+
can_search
|
|
4247
|
+
}) => can_search);
|
|
4248
|
+
}
|
|
4249
|
+
},
|
|
3683
4250
|
people: () => fetchMe("people", "people_permissions,site_administrator").then(({
|
|
3684
4251
|
people_permissions,
|
|
3685
4252
|
site_administrator
|
|
3686
4253
|
}) => site_administrator || people_permissions !== null),
|
|
4254
|
+
registrations: () => fetchMe("registrations", "can_search").then(({
|
|
4255
|
+
can_search
|
|
4256
|
+
}) => can_search),
|
|
3687
4257
|
services: () => fetchMe("services", "can_search").then(({
|
|
3688
4258
|
can_search
|
|
3689
4259
|
}) => can_search)
|
|
3690
4260
|
};
|
|
3691
4261
|
function fetchMe(product, fields) {
|
|
3692
|
-
|
|
4262
|
+
return fetchAttributes(`${product}/v2/me`, {
|
|
3693
4263
|
"fields[Person]": fields
|
|
3694
4264
|
});
|
|
3695
|
-
|
|
4265
|
+
}
|
|
4266
|
+
function fetchAttributes(path, params) {
|
|
4267
|
+
const url = `${window.location.origin}/~api/${path}?${new URLSearchParams(params)}`;
|
|
4268
|
+
return apiRequest(defaultFetch, url, {
|
|
4269
|
+
headers: apiVersionHeaders(url)
|
|
4270
|
+
}).then(({
|
|
3696
4271
|
json,
|
|
3697
4272
|
ok
|
|
3698
4273
|
}) => {
|
|
@@ -3703,7 +4278,7 @@ function fetchMe(product, fields) {
|
|
|
3703
4278
|
// Permissions rarely change mid-session, so a successful check is kept until
|
|
3704
4279
|
// the page reloads. A failed one is dropped so a remount tries again.
|
|
3705
4280
|
const permissionCache = new Map();
|
|
3706
|
-
function canSearch(slug) {
|
|
4281
|
+
function canSearch(slug, personId) {
|
|
3707
4282
|
const check = PERMISSION_CHECKS[slug];
|
|
3708
4283
|
const cached = permissionCache.get(slug);
|
|
3709
4284
|
if (!check) {
|
|
@@ -3711,7 +4286,7 @@ function canSearch(slug) {
|
|
|
3711
4286
|
} else if (cached) {
|
|
3712
4287
|
return cached;
|
|
3713
4288
|
} else {
|
|
3714
|
-
const promise = check().catch(() => {
|
|
4289
|
+
const promise = check(personId).catch(() => {
|
|
3715
4290
|
permissionCache.delete(slug);
|
|
3716
4291
|
return false;
|
|
3717
4292
|
});
|
|
@@ -3725,16 +4300,18 @@ function useSearchableProducts(apps) {
|
|
|
3725
4300
|
const accessibleKey = inSwitcherOrder(apps).map(({
|
|
3726
4301
|
slug
|
|
3727
4302
|
}) => slug).join(",");
|
|
4303
|
+
const personId = useCurrentPersonId();
|
|
3728
4304
|
const [settled, setSettled] = React.useState();
|
|
3729
4305
|
React.useEffect(() => {
|
|
3730
4306
|
let current = true;
|
|
3731
4307
|
const accessible = accessibleKey.split(",").flatMap(slug => SEARCHABLE_PRODUCTS.filter(product => product.slug === slug));
|
|
3732
4308
|
Promise.all(accessible.map(({
|
|
3733
4309
|
slug
|
|
3734
|
-
}) => canSearch(slug))).then(allowed => {
|
|
4310
|
+
}) => canSearch(slug, personId))).then(allowed => {
|
|
3735
4311
|
if (current) {
|
|
3736
4312
|
setSettled({
|
|
3737
4313
|
accessibleKey,
|
|
4314
|
+
personId,
|
|
3738
4315
|
products: accessible.filter((_, index) => allowed[index])
|
|
3739
4316
|
});
|
|
3740
4317
|
}
|
|
@@ -3742,8 +4319,8 @@ function useSearchableProducts(apps) {
|
|
|
3742
4319
|
return () => {
|
|
3743
4320
|
current = false;
|
|
3744
4321
|
};
|
|
3745
|
-
}, [accessibleKey]);
|
|
3746
|
-
if ((settled === null || settled === void 0 ? void 0 : settled.accessibleKey) === accessibleKey) {
|
|
4322
|
+
}, [accessibleKey, personId]);
|
|
4323
|
+
if ((settled === null || settled === void 0 ? void 0 : settled.accessibleKey) === accessibleKey && settled.personId === personId) {
|
|
3747
4324
|
return {
|
|
3748
4325
|
products: settled.products,
|
|
3749
4326
|
status: "done"
|
|
@@ -3764,11 +4341,13 @@ const isLegacySearchShortcut = event => event.key === "/" && !event.altKey && !e
|
|
|
3764
4341
|
const isTypingKey = event => event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey;
|
|
3765
4342
|
const isTypingIn = target => target instanceof Element && target.closest('input, select, textarea, [contenteditable]:not([contenteditable="false"])') !== null;
|
|
3766
4343
|
function ProductSearch({
|
|
3767
|
-
apps
|
|
4344
|
+
apps,
|
|
4345
|
+
env
|
|
3768
4346
|
}) {
|
|
3769
4347
|
const searchable = useSearchableProducts(apps);
|
|
3770
4348
|
if (searchable.status === "done" && searchable.products.length > 0) {
|
|
3771
4349
|
return /*#__PURE__*/React.createElement(SearchButtonAndDialog, {
|
|
4350
|
+
env: env,
|
|
3772
4351
|
products: searchable.products
|
|
3773
4352
|
});
|
|
3774
4353
|
} else {
|
|
@@ -3776,6 +4355,7 @@ function ProductSearch({
|
|
|
3776
4355
|
}
|
|
3777
4356
|
}
|
|
3778
4357
|
function SearchButtonAndDialog({
|
|
4358
|
+
env,
|
|
3779
4359
|
products
|
|
3780
4360
|
}) {
|
|
3781
4361
|
const [open, setOpen] = React.useState(false);
|
|
@@ -3813,10 +4393,15 @@ function SearchButtonAndDialog({
|
|
|
3813
4393
|
onClose: () => setOpen(false),
|
|
3814
4394
|
open: open
|
|
3815
4395
|
}, /*#__PURE__*/React.createElement(SearchPanel, {
|
|
4396
|
+
env: env,
|
|
3816
4397
|
products: products
|
|
3817
4398
|
})));
|
|
3818
4399
|
}
|
|
4400
|
+
// Keys the recents list for navigation, so the highlight stays put as more
|
|
4401
|
+
// recents load and as their lookups answer.
|
|
4402
|
+
const RECENTS = "recents";
|
|
3819
4403
|
function SearchPanel({
|
|
4404
|
+
env,
|
|
3820
4405
|
products
|
|
3821
4406
|
}) {
|
|
3822
4407
|
var _a, _b;
|
|
@@ -3833,7 +4418,23 @@ function SearchPanel({
|
|
|
3833
4418
|
var _a;
|
|
3834
4419
|
return (_a = groups === null || groups === void 0 ? void 0 : groups.flatMap(group => group.results)) !== null && _a !== void 0 ? _a : [];
|
|
3835
4420
|
}, [groups]);
|
|
3836
|
-
const
|
|
4421
|
+
const recents = useRecentVisits(env);
|
|
4422
|
+
const options = hasTerm ? results.map(({
|
|
4423
|
+
attributes
|
|
4424
|
+
}) => ({
|
|
4425
|
+
label: attributes.title,
|
|
4426
|
+
url: attributes.url
|
|
4427
|
+
})) : recents.visits.map(visit => {
|
|
4428
|
+
const {
|
|
4429
|
+
title,
|
|
4430
|
+
url
|
|
4431
|
+
} = describeVisit(visit);
|
|
4432
|
+
return {
|
|
4433
|
+
label: title,
|
|
4434
|
+
url
|
|
4435
|
+
};
|
|
4436
|
+
});
|
|
4437
|
+
const navigation = useResultNavigation(options, hasTerm ? results : RECENTS);
|
|
3837
4438
|
const {
|
|
3838
4439
|
highlightedIndex
|
|
3839
4440
|
} = navigation;
|
|
@@ -3859,8 +4460,8 @@ function SearchPanel({
|
|
|
3859
4460
|
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("input", {
|
|
3860
4461
|
"aria-activedescendant": highlightedIndex === null ? undefined : resultId(listboxId, highlightedIndex),
|
|
3861
4462
|
"aria-autocomplete": "list",
|
|
3862
|
-
"aria-controls":
|
|
3863
|
-
"aria-expanded":
|
|
4463
|
+
"aria-controls": options.length > 0 ? listboxId : undefined,
|
|
4464
|
+
"aria-expanded": options.length > 0,
|
|
3864
4465
|
"aria-label": "Search",
|
|
3865
4466
|
autoFocus: true,
|
|
3866
4467
|
onChange: event => setTerm(event.target.value),
|
|
@@ -3876,17 +4477,22 @@ function SearchPanel({
|
|
|
3876
4477
|
onSelect: setSelectedSlug,
|
|
3877
4478
|
products: products,
|
|
3878
4479
|
selected: product
|
|
3879
|
-
}, /*#__PURE__*/React.createElement(SearchResults, {
|
|
4480
|
+
}, hasTerm ? (/*#__PURE__*/React.createElement(SearchResults, {
|
|
3880
4481
|
highlightedIndex: highlightedIndex,
|
|
3881
4482
|
id: listboxId,
|
|
3882
4483
|
label: `${product.appName} results`,
|
|
3883
4484
|
onHighlight: navigation.highlightIndex,
|
|
3884
4485
|
search: search,
|
|
3885
4486
|
term: term
|
|
3886
|
-
}))
|
|
4487
|
+
})) : (/*#__PURE__*/React.createElement(RecentlyVisited, {
|
|
4488
|
+
highlightedIndex: highlightedIndex,
|
|
4489
|
+
id: listboxId,
|
|
4490
|
+
onHighlight: navigation.highlightIndex,
|
|
4491
|
+
...recents
|
|
4492
|
+
}))), /*#__PURE__*/React.createElement("div", {
|
|
3887
4493
|
"aria-live": "polite",
|
|
3888
4494
|
className: styles$c.visuallyHidden
|
|
3889
|
-
}, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.
|
|
4495
|
+
}, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.label : null), /*#__PURE__*/React.createElement(KeyboardHints, {
|
|
3890
4496
|
copied: navigation.copied
|
|
3891
4497
|
}));
|
|
3892
4498
|
}
|
|
@@ -3905,249 +4511,6 @@ function initialProduct(products) {
|
|
|
3905
4511
|
return (_c = (_b = (_a = bySlug(lastProduct)) !== null && _a !== void 0 ? _a : bySlug("people")) !== null && _b !== void 0 ? _b : bySlug("services")) !== null && _c !== void 0 ? _c : products[0];
|
|
3906
4512
|
}
|
|
3907
4513
|
|
|
3908
|
-
function parseValue(element) {
|
|
3909
|
-
const datatype = element.getAttribute("datatype");
|
|
3910
|
-
switch (datatype) {
|
|
3911
|
-
case "boolean":
|
|
3912
|
-
return element.content === "true" || element.content === "1";
|
|
3913
|
-
default:
|
|
3914
|
-
return element.content;
|
|
3915
|
-
}
|
|
3916
|
-
}
|
|
3917
|
-
function getAllPcoMeta() {
|
|
3918
|
-
const elements = document.head.querySelectorAll(`meta[property^="pco:"]`);
|
|
3919
|
-
const meta = {};
|
|
3920
|
-
elements.forEach(e => {
|
|
3921
|
-
const property = e.getAttribute("property");
|
|
3922
|
-
meta[property.substring("pco:".length)] = parseValue(e);
|
|
3923
|
-
});
|
|
3924
|
-
return meta;
|
|
3925
|
-
}
|
|
3926
|
-
// usePcoMeta only reads each tag's content. Recently Visited also needs the
|
|
3927
|
-
// pco:recently_visitable tag's `metadata` attribute, which can change while
|
|
3928
|
-
// the content stays the same, so it is tracked in its own store.
|
|
3929
|
-
function getRecentlyVisitableMetadata() {
|
|
3930
|
-
var _a, _b;
|
|
3931
|
-
return (_b = (_a = document.head.querySelector(`meta[property="pco:recently_visitable"]`)) === null || _a === void 0 ? void 0 : _a.getAttribute("metadata")) !== null && _b !== void 0 ? _b : null;
|
|
3932
|
-
}
|
|
3933
|
-
const store = createStore(getAllPcoMeta());
|
|
3934
|
-
const recentlyVisitableMetadataStore = createStore({
|
|
3935
|
-
metadata: getRecentlyVisitableMetadata()
|
|
3936
|
-
});
|
|
3937
|
-
function sync() {
|
|
3938
|
-
const nextState = getAllPcoMeta();
|
|
3939
|
-
if (!shallowCompare(store.getState(), nextState)) store.setState(nextState);
|
|
3940
|
-
const metadata = getRecentlyVisitableMetadata();
|
|
3941
|
-
if (recentlyVisitableMetadataStore.getState().metadata !== metadata) {
|
|
3942
|
-
recentlyVisitableMetadataStore.setState({
|
|
3943
|
-
metadata
|
|
3944
|
-
});
|
|
3945
|
-
}
|
|
3946
|
-
}
|
|
3947
|
-
const observer = new MutationObserver(sync);
|
|
3948
|
-
let hasTriggeredObserve = false;
|
|
3949
|
-
function observe() {
|
|
3950
|
-
if (hasTriggeredObserve) return;
|
|
3951
|
-
hasTriggeredObserve = true;
|
|
3952
|
-
// The module-load snapshot above can predate meta tags that appear later
|
|
3953
|
-
// in <head> (e.g. pco:resource, yielded right before </head>) if this
|
|
3954
|
-
// module's own script tag isn't deferred and runs mid-parse. Those tags
|
|
3955
|
-
// never mutate afterward, so the observer alone would never catch them.
|
|
3956
|
-
// Re-read once now, since observe() is only called once a consumer is
|
|
3957
|
-
// actually mounting, which is always after the document has fully parsed.
|
|
3958
|
-
sync();
|
|
3959
|
-
observer.observe(document.head, {
|
|
3960
|
-
attributes: true,
|
|
3961
|
-
childList: true,
|
|
3962
|
-
subtree: true
|
|
3963
|
-
});
|
|
3964
|
-
}
|
|
3965
|
-
function usePcoMeta() {
|
|
3966
|
-
observe();
|
|
3967
|
-
return shim.useSyncExternalStore(store.subscribe, () => store.getState());
|
|
3968
|
-
}
|
|
3969
|
-
function useRecentlyVisitableMetadata() {
|
|
3970
|
-
observe();
|
|
3971
|
-
return shim.useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
|
|
3972
|
-
}
|
|
3973
|
-
|
|
3974
|
-
const MAX_ENTRIES = 20;
|
|
3975
|
-
const FORMAT_VERSION = 1;
|
|
3976
|
-
const MAX_TEXT_LENGTH = 200;
|
|
3977
|
-
const PRODUCTS = ["accounts", "calendar", "check-ins", "giving", "groups", "home", "login", "people", "publishing", "registrations", "services"];
|
|
3978
|
-
function isId(value) {
|
|
3979
|
-
return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
|
|
3980
|
-
}
|
|
3981
|
-
// A PCRL looks like `<product>/v2/<resource_type>/<id>`.
|
|
3982
|
-
function isPcrl(value) {
|
|
3983
|
-
const segments = value.split("/");
|
|
3984
|
-
return segments.length === 4 && PRODUCTS.some(product => product === segments[0]) && isId(segments[3]);
|
|
3985
|
-
}
|
|
3986
|
-
function isNonBlankString(value) {
|
|
3987
|
-
return typeof value === "string" && value.trim() !== "";
|
|
3988
|
-
}
|
|
3989
|
-
function isRecord(value) {
|
|
3990
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3991
|
-
}
|
|
3992
|
-
function entryKey(entry) {
|
|
3993
|
-
if (entry.type === "resource") {
|
|
3994
|
-
return `resource:${entry.pcrl}`;
|
|
3995
|
-
} else {
|
|
3996
|
-
return `page:${entry.product}:${entry.path}`;
|
|
3997
|
-
}
|
|
3998
|
-
}
|
|
3999
|
-
function isEntry(value) {
|
|
4000
|
-
if (!isRecord(value)) {
|
|
4001
|
-
return false;
|
|
4002
|
-
} else if (value.type === "resource") {
|
|
4003
|
-
return typeof value.pcrl === "string" && isPcrl(value.pcrl);
|
|
4004
|
-
} else if (value.type === "page") {
|
|
4005
|
-
return isNonBlankString(value.product) && isNonBlankString(value.title) && isNonBlankString(value.path) && typeof value.details === "string";
|
|
4006
|
-
} else {
|
|
4007
|
-
return false;
|
|
4008
|
-
}
|
|
4009
|
-
}
|
|
4010
|
-
// Cuts on code points so a surrogate pair is never split.
|
|
4011
|
-
function clampText(value) {
|
|
4012
|
-
return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
|
|
4013
|
-
}
|
|
4014
|
-
function clampEntry(entry) {
|
|
4015
|
-
if (entry.type === "resource") {
|
|
4016
|
-
return entry;
|
|
4017
|
-
} else {
|
|
4018
|
-
return {
|
|
4019
|
-
...entry,
|
|
4020
|
-
details: clampText(entry.details),
|
|
4021
|
-
title: clampText(entry.title)
|
|
4022
|
-
};
|
|
4023
|
-
}
|
|
4024
|
-
}
|
|
4025
|
-
// Recents are disposable: anything unexpected reads as an empty list (or, for
|
|
4026
|
-
// a single bad entry, a list without it) rather than throwing.
|
|
4027
|
-
function parseRecentlyVisited(value, personId) {
|
|
4028
|
-
let parsed = null;
|
|
4029
|
-
try {
|
|
4030
|
-
parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
|
|
4031
|
-
} catch {
|
|
4032
|
-
parsed = null;
|
|
4033
|
-
}
|
|
4034
|
-
if (isRecord(parsed) && parsed.v === FORMAT_VERSION && parsed.person === personId && Array.isArray(parsed.entries)) {
|
|
4035
|
-
return parsed.entries.filter(isEntry);
|
|
4036
|
-
} else {
|
|
4037
|
-
return [];
|
|
4038
|
-
}
|
|
4039
|
-
}
|
|
4040
|
-
function addVisit(entries, visit) {
|
|
4041
|
-
const key = entryKey(visit);
|
|
4042
|
-
return [clampEntry(visit), ...entries.filter(entry => entryKey(entry) !== key)].slice(0, MAX_ENTRIES);
|
|
4043
|
-
}
|
|
4044
|
-
function serializeRecentlyVisited(entries, personId) {
|
|
4045
|
-
return JSON.stringify({
|
|
4046
|
-
entries,
|
|
4047
|
-
person: personId,
|
|
4048
|
-
v: FORMAT_VERSION
|
|
4049
|
-
});
|
|
4050
|
-
}
|
|
4051
|
-
|
|
4052
|
-
// The product whose URL, in this environment, has the current page's host.
|
|
4053
|
-
// Reversing pcoUrl handles every environment @planningcenter/url knows about,
|
|
4054
|
-
// and guarantees the stored product rebuilds a working link.
|
|
4055
|
-
function currentProduct() {
|
|
4056
|
-
var _a;
|
|
4057
|
-
return (_a = PRODUCTS.find(product => new URL(pcoUrl$1.pcoUrl(product)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
|
|
4058
|
-
}
|
|
4059
|
-
function detailsFromMetadata(metadata) {
|
|
4060
|
-
var _a;
|
|
4061
|
-
try {
|
|
4062
|
-
const details = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) === null || _a === void 0 ? void 0 : _a.details;
|
|
4063
|
-
return typeof details === "string" ? details : "";
|
|
4064
|
-
} catch {
|
|
4065
|
-
return "";
|
|
4066
|
-
}
|
|
4067
|
-
}
|
|
4068
|
-
// pco:resource is the more specific signal, so it wins when both tags are
|
|
4069
|
-
// present. A resource that isn't a PCRL is ignored rather than recorded.
|
|
4070
|
-
function visitFromPage({
|
|
4071
|
-
metadata,
|
|
4072
|
-
resource,
|
|
4073
|
-
title
|
|
4074
|
-
}) {
|
|
4075
|
-
const product = currentProduct();
|
|
4076
|
-
if (resource && isPcrl(resource)) {
|
|
4077
|
-
return {
|
|
4078
|
-
pcrl: resource,
|
|
4079
|
-
type: "resource"
|
|
4080
|
-
};
|
|
4081
|
-
} else if (title && title.trim() !== "" && product) {
|
|
4082
|
-
return {
|
|
4083
|
-
details: detailsFromMetadata(metadata),
|
|
4084
|
-
path: window.location.pathname,
|
|
4085
|
-
product,
|
|
4086
|
-
title,
|
|
4087
|
-
type: "page"
|
|
4088
|
-
};
|
|
4089
|
-
} else {
|
|
4090
|
-
return null;
|
|
4091
|
-
}
|
|
4092
|
-
}
|
|
4093
|
-
// The entry Recently Visited would record for the current page, or null when
|
|
4094
|
-
// the page isn't eligible. Shared by the recorder and the debug panel so the
|
|
4095
|
-
// panel always shows what actually gets stored.
|
|
4096
|
-
function useCurrentVisit() {
|
|
4097
|
-
const {
|
|
4098
|
-
recently_visitable: title,
|
|
4099
|
-
resource
|
|
4100
|
-
} = usePcoMeta();
|
|
4101
|
-
const metadata = useRecentlyVisitableMetadata();
|
|
4102
|
-
return React.useMemo(() => visitFromPage({
|
|
4103
|
-
metadata,
|
|
4104
|
-
resource,
|
|
4105
|
-
title
|
|
4106
|
-
}), [metadata, resource, title]);
|
|
4107
|
-
}
|
|
4108
|
-
// Products hand the person's ID to AppsProvider, not to the topbar itself,
|
|
4109
|
-
// and several pass it as a number.
|
|
4110
|
-
function useCurrentPersonId() {
|
|
4111
|
-
const {
|
|
4112
|
-
userId
|
|
4113
|
-
} = useStore();
|
|
4114
|
-
return userId == null ? undefined : String(userId);
|
|
4115
|
-
}
|
|
4116
|
-
|
|
4117
|
-
// Per-origin localStorage is a stopgap: each product keeps its own list until
|
|
4118
|
-
// PCA-409 moves this to storage shared across product subdomains. Only this
|
|
4119
|
-
// file should need to change when it does.
|
|
4120
|
-
function storageKey({
|
|
4121
|
-
env,
|
|
4122
|
-
personId
|
|
4123
|
-
}) {
|
|
4124
|
-
return `topbar.recently_visited.${env}.${personId}`;
|
|
4125
|
-
}
|
|
4126
|
-
// localStorage doesn't notify the tab that wrote it, so readers in this tab
|
|
4127
|
-
// learn about new visits from this counter instead.
|
|
4128
|
-
const changes = createStore({
|
|
4129
|
-
version: 0
|
|
4130
|
-
});
|
|
4131
|
-
function readRecentlyVisited(scope) {
|
|
4132
|
-
try {
|
|
4133
|
-
return parseRecentlyVisited(localStorage.getItem(storageKey(scope)), scope.personId);
|
|
4134
|
-
} catch {
|
|
4135
|
-
return [];
|
|
4136
|
-
}
|
|
4137
|
-
}
|
|
4138
|
-
function recordVisit(visit, scope) {
|
|
4139
|
-
if (isEntry(visit)) {
|
|
4140
|
-
try {
|
|
4141
|
-
localStorage.setItem(storageKey(scope), serializeRecentlyVisited(addVisit(readRecentlyVisited(scope), visit), scope.personId));
|
|
4142
|
-
changes.setState({
|
|
4143
|
-
version: changes.getState().version + 1
|
|
4144
|
-
});
|
|
4145
|
-
} catch {
|
|
4146
|
-
// Storage can be full or blocked; recents are disposable.
|
|
4147
|
-
}
|
|
4148
|
-
}
|
|
4149
|
-
}
|
|
4150
|
-
|
|
4151
4514
|
function useRecordRecentlyVisited({
|
|
4152
4515
|
enabled,
|
|
4153
4516
|
env
|
|
@@ -4437,7 +4800,8 @@ const NotSmallTopbar = ({
|
|
|
4437
4800
|
margin: "auto"
|
|
4438
4801
|
}
|
|
4439
4802
|
}), !notificationsEnabled && notifications(), productSearchEnabled ? (/*#__PURE__*/React.createElement(ProductSearch, {
|
|
4440
|
-
apps: props.apps
|
|
4803
|
+
apps: props.apps,
|
|
4804
|
+
env: props.env
|
|
4441
4805
|
})) : search({
|
|
4442
4806
|
hideRoutes: () => setRoutesVisible(false),
|
|
4443
4807
|
showRoutes: () => setRoutesVisible(true)
|
|
@@ -32570,7 +32934,7 @@ const SmallTopbar = ({
|
|
|
32570
32934
|
setProductSwitcherVisible(false);
|
|
32571
32935
|
setRoutesMenuVisible(visible => !visible);
|
|
32572
32936
|
} : null
|
|
32573
|
-
}, /*#__PURE__*/React.createElement(CurrentRouteComponent, null, capitalize(currentRouteText)), props.routes.length > 0 && /*#__PURE__*/React.createElement(DisclosureChevronIcon, null)), /*#__PURE__*/React.createElement("button", {
|
|
32937
|
+
}, /*#__PURE__*/React.createElement(CurrentRouteComponent, null, capitalize$1(currentRouteText)), props.routes.length > 0 && /*#__PURE__*/React.createElement(DisclosureChevronIcon, null)), /*#__PURE__*/React.createElement("button", {
|
|
32574
32938
|
type: "button",
|
|
32575
32939
|
className: styles$5["profile-button"],
|
|
32576
32940
|
onClick: () => {
|
|
@@ -32763,8 +33127,24 @@ function Field({
|
|
|
32763
33127
|
className: styles$4["debug-pco-resource-field-value"]
|
|
32764
33128
|
}, value));
|
|
32765
33129
|
}
|
|
33130
|
+
// Only staff ghosting or impersonating a person get pco:staff-access-level,
|
|
33131
|
+
// which keeps the panel away from regular users on staging.
|
|
32766
33132
|
function DebugPcoResource({
|
|
32767
33133
|
env
|
|
33134
|
+
}) {
|
|
33135
|
+
const {
|
|
33136
|
+
"staff-access-level": staffAccessLevel
|
|
33137
|
+
} = usePcoMeta();
|
|
33138
|
+
if (isPresent(staffAccessLevel)) {
|
|
33139
|
+
return /*#__PURE__*/React.createElement(DebugPanel, {
|
|
33140
|
+
env: env
|
|
33141
|
+
});
|
|
33142
|
+
} else {
|
|
33143
|
+
return null;
|
|
33144
|
+
}
|
|
33145
|
+
}
|
|
33146
|
+
function DebugPanel({
|
|
33147
|
+
env
|
|
32768
33148
|
}) {
|
|
32769
33149
|
const {
|
|
32770
33150
|
recently_visitable: recentlyVisitable,
|
|
@@ -32773,12 +33153,17 @@ function DebugPcoResource({
|
|
|
32773
33153
|
const [lookup, setLookup] = React.useState({
|
|
32774
33154
|
status: "idle"
|
|
32775
33155
|
});
|
|
32776
|
-
const
|
|
33156
|
+
const visit = useCurrentVisit();
|
|
33157
|
+
const personId = useCurrentPersonId();
|
|
33158
|
+
const recentlyVisited = useRecentlyVisited({
|
|
33159
|
+
env,
|
|
33160
|
+
personId
|
|
33161
|
+
});
|
|
33162
|
+
const pcrl = (visit === null || visit === void 0 ? void 0 : visit.type) === "resource" ? visit.pcrl : null;
|
|
32777
33163
|
const hasRecentlyVisitable = isPresent(recentlyVisitable);
|
|
33164
|
+
const hasInvalidResource = isPresent(resource) && !isPcrl(resource);
|
|
32778
33165
|
React.useEffect(() => {
|
|
32779
|
-
|
|
32780
|
-
// tags are present.
|
|
32781
|
-
const url = hasResource ? pcrlLookupUrl(resource, env) : null;
|
|
33166
|
+
const url = pcrl ? pcrlLookupUrl(pcrl, env) : null;
|
|
32782
33167
|
if (!url) {
|
|
32783
33168
|
setLookup({
|
|
32784
33169
|
status: "idle"
|
|
@@ -32810,7 +33195,7 @@ function DebugPcoResource({
|
|
|
32810
33195
|
return () => {
|
|
32811
33196
|
cancelled = true;
|
|
32812
33197
|
};
|
|
32813
|
-
}, [
|
|
33198
|
+
}, [pcrl, env]);
|
|
32814
33199
|
return /*#__PURE__*/React.createElement("div", {
|
|
32815
33200
|
className: classnames("staff-only", styles$4["debug-pco-resource"])
|
|
32816
33201
|
}, /*#__PURE__*/React.createElement("div", {
|
|
@@ -32825,19 +33210,27 @@ function DebugPcoResource({
|
|
|
32825
33210
|
}), /*#__PURE__*/React.createElement(Field, {
|
|
32826
33211
|
label: "pco:recently_visitable",
|
|
32827
33212
|
value: describeTag("recently_visitable", recentlyVisitable)
|
|
32828
|
-
}),
|
|
33213
|
+
}), hasInvalidResource && (/*#__PURE__*/React.createElement("p", {
|
|
32829
33214
|
className: styles$4["debug-pco-resource-note"]
|
|
32830
|
-
}, "
|
|
33215
|
+
}, "pco:resource isn't a PCRL (product/v2/type/id), so Recently Visited ignores it.")), visit === null && (/*#__PURE__*/React.createElement("p", {
|
|
32831
33216
|
className: styles$4["debug-pco-resource-note"]
|
|
32832
|
-
}, "
|
|
33217
|
+
}, "This page is not eligible for Recently Visited")), pcrl && hasRecentlyVisitable && (/*#__PURE__*/React.createElement("p", {
|
|
32833
33218
|
className: styles$4["debug-pco-resource-note"]
|
|
32834
|
-
}, "
|
|
33219
|
+
}, "Both tags are present \u2014 pco:resource takes precedence for the Recently Visited entry.")), pcrl && lookup.status === "loading" && (/*#__PURE__*/React.createElement("p", {
|
|
33220
|
+
className: styles$4["debug-pco-resource-note"]
|
|
33221
|
+
}, "Looking up resource\u2026")), pcrl && lookup.status === "error" && (/*#__PURE__*/React.createElement("p", {
|
|
33222
|
+
className: styles$4["debug-pco-resource-note"]
|
|
33223
|
+
}, lookup.message)), pcrl && lookup.status === "loaded" && (/*#__PURE__*/React.createElement("pre", {
|
|
33224
|
+
className: styles$4["debug-pco-resource-json"]
|
|
33225
|
+
}, JSON.stringify(lookup.json, null, 2))), (visit === null || visit === void 0 ? void 0 : visit.type) === "page" && (/*#__PURE__*/React.createElement("p", {
|
|
32835
33226
|
className: styles$4["debug-pco-resource-note"]
|
|
32836
|
-
},
|
|
33227
|
+
}, "Recently Visited title: ", visit.title)), /*#__PURE__*/React.createElement("span", {
|
|
33228
|
+
className: styles$4["debug-pco-resource-field-label"]
|
|
33229
|
+
}, "Recently Visited"), personId ? (/*#__PURE__*/React.createElement("pre", {
|
|
32837
33230
|
className: styles$4["debug-pco-resource-json"]
|
|
32838
|
-
}, JSON.stringify(
|
|
33231
|
+
}, JSON.stringify(recentlyVisited, null, 2))) : (/*#__PURE__*/React.createElement("p", {
|
|
32839
33232
|
className: styles$4["debug-pco-resource-note"]
|
|
32840
|
-
}, "
|
|
33233
|
+
}, "No current person, so nothing is recorded.")));
|
|
32841
33234
|
}
|
|
32842
33235
|
|
|
32843
33236
|
var styles$3 = {"drawer-wrap":"drawer-wrap-J0rSL","is-visible":"is-visible-KTME8","drawer":"drawer-j7f4l"};
|
|
@@ -33042,15 +33435,50 @@ const SwitchView = ({
|
|
|
33042
33435
|
}));
|
|
33043
33436
|
};
|
|
33044
33437
|
|
|
33438
|
+
const collections = {
|
|
33439
|
+
general: general__namespace,
|
|
33440
|
+
toolbar: toolbar__namespace
|
|
33441
|
+
};
|
|
33442
|
+
const paths = collections;
|
|
33443
|
+
function Icon({
|
|
33444
|
+
name,
|
|
33445
|
+
size = 16,
|
|
33446
|
+
style,
|
|
33447
|
+
theme,
|
|
33448
|
+
title,
|
|
33449
|
+
...props
|
|
33450
|
+
}) {
|
|
33451
|
+
const [collection, symbol] = name.split(".");
|
|
33452
|
+
return /*#__PURE__*/React.createElement("svg", {
|
|
33453
|
+
"aria-hidden": title ? undefined : true,
|
|
33454
|
+
fill: "currentColor",
|
|
33455
|
+
height: size,
|
|
33456
|
+
role: title ? "img" : undefined,
|
|
33457
|
+
style: {
|
|
33458
|
+
color: theme && `var(${theme})`,
|
|
33459
|
+
flexShrink: 0,
|
|
33460
|
+
...style
|
|
33461
|
+
},
|
|
33462
|
+
viewBox: "0 0 16 16",
|
|
33463
|
+
width: size,
|
|
33464
|
+
...props
|
|
33465
|
+
}, title && /*#__PURE__*/React.createElement("title", null, title), /*#__PURE__*/React.createElement("path", {
|
|
33466
|
+
d: paths[collection][symbol]
|
|
33467
|
+
}));
|
|
33468
|
+
}
|
|
33469
|
+
|
|
33045
33470
|
var styles$2 = {"color-mode-switcher":"color-mode-switcher-JpczO","color-mode-switcher-label":"color-mode-switcher-label-3jPM2","color-mode-switcher-modes":"color-mode-switcher-modes-u-bic"};
|
|
33046
33471
|
|
|
33047
33472
|
const COLOR_MODES = [{
|
|
33473
|
+
icon: "toolbar.sun",
|
|
33048
33474
|
label: "Light",
|
|
33049
33475
|
value: "light"
|
|
33050
33476
|
}, {
|
|
33477
|
+
icon: "toolbar.moon",
|
|
33051
33478
|
label: "Dark",
|
|
33052
33479
|
value: "dark"
|
|
33053
33480
|
}, {
|
|
33481
|
+
icon: "toolbar.display",
|
|
33054
33482
|
label: "System",
|
|
33055
33483
|
value: "system"
|
|
33056
33484
|
}];
|
|
@@ -33068,8 +33496,9 @@ function ColorModeSwitcher() {
|
|
|
33068
33496
|
}, "Appearance", " "), /*#__PURE__*/React.createElement("div", {
|
|
33069
33497
|
className: styles$2["color-mode-switcher-modes"]
|
|
33070
33498
|
}, COLOR_MODES.map(({
|
|
33071
|
-
|
|
33072
|
-
label
|
|
33499
|
+
icon,
|
|
33500
|
+
label,
|
|
33501
|
+
value
|
|
33073
33502
|
}) => (/*#__PURE__*/React.createElement("label", {
|
|
33074
33503
|
key: value
|
|
33075
33504
|
}, /*#__PURE__*/React.createElement("input", {
|
|
@@ -33086,8 +33515,9 @@ function ColorModeSwitcher() {
|
|
|
33086
33515
|
document.documentElement.setAttribute(RESOLVED_COLOR_MODE_ATTRIBUTE, resolveColorMode(colorMode));
|
|
33087
33516
|
updateColorMode(colorMode);
|
|
33088
33517
|
}
|
|
33089
|
-
}), /*#__PURE__*/React.createElement(
|
|
33090
|
-
|
|
33518
|
+
}), /*#__PURE__*/React.createElement(Icon, {
|
|
33519
|
+
name: icon,
|
|
33520
|
+
size: 15
|
|
33091
33521
|
}), label)))));
|
|
33092
33522
|
}
|
|
33093
33523
|
function updateColorMode(mode) {
|
|
@@ -33102,28 +33532,6 @@ function updateColorMode(mode) {
|
|
|
33102
33532
|
method: "POST"
|
|
33103
33533
|
});
|
|
33104
33534
|
}
|
|
33105
|
-
const COLOR_MODE_ICON_PATHS = {
|
|
33106
|
-
dark: "M12.66667,9.07579 C11.90047,9.42229 11.04997,9.61514 10.15457,9.61514 C6.78392,9.61514 4.05151,6.88275 4.05151,3.51215 C4.05151,2.61663 4.24438,1.76615 4.59086,1 C2.47336,1.95762 1,4.08858 1,6.56366 C1,9.93423 3.73241,12.66663 7.103,12.66663 C9.57807,12.66663 11.70907,11.19333 12.66667,9.07579 Z",
|
|
33107
|
-
light: "M6.83333,1 L6.83333,2.16667 M6.83333,11.50003 L6.83333,12.66663 M2.16667,6.83334 L1,6.83334 M3.51657,3.51657 L2.69161,2.69161 M10.1501,3.51657 L10.9751,2.69161 M3.51657,10.15253 L2.69161,10.97743 M10.1501,10.15253 L10.9751,10.97743 M12.6667,6.83334 L11.5,6.83334 M9.75,6.83334 C9.75,8.44415 8.44415,9.75 6.83333,9.75 C5.2225,9.75 3.91667,8.44415 3.91667,6.83334 C3.91667,5.22251 5.2225,3.91667 6.83333,3.91667 C8.44415,3.91667 9.75,5.22251 9.75,6.83334 Z",
|
|
33108
|
-
system: "M4.75,12.50003 L9.41667,12.50003 M7.08334,10.16667 L7.08334,12.50003 M4.05,10.16667 L10.11665,10.16667 C11.09675,10.16667 11.58675,10.16667 11.96115,9.97592 C12.29045,9.80815 12.55815,9.54046 12.72595,9.21117 C12.91665,8.83678 12.91665,8.34678 12.91665,7.36667 L12.91665,4.8 C12.91665,3.81991 12.91665,3.32986 12.72595,2.95552 C12.55815,2.62623 12.29045,2.35852 11.96115,2.19074 C11.58675,2 11.09675,2 10.11665,2 L4.05,2 C3.06991,2 2.57986,2 2.20552,2.19074 C1.87623,2.35852 1.60852,2.62623 1.44074,2.95552 C1.25,3.32986 1.25,3.81991 1.25,4.8 L1.25,7.36667 C1.25,8.34678 1.25,8.83678 1.44074,9.21117 C1.60852,9.54046 1.87623,9.80815 2.20552,9.97592 C2.57986,10.16667 3.06991,10.16667 4.05,10.16667 Z"
|
|
33109
|
-
};
|
|
33110
|
-
function ColorModeIcon({
|
|
33111
|
-
mode
|
|
33112
|
-
}) {
|
|
33113
|
-
return /*#__PURE__*/React.createElement("svg", {
|
|
33114
|
-
width: "16",
|
|
33115
|
-
height: "16",
|
|
33116
|
-
viewBox: "0 0 14 14",
|
|
33117
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
33118
|
-
"aria-hidden": "true"
|
|
33119
|
-
}, /*#__PURE__*/React.createElement("path", {
|
|
33120
|
-
d: COLOR_MODE_ICON_PATHS[mode],
|
|
33121
|
-
strokeWidth: "1.5",
|
|
33122
|
-
fill: "none",
|
|
33123
|
-
strokeLinecap: "round",
|
|
33124
|
-
strokeLinejoin: "round"
|
|
33125
|
-
}));
|
|
33126
|
-
}
|
|
33127
33535
|
|
|
33128
33536
|
const requestClearConnectedPeopleCache = () => remove();
|
|
33129
33537
|
function UserProfile({
|
|
@@ -33186,10 +33594,10 @@ function UserProfile({
|
|
|
33186
33594
|
alignment: "center",
|
|
33187
33595
|
spacing: 0.5,
|
|
33188
33596
|
width: "100%"
|
|
33189
|
-
}, /*#__PURE__*/React.createElement(
|
|
33597
|
+
}, /*#__PURE__*/React.createElement(Icon, {
|
|
33190
33598
|
name: mfaConfigured ? "general.lock" : "general.unlock",
|
|
33191
|
-
size:
|
|
33192
|
-
|
|
33599
|
+
size: 12,
|
|
33600
|
+
theme: mfaConfigured ? "--t-icon-color-status-success" : "--t-icon-color-status-error"
|
|
33193
33601
|
}), /*#__PURE__*/React.createElement(tapestryReact.Text, {
|
|
33194
33602
|
size: 4,
|
|
33195
33603
|
weight: 400,
|
|
@@ -33237,9 +33645,10 @@ function UserProfile({
|
|
|
33237
33645
|
}, /*#__PURE__*/React.createElement(tapestryReact.Text, {
|
|
33238
33646
|
color: "var(--text-color-default-secondary)",
|
|
33239
33647
|
size: 3
|
|
33240
|
-
}, "Log out"), /*#__PURE__*/React.createElement(
|
|
33648
|
+
}, "Log out"), /*#__PURE__*/React.createElement(Icon, {
|
|
33241
33649
|
name: "general.intoBox",
|
|
33242
|
-
|
|
33650
|
+
size: 14,
|
|
33651
|
+
theme: "--t-icon-color-secondary"
|
|
33243
33652
|
})))), env !== "production" && /*#__PURE__*/React.createElement(DebugPcoResource, {
|
|
33244
33653
|
env: env
|
|
33245
33654
|
}), hasConnectedPeople && (/*#__PURE__*/React.createElement(tapestryReact.StackView, {
|
|
@@ -33301,26 +33710,42 @@ function UserProfile({
|
|
|
33301
33710
|
|
|
33302
33711
|
var ActionType;
|
|
33303
33712
|
(function (ActionType) {
|
|
33713
|
+
ActionType["clearPending"] = "clearPending";
|
|
33304
33714
|
ActionType["close"] = "close";
|
|
33305
33715
|
ActionType["mount"] = "mount";
|
|
33306
33716
|
ActionType["open"] = "open";
|
|
33717
|
+
ActionType["setPending"] = "setPending";
|
|
33307
33718
|
ActionType["unmount"] = "unmount";
|
|
33308
33719
|
})(ActionType || (ActionType = {}));
|
|
33309
33720
|
const initialState = {
|
|
33310
33721
|
currentContent: null,
|
|
33311
|
-
open: false
|
|
33722
|
+
open: false,
|
|
33723
|
+
pending: null
|
|
33312
33724
|
};
|
|
33313
33725
|
const reducer = (state, action) => {
|
|
33726
|
+
var _a, _b;
|
|
33314
33727
|
switch (action.type) {
|
|
33315
33728
|
case ActionType.mount:
|
|
33316
33729
|
return {
|
|
33317
33730
|
...state,
|
|
33318
|
-
currentContent: action.payload.content
|
|
33731
|
+
currentContent: action.payload.content,
|
|
33732
|
+
pending: ((_a = state.pending) === null || _a === void 0 ? void 0 : _a.tool) === ((_b = action.payload.content) === null || _b === void 0 ? void 0 : _b.name) ? state.pending : null
|
|
33319
33733
|
};
|
|
33320
33734
|
case ActionType.unmount:
|
|
33321
33735
|
return {
|
|
33322
33736
|
...state,
|
|
33323
|
-
currentContent: null
|
|
33737
|
+
currentContent: null,
|
|
33738
|
+
pending: null
|
|
33739
|
+
};
|
|
33740
|
+
case ActionType.setPending:
|
|
33741
|
+
return {
|
|
33742
|
+
...state,
|
|
33743
|
+
pending: action.payload
|
|
33744
|
+
};
|
|
33745
|
+
case ActionType.clearPending:
|
|
33746
|
+
return {
|
|
33747
|
+
...state,
|
|
33748
|
+
pending: null
|
|
33324
33749
|
};
|
|
33325
33750
|
case ActionType.open:
|
|
33326
33751
|
return {
|
|
@@ -33341,11 +33766,179 @@ const useToolbarReducer = overrides => React__namespace.useReducer(reducer, {
|
|
|
33341
33766
|
|
|
33342
33767
|
var styles$1 = {"toolbar":"toolbar-KHJR5","toolbar-button":"toolbar-button-Ye4h5","is-current":"is-current-DZK47","toolbar-button--avatar":"toolbar-button--avatar-5FUik","toolbar-button--home":"toolbar-button--home-Pthd0"};
|
|
33343
33768
|
|
|
33344
|
-
|
|
33769
|
+
// PCO Box serves TLS by default, and a bare http:// frame inside an https
|
|
33770
|
+
// product page is blocked as mixed content, so follow the page's scheme.
|
|
33771
|
+
const iframeSrc = () => `${window.location.protocol}//pco.test/iframe`;
|
|
33772
|
+
const STAFF_PINK = "var(--t-fill-color-product-staff)";
|
|
33773
|
+
// The pco.test domain is only ever served by a local PCO Box, so the tool
|
|
33774
|
+
// stays hidden in staging and production — and on Topbar's own dev server,
|
|
33775
|
+
// which runs on localhost and has no pco.test to frame.
|
|
33776
|
+
function isPcoTestHost() {
|
|
33777
|
+
const {
|
|
33778
|
+
hostname
|
|
33779
|
+
} = window.location;
|
|
33780
|
+
return hostname === "pco.test" || hostname.endsWith(".pco.test");
|
|
33781
|
+
}
|
|
33782
|
+
// Flattened from the source artwork's two outline paths — its gradients,
|
|
33783
|
+
// filters and clip paths carry nothing at icon size. Those paths are authored
|
|
33784
|
+
// on a 128px canvas, hence the transform: it fits the pair into the same ~24px
|
|
33785
|
+
// optical box the sibling toolbar icons occupy. Filled explicitly rather than
|
|
33786
|
+
// with `currentColor` so the mark stays staff pink instead of inheriting the
|
|
33787
|
+
// toolbar's text color and its hover/active states.
|
|
33788
|
+
const WRENCH = "m22.42 17.2 12.96 12.95-2.5 8.78-8.78 2.5-12.96-12.96-5.86 5.86 14.89 21.46 17.56-3.34 61.63 65.62c9.44 9.51 22.17-4.75 13.34-12.87L46.85 42.92l4.01-17.76-22.14-14.27-6.3 6.31zm81.06 91.97c1.68-1.68 4.4-1.68 6.08 0 1.68 1.68 1.68 4.4 0 6.08-1.68 1.67-4.4 1.67-6.08 0-1.68-1.68-1.68-4.4 0-6.08z";
|
|
33789
|
+
const SCREWDRIVER = "m55.8 12 .76.16c8.84-2.94 28.68 8.64 24.26 18.96-1.12 1.62-.93 4.5.42 6.31L56.43 62.25c-1.39-1.39-4.48-1.36-5.9 0L9.5 101.63c-3.74 3.59-.8 6.64.78 8.25l7.36 7.36c1.61 1.6 5.86 3.76 8.88.56l38.72-40.81c1.35-1.42 1.36-4.47 0-5.87 0 0 .01-.02 0-.02l24.91-24.9c2.63 2.37 5.11 2.57 7.47 1.93 4.55-1.25 7.09 3.21 9.81 6.46l-1.23 1.21c-1.82 1.84-1.65 4.94.39 6.98s5.15 2.21 6.97.39l8.14-8.13c1.82-1.84 1.65-4.94-.4-6.97-1.84-1.85-4.51-2.09-6.37-.77-5.58-6.26-2.65-7.2-10.01-15.35L91.13 17.86C81.2 8.12 68.3 8.13 55.8 12z";
|
|
33790
|
+
function ToolsIcon() {
|
|
33791
|
+
return /*#__PURE__*/React.createElement("svg", {
|
|
33792
|
+
"aria-hidden": "true",
|
|
33793
|
+
width: "32",
|
|
33794
|
+
height: "32",
|
|
33795
|
+
viewBox: "0 0 32 32",
|
|
33796
|
+
xmlns: "http://www.w3.org/2000/svg"
|
|
33797
|
+
}, /*#__PURE__*/React.createElement("g", {
|
|
33798
|
+
fill: STAFF_PINK,
|
|
33799
|
+
transform: "translate(2.92 2.66) scale(.204)"
|
|
33800
|
+
}, /*#__PURE__*/React.createElement("path", {
|
|
33801
|
+
d: WRENCH
|
|
33802
|
+
}), /*#__PURE__*/React.createElement("path", {
|
|
33803
|
+
d: SCREWDRIVER
|
|
33804
|
+
})));
|
|
33805
|
+
}
|
|
33806
|
+
// A cross-origin frame's own history object is unreachable (touching it throws
|
|
33807
|
+
// a SecurityError), but the frame's navigations do land in the top-level joint
|
|
33808
|
+
// session history, so the *parent's* back/forward traverse them while leaving
|
|
33809
|
+
// the host page in place. Counting frame loads tracks how deep we are, which is
|
|
33810
|
+
// what keeps a click on Back from stepping past the frame's first page and
|
|
33811
|
+
// navigating the host app instead. Loads we caused are attributed to the
|
|
33812
|
+
// traversal that requested them rather than counted as new navigations.
|
|
33813
|
+
function useFrameHistory() {
|
|
33814
|
+
const [steps, setSteps] = React.useState({
|
|
33815
|
+
back: 0,
|
|
33816
|
+
forward: 0
|
|
33817
|
+
});
|
|
33818
|
+
// Traversal has to read the current counts outside of render, so the ref is
|
|
33819
|
+
// the source of truth and state exists to paint the buttons.
|
|
33820
|
+
const stepsRef = React.useRef(steps);
|
|
33821
|
+
const pendingTraversal = React.useRef(null);
|
|
33822
|
+
const frameIsLoaded = React.useRef(false);
|
|
33823
|
+
const moveTo = React.useCallback(next => {
|
|
33824
|
+
stepsRef.current = next;
|
|
33825
|
+
setSteps(next);
|
|
33826
|
+
}, []);
|
|
33827
|
+
const onFrameLoad = React.useCallback(() => {
|
|
33828
|
+
if (!frameIsLoaded.current) {
|
|
33829
|
+
// The frame's first page is the floor, not a step.
|
|
33830
|
+
frameIsLoaded.current = true;
|
|
33831
|
+
} else {
|
|
33832
|
+
const traversal = pendingTraversal.current;
|
|
33833
|
+
pendingTraversal.current = null;
|
|
33834
|
+
const {
|
|
33835
|
+
back,
|
|
33836
|
+
forward
|
|
33837
|
+
} = stepsRef.current;
|
|
33838
|
+
if (traversal === "back") {
|
|
33839
|
+
moveTo({
|
|
33840
|
+
back: back - 1,
|
|
33841
|
+
forward: forward + 1
|
|
33842
|
+
});
|
|
33843
|
+
} else if (traversal === "forward") {
|
|
33844
|
+
moveTo({
|
|
33845
|
+
back: back + 1,
|
|
33846
|
+
forward: forward - 1
|
|
33847
|
+
});
|
|
33848
|
+
} else {
|
|
33849
|
+
moveTo({
|
|
33850
|
+
back: back + 1,
|
|
33851
|
+
forward: 0
|
|
33852
|
+
});
|
|
33853
|
+
}
|
|
33854
|
+
}
|
|
33855
|
+
}, [moveTo]);
|
|
33856
|
+
// Guarded here rather than trusting the button's disabled state: traversing
|
|
33857
|
+
// with no frame step left would navigate the host app out from under us.
|
|
33858
|
+
const traverse = React.useCallback(direction => {
|
|
33859
|
+
if (stepsRef.current[direction] > 0) {
|
|
33860
|
+
pendingTraversal.current = direction;
|
|
33861
|
+
window.history[direction]();
|
|
33862
|
+
}
|
|
33863
|
+
}, []);
|
|
33864
|
+
return {
|
|
33865
|
+
onFrameLoad,
|
|
33866
|
+
steps,
|
|
33867
|
+
traverse
|
|
33868
|
+
};
|
|
33869
|
+
}
|
|
33870
|
+
function NavButton({
|
|
33871
|
+
direction,
|
|
33872
|
+
disabled,
|
|
33873
|
+
onClick
|
|
33874
|
+
}) {
|
|
33875
|
+
return /*#__PURE__*/React.createElement(tapestryReact.Button
|
|
33876
|
+
// Tapestry's `disabled` suppresses the click but emits no ARIA state.
|
|
33877
|
+
, {
|
|
33878
|
+
"aria-disabled": disabled,
|
|
33879
|
+
disabled: disabled,
|
|
33880
|
+
hover: {
|
|
33881
|
+
backgroundColor: "var(--t-fill-color-neutral-070)"
|
|
33882
|
+
},
|
|
33883
|
+
icon: {
|
|
33884
|
+
fill: "var(--t-icon-color-dim)",
|
|
33885
|
+
name: direction === "back" ? "general.leftChevron" : "general.rightChevron",
|
|
33886
|
+
size: "md"
|
|
33887
|
+
},
|
|
33888
|
+
onClick: onClick,
|
|
33889
|
+
opacity: disabled ? 0.35 : 1,
|
|
33890
|
+
theme: false,
|
|
33891
|
+
title: direction === "back" ? "Back" : "Forward"
|
|
33892
|
+
});
|
|
33893
|
+
}
|
|
33894
|
+
function DeveloperToolsComponent({
|
|
33895
|
+
closeDrawer
|
|
33896
|
+
}) {
|
|
33897
|
+
const {
|
|
33898
|
+
onFrameLoad,
|
|
33899
|
+
steps,
|
|
33900
|
+
traverse
|
|
33901
|
+
} = useFrameHistory();
|
|
33902
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(DrawerHeader, {
|
|
33903
|
+
closeDrawer: closeDrawer
|
|
33904
|
+
}, /*#__PURE__*/React.createElement(tapestryReact.StackView, {
|
|
33905
|
+
alignment: "center",
|
|
33906
|
+
axis: "horizontal",
|
|
33907
|
+
spacing: 0.25
|
|
33908
|
+
}, /*#__PURE__*/React.createElement(NavButton, {
|
|
33909
|
+
direction: "back",
|
|
33910
|
+
disabled: steps.back === 0,
|
|
33911
|
+
onClick: () => traverse("back")
|
|
33912
|
+
}), /*#__PURE__*/React.createElement(NavButton, {
|
|
33913
|
+
direction: "forward",
|
|
33914
|
+
disabled: steps.forward === 0,
|
|
33915
|
+
onClick: () => traverse("forward")
|
|
33916
|
+
}), /*#__PURE__*/React.createElement(DrawerHeading, {
|
|
33917
|
+
paddingLeft: 0.5
|
|
33918
|
+
}, "Developer Tools"))), /*#__PURE__*/React.createElement(tapestryReact.Box, {
|
|
33919
|
+
as: "iframe",
|
|
33920
|
+
border: "none",
|
|
33921
|
+
flex: "1",
|
|
33922
|
+
minHeight: 0,
|
|
33923
|
+
onLoad: onFrameLoad,
|
|
33924
|
+
src: iframeSrc(),
|
|
33925
|
+
title: "Developer Tools",
|
|
33926
|
+
width: "100%"
|
|
33927
|
+
}));
|
|
33928
|
+
}
|
|
33929
|
+
const DeveloperTools = {
|
|
33930
|
+
component: DeveloperToolsComponent,
|
|
33931
|
+
icons: {
|
|
33932
|
+
component: ToolsIcon
|
|
33933
|
+
},
|
|
33934
|
+
label: "Developer Tools",
|
|
33935
|
+
// No space: `name` becomes an element id and a pendo attribute.
|
|
33936
|
+
name: "DeveloperTools"
|
|
33937
|
+
};
|
|
33345
33938
|
|
|
33346
33939
|
var styles = {"banner":"banner-YfQ37","stacked":"stacked-EwLRh","content":"content-lNjWf","icon":"icon-abz7q","iconShape":"iconShape-6uyot","iconSparkle":"iconSparkle-t0OzK","text":"text--Vlhl","title":"title-zOEOR","subtitle":"subtitle-ufLnk","chevron":"chevron-oOUA-","inlineContent":"inlineContent-h5sVl","sparkle":"sparkle-uuMBp","inlineTitle":"inlineTitle-ru2Rk","action":"action-fSY3n","actionLabel":"actionLabel-MRBjb","actionChevron":"actionChevron-DghlN"};
|
|
33347
33940
|
|
|
33348
|
-
const SPARKLE_PATH = "M3.809 10.1332C3.87511 9.95596 4.12579 9.95588 4.19181 10.1332L4.63712 11.3363C4.64169 11.3487 4.65116 11.359 4.66349 11.3637L5.86661 11.809C6.04452 11.8749 6.04455 12.126 5.86661 12.1918L4.66349 12.6371C4.65134 12.6417 4.64173 12.6513 4.63712 12.6635L4.19181 13.8666C4.12597 14.0445 3.87486 14.0445 3.809 13.8666L3.36368 12.6635C3.35905 12.6512 3.3487 12.6417 3.33634 12.6371L2.13321 12.1918C1.95588 12.1258 1.95596 11.8751 2.13321 11.809L3.33634 11.3637C3.34877 11.3591 3.35908 11.3488 3.36368 11.3363L3.809 10.1332ZM8.52091 3.33341C8.68571 2.88899 9.31518 2.88891 9.47989 3.33341L10.5922 6.34122C10.6037 6.37226 10.6285 6.39712 10.6596 6.4086L13.6664 7.52091C14.1113 7.68553 14.1113 8.31528 13.6664 8.47989L10.6596 9.5922C10.6285 9.6037 10.6037 9.6285 10.5922 9.65958L9.47989 12.6664C9.31528 13.1113 8.68553 13.1113 8.52091 12.6664L7.4086 9.65958C7.39712 9.62853 7.37226 9.60371 7.34122 9.5922L4.33341 8.47989C3.88891 8.31518 3.88899 7.68571 4.33341 7.52091L7.34122 6.4086C7.3723 6.3971 7.3971 6.3723 7.4086 6.34122L8.52091 3.33341ZM3.809 2.13321C3.87511 1.95596 4.12579 1.95588 4.19181 2.13321L4.63712 3.33634C4.64169 3.3487 4.65116 3.35905 4.66349 3.36368L5.86661 3.809C6.04452 3.87486 6.04455 4.12597 5.86661 4.19181L4.66349 4.63712C4.65134 4.64173 4.64173 4.65134 4.63712 4.66349L4.19181 5.86661C4.12597 6.04455 3.87486 6.04452 3.809 5.86661L3.36368 4.66349C3.35905 4.65116 3.3487 4.64169 3.33634 4.63712L2.13321 4.19181C1.95588 4.12579 1.95596 3.87511 2.13321 3.809L3.33634 3.36368C3.34877 3.35908 3.35908 3.34877 3.36368 3.33634L3.809 2.13321Z";
|
|
33941
|
+
const SPARKLE_PATH$1 = "M3.809 10.1332C3.87511 9.95596 4.12579 9.95588 4.19181 10.1332L4.63712 11.3363C4.64169 11.3487 4.65116 11.359 4.66349 11.3637L5.86661 11.809C6.04452 11.8749 6.04455 12.126 5.86661 12.1918L4.66349 12.6371C4.65134 12.6417 4.64173 12.6513 4.63712 12.6635L4.19181 13.8666C4.12597 14.0445 3.87486 14.0445 3.809 13.8666L3.36368 12.6635C3.35905 12.6512 3.3487 12.6417 3.33634 12.6371L2.13321 12.1918C1.95588 12.1258 1.95596 11.8751 2.13321 11.809L3.33634 11.3637C3.34877 11.3591 3.35908 11.3488 3.36368 11.3363L3.809 10.1332ZM8.52091 3.33341C8.68571 2.88899 9.31518 2.88891 9.47989 3.33341L10.5922 6.34122C10.6037 6.37226 10.6285 6.39712 10.6596 6.4086L13.6664 7.52091C14.1113 7.68553 14.1113 8.31528 13.6664 8.47989L10.6596 9.5922C10.6285 9.6037 10.6037 9.6285 10.5922 9.65958L9.47989 12.6664C9.31528 13.1113 8.68553 13.1113 8.52091 12.6664L7.4086 9.65958C7.39712 9.62853 7.37226 9.60371 7.34122 9.5922L4.33341 8.47989C3.88891 8.31518 3.88899 7.68571 4.33341 7.52091L7.34122 6.4086C7.3723 6.3971 7.3971 6.3723 7.4086 6.34122L8.52091 3.33341ZM3.809 2.13321C3.87511 1.95596 4.12579 1.95588 4.19181 2.13321L4.63712 3.33634C4.64169 3.3487 4.65116 3.35905 4.66349 3.36368L5.86661 3.809C6.04452 3.87486 6.04455 4.12597 5.86661 4.19181L4.66349 4.63712C4.65134 4.64173 4.64173 4.65134 4.63712 4.66349L4.19181 5.86661C4.12597 6.04455 3.87486 6.04452 3.809 5.86661L3.36368 4.66349C3.35905 4.65116 3.3487 4.64169 3.33634 4.63712L2.13321 4.19181C1.95588 4.12579 1.95596 3.87511 2.13321 3.809L3.33634 3.36368C3.34877 3.35908 3.35908 3.34877 3.36368 3.33634L3.809 2.13321Z";
|
|
33349
33942
|
// Same stops as the AskAIBanner border gradient, so the two stay in sync.
|
|
33350
33943
|
const GRADIENT_STOPS = [{
|
|
33351
33944
|
color: "var(--t-fill-color-tag-teal-040)",
|
|
@@ -33394,7 +33987,7 @@ function SparkleIcon({
|
|
|
33394
33987
|
stopColor: color
|
|
33395
33988
|
}
|
|
33396
33989
|
})))))), /*#__PURE__*/React.createElement("path", {
|
|
33397
|
-
d: SPARKLE_PATH,
|
|
33990
|
+
d: SPARKLE_PATH$1,
|
|
33398
33991
|
fill: isGradient ? `url(#${gradientId})` : "currentColor"
|
|
33399
33992
|
}));
|
|
33400
33993
|
}
|
|
@@ -33802,16 +34395,24 @@ const DocumentationSearch = ({
|
|
|
33802
34395
|
// not the path, so updating it is a same-document navigation that doesn't
|
|
33803
34396
|
// reload the iframe, but also lets the route survive a forced reload (e.g.
|
|
33804
34397
|
// Turbo re-parenting the topbar).
|
|
33805
|
-
|
|
34398
|
+
//
|
|
34399
|
+
// `panel` loads picobot as its own toolbar panel rather than inside Helpdesk.
|
|
34400
|
+
function buildPicobotSrc(origin, path, {
|
|
34401
|
+
panel = false
|
|
34402
|
+
} = {}) {
|
|
33806
34403
|
const [rawPath, queryPart] = path.replace(/^\/+/, "").split("?", 2);
|
|
33807
34404
|
const safePath = rawPath.split("/").filter(segment => segment && segment !== "." && segment !== "..").join("/");
|
|
33808
34405
|
const fragment = queryPart !== undefined ? `${safePath}?${queryPart}` : safePath;
|
|
33809
|
-
const
|
|
34406
|
+
const url = new URL("topbar", origin);
|
|
34407
|
+
if (panel) url.searchParams.set("panel", "1");
|
|
34408
|
+
const base = url.toString();
|
|
33810
34409
|
return fragment ? `${base}#${fragment}` : base;
|
|
33811
34410
|
}
|
|
33812
34411
|
|
|
33813
34412
|
const LOAD_TIMEOUT_MS = 3000;
|
|
33814
|
-
function createPicobotIframe(picobotOrigin
|
|
34413
|
+
function createPicobotIframe(picobotOrigin, {
|
|
34414
|
+
panel = false
|
|
34415
|
+
} = {}) {
|
|
33815
34416
|
return /*#__PURE__*/React.forwardRef(function PicobotIframe({
|
|
33816
34417
|
currentPath: iframePath,
|
|
33817
34418
|
onChangePath: onIframeChangePath,
|
|
@@ -33867,7 +34468,9 @@ function createPicobotIframe(picobotOrigin) {
|
|
|
33867
34468
|
(_b = (_a = iframeRef.current) === null || _a === void 0 ? void 0 : _a.contentWindow) === null || _b === void 0 ? void 0 : _b.postMessage(message, picobotOrigin);
|
|
33868
34469
|
}
|
|
33869
34470
|
}), []);
|
|
33870
|
-
const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath
|
|
34471
|
+
const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath, {
|
|
34472
|
+
panel
|
|
34473
|
+
});
|
|
33871
34474
|
return /*#__PURE__*/React.createElement("iframe", {
|
|
33872
34475
|
ref: iframeRef,
|
|
33873
34476
|
src: iframeSrc,
|
|
@@ -33906,22 +34509,28 @@ function HelpdeskComponent({
|
|
|
33906
34509
|
currentPath,
|
|
33907
34510
|
env,
|
|
33908
34511
|
helpdeskConfig,
|
|
33909
|
-
|
|
34512
|
+
isPicobotLoading,
|
|
34513
|
+
onChangePath,
|
|
34514
|
+
openPicobot
|
|
33910
34515
|
}) {
|
|
33911
34516
|
const PicobotIframe = React.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot")), [env]);
|
|
34517
|
+
const loadingFallback = /*#__PURE__*/React.createElement(StackView$1, {
|
|
34518
|
+
flex: 1,
|
|
34519
|
+
axis: "vertical"
|
|
34520
|
+
}, /*#__PURE__*/React.createElement(DrawerHeader, {
|
|
34521
|
+
closeDrawer: closeDrawer
|
|
34522
|
+
}, /*#__PURE__*/React.createElement(StackView$1, {
|
|
34523
|
+
axis: "horizontal",
|
|
34524
|
+
spacing: 1
|
|
34525
|
+
}, /*#__PURE__*/React.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React.createElement(LoadingIndicator$1, null))));
|
|
34526
|
+
// The add-on reads its args once, so mounting before the gate answers
|
|
34527
|
+
// would leave out openPicobot for good.
|
|
34528
|
+
if (isPicobotLoading) return loadingFallback;
|
|
33912
34529
|
return /*#__PURE__*/React.createElement(addOns.UiExtensionFirstPartyComponent, {
|
|
33913
34530
|
grow: 1,
|
|
33914
34531
|
display: "flex",
|
|
33915
34532
|
extensionId: env === "staging" ? "42" : "11",
|
|
33916
|
-
loadingFallback:
|
|
33917
|
-
flex: 1,
|
|
33918
|
-
axis: "vertical"
|
|
33919
|
-
}, /*#__PURE__*/React.createElement(DrawerHeader, {
|
|
33920
|
-
closeDrawer: closeDrawer
|
|
33921
|
-
}, /*#__PURE__*/React.createElement(StackView$1, {
|
|
33922
|
-
axis: "horizontal",
|
|
33923
|
-
spacing: 1
|
|
33924
|
-
}, /*#__PURE__*/React.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React.createElement(LoadingIndicator$1, null)))),
|
|
34533
|
+
loadingFallback: loadingFallback,
|
|
33925
34534
|
additionalComponents: {
|
|
33926
34535
|
AskAIBanner: {
|
|
33927
34536
|
component: AskAIBanner,
|
|
@@ -34019,7 +34628,11 @@ function HelpdeskComponent({
|
|
|
34019
34628
|
currentPageParams: location.search,
|
|
34020
34629
|
currentPath,
|
|
34021
34630
|
helpdeskConfig,
|
|
34022
|
-
onChangePath
|
|
34631
|
+
onChangePath,
|
|
34632
|
+
// Its presence tells helpdesk that Picobot has its own panel.
|
|
34633
|
+
...(openPicobot && {
|
|
34634
|
+
openPicobot
|
|
34635
|
+
})
|
|
34023
34636
|
}
|
|
34024
34637
|
});
|
|
34025
34638
|
}
|
|
@@ -34566,6 +35179,142 @@ const Notifications = {
|
|
|
34566
35179
|
name: "Notifications"
|
|
34567
35180
|
};
|
|
34568
35181
|
|
|
35182
|
+
const SPARKLE_PATH = "M8.7824 18.8791C8.88371 18.5973 9.26996 18.5973 9.37127 18.8791L9.94256 20.465C10.0222 20.6863 10.1914 20.8616 10.4064 20.9435L11.6779 21.4269L11.9484 21.5304L11.9963 21.5539C12.2205 21.6839 12.2047 22.039 11.9484 22.1369L11.6779 22.2394L10.4064 22.7248C10.1915 22.8068 10.0222 22.9812 9.94256 23.2023L9.37127 24.7893L9.34881 24.8381C9.23098 25.054 8.92273 25.0539 8.80486 24.8381L8.7824 24.7893L8.2111 23.2023C8.14141 23.0088 8.0029 22.8513 7.82536 22.76L7.74723 22.7248L6.47573 22.2394L6.20522 22.1369C5.93172 22.0325 5.93164 21.6347 6.20522 21.5304L6.47476 21.4269L7.74723 20.9435C7.96226 20.8616 8.13147 20.6863 8.2111 20.465L8.7824 18.8791ZM17.5706 8.11138C17.8241 7.40747 18.7928 7.40734 19.0462 8.11138L20.472 12.0782C20.6711 12.6318 21.0952 13.0687 21.6331 13.2735L25.4867 14.7413C26.1711 15.002 26.1711 15.9983 25.4867 16.2589L21.6331 17.7267C21.0952 17.9316 20.6711 18.3684 20.472 18.922L19.2649 22.2785L19.0462 22.8889L18.9905 23.0119C18.6956 23.5517 17.9212 23.5515 17.6262 23.0119L17.5706 22.8889L17.3508 22.2795L16.1448 18.922C15.9457 18.3684 15.5206 17.9316 14.9827 17.7267L11.1291 16.2589C10.4449 15.9982 10.4449 15.0021 11.1291 14.7413L14.9827 13.2735C15.4533 13.0943 15.8379 12.7376 16.0598 12.2804L16.1448 12.0782L17.5706 8.11138ZM18.0266 12.755C17.6351 13.8438 16.7916 14.7249 15.6946 15.1427L14.7551 15.5001L15.6946 15.8575C16.7916 16.2753 17.6351 17.1564 18.0266 18.2453L18.3079 19.0275L18.5891 18.2453C18.9806 17.1564 19.8242 16.2753 20.9212 15.8575L21.8597 15.5001L20.9212 15.1427C19.8242 14.7249 18.9806 13.8439 18.5891 12.755L18.3079 11.9718L18.0266 12.755ZM8.7824 6.21097C8.88388 5.9297 9.26982 5.92966 9.37127 6.21097L9.94256 7.7979C10.0222 8.01911 10.1915 8.19346 10.4064 8.27544L11.6779 8.75982L11.8322 9.16607L11.9484 8.86334C12.2218 8.96779 12.2218 9.36542 11.9484 9.46979L11.6779 9.57233L10.4064 10.0567C10.1914 10.1387 10.0222 10.3139 9.94256 10.5352L9.37127 12.1212C9.26996 12.4029 8.88371 12.4029 8.7824 12.1212L8.2111 10.5352C8.14139 10.3415 8.00304 10.1832 7.82536 10.0919L7.74723 10.0567L6.47573 9.57233L6.20522 9.46979C5.9486 9.37195 5.93265 9.01568 6.15737 8.8858L6.20522 8.86334L7.74723 8.27544C7.96214 8.19346 8.13149 8.0191 8.2111 7.7979L8.69451 6.45413L8.7824 6.21097ZM11.9484 8.86334L11.8322 9.16607L11.6779 8.75982L11.9484 8.86334Z";
|
|
35183
|
+
// Helpdesk reads this context off the email path to open its email form the
|
|
35184
|
+
// same way a handoff inside its own drawer does.
|
|
35185
|
+
function emailPathWithContext(context = {}) {
|
|
35186
|
+
const params = new URLSearchParams();
|
|
35187
|
+
for (const key of ["conversationId", "source", "turnIndex"]) {
|
|
35188
|
+
const value = context[key];
|
|
35189
|
+
if (value !== null && value !== undefined) params.set(key, String(value));
|
|
35190
|
+
}
|
|
35191
|
+
return `email?${params}`;
|
|
35192
|
+
}
|
|
35193
|
+
function PicobotComponent({
|
|
35194
|
+
clearPending,
|
|
35195
|
+
closeDrawer,
|
|
35196
|
+
currentPath,
|
|
35197
|
+
env,
|
|
35198
|
+
onChangePath,
|
|
35199
|
+
openTool,
|
|
35200
|
+
pending
|
|
35201
|
+
}) {
|
|
35202
|
+
const PicobotIframe = React.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot"), {
|
|
35203
|
+
panel: true
|
|
35204
|
+
}), [env]);
|
|
35205
|
+
const picobotRef = React.useRef(null);
|
|
35206
|
+
const [isPicobotReady, setIsPicobotReady] = React.useState(false);
|
|
35207
|
+
// Picobot has no listener until it sends READY, so openPicobot's draft or
|
|
35208
|
+
// conversation waits for it.
|
|
35209
|
+
React.useEffect(function handOverPendingOncePicobotIsReady() {
|
|
35210
|
+
var _a, _b;
|
|
35211
|
+
if (!pending || !isPicobotReady) return;
|
|
35212
|
+
const {
|
|
35213
|
+
conversationId,
|
|
35214
|
+
draft,
|
|
35215
|
+
ticketExternalId
|
|
35216
|
+
} = pending;
|
|
35217
|
+
if (conversationId) {
|
|
35218
|
+
(_a = picobotRef.current) === null || _a === void 0 ? void 0 : _a.postMessage({
|
|
35219
|
+
payload: {
|
|
35220
|
+
conversation_id: conversationId,
|
|
35221
|
+
// Picobot asks whether this resolved the ticket, and closes it.
|
|
35222
|
+
...(ticketExternalId && {
|
|
35223
|
+
external_id: ticketExternalId,
|
|
35224
|
+
source: "ticket_submitted"
|
|
35225
|
+
})
|
|
35226
|
+
},
|
|
35227
|
+
type: "LOAD_CONVERSATION"
|
|
35228
|
+
});
|
|
35229
|
+
} else if (draft) {
|
|
35230
|
+
// Not both: Picobot cancels a conversation load when a prefill
|
|
35231
|
+
// starts a new conversation.
|
|
35232
|
+
(_b = picobotRef.current) === null || _b === void 0 ? void 0 : _b.postMessage({
|
|
35233
|
+
payload: {
|
|
35234
|
+
source: "email_form",
|
|
35235
|
+
text: draft
|
|
35236
|
+
},
|
|
35237
|
+
type: "PREFILL_COMPOSER"
|
|
35238
|
+
});
|
|
35239
|
+
}
|
|
35240
|
+
clearPending();
|
|
35241
|
+
}, [pending, isPicobotReady, clearPending]);
|
|
35242
|
+
const [iframeErrored, setIframeErrored] = React.useState(false);
|
|
35243
|
+
const [mountKey, setMountKey] = React.useState(0);
|
|
35244
|
+
const handleRetry = React.useCallback(() => {
|
|
35245
|
+
setIframeErrored(false);
|
|
35246
|
+
setIsPicobotReady(false);
|
|
35247
|
+
setMountKey(key => key + 1);
|
|
35248
|
+
}, []);
|
|
35249
|
+
const handleMessage = React.useCallback(data => {
|
|
35250
|
+
var _a;
|
|
35251
|
+
const message = data;
|
|
35252
|
+
switch (message === null || message === void 0 ? void 0 : message.type) {
|
|
35253
|
+
case "CLOSE_DRAWER":
|
|
35254
|
+
return closeDrawer();
|
|
35255
|
+
case "READY":
|
|
35256
|
+
return setIsPicobotReady(true);
|
|
35257
|
+
case "PICOBOT_SUPPORT_HANDOFF":
|
|
35258
|
+
return openTool("helpdesk", ((_a = message.payload) === null || _a === void 0 ? void 0 : _a.method) === "zoom" ? "phone" : emailPathWithContext(message.payload));
|
|
35259
|
+
case "IFRAME_LOAD_ERROR":
|
|
35260
|
+
console.warn("topbar: Picobot iframe failed to load");
|
|
35261
|
+
return setIframeErrored(true);
|
|
35262
|
+
}
|
|
35263
|
+
}, [closeDrawer, openTool]);
|
|
35264
|
+
if (iframeErrored) {
|
|
35265
|
+
return /*#__PURE__*/React.createElement(LoadError, {
|
|
35266
|
+
onContactSupport: () => openTool("helpdesk", "email"),
|
|
35267
|
+
onRetry: handleRetry
|
|
35268
|
+
});
|
|
35269
|
+
}
|
|
35270
|
+
return /*#__PURE__*/React.createElement(PicobotIframe, {
|
|
35271
|
+
key: mountKey,
|
|
35272
|
+
ref: picobotRef,
|
|
35273
|
+
currentPath: currentPath,
|
|
35274
|
+
onChangePath: onChangePath,
|
|
35275
|
+
onMessage: handleMessage
|
|
35276
|
+
});
|
|
35277
|
+
}
|
|
35278
|
+
function LoadError({
|
|
35279
|
+
onContactSupport,
|
|
35280
|
+
onRetry
|
|
35281
|
+
}) {
|
|
35282
|
+
return /*#__PURE__*/React.createElement(tapestryReact.StackView, {
|
|
35283
|
+
axis: "vertical",
|
|
35284
|
+
flex: 1,
|
|
35285
|
+
alignment: "center",
|
|
35286
|
+
distribution: "center",
|
|
35287
|
+
padding: 3,
|
|
35288
|
+
spacing: 1
|
|
35289
|
+
}, /*#__PURE__*/React.createElement(tapestryReact.Heading, {
|
|
35290
|
+
level: 2
|
|
35291
|
+
}, "Picobot is temporarily unavailable"), /*#__PURE__*/React.createElement(tapestryReact.Text, {
|
|
35292
|
+
size: 5,
|
|
35293
|
+
color: "var(--t-text-color-secondary)"
|
|
35294
|
+
}, "Try again or contact support."), /*#__PURE__*/React.createElement(tapestryReact.Box, {
|
|
35295
|
+
paddingTop: 2
|
|
35296
|
+
}, /*#__PURE__*/React.createElement(tapestryReact.Button, {
|
|
35297
|
+
onClick: onRetry,
|
|
35298
|
+
iconLeft: "general.refresh",
|
|
35299
|
+
theme: "primary",
|
|
35300
|
+
variant: "fill",
|
|
35301
|
+
title: "Retry"
|
|
35302
|
+
})), /*#__PURE__*/React.createElement(tapestryReact.Button, {
|
|
35303
|
+
onClick: onContactSupport,
|
|
35304
|
+
variant: "naked",
|
|
35305
|
+
theme: "primary",
|
|
35306
|
+
title: "Contact support"
|
|
35307
|
+
}));
|
|
35308
|
+
}
|
|
35309
|
+
const Picobot = {
|
|
35310
|
+
component: PicobotComponent,
|
|
35311
|
+
icons: {
|
|
35312
|
+
staticDotPath: SPARKLE_PATH,
|
|
35313
|
+
staticPath: SPARKLE_PATH
|
|
35314
|
+
},
|
|
35315
|
+
name: "Picobot"
|
|
35316
|
+
};
|
|
35317
|
+
|
|
34569
35318
|
const {
|
|
34570
35319
|
LoadingIndicator,
|
|
34571
35320
|
StackView
|
|
@@ -34961,6 +35710,41 @@ const useChatConfig = ({
|
|
|
34961
35710
|
};
|
|
34962
35711
|
};
|
|
34963
35712
|
|
|
35713
|
+
// Picobot's API rejects requests without a version header, and apiRequest
|
|
35714
|
+
// sets its own headers, so the version rides in on the fetch.
|
|
35715
|
+
const picobotFetch = (url, opts) => defaultFetch(url, {
|
|
35716
|
+
...opts,
|
|
35717
|
+
headers: {
|
|
35718
|
+
...(opts === null || opts === void 0 ? void 0 : opts.headers),
|
|
35719
|
+
"X-PCO-API-Version": "2026-08-11"
|
|
35720
|
+
}
|
|
35721
|
+
});
|
|
35722
|
+
// Routes wait on the gate, so a slow Picobot can't hold them up for long.
|
|
35723
|
+
const GATE_TIMEOUT_MS = 3000;
|
|
35724
|
+
const usePicobotConfig = ({
|
|
35725
|
+
isEnabled
|
|
35726
|
+
}) => {
|
|
35727
|
+
// Undefined until Picobot answers.
|
|
35728
|
+
const [panelEnabled, setPanelEnabled] = React.useState();
|
|
35729
|
+
React.useEffect(() => {
|
|
35730
|
+
if (!isEnabled) return undefined;
|
|
35731
|
+
// Whatever settles first wins, so a late answer can't reopen the gate.
|
|
35732
|
+
const settle = enabled => setPanelEnabled(current => current !== null && current !== void 0 ? current : enabled);
|
|
35733
|
+
const timeout = setTimeout(() => settle(false), GATE_TIMEOUT_MS);
|
|
35734
|
+
apiRequest(picobotFetch, `${window.location.origin}/~api/picobot/v2/me`).then(({
|
|
35735
|
+
json
|
|
35736
|
+
}) => {
|
|
35737
|
+
var _a, _b;
|
|
35738
|
+
return settle(((_b = (_a = json === null || json === void 0 ? void 0 : json.data) === null || _a === void 0 ? void 0 : _a.attributes) === null || _b === void 0 ? void 0 : _b.picobot_panel_enabled) === true);
|
|
35739
|
+
}).catch(() => settle(false)).finally(() => clearTimeout(timeout));
|
|
35740
|
+
return () => clearTimeout(timeout);
|
|
35741
|
+
}, [isEnabled]);
|
|
35742
|
+
return {
|
|
35743
|
+
displayPicobotIcon: panelEnabled === true,
|
|
35744
|
+
isLoading: isEnabled && panelEnabled === undefined
|
|
35745
|
+
};
|
|
35746
|
+
};
|
|
35747
|
+
|
|
34964
35748
|
const Profile = {
|
|
34965
35749
|
component: UserProfile,
|
|
34966
35750
|
icons: {
|
|
@@ -34971,10 +35755,16 @@ const Profile = {
|
|
|
34971
35755
|
};
|
|
34972
35756
|
|
|
34973
35757
|
const DURATION = 100;
|
|
35758
|
+
// Exempt paths match regardless of query, so `email?conversationId=…` is
|
|
35759
|
+
// exempt along with `email`.
|
|
34974
35760
|
function isToolPathExemptFromStorage(toolName, path) {
|
|
34975
35761
|
var _a, _b;
|
|
34976
35762
|
const tool = toolsRouteMap[toolName];
|
|
34977
|
-
|
|
35763
|
+
const [pathWithoutQuery] = path.split("?");
|
|
35764
|
+
return (_b = (_a = tool === null || tool === void 0 ? void 0 : tool.storageExemptPaths) === null || _a === void 0 ? void 0 : _a.includes(pathWithoutQuery)) !== null && _b !== void 0 ? _b : false;
|
|
35765
|
+
}
|
|
35766
|
+
function isConversationPath(path) {
|
|
35767
|
+
return Boolean(path === null || path === void 0 ? void 0 : path.startsWith("conversations/"));
|
|
34978
35768
|
}
|
|
34979
35769
|
function scrollToolDrawerToTop(toolName) {
|
|
34980
35770
|
var _a;
|
|
@@ -34987,6 +35777,7 @@ const TOOLS_MAP = {
|
|
|
34987
35777
|
Chat,
|
|
34988
35778
|
Helpdesk,
|
|
34989
35779
|
Notifications,
|
|
35780
|
+
Picobot,
|
|
34990
35781
|
Profile,
|
|
34991
35782
|
Tasks
|
|
34992
35783
|
};
|
|
@@ -35014,7 +35805,8 @@ function Toolbar({
|
|
|
35014
35805
|
});
|
|
35015
35806
|
const {
|
|
35016
35807
|
currentContent,
|
|
35017
|
-
open
|
|
35808
|
+
open,
|
|
35809
|
+
pending
|
|
35018
35810
|
} = state;
|
|
35019
35811
|
const mounted = !!currentContent;
|
|
35020
35812
|
const storeData = useStore();
|
|
@@ -35027,6 +35819,15 @@ function Toolbar({
|
|
|
35027
35819
|
...(env === "development" ? _storeDataOverride !== null && _storeDataOverride !== void 0 ? _storeDataOverride : {} : {})
|
|
35028
35820
|
};
|
|
35029
35821
|
const updateStore = useUpdateStore();
|
|
35822
|
+
const {
|
|
35823
|
+
displayPicobotIcon,
|
|
35824
|
+
isLoading: isPicobotGateLoading
|
|
35825
|
+
} = usePicobotConfig({
|
|
35826
|
+
isEnabled: Boolean(userId)
|
|
35827
|
+
});
|
|
35828
|
+
// Picobot is a kill-switched tool: until its gate says the panel is
|
|
35829
|
+
// enabled, its routes are held, and if it never does, they're ignored.
|
|
35830
|
+
const routableTool = React.useCallback(name => name === Picobot.name.toLowerCase() && !displayPicobotIcon ? undefined : toolsRouteMap[name], [displayPicobotIcon]);
|
|
35030
35831
|
React.useEffect(() => {
|
|
35031
35832
|
if (open) {
|
|
35032
35833
|
document.body.style.overflow = "hidden";
|
|
@@ -35114,7 +35915,9 @@ function Toolbar({
|
|
|
35114
35915
|
});
|
|
35115
35916
|
}, [clearRoute]);
|
|
35116
35917
|
React.useEffect(() => {
|
|
35117
|
-
if (!toolbarFragmentRoute)
|
|
35918
|
+
if (!toolbarFragmentRoute || !routableTool(toolbarFragmentRoute.tool)) {
|
|
35919
|
+
return;
|
|
35920
|
+
}
|
|
35118
35921
|
const isExempt = isToolPathExemptFromStorage(toolbarFragmentRoute.tool, toolbarFragmentRoute.path);
|
|
35119
35922
|
if (isExempt) return;
|
|
35120
35923
|
const nextState = {
|
|
@@ -35124,12 +35927,32 @@ function Toolbar({
|
|
|
35124
35927
|
if (!shallowCompare(nextState, toolStateCookie)) {
|
|
35125
35928
|
setToolStateCookie(nextState);
|
|
35126
35929
|
}
|
|
35127
|
-
}, [toolbarFragmentRoute, toolStateCookie]);
|
|
35930
|
+
}, [toolbarFragmentRoute, toolStateCookie, routableTool]);
|
|
35128
35931
|
React.useEffect(() => {
|
|
35129
|
-
|
|
35130
|
-
|
|
35932
|
+
// Helpdesk conversations may belong to Picobot, so they wait for its gate
|
|
35933
|
+
// rather than flashing open in Helpdesk first.
|
|
35934
|
+
const mayRedirectToPicobot = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path) && (isPicobotGateLoading || displayPicobotIcon);
|
|
35935
|
+
const tool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && !mayRedirectToPicobot && routableTool(toolbarFragmentRoute.tool);
|
|
35936
|
+
if (tool && (currentContent !== tool || !open)) {
|
|
35937
|
+
setToolAndOpenDrawer(tool);
|
|
35131
35938
|
}
|
|
35132
|
-
}, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, currentContent, open]);
|
|
35939
|
+
}, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path, currentContent, open, routableTool, isPicobotGateLoading, displayPicobotIcon]);
|
|
35940
|
+
const openTool = React.useCallback((name, path, pendingValue) => {
|
|
35941
|
+
const tool = routableTool(name.toLowerCase());
|
|
35942
|
+
if (!tool) return;
|
|
35943
|
+
dispatch({
|
|
35944
|
+
payload: pendingValue === undefined ? null : {
|
|
35945
|
+
tool: tool.name,
|
|
35946
|
+
value: pendingValue
|
|
35947
|
+
},
|
|
35948
|
+
type: ActionType.setPending
|
|
35949
|
+
});
|
|
35950
|
+
onChangeToolbarFragmentRoute(tool.name.toLowerCase(), path);
|
|
35951
|
+
scrollToolDrawerToTop(tool.name);
|
|
35952
|
+
}, [onChangeToolbarFragmentRoute, routableTool]);
|
|
35953
|
+
const clearPending = React.useCallback(() => dispatch({
|
|
35954
|
+
type: ActionType.clearPending
|
|
35955
|
+
}), []);
|
|
35133
35956
|
const toggleDrawer = content => {
|
|
35134
35957
|
if (!open) {
|
|
35135
35958
|
return openDrawer(content);
|
|
@@ -35146,12 +35969,52 @@ function Toolbar({
|
|
|
35146
35969
|
joltPromise: storeData === null || storeData === void 0 ? void 0 : storeData.joltPromise,
|
|
35147
35970
|
userId
|
|
35148
35971
|
});
|
|
35149
|
-
|
|
35972
|
+
// Picobot conversations moved from Helpdesk to the Picobot panel, so
|
|
35973
|
+
// send old links and saved paths there once the panel is enabled.
|
|
35974
|
+
React.useEffect(() => {
|
|
35975
|
+
if (!displayPicobotIcon) return;
|
|
35976
|
+
if ((toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path)) {
|
|
35977
|
+
onChangeToolbarFragmentRoute(Picobot.name.toLowerCase(), toolbarFragmentRoute.path);
|
|
35978
|
+
}
|
|
35979
|
+
}, [displayPicobotIcon, toolbarFragmentRoute, onChangeToolbarFragmentRoute]);
|
|
35980
|
+
React.useEffect(() => {
|
|
35981
|
+
if (!displayPicobotIcon) return;
|
|
35982
|
+
const {
|
|
35983
|
+
helpdesk,
|
|
35984
|
+
picobot
|
|
35985
|
+
} = toolStateCookie;
|
|
35986
|
+
if (!isConversationPath(helpdesk)) return;
|
|
35987
|
+
setToolStateCookie({
|
|
35988
|
+
...toolStateCookie,
|
|
35989
|
+
helpdesk: "",
|
|
35990
|
+
picobot: picobot || helpdesk
|
|
35991
|
+
});
|
|
35992
|
+
}, [displayPicobotIcon, toolStateCookie]);
|
|
35993
|
+
const openPicobot = React.useCallback(options => {
|
|
35994
|
+
var _a;
|
|
35995
|
+
// Moving an open Picobot off its path reloads or navigates the iframe,
|
|
35996
|
+
// which would race the options it's being handed.
|
|
35997
|
+
const path = currentContent === Picobot ? (_a = toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path) !== null && _a !== void 0 ? _a : "" : "";
|
|
35998
|
+
openTool(Picobot.name, path, options);
|
|
35999
|
+
}, [openTool, currentContent, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path]);
|
|
36000
|
+
React.useEffect(() => {
|
|
36001
|
+
if (!displayPicobotIcon) return undefined;
|
|
36002
|
+
window.topbar = window.topbar || {};
|
|
36003
|
+
window.topbar.openPicobot = openPicobot;
|
|
36004
|
+
return () => {
|
|
36005
|
+
delete window.topbar.openPicobot;
|
|
36006
|
+
};
|
|
36007
|
+
}, [displayPicobotIcon, openPicobot]);
|
|
36008
|
+
const TOOLS = [Notifications, Tasks, displayChatIcon && Chat, displayPicobotIcon && Picobot, isPcoTestHost() && DeveloperTools].filter(Boolean);
|
|
35150
36009
|
const handleToolOnClick = tool => {
|
|
35151
36010
|
toggleDrawer(tool);
|
|
35152
36011
|
scrollToolDrawerToTop(tool.name);
|
|
35153
36012
|
};
|
|
35154
36013
|
const toolSpecificProps = {
|
|
36014
|
+
Helpdesk: {
|
|
36015
|
+
isPicobotLoading: isPicobotGateLoading,
|
|
36016
|
+
openPicobot: displayPicobotIcon ? openPicobot : undefined
|
|
36017
|
+
},
|
|
35155
36018
|
Profile: {
|
|
35156
36019
|
appName,
|
|
35157
36020
|
closeDrawer,
|
|
@@ -35174,6 +36037,7 @@ function Toolbar({
|
|
|
35174
36037
|
const routeIsForCurrentTool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && toolsRouteMap[toolbarFragmentRoute.tool] === currentContent;
|
|
35175
36038
|
const currentToolProps = {
|
|
35176
36039
|
appName,
|
|
36040
|
+
clearPending,
|
|
35177
36041
|
closeDrawer,
|
|
35178
36042
|
currentPath: routeIsForCurrentTool ? toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path : null,
|
|
35179
36043
|
env,
|
|
@@ -35182,6 +36046,8 @@ function Toolbar({
|
|
|
35182
36046
|
joltPromise,
|
|
35183
36047
|
onChangePath,
|
|
35184
36048
|
openDrawer: () => currentContent && openDrawer(currentContent),
|
|
36049
|
+
openTool,
|
|
36050
|
+
pending: (pending === null || pending === void 0 ? void 0 : pending.tool) === (currentContent === null || currentContent === void 0 ? void 0 : currentContent.name) ? pending === null || pending === void 0 ? void 0 : pending.value : undefined,
|
|
35185
36051
|
userId,
|
|
35186
36052
|
...(currentContent ? toolSpecificProps[currentContent.name] : {})
|
|
35187
36053
|
};
|
|
@@ -35209,21 +36075,20 @@ function Toolbar({
|
|
|
35209
36075
|
name: "toolbar.planningCenter",
|
|
35210
36076
|
size: "24px"
|
|
35211
36077
|
})), [HelpDeskTool, ...TOOLS].map(tool => {
|
|
36078
|
+
var _a;
|
|
35212
36079
|
const current = open && currentContent === tool;
|
|
35213
36080
|
const indicator = indicatorState[tool.name];
|
|
35214
36081
|
return /*#__PURE__*/React.createElement(ToolbarButton, {
|
|
35215
36082
|
current: current,
|
|
35216
36083
|
onClick: () => handleToolOnClick(tool),
|
|
35217
36084
|
id: getDataPendoAttribute(tool.name),
|
|
35218
|
-
"aria-label": `Open ${tool.name} drawer`,
|
|
36085
|
+
"aria-label": `Open ${(_a = tool.label) !== null && _a !== void 0 ? _a : tool.name} drawer`,
|
|
35219
36086
|
"data-pendo": getDataPendoAttribute(tool.name),
|
|
35220
36087
|
key: tool.name
|
|
35221
|
-
},
|
|
35222
|
-
|
|
35223
|
-
|
|
35224
|
-
|
|
35225
|
-
dot: true
|
|
35226
|
-
})));
|
|
36088
|
+
}, /*#__PURE__*/React.createElement(ToolbarToolIcon, {
|
|
36089
|
+
icons: tool.icons,
|
|
36090
|
+
dot: !!indicator
|
|
36091
|
+
}));
|
|
35227
36092
|
})), /*#__PURE__*/React.createElement(DrawerWrap, {
|
|
35228
36093
|
isVisible: open
|
|
35229
36094
|
}, mounted && (/*#__PURE__*/React.createElement(Drawer, {
|
|
@@ -35247,6 +36112,19 @@ const ToolbarButton = ({
|
|
|
35247
36112
|
...restProps
|
|
35248
36113
|
}, children);
|
|
35249
36114
|
};
|
|
36115
|
+
function ToolbarToolIcon({
|
|
36116
|
+
dot,
|
|
36117
|
+
icons
|
|
36118
|
+
}) {
|
|
36119
|
+
if ("component" in icons) {
|
|
36120
|
+
return /*#__PURE__*/React.createElement(icons.component, null);
|
|
36121
|
+
} else {
|
|
36122
|
+
return /*#__PURE__*/React.createElement(ToolbarButtonIcon, {
|
|
36123
|
+
path: dot ? icons.staticDotPath : icons.staticPath,
|
|
36124
|
+
dot: dot
|
|
36125
|
+
});
|
|
36126
|
+
}
|
|
36127
|
+
}
|
|
35250
36128
|
function ToolbarButtonIcon({
|
|
35251
36129
|
dot,
|
|
35252
36130
|
path
|