@planningcenter/topbar 12.3.0-qa-947.0 → 12.3.0-qa-937.3
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.js +570 -459
- package/dist/topbar.js.map +1 -1
- package/dist/topbar.modern.js +570 -459
- package/dist/topbar.modern.js.map +1 -1
- package/dist/topbar.module.js +570 -459
- package/dist/topbar.module.js.map +1 -1
- package/dist/topbar.umd.js +570 -459
- package/dist/topbar.umd.js.map +1 -1
- package/package.json +1 -1
package/dist/topbar.js
CHANGED
|
@@ -3266,45 +3266,6 @@ function saveLastProduct(slug) {
|
|
|
3266
3266
|
}
|
|
3267
3267
|
}
|
|
3268
3268
|
|
|
3269
|
-
// Products open modals from many libraries, not all of which mark themselves
|
|
3270
|
-
// with roles, so a modal is recognized by what it does: cover the page.
|
|
3271
|
-
function isModalOpen() {
|
|
3272
|
-
const {
|
|
3273
|
-
clientHeight,
|
|
3274
|
-
clientWidth
|
|
3275
|
-
} = document.documentElement;
|
|
3276
|
-
const element = document.elementFromPoint(clientWidth / 2, clientHeight / 2);
|
|
3277
|
-
return element !== null && (isInNativeDialog(element) || hasBackdrop(element, clientWidth, clientHeight));
|
|
3278
|
-
}
|
|
3279
|
-
// A native modal's backdrop is drawn by the browser rather than being an
|
|
3280
|
-
// element; hit-testing it lands on the dialog itself.
|
|
3281
|
-
function isInNativeDialog(element) {
|
|
3282
|
-
return element.closest("dialog[open]") !== null;
|
|
3283
|
-
}
|
|
3284
|
-
function hasBackdrop(element, width, height) {
|
|
3285
|
-
let current = element;
|
|
3286
|
-
while (current && !isBackdrop(current, width, height)) {
|
|
3287
|
-
current = current.parentElement;
|
|
3288
|
-
}
|
|
3289
|
-
return current !== null;
|
|
3290
|
-
}
|
|
3291
|
-
// Some backdrops start below the topbar rather than at the top of the screen,
|
|
3292
|
-
// so the layer only has to span the width and reach from the center to the
|
|
3293
|
-
// bottom. Its own box has to reach the center: a bar along the bottom can
|
|
3294
|
-
// hold a panel that pops up over the center, like Services' media player.
|
|
3295
|
-
// react-aria leaves a closed modal's backdrop up while it fades out, search's
|
|
3296
|
-
// own included, so one that's on its way out doesn't count.
|
|
3297
|
-
function isBackdrop(element, width, height) {
|
|
3298
|
-
const rect = element.getBoundingClientRect();
|
|
3299
|
-
return !element.matches("[data-exiting]") && getComputedStyle(element).position === "fixed" && rect.width >= width - SCROLLBAR_SLACK && rect.top <= height / 2 && rect.bottom >= height - ROUNDING_SLACK;
|
|
3300
|
-
}
|
|
3301
|
-
// react-aria keeps a classic scrollbar's gutter reserved while its modals lock
|
|
3302
|
-
// scrolling, so their backdrops stop short of the right edge by its width.
|
|
3303
|
-
const SCROLLBAR_SLACK = 24;
|
|
3304
|
-
// Rects are fractional at non-100% zoom, but clientWidth and clientHeight are
|
|
3305
|
-
// rounded to whole pixels.
|
|
3306
|
-
const ROUNDING_SLACK = 1;
|
|
3307
|
-
|
|
3308
3269
|
var styles$c = {"visuallyHidden":"visuallyHidden-C28ju"};
|
|
3309
3270
|
|
|
3310
3271
|
var styles$b = {"tab":"tab-lQLDx"};
|
|
@@ -3436,11 +3397,10 @@ function useRecentlyVisitableMetadata() {
|
|
|
3436
3397
|
return shim.useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
|
|
3437
3398
|
}
|
|
3438
3399
|
|
|
3439
|
-
const MAX_ENTRIES$1 =
|
|
3400
|
+
const MAX_ENTRIES$1 = 20;
|
|
3401
|
+
const FORMAT_VERSION = 1;
|
|
3440
3402
|
const MAX_TEXT_LENGTH = 200;
|
|
3441
3403
|
const PRODUCTS = ["accounts", "calendar", "check-ins", "giving", "groups", "home", "login", "people", "publishing", "registrations", "services"];
|
|
3442
|
-
// Login has no pages worth returning to.
|
|
3443
|
-
const APPS = PRODUCTS.filter(product => product !== "login");
|
|
3444
3404
|
function isId(value) {
|
|
3445
3405
|
return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
|
|
3446
3406
|
}
|
|
@@ -3449,47 +3409,26 @@ function isPcrl(value) {
|
|
|
3449
3409
|
const segments = value.split("/");
|
|
3450
3410
|
return segments.length === 4 && PRODUCTS.some(product => product === segments[0]) && isId(segments[3]);
|
|
3451
3411
|
}
|
|
3452
|
-
function isApp(value) {
|
|
3453
|
-
return APPS.some(app => app === value);
|
|
3454
|
-
}
|
|
3455
|
-
// Links are built by resolving the path against its app's origin, so a stored
|
|
3456
|
-
// path must not be able to resolve anywhere else (`//evil.com`, `/\evil.com`).
|
|
3457
|
-
function isPath(value) {
|
|
3458
|
-
const origin = "https://app.invalid";
|
|
3459
|
-
try {
|
|
3460
|
-
return typeof value === "string" && value.startsWith("/") && new URL(value, origin).origin === origin;
|
|
3461
|
-
} catch {
|
|
3462
|
-
return false;
|
|
3463
|
-
}
|
|
3464
|
-
}
|
|
3465
3412
|
function isNonBlankString(value) {
|
|
3466
3413
|
return typeof value === "string" && value.trim() !== "";
|
|
3467
3414
|
}
|
|
3468
|
-
function isOptionalString(value) {
|
|
3469
|
-
return value === undefined || typeof value === "string";
|
|
3470
|
-
}
|
|
3471
3415
|
function isRecord(value) {
|
|
3472
3416
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3473
3417
|
}
|
|
3474
|
-
function isPageMetadata(value) {
|
|
3475
|
-
return value === undefined || isRecord(value) && isOptionalString(value.details) && isOptionalString(value.type);
|
|
3476
|
-
}
|
|
3477
|
-
// The query string is part of the link but not of the page's identity, so
|
|
3478
|
-
// revisiting with other filters moves the existing entry instead of adding one.
|
|
3479
3418
|
function entryKey(entry) {
|
|
3480
3419
|
if (entry.type === "resource") {
|
|
3481
3420
|
return `resource:${entry.pcrl}`;
|
|
3482
3421
|
} else {
|
|
3483
|
-
return `page:${entry.
|
|
3422
|
+
return `page:${entry.product}:${entry.path}`;
|
|
3484
3423
|
}
|
|
3485
3424
|
}
|
|
3486
3425
|
function isEntry(value) {
|
|
3487
|
-
if (!isRecord(value)
|
|
3426
|
+
if (!isRecord(value)) {
|
|
3488
3427
|
return false;
|
|
3489
3428
|
} else if (value.type === "resource") {
|
|
3490
3429
|
return typeof value.pcrl === "string" && isPcrl(value.pcrl);
|
|
3491
3430
|
} else if (value.type === "page") {
|
|
3492
|
-
return
|
|
3431
|
+
return isNonBlankString(value.product) && isNonBlankString(value.title) && isNonBlankString(value.path) && typeof value.details === "string";
|
|
3493
3432
|
} else {
|
|
3494
3433
|
return false;
|
|
3495
3434
|
}
|
|
@@ -3498,175 +3437,58 @@ function isEntry(value) {
|
|
|
3498
3437
|
function clampText(value) {
|
|
3499
3438
|
return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
|
|
3500
3439
|
}
|
|
3501
|
-
function
|
|
3502
|
-
details,
|
|
3503
|
-
type
|
|
3504
|
-
}) {
|
|
3505
|
-
return {
|
|
3506
|
-
...(details && {
|
|
3507
|
-
details: clampText(details)
|
|
3508
|
-
}),
|
|
3509
|
-
...(type && {
|
|
3510
|
-
type: clampText(type)
|
|
3511
|
-
})
|
|
3512
|
-
};
|
|
3513
|
-
}
|
|
3514
|
-
function clampVisit(visit) {
|
|
3515
|
-
if (visit.type === "resource") {
|
|
3516
|
-
return visit;
|
|
3517
|
-
} else {
|
|
3518
|
-
return {
|
|
3519
|
-
...visit,
|
|
3520
|
-
...(visit.metadata && {
|
|
3521
|
-
metadata: clampMetadata(visit.metadata)
|
|
3522
|
-
}),
|
|
3523
|
-
title: clampText(visit.title.trim())
|
|
3524
|
-
};
|
|
3525
|
-
}
|
|
3526
|
-
}
|
|
3527
|
-
function toEntry(visit, visitedAt) {
|
|
3528
|
-
return {
|
|
3529
|
-
...clampVisit(visit),
|
|
3530
|
-
visitedAt: visitedAt.toISOString()
|
|
3531
|
-
};
|
|
3532
|
-
}
|
|
3533
|
-
function knownMetadata({
|
|
3534
|
-
details,
|
|
3535
|
-
type
|
|
3536
|
-
}) {
|
|
3537
|
-
if (details === undefined && type === undefined) {
|
|
3538
|
-
return undefined;
|
|
3539
|
-
} else {
|
|
3540
|
-
return {
|
|
3541
|
-
...(details !== undefined && {
|
|
3542
|
-
details
|
|
3543
|
-
}),
|
|
3544
|
-
...(type !== undefined && {
|
|
3545
|
-
type
|
|
3546
|
-
})
|
|
3547
|
-
};
|
|
3548
|
-
}
|
|
3549
|
-
}
|
|
3550
|
-
// user-store-sync rejects a whole write over one field it doesn't know, so
|
|
3551
|
-
// anything else an entry carries (like the version older entries stored) is
|
|
3552
|
-
// dropped on the way in.
|
|
3553
|
-
function withKnownFields(entry) {
|
|
3440
|
+
function clampEntry(entry) {
|
|
3554
3441
|
if (entry.type === "resource") {
|
|
3555
|
-
return
|
|
3556
|
-
pcrl: entry.pcrl,
|
|
3557
|
-
type: entry.type,
|
|
3558
|
-
visitedAt: entry.visitedAt
|
|
3559
|
-
};
|
|
3442
|
+
return entry;
|
|
3560
3443
|
} else {
|
|
3561
|
-
const metadata = entry.metadata && knownMetadata(entry.metadata);
|
|
3562
3444
|
return {
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
}),
|
|
3567
|
-
path: entry.path,
|
|
3568
|
-
title: entry.title,
|
|
3569
|
-
type: entry.type,
|
|
3570
|
-
visitedAt: entry.visitedAt
|
|
3445
|
+
...entry,
|
|
3446
|
+
details: clampText(entry.details),
|
|
3447
|
+
title: clampText(entry.title)
|
|
3571
3448
|
};
|
|
3572
3449
|
}
|
|
3573
3450
|
}
|
|
3574
3451
|
// Recents are disposable: anything unexpected reads as an empty list (or, for
|
|
3575
3452
|
// a single bad entry, a list without it) rather than throwing.
|
|
3576
|
-
function
|
|
3577
|
-
|
|
3578
|
-
return value.filter(isEntry).map(withKnownFields);
|
|
3579
|
-
} else {
|
|
3580
|
-
return [];
|
|
3581
|
-
}
|
|
3582
|
-
}
|
|
3583
|
-
function parseRecentlyVisited(value) {
|
|
3453
|
+
function parseRecentlyVisited(value, personId) {
|
|
3454
|
+
let parsed = null;
|
|
3584
3455
|
try {
|
|
3585
|
-
|
|
3456
|
+
parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
|
|
3586
3457
|
} catch {
|
|
3458
|
+
parsed = null;
|
|
3459
|
+
}
|
|
3460
|
+
if (isRecord(parsed) && parsed.v === FORMAT_VERSION && parsed.person === personId && Array.isArray(parsed.entries)) {
|
|
3461
|
+
return parsed.entries.filter(isEntry);
|
|
3462
|
+
} else {
|
|
3587
3463
|
return [];
|
|
3588
3464
|
}
|
|
3589
3465
|
}
|
|
3590
|
-
function addVisit(entries,
|
|
3591
|
-
const key = entryKey(
|
|
3592
|
-
return [
|
|
3593
|
-
}
|
|
3594
|
-
// The service may write timestamps with fewer fractional digits than
|
|
3595
|
-
// toISOString, so they're compared as times rather than strings.
|
|
3596
|
-
function visitTime({
|
|
3597
|
-
visitedAt
|
|
3598
|
-
}) {
|
|
3599
|
-
return Date.parse(visitedAt) || 0;
|
|
3600
|
-
}
|
|
3601
|
-
// One list from several, newest visit first wherever it was recorded.
|
|
3602
|
-
function mergeEntries(...lists) {
|
|
3603
|
-
const seen = new Set();
|
|
3604
|
-
return lists.flat().sort((a, b) => visitTime(b) - visitTime(a)).filter(entry => {
|
|
3605
|
-
const key = entryKey(entry);
|
|
3606
|
-
const isFirst = !seen.has(key);
|
|
3607
|
-
seen.add(key);
|
|
3608
|
-
return isFirst;
|
|
3609
|
-
}).slice(0, MAX_ENTRIES$1);
|
|
3466
|
+
function addVisit(entries, visit) {
|
|
3467
|
+
const key = entryKey(visit);
|
|
3468
|
+
return [clampEntry(visit), ...entries.filter(entry => entryKey(entry) !== key)].slice(0, MAX_ENTRIES$1);
|
|
3610
3469
|
}
|
|
3611
|
-
function
|
|
3612
|
-
return
|
|
3470
|
+
function serializeRecentlyVisited(entries, personId) {
|
|
3471
|
+
return JSON.stringify({
|
|
3472
|
+
entries,
|
|
3473
|
+
person: personId,
|
|
3474
|
+
v: FORMAT_VERSION
|
|
3475
|
+
});
|
|
3613
3476
|
}
|
|
3614
3477
|
|
|
3615
|
-
// The
|
|
3478
|
+
// The product whose URL, in this environment, has the current page's host.
|
|
3616
3479
|
// Reversing pcoUrl handles every environment @planningcenter/url knows about,
|
|
3617
|
-
// and guarantees the stored
|
|
3618
|
-
function
|
|
3480
|
+
// and guarantees the stored product rebuilds a working link.
|
|
3481
|
+
function currentProduct() {
|
|
3619
3482
|
var _a;
|
|
3620
|
-
return (_a =
|
|
3483
|
+
return (_a = PRODUCTS.find(product => new URL(pcoUrl$1.pcoUrl(product)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
|
|
3621
3484
|
}
|
|
3622
|
-
function
|
|
3623
|
-
return typeof value === "string" && value !== "";
|
|
3624
|
-
}
|
|
3625
|
-
// The tag's metadata is product-supplied JSON; only its string `type` and
|
|
3626
|
-
// `details` carry over, and an entry without either has no metadata at all.
|
|
3627
|
-
function metadataFromPage(metadata) {
|
|
3485
|
+
function detailsFromMetadata(metadata) {
|
|
3628
3486
|
var _a;
|
|
3629
3487
|
try {
|
|
3630
|
-
const {
|
|
3631
|
-
|
|
3632
|
-
type
|
|
3633
|
-
} = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) !== null && _a !== void 0 ? _a : {};
|
|
3634
|
-
if (isFilled(details) || isFilled(type)) {
|
|
3635
|
-
return {
|
|
3636
|
-
...(isFilled(details) && {
|
|
3637
|
-
details
|
|
3638
|
-
}),
|
|
3639
|
-
...(isFilled(type) && {
|
|
3640
|
-
type
|
|
3641
|
-
})
|
|
3642
|
-
};
|
|
3643
|
-
} else {
|
|
3644
|
-
return undefined;
|
|
3645
|
-
}
|
|
3488
|
+
const details = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) === null || _a === void 0 ? void 0 : _a.details;
|
|
3489
|
+
return typeof details === "string" ? details : "";
|
|
3646
3490
|
} catch {
|
|
3647
|
-
return
|
|
3648
|
-
}
|
|
3649
|
-
}
|
|
3650
|
-
// Staff impersonating someone browse under `/O<org id>/P<person id>/…`.
|
|
3651
|
-
function withoutImpersonation(segments) {
|
|
3652
|
-
const [org = "", person = ""] = segments;
|
|
3653
|
-
if (org.startsWith("O") && isId(org.slice(1)) && person.startsWith("P") && isId(person.slice(1))) {
|
|
3654
|
-
return segments.slice(2);
|
|
3655
|
-
} else {
|
|
3656
|
-
return segments;
|
|
3657
|
-
}
|
|
3658
|
-
}
|
|
3659
|
-
// Normalized the way home-mobile does it, so a page visited on web and on
|
|
3660
|
-
// mobile is one entry once the list syncs.
|
|
3661
|
-
function pathFromLocation({
|
|
3662
|
-
pathname,
|
|
3663
|
-
search
|
|
3664
|
-
}) {
|
|
3665
|
-
const segments = withoutImpersonation(pathname.split("/").filter(Boolean));
|
|
3666
|
-
if (segments.length === 0) {
|
|
3667
|
-
return "/";
|
|
3668
|
-
} else {
|
|
3669
|
-
return `/${segments.join("/")}${search.length > 1 ? search : ""}`;
|
|
3491
|
+
return "";
|
|
3670
3492
|
}
|
|
3671
3493
|
}
|
|
3672
3494
|
// pco:resource is the more specific signal, so it wins when both tags are
|
|
@@ -3676,20 +3498,17 @@ function visitFromPage({
|
|
|
3676
3498
|
resource,
|
|
3677
3499
|
title
|
|
3678
3500
|
}) {
|
|
3679
|
-
const
|
|
3501
|
+
const product = currentProduct();
|
|
3680
3502
|
if (resource && isPcrl(resource)) {
|
|
3681
3503
|
return {
|
|
3682
3504
|
pcrl: resource,
|
|
3683
3505
|
type: "resource"
|
|
3684
3506
|
};
|
|
3685
|
-
} else if (title && title.trim() !== "" &&
|
|
3686
|
-
const pageMetadata = metadataFromPage(metadata);
|
|
3507
|
+
} else if (title && title.trim() !== "" && product) {
|
|
3687
3508
|
return {
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
}),
|
|
3692
|
-
path: pathFromLocation(window.location),
|
|
3509
|
+
details: detailsFromMetadata(metadata),
|
|
3510
|
+
path: window.location.pathname,
|
|
3511
|
+
product,
|
|
3693
3512
|
title,
|
|
3694
3513
|
type: "page"
|
|
3695
3514
|
};
|
|
@@ -3721,136 +3540,9 @@ function useCurrentPersonId() {
|
|
|
3721
3540
|
return userId == null ? undefined : String(userId);
|
|
3722
3541
|
}
|
|
3723
3542
|
|
|
3724
|
-
//
|
|
3725
|
-
//
|
|
3726
|
-
|
|
3727
|
-
const MAX_ATTEMPTS = 3;
|
|
3728
|
-
// A product without the endpoint answers 404 for the rest of the session, so
|
|
3729
|
-
// it's asked once per tab rather than on every page.
|
|
3730
|
-
const UNAVAILABLE_KEY = "topbar.user_store_sync.unavailable";
|
|
3731
|
-
// Per person, so switching connected people never writes one person's visits
|
|
3732
|
-
// with another's token.
|
|
3733
|
-
function tokenKey(personId) {
|
|
3734
|
-
return `topbar.user_store_sync.token.${personId}`;
|
|
3735
|
-
}
|
|
3736
|
-
function readSession(key) {
|
|
3737
|
-
try {
|
|
3738
|
-
return sessionStorage.getItem(key);
|
|
3739
|
-
} catch {
|
|
3740
|
-
return null;
|
|
3741
|
-
}
|
|
3742
|
-
}
|
|
3743
|
-
function writeSession(key, value) {
|
|
3744
|
-
try {
|
|
3745
|
-
sessionStorage.setItem(key, value);
|
|
3746
|
-
} catch {
|
|
3747
|
-
// Without sessionStorage the next request just mints again.
|
|
3748
|
-
}
|
|
3749
|
-
}
|
|
3750
|
-
function isUserStoreSyncUnavailable() {
|
|
3751
|
-
return readSession(UNAVAILABLE_KEY) === "true";
|
|
3752
|
-
}
|
|
3753
|
-
function isToken(value) {
|
|
3754
|
-
return typeof value === "object" && value !== null && typeof value.token === "string" && typeof value.url === "string" && typeof value.expiresAt === "number";
|
|
3755
|
-
}
|
|
3756
|
-
function cachedToken(personId) {
|
|
3757
|
-
var _a;
|
|
3758
|
-
try {
|
|
3759
|
-
const token = JSON.parse((_a = readSession(tokenKey(personId))) !== null && _a !== void 0 ? _a : "null");
|
|
3760
|
-
if (isToken(token) && token.expiresAt - REFRESH_MARGIN_MS > Date.now()) {
|
|
3761
|
-
return token;
|
|
3762
|
-
} else {
|
|
3763
|
-
return null;
|
|
3764
|
-
}
|
|
3765
|
-
} catch {
|
|
3766
|
-
return null;
|
|
3767
|
-
}
|
|
3768
|
-
}
|
|
3769
|
-
async function mintToken(personId) {
|
|
3770
|
-
const {
|
|
3771
|
-
json,
|
|
3772
|
-
ok,
|
|
3773
|
-
response
|
|
3774
|
-
} = await apiRequest(railsCsrfFetch, `${window.location.origin}/user_store_sync/token`, {
|
|
3775
|
-
method: "POST"
|
|
3776
|
-
});
|
|
3777
|
-
if (ok && typeof (json === null || json === void 0 ? void 0 : json.token) === "string" && typeof (json === null || json === void 0 ? void 0 : json.url) === "string") {
|
|
3778
|
-
const token = {
|
|
3779
|
-
expiresAt: Date.parse(json.expires_at),
|
|
3780
|
-
token: json.token,
|
|
3781
|
-
url: json.url
|
|
3782
|
-
};
|
|
3783
|
-
writeSession(tokenKey(personId), JSON.stringify(token));
|
|
3784
|
-
return token;
|
|
3785
|
-
} else {
|
|
3786
|
-
if ((response === null || response === void 0 ? void 0 : response.status) === 404) {
|
|
3787
|
-
writeSession(UNAVAILABLE_KEY, "true");
|
|
3788
|
-
}
|
|
3789
|
-
return null;
|
|
3790
|
-
}
|
|
3791
|
-
}
|
|
3792
|
-
async function request(personId, key, init = {}, isRetry = false) {
|
|
3793
|
-
var _a;
|
|
3794
|
-
const token = isRetry ? await mintToken(personId) : (_a = cachedToken(personId)) !== null && _a !== void 0 ? _a : await mintToken(personId);
|
|
3795
|
-
if (token) {
|
|
3796
|
-
const response = await fetch(`${token.url}/v1/data/${key}`, {
|
|
3797
|
-
...init,
|
|
3798
|
-
headers: {
|
|
3799
|
-
...init.headers,
|
|
3800
|
-
Authorization: `Bearer ${token.token}`
|
|
3801
|
-
}
|
|
3802
|
-
});
|
|
3803
|
-
if (response.status === 401 && !isRetry) {
|
|
3804
|
-
return request(personId, key, init, true);
|
|
3805
|
-
} else {
|
|
3806
|
-
return response;
|
|
3807
|
-
}
|
|
3808
|
-
} else {
|
|
3809
|
-
return null;
|
|
3810
|
-
}
|
|
3811
|
-
}
|
|
3812
|
-
// Applies `change` to the stored value and writes the result, rereading and
|
|
3813
|
-
// retrying when another tab or device wrote in between. `change` returns null
|
|
3814
|
-
// when there's nothing to write. Resolves to the value now stored, or null
|
|
3815
|
-
// when the service couldn't be reached.
|
|
3816
|
-
async function updateStoredValue(personId, key, change, attemptsLeft = MAX_ATTEMPTS) {
|
|
3817
|
-
const read = await request(personId, key);
|
|
3818
|
-
if (read === null || read === void 0 ? void 0 : read.ok) {
|
|
3819
|
-
const {
|
|
3820
|
-
value,
|
|
3821
|
-
version
|
|
3822
|
-
} = await read.json();
|
|
3823
|
-
const next = change(value);
|
|
3824
|
-
if (next === null) {
|
|
3825
|
-
return Array.isArray(value) ? value : [];
|
|
3826
|
-
} else {
|
|
3827
|
-
const write = await request(personId, key, {
|
|
3828
|
-
body: JSON.stringify({
|
|
3829
|
-
value: next
|
|
3830
|
-
}),
|
|
3831
|
-
headers: {
|
|
3832
|
-
"Content-Type": "application/json",
|
|
3833
|
-
"If-Match": `"${version}"`
|
|
3834
|
-
},
|
|
3835
|
-
method: "PUT"
|
|
3836
|
-
});
|
|
3837
|
-
if (write === null || write === void 0 ? void 0 : write.ok) {
|
|
3838
|
-
return next;
|
|
3839
|
-
} else if ((write === null || write === void 0 ? void 0 : write.status) === 409 && attemptsLeft > 1) {
|
|
3840
|
-
return updateStoredValue(personId, key, change, attemptsLeft - 1);
|
|
3841
|
-
} else {
|
|
3842
|
-
return null;
|
|
3843
|
-
}
|
|
3844
|
-
}
|
|
3845
|
-
} else {
|
|
3846
|
-
return null;
|
|
3847
|
-
}
|
|
3848
|
-
}
|
|
3849
|
-
|
|
3850
|
-
// Each product keeps its own copy in localStorage, and user-store-sync joins
|
|
3851
|
-
// the copies up across products and devices wherever the product can mint a
|
|
3852
|
-
// token for it.
|
|
3853
|
-
const SYNC_KEY = "recently_viewed";
|
|
3543
|
+
// Per-origin localStorage is a stopgap: each product keeps its own list until
|
|
3544
|
+
// PCA-409 moves this to storage shared across product subdomains. Only this
|
|
3545
|
+
// file should need to change when it does.
|
|
3854
3546
|
function storageKey({
|
|
3855
3547
|
env,
|
|
3856
3548
|
personId
|
|
@@ -3864,62 +3556,21 @@ const changes = createStore({
|
|
|
3864
3556
|
});
|
|
3865
3557
|
function readRecentlyVisited(scope) {
|
|
3866
3558
|
try {
|
|
3867
|
-
return parseRecentlyVisited(localStorage.getItem(storageKey(scope)));
|
|
3559
|
+
return parseRecentlyVisited(localStorage.getItem(storageKey(scope)), scope.personId);
|
|
3868
3560
|
} catch {
|
|
3869
3561
|
return [];
|
|
3870
3562
|
}
|
|
3871
3563
|
}
|
|
3872
|
-
function writeRecentlyVisited(scope, entries) {
|
|
3873
|
-
try {
|
|
3874
|
-
localStorage.setItem(storageKey(scope), JSON.stringify(entries));
|
|
3875
|
-
changes.setState({
|
|
3876
|
-
version: changes.getState().version + 1
|
|
3877
|
-
});
|
|
3878
|
-
} catch {
|
|
3879
|
-
// Storage can be full or blocked; recents are disposable.
|
|
3880
|
-
}
|
|
3881
|
-
}
|
|
3882
|
-
async function syncOnce(scope) {
|
|
3883
|
-
try {
|
|
3884
|
-
const synced = await updateStoredValue(scope.personId, SYNC_KEY, stored => {
|
|
3885
|
-
const shared = entriesFrom(stored);
|
|
3886
|
-
const merged = mergeEntries(shared, readRecentlyVisited(scope));
|
|
3887
|
-
const isUnchanged = Array.isArray(stored) && stored.length === shared.length && isSameList(merged, shared);
|
|
3888
|
-
return isUnchanged ? null : merged;
|
|
3889
|
-
});
|
|
3890
|
-
if (synced) {
|
|
3891
|
-
writeRecentlyVisited(scope, mergeEntries(entriesFrom(synced), readRecentlyVisited(scope)));
|
|
3892
|
-
}
|
|
3893
|
-
} catch {
|
|
3894
|
-
// The service is best-effort; the local copy stands on its own.
|
|
3895
|
-
}
|
|
3896
|
-
}
|
|
3897
|
-
const syncing = new Map();
|
|
3898
|
-
const syncAgain = new Set();
|
|
3899
|
-
// One sync per list at a time. Asking while one runs schedules a single
|
|
3900
|
-
// follow-up, so a visit recorded mid-sync still reaches the service.
|
|
3901
|
-
function syncRecentlyVisited(scope) {
|
|
3902
|
-
const key = storageKey(scope);
|
|
3903
|
-
const running = syncing.get(key);
|
|
3904
|
-
if (isUserStoreSyncUnavailable()) {
|
|
3905
|
-
return Promise.resolve();
|
|
3906
|
-
} else if (running) {
|
|
3907
|
-
syncAgain.add(key);
|
|
3908
|
-
return running;
|
|
3909
|
-
} else {
|
|
3910
|
-
const run = syncOnce(scope).then(() => {
|
|
3911
|
-
syncing.delete(key);
|
|
3912
|
-
return syncAgain.delete(key) ? syncRecentlyVisited(scope) : undefined;
|
|
3913
|
-
});
|
|
3914
|
-
syncing.set(key, run);
|
|
3915
|
-
return run;
|
|
3916
|
-
}
|
|
3917
|
-
}
|
|
3918
3564
|
function recordVisit(visit, scope) {
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3565
|
+
if (isEntry(visit)) {
|
|
3566
|
+
try {
|
|
3567
|
+
localStorage.setItem(storageKey(scope), serializeRecentlyVisited(addVisit(readRecentlyVisited(scope), visit), scope.personId));
|
|
3568
|
+
changes.setState({
|
|
3569
|
+
version: changes.getState().version + 1
|
|
3570
|
+
});
|
|
3571
|
+
} catch {
|
|
3572
|
+
// Storage can be full or blocked; recents are disposable.
|
|
3573
|
+
}
|
|
3923
3574
|
}
|
|
3924
3575
|
}
|
|
3925
3576
|
function useRecentlyVisited({
|
|
@@ -3927,14 +3578,6 @@ function useRecentlyVisited({
|
|
|
3927
3578
|
personId
|
|
3928
3579
|
}) {
|
|
3929
3580
|
const version = shim.useSyncExternalStore(changes.subscribe, () => changes.getState().version);
|
|
3930
|
-
React.useEffect(() => {
|
|
3931
|
-
if (personId) {
|
|
3932
|
-
syncRecentlyVisited({
|
|
3933
|
-
env,
|
|
3934
|
-
personId
|
|
3935
|
-
});
|
|
3936
|
-
}
|
|
3937
|
-
}, [env, personId]);
|
|
3938
3581
|
// `version` isn't read, but listing it re-reads storage after each visit.
|
|
3939
3582
|
return React.useMemo(() => personId ? readRecentlyVisited({
|
|
3940
3583
|
env,
|
|
@@ -4179,10 +3822,10 @@ function pcrlLookupsUrl(product, pcrls) {
|
|
|
4179
3822
|
});
|
|
4180
3823
|
return `${window.location.origin}/~api/${product}/v2/pcrl_lookups?${params}`;
|
|
4181
3824
|
}
|
|
4182
|
-
// Built from the
|
|
4183
|
-
// PCA-409 shares one list across product subdomains.
|
|
3825
|
+
// Built from the product rather than the current origin, so it stays right
|
|
3826
|
+
// once PCA-409 shares one list across product subdomains.
|
|
4184
3827
|
function pageUrl(entry, env) {
|
|
4185
|
-
return
|
|
3828
|
+
return `${pcoUrl(env)(entry.product)}${entry.path}`;
|
|
4186
3829
|
}
|
|
4187
3830
|
|
|
4188
3831
|
var styles$a = {"result":"result-WCnxr"};
|
|
@@ -4375,16 +4018,15 @@ function ShowMoreWhenVisible({
|
|
|
4375
4018
|
// Unlike search results, recents come from every product, so each one names
|
|
4376
4019
|
// its product and type.
|
|
4377
4020
|
function describeVisit(visit) {
|
|
4378
|
-
var _a, _b;
|
|
4379
4021
|
const subtitle = (...parts) => parts.filter(Boolean).join(" · ");
|
|
4380
4022
|
if (visit.status === "page") {
|
|
4381
4023
|
const {
|
|
4382
|
-
|
|
4383
|
-
|
|
4024
|
+
details,
|
|
4025
|
+
product,
|
|
4384
4026
|
title
|
|
4385
4027
|
} = visit.entry;
|
|
4386
4028
|
return {
|
|
4387
|
-
subtitle: subtitle(productName(
|
|
4029
|
+
subtitle: subtitle(productName(product), details),
|
|
4388
4030
|
title,
|
|
4389
4031
|
url: visit.url
|
|
4390
4032
|
};
|
|
@@ -4719,15 +4361,13 @@ function SearchButtonAndDialog({
|
|
|
4719
4361
|
const [open, setOpen] = React.useState(false);
|
|
4720
4362
|
React.useEffect(() => {
|
|
4721
4363
|
// Rich text editors like Trix bind ⌘K to "insert link"; capturing at the
|
|
4722
|
-
// window and stopping propagation keeps them from ever seeing it.
|
|
4723
|
-
// product's modal, search stays shut rather than stacking a second modal,
|
|
4724
|
-
// and the keystroke goes on to the product.
|
|
4364
|
+
// window and stopping propagation keeps them from ever seeing it.
|
|
4725
4365
|
const handleKeyDown = event => {
|
|
4726
|
-
if (
|
|
4366
|
+
if (isSearchShortcut(event)) {
|
|
4727
4367
|
event.preventDefault();
|
|
4728
4368
|
event.stopPropagation();
|
|
4729
|
-
setOpen(
|
|
4730
|
-
} else if (
|
|
4369
|
+
setOpen(wasOpen => !wasOpen);
|
|
4370
|
+
} else if (isLegacySearchShortcut(event)) {
|
|
4731
4371
|
event.preventDefault();
|
|
4732
4372
|
event.stopPropagation();
|
|
4733
4373
|
setOpen(true);
|
|
@@ -4739,7 +4379,7 @@ function SearchButtonAndDialog({
|
|
|
4739
4379
|
return () => window.removeEventListener("keydown", handleKeyDown, {
|
|
4740
4380
|
capture: true
|
|
4741
4381
|
});
|
|
4742
|
-
}, [
|
|
4382
|
+
}, []);
|
|
4743
4383
|
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
|
|
4744
4384
|
"aria-keyshortcuts": `${isMac() ? "Meta+K" : "Control+K"} /`,
|
|
4745
4385
|
"aria-label": "Search",
|
|
@@ -34054,26 +33694,42 @@ function UserProfile({
|
|
|
34054
33694
|
|
|
34055
33695
|
var ActionType;
|
|
34056
33696
|
(function (ActionType) {
|
|
33697
|
+
ActionType["clearPending"] = "clearPending";
|
|
34057
33698
|
ActionType["close"] = "close";
|
|
34058
33699
|
ActionType["mount"] = "mount";
|
|
34059
33700
|
ActionType["open"] = "open";
|
|
33701
|
+
ActionType["setPending"] = "setPending";
|
|
34060
33702
|
ActionType["unmount"] = "unmount";
|
|
34061
33703
|
})(ActionType || (ActionType = {}));
|
|
34062
33704
|
const initialState = {
|
|
34063
33705
|
currentContent: null,
|
|
34064
|
-
open: false
|
|
33706
|
+
open: false,
|
|
33707
|
+
pending: null
|
|
34065
33708
|
};
|
|
34066
33709
|
const reducer = (state, action) => {
|
|
33710
|
+
var _a, _b;
|
|
34067
33711
|
switch (action.type) {
|
|
34068
33712
|
case ActionType.mount:
|
|
34069
33713
|
return {
|
|
34070
33714
|
...state,
|
|
34071
|
-
currentContent: action.payload.content
|
|
33715
|
+
currentContent: action.payload.content,
|
|
33716
|
+
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
|
|
34072
33717
|
};
|
|
34073
33718
|
case ActionType.unmount:
|
|
34074
33719
|
return {
|
|
34075
33720
|
...state,
|
|
34076
|
-
currentContent: null
|
|
33721
|
+
currentContent: null,
|
|
33722
|
+
pending: null
|
|
33723
|
+
};
|
|
33724
|
+
case ActionType.setPending:
|
|
33725
|
+
return {
|
|
33726
|
+
...state,
|
|
33727
|
+
pending: action.payload
|
|
33728
|
+
};
|
|
33729
|
+
case ActionType.clearPending:
|
|
33730
|
+
return {
|
|
33731
|
+
...state,
|
|
33732
|
+
pending: null
|
|
34077
33733
|
};
|
|
34078
33734
|
case ActionType.open:
|
|
34079
33735
|
return {
|
|
@@ -34094,9 +33750,179 @@ const useToolbarReducer = overrides => React__namespace.useReducer(reducer, {
|
|
|
34094
33750
|
|
|
34095
33751
|
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"};
|
|
34096
33752
|
|
|
33753
|
+
// PCO Box serves TLS by default, and a bare http:// frame inside an https
|
|
33754
|
+
// product page is blocked as mixed content, so follow the page's scheme.
|
|
33755
|
+
const iframeSrc = () => `${window.location.protocol}//pco.test/iframe`;
|
|
33756
|
+
const STAFF_PINK = "var(--t-fill-color-product-staff)";
|
|
33757
|
+
// The pco.test domain is only ever served by a local PCO Box, so the tool
|
|
33758
|
+
// stays hidden in staging and production — and on Topbar's own dev server,
|
|
33759
|
+
// which runs on localhost and has no pco.test to frame.
|
|
33760
|
+
function isPcoTestHost() {
|
|
33761
|
+
const {
|
|
33762
|
+
hostname
|
|
33763
|
+
} = window.location;
|
|
33764
|
+
return hostname === "pco.test" || hostname.endsWith(".pco.test");
|
|
33765
|
+
}
|
|
33766
|
+
// Flattened from the source artwork's two outline paths — its gradients,
|
|
33767
|
+
// filters and clip paths carry nothing at icon size. Those paths are authored
|
|
33768
|
+
// on a 128px canvas, hence the transform: it fits the pair into the same ~24px
|
|
33769
|
+
// optical box the sibling toolbar icons occupy. Filled explicitly rather than
|
|
33770
|
+
// with `currentColor` so the mark stays staff pink instead of inheriting the
|
|
33771
|
+
// toolbar's text color and its hover/active states.
|
|
33772
|
+
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";
|
|
33773
|
+
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";
|
|
33774
|
+
function ToolsIcon() {
|
|
33775
|
+
return /*#__PURE__*/React.createElement("svg", {
|
|
33776
|
+
"aria-hidden": "true",
|
|
33777
|
+
width: "32",
|
|
33778
|
+
height: "32",
|
|
33779
|
+
viewBox: "0 0 32 32",
|
|
33780
|
+
xmlns: "http://www.w3.org/2000/svg"
|
|
33781
|
+
}, /*#__PURE__*/React.createElement("g", {
|
|
33782
|
+
fill: STAFF_PINK,
|
|
33783
|
+
transform: "translate(2.92 2.66) scale(.204)"
|
|
33784
|
+
}, /*#__PURE__*/React.createElement("path", {
|
|
33785
|
+
d: WRENCH
|
|
33786
|
+
}), /*#__PURE__*/React.createElement("path", {
|
|
33787
|
+
d: SCREWDRIVER
|
|
33788
|
+
})));
|
|
33789
|
+
}
|
|
33790
|
+
// A cross-origin frame's own history object is unreachable (touching it throws
|
|
33791
|
+
// a SecurityError), but the frame's navigations do land in the top-level joint
|
|
33792
|
+
// session history, so the *parent's* back/forward traverse them while leaving
|
|
33793
|
+
// the host page in place. Counting frame loads tracks how deep we are, which is
|
|
33794
|
+
// what keeps a click on Back from stepping past the frame's first page and
|
|
33795
|
+
// navigating the host app instead. Loads we caused are attributed to the
|
|
33796
|
+
// traversal that requested them rather than counted as new navigations.
|
|
33797
|
+
function useFrameHistory() {
|
|
33798
|
+
const [steps, setSteps] = React.useState({
|
|
33799
|
+
back: 0,
|
|
33800
|
+
forward: 0
|
|
33801
|
+
});
|
|
33802
|
+
// Traversal has to read the current counts outside of render, so the ref is
|
|
33803
|
+
// the source of truth and state exists to paint the buttons.
|
|
33804
|
+
const stepsRef = React.useRef(steps);
|
|
33805
|
+
const pendingTraversal = React.useRef(null);
|
|
33806
|
+
const frameIsLoaded = React.useRef(false);
|
|
33807
|
+
const moveTo = React.useCallback(next => {
|
|
33808
|
+
stepsRef.current = next;
|
|
33809
|
+
setSteps(next);
|
|
33810
|
+
}, []);
|
|
33811
|
+
const onFrameLoad = React.useCallback(() => {
|
|
33812
|
+
if (!frameIsLoaded.current) {
|
|
33813
|
+
// The frame's first page is the floor, not a step.
|
|
33814
|
+
frameIsLoaded.current = true;
|
|
33815
|
+
} else {
|
|
33816
|
+
const traversal = pendingTraversal.current;
|
|
33817
|
+
pendingTraversal.current = null;
|
|
33818
|
+
const {
|
|
33819
|
+
back,
|
|
33820
|
+
forward
|
|
33821
|
+
} = stepsRef.current;
|
|
33822
|
+
if (traversal === "back") {
|
|
33823
|
+
moveTo({
|
|
33824
|
+
back: back - 1,
|
|
33825
|
+
forward: forward + 1
|
|
33826
|
+
});
|
|
33827
|
+
} else if (traversal === "forward") {
|
|
33828
|
+
moveTo({
|
|
33829
|
+
back: back + 1,
|
|
33830
|
+
forward: forward - 1
|
|
33831
|
+
});
|
|
33832
|
+
} else {
|
|
33833
|
+
moveTo({
|
|
33834
|
+
back: back + 1,
|
|
33835
|
+
forward: 0
|
|
33836
|
+
});
|
|
33837
|
+
}
|
|
33838
|
+
}
|
|
33839
|
+
}, [moveTo]);
|
|
33840
|
+
// Guarded here rather than trusting the button's disabled state: traversing
|
|
33841
|
+
// with no frame step left would navigate the host app out from under us.
|
|
33842
|
+
const traverse = React.useCallback(direction => {
|
|
33843
|
+
if (stepsRef.current[direction] > 0) {
|
|
33844
|
+
pendingTraversal.current = direction;
|
|
33845
|
+
window.history[direction]();
|
|
33846
|
+
}
|
|
33847
|
+
}, []);
|
|
33848
|
+
return {
|
|
33849
|
+
onFrameLoad,
|
|
33850
|
+
steps,
|
|
33851
|
+
traverse
|
|
33852
|
+
};
|
|
33853
|
+
}
|
|
33854
|
+
function NavButton({
|
|
33855
|
+
direction,
|
|
33856
|
+
disabled,
|
|
33857
|
+
onClick
|
|
33858
|
+
}) {
|
|
33859
|
+
return /*#__PURE__*/React.createElement(tapestryReact.Button
|
|
33860
|
+
// Tapestry's `disabled` suppresses the click but emits no ARIA state.
|
|
33861
|
+
, {
|
|
33862
|
+
"aria-disabled": disabled,
|
|
33863
|
+
disabled: disabled,
|
|
33864
|
+
hover: {
|
|
33865
|
+
backgroundColor: "var(--t-fill-color-neutral-070)"
|
|
33866
|
+
},
|
|
33867
|
+
icon: {
|
|
33868
|
+
fill: "var(--t-icon-color-dim)",
|
|
33869
|
+
name: direction === "back" ? "general.leftChevron" : "general.rightChevron",
|
|
33870
|
+
size: "md"
|
|
33871
|
+
},
|
|
33872
|
+
onClick: onClick,
|
|
33873
|
+
opacity: disabled ? 0.35 : 1,
|
|
33874
|
+
theme: false,
|
|
33875
|
+
title: direction === "back" ? "Back" : "Forward"
|
|
33876
|
+
});
|
|
33877
|
+
}
|
|
33878
|
+
function DeveloperToolsComponent({
|
|
33879
|
+
closeDrawer
|
|
33880
|
+
}) {
|
|
33881
|
+
const {
|
|
33882
|
+
onFrameLoad,
|
|
33883
|
+
steps,
|
|
33884
|
+
traverse
|
|
33885
|
+
} = useFrameHistory();
|
|
33886
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(DrawerHeader, {
|
|
33887
|
+
closeDrawer: closeDrawer
|
|
33888
|
+
}, /*#__PURE__*/React.createElement(tapestryReact.StackView, {
|
|
33889
|
+
alignment: "center",
|
|
33890
|
+
axis: "horizontal",
|
|
33891
|
+
spacing: 0.25
|
|
33892
|
+
}, /*#__PURE__*/React.createElement(NavButton, {
|
|
33893
|
+
direction: "back",
|
|
33894
|
+
disabled: steps.back === 0,
|
|
33895
|
+
onClick: () => traverse("back")
|
|
33896
|
+
}), /*#__PURE__*/React.createElement(NavButton, {
|
|
33897
|
+
direction: "forward",
|
|
33898
|
+
disabled: steps.forward === 0,
|
|
33899
|
+
onClick: () => traverse("forward")
|
|
33900
|
+
}), /*#__PURE__*/React.createElement(DrawerHeading, {
|
|
33901
|
+
paddingLeft: 0.5
|
|
33902
|
+
}, "Developer Tools"))), /*#__PURE__*/React.createElement(tapestryReact.Box, {
|
|
33903
|
+
as: "iframe",
|
|
33904
|
+
border: "none",
|
|
33905
|
+
flex: "1",
|
|
33906
|
+
minHeight: 0,
|
|
33907
|
+
onLoad: onFrameLoad,
|
|
33908
|
+
src: iframeSrc(),
|
|
33909
|
+
title: "Developer Tools",
|
|
33910
|
+
width: "100%"
|
|
33911
|
+
}));
|
|
33912
|
+
}
|
|
33913
|
+
const DeveloperTools = {
|
|
33914
|
+
component: DeveloperToolsComponent,
|
|
33915
|
+
icons: {
|
|
33916
|
+
component: ToolsIcon
|
|
33917
|
+
},
|
|
33918
|
+
label: "Developer Tools",
|
|
33919
|
+
// No space: `name` becomes an element id and a pendo attribute.
|
|
33920
|
+
name: "DeveloperTools"
|
|
33921
|
+
};
|
|
33922
|
+
|
|
34097
33923
|
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"};
|
|
34098
33924
|
|
|
34099
|
-
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";
|
|
33925
|
+
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";
|
|
34100
33926
|
// Same stops as the AskAIBanner border gradient, so the two stay in sync.
|
|
34101
33927
|
const GRADIENT_STOPS = [{
|
|
34102
33928
|
color: "var(--t-fill-color-tag-teal-040)",
|
|
@@ -34145,7 +33971,7 @@ function SparkleIcon({
|
|
|
34145
33971
|
stopColor: color
|
|
34146
33972
|
}
|
|
34147
33973
|
})))))), /*#__PURE__*/React.createElement("path", {
|
|
34148
|
-
d: SPARKLE_PATH,
|
|
33974
|
+
d: SPARKLE_PATH$1,
|
|
34149
33975
|
fill: isGradient ? `url(#${gradientId})` : "currentColor"
|
|
34150
33976
|
}));
|
|
34151
33977
|
}
|
|
@@ -34553,16 +34379,24 @@ const DocumentationSearch = ({
|
|
|
34553
34379
|
// not the path, so updating it is a same-document navigation that doesn't
|
|
34554
34380
|
// reload the iframe, but also lets the route survive a forced reload (e.g.
|
|
34555
34381
|
// Turbo re-parenting the topbar).
|
|
34556
|
-
|
|
34382
|
+
//
|
|
34383
|
+
// `panel` loads picobot as its own toolbar panel rather than inside Helpdesk.
|
|
34384
|
+
function buildPicobotSrc(origin, path, {
|
|
34385
|
+
panel = false
|
|
34386
|
+
} = {}) {
|
|
34557
34387
|
const [rawPath, queryPart] = path.replace(/^\/+/, "").split("?", 2);
|
|
34558
34388
|
const safePath = rawPath.split("/").filter(segment => segment && segment !== "." && segment !== "..").join("/");
|
|
34559
34389
|
const fragment = queryPart !== undefined ? `${safePath}?${queryPart}` : safePath;
|
|
34560
|
-
const
|
|
34390
|
+
const url = new URL("topbar", origin);
|
|
34391
|
+
if (panel) url.searchParams.set("panel", "1");
|
|
34392
|
+
const base = url.toString();
|
|
34561
34393
|
return fragment ? `${base}#${fragment}` : base;
|
|
34562
34394
|
}
|
|
34563
34395
|
|
|
34564
34396
|
const LOAD_TIMEOUT_MS = 3000;
|
|
34565
|
-
function createPicobotIframe(picobotOrigin
|
|
34397
|
+
function createPicobotIframe(picobotOrigin, {
|
|
34398
|
+
panel = false
|
|
34399
|
+
} = {}) {
|
|
34566
34400
|
return /*#__PURE__*/React.forwardRef(function PicobotIframe({
|
|
34567
34401
|
currentPath: iframePath,
|
|
34568
34402
|
onChangePath: onIframeChangePath,
|
|
@@ -34618,7 +34452,9 @@ function createPicobotIframe(picobotOrigin) {
|
|
|
34618
34452
|
(_b = (_a = iframeRef.current) === null || _a === void 0 ? void 0 : _a.contentWindow) === null || _b === void 0 ? void 0 : _b.postMessage(message, picobotOrigin);
|
|
34619
34453
|
}
|
|
34620
34454
|
}), []);
|
|
34621
|
-
const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath
|
|
34455
|
+
const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath, {
|
|
34456
|
+
panel
|
|
34457
|
+
});
|
|
34622
34458
|
return /*#__PURE__*/React.createElement("iframe", {
|
|
34623
34459
|
ref: iframeRef,
|
|
34624
34460
|
src: iframeSrc,
|
|
@@ -34657,22 +34493,28 @@ function HelpdeskComponent({
|
|
|
34657
34493
|
currentPath,
|
|
34658
34494
|
env,
|
|
34659
34495
|
helpdeskConfig,
|
|
34660
|
-
|
|
34496
|
+
isPicobotLoading,
|
|
34497
|
+
onChangePath,
|
|
34498
|
+
openPicobot
|
|
34661
34499
|
}) {
|
|
34662
34500
|
const PicobotIframe = React.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot")), [env]);
|
|
34501
|
+
const loadingFallback = /*#__PURE__*/React.createElement(StackView$1, {
|
|
34502
|
+
flex: 1,
|
|
34503
|
+
axis: "vertical"
|
|
34504
|
+
}, /*#__PURE__*/React.createElement(DrawerHeader, {
|
|
34505
|
+
closeDrawer: closeDrawer
|
|
34506
|
+
}, /*#__PURE__*/React.createElement(StackView$1, {
|
|
34507
|
+
axis: "horizontal",
|
|
34508
|
+
spacing: 1
|
|
34509
|
+
}, /*#__PURE__*/React.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React.createElement(LoadingIndicator$1, null))));
|
|
34510
|
+
// The add-on reads its args once, so mounting before the gate answers
|
|
34511
|
+
// would leave out openPicobot for good.
|
|
34512
|
+
if (isPicobotLoading) return loadingFallback;
|
|
34663
34513
|
return /*#__PURE__*/React.createElement(addOns.UiExtensionFirstPartyComponent, {
|
|
34664
34514
|
grow: 1,
|
|
34665
34515
|
display: "flex",
|
|
34666
34516
|
extensionId: env === "staging" ? "42" : "11",
|
|
34667
|
-
loadingFallback:
|
|
34668
|
-
flex: 1,
|
|
34669
|
-
axis: "vertical"
|
|
34670
|
-
}, /*#__PURE__*/React.createElement(DrawerHeader, {
|
|
34671
|
-
closeDrawer: closeDrawer
|
|
34672
|
-
}, /*#__PURE__*/React.createElement(StackView$1, {
|
|
34673
|
-
axis: "horizontal",
|
|
34674
|
-
spacing: 1
|
|
34675
|
-
}, /*#__PURE__*/React.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React.createElement(LoadingIndicator$1, null)))),
|
|
34517
|
+
loadingFallback: loadingFallback,
|
|
34676
34518
|
additionalComponents: {
|
|
34677
34519
|
AskAIBanner: {
|
|
34678
34520
|
component: AskAIBanner,
|
|
@@ -34770,7 +34612,11 @@ function HelpdeskComponent({
|
|
|
34770
34612
|
currentPageParams: location.search,
|
|
34771
34613
|
currentPath,
|
|
34772
34614
|
helpdeskConfig,
|
|
34773
|
-
onChangePath
|
|
34615
|
+
onChangePath,
|
|
34616
|
+
// Its presence tells helpdesk that Picobot has its own panel.
|
|
34617
|
+
...(openPicobot && {
|
|
34618
|
+
openPicobot
|
|
34619
|
+
})
|
|
34774
34620
|
}
|
|
34775
34621
|
});
|
|
34776
34622
|
}
|
|
@@ -35317,6 +35163,142 @@ const Notifications = {
|
|
|
35317
35163
|
name: "Notifications"
|
|
35318
35164
|
};
|
|
35319
35165
|
|
|
35166
|
+
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";
|
|
35167
|
+
// Helpdesk reads this context off the email path to open its email form the
|
|
35168
|
+
// same way a handoff inside its own drawer does.
|
|
35169
|
+
function emailPathWithContext(context = {}) {
|
|
35170
|
+
const params = new URLSearchParams();
|
|
35171
|
+
for (const key of ["conversationId", "source", "turnIndex"]) {
|
|
35172
|
+
const value = context[key];
|
|
35173
|
+
if (value !== null && value !== undefined) params.set(key, String(value));
|
|
35174
|
+
}
|
|
35175
|
+
return `email?${params}`;
|
|
35176
|
+
}
|
|
35177
|
+
function PicobotComponent({
|
|
35178
|
+
clearPending,
|
|
35179
|
+
closeDrawer,
|
|
35180
|
+
currentPath,
|
|
35181
|
+
env,
|
|
35182
|
+
onChangePath,
|
|
35183
|
+
openTool,
|
|
35184
|
+
pending
|
|
35185
|
+
}) {
|
|
35186
|
+
const PicobotIframe = React.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot"), {
|
|
35187
|
+
panel: true
|
|
35188
|
+
}), [env]);
|
|
35189
|
+
const picobotRef = React.useRef(null);
|
|
35190
|
+
const [isPicobotReady, setIsPicobotReady] = React.useState(false);
|
|
35191
|
+
// Picobot has no listener until it sends READY, so openPicobot's draft or
|
|
35192
|
+
// conversation waits for it.
|
|
35193
|
+
React.useEffect(function handOverPendingOncePicobotIsReady() {
|
|
35194
|
+
var _a, _b;
|
|
35195
|
+
if (!pending || !isPicobotReady) return;
|
|
35196
|
+
const {
|
|
35197
|
+
conversationId,
|
|
35198
|
+
draft,
|
|
35199
|
+
ticketExternalId
|
|
35200
|
+
} = pending;
|
|
35201
|
+
if (conversationId) {
|
|
35202
|
+
(_a = picobotRef.current) === null || _a === void 0 ? void 0 : _a.postMessage({
|
|
35203
|
+
payload: {
|
|
35204
|
+
conversation_id: conversationId,
|
|
35205
|
+
// Picobot asks whether this resolved the ticket, and closes it.
|
|
35206
|
+
...(ticketExternalId && {
|
|
35207
|
+
external_id: ticketExternalId,
|
|
35208
|
+
source: "ticket_submitted"
|
|
35209
|
+
})
|
|
35210
|
+
},
|
|
35211
|
+
type: "LOAD_CONVERSATION"
|
|
35212
|
+
});
|
|
35213
|
+
} else if (draft) {
|
|
35214
|
+
// Not both: Picobot cancels a conversation load when a prefill
|
|
35215
|
+
// starts a new conversation.
|
|
35216
|
+
(_b = picobotRef.current) === null || _b === void 0 ? void 0 : _b.postMessage({
|
|
35217
|
+
payload: {
|
|
35218
|
+
source: "email_form",
|
|
35219
|
+
text: draft
|
|
35220
|
+
},
|
|
35221
|
+
type: "PREFILL_COMPOSER"
|
|
35222
|
+
});
|
|
35223
|
+
}
|
|
35224
|
+
clearPending();
|
|
35225
|
+
}, [pending, isPicobotReady, clearPending]);
|
|
35226
|
+
const [iframeErrored, setIframeErrored] = React.useState(false);
|
|
35227
|
+
const [mountKey, setMountKey] = React.useState(0);
|
|
35228
|
+
const handleRetry = React.useCallback(() => {
|
|
35229
|
+
setIframeErrored(false);
|
|
35230
|
+
setIsPicobotReady(false);
|
|
35231
|
+
setMountKey(key => key + 1);
|
|
35232
|
+
}, []);
|
|
35233
|
+
const handleMessage = React.useCallback(data => {
|
|
35234
|
+
var _a;
|
|
35235
|
+
const message = data;
|
|
35236
|
+
switch (message === null || message === void 0 ? void 0 : message.type) {
|
|
35237
|
+
case "CLOSE_DRAWER":
|
|
35238
|
+
return closeDrawer();
|
|
35239
|
+
case "READY":
|
|
35240
|
+
return setIsPicobotReady(true);
|
|
35241
|
+
case "PICOBOT_SUPPORT_HANDOFF":
|
|
35242
|
+
return openTool("helpdesk", ((_a = message.payload) === null || _a === void 0 ? void 0 : _a.method) === "zoom" ? "phone" : emailPathWithContext(message.payload));
|
|
35243
|
+
case "IFRAME_LOAD_ERROR":
|
|
35244
|
+
console.warn("topbar: Picobot iframe failed to load");
|
|
35245
|
+
return setIframeErrored(true);
|
|
35246
|
+
}
|
|
35247
|
+
}, [closeDrawer, openTool]);
|
|
35248
|
+
if (iframeErrored) {
|
|
35249
|
+
return /*#__PURE__*/React.createElement(LoadError, {
|
|
35250
|
+
onContactSupport: () => openTool("helpdesk", "email"),
|
|
35251
|
+
onRetry: handleRetry
|
|
35252
|
+
});
|
|
35253
|
+
}
|
|
35254
|
+
return /*#__PURE__*/React.createElement(PicobotIframe, {
|
|
35255
|
+
key: mountKey,
|
|
35256
|
+
ref: picobotRef,
|
|
35257
|
+
currentPath: currentPath,
|
|
35258
|
+
onChangePath: onChangePath,
|
|
35259
|
+
onMessage: handleMessage
|
|
35260
|
+
});
|
|
35261
|
+
}
|
|
35262
|
+
function LoadError({
|
|
35263
|
+
onContactSupport,
|
|
35264
|
+
onRetry
|
|
35265
|
+
}) {
|
|
35266
|
+
return /*#__PURE__*/React.createElement(tapestryReact.StackView, {
|
|
35267
|
+
axis: "vertical",
|
|
35268
|
+
flex: 1,
|
|
35269
|
+
alignment: "center",
|
|
35270
|
+
distribution: "center",
|
|
35271
|
+
padding: 3,
|
|
35272
|
+
spacing: 1
|
|
35273
|
+
}, /*#__PURE__*/React.createElement(tapestryReact.Heading, {
|
|
35274
|
+
level: 2
|
|
35275
|
+
}, "Picobot is temporarily unavailable"), /*#__PURE__*/React.createElement(tapestryReact.Text, {
|
|
35276
|
+
size: 5,
|
|
35277
|
+
color: "var(--t-text-color-secondary)"
|
|
35278
|
+
}, "Try again or contact support."), /*#__PURE__*/React.createElement(tapestryReact.Box, {
|
|
35279
|
+
paddingTop: 2
|
|
35280
|
+
}, /*#__PURE__*/React.createElement(tapestryReact.Button, {
|
|
35281
|
+
onClick: onRetry,
|
|
35282
|
+
iconLeft: "general.refresh",
|
|
35283
|
+
theme: "primary",
|
|
35284
|
+
variant: "fill",
|
|
35285
|
+
title: "Retry"
|
|
35286
|
+
})), /*#__PURE__*/React.createElement(tapestryReact.Button, {
|
|
35287
|
+
onClick: onContactSupport,
|
|
35288
|
+
variant: "naked",
|
|
35289
|
+
theme: "primary",
|
|
35290
|
+
title: "Contact support"
|
|
35291
|
+
}));
|
|
35292
|
+
}
|
|
35293
|
+
const Picobot = {
|
|
35294
|
+
component: PicobotComponent,
|
|
35295
|
+
icons: {
|
|
35296
|
+
staticDotPath: SPARKLE_PATH,
|
|
35297
|
+
staticPath: SPARKLE_PATH
|
|
35298
|
+
},
|
|
35299
|
+
name: "Picobot"
|
|
35300
|
+
};
|
|
35301
|
+
|
|
35320
35302
|
const {
|
|
35321
35303
|
LoadingIndicator,
|
|
35322
35304
|
StackView
|
|
@@ -35712,6 +35694,41 @@ const useChatConfig = ({
|
|
|
35712
35694
|
};
|
|
35713
35695
|
};
|
|
35714
35696
|
|
|
35697
|
+
// Picobot's API rejects requests without a version header, and apiRequest
|
|
35698
|
+
// sets its own headers, so the version rides in on the fetch.
|
|
35699
|
+
const picobotFetch = (url, opts) => defaultFetch(url, {
|
|
35700
|
+
...opts,
|
|
35701
|
+
headers: {
|
|
35702
|
+
...(opts === null || opts === void 0 ? void 0 : opts.headers),
|
|
35703
|
+
"X-PCO-API-Version": "2026-08-11"
|
|
35704
|
+
}
|
|
35705
|
+
});
|
|
35706
|
+
// Routes wait on the gate, so a slow Picobot can't hold them up for long.
|
|
35707
|
+
const GATE_TIMEOUT_MS = 3000;
|
|
35708
|
+
const usePicobotConfig = ({
|
|
35709
|
+
isEnabled
|
|
35710
|
+
}) => {
|
|
35711
|
+
// Undefined until Picobot answers.
|
|
35712
|
+
const [panelEnabled, setPanelEnabled] = React.useState();
|
|
35713
|
+
React.useEffect(() => {
|
|
35714
|
+
if (!isEnabled) return undefined;
|
|
35715
|
+
// Whatever settles first wins, so a late answer can't reopen the gate.
|
|
35716
|
+
const settle = enabled => setPanelEnabled(current => current !== null && current !== void 0 ? current : enabled);
|
|
35717
|
+
const timeout = setTimeout(() => settle(false), GATE_TIMEOUT_MS);
|
|
35718
|
+
apiRequest(picobotFetch, `${window.location.origin}/~api/picobot/v2/me`).then(({
|
|
35719
|
+
json
|
|
35720
|
+
}) => {
|
|
35721
|
+
var _a, _b;
|
|
35722
|
+
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);
|
|
35723
|
+
}).catch(() => settle(false)).finally(() => clearTimeout(timeout));
|
|
35724
|
+
return () => clearTimeout(timeout);
|
|
35725
|
+
}, [isEnabled]);
|
|
35726
|
+
return {
|
|
35727
|
+
displayPicobotIcon: panelEnabled === true,
|
|
35728
|
+
isLoading: isEnabled && panelEnabled === undefined
|
|
35729
|
+
};
|
|
35730
|
+
};
|
|
35731
|
+
|
|
35715
35732
|
const Profile = {
|
|
35716
35733
|
component: UserProfile,
|
|
35717
35734
|
icons: {
|
|
@@ -35722,10 +35739,16 @@ const Profile = {
|
|
|
35722
35739
|
};
|
|
35723
35740
|
|
|
35724
35741
|
const DURATION = 100;
|
|
35742
|
+
// Exempt paths match regardless of query, so `email?conversationId=…` is
|
|
35743
|
+
// exempt along with `email`.
|
|
35725
35744
|
function isToolPathExemptFromStorage(toolName, path) {
|
|
35726
35745
|
var _a, _b;
|
|
35727
35746
|
const tool = toolsRouteMap[toolName];
|
|
35728
|
-
|
|
35747
|
+
const [pathWithoutQuery] = path.split("?");
|
|
35748
|
+
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;
|
|
35749
|
+
}
|
|
35750
|
+
function isConversationPath(path) {
|
|
35751
|
+
return Boolean(path === null || path === void 0 ? void 0 : path.startsWith("conversations/"));
|
|
35729
35752
|
}
|
|
35730
35753
|
function scrollToolDrawerToTop(toolName) {
|
|
35731
35754
|
var _a;
|
|
@@ -35738,6 +35761,7 @@ const TOOLS_MAP = {
|
|
|
35738
35761
|
Chat,
|
|
35739
35762
|
Helpdesk,
|
|
35740
35763
|
Notifications,
|
|
35764
|
+
Picobot,
|
|
35741
35765
|
Profile,
|
|
35742
35766
|
Tasks
|
|
35743
35767
|
};
|
|
@@ -35765,7 +35789,8 @@ function Toolbar({
|
|
|
35765
35789
|
});
|
|
35766
35790
|
const {
|
|
35767
35791
|
currentContent,
|
|
35768
|
-
open
|
|
35792
|
+
open,
|
|
35793
|
+
pending
|
|
35769
35794
|
} = state;
|
|
35770
35795
|
const mounted = !!currentContent;
|
|
35771
35796
|
const storeData = useStore();
|
|
@@ -35778,6 +35803,15 @@ function Toolbar({
|
|
|
35778
35803
|
...(env === "development" ? _storeDataOverride !== null && _storeDataOverride !== void 0 ? _storeDataOverride : {} : {})
|
|
35779
35804
|
};
|
|
35780
35805
|
const updateStore = useUpdateStore();
|
|
35806
|
+
const {
|
|
35807
|
+
displayPicobotIcon,
|
|
35808
|
+
isLoading: isPicobotGateLoading
|
|
35809
|
+
} = usePicobotConfig({
|
|
35810
|
+
isEnabled: Boolean(userId)
|
|
35811
|
+
});
|
|
35812
|
+
// Picobot is a kill-switched tool: until its gate says the panel is
|
|
35813
|
+
// enabled, its routes are held, and if it never does, they're ignored.
|
|
35814
|
+
const routableTool = React.useCallback(name => name === Picobot.name.toLowerCase() && !displayPicobotIcon ? undefined : toolsRouteMap[name], [displayPicobotIcon]);
|
|
35781
35815
|
React.useEffect(() => {
|
|
35782
35816
|
if (open) {
|
|
35783
35817
|
document.body.style.overflow = "hidden";
|
|
@@ -35865,7 +35899,9 @@ function Toolbar({
|
|
|
35865
35899
|
});
|
|
35866
35900
|
}, [clearRoute]);
|
|
35867
35901
|
React.useEffect(() => {
|
|
35868
|
-
if (!toolbarFragmentRoute)
|
|
35902
|
+
if (!toolbarFragmentRoute || !routableTool(toolbarFragmentRoute.tool)) {
|
|
35903
|
+
return;
|
|
35904
|
+
}
|
|
35869
35905
|
const isExempt = isToolPathExemptFromStorage(toolbarFragmentRoute.tool, toolbarFragmentRoute.path);
|
|
35870
35906
|
if (isExempt) return;
|
|
35871
35907
|
const nextState = {
|
|
@@ -35875,12 +35911,32 @@ function Toolbar({
|
|
|
35875
35911
|
if (!shallowCompare(nextState, toolStateCookie)) {
|
|
35876
35912
|
setToolStateCookie(nextState);
|
|
35877
35913
|
}
|
|
35878
|
-
}, [toolbarFragmentRoute, toolStateCookie]);
|
|
35914
|
+
}, [toolbarFragmentRoute, toolStateCookie, routableTool]);
|
|
35879
35915
|
React.useEffect(() => {
|
|
35880
|
-
|
|
35881
|
-
|
|
35916
|
+
// Helpdesk conversations may belong to Picobot, so they wait for its gate
|
|
35917
|
+
// rather than flashing open in Helpdesk first.
|
|
35918
|
+
const mayRedirectToPicobot = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path) && (isPicobotGateLoading || displayPicobotIcon);
|
|
35919
|
+
const tool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && !mayRedirectToPicobot && routableTool(toolbarFragmentRoute.tool);
|
|
35920
|
+
if (tool && (currentContent !== tool || !open)) {
|
|
35921
|
+
setToolAndOpenDrawer(tool);
|
|
35882
35922
|
}
|
|
35883
|
-
}, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, currentContent, open]);
|
|
35923
|
+
}, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path, currentContent, open, routableTool, isPicobotGateLoading, displayPicobotIcon]);
|
|
35924
|
+
const openTool = React.useCallback((name, path, pendingValue) => {
|
|
35925
|
+
const tool = routableTool(name.toLowerCase());
|
|
35926
|
+
if (!tool) return;
|
|
35927
|
+
dispatch({
|
|
35928
|
+
payload: pendingValue === undefined ? null : {
|
|
35929
|
+
tool: tool.name,
|
|
35930
|
+
value: pendingValue
|
|
35931
|
+
},
|
|
35932
|
+
type: ActionType.setPending
|
|
35933
|
+
});
|
|
35934
|
+
onChangeToolbarFragmentRoute(tool.name.toLowerCase(), path);
|
|
35935
|
+
scrollToolDrawerToTop(tool.name);
|
|
35936
|
+
}, [onChangeToolbarFragmentRoute, routableTool]);
|
|
35937
|
+
const clearPending = React.useCallback(() => dispatch({
|
|
35938
|
+
type: ActionType.clearPending
|
|
35939
|
+
}), []);
|
|
35884
35940
|
const toggleDrawer = content => {
|
|
35885
35941
|
if (!open) {
|
|
35886
35942
|
return openDrawer(content);
|
|
@@ -35897,12 +35953,52 @@ function Toolbar({
|
|
|
35897
35953
|
joltPromise: storeData === null || storeData === void 0 ? void 0 : storeData.joltPromise,
|
|
35898
35954
|
userId
|
|
35899
35955
|
});
|
|
35900
|
-
|
|
35956
|
+
// Picobot conversations moved from Helpdesk to the Picobot panel, so
|
|
35957
|
+
// send old links and saved paths there once the panel is enabled.
|
|
35958
|
+
React.useEffect(() => {
|
|
35959
|
+
if (!displayPicobotIcon) return;
|
|
35960
|
+
if ((toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path)) {
|
|
35961
|
+
onChangeToolbarFragmentRoute(Picobot.name.toLowerCase(), toolbarFragmentRoute.path);
|
|
35962
|
+
}
|
|
35963
|
+
}, [displayPicobotIcon, toolbarFragmentRoute, onChangeToolbarFragmentRoute]);
|
|
35964
|
+
React.useEffect(() => {
|
|
35965
|
+
if (!displayPicobotIcon) return;
|
|
35966
|
+
const {
|
|
35967
|
+
helpdesk,
|
|
35968
|
+
picobot
|
|
35969
|
+
} = toolStateCookie;
|
|
35970
|
+
if (!isConversationPath(helpdesk)) return;
|
|
35971
|
+
setToolStateCookie({
|
|
35972
|
+
...toolStateCookie,
|
|
35973
|
+
helpdesk: "",
|
|
35974
|
+
picobot: picobot || helpdesk
|
|
35975
|
+
});
|
|
35976
|
+
}, [displayPicobotIcon, toolStateCookie]);
|
|
35977
|
+
const openPicobot = React.useCallback(options => {
|
|
35978
|
+
var _a;
|
|
35979
|
+
// Moving an open Picobot off its path reloads or navigates the iframe,
|
|
35980
|
+
// which would race the options it's being handed.
|
|
35981
|
+
const path = currentContent === Picobot ? (_a = toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path) !== null && _a !== void 0 ? _a : "" : "";
|
|
35982
|
+
openTool(Picobot.name, path, options);
|
|
35983
|
+
}, [openTool, currentContent, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path]);
|
|
35984
|
+
React.useEffect(() => {
|
|
35985
|
+
if (!displayPicobotIcon) return undefined;
|
|
35986
|
+
window.topbar = window.topbar || {};
|
|
35987
|
+
window.topbar.openPicobot = openPicobot;
|
|
35988
|
+
return () => {
|
|
35989
|
+
delete window.topbar.openPicobot;
|
|
35990
|
+
};
|
|
35991
|
+
}, [displayPicobotIcon, openPicobot]);
|
|
35992
|
+
const TOOLS = [Notifications, Tasks, displayChatIcon && Chat, displayPicobotIcon && Picobot, isPcoTestHost() && DeveloperTools].filter(Boolean);
|
|
35901
35993
|
const handleToolOnClick = tool => {
|
|
35902
35994
|
toggleDrawer(tool);
|
|
35903
35995
|
scrollToolDrawerToTop(tool.name);
|
|
35904
35996
|
};
|
|
35905
35997
|
const toolSpecificProps = {
|
|
35998
|
+
Helpdesk: {
|
|
35999
|
+
isPicobotLoading: isPicobotGateLoading,
|
|
36000
|
+
openPicobot: displayPicobotIcon ? openPicobot : undefined
|
|
36001
|
+
},
|
|
35906
36002
|
Profile: {
|
|
35907
36003
|
appName,
|
|
35908
36004
|
closeDrawer,
|
|
@@ -35925,6 +36021,7 @@ function Toolbar({
|
|
|
35925
36021
|
const routeIsForCurrentTool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && toolsRouteMap[toolbarFragmentRoute.tool] === currentContent;
|
|
35926
36022
|
const currentToolProps = {
|
|
35927
36023
|
appName,
|
|
36024
|
+
clearPending,
|
|
35928
36025
|
closeDrawer,
|
|
35929
36026
|
currentPath: routeIsForCurrentTool ? toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path : null,
|
|
35930
36027
|
env,
|
|
@@ -35933,6 +36030,8 @@ function Toolbar({
|
|
|
35933
36030
|
joltPromise,
|
|
35934
36031
|
onChangePath,
|
|
35935
36032
|
openDrawer: () => currentContent && openDrawer(currentContent),
|
|
36033
|
+
openTool,
|
|
36034
|
+
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,
|
|
35936
36035
|
userId,
|
|
35937
36036
|
...(currentContent ? toolSpecificProps[currentContent.name] : {})
|
|
35938
36037
|
};
|
|
@@ -35960,21 +36059,20 @@ function Toolbar({
|
|
|
35960
36059
|
name: "toolbar.planningCenter",
|
|
35961
36060
|
size: "24px"
|
|
35962
36061
|
})), [HelpDeskTool, ...TOOLS].map(tool => {
|
|
36062
|
+
var _a;
|
|
35963
36063
|
const current = open && currentContent === tool;
|
|
35964
36064
|
const indicator = indicatorState[tool.name];
|
|
35965
36065
|
return /*#__PURE__*/React.createElement(ToolbarButton, {
|
|
35966
36066
|
current: current,
|
|
35967
36067
|
onClick: () => handleToolOnClick(tool),
|
|
35968
36068
|
id: getDataPendoAttribute(tool.name),
|
|
35969
|
-
"aria-label": `Open ${tool.name} drawer`,
|
|
36069
|
+
"aria-label": `Open ${(_a = tool.label) !== null && _a !== void 0 ? _a : tool.name} drawer`,
|
|
35970
36070
|
"data-pendo": getDataPendoAttribute(tool.name),
|
|
35971
36071
|
key: tool.name
|
|
35972
|
-
},
|
|
35973
|
-
|
|
35974
|
-
|
|
35975
|
-
|
|
35976
|
-
dot: true
|
|
35977
|
-
})));
|
|
36072
|
+
}, /*#__PURE__*/React.createElement(ToolbarToolIcon, {
|
|
36073
|
+
icons: tool.icons,
|
|
36074
|
+
dot: !!indicator
|
|
36075
|
+
}));
|
|
35978
36076
|
})), /*#__PURE__*/React.createElement(DrawerWrap, {
|
|
35979
36077
|
isVisible: open
|
|
35980
36078
|
}, mounted && (/*#__PURE__*/React.createElement(Drawer, {
|
|
@@ -35998,6 +36096,19 @@ const ToolbarButton = ({
|
|
|
35998
36096
|
...restProps
|
|
35999
36097
|
}, children);
|
|
36000
36098
|
};
|
|
36099
|
+
function ToolbarToolIcon({
|
|
36100
|
+
dot,
|
|
36101
|
+
icons
|
|
36102
|
+
}) {
|
|
36103
|
+
if ("component" in icons) {
|
|
36104
|
+
return /*#__PURE__*/React.createElement(icons.component, null);
|
|
36105
|
+
} else {
|
|
36106
|
+
return /*#__PURE__*/React.createElement(ToolbarButtonIcon, {
|
|
36107
|
+
path: dot ? icons.staticDotPath : icons.staticPath,
|
|
36108
|
+
dot: dot
|
|
36109
|
+
});
|
|
36110
|
+
}
|
|
36111
|
+
}
|
|
36001
36112
|
function ToolbarButtonIcon({
|
|
36002
36113
|
dot,
|
|
36003
36114
|
path
|