@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.umd.js
CHANGED
|
@@ -166,12 +166,14 @@
|
|
|
166
166
|
}
|
|
167
167
|
function apiRequest(fetch, url, {
|
|
168
168
|
method = "GET",
|
|
169
|
-
data = null
|
|
169
|
+
data = null,
|
|
170
|
+
headers = {}
|
|
170
171
|
} = {}) {
|
|
171
172
|
const options = {
|
|
172
173
|
headers: {
|
|
173
174
|
Accept: "application/json",
|
|
174
|
-
"Content-Type": "application/json"
|
|
175
|
+
"Content-Type": "application/json",
|
|
176
|
+
...headers
|
|
175
177
|
},
|
|
176
178
|
method,
|
|
177
179
|
redirect: "follow"
|
|
@@ -791,7 +793,7 @@
|
|
|
791
793
|
return matches;
|
|
792
794
|
}
|
|
793
795
|
|
|
794
|
-
const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1);
|
|
796
|
+
const capitalize$1 = s => s.charAt(0).toUpperCase() + s.slice(1);
|
|
795
797
|
const RESOLVED_COLOR_MODE_ATTRIBUTE = "data-resolved-color-mode";
|
|
796
798
|
function resolveColorMode(colorMode) {
|
|
797
799
|
if (colorMode === "system") {
|
|
@@ -3315,19 +3317,513 @@ true;
|
|
|
3315
3317
|
}, children));
|
|
3316
3318
|
}
|
|
3317
3319
|
|
|
3320
|
+
function parseValue(element) {
|
|
3321
|
+
const datatype = element.getAttribute("datatype");
|
|
3322
|
+
switch (datatype) {
|
|
3323
|
+
case "boolean":
|
|
3324
|
+
return element.content === "true" || element.content === "1";
|
|
3325
|
+
default:
|
|
3326
|
+
return element.content;
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
function getAllPcoMeta() {
|
|
3330
|
+
const elements = document.head.querySelectorAll(`meta[property^="pco:"]`);
|
|
3331
|
+
const meta = {};
|
|
3332
|
+
elements.forEach(e => {
|
|
3333
|
+
const property = e.getAttribute("property");
|
|
3334
|
+
meta[property.substring("pco:".length)] = parseValue(e);
|
|
3335
|
+
});
|
|
3336
|
+
return meta;
|
|
3337
|
+
}
|
|
3338
|
+
// usePcoMeta only reads each tag's content. Recently Visited also needs the
|
|
3339
|
+
// pco:recently_visitable tag's `metadata` attribute, which can change while
|
|
3340
|
+
// the content stays the same, so it is tracked in its own store.
|
|
3341
|
+
function getRecentlyVisitableMetadata() {
|
|
3342
|
+
var _a, _b;
|
|
3343
|
+
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;
|
|
3344
|
+
}
|
|
3345
|
+
const store = createStore(getAllPcoMeta());
|
|
3346
|
+
const recentlyVisitableMetadataStore = createStore({
|
|
3347
|
+
metadata: getRecentlyVisitableMetadata()
|
|
3348
|
+
});
|
|
3349
|
+
function sync() {
|
|
3350
|
+
const nextState = getAllPcoMeta();
|
|
3351
|
+
if (!shallowCompare(store.getState(), nextState)) store.setState(nextState);
|
|
3352
|
+
const metadata = getRecentlyVisitableMetadata();
|
|
3353
|
+
if (recentlyVisitableMetadataStore.getState().metadata !== metadata) {
|
|
3354
|
+
recentlyVisitableMetadataStore.setState({
|
|
3355
|
+
metadata
|
|
3356
|
+
});
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
const observer = new MutationObserver(sync);
|
|
3360
|
+
let hasTriggeredObserve = false;
|
|
3361
|
+
function observe() {
|
|
3362
|
+
if (hasTriggeredObserve) return;
|
|
3363
|
+
hasTriggeredObserve = true;
|
|
3364
|
+
// The module-load snapshot above can predate meta tags that appear later
|
|
3365
|
+
// in <head> (e.g. pco:resource, yielded right before </head>) if this
|
|
3366
|
+
// module's own script tag isn't deferred and runs mid-parse. Those tags
|
|
3367
|
+
// never mutate afterward, so the observer alone would never catch them.
|
|
3368
|
+
// Re-read once now, since observe() is only called once a consumer is
|
|
3369
|
+
// actually mounting, which is always after the document has fully parsed.
|
|
3370
|
+
sync();
|
|
3371
|
+
observer.observe(document.head, {
|
|
3372
|
+
attributes: true,
|
|
3373
|
+
childList: true,
|
|
3374
|
+
subtree: true
|
|
3375
|
+
});
|
|
3376
|
+
}
|
|
3377
|
+
function usePcoMeta() {
|
|
3378
|
+
observe();
|
|
3379
|
+
return shim.useSyncExternalStore(store.subscribe, () => store.getState());
|
|
3380
|
+
}
|
|
3381
|
+
function useRecentlyVisitableMetadata() {
|
|
3382
|
+
observe();
|
|
3383
|
+
return shim.useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
const MAX_ENTRIES$1 = 20;
|
|
3387
|
+
const FORMAT_VERSION = 1;
|
|
3388
|
+
const MAX_TEXT_LENGTH = 200;
|
|
3389
|
+
const PRODUCTS = ["accounts", "calendar", "check-ins", "giving", "groups", "home", "login", "people", "publishing", "registrations", "services"];
|
|
3390
|
+
function isId(value) {
|
|
3391
|
+
return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
|
|
3392
|
+
}
|
|
3393
|
+
// A PCRL looks like `<product>/v2/<resource_type>/<id>`.
|
|
3394
|
+
function isPcrl(value) {
|
|
3395
|
+
const segments = value.split("/");
|
|
3396
|
+
return segments.length === 4 && PRODUCTS.some(product => product === segments[0]) && isId(segments[3]);
|
|
3397
|
+
}
|
|
3398
|
+
function isNonBlankString(value) {
|
|
3399
|
+
return typeof value === "string" && value.trim() !== "";
|
|
3400
|
+
}
|
|
3401
|
+
function isRecord(value) {
|
|
3402
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3403
|
+
}
|
|
3404
|
+
function entryKey(entry) {
|
|
3405
|
+
if (entry.type === "resource") {
|
|
3406
|
+
return `resource:${entry.pcrl}`;
|
|
3407
|
+
} else {
|
|
3408
|
+
return `page:${entry.product}:${entry.path}`;
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
function isEntry(value) {
|
|
3412
|
+
if (!isRecord(value)) {
|
|
3413
|
+
return false;
|
|
3414
|
+
} else if (value.type === "resource") {
|
|
3415
|
+
return typeof value.pcrl === "string" && isPcrl(value.pcrl);
|
|
3416
|
+
} else if (value.type === "page") {
|
|
3417
|
+
return isNonBlankString(value.product) && isNonBlankString(value.title) && isNonBlankString(value.path) && typeof value.details === "string";
|
|
3418
|
+
} else {
|
|
3419
|
+
return false;
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
// Cuts on code points so a surrogate pair is never split.
|
|
3423
|
+
function clampText(value) {
|
|
3424
|
+
return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
|
|
3425
|
+
}
|
|
3426
|
+
function clampEntry(entry) {
|
|
3427
|
+
if (entry.type === "resource") {
|
|
3428
|
+
return entry;
|
|
3429
|
+
} else {
|
|
3430
|
+
return {
|
|
3431
|
+
...entry,
|
|
3432
|
+
details: clampText(entry.details),
|
|
3433
|
+
title: clampText(entry.title)
|
|
3434
|
+
};
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
// Recents are disposable: anything unexpected reads as an empty list (or, for
|
|
3438
|
+
// a single bad entry, a list without it) rather than throwing.
|
|
3439
|
+
function parseRecentlyVisited(value, personId) {
|
|
3440
|
+
let parsed = null;
|
|
3441
|
+
try {
|
|
3442
|
+
parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
|
|
3443
|
+
} catch {
|
|
3444
|
+
parsed = null;
|
|
3445
|
+
}
|
|
3446
|
+
if (isRecord(parsed) && parsed.v === FORMAT_VERSION && parsed.person === personId && Array.isArray(parsed.entries)) {
|
|
3447
|
+
return parsed.entries.filter(isEntry);
|
|
3448
|
+
} else {
|
|
3449
|
+
return [];
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
function addVisit(entries, visit) {
|
|
3453
|
+
const key = entryKey(visit);
|
|
3454
|
+
return [clampEntry(visit), ...entries.filter(entry => entryKey(entry) !== key)].slice(0, MAX_ENTRIES$1);
|
|
3455
|
+
}
|
|
3456
|
+
function serializeRecentlyVisited(entries, personId) {
|
|
3457
|
+
return JSON.stringify({
|
|
3458
|
+
entries,
|
|
3459
|
+
person: personId,
|
|
3460
|
+
v: FORMAT_VERSION
|
|
3461
|
+
});
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
// The product whose URL, in this environment, has the current page's host.
|
|
3465
|
+
// Reversing pcoUrl handles every environment @planningcenter/url knows about,
|
|
3466
|
+
// and guarantees the stored product rebuilds a working link.
|
|
3467
|
+
function currentProduct() {
|
|
3468
|
+
var _a;
|
|
3469
|
+
return (_a = PRODUCTS.find(product => new URL(pcoUrl$1.pcoUrl(product)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
|
|
3470
|
+
}
|
|
3471
|
+
function detailsFromMetadata(metadata) {
|
|
3472
|
+
var _a;
|
|
3473
|
+
try {
|
|
3474
|
+
const details = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) === null || _a === void 0 ? void 0 : _a.details;
|
|
3475
|
+
return typeof details === "string" ? details : "";
|
|
3476
|
+
} catch {
|
|
3477
|
+
return "";
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
// pco:resource is the more specific signal, so it wins when both tags are
|
|
3481
|
+
// present. A resource that isn't a PCRL is ignored rather than recorded.
|
|
3482
|
+
function visitFromPage({
|
|
3483
|
+
metadata,
|
|
3484
|
+
resource,
|
|
3485
|
+
title
|
|
3486
|
+
}) {
|
|
3487
|
+
const product = currentProduct();
|
|
3488
|
+
if (resource && isPcrl(resource)) {
|
|
3489
|
+
return {
|
|
3490
|
+
pcrl: resource,
|
|
3491
|
+
type: "resource"
|
|
3492
|
+
};
|
|
3493
|
+
} else if (title && title.trim() !== "" && product) {
|
|
3494
|
+
return {
|
|
3495
|
+
details: detailsFromMetadata(metadata),
|
|
3496
|
+
path: window.location.pathname,
|
|
3497
|
+
product,
|
|
3498
|
+
title,
|
|
3499
|
+
type: "page"
|
|
3500
|
+
};
|
|
3501
|
+
} else {
|
|
3502
|
+
return null;
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
// The entry Recently Visited would record for the current page, or null when
|
|
3506
|
+
// the page isn't eligible. Shared by the recorder and the debug panel so the
|
|
3507
|
+
// panel always shows what actually gets stored.
|
|
3508
|
+
function useCurrentVisit() {
|
|
3509
|
+
const {
|
|
3510
|
+
recently_visitable: title,
|
|
3511
|
+
resource
|
|
3512
|
+
} = usePcoMeta();
|
|
3513
|
+
const metadata = useRecentlyVisitableMetadata();
|
|
3514
|
+
return React.useMemo(() => visitFromPage({
|
|
3515
|
+
metadata,
|
|
3516
|
+
resource,
|
|
3517
|
+
title
|
|
3518
|
+
}), [metadata, resource, title]);
|
|
3519
|
+
}
|
|
3520
|
+
// Products hand the person's ID to AppsProvider, not to the topbar itself,
|
|
3521
|
+
// and several pass it as a number.
|
|
3522
|
+
function useCurrentPersonId() {
|
|
3523
|
+
const {
|
|
3524
|
+
userId
|
|
3525
|
+
} = useStore();
|
|
3526
|
+
return userId == null ? undefined : String(userId);
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
// Per-origin localStorage is a stopgap: each product keeps its own list until
|
|
3530
|
+
// PCA-409 moves this to storage shared across product subdomains. Only this
|
|
3531
|
+
// file should need to change when it does.
|
|
3532
|
+
function storageKey({
|
|
3533
|
+
env,
|
|
3534
|
+
personId
|
|
3535
|
+
}) {
|
|
3536
|
+
return `topbar.recently_visited.${env}.${personId}`;
|
|
3537
|
+
}
|
|
3538
|
+
// localStorage doesn't notify the tab that wrote it, so readers in this tab
|
|
3539
|
+
// learn about new visits from this counter instead.
|
|
3540
|
+
const changes = createStore({
|
|
3541
|
+
version: 0
|
|
3542
|
+
});
|
|
3543
|
+
function readRecentlyVisited(scope) {
|
|
3544
|
+
try {
|
|
3545
|
+
return parseRecentlyVisited(localStorage.getItem(storageKey(scope)), scope.personId);
|
|
3546
|
+
} catch {
|
|
3547
|
+
return [];
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
function recordVisit(visit, scope) {
|
|
3551
|
+
if (isEntry(visit)) {
|
|
3552
|
+
try {
|
|
3553
|
+
localStorage.setItem(storageKey(scope), serializeRecentlyVisited(addVisit(readRecentlyVisited(scope), visit), scope.personId));
|
|
3554
|
+
changes.setState({
|
|
3555
|
+
version: changes.getState().version + 1
|
|
3556
|
+
});
|
|
3557
|
+
} catch {
|
|
3558
|
+
// Storage can be full or blocked; recents are disposable.
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
function useRecentlyVisited({
|
|
3563
|
+
env,
|
|
3564
|
+
personId
|
|
3565
|
+
}) {
|
|
3566
|
+
const version = shim.useSyncExternalStore(changes.subscribe, () => changes.getState().version);
|
|
3567
|
+
// `version` isn't read, but listing it re-reads storage after each visit.
|
|
3568
|
+
return React.useMemo(() => personId ? readRecentlyVisited({
|
|
3569
|
+
env,
|
|
3570
|
+
personId
|
|
3571
|
+
}) : [], [env, personId, version]);
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
// The versions home-mobile pins, so web and mobile read the same response
|
|
3575
|
+
// shapes. Some endpoints only exist from a version on: Registrations' me,
|
|
3576
|
+
// product_search_results, and pcrl_lookups need 2025-05-01.
|
|
3577
|
+
const API_VERSIONS = {
|
|
3578
|
+
calendar: "2022-07-07",
|
|
3579
|
+
"check-ins": "2025-05-28",
|
|
3580
|
+
giving: "2019-10-18",
|
|
3581
|
+
groups: "2023-07-10",
|
|
3582
|
+
home: "2018-08-01",
|
|
3583
|
+
people: "2023-03-21",
|
|
3584
|
+
publishing: "2024-03-25",
|
|
3585
|
+
registrations: "2025-05-01",
|
|
3586
|
+
services: "2018-11-01"
|
|
3587
|
+
};
|
|
3588
|
+
// For a `/~api/{product}/v2/...` URL, the header that pins its product's
|
|
3589
|
+
// version.
|
|
3590
|
+
function apiVersionHeaders(url) {
|
|
3591
|
+
const [,, product] = new URL(url).pathname.split("/");
|
|
3592
|
+
const version = API_VERSIONS[product];
|
|
3593
|
+
if (version) {
|
|
3594
|
+
return {
|
|
3595
|
+
"X-PCO-API-Version": version
|
|
3596
|
+
};
|
|
3597
|
+
} else {
|
|
3598
|
+
return {};
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
const CACHE_TTL_MS = 60000;
|
|
3603
|
+
const MAX_ENTRIES = 50;
|
|
3604
|
+
const cache = new Map();
|
|
3605
|
+
function cachedRequest(url) {
|
|
3606
|
+
const cached = cache.get(url);
|
|
3607
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
3608
|
+
// Re-inserting keeps the Map in recently-used order for eviction.
|
|
3609
|
+
cache.delete(url);
|
|
3610
|
+
cache.set(url, cached);
|
|
3611
|
+
return cached.promise;
|
|
3612
|
+
} else {
|
|
3613
|
+
return fetchIntoCache(url);
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
function fetchIntoCache(url) {
|
|
3617
|
+
const promise = apiRequest(defaultFetch, url, {
|
|
3618
|
+
headers: apiVersionHeaders(url)
|
|
3619
|
+
}).then(({
|
|
3620
|
+
json,
|
|
3621
|
+
ok
|
|
3622
|
+
}) => {
|
|
3623
|
+
if (!ok || json === null) throw new Error(`Request failed: ${url}`);
|
|
3624
|
+
return json;
|
|
3625
|
+
});
|
|
3626
|
+
// In-flight entries never expire, so every caller shares the one fetch.
|
|
3627
|
+
const entry = {
|
|
3628
|
+
expiresAt: Infinity,
|
|
3629
|
+
promise
|
|
3630
|
+
};
|
|
3631
|
+
cache.set(url, entry);
|
|
3632
|
+
promise.then(() => entry.expiresAt = Date.now() + CACHE_TTL_MS, () => cache.get(url) === entry && cache.delete(url));
|
|
3633
|
+
if (cache.size > MAX_ENTRIES) cache.delete(cache.keys().next().value);
|
|
3634
|
+
return promise;
|
|
3635
|
+
}
|
|
3636
|
+
function useCachedRequest(url) {
|
|
3637
|
+
const [settled, setSettled] = React.useState();
|
|
3638
|
+
React.useEffect(() => {
|
|
3639
|
+
if (url) {
|
|
3640
|
+
let current = true;
|
|
3641
|
+
cachedRequest(url).then(data => current && setSettled({
|
|
3642
|
+
data,
|
|
3643
|
+
url
|
|
3644
|
+
}), () => current && setSettled({
|
|
3645
|
+
failed: true,
|
|
3646
|
+
url
|
|
3647
|
+
}));
|
|
3648
|
+
return () => {
|
|
3649
|
+
current = false;
|
|
3650
|
+
};
|
|
3651
|
+
} else {
|
|
3652
|
+
return undefined;
|
|
3653
|
+
}
|
|
3654
|
+
}, [url]);
|
|
3655
|
+
if (!url) {
|
|
3656
|
+
return {
|
|
3657
|
+
status: "idle"
|
|
3658
|
+
};
|
|
3659
|
+
} else if ((settled === null || settled === void 0 ? void 0 : settled.url) !== url) {
|
|
3660
|
+
return {
|
|
3661
|
+
data: settled && "data" in settled ? settled.data : undefined,
|
|
3662
|
+
status: "loading"
|
|
3663
|
+
};
|
|
3664
|
+
} else if ("data" in settled) {
|
|
3665
|
+
return {
|
|
3666
|
+
data: settled.data,
|
|
3667
|
+
status: "done"
|
|
3668
|
+
};
|
|
3669
|
+
} else {
|
|
3670
|
+
return {
|
|
3671
|
+
status: "error"
|
|
3672
|
+
};
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
// One request per URL, for callers that fan out to a varying number of
|
|
3676
|
+
// endpoints. Unlike `useCachedRequest`, a URL that hasn't settled yet has no
|
|
3677
|
+
// earlier data to fall back on.
|
|
3678
|
+
function useCachedRequests(urls) {
|
|
3679
|
+
const [settled, setSettled] = React.useState({});
|
|
3680
|
+
// Keyed by value because each settle re-renders the caller, and a caller
|
|
3681
|
+
// that builds a fresh `urls` array on render would otherwise loop forever.
|
|
3682
|
+
const key = urls.join("\n");
|
|
3683
|
+
React.useEffect(() => {
|
|
3684
|
+
let current = true;
|
|
3685
|
+
const settle = result => current && setSettled(prev => ({
|
|
3686
|
+
...prev,
|
|
3687
|
+
[result.url]: result
|
|
3688
|
+
}));
|
|
3689
|
+
for (const url of key.split("\n").filter(Boolean)) {
|
|
3690
|
+
cachedRequest(url).then(data => settle({
|
|
3691
|
+
data,
|
|
3692
|
+
url
|
|
3693
|
+
}), () => settle({
|
|
3694
|
+
failed: true,
|
|
3695
|
+
url
|
|
3696
|
+
}));
|
|
3697
|
+
}
|
|
3698
|
+
return () => {
|
|
3699
|
+
current = false;
|
|
3700
|
+
};
|
|
3701
|
+
}, [key]);
|
|
3702
|
+
return urls.map(url => {
|
|
3703
|
+
const result = settled[url];
|
|
3704
|
+
if (!result) {
|
|
3705
|
+
return {
|
|
3706
|
+
status: "loading"
|
|
3707
|
+
};
|
|
3708
|
+
} else if ("data" in result) {
|
|
3709
|
+
return {
|
|
3710
|
+
data: result.data,
|
|
3711
|
+
status: "done"
|
|
3712
|
+
};
|
|
3713
|
+
} else {
|
|
3714
|
+
return {
|
|
3715
|
+
status: "error"
|
|
3716
|
+
};
|
|
3717
|
+
}
|
|
3718
|
+
});
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
const pcoUrl = pcoUrl$1;
|
|
3722
|
+
|
|
3723
|
+
// Resolves resource entries with one pcrl_lookups request per product.
|
|
3724
|
+
// pcrl_lookups reports forbidden and deleted resources the same way, so both
|
|
3725
|
+
// become `unresolved`, as does every entry in a batch whose request failed.
|
|
3726
|
+
function useHydratedRecentlyVisited(entries, env) {
|
|
3727
|
+
const batches = React.useMemo(() => batchByProduct(entries), [entries]);
|
|
3728
|
+
const urls = React.useMemo(() => batches.map(([product, pcrls]) => pcrlLookupsUrl(product, pcrls)), [batches]);
|
|
3729
|
+
const requests = useCachedRequests(urls);
|
|
3730
|
+
const lookups = new Map();
|
|
3731
|
+
batches.forEach(([, pcrls], index) => {
|
|
3732
|
+
const request = requests[index];
|
|
3733
|
+
for (const pcrl of pcrls) lookups.set(pcrl, lookupFor(pcrl, request));
|
|
3734
|
+
});
|
|
3735
|
+
return entries.map(entry => {
|
|
3736
|
+
var _a;
|
|
3737
|
+
if (entry.type === "page") {
|
|
3738
|
+
return {
|
|
3739
|
+
entry,
|
|
3740
|
+
status: "page",
|
|
3741
|
+
url: pageUrl(entry, env)
|
|
3742
|
+
};
|
|
3743
|
+
} else {
|
|
3744
|
+
const lookup = (_a = lookups.get(entry.pcrl)) !== null && _a !== void 0 ? _a : {
|
|
3745
|
+
status: "unresolved"
|
|
3746
|
+
};
|
|
3747
|
+
if (lookup.status === "resolved") {
|
|
3748
|
+
return {
|
|
3749
|
+
...lookup,
|
|
3750
|
+
entry,
|
|
3751
|
+
url: lookup.result.attributes.url
|
|
3752
|
+
};
|
|
3753
|
+
} else {
|
|
3754
|
+
return {
|
|
3755
|
+
entry,
|
|
3756
|
+
status: lookup.status
|
|
3757
|
+
};
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
});
|
|
3761
|
+
}
|
|
3762
|
+
function lookupFor(pcrl, request) {
|
|
3763
|
+
if (request.status === "done") {
|
|
3764
|
+
const result = request.data.data.find(({
|
|
3765
|
+
attributes
|
|
3766
|
+
}) => attributes.pcrl === pcrl);
|
|
3767
|
+
return result ? {
|
|
3768
|
+
result,
|
|
3769
|
+
status: "resolved"
|
|
3770
|
+
} : {
|
|
3771
|
+
status: "unresolved"
|
|
3772
|
+
};
|
|
3773
|
+
} else if (request.status === "error") {
|
|
3774
|
+
return {
|
|
3775
|
+
status: "unresolved"
|
|
3776
|
+
};
|
|
3777
|
+
} else {
|
|
3778
|
+
return {
|
|
3779
|
+
status: "loading"
|
|
3780
|
+
};
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
const RECENTLY_VISITED_PAGE_SIZE = 10;
|
|
3784
|
+
// A PCRL can only be looked up through its own product's API, which may not be
|
|
3785
|
+
// the product it was visited in. Each page of entries is batched on its own,
|
|
3786
|
+
// so loading the next page leaves earlier pages' URLs, and so their cache
|
|
3787
|
+
// keys, alone. Sorting keeps them stable as a page reorders.
|
|
3788
|
+
function batchByProduct(entries) {
|
|
3789
|
+
var _a;
|
|
3790
|
+
const batches = [];
|
|
3791
|
+
for (let start = 0; start < entries.length; start += RECENTLY_VISITED_PAGE_SIZE) {
|
|
3792
|
+
const page = new Map();
|
|
3793
|
+
for (const entry of entries.slice(start, start + RECENTLY_VISITED_PAGE_SIZE)) {
|
|
3794
|
+
if (entry.type === "resource") {
|
|
3795
|
+
const [product] = entry.pcrl.split("/");
|
|
3796
|
+
page.set(product, [...((_a = page.get(product)) !== null && _a !== void 0 ? _a : []), entry.pcrl]);
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
for (const [product, pcrls] of page) {
|
|
3800
|
+
batches.push([product, [...pcrls].sort()]);
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
return batches;
|
|
3804
|
+
}
|
|
3805
|
+
function pcrlLookupsUrl(product, pcrls) {
|
|
3806
|
+
const params = new URLSearchParams({
|
|
3807
|
+
"where[pcrl]": pcrls.join(",")
|
|
3808
|
+
});
|
|
3809
|
+
return `${window.location.origin}/~api/${product}/v2/pcrl_lookups?${params}`;
|
|
3810
|
+
}
|
|
3811
|
+
// Built from the product rather than the current origin, so it stays right
|
|
3812
|
+
// once PCA-409 shares one list across product subdomains.
|
|
3813
|
+
function pageUrl(entry, env) {
|
|
3814
|
+
return `${pcoUrl(env)(entry.product)}${entry.path}`;
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3318
3817
|
var styles$a = {"result":"result-WCnxr"};
|
|
3319
3818
|
|
|
3320
3819
|
function SearchResult({
|
|
3321
3820
|
highlighted,
|
|
3322
3821
|
id,
|
|
3323
3822
|
onHighlight,
|
|
3324
|
-
|
|
3823
|
+
subtitle,
|
|
3824
|
+
title,
|
|
3825
|
+
url
|
|
3325
3826
|
}) {
|
|
3326
|
-
const {
|
|
3327
|
-
metadata,
|
|
3328
|
-
title,
|
|
3329
|
-
url
|
|
3330
|
-
} = result.attributes;
|
|
3331
3827
|
const ref = React.useRef(null);
|
|
3332
3828
|
React.useEffect(() => {
|
|
3333
3829
|
var _a;
|
|
@@ -3348,10 +3844,7 @@ true;
|
|
|
3348
3844
|
ref: ref,
|
|
3349
3845
|
role: "option",
|
|
3350
3846
|
tabIndex: -1
|
|
3351
|
-
}, /*#__PURE__*/React.createElement("div", null, title), /*#__PURE__*/React.createElement("div", null,
|
|
3352
|
-
}
|
|
3353
|
-
function summarize(metadata) {
|
|
3354
|
-
return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
|
|
3847
|
+
}, /*#__PURE__*/React.createElement("div", null, title), /*#__PURE__*/React.createElement("div", null, subtitle));
|
|
3355
3848
|
}
|
|
3356
3849
|
|
|
3357
3850
|
const resultId = (listboxId, index) => `${listboxId}-result-${index}`;
|
|
@@ -3423,83 +3916,127 @@ true;
|
|
|
3423
3916
|
id: resultId(id, resultIndex),
|
|
3424
3917
|
key: result.id,
|
|
3425
3918
|
onHighlight: () => onHighlight(resultIndex),
|
|
3426
|
-
|
|
3919
|
+
subtitle: summarize(result.attributes.metadata),
|
|
3920
|
+
title: result.attributes.title,
|
|
3921
|
+
url: result.attributes.url
|
|
3427
3922
|
});
|
|
3428
3923
|
}));
|
|
3429
3924
|
}));
|
|
3430
3925
|
}
|
|
3926
|
+
function summarize(metadata) {
|
|
3927
|
+
return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
|
|
3928
|
+
}
|
|
3431
3929
|
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3930
|
+
function useRecentVisits(env) {
|
|
3931
|
+
const personId = useCurrentPersonId();
|
|
3932
|
+
const entries = useRecentlyVisited({
|
|
3933
|
+
env,
|
|
3934
|
+
personId
|
|
3935
|
+
});
|
|
3936
|
+
const [count, setCount] = React.useState(RECENTLY_VISITED_PAGE_SIZE);
|
|
3937
|
+
const shown = React.useMemo(() => entries.slice(0, count), [entries, count]);
|
|
3938
|
+
const hydrated = useHydratedRecentlyVisited(shown, env);
|
|
3939
|
+
// Resources show up once they're found. Ones that can't be found anymore,
|
|
3940
|
+
// or that the person can no longer see, are left out.
|
|
3941
|
+
const visits = hydrated.filter(isShown);
|
|
3942
|
+
const loading = hydrated.some(({
|
|
3943
|
+
status
|
|
3944
|
+
}) => status === "loading");
|
|
3945
|
+
const showMore = React.useCallback(() => setCount(current => current + RECENTLY_VISITED_PAGE_SIZE), []);
|
|
3946
|
+
return {
|
|
3947
|
+
count,
|
|
3948
|
+
hasMore: entries.length > count,
|
|
3949
|
+
loading,
|
|
3950
|
+
showMore,
|
|
3951
|
+
visits
|
|
3952
|
+
};
|
|
3953
|
+
}
|
|
3954
|
+
function isShown(visit) {
|
|
3955
|
+
return visit.status === "page" || visit.status === "resolved";
|
|
3956
|
+
}
|
|
3957
|
+
function RecentlyVisited({
|
|
3958
|
+
count,
|
|
3959
|
+
hasMore,
|
|
3960
|
+
highlightedIndex,
|
|
3961
|
+
id,
|
|
3962
|
+
loading,
|
|
3963
|
+
onHighlight,
|
|
3964
|
+
showMore,
|
|
3965
|
+
visits
|
|
3966
|
+
}) {
|
|
3967
|
+
const headingId = `${id}-heading`;
|
|
3968
|
+
if (visits.length === 0 && !hasMore && !loading) {
|
|
3969
|
+
return /*#__PURE__*/React.createElement("p", null, "Pages you visit will show up here.");
|
|
3442
3970
|
} else {
|
|
3443
|
-
return
|
|
3971
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
|
|
3972
|
+
id: headingId
|
|
3973
|
+
}, "Recently visited"), /*#__PURE__*/React.createElement("div", {
|
|
3974
|
+
"aria-labelledby": headingId,
|
|
3975
|
+
id: id,
|
|
3976
|
+
role: "listbox"
|
|
3977
|
+
}, visits.map((visit, index) => (/*#__PURE__*/React.createElement(SearchResult, {
|
|
3978
|
+
highlighted: index === highlightedIndex,
|
|
3979
|
+
id: resultId(id, index),
|
|
3980
|
+
key: entryKey(visit.entry),
|
|
3981
|
+
onHighlight: () => onHighlight(index),
|
|
3982
|
+
...describeVisit(visit)
|
|
3983
|
+
})))), hasMore && /*#__PURE__*/React.createElement(ShowMoreWhenVisible, {
|
|
3984
|
+
key: count,
|
|
3985
|
+
onVisible: showMore
|
|
3986
|
+
}));
|
|
3444
3987
|
}
|
|
3445
3988
|
}
|
|
3446
|
-
function
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3989
|
+
function ShowMoreWhenVisible({
|
|
3990
|
+
onVisible
|
|
3991
|
+
}) {
|
|
3992
|
+
const ref = React.useRef(null);
|
|
3993
|
+
React.useEffect(() => {
|
|
3994
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
3995
|
+
if (entry.isIntersecting) onVisible();
|
|
3996
|
+
});
|
|
3997
|
+
if (ref.current) observer.observe(ref.current);
|
|
3998
|
+
return () => observer.disconnect();
|
|
3999
|
+
}, [onVisible]);
|
|
4000
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
4001
|
+
ref: ref
|
|
3453
4002
|
});
|
|
3454
|
-
// In-flight entries never expire, so every caller shares the one fetch.
|
|
3455
|
-
const entry = {
|
|
3456
|
-
expiresAt: Infinity,
|
|
3457
|
-
promise
|
|
3458
|
-
};
|
|
3459
|
-
cache.set(url, entry);
|
|
3460
|
-
promise.then(() => entry.expiresAt = Date.now() + CACHE_TTL_MS, () => cache.get(url) === entry && cache.delete(url));
|
|
3461
|
-
if (cache.size > MAX_ENTRIES$1) cache.delete(cache.keys().next().value);
|
|
3462
|
-
return promise;
|
|
3463
4003
|
}
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
url
|
|
3475
|
-
}));
|
|
3476
|
-
return () => {
|
|
3477
|
-
current = false;
|
|
3478
|
-
};
|
|
3479
|
-
} else {
|
|
3480
|
-
return undefined;
|
|
3481
|
-
}
|
|
3482
|
-
}, [url]);
|
|
3483
|
-
if (!url) {
|
|
3484
|
-
return {
|
|
3485
|
-
status: "idle"
|
|
3486
|
-
};
|
|
3487
|
-
} else if ((settled === null || settled === void 0 ? void 0 : settled.url) !== url) {
|
|
3488
|
-
return {
|
|
3489
|
-
data: settled && "data" in settled ? settled.data : undefined,
|
|
3490
|
-
status: "loading"
|
|
3491
|
-
};
|
|
3492
|
-
} else if ("data" in settled) {
|
|
4004
|
+
// Unlike search results, recents come from every product, so each one names
|
|
4005
|
+
// its product and type.
|
|
4006
|
+
function describeVisit(visit) {
|
|
4007
|
+
const subtitle = (...parts) => parts.filter(Boolean).join(" · ");
|
|
4008
|
+
if (visit.status === "page") {
|
|
4009
|
+
const {
|
|
4010
|
+
details,
|
|
4011
|
+
product,
|
|
4012
|
+
title
|
|
4013
|
+
} = visit.entry;
|
|
3493
4014
|
return {
|
|
3494
|
-
|
|
3495
|
-
|
|
4015
|
+
subtitle: subtitle(productName(product), details),
|
|
4016
|
+
title,
|
|
4017
|
+
url: visit.url
|
|
3496
4018
|
};
|
|
3497
4019
|
} else {
|
|
4020
|
+
const {
|
|
4021
|
+
metadata,
|
|
4022
|
+
product,
|
|
4023
|
+
title,
|
|
4024
|
+
type
|
|
4025
|
+
} = visit.result.attributes;
|
|
3498
4026
|
return {
|
|
3499
|
-
|
|
4027
|
+
subtitle: subtitle(productName(product), type, summarize(metadata)),
|
|
4028
|
+
title,
|
|
4029
|
+
url: visit.url
|
|
3500
4030
|
};
|
|
3501
4031
|
}
|
|
3502
4032
|
}
|
|
4033
|
+
// check-ins → Check-Ins
|
|
4034
|
+
function productName(slug) {
|
|
4035
|
+
return slug.split("-").map(capitalize).join("-");
|
|
4036
|
+
}
|
|
4037
|
+
function capitalize(word) {
|
|
4038
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
4039
|
+
}
|
|
3503
4040
|
|
|
3504
4041
|
const SEARCH_DEBOUNCE_MS = 300;
|
|
3505
4042
|
function useProductSearch(product, term) {
|
|
@@ -3590,15 +4127,16 @@ true;
|
|
|
3590
4127
|
ArrowDown: 1,
|
|
3591
4128
|
ArrowUp: -1
|
|
3592
4129
|
})[key];
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
4130
|
+
// Remembering which list the index belongs to puts the highlight back on the
|
|
4131
|
+
// first option whenever a new list replaces it, without an effect to reset it.
|
|
4132
|
+
// A list that grows or fills in under the same `list` keeps its highlight.
|
|
4133
|
+
function useResultNavigation(options, list) {
|
|
3596
4134
|
const [highlight, setHighlight] = React.useState({
|
|
3597
4135
|
index: 0,
|
|
3598
|
-
|
|
4136
|
+
list
|
|
3599
4137
|
});
|
|
3600
|
-
const highlightedIndex = highlight.
|
|
3601
|
-
const highlighted =
|
|
4138
|
+
const highlightedIndex = highlight.list === list ? highlight.index : 0;
|
|
4139
|
+
const highlighted = options[highlightedIndex];
|
|
3602
4140
|
const [copiedUrl, setCopiedUrl] = React.useState(null);
|
|
3603
4141
|
React.useEffect(() => {
|
|
3604
4142
|
if (copiedUrl) {
|
|
@@ -3608,30 +4146,30 @@ true;
|
|
|
3608
4146
|
return undefined;
|
|
3609
4147
|
}
|
|
3610
4148
|
}, [copiedUrl]);
|
|
3611
|
-
const highlightIndex = React.useCallback(index => setHighlight(current => current.
|
|
4149
|
+
const highlightIndex = React.useCallback(index => setHighlight(current => current.list === list && current.index === index ? current : {
|
|
3612
4150
|
index,
|
|
3613
|
-
|
|
3614
|
-
}), [
|
|
4151
|
+
list
|
|
4152
|
+
}), [list]);
|
|
3615
4153
|
const handleKeyDown = event => {
|
|
3616
4154
|
const step = verticalStep(event.key);
|
|
3617
|
-
if (step &&
|
|
4155
|
+
if (step && options.length > 0) {
|
|
3618
4156
|
event.preventDefault();
|
|
3619
|
-
highlightIndex((highlightedIndex + step +
|
|
4157
|
+
highlightIndex((highlightedIndex + step + options.length) % options.length);
|
|
3620
4158
|
} else if (event.key === "Enter" && highlighted && !event.nativeEvent.isComposing) {
|
|
3621
4159
|
event.preventDefault();
|
|
3622
|
-
open(highlighted.
|
|
4160
|
+
open(highlighted.url, {
|
|
3623
4161
|
newTab: hasCommandKey(event)
|
|
3624
4162
|
});
|
|
3625
4163
|
} else if (isCopyShortcut(event) && highlighted && !hasSelectedText(event.currentTarget)) {
|
|
3626
4164
|
event.preventDefault();
|
|
3627
4165
|
const {
|
|
3628
4166
|
url
|
|
3629
|
-
} = highlighted
|
|
4167
|
+
} = highlighted;
|
|
3630
4168
|
navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
|
|
3631
4169
|
}
|
|
3632
4170
|
};
|
|
3633
4171
|
return {
|
|
3634
|
-
copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.
|
|
4172
|
+
copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.url),
|
|
3635
4173
|
handleKeyDown,
|
|
3636
4174
|
highlightIndex,
|
|
3637
4175
|
highlighted,
|
|
@@ -3656,29 +4194,66 @@ true;
|
|
|
3656
4194
|
}
|
|
3657
4195
|
|
|
3658
4196
|
const SEARCHABLE_PRODUCTS = [{
|
|
4197
|
+
appName: "Calendar",
|
|
4198
|
+
slug: "calendar"
|
|
4199
|
+
}, {
|
|
4200
|
+
appName: "Check-Ins",
|
|
4201
|
+
slug: "check-ins"
|
|
4202
|
+
}, {
|
|
4203
|
+
appName: "Giving",
|
|
4204
|
+
slug: "giving"
|
|
4205
|
+
}, {
|
|
3659
4206
|
appName: "Groups",
|
|
3660
4207
|
slug: "groups"
|
|
3661
4208
|
}, {
|
|
3662
4209
|
appName: "People",
|
|
3663
4210
|
slug: "people"
|
|
4211
|
+
}, {
|
|
4212
|
+
appName: "Publishing",
|
|
4213
|
+
slug: "publishing"
|
|
4214
|
+
}, {
|
|
4215
|
+
appName: "Registrations",
|
|
4216
|
+
slug: "registrations"
|
|
3664
4217
|
}, {
|
|
3665
4218
|
appName: "Services",
|
|
3666
4219
|
slug: "services"
|
|
3667
4220
|
}];
|
|
3668
4221
|
const PERMISSION_CHECKS = {
|
|
4222
|
+
"check-ins": () => fetchMe("check-ins", "permission").then(({
|
|
4223
|
+
permission
|
|
4224
|
+
}) => permission !== null),
|
|
4225
|
+
giving: personId => {
|
|
4226
|
+
if (personId === undefined) {
|
|
4227
|
+
return Promise.reject(new Error("No person to check Giving access for"));
|
|
4228
|
+
} else {
|
|
4229
|
+
return fetchAttributes(`giving/v2/person_permissions/${personId}`, {
|
|
4230
|
+
"fields[PersonPermissions]": "can_search"
|
|
4231
|
+
}).then(({
|
|
4232
|
+
can_search
|
|
4233
|
+
}) => can_search);
|
|
4234
|
+
}
|
|
4235
|
+
},
|
|
3669
4236
|
people: () => fetchMe("people", "people_permissions,site_administrator").then(({
|
|
3670
4237
|
people_permissions,
|
|
3671
4238
|
site_administrator
|
|
3672
4239
|
}) => site_administrator || people_permissions !== null),
|
|
4240
|
+
registrations: () => fetchMe("registrations", "can_search").then(({
|
|
4241
|
+
can_search
|
|
4242
|
+
}) => can_search),
|
|
3673
4243
|
services: () => fetchMe("services", "can_search").then(({
|
|
3674
4244
|
can_search
|
|
3675
4245
|
}) => can_search)
|
|
3676
4246
|
};
|
|
3677
4247
|
function fetchMe(product, fields) {
|
|
3678
|
-
|
|
4248
|
+
return fetchAttributes(`${product}/v2/me`, {
|
|
3679
4249
|
"fields[Person]": fields
|
|
3680
4250
|
});
|
|
3681
|
-
|
|
4251
|
+
}
|
|
4252
|
+
function fetchAttributes(path, params) {
|
|
4253
|
+
const url = `${window.location.origin}/~api/${path}?${new URLSearchParams(params)}`;
|
|
4254
|
+
return apiRequest(defaultFetch, url, {
|
|
4255
|
+
headers: apiVersionHeaders(url)
|
|
4256
|
+
}).then(({
|
|
3682
4257
|
json,
|
|
3683
4258
|
ok
|
|
3684
4259
|
}) => {
|
|
@@ -3689,7 +4264,7 @@ true;
|
|
|
3689
4264
|
// Permissions rarely change mid-session, so a successful check is kept until
|
|
3690
4265
|
// the page reloads. A failed one is dropped so a remount tries again.
|
|
3691
4266
|
const permissionCache = new Map();
|
|
3692
|
-
function canSearch(slug) {
|
|
4267
|
+
function canSearch(slug, personId) {
|
|
3693
4268
|
const check = PERMISSION_CHECKS[slug];
|
|
3694
4269
|
const cached = permissionCache.get(slug);
|
|
3695
4270
|
if (!check) {
|
|
@@ -3697,7 +4272,7 @@ true;
|
|
|
3697
4272
|
} else if (cached) {
|
|
3698
4273
|
return cached;
|
|
3699
4274
|
} else {
|
|
3700
|
-
const promise = check().catch(() => {
|
|
4275
|
+
const promise = check(personId).catch(() => {
|
|
3701
4276
|
permissionCache.delete(slug);
|
|
3702
4277
|
return false;
|
|
3703
4278
|
});
|
|
@@ -3711,16 +4286,18 @@ true;
|
|
|
3711
4286
|
const accessibleKey = inSwitcherOrder(apps).map(({
|
|
3712
4287
|
slug
|
|
3713
4288
|
}) => slug).join(",");
|
|
4289
|
+
const personId = useCurrentPersonId();
|
|
3714
4290
|
const [settled, setSettled] = React.useState();
|
|
3715
4291
|
React.useEffect(() => {
|
|
3716
4292
|
let current = true;
|
|
3717
4293
|
const accessible = accessibleKey.split(",").flatMap(slug => SEARCHABLE_PRODUCTS.filter(product => product.slug === slug));
|
|
3718
4294
|
Promise.all(accessible.map(({
|
|
3719
4295
|
slug
|
|
3720
|
-
}) => canSearch(slug))).then(allowed => {
|
|
4296
|
+
}) => canSearch(slug, personId))).then(allowed => {
|
|
3721
4297
|
if (current) {
|
|
3722
4298
|
setSettled({
|
|
3723
4299
|
accessibleKey,
|
|
4300
|
+
personId,
|
|
3724
4301
|
products: accessible.filter((_, index) => allowed[index])
|
|
3725
4302
|
});
|
|
3726
4303
|
}
|
|
@@ -3728,8 +4305,8 @@ true;
|
|
|
3728
4305
|
return () => {
|
|
3729
4306
|
current = false;
|
|
3730
4307
|
};
|
|
3731
|
-
}, [accessibleKey]);
|
|
3732
|
-
if ((settled === null || settled === void 0 ? void 0 : settled.accessibleKey) === accessibleKey) {
|
|
4308
|
+
}, [accessibleKey, personId]);
|
|
4309
|
+
if ((settled === null || settled === void 0 ? void 0 : settled.accessibleKey) === accessibleKey && settled.personId === personId) {
|
|
3733
4310
|
return {
|
|
3734
4311
|
products: settled.products,
|
|
3735
4312
|
status: "done"
|
|
@@ -3750,11 +4327,13 @@ true;
|
|
|
3750
4327
|
const isTypingKey = event => event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey;
|
|
3751
4328
|
const isTypingIn = target => target instanceof Element && target.closest('input, select, textarea, [contenteditable]:not([contenteditable="false"])') !== null;
|
|
3752
4329
|
function ProductSearch({
|
|
3753
|
-
apps
|
|
4330
|
+
apps,
|
|
4331
|
+
env
|
|
3754
4332
|
}) {
|
|
3755
4333
|
const searchable = useSearchableProducts(apps);
|
|
3756
4334
|
if (searchable.status === "done" && searchable.products.length > 0) {
|
|
3757
4335
|
return /*#__PURE__*/React.createElement(SearchButtonAndDialog, {
|
|
4336
|
+
env: env,
|
|
3758
4337
|
products: searchable.products
|
|
3759
4338
|
});
|
|
3760
4339
|
} else {
|
|
@@ -3762,6 +4341,7 @@ true;
|
|
|
3762
4341
|
}
|
|
3763
4342
|
}
|
|
3764
4343
|
function SearchButtonAndDialog({
|
|
4344
|
+
env,
|
|
3765
4345
|
products
|
|
3766
4346
|
}) {
|
|
3767
4347
|
const [open, setOpen] = React.useState(false);
|
|
@@ -3799,10 +4379,15 @@ true;
|
|
|
3799
4379
|
onClose: () => setOpen(false),
|
|
3800
4380
|
open: open
|
|
3801
4381
|
}, /*#__PURE__*/React.createElement(SearchPanel, {
|
|
4382
|
+
env: env,
|
|
3802
4383
|
products: products
|
|
3803
4384
|
})));
|
|
3804
4385
|
}
|
|
4386
|
+
// Keys the recents list for navigation, so the highlight stays put as more
|
|
4387
|
+
// recents load and as their lookups answer.
|
|
4388
|
+
const RECENTS = "recents";
|
|
3805
4389
|
function SearchPanel({
|
|
4390
|
+
env,
|
|
3806
4391
|
products
|
|
3807
4392
|
}) {
|
|
3808
4393
|
var _a, _b;
|
|
@@ -3819,7 +4404,23 @@ true;
|
|
|
3819
4404
|
var _a;
|
|
3820
4405
|
return (_a = groups === null || groups === void 0 ? void 0 : groups.flatMap(group => group.results)) !== null && _a !== void 0 ? _a : [];
|
|
3821
4406
|
}, [groups]);
|
|
3822
|
-
const
|
|
4407
|
+
const recents = useRecentVisits(env);
|
|
4408
|
+
const options = hasTerm ? results.map(({
|
|
4409
|
+
attributes
|
|
4410
|
+
}) => ({
|
|
4411
|
+
label: attributes.title,
|
|
4412
|
+
url: attributes.url
|
|
4413
|
+
})) : recents.visits.map(visit => {
|
|
4414
|
+
const {
|
|
4415
|
+
title,
|
|
4416
|
+
url
|
|
4417
|
+
} = describeVisit(visit);
|
|
4418
|
+
return {
|
|
4419
|
+
label: title,
|
|
4420
|
+
url
|
|
4421
|
+
};
|
|
4422
|
+
});
|
|
4423
|
+
const navigation = useResultNavigation(options, hasTerm ? results : RECENTS);
|
|
3823
4424
|
const {
|
|
3824
4425
|
highlightedIndex
|
|
3825
4426
|
} = navigation;
|
|
@@ -3845,8 +4446,8 @@ true;
|
|
|
3845
4446
|
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("input", {
|
|
3846
4447
|
"aria-activedescendant": highlightedIndex === null ? undefined : resultId(listboxId, highlightedIndex),
|
|
3847
4448
|
"aria-autocomplete": "list",
|
|
3848
|
-
"aria-controls":
|
|
3849
|
-
"aria-expanded":
|
|
4449
|
+
"aria-controls": options.length > 0 ? listboxId : undefined,
|
|
4450
|
+
"aria-expanded": options.length > 0,
|
|
3850
4451
|
"aria-label": "Search",
|
|
3851
4452
|
autoFocus: true,
|
|
3852
4453
|
onChange: event => setTerm(event.target.value),
|
|
@@ -3862,17 +4463,22 @@ true;
|
|
|
3862
4463
|
onSelect: setSelectedSlug,
|
|
3863
4464
|
products: products,
|
|
3864
4465
|
selected: product
|
|
3865
|
-
}, /*#__PURE__*/React.createElement(SearchResults, {
|
|
4466
|
+
}, hasTerm ? (/*#__PURE__*/React.createElement(SearchResults, {
|
|
3866
4467
|
highlightedIndex: highlightedIndex,
|
|
3867
4468
|
id: listboxId,
|
|
3868
4469
|
label: `${product.appName} results`,
|
|
3869
4470
|
onHighlight: navigation.highlightIndex,
|
|
3870
4471
|
search: search,
|
|
3871
4472
|
term: term
|
|
3872
|
-
}))
|
|
4473
|
+
})) : (/*#__PURE__*/React.createElement(RecentlyVisited, {
|
|
4474
|
+
highlightedIndex: highlightedIndex,
|
|
4475
|
+
id: listboxId,
|
|
4476
|
+
onHighlight: navigation.highlightIndex,
|
|
4477
|
+
...recents
|
|
4478
|
+
}))), /*#__PURE__*/React.createElement("div", {
|
|
3873
4479
|
"aria-live": "polite",
|
|
3874
4480
|
className: styles$c.visuallyHidden
|
|
3875
|
-
}, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.
|
|
4481
|
+
}, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.label : null), /*#__PURE__*/React.createElement(KeyboardHints, {
|
|
3876
4482
|
copied: navigation.copied
|
|
3877
4483
|
}));
|
|
3878
4484
|
}
|
|
@@ -3891,249 +4497,6 @@ true;
|
|
|
3891
4497
|
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];
|
|
3892
4498
|
}
|
|
3893
4499
|
|
|
3894
|
-
function parseValue(element) {
|
|
3895
|
-
const datatype = element.getAttribute("datatype");
|
|
3896
|
-
switch (datatype) {
|
|
3897
|
-
case "boolean":
|
|
3898
|
-
return element.content === "true" || element.content === "1";
|
|
3899
|
-
default:
|
|
3900
|
-
return element.content;
|
|
3901
|
-
}
|
|
3902
|
-
}
|
|
3903
|
-
function getAllPcoMeta() {
|
|
3904
|
-
const elements = document.head.querySelectorAll(`meta[property^="pco:"]`);
|
|
3905
|
-
const meta = {};
|
|
3906
|
-
elements.forEach(e => {
|
|
3907
|
-
const property = e.getAttribute("property");
|
|
3908
|
-
meta[property.substring("pco:".length)] = parseValue(e);
|
|
3909
|
-
});
|
|
3910
|
-
return meta;
|
|
3911
|
-
}
|
|
3912
|
-
// usePcoMeta only reads each tag's content. Recently Visited also needs the
|
|
3913
|
-
// pco:recently_visitable tag's `metadata` attribute, which can change while
|
|
3914
|
-
// the content stays the same, so it is tracked in its own store.
|
|
3915
|
-
function getRecentlyVisitableMetadata() {
|
|
3916
|
-
var _a, _b;
|
|
3917
|
-
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;
|
|
3918
|
-
}
|
|
3919
|
-
const store = createStore(getAllPcoMeta());
|
|
3920
|
-
const recentlyVisitableMetadataStore = createStore({
|
|
3921
|
-
metadata: getRecentlyVisitableMetadata()
|
|
3922
|
-
});
|
|
3923
|
-
function sync() {
|
|
3924
|
-
const nextState = getAllPcoMeta();
|
|
3925
|
-
if (!shallowCompare(store.getState(), nextState)) store.setState(nextState);
|
|
3926
|
-
const metadata = getRecentlyVisitableMetadata();
|
|
3927
|
-
if (recentlyVisitableMetadataStore.getState().metadata !== metadata) {
|
|
3928
|
-
recentlyVisitableMetadataStore.setState({
|
|
3929
|
-
metadata
|
|
3930
|
-
});
|
|
3931
|
-
}
|
|
3932
|
-
}
|
|
3933
|
-
const observer = new MutationObserver(sync);
|
|
3934
|
-
let hasTriggeredObserve = false;
|
|
3935
|
-
function observe() {
|
|
3936
|
-
if (hasTriggeredObserve) return;
|
|
3937
|
-
hasTriggeredObserve = true;
|
|
3938
|
-
// The module-load snapshot above can predate meta tags that appear later
|
|
3939
|
-
// in <head> (e.g. pco:resource, yielded right before </head>) if this
|
|
3940
|
-
// module's own script tag isn't deferred and runs mid-parse. Those tags
|
|
3941
|
-
// never mutate afterward, so the observer alone would never catch them.
|
|
3942
|
-
// Re-read once now, since observe() is only called once a consumer is
|
|
3943
|
-
// actually mounting, which is always after the document has fully parsed.
|
|
3944
|
-
sync();
|
|
3945
|
-
observer.observe(document.head, {
|
|
3946
|
-
attributes: true,
|
|
3947
|
-
childList: true,
|
|
3948
|
-
subtree: true
|
|
3949
|
-
});
|
|
3950
|
-
}
|
|
3951
|
-
function usePcoMeta() {
|
|
3952
|
-
observe();
|
|
3953
|
-
return shim.useSyncExternalStore(store.subscribe, () => store.getState());
|
|
3954
|
-
}
|
|
3955
|
-
function useRecentlyVisitableMetadata() {
|
|
3956
|
-
observe();
|
|
3957
|
-
return shim.useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
|
|
3958
|
-
}
|
|
3959
|
-
|
|
3960
|
-
const MAX_ENTRIES = 20;
|
|
3961
|
-
const FORMAT_VERSION = 1;
|
|
3962
|
-
const MAX_TEXT_LENGTH = 200;
|
|
3963
|
-
const PRODUCTS = ["accounts", "calendar", "check-ins", "giving", "groups", "home", "login", "people", "publishing", "registrations", "services"];
|
|
3964
|
-
function isId(value) {
|
|
3965
|
-
return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
|
|
3966
|
-
}
|
|
3967
|
-
// A PCRL looks like `<product>/v2/<resource_type>/<id>`.
|
|
3968
|
-
function isPcrl(value) {
|
|
3969
|
-
const segments = value.split("/");
|
|
3970
|
-
return segments.length === 4 && PRODUCTS.some(product => product === segments[0]) && isId(segments[3]);
|
|
3971
|
-
}
|
|
3972
|
-
function isNonBlankString(value) {
|
|
3973
|
-
return typeof value === "string" && value.trim() !== "";
|
|
3974
|
-
}
|
|
3975
|
-
function isRecord(value) {
|
|
3976
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3977
|
-
}
|
|
3978
|
-
function entryKey(entry) {
|
|
3979
|
-
if (entry.type === "resource") {
|
|
3980
|
-
return `resource:${entry.pcrl}`;
|
|
3981
|
-
} else {
|
|
3982
|
-
return `page:${entry.product}:${entry.path}`;
|
|
3983
|
-
}
|
|
3984
|
-
}
|
|
3985
|
-
function isEntry(value) {
|
|
3986
|
-
if (!isRecord(value)) {
|
|
3987
|
-
return false;
|
|
3988
|
-
} else if (value.type === "resource") {
|
|
3989
|
-
return typeof value.pcrl === "string" && isPcrl(value.pcrl);
|
|
3990
|
-
} else if (value.type === "page") {
|
|
3991
|
-
return isNonBlankString(value.product) && isNonBlankString(value.title) && isNonBlankString(value.path) && typeof value.details === "string";
|
|
3992
|
-
} else {
|
|
3993
|
-
return false;
|
|
3994
|
-
}
|
|
3995
|
-
}
|
|
3996
|
-
// Cuts on code points so a surrogate pair is never split.
|
|
3997
|
-
function clampText(value) {
|
|
3998
|
-
return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
|
|
3999
|
-
}
|
|
4000
|
-
function clampEntry(entry) {
|
|
4001
|
-
if (entry.type === "resource") {
|
|
4002
|
-
return entry;
|
|
4003
|
-
} else {
|
|
4004
|
-
return {
|
|
4005
|
-
...entry,
|
|
4006
|
-
details: clampText(entry.details),
|
|
4007
|
-
title: clampText(entry.title)
|
|
4008
|
-
};
|
|
4009
|
-
}
|
|
4010
|
-
}
|
|
4011
|
-
// Recents are disposable: anything unexpected reads as an empty list (or, for
|
|
4012
|
-
// a single bad entry, a list without it) rather than throwing.
|
|
4013
|
-
function parseRecentlyVisited(value, personId) {
|
|
4014
|
-
let parsed = null;
|
|
4015
|
-
try {
|
|
4016
|
-
parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
|
|
4017
|
-
} catch {
|
|
4018
|
-
parsed = null;
|
|
4019
|
-
}
|
|
4020
|
-
if (isRecord(parsed) && parsed.v === FORMAT_VERSION && parsed.person === personId && Array.isArray(parsed.entries)) {
|
|
4021
|
-
return parsed.entries.filter(isEntry);
|
|
4022
|
-
} else {
|
|
4023
|
-
return [];
|
|
4024
|
-
}
|
|
4025
|
-
}
|
|
4026
|
-
function addVisit(entries, visit) {
|
|
4027
|
-
const key = entryKey(visit);
|
|
4028
|
-
return [clampEntry(visit), ...entries.filter(entry => entryKey(entry) !== key)].slice(0, MAX_ENTRIES);
|
|
4029
|
-
}
|
|
4030
|
-
function serializeRecentlyVisited(entries, personId) {
|
|
4031
|
-
return JSON.stringify({
|
|
4032
|
-
entries,
|
|
4033
|
-
person: personId,
|
|
4034
|
-
v: FORMAT_VERSION
|
|
4035
|
-
});
|
|
4036
|
-
}
|
|
4037
|
-
|
|
4038
|
-
// The product whose URL, in this environment, has the current page's host.
|
|
4039
|
-
// Reversing pcoUrl handles every environment @planningcenter/url knows about,
|
|
4040
|
-
// and guarantees the stored product rebuilds a working link.
|
|
4041
|
-
function currentProduct() {
|
|
4042
|
-
var _a;
|
|
4043
|
-
return (_a = PRODUCTS.find(product => new URL(pcoUrl$1.pcoUrl(product)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
|
|
4044
|
-
}
|
|
4045
|
-
function detailsFromMetadata(metadata) {
|
|
4046
|
-
var _a;
|
|
4047
|
-
try {
|
|
4048
|
-
const details = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) === null || _a === void 0 ? void 0 : _a.details;
|
|
4049
|
-
return typeof details === "string" ? details : "";
|
|
4050
|
-
} catch {
|
|
4051
|
-
return "";
|
|
4052
|
-
}
|
|
4053
|
-
}
|
|
4054
|
-
// pco:resource is the more specific signal, so it wins when both tags are
|
|
4055
|
-
// present. A resource that isn't a PCRL is ignored rather than recorded.
|
|
4056
|
-
function visitFromPage({
|
|
4057
|
-
metadata,
|
|
4058
|
-
resource,
|
|
4059
|
-
title
|
|
4060
|
-
}) {
|
|
4061
|
-
const product = currentProduct();
|
|
4062
|
-
if (resource && isPcrl(resource)) {
|
|
4063
|
-
return {
|
|
4064
|
-
pcrl: resource,
|
|
4065
|
-
type: "resource"
|
|
4066
|
-
};
|
|
4067
|
-
} else if (title && title.trim() !== "" && product) {
|
|
4068
|
-
return {
|
|
4069
|
-
details: detailsFromMetadata(metadata),
|
|
4070
|
-
path: window.location.pathname,
|
|
4071
|
-
product,
|
|
4072
|
-
title,
|
|
4073
|
-
type: "page"
|
|
4074
|
-
};
|
|
4075
|
-
} else {
|
|
4076
|
-
return null;
|
|
4077
|
-
}
|
|
4078
|
-
}
|
|
4079
|
-
// The entry Recently Visited would record for the current page, or null when
|
|
4080
|
-
// the page isn't eligible. Shared by the recorder and the debug panel so the
|
|
4081
|
-
// panel always shows what actually gets stored.
|
|
4082
|
-
function useCurrentVisit() {
|
|
4083
|
-
const {
|
|
4084
|
-
recently_visitable: title,
|
|
4085
|
-
resource
|
|
4086
|
-
} = usePcoMeta();
|
|
4087
|
-
const metadata = useRecentlyVisitableMetadata();
|
|
4088
|
-
return React.useMemo(() => visitFromPage({
|
|
4089
|
-
metadata,
|
|
4090
|
-
resource,
|
|
4091
|
-
title
|
|
4092
|
-
}), [metadata, resource, title]);
|
|
4093
|
-
}
|
|
4094
|
-
// Products hand the person's ID to AppsProvider, not to the topbar itself,
|
|
4095
|
-
// and several pass it as a number.
|
|
4096
|
-
function useCurrentPersonId() {
|
|
4097
|
-
const {
|
|
4098
|
-
userId
|
|
4099
|
-
} = useStore();
|
|
4100
|
-
return userId == null ? undefined : String(userId);
|
|
4101
|
-
}
|
|
4102
|
-
|
|
4103
|
-
// Per-origin localStorage is a stopgap: each product keeps its own list until
|
|
4104
|
-
// PCA-409 moves this to storage shared across product subdomains. Only this
|
|
4105
|
-
// file should need to change when it does.
|
|
4106
|
-
function storageKey({
|
|
4107
|
-
env,
|
|
4108
|
-
personId
|
|
4109
|
-
}) {
|
|
4110
|
-
return `topbar.recently_visited.${env}.${personId}`;
|
|
4111
|
-
}
|
|
4112
|
-
// localStorage doesn't notify the tab that wrote it, so readers in this tab
|
|
4113
|
-
// learn about new visits from this counter instead.
|
|
4114
|
-
const changes = createStore({
|
|
4115
|
-
version: 0
|
|
4116
|
-
});
|
|
4117
|
-
function readRecentlyVisited(scope) {
|
|
4118
|
-
try {
|
|
4119
|
-
return parseRecentlyVisited(localStorage.getItem(storageKey(scope)), scope.personId);
|
|
4120
|
-
} catch {
|
|
4121
|
-
return [];
|
|
4122
|
-
}
|
|
4123
|
-
}
|
|
4124
|
-
function recordVisit(visit, scope) {
|
|
4125
|
-
if (isEntry(visit)) {
|
|
4126
|
-
try {
|
|
4127
|
-
localStorage.setItem(storageKey(scope), serializeRecentlyVisited(addVisit(readRecentlyVisited(scope), visit), scope.personId));
|
|
4128
|
-
changes.setState({
|
|
4129
|
-
version: changes.getState().version + 1
|
|
4130
|
-
});
|
|
4131
|
-
} catch {
|
|
4132
|
-
// Storage can be full or blocked; recents are disposable.
|
|
4133
|
-
}
|
|
4134
|
-
}
|
|
4135
|
-
}
|
|
4136
|
-
|
|
4137
4500
|
function useRecordRecentlyVisited({
|
|
4138
4501
|
enabled,
|
|
4139
4502
|
env
|
|
@@ -4423,7 +4786,8 @@ true;
|
|
|
4423
4786
|
margin: "auto"
|
|
4424
4787
|
}
|
|
4425
4788
|
}), !notificationsEnabled && notifications(), productSearchEnabled ? (/*#__PURE__*/React.createElement(ProductSearch, {
|
|
4426
|
-
apps: props.apps
|
|
4789
|
+
apps: props.apps,
|
|
4790
|
+
env: props.env
|
|
4427
4791
|
})) : search({
|
|
4428
4792
|
hideRoutes: () => setRoutesVisible(false),
|
|
4429
4793
|
showRoutes: () => setRoutesVisible(true)
|
|
@@ -32556,7 +32920,7 @@ true;
|
|
|
32556
32920
|
setProductSwitcherVisible(false);
|
|
32557
32921
|
setRoutesMenuVisible(visible => !visible);
|
|
32558
32922
|
} : null
|
|
32559
|
-
}, /*#__PURE__*/React.createElement(CurrentRouteComponent, null, capitalize(currentRouteText)), props.routes.length > 0 && /*#__PURE__*/React.createElement(DisclosureChevronIcon, null)), /*#__PURE__*/React.createElement("button", {
|
|
32923
|
+
}, /*#__PURE__*/React.createElement(CurrentRouteComponent, null, capitalize$1(currentRouteText)), props.routes.length > 0 && /*#__PURE__*/React.createElement(DisclosureChevronIcon, null)), /*#__PURE__*/React.createElement("button", {
|
|
32560
32924
|
type: "button",
|
|
32561
32925
|
className: styles$5["profile-button"],
|
|
32562
32926
|
onClick: () => {
|
|
@@ -32749,8 +33113,24 @@ true;
|
|
|
32749
33113
|
className: styles$4["debug-pco-resource-field-value"]
|
|
32750
33114
|
}, value));
|
|
32751
33115
|
}
|
|
33116
|
+
// Only staff ghosting or impersonating a person get pco:staff-access-level,
|
|
33117
|
+
// which keeps the panel away from regular users on staging.
|
|
32752
33118
|
function DebugPcoResource({
|
|
32753
33119
|
env
|
|
33120
|
+
}) {
|
|
33121
|
+
const {
|
|
33122
|
+
"staff-access-level": staffAccessLevel
|
|
33123
|
+
} = usePcoMeta();
|
|
33124
|
+
if (isPresent(staffAccessLevel)) {
|
|
33125
|
+
return /*#__PURE__*/React.createElement(DebugPanel, {
|
|
33126
|
+
env: env
|
|
33127
|
+
});
|
|
33128
|
+
} else {
|
|
33129
|
+
return null;
|
|
33130
|
+
}
|
|
33131
|
+
}
|
|
33132
|
+
function DebugPanel({
|
|
33133
|
+
env
|
|
32754
33134
|
}) {
|
|
32755
33135
|
const {
|
|
32756
33136
|
recently_visitable: recentlyVisitable,
|
|
@@ -32759,12 +33139,17 @@ true;
|
|
|
32759
33139
|
const [lookup, setLookup] = React.useState({
|
|
32760
33140
|
status: "idle"
|
|
32761
33141
|
});
|
|
32762
|
-
const
|
|
33142
|
+
const visit = useCurrentVisit();
|
|
33143
|
+
const personId = useCurrentPersonId();
|
|
33144
|
+
const recentlyVisited = useRecentlyVisited({
|
|
33145
|
+
env,
|
|
33146
|
+
personId
|
|
33147
|
+
});
|
|
33148
|
+
const pcrl = (visit === null || visit === void 0 ? void 0 : visit.type) === "resource" ? visit.pcrl : null;
|
|
32763
33149
|
const hasRecentlyVisitable = isPresent(recentlyVisitable);
|
|
33150
|
+
const hasInvalidResource = isPresent(resource) && !isPcrl(resource);
|
|
32764
33151
|
React.useEffect(() => {
|
|
32765
|
-
|
|
32766
|
-
// tags are present.
|
|
32767
|
-
const url = hasResource ? pcrlLookupUrl(resource, env) : null;
|
|
33152
|
+
const url = pcrl ? pcrlLookupUrl(pcrl, env) : null;
|
|
32768
33153
|
if (!url) {
|
|
32769
33154
|
setLookup({
|
|
32770
33155
|
status: "idle"
|
|
@@ -32796,7 +33181,7 @@ true;
|
|
|
32796
33181
|
return () => {
|
|
32797
33182
|
cancelled = true;
|
|
32798
33183
|
};
|
|
32799
|
-
}, [
|
|
33184
|
+
}, [pcrl, env]);
|
|
32800
33185
|
return /*#__PURE__*/React.createElement("div", {
|
|
32801
33186
|
className: classnames("staff-only", styles$4["debug-pco-resource"])
|
|
32802
33187
|
}, /*#__PURE__*/React.createElement("div", {
|
|
@@ -32811,19 +33196,27 @@ true;
|
|
|
32811
33196
|
}), /*#__PURE__*/React.createElement(Field, {
|
|
32812
33197
|
label: "pco:recently_visitable",
|
|
32813
33198
|
value: describeTag("recently_visitable", recentlyVisitable)
|
|
32814
|
-
}),
|
|
33199
|
+
}), hasInvalidResource && (/*#__PURE__*/React.createElement("p", {
|
|
32815
33200
|
className: styles$4["debug-pco-resource-note"]
|
|
32816
|
-
}, "
|
|
33201
|
+
}, "pco:resource isn't a PCRL (product/v2/type/id), so Recently Visited ignores it.")), visit === null && (/*#__PURE__*/React.createElement("p", {
|
|
32817
33202
|
className: styles$4["debug-pco-resource-note"]
|
|
32818
|
-
}, "
|
|
33203
|
+
}, "This page is not eligible for Recently Visited")), pcrl && hasRecentlyVisitable && (/*#__PURE__*/React.createElement("p", {
|
|
32819
33204
|
className: styles$4["debug-pco-resource-note"]
|
|
32820
|
-
}, "
|
|
33205
|
+
}, "Both tags are present \u2014 pco:resource takes precedence for the Recently Visited entry.")), pcrl && lookup.status === "loading" && (/*#__PURE__*/React.createElement("p", {
|
|
33206
|
+
className: styles$4["debug-pco-resource-note"]
|
|
33207
|
+
}, "Looking up resource\u2026")), pcrl && lookup.status === "error" && (/*#__PURE__*/React.createElement("p", {
|
|
33208
|
+
className: styles$4["debug-pco-resource-note"]
|
|
33209
|
+
}, lookup.message)), pcrl && lookup.status === "loaded" && (/*#__PURE__*/React.createElement("pre", {
|
|
33210
|
+
className: styles$4["debug-pco-resource-json"]
|
|
33211
|
+
}, JSON.stringify(lookup.json, null, 2))), (visit === null || visit === void 0 ? void 0 : visit.type) === "page" && (/*#__PURE__*/React.createElement("p", {
|
|
32821
33212
|
className: styles$4["debug-pco-resource-note"]
|
|
32822
|
-
},
|
|
33213
|
+
}, "Recently Visited title: ", visit.title)), /*#__PURE__*/React.createElement("span", {
|
|
33214
|
+
className: styles$4["debug-pco-resource-field-label"]
|
|
33215
|
+
}, "Recently Visited"), personId ? (/*#__PURE__*/React.createElement("pre", {
|
|
32823
33216
|
className: styles$4["debug-pco-resource-json"]
|
|
32824
|
-
}, JSON.stringify(
|
|
33217
|
+
}, JSON.stringify(recentlyVisited, null, 2))) : (/*#__PURE__*/React.createElement("p", {
|
|
32825
33218
|
className: styles$4["debug-pco-resource-note"]
|
|
32826
|
-
}, "
|
|
33219
|
+
}, "No current person, so nothing is recorded.")));
|
|
32827
33220
|
}
|
|
32828
33221
|
|
|
32829
33222
|
var styles$3 = {"drawer-wrap":"drawer-wrap-J0rSL","is-visible":"is-visible-KTME8","drawer":"drawer-j7f4l"};
|
|
@@ -33028,15 +33421,50 @@ true;
|
|
|
33028
33421
|
}));
|
|
33029
33422
|
};
|
|
33030
33423
|
|
|
33424
|
+
const collections = {
|
|
33425
|
+
general: general__namespace,
|
|
33426
|
+
toolbar: toolbar__namespace
|
|
33427
|
+
};
|
|
33428
|
+
const paths = collections;
|
|
33429
|
+
function Icon({
|
|
33430
|
+
name,
|
|
33431
|
+
size = 16,
|
|
33432
|
+
style,
|
|
33433
|
+
theme,
|
|
33434
|
+
title,
|
|
33435
|
+
...props
|
|
33436
|
+
}) {
|
|
33437
|
+
const [collection, symbol] = name.split(".");
|
|
33438
|
+
return /*#__PURE__*/React.createElement("svg", {
|
|
33439
|
+
"aria-hidden": title ? undefined : true,
|
|
33440
|
+
fill: "currentColor",
|
|
33441
|
+
height: size,
|
|
33442
|
+
role: title ? "img" : undefined,
|
|
33443
|
+
style: {
|
|
33444
|
+
color: theme && `var(${theme})`,
|
|
33445
|
+
flexShrink: 0,
|
|
33446
|
+
...style
|
|
33447
|
+
},
|
|
33448
|
+
viewBox: "0 0 16 16",
|
|
33449
|
+
width: size,
|
|
33450
|
+
...props
|
|
33451
|
+
}, title && /*#__PURE__*/React.createElement("title", null, title), /*#__PURE__*/React.createElement("path", {
|
|
33452
|
+
d: paths[collection][symbol]
|
|
33453
|
+
}));
|
|
33454
|
+
}
|
|
33455
|
+
|
|
33031
33456
|
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"};
|
|
33032
33457
|
|
|
33033
33458
|
const COLOR_MODES = [{
|
|
33459
|
+
icon: "toolbar.sun",
|
|
33034
33460
|
label: "Light",
|
|
33035
33461
|
value: "light"
|
|
33036
33462
|
}, {
|
|
33463
|
+
icon: "toolbar.moon",
|
|
33037
33464
|
label: "Dark",
|
|
33038
33465
|
value: "dark"
|
|
33039
33466
|
}, {
|
|
33467
|
+
icon: "toolbar.display",
|
|
33040
33468
|
label: "System",
|
|
33041
33469
|
value: "system"
|
|
33042
33470
|
}];
|
|
@@ -33054,8 +33482,9 @@ true;
|
|
|
33054
33482
|
}, "Appearance", " "), /*#__PURE__*/React.createElement("div", {
|
|
33055
33483
|
className: styles$2["color-mode-switcher-modes"]
|
|
33056
33484
|
}, COLOR_MODES.map(({
|
|
33057
|
-
|
|
33058
|
-
label
|
|
33485
|
+
icon,
|
|
33486
|
+
label,
|
|
33487
|
+
value
|
|
33059
33488
|
}) => (/*#__PURE__*/React.createElement("label", {
|
|
33060
33489
|
key: value
|
|
33061
33490
|
}, /*#__PURE__*/React.createElement("input", {
|
|
@@ -33072,8 +33501,9 @@ true;
|
|
|
33072
33501
|
document.documentElement.setAttribute(RESOLVED_COLOR_MODE_ATTRIBUTE, resolveColorMode(colorMode));
|
|
33073
33502
|
updateColorMode(colorMode);
|
|
33074
33503
|
}
|
|
33075
|
-
}), /*#__PURE__*/React.createElement(
|
|
33076
|
-
|
|
33504
|
+
}), /*#__PURE__*/React.createElement(Icon, {
|
|
33505
|
+
name: icon,
|
|
33506
|
+
size: 15
|
|
33077
33507
|
}), label)))));
|
|
33078
33508
|
}
|
|
33079
33509
|
function updateColorMode(mode) {
|
|
@@ -33088,28 +33518,6 @@ true;
|
|
|
33088
33518
|
method: "POST"
|
|
33089
33519
|
});
|
|
33090
33520
|
}
|
|
33091
|
-
const COLOR_MODE_ICON_PATHS = {
|
|
33092
|
-
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",
|
|
33093
|
-
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",
|
|
33094
|
-
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"
|
|
33095
|
-
};
|
|
33096
|
-
function ColorModeIcon({
|
|
33097
|
-
mode
|
|
33098
|
-
}) {
|
|
33099
|
-
return /*#__PURE__*/React.createElement("svg", {
|
|
33100
|
-
width: "16",
|
|
33101
|
-
height: "16",
|
|
33102
|
-
viewBox: "0 0 14 14",
|
|
33103
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
33104
|
-
"aria-hidden": "true"
|
|
33105
|
-
}, /*#__PURE__*/React.createElement("path", {
|
|
33106
|
-
d: COLOR_MODE_ICON_PATHS[mode],
|
|
33107
|
-
strokeWidth: "1.5",
|
|
33108
|
-
fill: "none",
|
|
33109
|
-
strokeLinecap: "round",
|
|
33110
|
-
strokeLinejoin: "round"
|
|
33111
|
-
}));
|
|
33112
|
-
}
|
|
33113
33521
|
|
|
33114
33522
|
const requestClearConnectedPeopleCache = () => remove();
|
|
33115
33523
|
function UserProfile({
|
|
@@ -33172,10 +33580,10 @@ true;
|
|
|
33172
33580
|
alignment: "center",
|
|
33173
33581
|
spacing: 0.5,
|
|
33174
33582
|
width: "100%"
|
|
33175
|
-
}, /*#__PURE__*/React.createElement(
|
|
33583
|
+
}, /*#__PURE__*/React.createElement(Icon, {
|
|
33176
33584
|
name: mfaConfigured ? "general.lock" : "general.unlock",
|
|
33177
|
-
size:
|
|
33178
|
-
|
|
33585
|
+
size: 12,
|
|
33586
|
+
theme: mfaConfigured ? "--t-icon-color-status-success" : "--t-icon-color-status-error"
|
|
33179
33587
|
}), /*#__PURE__*/React.createElement(tapestryReact.Text, {
|
|
33180
33588
|
size: 4,
|
|
33181
33589
|
weight: 400,
|
|
@@ -33223,9 +33631,10 @@ true;
|
|
|
33223
33631
|
}, /*#__PURE__*/React.createElement(tapestryReact.Text, {
|
|
33224
33632
|
color: "var(--text-color-default-secondary)",
|
|
33225
33633
|
size: 3
|
|
33226
|
-
}, "Log out"), /*#__PURE__*/React.createElement(
|
|
33634
|
+
}, "Log out"), /*#__PURE__*/React.createElement(Icon, {
|
|
33227
33635
|
name: "general.intoBox",
|
|
33228
|
-
|
|
33636
|
+
size: 14,
|
|
33637
|
+
theme: "--t-icon-color-secondary"
|
|
33229
33638
|
})))), env !== "production" && /*#__PURE__*/React.createElement(DebugPcoResource, {
|
|
33230
33639
|
env: env
|
|
33231
33640
|
}), hasConnectedPeople && (/*#__PURE__*/React.createElement(tapestryReact.StackView, {
|
|
@@ -33287,26 +33696,42 @@ true;
|
|
|
33287
33696
|
|
|
33288
33697
|
var ActionType;
|
|
33289
33698
|
(function (ActionType) {
|
|
33699
|
+
ActionType["clearPending"] = "clearPending";
|
|
33290
33700
|
ActionType["close"] = "close";
|
|
33291
33701
|
ActionType["mount"] = "mount";
|
|
33292
33702
|
ActionType["open"] = "open";
|
|
33703
|
+
ActionType["setPending"] = "setPending";
|
|
33293
33704
|
ActionType["unmount"] = "unmount";
|
|
33294
33705
|
})(ActionType || (ActionType = {}));
|
|
33295
33706
|
const initialState = {
|
|
33296
33707
|
currentContent: null,
|
|
33297
|
-
open: false
|
|
33708
|
+
open: false,
|
|
33709
|
+
pending: null
|
|
33298
33710
|
};
|
|
33299
33711
|
const reducer = (state, action) => {
|
|
33712
|
+
var _a, _b;
|
|
33300
33713
|
switch (action.type) {
|
|
33301
33714
|
case ActionType.mount:
|
|
33302
33715
|
return {
|
|
33303
33716
|
...state,
|
|
33304
|
-
currentContent: action.payload.content
|
|
33717
|
+
currentContent: action.payload.content,
|
|
33718
|
+
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
|
|
33305
33719
|
};
|
|
33306
33720
|
case ActionType.unmount:
|
|
33307
33721
|
return {
|
|
33308
33722
|
...state,
|
|
33309
|
-
currentContent: null
|
|
33723
|
+
currentContent: null,
|
|
33724
|
+
pending: null
|
|
33725
|
+
};
|
|
33726
|
+
case ActionType.setPending:
|
|
33727
|
+
return {
|
|
33728
|
+
...state,
|
|
33729
|
+
pending: action.payload
|
|
33730
|
+
};
|
|
33731
|
+
case ActionType.clearPending:
|
|
33732
|
+
return {
|
|
33733
|
+
...state,
|
|
33734
|
+
pending: null
|
|
33310
33735
|
};
|
|
33311
33736
|
case ActionType.open:
|
|
33312
33737
|
return {
|
|
@@ -33327,11 +33752,179 @@ true;
|
|
|
33327
33752
|
|
|
33328
33753
|
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"};
|
|
33329
33754
|
|
|
33330
|
-
|
|
33755
|
+
// PCO Box serves TLS by default, and a bare http:// frame inside an https
|
|
33756
|
+
// product page is blocked as mixed content, so follow the page's scheme.
|
|
33757
|
+
const iframeSrc = () => `${window.location.protocol}//pco.test/iframe`;
|
|
33758
|
+
const STAFF_PINK = "var(--t-fill-color-product-staff)";
|
|
33759
|
+
// The pco.test domain is only ever served by a local PCO Box, so the tool
|
|
33760
|
+
// stays hidden in staging and production — and on Topbar's own dev server,
|
|
33761
|
+
// which runs on localhost and has no pco.test to frame.
|
|
33762
|
+
function isPcoTestHost() {
|
|
33763
|
+
const {
|
|
33764
|
+
hostname
|
|
33765
|
+
} = window.location;
|
|
33766
|
+
return hostname === "pco.test" || hostname.endsWith(".pco.test");
|
|
33767
|
+
}
|
|
33768
|
+
// Flattened from the source artwork's two outline paths — its gradients,
|
|
33769
|
+
// filters and clip paths carry nothing at icon size. Those paths are authored
|
|
33770
|
+
// on a 128px canvas, hence the transform: it fits the pair into the same ~24px
|
|
33771
|
+
// optical box the sibling toolbar icons occupy. Filled explicitly rather than
|
|
33772
|
+
// with `currentColor` so the mark stays staff pink instead of inheriting the
|
|
33773
|
+
// toolbar's text color and its hover/active states.
|
|
33774
|
+
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";
|
|
33775
|
+
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";
|
|
33776
|
+
function ToolsIcon() {
|
|
33777
|
+
return /*#__PURE__*/React.createElement("svg", {
|
|
33778
|
+
"aria-hidden": "true",
|
|
33779
|
+
width: "32",
|
|
33780
|
+
height: "32",
|
|
33781
|
+
viewBox: "0 0 32 32",
|
|
33782
|
+
xmlns: "http://www.w3.org/2000/svg"
|
|
33783
|
+
}, /*#__PURE__*/React.createElement("g", {
|
|
33784
|
+
fill: STAFF_PINK,
|
|
33785
|
+
transform: "translate(2.92 2.66) scale(.204)"
|
|
33786
|
+
}, /*#__PURE__*/React.createElement("path", {
|
|
33787
|
+
d: WRENCH
|
|
33788
|
+
}), /*#__PURE__*/React.createElement("path", {
|
|
33789
|
+
d: SCREWDRIVER
|
|
33790
|
+
})));
|
|
33791
|
+
}
|
|
33792
|
+
// A cross-origin frame's own history object is unreachable (touching it throws
|
|
33793
|
+
// a SecurityError), but the frame's navigations do land in the top-level joint
|
|
33794
|
+
// session history, so the *parent's* back/forward traverse them while leaving
|
|
33795
|
+
// the host page in place. Counting frame loads tracks how deep we are, which is
|
|
33796
|
+
// what keeps a click on Back from stepping past the frame's first page and
|
|
33797
|
+
// navigating the host app instead. Loads we caused are attributed to the
|
|
33798
|
+
// traversal that requested them rather than counted as new navigations.
|
|
33799
|
+
function useFrameHistory() {
|
|
33800
|
+
const [steps, setSteps] = React.useState({
|
|
33801
|
+
back: 0,
|
|
33802
|
+
forward: 0
|
|
33803
|
+
});
|
|
33804
|
+
// Traversal has to read the current counts outside of render, so the ref is
|
|
33805
|
+
// the source of truth and state exists to paint the buttons.
|
|
33806
|
+
const stepsRef = React.useRef(steps);
|
|
33807
|
+
const pendingTraversal = React.useRef(null);
|
|
33808
|
+
const frameIsLoaded = React.useRef(false);
|
|
33809
|
+
const moveTo = React.useCallback(next => {
|
|
33810
|
+
stepsRef.current = next;
|
|
33811
|
+
setSteps(next);
|
|
33812
|
+
}, []);
|
|
33813
|
+
const onFrameLoad = React.useCallback(() => {
|
|
33814
|
+
if (!frameIsLoaded.current) {
|
|
33815
|
+
// The frame's first page is the floor, not a step.
|
|
33816
|
+
frameIsLoaded.current = true;
|
|
33817
|
+
} else {
|
|
33818
|
+
const traversal = pendingTraversal.current;
|
|
33819
|
+
pendingTraversal.current = null;
|
|
33820
|
+
const {
|
|
33821
|
+
back,
|
|
33822
|
+
forward
|
|
33823
|
+
} = stepsRef.current;
|
|
33824
|
+
if (traversal === "back") {
|
|
33825
|
+
moveTo({
|
|
33826
|
+
back: back - 1,
|
|
33827
|
+
forward: forward + 1
|
|
33828
|
+
});
|
|
33829
|
+
} else if (traversal === "forward") {
|
|
33830
|
+
moveTo({
|
|
33831
|
+
back: back + 1,
|
|
33832
|
+
forward: forward - 1
|
|
33833
|
+
});
|
|
33834
|
+
} else {
|
|
33835
|
+
moveTo({
|
|
33836
|
+
back: back + 1,
|
|
33837
|
+
forward: 0
|
|
33838
|
+
});
|
|
33839
|
+
}
|
|
33840
|
+
}
|
|
33841
|
+
}, [moveTo]);
|
|
33842
|
+
// Guarded here rather than trusting the button's disabled state: traversing
|
|
33843
|
+
// with no frame step left would navigate the host app out from under us.
|
|
33844
|
+
const traverse = React.useCallback(direction => {
|
|
33845
|
+
if (stepsRef.current[direction] > 0) {
|
|
33846
|
+
pendingTraversal.current = direction;
|
|
33847
|
+
window.history[direction]();
|
|
33848
|
+
}
|
|
33849
|
+
}, []);
|
|
33850
|
+
return {
|
|
33851
|
+
onFrameLoad,
|
|
33852
|
+
steps,
|
|
33853
|
+
traverse
|
|
33854
|
+
};
|
|
33855
|
+
}
|
|
33856
|
+
function NavButton({
|
|
33857
|
+
direction,
|
|
33858
|
+
disabled,
|
|
33859
|
+
onClick
|
|
33860
|
+
}) {
|
|
33861
|
+
return /*#__PURE__*/React.createElement(tapestryReact.Button
|
|
33862
|
+
// Tapestry's `disabled` suppresses the click but emits no ARIA state.
|
|
33863
|
+
, {
|
|
33864
|
+
"aria-disabled": disabled,
|
|
33865
|
+
disabled: disabled,
|
|
33866
|
+
hover: {
|
|
33867
|
+
backgroundColor: "var(--t-fill-color-neutral-070)"
|
|
33868
|
+
},
|
|
33869
|
+
icon: {
|
|
33870
|
+
fill: "var(--t-icon-color-dim)",
|
|
33871
|
+
name: direction === "back" ? "general.leftChevron" : "general.rightChevron",
|
|
33872
|
+
size: "md"
|
|
33873
|
+
},
|
|
33874
|
+
onClick: onClick,
|
|
33875
|
+
opacity: disabled ? 0.35 : 1,
|
|
33876
|
+
theme: false,
|
|
33877
|
+
title: direction === "back" ? "Back" : "Forward"
|
|
33878
|
+
});
|
|
33879
|
+
}
|
|
33880
|
+
function DeveloperToolsComponent({
|
|
33881
|
+
closeDrawer
|
|
33882
|
+
}) {
|
|
33883
|
+
const {
|
|
33884
|
+
onFrameLoad,
|
|
33885
|
+
steps,
|
|
33886
|
+
traverse
|
|
33887
|
+
} = useFrameHistory();
|
|
33888
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(DrawerHeader, {
|
|
33889
|
+
closeDrawer: closeDrawer
|
|
33890
|
+
}, /*#__PURE__*/React.createElement(tapestryReact.StackView, {
|
|
33891
|
+
alignment: "center",
|
|
33892
|
+
axis: "horizontal",
|
|
33893
|
+
spacing: 0.25
|
|
33894
|
+
}, /*#__PURE__*/React.createElement(NavButton, {
|
|
33895
|
+
direction: "back",
|
|
33896
|
+
disabled: steps.back === 0,
|
|
33897
|
+
onClick: () => traverse("back")
|
|
33898
|
+
}), /*#__PURE__*/React.createElement(NavButton, {
|
|
33899
|
+
direction: "forward",
|
|
33900
|
+
disabled: steps.forward === 0,
|
|
33901
|
+
onClick: () => traverse("forward")
|
|
33902
|
+
}), /*#__PURE__*/React.createElement(DrawerHeading, {
|
|
33903
|
+
paddingLeft: 0.5
|
|
33904
|
+
}, "Developer Tools"))), /*#__PURE__*/React.createElement(tapestryReact.Box, {
|
|
33905
|
+
as: "iframe",
|
|
33906
|
+
border: "none",
|
|
33907
|
+
flex: "1",
|
|
33908
|
+
minHeight: 0,
|
|
33909
|
+
onLoad: onFrameLoad,
|
|
33910
|
+
src: iframeSrc(),
|
|
33911
|
+
title: "Developer Tools",
|
|
33912
|
+
width: "100%"
|
|
33913
|
+
}));
|
|
33914
|
+
}
|
|
33915
|
+
const DeveloperTools = {
|
|
33916
|
+
component: DeveloperToolsComponent,
|
|
33917
|
+
icons: {
|
|
33918
|
+
component: ToolsIcon
|
|
33919
|
+
},
|
|
33920
|
+
label: "Developer Tools",
|
|
33921
|
+
// No space: `name` becomes an element id and a pendo attribute.
|
|
33922
|
+
name: "DeveloperTools"
|
|
33923
|
+
};
|
|
33331
33924
|
|
|
33332
33925
|
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"};
|
|
33333
33926
|
|
|
33334
|
-
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";
|
|
33927
|
+
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";
|
|
33335
33928
|
// Same stops as the AskAIBanner border gradient, so the two stay in sync.
|
|
33336
33929
|
const GRADIENT_STOPS = [{
|
|
33337
33930
|
color: "var(--t-fill-color-tag-teal-040)",
|
|
@@ -33380,7 +33973,7 @@ true;
|
|
|
33380
33973
|
stopColor: color
|
|
33381
33974
|
}
|
|
33382
33975
|
})))))), /*#__PURE__*/React.createElement("path", {
|
|
33383
|
-
d: SPARKLE_PATH,
|
|
33976
|
+
d: SPARKLE_PATH$1,
|
|
33384
33977
|
fill: isGradient ? `url(#${gradientId})` : "currentColor"
|
|
33385
33978
|
}));
|
|
33386
33979
|
}
|
|
@@ -33788,16 +34381,24 @@ true;
|
|
|
33788
34381
|
// not the path, so updating it is a same-document navigation that doesn't
|
|
33789
34382
|
// reload the iframe, but also lets the route survive a forced reload (e.g.
|
|
33790
34383
|
// Turbo re-parenting the topbar).
|
|
33791
|
-
|
|
34384
|
+
//
|
|
34385
|
+
// `panel` loads picobot as its own toolbar panel rather than inside Helpdesk.
|
|
34386
|
+
function buildPicobotSrc(origin, path, {
|
|
34387
|
+
panel = false
|
|
34388
|
+
} = {}) {
|
|
33792
34389
|
const [rawPath, queryPart] = path.replace(/^\/+/, "").split("?", 2);
|
|
33793
34390
|
const safePath = rawPath.split("/").filter(segment => segment && segment !== "." && segment !== "..").join("/");
|
|
33794
34391
|
const fragment = queryPart !== undefined ? `${safePath}?${queryPart}` : safePath;
|
|
33795
|
-
const
|
|
34392
|
+
const url = new URL("topbar", origin);
|
|
34393
|
+
if (panel) url.searchParams.set("panel", "1");
|
|
34394
|
+
const base = url.toString();
|
|
33796
34395
|
return fragment ? `${base}#${fragment}` : base;
|
|
33797
34396
|
}
|
|
33798
34397
|
|
|
33799
34398
|
const LOAD_TIMEOUT_MS = 3000;
|
|
33800
|
-
function createPicobotIframe(picobotOrigin
|
|
34399
|
+
function createPicobotIframe(picobotOrigin, {
|
|
34400
|
+
panel = false
|
|
34401
|
+
} = {}) {
|
|
33801
34402
|
return /*#__PURE__*/React.forwardRef(function PicobotIframe({
|
|
33802
34403
|
currentPath: iframePath,
|
|
33803
34404
|
onChangePath: onIframeChangePath,
|
|
@@ -33853,7 +34454,9 @@ true;
|
|
|
33853
34454
|
(_b = (_a = iframeRef.current) === null || _a === void 0 ? void 0 : _a.contentWindow) === null || _b === void 0 ? void 0 : _b.postMessage(message, picobotOrigin);
|
|
33854
34455
|
}
|
|
33855
34456
|
}), []);
|
|
33856
|
-
const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath
|
|
34457
|
+
const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath, {
|
|
34458
|
+
panel
|
|
34459
|
+
});
|
|
33857
34460
|
return /*#__PURE__*/React.createElement("iframe", {
|
|
33858
34461
|
ref: iframeRef,
|
|
33859
34462
|
src: iframeSrc,
|
|
@@ -33892,22 +34495,28 @@ true;
|
|
|
33892
34495
|
currentPath,
|
|
33893
34496
|
env,
|
|
33894
34497
|
helpdeskConfig,
|
|
33895
|
-
|
|
34498
|
+
isPicobotLoading,
|
|
34499
|
+
onChangePath,
|
|
34500
|
+
openPicobot
|
|
33896
34501
|
}) {
|
|
33897
34502
|
const PicobotIframe = React.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot")), [env]);
|
|
34503
|
+
const loadingFallback = /*#__PURE__*/React.createElement(StackView$1, {
|
|
34504
|
+
flex: 1,
|
|
34505
|
+
axis: "vertical"
|
|
34506
|
+
}, /*#__PURE__*/React.createElement(DrawerHeader, {
|
|
34507
|
+
closeDrawer: closeDrawer
|
|
34508
|
+
}, /*#__PURE__*/React.createElement(StackView$1, {
|
|
34509
|
+
axis: "horizontal",
|
|
34510
|
+
spacing: 1
|
|
34511
|
+
}, /*#__PURE__*/React.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React.createElement(LoadingIndicator$1, null))));
|
|
34512
|
+
// The add-on reads its args once, so mounting before the gate answers
|
|
34513
|
+
// would leave out openPicobot for good.
|
|
34514
|
+
if (isPicobotLoading) return loadingFallback;
|
|
33898
34515
|
return /*#__PURE__*/React.createElement(addOns.UiExtensionFirstPartyComponent, {
|
|
33899
34516
|
grow: 1,
|
|
33900
34517
|
display: "flex",
|
|
33901
34518
|
extensionId: env === "staging" ? "42" : "11",
|
|
33902
|
-
loadingFallback:
|
|
33903
|
-
flex: 1,
|
|
33904
|
-
axis: "vertical"
|
|
33905
|
-
}, /*#__PURE__*/React.createElement(DrawerHeader, {
|
|
33906
|
-
closeDrawer: closeDrawer
|
|
33907
|
-
}, /*#__PURE__*/React.createElement(StackView$1, {
|
|
33908
|
-
axis: "horizontal",
|
|
33909
|
-
spacing: 1
|
|
33910
|
-
}, /*#__PURE__*/React.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React.createElement(LoadingIndicator$1, null)))),
|
|
34519
|
+
loadingFallback: loadingFallback,
|
|
33911
34520
|
additionalComponents: {
|
|
33912
34521
|
AskAIBanner: {
|
|
33913
34522
|
component: AskAIBanner,
|
|
@@ -34005,7 +34614,11 @@ true;
|
|
|
34005
34614
|
currentPageParams: location.search,
|
|
34006
34615
|
currentPath,
|
|
34007
34616
|
helpdeskConfig,
|
|
34008
|
-
onChangePath
|
|
34617
|
+
onChangePath,
|
|
34618
|
+
// Its presence tells helpdesk that Picobot has its own panel.
|
|
34619
|
+
...(openPicobot && {
|
|
34620
|
+
openPicobot
|
|
34621
|
+
})
|
|
34009
34622
|
}
|
|
34010
34623
|
});
|
|
34011
34624
|
}
|
|
@@ -34552,6 +35165,142 @@ true;
|
|
|
34552
35165
|
name: "Notifications"
|
|
34553
35166
|
};
|
|
34554
35167
|
|
|
35168
|
+
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";
|
|
35169
|
+
// Helpdesk reads this context off the email path to open its email form the
|
|
35170
|
+
// same way a handoff inside its own drawer does.
|
|
35171
|
+
function emailPathWithContext(context = {}) {
|
|
35172
|
+
const params = new URLSearchParams();
|
|
35173
|
+
for (const key of ["conversationId", "source", "turnIndex"]) {
|
|
35174
|
+
const value = context[key];
|
|
35175
|
+
if (value !== null && value !== undefined) params.set(key, String(value));
|
|
35176
|
+
}
|
|
35177
|
+
return `email?${params}`;
|
|
35178
|
+
}
|
|
35179
|
+
function PicobotComponent({
|
|
35180
|
+
clearPending,
|
|
35181
|
+
closeDrawer,
|
|
35182
|
+
currentPath,
|
|
35183
|
+
env,
|
|
35184
|
+
onChangePath,
|
|
35185
|
+
openTool,
|
|
35186
|
+
pending
|
|
35187
|
+
}) {
|
|
35188
|
+
const PicobotIframe = React.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot"), {
|
|
35189
|
+
panel: true
|
|
35190
|
+
}), [env]);
|
|
35191
|
+
const picobotRef = React.useRef(null);
|
|
35192
|
+
const [isPicobotReady, setIsPicobotReady] = React.useState(false);
|
|
35193
|
+
// Picobot has no listener until it sends READY, so openPicobot's draft or
|
|
35194
|
+
// conversation waits for it.
|
|
35195
|
+
React.useEffect(function handOverPendingOncePicobotIsReady() {
|
|
35196
|
+
var _a, _b;
|
|
35197
|
+
if (!pending || !isPicobotReady) return;
|
|
35198
|
+
const {
|
|
35199
|
+
conversationId,
|
|
35200
|
+
draft,
|
|
35201
|
+
ticketExternalId
|
|
35202
|
+
} = pending;
|
|
35203
|
+
if (conversationId) {
|
|
35204
|
+
(_a = picobotRef.current) === null || _a === void 0 ? void 0 : _a.postMessage({
|
|
35205
|
+
payload: {
|
|
35206
|
+
conversation_id: conversationId,
|
|
35207
|
+
// Picobot asks whether this resolved the ticket, and closes it.
|
|
35208
|
+
...(ticketExternalId && {
|
|
35209
|
+
external_id: ticketExternalId,
|
|
35210
|
+
source: "ticket_submitted"
|
|
35211
|
+
})
|
|
35212
|
+
},
|
|
35213
|
+
type: "LOAD_CONVERSATION"
|
|
35214
|
+
});
|
|
35215
|
+
} else if (draft) {
|
|
35216
|
+
// Not both: Picobot cancels a conversation load when a prefill
|
|
35217
|
+
// starts a new conversation.
|
|
35218
|
+
(_b = picobotRef.current) === null || _b === void 0 ? void 0 : _b.postMessage({
|
|
35219
|
+
payload: {
|
|
35220
|
+
source: "email_form",
|
|
35221
|
+
text: draft
|
|
35222
|
+
},
|
|
35223
|
+
type: "PREFILL_COMPOSER"
|
|
35224
|
+
});
|
|
35225
|
+
}
|
|
35226
|
+
clearPending();
|
|
35227
|
+
}, [pending, isPicobotReady, clearPending]);
|
|
35228
|
+
const [iframeErrored, setIframeErrored] = React.useState(false);
|
|
35229
|
+
const [mountKey, setMountKey] = React.useState(0);
|
|
35230
|
+
const handleRetry = React.useCallback(() => {
|
|
35231
|
+
setIframeErrored(false);
|
|
35232
|
+
setIsPicobotReady(false);
|
|
35233
|
+
setMountKey(key => key + 1);
|
|
35234
|
+
}, []);
|
|
35235
|
+
const handleMessage = React.useCallback(data => {
|
|
35236
|
+
var _a;
|
|
35237
|
+
const message = data;
|
|
35238
|
+
switch (message === null || message === void 0 ? void 0 : message.type) {
|
|
35239
|
+
case "CLOSE_DRAWER":
|
|
35240
|
+
return closeDrawer();
|
|
35241
|
+
case "READY":
|
|
35242
|
+
return setIsPicobotReady(true);
|
|
35243
|
+
case "PICOBOT_SUPPORT_HANDOFF":
|
|
35244
|
+
return openTool("helpdesk", ((_a = message.payload) === null || _a === void 0 ? void 0 : _a.method) === "zoom" ? "phone" : emailPathWithContext(message.payload));
|
|
35245
|
+
case "IFRAME_LOAD_ERROR":
|
|
35246
|
+
console.warn("topbar: Picobot iframe failed to load");
|
|
35247
|
+
return setIframeErrored(true);
|
|
35248
|
+
}
|
|
35249
|
+
}, [closeDrawer, openTool]);
|
|
35250
|
+
if (iframeErrored) {
|
|
35251
|
+
return /*#__PURE__*/React.createElement(LoadError, {
|
|
35252
|
+
onContactSupport: () => openTool("helpdesk", "email"),
|
|
35253
|
+
onRetry: handleRetry
|
|
35254
|
+
});
|
|
35255
|
+
}
|
|
35256
|
+
return /*#__PURE__*/React.createElement(PicobotIframe, {
|
|
35257
|
+
key: mountKey,
|
|
35258
|
+
ref: picobotRef,
|
|
35259
|
+
currentPath: currentPath,
|
|
35260
|
+
onChangePath: onChangePath,
|
|
35261
|
+
onMessage: handleMessage
|
|
35262
|
+
});
|
|
35263
|
+
}
|
|
35264
|
+
function LoadError({
|
|
35265
|
+
onContactSupport,
|
|
35266
|
+
onRetry
|
|
35267
|
+
}) {
|
|
35268
|
+
return /*#__PURE__*/React.createElement(tapestryReact.StackView, {
|
|
35269
|
+
axis: "vertical",
|
|
35270
|
+
flex: 1,
|
|
35271
|
+
alignment: "center",
|
|
35272
|
+
distribution: "center",
|
|
35273
|
+
padding: 3,
|
|
35274
|
+
spacing: 1
|
|
35275
|
+
}, /*#__PURE__*/React.createElement(tapestryReact.Heading, {
|
|
35276
|
+
level: 2
|
|
35277
|
+
}, "Picobot is temporarily unavailable"), /*#__PURE__*/React.createElement(tapestryReact.Text, {
|
|
35278
|
+
size: 5,
|
|
35279
|
+
color: "var(--t-text-color-secondary)"
|
|
35280
|
+
}, "Try again or contact support."), /*#__PURE__*/React.createElement(tapestryReact.Box, {
|
|
35281
|
+
paddingTop: 2
|
|
35282
|
+
}, /*#__PURE__*/React.createElement(tapestryReact.Button, {
|
|
35283
|
+
onClick: onRetry,
|
|
35284
|
+
iconLeft: "general.refresh",
|
|
35285
|
+
theme: "primary",
|
|
35286
|
+
variant: "fill",
|
|
35287
|
+
title: "Retry"
|
|
35288
|
+
})), /*#__PURE__*/React.createElement(tapestryReact.Button, {
|
|
35289
|
+
onClick: onContactSupport,
|
|
35290
|
+
variant: "naked",
|
|
35291
|
+
theme: "primary",
|
|
35292
|
+
title: "Contact support"
|
|
35293
|
+
}));
|
|
35294
|
+
}
|
|
35295
|
+
const Picobot = {
|
|
35296
|
+
component: PicobotComponent,
|
|
35297
|
+
icons: {
|
|
35298
|
+
staticDotPath: SPARKLE_PATH,
|
|
35299
|
+
staticPath: SPARKLE_PATH
|
|
35300
|
+
},
|
|
35301
|
+
name: "Picobot"
|
|
35302
|
+
};
|
|
35303
|
+
|
|
34555
35304
|
const {
|
|
34556
35305
|
LoadingIndicator,
|
|
34557
35306
|
StackView
|
|
@@ -34947,6 +35696,41 @@ true;
|
|
|
34947
35696
|
};
|
|
34948
35697
|
};
|
|
34949
35698
|
|
|
35699
|
+
// Picobot's API rejects requests without a version header, and apiRequest
|
|
35700
|
+
// sets its own headers, so the version rides in on the fetch.
|
|
35701
|
+
const picobotFetch = (url, opts) => defaultFetch(url, {
|
|
35702
|
+
...opts,
|
|
35703
|
+
headers: {
|
|
35704
|
+
...(opts === null || opts === void 0 ? void 0 : opts.headers),
|
|
35705
|
+
"X-PCO-API-Version": "2026-08-11"
|
|
35706
|
+
}
|
|
35707
|
+
});
|
|
35708
|
+
// Routes wait on the gate, so a slow Picobot can't hold them up for long.
|
|
35709
|
+
const GATE_TIMEOUT_MS = 3000;
|
|
35710
|
+
const usePicobotConfig = ({
|
|
35711
|
+
isEnabled
|
|
35712
|
+
}) => {
|
|
35713
|
+
// Undefined until Picobot answers.
|
|
35714
|
+
const [panelEnabled, setPanelEnabled] = React.useState();
|
|
35715
|
+
React.useEffect(() => {
|
|
35716
|
+
if (!isEnabled) return undefined;
|
|
35717
|
+
// Whatever settles first wins, so a late answer can't reopen the gate.
|
|
35718
|
+
const settle = enabled => setPanelEnabled(current => current !== null && current !== void 0 ? current : enabled);
|
|
35719
|
+
const timeout = setTimeout(() => settle(false), GATE_TIMEOUT_MS);
|
|
35720
|
+
apiRequest(picobotFetch, `${window.location.origin}/~api/picobot/v2/me`).then(({
|
|
35721
|
+
json
|
|
35722
|
+
}) => {
|
|
35723
|
+
var _a, _b;
|
|
35724
|
+
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);
|
|
35725
|
+
}).catch(() => settle(false)).finally(() => clearTimeout(timeout));
|
|
35726
|
+
return () => clearTimeout(timeout);
|
|
35727
|
+
}, [isEnabled]);
|
|
35728
|
+
return {
|
|
35729
|
+
displayPicobotIcon: panelEnabled === true,
|
|
35730
|
+
isLoading: isEnabled && panelEnabled === undefined
|
|
35731
|
+
};
|
|
35732
|
+
};
|
|
35733
|
+
|
|
34950
35734
|
const Profile = {
|
|
34951
35735
|
component: UserProfile,
|
|
34952
35736
|
icons: {
|
|
@@ -34957,10 +35741,16 @@ true;
|
|
|
34957
35741
|
};
|
|
34958
35742
|
|
|
34959
35743
|
const DURATION = 100;
|
|
35744
|
+
// Exempt paths match regardless of query, so `email?conversationId=…` is
|
|
35745
|
+
// exempt along with `email`.
|
|
34960
35746
|
function isToolPathExemptFromStorage(toolName, path) {
|
|
34961
35747
|
var _a, _b;
|
|
34962
35748
|
const tool = toolsRouteMap[toolName];
|
|
34963
|
-
|
|
35749
|
+
const [pathWithoutQuery] = path.split("?");
|
|
35750
|
+
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;
|
|
35751
|
+
}
|
|
35752
|
+
function isConversationPath(path) {
|
|
35753
|
+
return Boolean(path === null || path === void 0 ? void 0 : path.startsWith("conversations/"));
|
|
34964
35754
|
}
|
|
34965
35755
|
function scrollToolDrawerToTop(toolName) {
|
|
34966
35756
|
var _a;
|
|
@@ -34973,6 +35763,7 @@ true;
|
|
|
34973
35763
|
Chat,
|
|
34974
35764
|
Helpdesk,
|
|
34975
35765
|
Notifications,
|
|
35766
|
+
Picobot,
|
|
34976
35767
|
Profile,
|
|
34977
35768
|
Tasks
|
|
34978
35769
|
};
|
|
@@ -35000,7 +35791,8 @@ true;
|
|
|
35000
35791
|
});
|
|
35001
35792
|
const {
|
|
35002
35793
|
currentContent,
|
|
35003
|
-
open
|
|
35794
|
+
open,
|
|
35795
|
+
pending
|
|
35004
35796
|
} = state;
|
|
35005
35797
|
const mounted = !!currentContent;
|
|
35006
35798
|
const storeData = useStore();
|
|
@@ -35013,6 +35805,15 @@ true;
|
|
|
35013
35805
|
...(env === "development" ? _storeDataOverride !== null && _storeDataOverride !== void 0 ? _storeDataOverride : {} : {})
|
|
35014
35806
|
};
|
|
35015
35807
|
const updateStore = useUpdateStore();
|
|
35808
|
+
const {
|
|
35809
|
+
displayPicobotIcon,
|
|
35810
|
+
isLoading: isPicobotGateLoading
|
|
35811
|
+
} = usePicobotConfig({
|
|
35812
|
+
isEnabled: Boolean(userId)
|
|
35813
|
+
});
|
|
35814
|
+
// Picobot is a kill-switched tool: until its gate says the panel is
|
|
35815
|
+
// enabled, its routes are held, and if it never does, they're ignored.
|
|
35816
|
+
const routableTool = React.useCallback(name => name === Picobot.name.toLowerCase() && !displayPicobotIcon ? undefined : toolsRouteMap[name], [displayPicobotIcon]);
|
|
35016
35817
|
React.useEffect(() => {
|
|
35017
35818
|
if (open) {
|
|
35018
35819
|
document.body.style.overflow = "hidden";
|
|
@@ -35100,7 +35901,9 @@ true;
|
|
|
35100
35901
|
});
|
|
35101
35902
|
}, [clearRoute]);
|
|
35102
35903
|
React.useEffect(() => {
|
|
35103
|
-
if (!toolbarFragmentRoute)
|
|
35904
|
+
if (!toolbarFragmentRoute || !routableTool(toolbarFragmentRoute.tool)) {
|
|
35905
|
+
return;
|
|
35906
|
+
}
|
|
35104
35907
|
const isExempt = isToolPathExemptFromStorage(toolbarFragmentRoute.tool, toolbarFragmentRoute.path);
|
|
35105
35908
|
if (isExempt) return;
|
|
35106
35909
|
const nextState = {
|
|
@@ -35110,12 +35913,32 @@ true;
|
|
|
35110
35913
|
if (!shallowCompare(nextState, toolStateCookie)) {
|
|
35111
35914
|
setToolStateCookie(nextState);
|
|
35112
35915
|
}
|
|
35113
|
-
}, [toolbarFragmentRoute, toolStateCookie]);
|
|
35916
|
+
}, [toolbarFragmentRoute, toolStateCookie, routableTool]);
|
|
35114
35917
|
React.useEffect(() => {
|
|
35115
|
-
|
|
35116
|
-
|
|
35918
|
+
// Helpdesk conversations may belong to Picobot, so they wait for its gate
|
|
35919
|
+
// rather than flashing open in Helpdesk first.
|
|
35920
|
+
const mayRedirectToPicobot = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path) && (isPicobotGateLoading || displayPicobotIcon);
|
|
35921
|
+
const tool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && !mayRedirectToPicobot && routableTool(toolbarFragmentRoute.tool);
|
|
35922
|
+
if (tool && (currentContent !== tool || !open)) {
|
|
35923
|
+
setToolAndOpenDrawer(tool);
|
|
35117
35924
|
}
|
|
35118
|
-
}, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, currentContent, open]);
|
|
35925
|
+
}, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path, currentContent, open, routableTool, isPicobotGateLoading, displayPicobotIcon]);
|
|
35926
|
+
const openTool = React.useCallback((name, path, pendingValue) => {
|
|
35927
|
+
const tool = routableTool(name.toLowerCase());
|
|
35928
|
+
if (!tool) return;
|
|
35929
|
+
dispatch({
|
|
35930
|
+
payload: pendingValue === undefined ? null : {
|
|
35931
|
+
tool: tool.name,
|
|
35932
|
+
value: pendingValue
|
|
35933
|
+
},
|
|
35934
|
+
type: ActionType.setPending
|
|
35935
|
+
});
|
|
35936
|
+
onChangeToolbarFragmentRoute(tool.name.toLowerCase(), path);
|
|
35937
|
+
scrollToolDrawerToTop(tool.name);
|
|
35938
|
+
}, [onChangeToolbarFragmentRoute, routableTool]);
|
|
35939
|
+
const clearPending = React.useCallback(() => dispatch({
|
|
35940
|
+
type: ActionType.clearPending
|
|
35941
|
+
}), []);
|
|
35119
35942
|
const toggleDrawer = content => {
|
|
35120
35943
|
if (!open) {
|
|
35121
35944
|
return openDrawer(content);
|
|
@@ -35132,12 +35955,52 @@ true;
|
|
|
35132
35955
|
joltPromise: storeData === null || storeData === void 0 ? void 0 : storeData.joltPromise,
|
|
35133
35956
|
userId
|
|
35134
35957
|
});
|
|
35135
|
-
|
|
35958
|
+
// Picobot conversations moved from Helpdesk to the Picobot panel, so
|
|
35959
|
+
// send old links and saved paths there once the panel is enabled.
|
|
35960
|
+
React.useEffect(() => {
|
|
35961
|
+
if (!displayPicobotIcon) return;
|
|
35962
|
+
if ((toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path)) {
|
|
35963
|
+
onChangeToolbarFragmentRoute(Picobot.name.toLowerCase(), toolbarFragmentRoute.path);
|
|
35964
|
+
}
|
|
35965
|
+
}, [displayPicobotIcon, toolbarFragmentRoute, onChangeToolbarFragmentRoute]);
|
|
35966
|
+
React.useEffect(() => {
|
|
35967
|
+
if (!displayPicobotIcon) return;
|
|
35968
|
+
const {
|
|
35969
|
+
helpdesk,
|
|
35970
|
+
picobot
|
|
35971
|
+
} = toolStateCookie;
|
|
35972
|
+
if (!isConversationPath(helpdesk)) return;
|
|
35973
|
+
setToolStateCookie({
|
|
35974
|
+
...toolStateCookie,
|
|
35975
|
+
helpdesk: "",
|
|
35976
|
+
picobot: picobot || helpdesk
|
|
35977
|
+
});
|
|
35978
|
+
}, [displayPicobotIcon, toolStateCookie]);
|
|
35979
|
+
const openPicobot = React.useCallback(options => {
|
|
35980
|
+
var _a;
|
|
35981
|
+
// Moving an open Picobot off its path reloads or navigates the iframe,
|
|
35982
|
+
// which would race the options it's being handed.
|
|
35983
|
+
const path = currentContent === Picobot ? (_a = toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path) !== null && _a !== void 0 ? _a : "" : "";
|
|
35984
|
+
openTool(Picobot.name, path, options);
|
|
35985
|
+
}, [openTool, currentContent, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path]);
|
|
35986
|
+
React.useEffect(() => {
|
|
35987
|
+
if (!displayPicobotIcon) return undefined;
|
|
35988
|
+
window.topbar = window.topbar || {};
|
|
35989
|
+
window.topbar.openPicobot = openPicobot;
|
|
35990
|
+
return () => {
|
|
35991
|
+
delete window.topbar.openPicobot;
|
|
35992
|
+
};
|
|
35993
|
+
}, [displayPicobotIcon, openPicobot]);
|
|
35994
|
+
const TOOLS = [Notifications, Tasks, displayChatIcon && Chat, displayPicobotIcon && Picobot, isPcoTestHost() && DeveloperTools].filter(Boolean);
|
|
35136
35995
|
const handleToolOnClick = tool => {
|
|
35137
35996
|
toggleDrawer(tool);
|
|
35138
35997
|
scrollToolDrawerToTop(tool.name);
|
|
35139
35998
|
};
|
|
35140
35999
|
const toolSpecificProps = {
|
|
36000
|
+
Helpdesk: {
|
|
36001
|
+
isPicobotLoading: isPicobotGateLoading,
|
|
36002
|
+
openPicobot: displayPicobotIcon ? openPicobot : undefined
|
|
36003
|
+
},
|
|
35141
36004
|
Profile: {
|
|
35142
36005
|
appName,
|
|
35143
36006
|
closeDrawer,
|
|
@@ -35160,6 +36023,7 @@ true;
|
|
|
35160
36023
|
const routeIsForCurrentTool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && toolsRouteMap[toolbarFragmentRoute.tool] === currentContent;
|
|
35161
36024
|
const currentToolProps = {
|
|
35162
36025
|
appName,
|
|
36026
|
+
clearPending,
|
|
35163
36027
|
closeDrawer,
|
|
35164
36028
|
currentPath: routeIsForCurrentTool ? toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path : null,
|
|
35165
36029
|
env,
|
|
@@ -35168,6 +36032,8 @@ true;
|
|
|
35168
36032
|
joltPromise,
|
|
35169
36033
|
onChangePath,
|
|
35170
36034
|
openDrawer: () => currentContent && openDrawer(currentContent),
|
|
36035
|
+
openTool,
|
|
36036
|
+
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,
|
|
35171
36037
|
userId,
|
|
35172
36038
|
...(currentContent ? toolSpecificProps[currentContent.name] : {})
|
|
35173
36039
|
};
|
|
@@ -35195,21 +36061,20 @@ true;
|
|
|
35195
36061
|
name: "toolbar.planningCenter",
|
|
35196
36062
|
size: "24px"
|
|
35197
36063
|
})), [HelpDeskTool, ...TOOLS].map(tool => {
|
|
36064
|
+
var _a;
|
|
35198
36065
|
const current = open && currentContent === tool;
|
|
35199
36066
|
const indicator = indicatorState[tool.name];
|
|
35200
36067
|
return /*#__PURE__*/React.createElement(ToolbarButton, {
|
|
35201
36068
|
current: current,
|
|
35202
36069
|
onClick: () => handleToolOnClick(tool),
|
|
35203
36070
|
id: getDataPendoAttribute(tool.name),
|
|
35204
|
-
"aria-label": `Open ${tool.name} drawer`,
|
|
36071
|
+
"aria-label": `Open ${(_a = tool.label) !== null && _a !== void 0 ? _a : tool.name} drawer`,
|
|
35205
36072
|
"data-pendo": getDataPendoAttribute(tool.name),
|
|
35206
36073
|
key: tool.name
|
|
35207
|
-
},
|
|
35208
|
-
|
|
35209
|
-
|
|
35210
|
-
|
|
35211
|
-
dot: true
|
|
35212
|
-
})));
|
|
36074
|
+
}, /*#__PURE__*/React.createElement(ToolbarToolIcon, {
|
|
36075
|
+
icons: tool.icons,
|
|
36076
|
+
dot: !!indicator
|
|
36077
|
+
}));
|
|
35213
36078
|
})), /*#__PURE__*/React.createElement(DrawerWrap, {
|
|
35214
36079
|
isVisible: open
|
|
35215
36080
|
}, mounted && (/*#__PURE__*/React.createElement(Drawer, {
|
|
@@ -35233,6 +36098,19 @@ true;
|
|
|
35233
36098
|
...restProps
|
|
35234
36099
|
}, children);
|
|
35235
36100
|
};
|
|
36101
|
+
function ToolbarToolIcon({
|
|
36102
|
+
dot,
|
|
36103
|
+
icons
|
|
36104
|
+
}) {
|
|
36105
|
+
if ("component" in icons) {
|
|
36106
|
+
return /*#__PURE__*/React.createElement(icons.component, null);
|
|
36107
|
+
} else {
|
|
36108
|
+
return /*#__PURE__*/React.createElement(ToolbarButtonIcon, {
|
|
36109
|
+
path: dot ? icons.staticDotPath : icons.staticPath,
|
|
36110
|
+
dot: dot
|
|
36111
|
+
});
|
|
36112
|
+
}
|
|
36113
|
+
}
|
|
35236
36114
|
function ToolbarButtonIcon({
|
|
35237
36115
|
dot,
|
|
35238
36116
|
path
|