@planningcenter/topbar 12.3.0 → 12.4.0-rc.1
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 +84 -18
- package/dist/topbar.css.map +1 -1
- package/dist/topbar.js +1980 -873
- package/dist/topbar.js.map +1 -1
- package/dist/topbar.modern.js +1985 -878
- package/dist/topbar.modern.js.map +1 -1
- package/dist/topbar.module.js +1985 -878
- package/dist/topbar.module.js.map +1 -1
- package/dist/topbar.umd.js +1980 -873
- package/dist/topbar.umd.js.map +1 -1
- package/package.json +4 -3
package/dist/topbar.js
CHANGED
|
@@ -41,7 +41,7 @@ var churchCenter__namespace = /*#__PURE__*/_interopNamespaceDefault(churchCenter
|
|
|
41
41
|
var people__namespace = /*#__PURE__*/_interopNamespaceDefault(people);
|
|
42
42
|
var toolbar__namespace = /*#__PURE__*/_interopNamespaceDefault(toolbar);
|
|
43
43
|
|
|
44
|
-
var styles$
|
|
44
|
+
var styles$m = {"accessibility-toolbar":"accessibility-toolbar-jgjUG"};
|
|
45
45
|
|
|
46
46
|
const AccessibilityToolbar = ({
|
|
47
47
|
children
|
|
@@ -53,7 +53,7 @@ const AccessibilityToolbar = ({
|
|
|
53
53
|
(_a = document.getElementById("main-content")) === null || _a === void 0 ? void 0 : _a.focus();
|
|
54
54
|
};
|
|
55
55
|
return /*#__PURE__*/React.createElement("div", {
|
|
56
|
-
className: styles$
|
|
56
|
+
className: styles$m["accessibility-toolbar"]
|
|
57
57
|
}, /*#__PURE__*/React.createElement(tapestry.Link, {
|
|
58
58
|
href: "#main-content",
|
|
59
59
|
kind: "primary",
|
|
@@ -180,12 +180,14 @@ function inferRailsCsrfToken() {
|
|
|
180
180
|
}
|
|
181
181
|
function apiRequest(fetch, url, {
|
|
182
182
|
method = "GET",
|
|
183
|
-
data = null
|
|
183
|
+
data = null,
|
|
184
|
+
headers = {}
|
|
184
185
|
} = {}) {
|
|
185
186
|
const options = {
|
|
186
187
|
headers: {
|
|
187
188
|
Accept: "application/json",
|
|
188
|
-
"Content-Type": "application/json"
|
|
189
|
+
"Content-Type": "application/json",
|
|
190
|
+
...headers
|
|
189
191
|
},
|
|
190
192
|
method,
|
|
191
193
|
redirect: "follow"
|
|
@@ -370,11 +372,11 @@ const X = ({
|
|
|
370
372
|
fill: fill
|
|
371
373
|
})));
|
|
372
374
|
|
|
373
|
-
var styles$
|
|
375
|
+
var styles$l = {"announcement":"announcement-pI5tM","announcement--platform":"announcement--platform-EcUn2","announcement--promo":"announcement--promo-FarUk","announcement-icon":"announcement-icon-Ov39-","announcement-content":"announcement-content-q2ad6","dismiss":"dismiss-wUAoU","dismiss--promo":"dismiss--promo-7zvO4"};
|
|
374
376
|
|
|
375
377
|
function AnnouncementIcon() {
|
|
376
378
|
return /*#__PURE__*/React.createElement("svg", {
|
|
377
|
-
className: styles$
|
|
379
|
+
className: styles$l["announcement-icon"],
|
|
378
380
|
width: "16",
|
|
379
381
|
height: "16",
|
|
380
382
|
viewBox: "0 0 16 16",
|
|
@@ -499,7 +501,7 @@ let StyledAnnouncement$1 = class StyledAnnouncement extends React.Component {
|
|
|
499
501
|
} = this.props;
|
|
500
502
|
const isPromo = variant === "promo";
|
|
501
503
|
return /*#__PURE__*/React.createElement("div", {
|
|
502
|
-
className: classnames(styles$
|
|
504
|
+
className: classnames(styles$l["announcement"], styles$l[`announcement--${variant !== null && variant !== void 0 ? variant : "platform"}`]),
|
|
503
505
|
"data-announcement-variant": variant !== null && variant !== void 0 ? variant : "platform",
|
|
504
506
|
style: isPromo ? {
|
|
505
507
|
backgroundImage: promoBackgroundUrl,
|
|
@@ -508,7 +510,7 @@ let StyledAnnouncement$1 = class StyledAnnouncement extends React.Component {
|
|
|
508
510
|
backgroundSize: "cover"
|
|
509
511
|
} : undefined
|
|
510
512
|
}, !isPromo && /*#__PURE__*/React.createElement(AnnouncementIcon, null), /*#__PURE__*/React.createElement("div", {
|
|
511
|
-
className: styles$
|
|
513
|
+
className: styles$l["announcement-content"],
|
|
512
514
|
style: style,
|
|
513
515
|
...platformProps
|
|
514
516
|
}));
|
|
@@ -557,7 +559,7 @@ function StyledDismissButton$1({
|
|
|
557
559
|
const isPromo = variant === "promo";
|
|
558
560
|
return /*#__PURE__*/React.createElement("button", {
|
|
559
561
|
type: "button",
|
|
560
|
-
className: classnames(styles$
|
|
562
|
+
className: classnames(styles$l.dismiss, isPromo && styles$l["dismiss--promo"]),
|
|
561
563
|
...props
|
|
562
564
|
}, /*#__PURE__*/React.createElement(X, {
|
|
563
565
|
fill: isPromo ? "currentColor" : "white",
|
|
@@ -637,9 +639,9 @@ class StyledAnnouncement extends React.Component {
|
|
|
637
639
|
...platformProps
|
|
638
640
|
} = this.props;
|
|
639
641
|
return /*#__PURE__*/React.createElement("div", {
|
|
640
|
-
className: styles$
|
|
642
|
+
className: styles$l["announcement"]
|
|
641
643
|
}, /*#__PURE__*/React.createElement(AnnouncementIcon, null), /*#__PURE__*/React.createElement("div", {
|
|
642
|
-
className: styles$
|
|
644
|
+
className: styles$l["announcement-content"],
|
|
643
645
|
style: style,
|
|
644
646
|
...platformProps
|
|
645
647
|
}));
|
|
@@ -650,7 +652,7 @@ function StyledDismissButton({
|
|
|
650
652
|
}) {
|
|
651
653
|
return /*#__PURE__*/React.createElement("button", {
|
|
652
654
|
type: "button",
|
|
653
|
-
className: styles$
|
|
655
|
+
className: styles$l.dismiss,
|
|
654
656
|
...props
|
|
655
657
|
}, /*#__PURE__*/React.createElement(X, {
|
|
656
658
|
fill: "white",
|
|
@@ -805,7 +807,7 @@ function useMediaQuery(query) {
|
|
|
805
807
|
return matches;
|
|
806
808
|
}
|
|
807
809
|
|
|
808
|
-
const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1);
|
|
810
|
+
const capitalize$1 = s => s.charAt(0).toUpperCase() + s.slice(1);
|
|
809
811
|
const RESOLVED_COLOR_MODE_ATTRIBUTE = "data-resolved-color-mode";
|
|
810
812
|
function resolveColorMode(colorMode) {
|
|
811
813
|
if (colorMode === "system") {
|
|
@@ -1772,7 +1774,7 @@ function MonoAppIcon({
|
|
|
1772
1774
|
})) : (/*#__PURE__*/React.createElement("div", null, "appName not supported")));
|
|
1773
1775
|
}
|
|
1774
1776
|
|
|
1775
|
-
var styles$
|
|
1777
|
+
var styles$k = {"icon-slot":"icon-slot-m3ZIk","name":"name-UNqbW"};
|
|
1776
1778
|
|
|
1777
1779
|
const AppRow = ({
|
|
1778
1780
|
appName,
|
|
@@ -1781,24 +1783,24 @@ const AppRow = ({
|
|
|
1781
1783
|
}) => {
|
|
1782
1784
|
if (appName === "Accounts") {
|
|
1783
1785
|
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
|
|
1784
|
-
className: styles$
|
|
1786
|
+
className: styles$k["icon-slot"]
|
|
1785
1787
|
}, /*#__PURE__*/React.createElement(MonoAppIcon, {
|
|
1786
1788
|
appName: "Accounts",
|
|
1787
1789
|
colors: colors,
|
|
1788
1790
|
size: iconSize
|
|
1789
1791
|
})), /*#__PURE__*/React.createElement("span", {
|
|
1790
|
-
className: styles$
|
|
1792
|
+
className: styles$k.name
|
|
1791
1793
|
}, "Account settings"));
|
|
1792
1794
|
}
|
|
1793
1795
|
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ColorAppIcon, {
|
|
1794
1796
|
appName: appName.replace(/[\s-]/, ""),
|
|
1795
1797
|
size: iconSize
|
|
1796
1798
|
}), /*#__PURE__*/React.createElement("span", {
|
|
1797
|
-
className: styles$
|
|
1799
|
+
className: styles$k.name
|
|
1798
1800
|
}, appName));
|
|
1799
1801
|
};
|
|
1800
1802
|
|
|
1801
|
-
var styles$
|
|
1803
|
+
var styles$j = {"button":"button-XMbeQ","label":"label-884of","chevron":"chevron-RRg9k"};
|
|
1802
1804
|
|
|
1803
1805
|
const AppsButton = /*#__PURE__*/React.forwardRef(({
|
|
1804
1806
|
appName,
|
|
@@ -1810,10 +1812,10 @@ const AppsButton = /*#__PURE__*/React.forwardRef(({
|
|
|
1810
1812
|
...restProps,
|
|
1811
1813
|
ref: ref,
|
|
1812
1814
|
"aria-label": `App switcher: ${appName}`,
|
|
1813
|
-
className: styles$
|
|
1815
|
+
className: styles$j.button,
|
|
1814
1816
|
kind: "neutral-inline",
|
|
1815
1817
|
label: /*#__PURE__*/React.createElement("span", {
|
|
1816
|
-
className: styles$
|
|
1818
|
+
className: styles$j.label
|
|
1817
1819
|
}, isAccounts ? "Account settings" : appName),
|
|
1818
1820
|
prefix: isAccounts ? (/*#__PURE__*/React.createElement(MonoAppIcon, {
|
|
1819
1821
|
appName: "Accounts",
|
|
@@ -1824,13 +1826,13 @@ const AppsButton = /*#__PURE__*/React.forwardRef(({
|
|
|
1824
1826
|
size: 22
|
|
1825
1827
|
})),
|
|
1826
1828
|
suffix: /*#__PURE__*/React.createElement(DisclosureChevronIcon, {
|
|
1827
|
-
className: `${styles$
|
|
1829
|
+
className: `${styles$j.chevron} symbol`
|
|
1828
1830
|
})
|
|
1829
1831
|
});
|
|
1830
1832
|
});
|
|
1831
1833
|
AppsButton.displayName = "AppsButton";
|
|
1832
1834
|
|
|
1833
|
-
var styles$
|
|
1835
|
+
var styles$i = {"nav":"nav-y-6ML","popover-content":"popover-content-jOAAY","list":"list-jUxvp","link":"link-LMp2h","current":"current-4U4VL","accounts-item":"accounts-item-erJW0","accounts-link":"accounts-link-4jwPW"};
|
|
1834
1836
|
|
|
1835
1837
|
const AppsMenu = ({
|
|
1836
1838
|
appName,
|
|
@@ -1851,8 +1853,8 @@ const AppsMenu = ({
|
|
|
1851
1853
|
}, /*#__PURE__*/React.createElement("a", {
|
|
1852
1854
|
"aria-current": isCurrent ? "page" : undefined,
|
|
1853
1855
|
"aria-label": name,
|
|
1854
|
-
className: classnames(styles$
|
|
1855
|
-
[styles$
|
|
1856
|
+
className: classnames(styles$i.link, {
|
|
1857
|
+
[styles$i.current]: isCurrent
|
|
1856
1858
|
}),
|
|
1857
1859
|
"data-pendo": `app-switcher--change-app--${name}`,
|
|
1858
1860
|
"data-turbolinks": false,
|
|
@@ -1868,22 +1870,22 @@ const AppsMenu = ({
|
|
|
1868
1870
|
colors: colors
|
|
1869
1871
|
})), /*#__PURE__*/React.createElement(tapestry.PopoverContent, {
|
|
1870
1872
|
"aria-label": "Switch app",
|
|
1871
|
-
className: styles$
|
|
1873
|
+
className: styles$i["popover-content"],
|
|
1872
1874
|
placement: "bottom start"
|
|
1873
1875
|
}, /*#__PURE__*/React.createElement("nav", {
|
|
1874
|
-
className: styles$
|
|
1876
|
+
className: styles$i.nav,
|
|
1875
1877
|
"data-pendo": "app-switcher"
|
|
1876
1878
|
}, /*#__PURE__*/React.createElement("ul", {
|
|
1877
|
-
className: styles$
|
|
1879
|
+
className: styles$i.list,
|
|
1878
1880
|
role: "list"
|
|
1879
1881
|
}, homeProduct && renderProductLink(homeProduct.name), restProducts.map(({
|
|
1880
1882
|
name
|
|
1881
1883
|
}) => renderProductLink(name)), accountsProduct && (/*#__PURE__*/React.createElement("li", {
|
|
1882
|
-
className: styles$
|
|
1884
|
+
className: styles$i["accounts-item"]
|
|
1883
1885
|
}, /*#__PURE__*/React.createElement("a", {
|
|
1884
1886
|
"aria-current": appName === accountsProduct.name ? "page" : undefined,
|
|
1885
1887
|
"aria-label": "Account settings",
|
|
1886
|
-
className: classnames(styles$
|
|
1888
|
+
className: classnames(styles$i.link, styles$i["accounts-link"]),
|
|
1887
1889
|
"data-pendo": "app-switcher--change-app--Accounts",
|
|
1888
1890
|
"data-turbolinks": false,
|
|
1889
1891
|
href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name)
|
|
@@ -2091,7 +2093,7 @@ function MonoAppName({
|
|
|
2091
2093
|
})) : (/*#__PURE__*/React.createElement("div", null, "appName not supported"));
|
|
2092
2094
|
}
|
|
2093
2095
|
|
|
2094
|
-
var styles$
|
|
2096
|
+
var styles$h = {"unbutton":"unbutton-lEbTR"};
|
|
2095
2097
|
|
|
2096
2098
|
function Unbutton({
|
|
2097
2099
|
className,
|
|
@@ -2100,7 +2102,7 @@ function Unbutton({
|
|
|
2100
2102
|
...nativeProps
|
|
2101
2103
|
}) {
|
|
2102
2104
|
return /*#__PURE__*/React.createElement("button", {
|
|
2103
|
-
className: classnames(styles$
|
|
2105
|
+
className: classnames(styles$h.unbutton, className),
|
|
2104
2106
|
style: {
|
|
2105
2107
|
...style
|
|
2106
2108
|
},
|
|
@@ -2109,7 +2111,7 @@ function Unbutton({
|
|
|
2109
2111
|
});
|
|
2110
2112
|
}
|
|
2111
2113
|
|
|
2112
|
-
var styles$
|
|
2114
|
+
var styles$g = {"button":"button-wsipc","button-accounts":"button-accounts-zQmBM","chevron":"chevron-obyBF","is-expanded":"is-expanded-PoCQ9"};
|
|
2113
2115
|
|
|
2114
2116
|
const LegacyAppsButton = ({
|
|
2115
2117
|
appName,
|
|
@@ -2124,8 +2126,8 @@ const LegacyAppsButton = ({
|
|
|
2124
2126
|
"aria-haspopup": "true",
|
|
2125
2127
|
"aria-label": `App switcher: ${appName}`,
|
|
2126
2128
|
onClick: onClick,
|
|
2127
|
-
className: classnames(styles$
|
|
2128
|
-
[styles$
|
|
2129
|
+
className: classnames(styles$g.button, {
|
|
2130
|
+
[styles$g["is-expanded"]]: expanded
|
|
2129
2131
|
}),
|
|
2130
2132
|
type: "button",
|
|
2131
2133
|
style: style,
|
|
@@ -2134,7 +2136,7 @@ const LegacyAppsButton = ({
|
|
|
2134
2136
|
appName: "Accounts",
|
|
2135
2137
|
colors: colors
|
|
2136
2138
|
}), /*#__PURE__*/React.createElement("div", {
|
|
2137
|
-
className: styles$
|
|
2139
|
+
className: styles$g["button-accounts"]
|
|
2138
2140
|
}, "Account settings"))) : (/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ColorAppIcon, {
|
|
2139
2141
|
appName: appName.replace(/[\s-]/, "")
|
|
2140
2142
|
}), /*#__PURE__*/React.createElement(MonoAppName, {
|
|
@@ -2142,7 +2144,7 @@ const LegacyAppsButton = ({
|
|
|
2142
2144
|
key: "mono-app-name",
|
|
2143
2145
|
size: 24
|
|
2144
2146
|
}))), /*#__PURE__*/React.createElement(DisclosureChevronIcon, {
|
|
2145
|
-
className: styles$
|
|
2147
|
+
className: styles$g.chevron,
|
|
2146
2148
|
key: "disclosure-chevron-icon"
|
|
2147
2149
|
}));
|
|
2148
2150
|
};
|
|
@@ -2275,7 +2277,7 @@ class Outsider extends React.Component {
|
|
|
2275
2277
|
}
|
|
2276
2278
|
}
|
|
2277
2279
|
|
|
2278
|
-
var styles$
|
|
2280
|
+
var styles$f = {"popup-root":"popup-root-SeL5u","popup":"popup-nQXYW","is-visible":"is-visible-3DAUP"};
|
|
2279
2281
|
|
|
2280
2282
|
const Popup = ({
|
|
2281
2283
|
className,
|
|
@@ -2283,8 +2285,8 @@ const Popup = ({
|
|
|
2283
2285
|
isVisible,
|
|
2284
2286
|
...nativeProps
|
|
2285
2287
|
}) => (/*#__PURE__*/React.createElement(C, {
|
|
2286
|
-
className: classnames(styles$
|
|
2287
|
-
[styles$
|
|
2288
|
+
className: classnames(styles$f.popup, {
|
|
2289
|
+
[styles$f["is-visible"]]: isVisible
|
|
2288
2290
|
}, className),
|
|
2289
2291
|
...nativeProps
|
|
2290
2292
|
}));
|
|
@@ -2292,11 +2294,11 @@ const Popup = ({
|
|
|
2292
2294
|
const PopupRoot = ({
|
|
2293
2295
|
...nativeProps
|
|
2294
2296
|
}) => (/*#__PURE__*/React.createElement("div", {
|
|
2295
|
-
className: styles$
|
|
2297
|
+
className: styles$f["popup-root"],
|
|
2296
2298
|
...nativeProps
|
|
2297
2299
|
}));
|
|
2298
2300
|
|
|
2299
|
-
var styles$
|
|
2301
|
+
var styles$e = {"popup":"popup-bVhSa"};
|
|
2300
2302
|
|
|
2301
2303
|
const LegacyAppsMenu = ({
|
|
2302
2304
|
appName,
|
|
@@ -2334,7 +2336,7 @@ const LegacyAppsMenu = ({
|
|
|
2334
2336
|
cleanup: !visible,
|
|
2335
2337
|
component: Outsider,
|
|
2336
2338
|
onOutsideClick: toggle,
|
|
2337
|
-
className: styles$
|
|
2339
|
+
className: styles$e.popup,
|
|
2338
2340
|
isVisible: visible
|
|
2339
2341
|
}, /*#__PURE__*/React.createElement("menu", {
|
|
2340
2342
|
ref: menuRef,
|
|
@@ -3247,27 +3249,34 @@ const useUpdateStore = () => React.useCallback(updates => {
|
|
|
3247
3249
|
const isMac = () => /Mac|iPhone|iPad|iPod/.test(navigator.platform);
|
|
3248
3250
|
const hasCommandKey = event => isMac() ? event.metaKey : event.ctrlKey;
|
|
3249
3251
|
const commandKeyLabel = () => isMac() ? "⌘" : "Ctrl+";
|
|
3252
|
+
const isSearchShortcut = event => event.key.toLowerCase() === "k" && !event.altKey && !event.shiftKey && hasCommandKey(event);
|
|
3253
|
+
// Products opened their own search with `/` before this one replaced it.
|
|
3254
|
+
const isLegacySearchShortcut = event => event.key === "/" && !event.altKey && !event.ctrlKey && !event.metaKey && !isTypingIn(event.target);
|
|
3255
|
+
const isCopyShortcut = event => event.key.toLowerCase() === "c" && !event.altKey && !event.shiftKey && hasCommandKey(event);
|
|
3256
|
+
const isTypingIn = target => target instanceof Element && target.closest('input, select, textarea, [contenteditable]:not([contenteditable="false"])') !== null;
|
|
3257
|
+
const isTypingKey = event => event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey;
|
|
3258
|
+
const verticalStep = key => ({
|
|
3259
|
+
ArrowDown: 1,
|
|
3260
|
+
ArrowUp: -1
|
|
3261
|
+
})[key];
|
|
3262
|
+
const horizontalStep = key => ({
|
|
3263
|
+
ArrowLeft: -1,
|
|
3264
|
+
ArrowRight: 1
|
|
3265
|
+
})[key];
|
|
3250
3266
|
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
} catch {
|
|
3263
|
-
// Storage can be full or blocked; search still works without it.
|
|
3264
|
-
}
|
|
3267
|
+
function SearchButton({
|
|
3268
|
+
onClick
|
|
3269
|
+
}) {
|
|
3270
|
+
return /*#__PURE__*/React.createElement("button", {
|
|
3271
|
+
"aria-keyshortcuts": `${isMac() ? "Meta+K" : "Control+K"} /`,
|
|
3272
|
+
"aria-label": "Search",
|
|
3273
|
+
onClick: onClick,
|
|
3274
|
+
type: "button"
|
|
3275
|
+
}, /*#__PURE__*/React.createElement(Spyglass, {
|
|
3276
|
+
fill: "currentColor"
|
|
3277
|
+
}));
|
|
3265
3278
|
}
|
|
3266
3279
|
|
|
3267
|
-
var styles$c = {"visuallyHidden":"visuallyHidden-C28ju"};
|
|
3268
|
-
|
|
3269
|
-
var styles$b = {"tab":"tab-lQLDx"};
|
|
3270
|
-
|
|
3271
3280
|
// useId isn't available on React 17, which our peer range still supports.
|
|
3272
3281
|
let instanceCount$1 = 0;
|
|
3273
3282
|
function useInstanceId(prefix) {
|
|
@@ -3275,173 +3284,33 @@ function useInstanceId(prefix) {
|
|
|
3275
3284
|
return id;
|
|
3276
3285
|
}
|
|
3277
3286
|
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
}
|
|
3301
|
-
|
|
3302
|
-
}
|
|
3303
|
-
};
|
|
3304
|
-
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
|
|
3305
|
-
"aria-label": "Product",
|
|
3306
|
-
onKeyDown: handleKeyDown,
|
|
3307
|
-
role: "tablist"
|
|
3308
|
-
}, products.map(product => (/*#__PURE__*/React.createElement("button", {
|
|
3309
|
-
"aria-controls": `${id}-panel`,
|
|
3310
|
-
"aria-selected": product === selected,
|
|
3311
|
-
className: styles$b.tab,
|
|
3312
|
-
id: tabId(product.slug),
|
|
3313
|
-
key: product.slug,
|
|
3314
|
-
onClick: () => onSelect(product.slug),
|
|
3315
|
-
ref: element => {
|
|
3316
|
-
if (element) {
|
|
3317
|
-
tabRefs.current.set(product.slug, element);
|
|
3318
|
-
} else {
|
|
3319
|
-
tabRefs.current.delete(product.slug);
|
|
3320
|
-
}
|
|
3321
|
-
},
|
|
3322
|
-
role: "tab",
|
|
3323
|
-
tabIndex: product === selected ? 0 : -1,
|
|
3324
|
-
type: "button"
|
|
3325
|
-
}, product.appName)))), /*#__PURE__*/React.createElement("div", {
|
|
3326
|
-
"aria-labelledby": tabId(selected.slug),
|
|
3327
|
-
id: `${id}-panel`,
|
|
3328
|
-
role: "tabpanel"
|
|
3329
|
-
}, children));
|
|
3330
|
-
}
|
|
3331
|
-
|
|
3332
|
-
var styles$a = {"result":"result-WCnxr"};
|
|
3333
|
-
|
|
3334
|
-
function SearchResult({
|
|
3335
|
-
highlighted,
|
|
3336
|
-
id,
|
|
3337
|
-
onHighlight,
|
|
3338
|
-
result
|
|
3339
|
-
}) {
|
|
3340
|
-
const {
|
|
3341
|
-
metadata,
|
|
3342
|
-
title,
|
|
3343
|
-
url
|
|
3344
|
-
} = result.attributes;
|
|
3345
|
-
const ref = React.useRef(null);
|
|
3346
|
-
React.useEffect(() => {
|
|
3347
|
-
var _a;
|
|
3348
|
-
if (highlighted) (_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({
|
|
3349
|
-
block: "nearest"
|
|
3350
|
-
});
|
|
3351
|
-
}, [highlighted]);
|
|
3352
|
-
// Focus stays in the search input, which points at the highlighted result
|
|
3353
|
-
// with aria-activedescendant, so results are kept out of the tab order.
|
|
3354
|
-
// Mouse move rather than enter: arrowing scrolls results under a resting
|
|
3355
|
-
// pointer, and that shouldn't steal the highlight.
|
|
3356
|
-
return /*#__PURE__*/React.createElement("a", {
|
|
3357
|
-
"aria-selected": highlighted,
|
|
3358
|
-
className: styles$a.result,
|
|
3359
|
-
href: url,
|
|
3360
|
-
id: id,
|
|
3361
|
-
onMouseMove: onHighlight,
|
|
3362
|
-
ref: ref,
|
|
3363
|
-
role: "option",
|
|
3364
|
-
tabIndex: -1
|
|
3365
|
-
}, /*#__PURE__*/React.createElement("div", null, title), /*#__PURE__*/React.createElement("div", null, summarize(metadata)));
|
|
3366
|
-
}
|
|
3367
|
-
function summarize(metadata) {
|
|
3368
|
-
return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
|
|
3369
|
-
}
|
|
3370
|
-
|
|
3371
|
-
const resultId = (listboxId, index) => `${listboxId}-result-${index}`;
|
|
3372
|
-
function SearchResults({
|
|
3373
|
-
search,
|
|
3374
|
-
term,
|
|
3375
|
-
...navigation
|
|
3376
|
-
}) {
|
|
3377
|
-
var _a;
|
|
3378
|
-
switch (search.status) {
|
|
3379
|
-
case "idle":
|
|
3380
|
-
return null;
|
|
3381
|
-
case "loading":
|
|
3382
|
-
if ((_a = search.groups) === null || _a === void 0 ? void 0 : _a.length) {
|
|
3383
|
-
return /*#__PURE__*/React.createElement(ResultGroups, {
|
|
3384
|
-
busy: true,
|
|
3385
|
-
groups: search.groups,
|
|
3386
|
-
...navigation
|
|
3387
|
-
});
|
|
3388
|
-
}
|
|
3389
|
-
return /*#__PURE__*/React.createElement("p", {
|
|
3390
|
-
role: "status"
|
|
3391
|
-
}, "Searching\u2026");
|
|
3392
|
-
case "error":
|
|
3393
|
-
return /*#__PURE__*/React.createElement("p", {
|
|
3394
|
-
role: "alert"
|
|
3395
|
-
}, "Search isn\u2019t working right now. Try again.");
|
|
3396
|
-
case "done":
|
|
3397
|
-
if (search.groups.length === 0) {
|
|
3398
|
-
return /*#__PURE__*/React.createElement("p", {
|
|
3399
|
-
role: "status"
|
|
3400
|
-
}, "No results for \u201C", term.trim(), "\u201D");
|
|
3401
|
-
}
|
|
3402
|
-
return /*#__PURE__*/React.createElement(ResultGroups, {
|
|
3403
|
-
groups: search.groups,
|
|
3404
|
-
...navigation
|
|
3405
|
-
});
|
|
3287
|
+
// The versions home-mobile pins, so web and mobile read the same response
|
|
3288
|
+
// shapes. Some endpoints only exist from a version on: Registrations' me,
|
|
3289
|
+
// product_search_results, and pcrl_lookups need 2025-05-01.
|
|
3290
|
+
const API_VERSIONS = {
|
|
3291
|
+
calendar: "2022-07-07",
|
|
3292
|
+
"check-ins": "2025-05-28",
|
|
3293
|
+
giving: "2019-10-18",
|
|
3294
|
+
groups: "2023-07-10",
|
|
3295
|
+
home: "2018-08-01",
|
|
3296
|
+
people: "2023-03-21",
|
|
3297
|
+
publishing: "2024-03-25",
|
|
3298
|
+
registrations: "2025-05-01",
|
|
3299
|
+
services: "2018-11-01"
|
|
3300
|
+
};
|
|
3301
|
+
// For a `/~api/{product}/v2/...` URL, the header that pins its product's
|
|
3302
|
+
// version.
|
|
3303
|
+
function apiVersionHeaders(url) {
|
|
3304
|
+
const [,, product] = new URL(url).pathname.split("/");
|
|
3305
|
+
const version = API_VERSIONS[product];
|
|
3306
|
+
if (version) {
|
|
3307
|
+
return {
|
|
3308
|
+
"X-PCO-API-Version": version
|
|
3309
|
+
};
|
|
3310
|
+
} else {
|
|
3311
|
+
return {};
|
|
3406
3312
|
}
|
|
3407
3313
|
}
|
|
3408
|
-
function ResultGroups({
|
|
3409
|
-
busy = false,
|
|
3410
|
-
groups,
|
|
3411
|
-
highlightedIndex,
|
|
3412
|
-
id,
|
|
3413
|
-
label,
|
|
3414
|
-
onHighlight
|
|
3415
|
-
}) {
|
|
3416
|
-
let index = 0;
|
|
3417
|
-
return /*#__PURE__*/React.createElement("div", {
|
|
3418
|
-
"aria-busy": busy,
|
|
3419
|
-
"aria-label": label,
|
|
3420
|
-
id: id,
|
|
3421
|
-
role: "listbox"
|
|
3422
|
-
}, groups.map(({
|
|
3423
|
-
results,
|
|
3424
|
-
type
|
|
3425
|
-
}, groupIndex) => {
|
|
3426
|
-
const headingId = `${id}-group-${groupIndex}`;
|
|
3427
|
-
return /*#__PURE__*/React.createElement("div", {
|
|
3428
|
-
"aria-labelledby": headingId,
|
|
3429
|
-
key: type,
|
|
3430
|
-
role: "group"
|
|
3431
|
-
}, /*#__PURE__*/React.createElement("div", {
|
|
3432
|
-
id: headingId
|
|
3433
|
-
}, type), results.map(result => {
|
|
3434
|
-
const resultIndex = index++;
|
|
3435
|
-
return /*#__PURE__*/React.createElement(SearchResult, {
|
|
3436
|
-
highlighted: resultIndex === highlightedIndex,
|
|
3437
|
-
id: resultId(id, resultIndex),
|
|
3438
|
-
key: result.id,
|
|
3439
|
-
onHighlight: () => onHighlight(resultIndex),
|
|
3440
|
-
result: result
|
|
3441
|
-
});
|
|
3442
|
-
}));
|
|
3443
|
-
}));
|
|
3444
|
-
}
|
|
3445
3314
|
|
|
3446
3315
|
const CACHE_TTL_MS = 60000;
|
|
3447
3316
|
const MAX_ENTRIES$1 = 50;
|
|
@@ -3458,7 +3327,9 @@ function cachedRequest(url) {
|
|
|
3458
3327
|
}
|
|
3459
3328
|
}
|
|
3460
3329
|
function fetchIntoCache(url) {
|
|
3461
|
-
const promise = apiRequest(defaultFetch, url
|
|
3330
|
+
const promise = apiRequest(defaultFetch, url, {
|
|
3331
|
+
headers: apiVersionHeaders(url)
|
|
3332
|
+
}).then(({
|
|
3462
3333
|
json,
|
|
3463
3334
|
ok
|
|
3464
3335
|
}) => {
|
|
@@ -3514,44 +3385,52 @@ function useCachedRequest(url) {
|
|
|
3514
3385
|
};
|
|
3515
3386
|
}
|
|
3516
3387
|
}
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
const
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
const
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
return {
|
|
3541
|
-
|
|
3542
|
-
status: "done"
|
|
3543
|
-
};
|
|
3544
|
-
} else if (request.status === "error") {
|
|
3545
|
-
return {
|
|
3546
|
-
status: "error"
|
|
3547
|
-
};
|
|
3548
|
-
} else {
|
|
3549
|
-
return {
|
|
3550
|
-
groups,
|
|
3551
|
-
status: "loading"
|
|
3388
|
+
// One request per URL, for callers that fan out to a varying number of
|
|
3389
|
+
// endpoints. Unlike `useCachedRequest`, a URL that hasn't settled yet has no
|
|
3390
|
+
// earlier data to fall back on.
|
|
3391
|
+
function useCachedRequests(urls) {
|
|
3392
|
+
const [settled, setSettled] = React.useState({});
|
|
3393
|
+
// Keyed by value because each settle re-renders the caller, and a caller
|
|
3394
|
+
// that builds a fresh `urls` array on render would otherwise loop forever.
|
|
3395
|
+
const key = urls.join("\n");
|
|
3396
|
+
React.useEffect(() => {
|
|
3397
|
+
let current = true;
|
|
3398
|
+
const settle = result => current && setSettled(prev => ({
|
|
3399
|
+
...prev,
|
|
3400
|
+
[result.url]: result
|
|
3401
|
+
}));
|
|
3402
|
+
for (const url of key.split("\n").filter(Boolean)) {
|
|
3403
|
+
cachedRequest(url).then(data => settle({
|
|
3404
|
+
data,
|
|
3405
|
+
url
|
|
3406
|
+
}), () => settle({
|
|
3407
|
+
failed: true,
|
|
3408
|
+
url
|
|
3409
|
+
}));
|
|
3410
|
+
}
|
|
3411
|
+
return () => {
|
|
3412
|
+
current = false;
|
|
3552
3413
|
};
|
|
3553
|
-
}
|
|
3414
|
+
}, [key]);
|
|
3415
|
+
return urls.map(url => {
|
|
3416
|
+
const result = settled[url];
|
|
3417
|
+
if (!result) {
|
|
3418
|
+
return {
|
|
3419
|
+
status: "loading"
|
|
3420
|
+
};
|
|
3421
|
+
} else if ("data" in result) {
|
|
3422
|
+
return {
|
|
3423
|
+
data: result.data,
|
|
3424
|
+
status: "done"
|
|
3425
|
+
};
|
|
3426
|
+
} else {
|
|
3427
|
+
return {
|
|
3428
|
+
status: "error"
|
|
3429
|
+
};
|
|
3430
|
+
}
|
|
3431
|
+
});
|
|
3554
3432
|
}
|
|
3433
|
+
|
|
3555
3434
|
function productSearchUrl(product, query) {
|
|
3556
3435
|
const params = new URLSearchParams({
|
|
3557
3436
|
per_page: "100",
|
|
@@ -3582,14 +3461,7 @@ function groupByType({
|
|
|
3582
3461
|
};
|
|
3583
3462
|
return [...groups.values()].sort((a, b) => rank(a.type) - rank(b.type));
|
|
3584
3463
|
}
|
|
3585
|
-
|
|
3586
|
-
function useSettledProduct(product, status) {
|
|
3587
|
-
const [settledProduct, setSettledProduct] = React.useState();
|
|
3588
|
-
React.useEffect(() => {
|
|
3589
|
-
if (status === "done") setSettledProduct(product);
|
|
3590
|
-
}, [product, status]);
|
|
3591
|
-
return settledProduct;
|
|
3592
|
-
}
|
|
3464
|
+
|
|
3593
3465
|
function useDebouncedValue(value, delay) {
|
|
3594
3466
|
const [debounced, setDebounced] = React.useState(value);
|
|
3595
3467
|
React.useEffect(() => {
|
|
@@ -3599,310 +3471,50 @@ function useDebouncedValue(value, delay) {
|
|
|
3599
3471
|
return debounced;
|
|
3600
3472
|
}
|
|
3601
3473
|
|
|
3602
|
-
const
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
//
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
return undefined;
|
|
3623
|
-
}
|
|
3624
|
-
}, [copiedUrl]);
|
|
3625
|
-
const highlightIndex = React.useCallback(index => setHighlight(current => current.results === results && current.index === index ? current : {
|
|
3626
|
-
index,
|
|
3627
|
-
results
|
|
3628
|
-
}), [results]);
|
|
3629
|
-
const handleKeyDown = event => {
|
|
3630
|
-
const step = verticalStep(event.key);
|
|
3631
|
-
if (step && results.length > 0) {
|
|
3632
|
-
event.preventDefault();
|
|
3633
|
-
highlightIndex((highlightedIndex + step + results.length) % results.length);
|
|
3634
|
-
} else if (event.key === "Enter" && highlighted && !event.nativeEvent.isComposing) {
|
|
3635
|
-
event.preventDefault();
|
|
3636
|
-
open(highlighted.attributes.url, {
|
|
3637
|
-
newTab: hasCommandKey(event)
|
|
3638
|
-
});
|
|
3639
|
-
} else if (isCopyShortcut(event) && highlighted && !hasSelectedText(event.currentTarget)) {
|
|
3640
|
-
event.preventDefault();
|
|
3641
|
-
const {
|
|
3642
|
-
url
|
|
3643
|
-
} = highlighted.attributes;
|
|
3644
|
-
navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
|
|
3645
|
-
}
|
|
3646
|
-
};
|
|
3647
|
-
return {
|
|
3648
|
-
copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.attributes.url),
|
|
3649
|
-
handleKeyDown,
|
|
3650
|
-
highlightIndex,
|
|
3651
|
-
highlighted,
|
|
3652
|
-
highlightedIndex: highlighted ? highlightedIndex : null
|
|
3653
|
-
};
|
|
3654
|
-
}
|
|
3655
|
-
function open(url, {
|
|
3656
|
-
newTab
|
|
3657
|
-
}) {
|
|
3658
|
-
if (newTab) {
|
|
3659
|
-
window.open(url, "_blank", "noopener");
|
|
3660
|
-
} else {
|
|
3661
|
-
window.location.assign(url);
|
|
3662
|
-
}
|
|
3663
|
-
}
|
|
3664
|
-
const isCopyShortcut = event => event.key.toLowerCase() === "c" && !event.altKey && !event.shiftKey && hasCommandKey(event);
|
|
3665
|
-
// With text selected, ⌘C should copy that text as usual, not the link.
|
|
3666
|
-
function hasSelectedText(element) {
|
|
3667
|
-
var _a, _b;
|
|
3668
|
-
const selectedInField = element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
|
|
3669
|
-
return selectedInField || ((_b = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "") !== "";
|
|
3670
|
-
}
|
|
3671
|
-
|
|
3672
|
-
const SEARCHABLE_PRODUCTS = [{
|
|
3673
|
-
appName: "Groups",
|
|
3674
|
-
slug: "groups"
|
|
3675
|
-
}, {
|
|
3676
|
-
appName: "People",
|
|
3677
|
-
slug: "people"
|
|
3678
|
-
}, {
|
|
3679
|
-
appName: "Services",
|
|
3680
|
-
slug: "services"
|
|
3681
|
-
}];
|
|
3682
|
-
const PERMISSION_CHECKS = {
|
|
3683
|
-
people: () => fetchMe("people", "people_permissions,site_administrator").then(({
|
|
3684
|
-
people_permissions,
|
|
3685
|
-
site_administrator
|
|
3686
|
-
}) => site_administrator || people_permissions !== null),
|
|
3687
|
-
services: () => fetchMe("services", "can_search").then(({
|
|
3688
|
-
can_search
|
|
3689
|
-
}) => can_search)
|
|
3690
|
-
};
|
|
3691
|
-
function fetchMe(product, fields) {
|
|
3692
|
-
const params = new URLSearchParams({
|
|
3693
|
-
"fields[Person]": fields
|
|
3694
|
-
});
|
|
3695
|
-
return apiRequest(defaultFetch, `${window.location.origin}/~api/${product}/v2/me?${params}`).then(({
|
|
3696
|
-
json,
|
|
3697
|
-
ok
|
|
3698
|
-
}) => {
|
|
3699
|
-
if (!ok || json === null) throw new Error("Permission check failed");
|
|
3700
|
-
return json.data.attributes;
|
|
3701
|
-
});
|
|
3702
|
-
}
|
|
3703
|
-
// Permissions rarely change mid-session, so a successful check is kept until
|
|
3704
|
-
// the page reloads. A failed one is dropped so a remount tries again.
|
|
3705
|
-
const permissionCache = new Map();
|
|
3706
|
-
function canSearch(slug) {
|
|
3707
|
-
const check = PERMISSION_CHECKS[slug];
|
|
3708
|
-
const cached = permissionCache.get(slug);
|
|
3709
|
-
if (!check) {
|
|
3710
|
-
return Promise.resolve(true);
|
|
3711
|
-
} else if (cached) {
|
|
3712
|
-
return cached;
|
|
3713
|
-
} else {
|
|
3714
|
-
const promise = check().catch(() => {
|
|
3715
|
-
permissionCache.delete(slug);
|
|
3716
|
-
return false;
|
|
3717
|
-
});
|
|
3718
|
-
permissionCache.set(slug, promise);
|
|
3719
|
-
return promise;
|
|
3720
|
-
}
|
|
3721
|
-
}
|
|
3722
|
-
function useSearchableProducts(apps) {
|
|
3723
|
-
// Keyed by name because providers hand over a fresh `apps` array on renders
|
|
3724
|
-
// where nothing changed.
|
|
3725
|
-
const accessibleKey = inSwitcherOrder(apps).map(({
|
|
3726
|
-
slug
|
|
3727
|
-
}) => slug).join(",");
|
|
3728
|
-
const [settled, setSettled] = React.useState();
|
|
3729
|
-
React.useEffect(() => {
|
|
3730
|
-
let current = true;
|
|
3731
|
-
const accessible = accessibleKey.split(",").flatMap(slug => SEARCHABLE_PRODUCTS.filter(product => product.slug === slug));
|
|
3732
|
-
Promise.all(accessible.map(({
|
|
3733
|
-
slug
|
|
3734
|
-
}) => canSearch(slug))).then(allowed => {
|
|
3735
|
-
if (current) {
|
|
3736
|
-
setSettled({
|
|
3737
|
-
accessibleKey,
|
|
3738
|
-
products: accessible.filter((_, index) => allowed[index])
|
|
3739
|
-
});
|
|
3740
|
-
}
|
|
3741
|
-
});
|
|
3742
|
-
return () => {
|
|
3743
|
-
current = false;
|
|
3474
|
+
const SEARCH_DEBOUNCE_MS = 300;
|
|
3475
|
+
function useProductSearch(product, term) {
|
|
3476
|
+
const query = term.trim();
|
|
3477
|
+
const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
|
|
3478
|
+
const request = useCachedRequest(debouncedQuery ? productSearchUrl(product, debouncedQuery) : null);
|
|
3479
|
+
const response = "data" in request ? request.data : undefined;
|
|
3480
|
+
const settledProduct = useSettledProduct(product, request.status);
|
|
3481
|
+
// While a request loads, the previous response stays on screen, but only
|
|
3482
|
+
// if it was for this product: People results must never show under the
|
|
3483
|
+
// Services tab.
|
|
3484
|
+
const visibleResponse = request.status === "done" || settledProduct === product ? response : undefined;
|
|
3485
|
+
const groups = React.useMemo(() => visibleResponse && groupByType(visibleResponse), [visibleResponse]);
|
|
3486
|
+
if (!query) {
|
|
3487
|
+
return {
|
|
3488
|
+
status: "idle"
|
|
3489
|
+
};
|
|
3490
|
+
} else if (query !== debouncedQuery) {
|
|
3491
|
+
return {
|
|
3492
|
+
groups,
|
|
3493
|
+
status: "loading"
|
|
3744
3494
|
};
|
|
3745
|
-
}
|
|
3746
|
-
if ((settled === null || settled === void 0 ? void 0 : settled.accessibleKey) === accessibleKey) {
|
|
3495
|
+
} else if (request.status === "done") {
|
|
3747
3496
|
return {
|
|
3748
|
-
|
|
3497
|
+
groups: groups !== null && groups !== void 0 ? groups : [],
|
|
3749
3498
|
status: "done"
|
|
3750
3499
|
};
|
|
3500
|
+
} else if (request.status === "error") {
|
|
3501
|
+
return {
|
|
3502
|
+
status: "error"
|
|
3503
|
+
};
|
|
3751
3504
|
} else {
|
|
3752
3505
|
return {
|
|
3506
|
+
groups,
|
|
3753
3507
|
status: "loading"
|
|
3754
3508
|
};
|
|
3755
3509
|
}
|
|
3756
3510
|
}
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
const isSearchShortcut = event => event.key.toLowerCase() === "k" && !event.altKey && !event.shiftKey && hasCommandKey(event);
|
|
3762
|
-
// Products opened their own search with `/` before this one replaced it.
|
|
3763
|
-
const isLegacySearchShortcut = event => event.key === "/" && !event.altKey && !event.ctrlKey && !event.metaKey && !isTypingIn(event.target);
|
|
3764
|
-
const isTypingKey = event => event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey;
|
|
3765
|
-
const isTypingIn = target => target instanceof Element && target.closest('input, select, textarea, [contenteditable]:not([contenteditable="false"])') !== null;
|
|
3766
|
-
function ProductSearch({
|
|
3767
|
-
apps
|
|
3768
|
-
}) {
|
|
3769
|
-
const searchable = useSearchableProducts(apps);
|
|
3770
|
-
if (searchable.status === "done" && searchable.products.length > 0) {
|
|
3771
|
-
return /*#__PURE__*/React.createElement(SearchButtonAndDialog, {
|
|
3772
|
-
products: searchable.products
|
|
3773
|
-
});
|
|
3774
|
-
} else {
|
|
3775
|
-
return null;
|
|
3776
|
-
}
|
|
3777
|
-
}
|
|
3778
|
-
function SearchButtonAndDialog({
|
|
3779
|
-
products
|
|
3780
|
-
}) {
|
|
3781
|
-
const [open, setOpen] = React.useState(false);
|
|
3782
|
-
React.useEffect(() => {
|
|
3783
|
-
// Rich text editors like Trix bind ⌘K to "insert link"; capturing at the
|
|
3784
|
-
// window and stopping propagation keeps them from ever seeing it.
|
|
3785
|
-
const handleKeyDown = event => {
|
|
3786
|
-
if (isSearchShortcut(event)) {
|
|
3787
|
-
event.preventDefault();
|
|
3788
|
-
event.stopPropagation();
|
|
3789
|
-
setOpen(wasOpen => !wasOpen);
|
|
3790
|
-
} else if (isLegacySearchShortcut(event)) {
|
|
3791
|
-
event.preventDefault();
|
|
3792
|
-
event.stopPropagation();
|
|
3793
|
-
setOpen(true);
|
|
3794
|
-
}
|
|
3795
|
-
};
|
|
3796
|
-
window.addEventListener("keydown", handleKeyDown, {
|
|
3797
|
-
capture: true
|
|
3798
|
-
});
|
|
3799
|
-
return () => window.removeEventListener("keydown", handleKeyDown, {
|
|
3800
|
-
capture: true
|
|
3801
|
-
});
|
|
3802
|
-
}, []);
|
|
3803
|
-
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
|
|
3804
|
-
"aria-keyshortcuts": `${isMac() ? "Meta+K" : "Control+K"} /`,
|
|
3805
|
-
"aria-label": "Search",
|
|
3806
|
-
onClick: () => setOpen(true),
|
|
3807
|
-
type: "button"
|
|
3808
|
-
}, /*#__PURE__*/React.createElement(Spyglass, {
|
|
3809
|
-
fill: "currentColor"
|
|
3810
|
-
})), /*#__PURE__*/React.createElement(tapestry.Modal, {
|
|
3811
|
-
"aria-label": "Search",
|
|
3812
|
-
dismissible: true,
|
|
3813
|
-
onClose: () => setOpen(false),
|
|
3814
|
-
open: open
|
|
3815
|
-
}, /*#__PURE__*/React.createElement(SearchPanel, {
|
|
3816
|
-
products: products
|
|
3817
|
-
})));
|
|
3818
|
-
}
|
|
3819
|
-
function SearchPanel({
|
|
3820
|
-
products
|
|
3821
|
-
}) {
|
|
3822
|
-
var _a, _b;
|
|
3823
|
-
const listboxId = useInstanceId("product-search-results");
|
|
3824
|
-
const [term, setTerm] = React.useState("");
|
|
3825
|
-
const [selectedSlug, setSelectedSlug] = React.useState(() => initialProduct(products).slug);
|
|
3826
|
-
const product = (_a = products.find(({
|
|
3827
|
-
slug
|
|
3828
|
-
}) => slug === selectedSlug)) !== null && _a !== void 0 ? _a : products[0];
|
|
3829
|
-
const hasTerm = term.trim() !== "";
|
|
3830
|
-
const search = useProductSearch(product.slug, term);
|
|
3831
|
-
const groups = "groups" in search ? search.groups : undefined;
|
|
3832
|
-
const results = React.useMemo(() => {
|
|
3833
|
-
var _a;
|
|
3834
|
-
return (_a = groups === null || groups === void 0 ? void 0 : groups.flatMap(group => group.results)) !== null && _a !== void 0 ? _a : [];
|
|
3835
|
-
}, [groups]);
|
|
3836
|
-
const navigation = useResultNavigation(results);
|
|
3837
|
-
const {
|
|
3838
|
-
highlightedIndex
|
|
3839
|
-
} = navigation;
|
|
3840
|
-
const inputRef = React.useRef(null);
|
|
3841
|
-
const [announceHighlight, setAnnounceHighlight] = React.useState(false);
|
|
3842
|
-
// The tabs keep focus while arrowing through results, but only the focused
|
|
3843
|
-
// input can point screen readers at the highlight, so from the tabs it's
|
|
3844
|
-
// announced instead. Typing goes back to the input, keystroke and all.
|
|
3845
|
-
const handleTabsKeyDown = event => {
|
|
3846
|
-
var _a;
|
|
3847
|
-
if (isTypingKey(event)) {
|
|
3848
|
-
(_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
3849
|
-
} else if (verticalStep(event.key)) {
|
|
3850
|
-
setAnnounceHighlight(true);
|
|
3851
|
-
navigation.handleKeyDown(event);
|
|
3852
|
-
} else {
|
|
3853
|
-
navigation.handleKeyDown(event);
|
|
3854
|
-
}
|
|
3855
|
-
};
|
|
3511
|
+
// The product the most recent finished request was for.
|
|
3512
|
+
function useSettledProduct(product, status) {
|
|
3513
|
+
const [settledProduct, setSettledProduct] = React.useState();
|
|
3856
3514
|
React.useEffect(() => {
|
|
3857
|
-
if (
|
|
3858
|
-
}, [
|
|
3859
|
-
return
|
|
3860
|
-
"aria-activedescendant": highlightedIndex === null ? undefined : resultId(listboxId, highlightedIndex),
|
|
3861
|
-
"aria-autocomplete": "list",
|
|
3862
|
-
"aria-controls": results.length > 0 ? listboxId : undefined,
|
|
3863
|
-
"aria-expanded": results.length > 0,
|
|
3864
|
-
"aria-label": "Search",
|
|
3865
|
-
autoFocus: true,
|
|
3866
|
-
onChange: event => setTerm(event.target.value),
|
|
3867
|
-
onFocus: () => setAnnounceHighlight(false),
|
|
3868
|
-
onKeyDown: navigation.handleKeyDown,
|
|
3869
|
-
placeholder: "Search",
|
|
3870
|
-
ref: inputRef,
|
|
3871
|
-
role: "combobox",
|
|
3872
|
-
type: "search",
|
|
3873
|
-
value: term
|
|
3874
|
-
}), /*#__PURE__*/React.createElement(ProductTabs, {
|
|
3875
|
-
onKeyDown: handleTabsKeyDown,
|
|
3876
|
-
onSelect: setSelectedSlug,
|
|
3877
|
-
products: products,
|
|
3878
|
-
selected: product
|
|
3879
|
-
}, /*#__PURE__*/React.createElement(SearchResults, {
|
|
3880
|
-
highlightedIndex: highlightedIndex,
|
|
3881
|
-
id: listboxId,
|
|
3882
|
-
label: `${product.appName} results`,
|
|
3883
|
-
onHighlight: navigation.highlightIndex,
|
|
3884
|
-
search: search,
|
|
3885
|
-
term: term
|
|
3886
|
-
})), /*#__PURE__*/React.createElement("div", {
|
|
3887
|
-
"aria-live": "polite",
|
|
3888
|
-
className: styles$c.visuallyHidden
|
|
3889
|
-
}, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.attributes.title : null), /*#__PURE__*/React.createElement(KeyboardHints, {
|
|
3890
|
-
copied: navigation.copied
|
|
3891
|
-
}));
|
|
3892
|
-
}
|
|
3893
|
-
function KeyboardHints({
|
|
3894
|
-
copied
|
|
3895
|
-
}) {
|
|
3896
|
-
const command = commandKeyLabel();
|
|
3897
|
-
return /*#__PURE__*/React.createElement("p", null, "\u2191\u2193 Select \u00B7 \u21B5 Open \u00B7 ", command, "\u21B5 Open in new tab \u00B7 ", command, "C", " ", /*#__PURE__*/React.createElement("span", {
|
|
3898
|
-
role: "status"
|
|
3899
|
-
}, copied ? "Copied!" : "Copy link"), " \u00B7 ", command, "K Open search");
|
|
3900
|
-
}
|
|
3901
|
-
function initialProduct(products) {
|
|
3902
|
-
var _a, _b, _c;
|
|
3903
|
-
const lastProduct = readLastProduct();
|
|
3904
|
-
const bySlug = slug => products.find(product => product.slug === slug);
|
|
3905
|
-
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];
|
|
3515
|
+
if (status === "done") setSettledProduct(product);
|
|
3516
|
+
}, [product, status]);
|
|
3517
|
+
return settledProduct;
|
|
3906
3518
|
}
|
|
3907
3519
|
|
|
3908
3520
|
function parseValue(element) {
|
|
@@ -3971,10 +3583,14 @@ function useRecentlyVisitableMetadata() {
|
|
|
3971
3583
|
return shim.useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
|
|
3972
3584
|
}
|
|
3973
3585
|
|
|
3974
|
-
|
|
3975
|
-
|
|
3586
|
+
// Entries match the ones home-mobile stores, so one list can sync across web
|
|
3587
|
+
// and mobile.
|
|
3588
|
+
const ENTRY_VERSION = 3;
|
|
3589
|
+
const MAX_ENTRIES = 50;
|
|
3976
3590
|
const MAX_TEXT_LENGTH = 200;
|
|
3977
3591
|
const PRODUCTS = ["accounts", "calendar", "check-ins", "giving", "groups", "home", "login", "people", "publishing", "registrations", "services"];
|
|
3592
|
+
// Login has no pages worth returning to.
|
|
3593
|
+
const APPS = PRODUCTS.filter(product => product !== "login");
|
|
3978
3594
|
function isId(value) {
|
|
3979
3595
|
return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
|
|
3980
3596
|
}
|
|
@@ -3983,169 +3599,1116 @@ function isPcrl(value) {
|
|
|
3983
3599
|
const segments = value.split("/");
|
|
3984
3600
|
return segments.length === 4 && PRODUCTS.some(product => product === segments[0]) && isId(segments[3]);
|
|
3985
3601
|
}
|
|
3986
|
-
function
|
|
3987
|
-
return
|
|
3602
|
+
function isApp(value) {
|
|
3603
|
+
return APPS.some(app => app === value);
|
|
3604
|
+
}
|
|
3605
|
+
// Links are built by resolving the path against its app's origin, so a stored
|
|
3606
|
+
// path must not be able to resolve anywhere else (`//evil.com`, `/\evil.com`).
|
|
3607
|
+
function isPath(value) {
|
|
3608
|
+
const origin = "https://app.invalid";
|
|
3609
|
+
try {
|
|
3610
|
+
return typeof value === "string" && value.startsWith("/") && new URL(value, origin).origin === origin;
|
|
3611
|
+
} catch {
|
|
3612
|
+
return false;
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
function isNonBlankString(value) {
|
|
3616
|
+
return typeof value === "string" && value.trim() !== "";
|
|
3617
|
+
}
|
|
3618
|
+
function isOptionalString(value) {
|
|
3619
|
+
return value === undefined || typeof value === "string";
|
|
3620
|
+
}
|
|
3621
|
+
function isRecord(value) {
|
|
3622
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3623
|
+
}
|
|
3624
|
+
function isPageMetadata(value) {
|
|
3625
|
+
return value === undefined || isRecord(value) && isOptionalString(value.details) && isOptionalString(value.type);
|
|
3626
|
+
}
|
|
3627
|
+
// The query string is part of the link but not of the page's identity, so
|
|
3628
|
+
// revisiting with other filters moves the existing entry instead of adding one.
|
|
3629
|
+
function entryKey(entry) {
|
|
3630
|
+
if (entry.type === "resource") {
|
|
3631
|
+
return `resource:${entry.pcrl}`;
|
|
3632
|
+
} else {
|
|
3633
|
+
return `page:${entry.app}:${entry.path.split("?")[0]}`;
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
function isEntry(value) {
|
|
3637
|
+
if (!isRecord(value) || value.version !== ENTRY_VERSION || typeof value.visitedAt !== "string") {
|
|
3638
|
+
return false;
|
|
3639
|
+
} else if (value.type === "resource") {
|
|
3640
|
+
return typeof value.pcrl === "string" && isPcrl(value.pcrl);
|
|
3641
|
+
} else if (value.type === "page") {
|
|
3642
|
+
return isApp(value.app) && isPath(value.path) && isNonBlankString(value.title) && isPageMetadata(value.metadata);
|
|
3643
|
+
} else {
|
|
3644
|
+
return false;
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
// Cuts on code points so a surrogate pair is never split.
|
|
3648
|
+
function clampText(value) {
|
|
3649
|
+
return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
|
|
3650
|
+
}
|
|
3651
|
+
function clampMetadata({
|
|
3652
|
+
details,
|
|
3653
|
+
type
|
|
3654
|
+
}) {
|
|
3655
|
+
return {
|
|
3656
|
+
...(details && {
|
|
3657
|
+
details: clampText(details)
|
|
3658
|
+
}),
|
|
3659
|
+
...(type && {
|
|
3660
|
+
type: clampText(type)
|
|
3661
|
+
})
|
|
3662
|
+
};
|
|
3663
|
+
}
|
|
3664
|
+
function clampVisit(visit) {
|
|
3665
|
+
if (visit.type === "resource") {
|
|
3666
|
+
return visit;
|
|
3667
|
+
} else {
|
|
3668
|
+
return {
|
|
3669
|
+
...visit,
|
|
3670
|
+
...(visit.metadata && {
|
|
3671
|
+
metadata: clampMetadata(visit.metadata)
|
|
3672
|
+
}),
|
|
3673
|
+
title: clampText(visit.title.trim())
|
|
3674
|
+
};
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
function toEntry(visit, visitedAt) {
|
|
3678
|
+
return {
|
|
3679
|
+
...clampVisit(visit),
|
|
3680
|
+
version: ENTRY_VERSION,
|
|
3681
|
+
visitedAt: visitedAt.toISOString()
|
|
3682
|
+
};
|
|
3683
|
+
}
|
|
3684
|
+
// Recents are disposable: anything unexpected reads as an empty list (or, for
|
|
3685
|
+
// a single bad entry, a list without it) rather than throwing.
|
|
3686
|
+
function parseRecentlyVisited(value) {
|
|
3687
|
+
let parsed = null;
|
|
3688
|
+
try {
|
|
3689
|
+
parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
|
|
3690
|
+
} catch {
|
|
3691
|
+
parsed = null;
|
|
3692
|
+
}
|
|
3693
|
+
if (Array.isArray(parsed)) {
|
|
3694
|
+
return parsed.filter(isEntry);
|
|
3695
|
+
} else {
|
|
3696
|
+
return [];
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
function addVisit(entries, entry) {
|
|
3700
|
+
const key = entryKey(entry);
|
|
3701
|
+
return [entry, ...entries.filter(existing => entryKey(existing) !== key)].slice(0, MAX_ENTRIES);
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
// The app whose URL, in this environment, has the current page's host.
|
|
3705
|
+
// Reversing pcoUrl handles every environment @planningcenter/url knows about,
|
|
3706
|
+
// and guarantees the stored app rebuilds a working link.
|
|
3707
|
+
function currentApp() {
|
|
3708
|
+
var _a;
|
|
3709
|
+
return (_a = APPS.find(app => new URL(pcoUrl$1.pcoUrl(app)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
|
|
3710
|
+
}
|
|
3711
|
+
function isFilled(value) {
|
|
3712
|
+
return typeof value === "string" && value !== "";
|
|
3713
|
+
}
|
|
3714
|
+
// The tag's metadata is product-supplied JSON; only its string `type` and
|
|
3715
|
+
// `details` carry over, and an entry without either has no metadata at all.
|
|
3716
|
+
function metadataFromPage(metadata) {
|
|
3717
|
+
var _a;
|
|
3718
|
+
try {
|
|
3719
|
+
const {
|
|
3720
|
+
details,
|
|
3721
|
+
type
|
|
3722
|
+
} = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) !== null && _a !== void 0 ? _a : {};
|
|
3723
|
+
if (isFilled(details) || isFilled(type)) {
|
|
3724
|
+
return {
|
|
3725
|
+
...(isFilled(details) && {
|
|
3726
|
+
details
|
|
3727
|
+
}),
|
|
3728
|
+
...(isFilled(type) && {
|
|
3729
|
+
type
|
|
3730
|
+
})
|
|
3731
|
+
};
|
|
3732
|
+
} else {
|
|
3733
|
+
return undefined;
|
|
3734
|
+
}
|
|
3735
|
+
} catch {
|
|
3736
|
+
return undefined;
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
// Staff impersonating someone browse under `/O<org id>/P<person id>/…`.
|
|
3740
|
+
function withoutImpersonation(segments) {
|
|
3741
|
+
const [org = "", person = ""] = segments;
|
|
3742
|
+
if (org.startsWith("O") && isId(org.slice(1)) && person.startsWith("P") && isId(person.slice(1))) {
|
|
3743
|
+
return segments.slice(2);
|
|
3744
|
+
} else {
|
|
3745
|
+
return segments;
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
// Normalized the way home-mobile does it, so a page visited on web and on
|
|
3749
|
+
// mobile is one entry once the list syncs.
|
|
3750
|
+
function pathFromLocation({
|
|
3751
|
+
pathname,
|
|
3752
|
+
search
|
|
3753
|
+
}) {
|
|
3754
|
+
const segments = withoutImpersonation(pathname.split("/").filter(Boolean));
|
|
3755
|
+
if (segments.length === 0) {
|
|
3756
|
+
return "/";
|
|
3757
|
+
} else {
|
|
3758
|
+
return `/${segments.join("/")}${search.length > 1 ? search : ""}`;
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
// pco:resource is the more specific signal, so it wins when both tags are
|
|
3762
|
+
// present. A resource that isn't a PCRL is ignored rather than recorded.
|
|
3763
|
+
function visitFromPage({
|
|
3764
|
+
metadata,
|
|
3765
|
+
resource,
|
|
3766
|
+
title
|
|
3767
|
+
}) {
|
|
3768
|
+
const app = currentApp();
|
|
3769
|
+
if (resource && isPcrl(resource)) {
|
|
3770
|
+
return {
|
|
3771
|
+
pcrl: resource,
|
|
3772
|
+
type: "resource"
|
|
3773
|
+
};
|
|
3774
|
+
} else if (title && title.trim() !== "" && app) {
|
|
3775
|
+
const pageMetadata = metadataFromPage(metadata);
|
|
3776
|
+
return {
|
|
3777
|
+
app,
|
|
3778
|
+
...(pageMetadata && {
|
|
3779
|
+
metadata: pageMetadata
|
|
3780
|
+
}),
|
|
3781
|
+
path: pathFromLocation(window.location),
|
|
3782
|
+
title,
|
|
3783
|
+
type: "page"
|
|
3784
|
+
};
|
|
3785
|
+
} else {
|
|
3786
|
+
return null;
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
// The entry Recently Visited would record for the current page, or null when
|
|
3790
|
+
// the page isn't eligible. Shared by the recorder and the debug panel so the
|
|
3791
|
+
// panel always shows what actually gets stored.
|
|
3792
|
+
function useCurrentVisit() {
|
|
3793
|
+
const {
|
|
3794
|
+
recently_visitable: title,
|
|
3795
|
+
resource
|
|
3796
|
+
} = usePcoMeta();
|
|
3797
|
+
const metadata = useRecentlyVisitableMetadata();
|
|
3798
|
+
return React.useMemo(() => visitFromPage({
|
|
3799
|
+
metadata,
|
|
3800
|
+
resource,
|
|
3801
|
+
title
|
|
3802
|
+
}), [metadata, resource, title]);
|
|
3803
|
+
}
|
|
3804
|
+
// Products hand the person's ID to AppsProvider, not to the topbar itself,
|
|
3805
|
+
// and several pass it as a number.
|
|
3806
|
+
function useCurrentPersonId() {
|
|
3807
|
+
const {
|
|
3808
|
+
userId
|
|
3809
|
+
} = useStore();
|
|
3810
|
+
return userId == null ? undefined : String(userId);
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
// Per-origin localStorage is a stopgap: each product keeps its own list until
|
|
3814
|
+
// PCA-409 moves this to storage shared across product subdomains. Only this
|
|
3815
|
+
// file should need to change when it does.
|
|
3816
|
+
function storageKey({
|
|
3817
|
+
env,
|
|
3818
|
+
personId
|
|
3819
|
+
}) {
|
|
3820
|
+
return `topbar.recently_visited.${env}.${personId}`;
|
|
3821
|
+
}
|
|
3822
|
+
// localStorage doesn't notify the tab that wrote it, so readers in this tab
|
|
3823
|
+
// learn about new visits from this counter instead.
|
|
3824
|
+
const changes = createStore({
|
|
3825
|
+
version: 0
|
|
3826
|
+
});
|
|
3827
|
+
function readRecentlyVisited(scope) {
|
|
3828
|
+
try {
|
|
3829
|
+
return parseRecentlyVisited(localStorage.getItem(storageKey(scope)));
|
|
3830
|
+
} catch {
|
|
3831
|
+
return [];
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
function recordVisit(visit, scope) {
|
|
3835
|
+
const entry = toEntry(visit, new Date());
|
|
3836
|
+
if (isEntry(entry)) {
|
|
3837
|
+
try {
|
|
3838
|
+
localStorage.setItem(storageKey(scope), JSON.stringify(addVisit(readRecentlyVisited(scope), entry)));
|
|
3839
|
+
changes.setState({
|
|
3840
|
+
version: changes.getState().version + 1
|
|
3841
|
+
});
|
|
3842
|
+
} catch {
|
|
3843
|
+
// Storage can be full or blocked; recents are disposable.
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
function useRecentlyVisited({
|
|
3848
|
+
env,
|
|
3849
|
+
personId
|
|
3850
|
+
}) {
|
|
3851
|
+
const version = shim.useSyncExternalStore(changes.subscribe, () => changes.getState().version);
|
|
3852
|
+
// `version` isn't read, but listing it re-reads storage after each visit.
|
|
3853
|
+
return React.useMemo(() => personId ? readRecentlyVisited({
|
|
3854
|
+
env,
|
|
3855
|
+
personId
|
|
3856
|
+
}) : [], [env, personId, version]);
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
const pcoUrl = pcoUrl$1;
|
|
3860
|
+
|
|
3861
|
+
// Resolves resource entries with one pcrl_lookups request per product.
|
|
3862
|
+
// pcrl_lookups reports forbidden and deleted resources the same way, so both
|
|
3863
|
+
// become `unresolved`, as does every entry in a batch whose request failed.
|
|
3864
|
+
function useHydratedRecentlyVisited(entries, env) {
|
|
3865
|
+
const batches = React.useMemo(() => batchByProduct(entries), [entries]);
|
|
3866
|
+
const urls = React.useMemo(() => batches.map(([product, pcrls]) => pcrlLookupsUrl(product, pcrls)), [batches]);
|
|
3867
|
+
const requests = useCachedRequests(urls);
|
|
3868
|
+
const lookups = new Map();
|
|
3869
|
+
batches.forEach(([, pcrls], index) => {
|
|
3870
|
+
const request = requests[index];
|
|
3871
|
+
for (const pcrl of pcrls) lookups.set(pcrl, lookupFor(pcrl, request));
|
|
3872
|
+
});
|
|
3873
|
+
return entries.map(entry => {
|
|
3874
|
+
var _a;
|
|
3875
|
+
if (entry.type === "page") {
|
|
3876
|
+
return {
|
|
3877
|
+
entry,
|
|
3878
|
+
status: "page",
|
|
3879
|
+
url: pageUrl(entry, env)
|
|
3880
|
+
};
|
|
3881
|
+
} else {
|
|
3882
|
+
const lookup = (_a = lookups.get(entry.pcrl)) !== null && _a !== void 0 ? _a : {
|
|
3883
|
+
status: "unresolved"
|
|
3884
|
+
};
|
|
3885
|
+
if (lookup.status === "resolved") {
|
|
3886
|
+
return {
|
|
3887
|
+
...lookup,
|
|
3888
|
+
entry,
|
|
3889
|
+
url: lookup.result.attributes.url
|
|
3890
|
+
};
|
|
3891
|
+
} else {
|
|
3892
|
+
return {
|
|
3893
|
+
entry,
|
|
3894
|
+
status: lookup.status
|
|
3895
|
+
};
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
});
|
|
3899
|
+
}
|
|
3900
|
+
function lookupFor(pcrl, request) {
|
|
3901
|
+
if (request.status === "done") {
|
|
3902
|
+
const result = request.data.data.find(({
|
|
3903
|
+
attributes
|
|
3904
|
+
}) => attributes.pcrl === pcrl);
|
|
3905
|
+
return result ? {
|
|
3906
|
+
result,
|
|
3907
|
+
status: "resolved"
|
|
3908
|
+
} : {
|
|
3909
|
+
status: "unresolved"
|
|
3910
|
+
};
|
|
3911
|
+
} else if (request.status === "error") {
|
|
3912
|
+
return {
|
|
3913
|
+
status: "unresolved"
|
|
3914
|
+
};
|
|
3915
|
+
} else {
|
|
3916
|
+
return {
|
|
3917
|
+
status: "loading"
|
|
3918
|
+
};
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
const RECENTLY_VISITED_PAGE_SIZE = 10;
|
|
3922
|
+
// A PCRL can only be looked up through its own product's API, which may not be
|
|
3923
|
+
// the product it was visited in. Each page of entries is batched on its own,
|
|
3924
|
+
// so loading the next page leaves earlier pages' URLs, and so their cache
|
|
3925
|
+
// keys, alone. Sorting keeps them stable as a page reorders.
|
|
3926
|
+
function batchByProduct(entries) {
|
|
3927
|
+
var _a;
|
|
3928
|
+
const batches = [];
|
|
3929
|
+
for (let start = 0; start < entries.length; start += RECENTLY_VISITED_PAGE_SIZE) {
|
|
3930
|
+
const page = new Map();
|
|
3931
|
+
for (const entry of entries.slice(start, start + RECENTLY_VISITED_PAGE_SIZE)) {
|
|
3932
|
+
if (entry.type === "resource") {
|
|
3933
|
+
const [product] = entry.pcrl.split("/");
|
|
3934
|
+
page.set(product, [...((_a = page.get(product)) !== null && _a !== void 0 ? _a : []), entry.pcrl]);
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
for (const [product, pcrls] of page) {
|
|
3938
|
+
batches.push([product, [...pcrls].sort()]);
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
return batches;
|
|
3942
|
+
}
|
|
3943
|
+
function pcrlLookupsUrl(product, pcrls) {
|
|
3944
|
+
const params = new URLSearchParams({
|
|
3945
|
+
"where[pcrl]": pcrls.join(",")
|
|
3946
|
+
});
|
|
3947
|
+
return `${window.location.origin}/~api/${product}/v2/pcrl_lookups?${params}`;
|
|
3948
|
+
}
|
|
3949
|
+
// Built from the app rather than the current origin, so it stays right once
|
|
3950
|
+
// PCA-409 shares one list across product subdomains.
|
|
3951
|
+
function pageUrl(entry, env) {
|
|
3952
|
+
return new URL(entry.path, pcoUrl(env)(entry.app)).href;
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
function useRecentVisits(env) {
|
|
3956
|
+
const personId = useCurrentPersonId();
|
|
3957
|
+
const entries = useRecentlyVisited({
|
|
3958
|
+
env,
|
|
3959
|
+
personId
|
|
3960
|
+
});
|
|
3961
|
+
const [count, setCount] = React.useState(RECENTLY_VISITED_PAGE_SIZE);
|
|
3962
|
+
const shown = React.useMemo(() => entries.slice(0, count), [entries, count]);
|
|
3963
|
+
const hydrated = useHydratedRecentlyVisited(shown, env);
|
|
3964
|
+
// Resources show up once they're found. Ones that can't be found anymore,
|
|
3965
|
+
// or that the person can no longer see, are left out.
|
|
3966
|
+
const visits = hydrated.filter(isShown);
|
|
3967
|
+
const loading = hydrated.some(({
|
|
3968
|
+
status
|
|
3969
|
+
}) => status === "loading");
|
|
3970
|
+
const showMore = React.useCallback(() => setCount(current => current + RECENTLY_VISITED_PAGE_SIZE), []);
|
|
3971
|
+
return {
|
|
3972
|
+
count,
|
|
3973
|
+
hasMore: entries.length > count,
|
|
3974
|
+
loading,
|
|
3975
|
+
showMore,
|
|
3976
|
+
visits
|
|
3977
|
+
};
|
|
3978
|
+
}
|
|
3979
|
+
function isShown(visit) {
|
|
3980
|
+
return visit.status === "page" || visit.status === "resolved";
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3983
|
+
function open(url, {
|
|
3984
|
+
newTab
|
|
3985
|
+
}) {
|
|
3986
|
+
if (newTab) {
|
|
3987
|
+
window.open(url, "_blank", "noopener");
|
|
3988
|
+
} else {
|
|
3989
|
+
window.location.assign(url);
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
// With text selected, ⌘C should copy that text as usual, not the link.
|
|
3993
|
+
function hasSelectedText(element) {
|
|
3994
|
+
var _a, _b;
|
|
3995
|
+
const selectedInField = element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
|
|
3996
|
+
return selectedInField || ((_b = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "") !== "";
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
const COPIED_FEEDBACK_MS = 1500;
|
|
4000
|
+
// `copiedUrl` stays set briefly after a copy so the UI can confirm it.
|
|
4001
|
+
function useCopyLink() {
|
|
4002
|
+
const [copiedUrl, setCopiedUrl] = React.useState(null);
|
|
4003
|
+
React.useEffect(() => {
|
|
4004
|
+
if (copiedUrl) {
|
|
4005
|
+
const timeout = setTimeout(() => setCopiedUrl(null), COPIED_FEEDBACK_MS);
|
|
4006
|
+
return () => clearTimeout(timeout);
|
|
4007
|
+
} else {
|
|
4008
|
+
return undefined;
|
|
4009
|
+
}
|
|
4010
|
+
}, [copiedUrl]);
|
|
4011
|
+
const copy = React.useCallback(url => {
|
|
4012
|
+
navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
|
|
4013
|
+
}, []);
|
|
4014
|
+
return {
|
|
4015
|
+
copiedUrl,
|
|
4016
|
+
copy
|
|
4017
|
+
};
|
|
4018
|
+
}
|
|
4019
|
+
|
|
4020
|
+
// Remembering which list the index belongs to puts the highlight back on the
|
|
4021
|
+
// first item whenever a new list replaces it, without an effect to reset it.
|
|
4022
|
+
// A list that grows or fills in under the same `list` keeps its highlight.
|
|
4023
|
+
function useResultNavigation(items, list) {
|
|
4024
|
+
const [highlight, setHighlight] = React.useState({
|
|
4025
|
+
index: 0,
|
|
4026
|
+
list
|
|
4027
|
+
});
|
|
4028
|
+
const highlightedIndex = highlight.list === list ? highlight.index : 0;
|
|
4029
|
+
const highlighted = items[highlightedIndex];
|
|
4030
|
+
const {
|
|
4031
|
+
copiedUrl,
|
|
4032
|
+
copy
|
|
4033
|
+
} = useCopyLink();
|
|
4034
|
+
const highlightIndex = React.useCallback(index => setHighlight(current => current.list === list && current.index === index ? current : {
|
|
4035
|
+
index,
|
|
4036
|
+
list
|
|
4037
|
+
}), [list]);
|
|
4038
|
+
const handleKeyDown = event => {
|
|
4039
|
+
const step = verticalStep(event.key);
|
|
4040
|
+
if (step && items.length > 0) {
|
|
4041
|
+
event.preventDefault();
|
|
4042
|
+
highlightIndex((highlightedIndex + step + items.length) % items.length);
|
|
4043
|
+
} else if (event.key === "Enter" && highlighted && !event.nativeEvent.isComposing) {
|
|
4044
|
+
event.preventDefault();
|
|
4045
|
+
open(highlighted.url, {
|
|
4046
|
+
newTab: hasCommandKey(event)
|
|
4047
|
+
});
|
|
4048
|
+
} else if (isCopyShortcut(event) && highlighted && !hasSelectedText(event.currentTarget)) {
|
|
4049
|
+
event.preventDefault();
|
|
4050
|
+
copy(highlighted.url);
|
|
4051
|
+
}
|
|
4052
|
+
};
|
|
4053
|
+
return {
|
|
4054
|
+
copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.url),
|
|
4055
|
+
handleKeyDown,
|
|
4056
|
+
highlightIndex,
|
|
4057
|
+
highlighted,
|
|
4058
|
+
highlightedIndex: highlighted ? highlightedIndex : null
|
|
4059
|
+
};
|
|
4060
|
+
}
|
|
4061
|
+
|
|
4062
|
+
function groupSections(groups = []) {
|
|
4063
|
+
return groups.map(({
|
|
4064
|
+
results,
|
|
4065
|
+
type
|
|
4066
|
+
}) => ({
|
|
4067
|
+
heading: type,
|
|
4068
|
+
items: results.map(resultItem),
|
|
4069
|
+
key: type
|
|
4070
|
+
}));
|
|
4071
|
+
}
|
|
4072
|
+
function resultItem({
|
|
4073
|
+
attributes,
|
|
4074
|
+
id
|
|
4075
|
+
}) {
|
|
4076
|
+
return {
|
|
4077
|
+
key: id,
|
|
4078
|
+
subtitle: summarize(attributes.metadata),
|
|
4079
|
+
title: attributes.title,
|
|
4080
|
+
url: attributes.url
|
|
4081
|
+
};
|
|
4082
|
+
}
|
|
4083
|
+
function visitItem(visit) {
|
|
4084
|
+
return {
|
|
4085
|
+
key: entryKey(visit.entry),
|
|
4086
|
+
...describeVisit(visit)
|
|
4087
|
+
};
|
|
4088
|
+
}
|
|
4089
|
+
function summarize(metadata) {
|
|
4090
|
+
return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
|
|
4091
|
+
}
|
|
4092
|
+
// Unlike search results, recents come from every product, so each one names
|
|
4093
|
+
// its product and type.
|
|
4094
|
+
function describeVisit(visit) {
|
|
4095
|
+
var _a, _b;
|
|
4096
|
+
const subtitle = (...parts) => parts.filter(Boolean).join(" · ");
|
|
4097
|
+
if (visit.status === "page") {
|
|
4098
|
+
const {
|
|
4099
|
+
app,
|
|
4100
|
+
metadata,
|
|
4101
|
+
title
|
|
4102
|
+
} = visit.entry;
|
|
4103
|
+
return {
|
|
4104
|
+
subtitle: subtitle(productName(app), (_a = metadata === null || metadata === void 0 ? void 0 : metadata.type) !== null && _a !== void 0 ? _a : "", (_b = metadata === null || metadata === void 0 ? void 0 : metadata.details) !== null && _b !== void 0 ? _b : ""),
|
|
4105
|
+
title,
|
|
4106
|
+
url: visit.url
|
|
4107
|
+
};
|
|
4108
|
+
} else {
|
|
4109
|
+
const {
|
|
4110
|
+
metadata,
|
|
4111
|
+
product,
|
|
4112
|
+
title,
|
|
4113
|
+
type
|
|
4114
|
+
} = visit.result.attributes;
|
|
4115
|
+
return {
|
|
4116
|
+
subtitle: subtitle(productName(product), type, summarize(metadata)),
|
|
4117
|
+
title,
|
|
4118
|
+
url: visit.url
|
|
4119
|
+
};
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
// check-ins → Check-Ins
|
|
4123
|
+
function productName(slug) {
|
|
4124
|
+
return slug.split("-").map(capitalize).join("-");
|
|
4125
|
+
}
|
|
4126
|
+
function capitalize(word) {
|
|
4127
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
4128
|
+
}
|
|
4129
|
+
|
|
4130
|
+
const STORAGE_KEY = "topbar.product_search.last_product";
|
|
4131
|
+
function readLastProduct() {
|
|
4132
|
+
try {
|
|
4133
|
+
return localStorage.getItem(STORAGE_KEY);
|
|
4134
|
+
} catch {
|
|
4135
|
+
return null;
|
|
4136
|
+
}
|
|
4137
|
+
}
|
|
4138
|
+
function saveLastProduct(slug) {
|
|
4139
|
+
try {
|
|
4140
|
+
localStorage.setItem(STORAGE_KEY, slug);
|
|
4141
|
+
} catch {
|
|
4142
|
+
// Storage can be full or blocked; search still works without it.
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
function initialProduct(products) {
|
|
4146
|
+
var _a, _b, _c;
|
|
4147
|
+
const lastProduct = readLastProduct();
|
|
4148
|
+
const bySlug = slug => products.find(product => product.slug === slug);
|
|
4149
|
+
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];
|
|
4150
|
+
}
|
|
4151
|
+
|
|
4152
|
+
function KeyboardHints({
|
|
4153
|
+
copied
|
|
4154
|
+
}) {
|
|
4155
|
+
const command = commandKeyLabel();
|
|
4156
|
+
return /*#__PURE__*/React.createElement("p", null, "\u2191\u2193 Select \u00B7 \u21B5 Open \u00B7 ", command, "\u21B5 Open in new tab \u00B7 ", command, "C", " ", /*#__PURE__*/React.createElement("span", {
|
|
4157
|
+
role: "status"
|
|
4158
|
+
}, copied ? "Copied!" : "Copy link"), " \u00B7 ", command, "K Open search");
|
|
4159
|
+
}
|
|
4160
|
+
|
|
4161
|
+
var styles$d = {"tab":"tab-dz8ki"};
|
|
4162
|
+
|
|
4163
|
+
function ProductTabs({
|
|
4164
|
+
children,
|
|
4165
|
+
onKeyDown,
|
|
4166
|
+
onSelect,
|
|
4167
|
+
products,
|
|
4168
|
+
selected
|
|
4169
|
+
}) {
|
|
4170
|
+
const id = useInstanceId("product-tabs");
|
|
4171
|
+
const tabId = slug => `${id}-tab-${slug}`;
|
|
4172
|
+
const tabRefs = React.useRef(new Map());
|
|
4173
|
+
const handleKeyDown = event => {
|
|
4174
|
+
var _a;
|
|
4175
|
+
const offset = horizontalStep(event.key);
|
|
4176
|
+
if (offset) {
|
|
4177
|
+
event.preventDefault();
|
|
4178
|
+
const index = products.indexOf(selected);
|
|
4179
|
+
const next = products[(index + offset + products.length) % products.length];
|
|
4180
|
+
onSelect(next.slug);
|
|
4181
|
+
(_a = tabRefs.current.get(next.slug)) === null || _a === void 0 ? void 0 : _a.focus();
|
|
4182
|
+
} else {
|
|
4183
|
+
onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
|
|
4184
|
+
}
|
|
4185
|
+
};
|
|
4186
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
|
|
4187
|
+
"aria-label": "Product",
|
|
4188
|
+
onKeyDown: handleKeyDown,
|
|
4189
|
+
role: "tablist"
|
|
4190
|
+
}, products.map(product => (/*#__PURE__*/React.createElement("button", {
|
|
4191
|
+
"aria-controls": `${id}-panel`,
|
|
4192
|
+
"aria-selected": product === selected,
|
|
4193
|
+
className: styles$d.tab,
|
|
4194
|
+
id: tabId(product.slug),
|
|
4195
|
+
key: product.slug,
|
|
4196
|
+
onClick: () => onSelect(product.slug),
|
|
4197
|
+
ref: element => {
|
|
4198
|
+
if (element) {
|
|
4199
|
+
tabRefs.current.set(product.slug, element);
|
|
4200
|
+
} else {
|
|
4201
|
+
tabRefs.current.delete(product.slug);
|
|
4202
|
+
}
|
|
4203
|
+
},
|
|
4204
|
+
role: "tab",
|
|
4205
|
+
tabIndex: product === selected ? 0 : -1,
|
|
4206
|
+
type: "button"
|
|
4207
|
+
}, product.appName)))), /*#__PURE__*/React.createElement("div", {
|
|
4208
|
+
"aria-labelledby": tabId(selected.slug),
|
|
4209
|
+
id: `${id}-panel`,
|
|
4210
|
+
role: "tabpanel"
|
|
4211
|
+
}, children));
|
|
4212
|
+
}
|
|
4213
|
+
|
|
4214
|
+
function EndOfResultsObserver({
|
|
4215
|
+
onVisible
|
|
4216
|
+
}) {
|
|
4217
|
+
const ref = React.useRef(null);
|
|
4218
|
+
React.useEffect(() => {
|
|
4219
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
4220
|
+
if (entry.isIntersecting) onVisible();
|
|
4221
|
+
});
|
|
4222
|
+
if (ref.current) observer.observe(ref.current);
|
|
4223
|
+
return () => observer.disconnect();
|
|
4224
|
+
}, [onVisible]);
|
|
4225
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
4226
|
+
ref: ref
|
|
4227
|
+
});
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
const resultId = (listboxId, index) => `${listboxId}-result-${index}`;
|
|
4231
|
+
|
|
4232
|
+
var styles$c = {"result":"result-Thq2C"};
|
|
4233
|
+
|
|
4234
|
+
function SearchResult({
|
|
4235
|
+
highlighted,
|
|
4236
|
+
id,
|
|
4237
|
+
onHighlight,
|
|
4238
|
+
subtitle,
|
|
4239
|
+
title,
|
|
4240
|
+
url
|
|
4241
|
+
}) {
|
|
4242
|
+
const ref = React.useRef(null);
|
|
4243
|
+
React.useEffect(() => {
|
|
4244
|
+
var _a;
|
|
4245
|
+
if (highlighted) (_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({
|
|
4246
|
+
block: "nearest"
|
|
4247
|
+
});
|
|
4248
|
+
}, [highlighted]);
|
|
4249
|
+
// Focus stays in the search input, which points at the highlighted result
|
|
4250
|
+
// with aria-activedescendant, so results are kept out of the tab order.
|
|
4251
|
+
// Mouse move rather than enter: arrowing scrolls results under a resting
|
|
4252
|
+
// pointer, and that shouldn't steal the highlight.
|
|
4253
|
+
return /*#__PURE__*/React.createElement("a", {
|
|
4254
|
+
"aria-selected": highlighted,
|
|
4255
|
+
className: styles$c.result,
|
|
4256
|
+
href: url,
|
|
4257
|
+
id: id,
|
|
4258
|
+
onMouseMove: onHighlight,
|
|
4259
|
+
ref: ref,
|
|
4260
|
+
role: "option",
|
|
4261
|
+
tabIndex: -1
|
|
4262
|
+
}, /*#__PURE__*/React.createElement("div", null, title), /*#__PURE__*/React.createElement("div", null, subtitle));
|
|
4263
|
+
}
|
|
4264
|
+
|
|
4265
|
+
// Sections with a heading render as labelled groups; ones without render
|
|
4266
|
+
// their items straight into the listbox. Indexes run across every section.
|
|
4267
|
+
function ResultListbox({
|
|
4268
|
+
busy,
|
|
4269
|
+
highlightedIndex,
|
|
4270
|
+
id,
|
|
4271
|
+
label,
|
|
4272
|
+
labelledBy,
|
|
4273
|
+
onHighlight,
|
|
4274
|
+
sections
|
|
4275
|
+
}) {
|
|
4276
|
+
let index = 0;
|
|
4277
|
+
const renderItem = item => {
|
|
4278
|
+
const itemIndex = index++;
|
|
4279
|
+
return /*#__PURE__*/React.createElement(SearchResult, {
|
|
4280
|
+
highlighted: itemIndex === highlightedIndex,
|
|
4281
|
+
id: resultId(id, itemIndex),
|
|
4282
|
+
key: item.key,
|
|
4283
|
+
onHighlight: () => onHighlight(itemIndex),
|
|
4284
|
+
subtitle: item.subtitle,
|
|
4285
|
+
title: item.title,
|
|
4286
|
+
url: item.url
|
|
4287
|
+
});
|
|
4288
|
+
};
|
|
4289
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
4290
|
+
"aria-busy": busy,
|
|
4291
|
+
"aria-label": label,
|
|
4292
|
+
"aria-labelledby": labelledBy,
|
|
4293
|
+
id: id,
|
|
4294
|
+
role: "listbox"
|
|
4295
|
+
}, sections.map((section, sectionIndex) => {
|
|
4296
|
+
if (section.heading === undefined) {
|
|
4297
|
+
return /*#__PURE__*/React.createElement(React.Fragment, {
|
|
4298
|
+
key: section.key
|
|
4299
|
+
}, section.items.map(renderItem));
|
|
4300
|
+
}
|
|
4301
|
+
const headingId = `${id}-group-${sectionIndex}`;
|
|
4302
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
4303
|
+
"aria-labelledby": headingId,
|
|
4304
|
+
key: section.key,
|
|
4305
|
+
role: "group"
|
|
4306
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
4307
|
+
id: headingId
|
|
4308
|
+
}, section.heading), section.items.map(renderItem));
|
|
4309
|
+
}));
|
|
4310
|
+
}
|
|
4311
|
+
|
|
4312
|
+
function RecentlyVisited({
|
|
4313
|
+
count,
|
|
4314
|
+
hasMore,
|
|
4315
|
+
items,
|
|
4316
|
+
loading,
|
|
4317
|
+
showMore,
|
|
4318
|
+
...navigation
|
|
4319
|
+
}) {
|
|
4320
|
+
const headingId = `${navigation.id}-heading`;
|
|
4321
|
+
if (items.length === 0 && !hasMore && !loading) {
|
|
4322
|
+
return /*#__PURE__*/React.createElement("p", null, "Pages you visit will show up here.");
|
|
4323
|
+
} else {
|
|
4324
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
|
|
4325
|
+
id: headingId
|
|
4326
|
+
}, "Recently visited"), /*#__PURE__*/React.createElement(ResultListbox, {
|
|
4327
|
+
labelledBy: headingId,
|
|
4328
|
+
sections: [{
|
|
4329
|
+
items,
|
|
4330
|
+
key: "recents"
|
|
4331
|
+
}],
|
|
4332
|
+
...navigation
|
|
4333
|
+
}), hasMore && /*#__PURE__*/React.createElement(EndOfResultsObserver, {
|
|
4334
|
+
key: count,
|
|
4335
|
+
onVisible: showMore
|
|
4336
|
+
}));
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
|
|
4340
|
+
// A combobox: focus stays here while aria-activedescendant points screen
|
|
4341
|
+
// readers at the highlighted result in the listbox it controls.
|
|
4342
|
+
function SearchInput({
|
|
4343
|
+
expanded,
|
|
4344
|
+
highlightedIndex,
|
|
4345
|
+
inputRef,
|
|
4346
|
+
listboxId,
|
|
4347
|
+
onChange,
|
|
4348
|
+
onFocus,
|
|
4349
|
+
onKeyDown,
|
|
4350
|
+
value
|
|
4351
|
+
}) {
|
|
4352
|
+
return /*#__PURE__*/React.createElement("input", {
|
|
4353
|
+
"aria-activedescendant": highlightedIndex === null ? undefined : resultId(listboxId, highlightedIndex),
|
|
4354
|
+
"aria-autocomplete": "list",
|
|
4355
|
+
"aria-controls": expanded ? listboxId : undefined,
|
|
4356
|
+
"aria-expanded": expanded,
|
|
4357
|
+
"aria-label": "Search",
|
|
4358
|
+
autoFocus: true,
|
|
4359
|
+
onChange: event => onChange(event.target.value),
|
|
4360
|
+
onFocus: onFocus,
|
|
4361
|
+
onKeyDown: onKeyDown,
|
|
4362
|
+
placeholder: "Search",
|
|
4363
|
+
ref: inputRef,
|
|
4364
|
+
role: "combobox",
|
|
4365
|
+
type: "search",
|
|
4366
|
+
value: value
|
|
4367
|
+
});
|
|
4368
|
+
}
|
|
4369
|
+
|
|
4370
|
+
var styles$b = {"visuallyHidden":"visuallyHidden-S9VG5"};
|
|
4371
|
+
|
|
4372
|
+
function SearchResults({
|
|
4373
|
+
label,
|
|
4374
|
+
sections,
|
|
4375
|
+
status,
|
|
4376
|
+
term,
|
|
4377
|
+
...navigation
|
|
4378
|
+
}) {
|
|
4379
|
+
switch (status) {
|
|
4380
|
+
case "idle":
|
|
4381
|
+
return null;
|
|
4382
|
+
case "loading":
|
|
4383
|
+
if (sections.length > 0) {
|
|
4384
|
+
return /*#__PURE__*/React.createElement(ResultListbox, {
|
|
4385
|
+
busy: true,
|
|
4386
|
+
label: label,
|
|
4387
|
+
sections: sections,
|
|
4388
|
+
...navigation
|
|
4389
|
+
});
|
|
4390
|
+
}
|
|
4391
|
+
return /*#__PURE__*/React.createElement("p", {
|
|
4392
|
+
role: "status"
|
|
4393
|
+
}, "Searching\u2026");
|
|
4394
|
+
case "error":
|
|
4395
|
+
return /*#__PURE__*/React.createElement("p", {
|
|
4396
|
+
role: "alert"
|
|
4397
|
+
}, "Search isn\u2019t working right now. Try again.");
|
|
4398
|
+
case "done":
|
|
4399
|
+
if (sections.length === 0) {
|
|
4400
|
+
return /*#__PURE__*/React.createElement("p", {
|
|
4401
|
+
role: "status"
|
|
4402
|
+
}, "No results for \u201C", term.trim(), "\u201D");
|
|
4403
|
+
}
|
|
4404
|
+
return /*#__PURE__*/React.createElement(ResultListbox, {
|
|
4405
|
+
busy: false,
|
|
4406
|
+
label: label,
|
|
4407
|
+
sections: sections,
|
|
4408
|
+
...navigation
|
|
4409
|
+
});
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4412
|
+
|
|
4413
|
+
// Keys the recents list for navigation, so the highlight
|
|
4414
|
+
// stays put as more recents load and as their lookups answer.
|
|
4415
|
+
const RECENTS = "recents";
|
|
4416
|
+
function SearchPanel({
|
|
4417
|
+
env,
|
|
4418
|
+
products
|
|
4419
|
+
}) {
|
|
4420
|
+
var _a, _b;
|
|
4421
|
+
const listboxId = useInstanceId("product-search-results");
|
|
4422
|
+
const [term, setTerm] = React.useState("");
|
|
4423
|
+
const [selectedSlug, setSelectedSlug] = React.useState(() => initialProduct(products).slug);
|
|
4424
|
+
const product = (_a = products.find(({
|
|
4425
|
+
slug
|
|
4426
|
+
}) => slug === selectedSlug)) !== null && _a !== void 0 ? _a : products[0];
|
|
4427
|
+
const hasTerm = term.trim() !== "";
|
|
4428
|
+
const search = useProductSearch(product.slug, term);
|
|
4429
|
+
const groups = "groups" in search ? search.groups : undefined;
|
|
4430
|
+
const sections = React.useMemo(() => groupSections(groups), [groups]);
|
|
4431
|
+
const {
|
|
4432
|
+
visits,
|
|
4433
|
+
...recents
|
|
4434
|
+
} = useRecentVisits(env);
|
|
4435
|
+
const recentItems = visits.map(visitItem);
|
|
4436
|
+
const items = hasTerm ? sections.flatMap(section => section.items) : recentItems;
|
|
4437
|
+
const navigation = useResultNavigation(items, hasTerm ? sections : RECENTS);
|
|
4438
|
+
const {
|
|
4439
|
+
highlightedIndex
|
|
4440
|
+
} = navigation;
|
|
4441
|
+
const inputRef = React.useRef(null);
|
|
4442
|
+
const [announceHighlight, setAnnounceHighlight] = React.useState(false);
|
|
4443
|
+
// The tabs keep focus while arrowing through results, but only the focused
|
|
4444
|
+
// input can point screen readers at the highlight, so from the tabs it's
|
|
4445
|
+
// announced instead. Typing goes back to the input, keystroke and all.
|
|
4446
|
+
const handleTabsKeyDown = event => {
|
|
4447
|
+
var _a;
|
|
4448
|
+
if (isTypingKey(event)) {
|
|
4449
|
+
(_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
4450
|
+
} else if (verticalStep(event.key)) {
|
|
4451
|
+
setAnnounceHighlight(true);
|
|
4452
|
+
navigation.handleKeyDown(event);
|
|
4453
|
+
} else {
|
|
4454
|
+
navigation.handleKeyDown(event);
|
|
4455
|
+
}
|
|
4456
|
+
};
|
|
4457
|
+
React.useEffect(() => {
|
|
4458
|
+
if (hasTerm) saveLastProduct(product.slug);
|
|
4459
|
+
}, [hasTerm, product.slug]);
|
|
4460
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SearchInput, {
|
|
4461
|
+
expanded: items.length > 0,
|
|
4462
|
+
highlightedIndex: highlightedIndex,
|
|
4463
|
+
inputRef: inputRef,
|
|
4464
|
+
listboxId: listboxId,
|
|
4465
|
+
onChange: setTerm,
|
|
4466
|
+
onFocus: () => setAnnounceHighlight(false),
|
|
4467
|
+
onKeyDown: navigation.handleKeyDown,
|
|
4468
|
+
value: term
|
|
4469
|
+
}), /*#__PURE__*/React.createElement(ProductTabs, {
|
|
4470
|
+
onKeyDown: handleTabsKeyDown,
|
|
4471
|
+
onSelect: setSelectedSlug,
|
|
4472
|
+
products: products,
|
|
4473
|
+
selected: product
|
|
4474
|
+
}, hasTerm ? (/*#__PURE__*/React.createElement(SearchResults, {
|
|
4475
|
+
highlightedIndex: highlightedIndex,
|
|
4476
|
+
id: listboxId,
|
|
4477
|
+
label: `${product.appName} results`,
|
|
4478
|
+
onHighlight: navigation.highlightIndex,
|
|
4479
|
+
sections: sections,
|
|
4480
|
+
status: search.status,
|
|
4481
|
+
term: term
|
|
4482
|
+
})) : (/*#__PURE__*/React.createElement(RecentlyVisited, {
|
|
4483
|
+
highlightedIndex: highlightedIndex,
|
|
4484
|
+
id: listboxId,
|
|
4485
|
+
items: recentItems,
|
|
4486
|
+
onHighlight: navigation.highlightIndex,
|
|
4487
|
+
...recents
|
|
4488
|
+
}))), /*#__PURE__*/React.createElement("div", {
|
|
4489
|
+
"aria-live": "polite",
|
|
4490
|
+
className: styles$b.visuallyHidden
|
|
4491
|
+
}, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.title : null), /*#__PURE__*/React.createElement(KeyboardHints, {
|
|
4492
|
+
copied: navigation.copied
|
|
4493
|
+
}));
|
|
3988
4494
|
}
|
|
3989
|
-
|
|
3990
|
-
|
|
4495
|
+
|
|
4496
|
+
// Products open modals from many libraries, not all of which mark themselves
|
|
4497
|
+
// with roles, so a modal is recognized by what it does: cover the page.
|
|
4498
|
+
function isModalOpen() {
|
|
4499
|
+
const {
|
|
4500
|
+
clientHeight,
|
|
4501
|
+
clientWidth
|
|
4502
|
+
} = document.documentElement;
|
|
4503
|
+
const element = document.elementFromPoint(clientWidth / 2, clientHeight / 2);
|
|
4504
|
+
return element !== null && (isInNativeDialog(element) || hasBackdrop(element, clientWidth, clientHeight));
|
|
3991
4505
|
}
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
return `page:${entry.product}:${entry.path}`;
|
|
3997
|
-
}
|
|
4506
|
+
// A native modal's backdrop is drawn by the browser rather than being an
|
|
4507
|
+
// element; hit-testing it lands on the dialog itself.
|
|
4508
|
+
function isInNativeDialog(element) {
|
|
4509
|
+
return element.closest("dialog[open]") !== null;
|
|
3998
4510
|
}
|
|
3999
|
-
function
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
return typeof value.pcrl === "string" && isPcrl(value.pcrl);
|
|
4004
|
-
} else if (value.type === "page") {
|
|
4005
|
-
return isNonBlankString(value.product) && isNonBlankString(value.title) && isNonBlankString(value.path) && typeof value.details === "string";
|
|
4006
|
-
} else {
|
|
4007
|
-
return false;
|
|
4511
|
+
function hasBackdrop(element, width, height) {
|
|
4512
|
+
let current = element;
|
|
4513
|
+
while (current && !isBackdrop(current, width, height)) {
|
|
4514
|
+
current = current.parentElement;
|
|
4008
4515
|
}
|
|
4516
|
+
return current !== null;
|
|
4009
4517
|
}
|
|
4010
|
-
//
|
|
4011
|
-
|
|
4012
|
-
|
|
4518
|
+
// Some backdrops start below the topbar rather than at the top of the screen,
|
|
4519
|
+
// so the layer only has to span the width and reach from the center to the
|
|
4520
|
+
// bottom. Its own box has to reach the center: a bar along the bottom can
|
|
4521
|
+
// hold a panel that pops up over the center, like Services' media player.
|
|
4522
|
+
// react-aria leaves a closed modal's backdrop up while it fades out, search's
|
|
4523
|
+
// own included, so one that's on its way out doesn't count.
|
|
4524
|
+
function isBackdrop(element, width, height) {
|
|
4525
|
+
const rect = element.getBoundingClientRect();
|
|
4526
|
+
return !element.matches("[data-exiting]") && getComputedStyle(element).position === "fixed" && rect.width >= width - SCROLLBAR_SLACK && rect.top <= height / 2 && rect.bottom >= height - ROUNDING_SLACK;
|
|
4013
4527
|
}
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4528
|
+
// react-aria keeps a classic scrollbar's gutter reserved while its modals lock
|
|
4529
|
+
// scrolling, so their backdrops stop short of the right edge by its width.
|
|
4530
|
+
const SCROLLBAR_SLACK = 24;
|
|
4531
|
+
// Rects are fractional at non-100% zoom, but clientWidth and clientHeight are
|
|
4532
|
+
// rounded to whole pixels.
|
|
4533
|
+
const ROUNDING_SLACK = 1;
|
|
4534
|
+
|
|
4535
|
+
function useSearchShortcut(enabled, open, setOpen) {
|
|
4536
|
+
React.useEffect(() => {
|
|
4537
|
+
if (!enabled) return undefined;
|
|
4538
|
+
// Rich text editors like Trix bind ⌘K to "insert link"; capturing at the
|
|
4539
|
+
// window and stopping propagation keeps them from ever seeing it. Under a
|
|
4540
|
+
// product's modal, search stays shut rather than stacking a second modal,
|
|
4541
|
+
// and the keystroke goes on to the product.
|
|
4542
|
+
const handleKeyDown = event => {
|
|
4543
|
+
if (open && isSearchShortcut(event)) {
|
|
4544
|
+
event.preventDefault();
|
|
4545
|
+
event.stopPropagation();
|
|
4546
|
+
setOpen(false);
|
|
4547
|
+
} else if (!open && (isSearchShortcut(event) || isLegacySearchShortcut(event)) && !isModalOpen()) {
|
|
4548
|
+
event.preventDefault();
|
|
4549
|
+
event.stopPropagation();
|
|
4550
|
+
setOpen(true);
|
|
4551
|
+
}
|
|
4022
4552
|
};
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
|
|
4031
|
-
} catch {
|
|
4032
|
-
parsed = null;
|
|
4033
|
-
}
|
|
4034
|
-
if (isRecord(parsed) && parsed.v === FORMAT_VERSION && parsed.person === personId && Array.isArray(parsed.entries)) {
|
|
4035
|
-
return parsed.entries.filter(isEntry);
|
|
4036
|
-
} else {
|
|
4037
|
-
return [];
|
|
4038
|
-
}
|
|
4039
|
-
}
|
|
4040
|
-
function addVisit(entries, visit) {
|
|
4041
|
-
const key = entryKey(visit);
|
|
4042
|
-
return [clampEntry(visit), ...entries.filter(entry => entryKey(entry) !== key)].slice(0, MAX_ENTRIES);
|
|
4553
|
+
window.addEventListener("keydown", handleKeyDown, {
|
|
4554
|
+
capture: true
|
|
4555
|
+
});
|
|
4556
|
+
return () => window.removeEventListener("keydown", handleKeyDown, {
|
|
4557
|
+
capture: true
|
|
4558
|
+
});
|
|
4559
|
+
}, [enabled, open, setOpen]);
|
|
4043
4560
|
}
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4561
|
+
|
|
4562
|
+
const SEARCHABLE_PRODUCTS = [{
|
|
4563
|
+
appName: "Calendar",
|
|
4564
|
+
slug: "calendar"
|
|
4565
|
+
}, {
|
|
4566
|
+
appName: "Check-Ins",
|
|
4567
|
+
slug: "check-ins"
|
|
4568
|
+
}, {
|
|
4569
|
+
appName: "Giving",
|
|
4570
|
+
slug: "giving"
|
|
4571
|
+
}, {
|
|
4572
|
+
appName: "Groups",
|
|
4573
|
+
slug: "groups"
|
|
4574
|
+
}, {
|
|
4575
|
+
appName: "People",
|
|
4576
|
+
slug: "people"
|
|
4577
|
+
}, {
|
|
4578
|
+
appName: "Publishing",
|
|
4579
|
+
slug: "publishing"
|
|
4580
|
+
}, {
|
|
4581
|
+
appName: "Registrations",
|
|
4582
|
+
slug: "registrations"
|
|
4583
|
+
}, {
|
|
4584
|
+
appName: "Services",
|
|
4585
|
+
slug: "services"
|
|
4586
|
+
}];
|
|
4587
|
+
|
|
4588
|
+
const PERMISSION_CHECKS = {
|
|
4589
|
+
"check-ins": () => fetchMe("check-ins", "permission").then(({
|
|
4590
|
+
permission
|
|
4591
|
+
}) => permission !== null),
|
|
4592
|
+
giving: personId => {
|
|
4593
|
+
if (personId === undefined) {
|
|
4594
|
+
return Promise.reject(new Error("No person to check Giving access for"));
|
|
4595
|
+
} else {
|
|
4596
|
+
return fetchAttributes(`giving/v2/person_permissions/${personId}`, {
|
|
4597
|
+
"fields[PersonPermissions]": "can_search"
|
|
4598
|
+
}).then(({
|
|
4599
|
+
can_search
|
|
4600
|
+
}) => can_search);
|
|
4601
|
+
}
|
|
4602
|
+
},
|
|
4603
|
+
people: () => fetchMe("people", "people_permissions,site_administrator").then(({
|
|
4604
|
+
people_permissions,
|
|
4605
|
+
site_administrator
|
|
4606
|
+
}) => site_administrator || people_permissions !== null),
|
|
4607
|
+
registrations: () => fetchMe("registrations", "can_search").then(({
|
|
4608
|
+
can_search
|
|
4609
|
+
}) => can_search),
|
|
4610
|
+
services: () => fetchMe("services", "can_search").then(({
|
|
4611
|
+
can_search
|
|
4612
|
+
}) => can_search)
|
|
4613
|
+
};
|
|
4614
|
+
function fetchMe(product, fields) {
|
|
4615
|
+
return fetchAttributes(`${product}/v2/me`, {
|
|
4616
|
+
"fields[Person]": fields
|
|
4049
4617
|
});
|
|
4050
4618
|
}
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4619
|
+
function fetchAttributes(path, params) {
|
|
4620
|
+
const url = `${window.location.origin}/~api/${path}?${new URLSearchParams(params)}`;
|
|
4621
|
+
return apiRequest(defaultFetch, url, {
|
|
4622
|
+
headers: apiVersionHeaders(url)
|
|
4623
|
+
}).then(({
|
|
4624
|
+
json,
|
|
4625
|
+
ok
|
|
4626
|
+
}) => {
|
|
4627
|
+
if (!ok || json === null) throw new Error("Permission check failed");
|
|
4628
|
+
return json.data.attributes;
|
|
4629
|
+
});
|
|
4058
4630
|
}
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4631
|
+
// Permissions rarely change mid-session, so a successful check is kept until
|
|
4632
|
+
// the page reloads. A failed one is dropped so a remount tries again.
|
|
4633
|
+
const permissionCache = new Map();
|
|
4634
|
+
function canSearch(slug, personId) {
|
|
4635
|
+
const check = PERMISSION_CHECKS[slug];
|
|
4636
|
+
const cached = permissionCache.get(slug);
|
|
4637
|
+
if (!check) {
|
|
4638
|
+
return Promise.resolve(true);
|
|
4639
|
+
} else if (cached) {
|
|
4640
|
+
return cached;
|
|
4641
|
+
} else {
|
|
4642
|
+
const promise = check(personId).catch(() => {
|
|
4643
|
+
permissionCache.delete(slug);
|
|
4644
|
+
return false;
|
|
4645
|
+
});
|
|
4646
|
+
permissionCache.set(slug, promise);
|
|
4647
|
+
return promise;
|
|
4066
4648
|
}
|
|
4067
4649
|
}
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
})
|
|
4075
|
-
const
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4650
|
+
|
|
4651
|
+
function useSearchableProducts(apps) {
|
|
4652
|
+
// Keyed by name because providers hand over a fresh `apps` array on renders
|
|
4653
|
+
// where nothing changed.
|
|
4654
|
+
const accessibleKey = inSwitcherOrder(apps).map(({
|
|
4655
|
+
slug
|
|
4656
|
+
}) => slug).join(",");
|
|
4657
|
+
const personId = useCurrentPersonId();
|
|
4658
|
+
const [settled, setSettled] = React.useState();
|
|
4659
|
+
React.useEffect(() => {
|
|
4660
|
+
let current = true;
|
|
4661
|
+
const accessible = accessibleKey.split(",").flatMap(slug => SEARCHABLE_PRODUCTS.filter(product => product.slug === slug));
|
|
4662
|
+
Promise.all(accessible.map(({
|
|
4663
|
+
slug
|
|
4664
|
+
}) => canSearch(slug, personId))).then(allowed => {
|
|
4665
|
+
if (current) {
|
|
4666
|
+
setSettled({
|
|
4667
|
+
accessibleKey,
|
|
4668
|
+
personId,
|
|
4669
|
+
products: accessible.filter((_, index) => allowed[index])
|
|
4670
|
+
});
|
|
4671
|
+
}
|
|
4672
|
+
});
|
|
4673
|
+
return () => {
|
|
4674
|
+
current = false;
|
|
4080
4675
|
};
|
|
4081
|
-
}
|
|
4676
|
+
}, [accessibleKey, personId]);
|
|
4677
|
+
if ((settled === null || settled === void 0 ? void 0 : settled.accessibleKey) === accessibleKey && settled.personId === personId) {
|
|
4082
4678
|
return {
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
product,
|
|
4086
|
-
title,
|
|
4087
|
-
type: "page"
|
|
4679
|
+
products: settled.products,
|
|
4680
|
+
status: "done"
|
|
4088
4681
|
};
|
|
4089
4682
|
} else {
|
|
4090
|
-
return
|
|
4683
|
+
return {
|
|
4684
|
+
status: "loading"
|
|
4685
|
+
};
|
|
4091
4686
|
}
|
|
4092
4687
|
}
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
// panel always shows what actually gets stored.
|
|
4096
|
-
function useCurrentVisit() {
|
|
4097
|
-
const {
|
|
4098
|
-
recently_visitable: title,
|
|
4099
|
-
resource
|
|
4100
|
-
} = usePcoMeta();
|
|
4101
|
-
const metadata = useRecentlyVisitableMetadata();
|
|
4102
|
-
return React.useMemo(() => visitFromPage({
|
|
4103
|
-
metadata,
|
|
4104
|
-
resource,
|
|
4105
|
-
title
|
|
4106
|
-
}), [metadata, resource, title]);
|
|
4107
|
-
}
|
|
4108
|
-
// Products hand the person's ID to AppsProvider, not to the topbar itself,
|
|
4109
|
-
// and several pass it as a number.
|
|
4110
|
-
function useCurrentPersonId() {
|
|
4111
|
-
const {
|
|
4112
|
-
userId
|
|
4113
|
-
} = useStore();
|
|
4114
|
-
return userId == null ? undefined : String(userId);
|
|
4688
|
+
function inSwitcherOrder(apps) {
|
|
4689
|
+
return groupApps(apps).restProducts.flatMap(app => SEARCHABLE_PRODUCTS.filter(product => product.appName === app.name));
|
|
4115
4690
|
}
|
|
4116
4691
|
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
function storageKey({
|
|
4121
|
-
env,
|
|
4122
|
-
personId
|
|
4692
|
+
function ProductSearch({
|
|
4693
|
+
apps,
|
|
4694
|
+
env
|
|
4123
4695
|
}) {
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
}
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
try {
|
|
4141
|
-
localStorage.setItem(storageKey(scope), serializeRecentlyVisited(addVisit(readRecentlyVisited(scope), visit), scope.personId));
|
|
4142
|
-
changes.setState({
|
|
4143
|
-
version: changes.getState().version + 1
|
|
4144
|
-
});
|
|
4145
|
-
} catch {
|
|
4146
|
-
// Storage can be full or blocked; recents are disposable.
|
|
4147
|
-
}
|
|
4148
|
-
}
|
|
4696
|
+
const searchable = useSearchableProducts(apps);
|
|
4697
|
+
const [open, setOpen] = React.useState(false);
|
|
4698
|
+
const available = searchable.status === "done" && searchable.products.length > 0;
|
|
4699
|
+
useSearchShortcut(available, open, setOpen);
|
|
4700
|
+
if (!available) return null;
|
|
4701
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SearchButton, {
|
|
4702
|
+
onClick: () => setOpen(true)
|
|
4703
|
+
}), /*#__PURE__*/React.createElement(tapestry.Modal, {
|
|
4704
|
+
"aria-label": "Search",
|
|
4705
|
+
dismissible: true,
|
|
4706
|
+
onClose: () => setOpen(false),
|
|
4707
|
+
open: open
|
|
4708
|
+
}, /*#__PURE__*/React.createElement(SearchPanel, {
|
|
4709
|
+
env: env,
|
|
4710
|
+
products: searchable.products
|
|
4711
|
+
})));
|
|
4149
4712
|
}
|
|
4150
4713
|
|
|
4151
4714
|
function useRecordRecentlyVisited({
|
|
@@ -4190,9 +4753,9 @@ function StyledRoot({
|
|
|
4190
4753
|
`), children);
|
|
4191
4754
|
}
|
|
4192
4755
|
|
|
4193
|
-
var styles$
|
|
4756
|
+
var styles$a = {"route":"route--8BWe","is-active":"is-active-FsU9Y","apps-routes-separator":"apps-routes-separator-Nlm-N"};
|
|
4194
4757
|
|
|
4195
|
-
var styles$
|
|
4758
|
+
var styles$9 = {"popup":"popup-mDNKn","trigger-wrap":"trigger-wrap-Jd-KR","trigger":"trigger-WgRT5","is-visible":"is-visible-GZC0-","trigger-orgname":"trigger-orgname-TDHCc"};
|
|
4196
4759
|
|
|
4197
4760
|
const UserMenu = ({
|
|
4198
4761
|
appName,
|
|
@@ -4211,7 +4774,7 @@ const UserMenu = ({
|
|
|
4211
4774
|
component: Outsider,
|
|
4212
4775
|
onOutsideClick: () => setUserMenuVisible(false),
|
|
4213
4776
|
cleanup: !userMenuVisible,
|
|
4214
|
-
className: styles$
|
|
4777
|
+
className: styles$9.popup,
|
|
4215
4778
|
isVisible: userMenuVisible
|
|
4216
4779
|
}, /*#__PURE__*/React.createElement("div", null, hasConnectedPeople ? (/*#__PURE__*/React.createElement("div", {
|
|
4217
4780
|
style: {
|
|
@@ -4355,11 +4918,11 @@ const UserMenuTrigger = ({
|
|
|
4355
4918
|
userMenuVisible
|
|
4356
4919
|
}) => {
|
|
4357
4920
|
return /*#__PURE__*/React.createElement("div", {
|
|
4358
|
-
className: styles$
|
|
4921
|
+
className: styles$9["trigger-wrap"],
|
|
4359
4922
|
"data-pendo": "topbar-org-switcher"
|
|
4360
4923
|
}, /*#__PURE__*/React.createElement(Hoverable, {
|
|
4361
|
-
className: classnames(styles$
|
|
4362
|
-
[styles$
|
|
4924
|
+
className: classnames(styles$9["trigger"], {
|
|
4925
|
+
[styles$9["is-visible"]]: userMenuVisible
|
|
4363
4926
|
}),
|
|
4364
4927
|
component: "button",
|
|
4365
4928
|
type: "button",
|
|
@@ -4371,7 +4934,7 @@ const UserMenuTrigger = ({
|
|
|
4371
4934
|
requestConnectedPeopleFetch();
|
|
4372
4935
|
}
|
|
4373
4936
|
}, showOrgName && (/*#__PURE__*/React.createElement("span", {
|
|
4374
|
-
className: styles$
|
|
4937
|
+
className: styles$9["trigger-orgname"]
|
|
4375
4938
|
}, orgName)), /*#__PURE__*/React.createElement(DisclosureChevronIcon, null)));
|
|
4376
4939
|
};
|
|
4377
4940
|
|
|
@@ -4431,13 +4994,14 @@ const NotSmallTopbar = ({
|
|
|
4431
4994
|
colors: props.colors,
|
|
4432
4995
|
requestAppsFetch: props.requestAppsFetch
|
|
4433
4996
|
})), /*#__PURE__*/React.createElement("div", {
|
|
4434
|
-
className: styles$
|
|
4997
|
+
className: styles$a["apps-routes-separator"]
|
|
4435
4998
|
}), routesVisible && props.routes, /*#__PURE__*/React.createElement("div", {
|
|
4436
4999
|
style: {
|
|
4437
5000
|
margin: "auto"
|
|
4438
5001
|
}
|
|
4439
5002
|
}), !notificationsEnabled && notifications(), productSearchEnabled ? (/*#__PURE__*/React.createElement(ProductSearch, {
|
|
4440
|
-
apps: props.apps
|
|
5003
|
+
apps: props.apps,
|
|
5004
|
+
env: props.env
|
|
4441
5005
|
})) : search({
|
|
4442
5006
|
hideRoutes: () => setRoutesVisible(false),
|
|
4443
5007
|
showRoutes: () => setRoutesVisible(true)
|
|
@@ -4471,12 +5035,12 @@ const NotSmallRoute = ({
|
|
|
4471
5035
|
...nativeProps
|
|
4472
5036
|
}) => {
|
|
4473
5037
|
return /*#__PURE__*/React.createElement("a", {
|
|
4474
|
-
className: classnames(styles$
|
|
5038
|
+
className: classnames(styles$a["route"], className, active && styles$a["is-active"]),
|
|
4475
5039
|
...nativeProps
|
|
4476
5040
|
});
|
|
4477
5041
|
};
|
|
4478
5042
|
|
|
4479
|
-
var styles$
|
|
5043
|
+
var styles$8 = {"avatar":"avatar-tW-wn"};
|
|
4480
5044
|
|
|
4481
5045
|
function Avatar({
|
|
4482
5046
|
env,
|
|
@@ -4486,7 +5050,7 @@ function Avatar({
|
|
|
4486
5050
|
}) {
|
|
4487
5051
|
const url = incomingUrl || `${pcoUrl$1(env)("people")}/static/no_photo_thumbnail_gray.svg`;
|
|
4488
5052
|
return /*#__PURE__*/React.createElement("div", {
|
|
4489
|
-
className: styles$
|
|
5053
|
+
className: styles$8.avatar,
|
|
4490
5054
|
style: {
|
|
4491
5055
|
backgroundImage: `url(${url})`,
|
|
4492
5056
|
...style
|
|
@@ -32245,7 +32809,7 @@ function requireReactDom () {
|
|
|
32245
32809
|
|
|
32246
32810
|
var reactDomExports = requireReactDom();
|
|
32247
32811
|
|
|
32248
|
-
var styles$
|
|
32812
|
+
var styles$7 = {"trigger":"trigger-Th-Up","layer":"layer-ck6BD","layer--visible":"layer--visible-4E2p1","panel":"panel-vobWE","header":"header-xndfZ","heading":"heading-uXyQf","close":"close-pK0Lk","list":"list-zWvrn","link":"link-Kf3qj","current":"current-g3Ey-","accounts-item":"accounts-item-eQqDD","accounts-link":"accounts-link-SeIbD"};
|
|
32249
32813
|
|
|
32250
32814
|
const AppsSwitcher = ({
|
|
32251
32815
|
appName,
|
|
@@ -32273,8 +32837,8 @@ const AppsSwitcher = ({
|
|
|
32273
32837
|
}, /*#__PURE__*/React.createElement("a", {
|
|
32274
32838
|
"aria-current": name === appName ? "page" : undefined,
|
|
32275
32839
|
"aria-label": name,
|
|
32276
|
-
className: classnames(styles$
|
|
32277
|
-
[styles$
|
|
32840
|
+
className: classnames(styles$7.link, {
|
|
32841
|
+
[styles$7.current]: name === appName
|
|
32278
32842
|
}),
|
|
32279
32843
|
"data-pendo": `app-switcher--change-app--${name}`,
|
|
32280
32844
|
"data-turbolinks": false,
|
|
@@ -32286,48 +32850,48 @@ const AppsSwitcher = ({
|
|
|
32286
32850
|
"aria-expanded": visible,
|
|
32287
32851
|
"aria-haspopup": "true",
|
|
32288
32852
|
"aria-label": `App switcher: ${appName}`,
|
|
32289
|
-
className: styles$
|
|
32853
|
+
className: styles$7.trigger,
|
|
32290
32854
|
onClick: onTriggerClick,
|
|
32291
32855
|
type: "button"
|
|
32292
32856
|
}, /*#__PURE__*/React.createElement(ColorAppIcon, {
|
|
32293
32857
|
appName: appName.replace(/[\s-]/, "")
|
|
32294
32858
|
})), /*#__PURE__*/reactDomExports.createPortal(/*#__PURE__*/React.createElement("div", {
|
|
32295
32859
|
ref: containerRef,
|
|
32296
|
-
className: classnames(styles$
|
|
32297
|
-
[styles$
|
|
32860
|
+
className: classnames(styles$7.layer, {
|
|
32861
|
+
[styles$7["layer--visible"]]: visible
|
|
32298
32862
|
}),
|
|
32299
32863
|
onClick: toggle,
|
|
32300
32864
|
onKeyDown: handleKeyDown
|
|
32301
32865
|
}, /*#__PURE__*/React.createElement("nav", {
|
|
32302
32866
|
"aria-label": "Switch app",
|
|
32303
32867
|
"aria-modal": visible,
|
|
32304
|
-
className: styles$
|
|
32868
|
+
className: styles$7.panel,
|
|
32305
32869
|
"data-pendo": "app-switcher",
|
|
32306
32870
|
onClick: event => event.stopPropagation(),
|
|
32307
32871
|
role: "dialog"
|
|
32308
32872
|
}, /*#__PURE__*/React.createElement("div", {
|
|
32309
|
-
className: styles$
|
|
32873
|
+
className: styles$7.header
|
|
32310
32874
|
}, /*#__PURE__*/React.createElement("span", {
|
|
32311
|
-
className: styles$
|
|
32875
|
+
className: styles$7.heading
|
|
32312
32876
|
}, "Switch to"), /*#__PURE__*/React.createElement("button", {
|
|
32313
32877
|
"aria-label": "Close",
|
|
32314
|
-
className: styles$
|
|
32878
|
+
className: styles$7.close,
|
|
32315
32879
|
onClick: toggle,
|
|
32316
32880
|
type: "button"
|
|
32317
32881
|
}, /*#__PURE__*/React.createElement(X, {
|
|
32318
32882
|
"aria-hidden": "true",
|
|
32319
32883
|
fill: "var(--t-icon-color-dim)"
|
|
32320
32884
|
}))), /*#__PURE__*/React.createElement("ul", {
|
|
32321
|
-
className: styles$
|
|
32885
|
+
className: styles$7.list,
|
|
32322
32886
|
role: "list"
|
|
32323
32887
|
}, homeProduct && productLink(homeProduct.name), restProducts.map(({
|
|
32324
32888
|
name
|
|
32325
32889
|
}) => productLink(name)), accountsProduct && (/*#__PURE__*/React.createElement("li", {
|
|
32326
|
-
className: styles$
|
|
32890
|
+
className: styles$7["accounts-item"]
|
|
32327
32891
|
}, /*#__PURE__*/React.createElement("a", {
|
|
32328
32892
|
"aria-current": appName === "Accounts" ? "page" : undefined,
|
|
32329
32893
|
"aria-label": "Account settings",
|
|
32330
|
-
className: classnames(styles$
|
|
32894
|
+
className: classnames(styles$7.link, styles$7["accounts-link"]),
|
|
32331
32895
|
"data-pendo": "app-switcher--change-app--Accounts",
|
|
32332
32896
|
"data-turbolinks": false,
|
|
32333
32897
|
href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name)
|
|
@@ -32350,14 +32914,14 @@ ClientStorage.defaultProps = {
|
|
|
32350
32914
|
method: "localStorage"
|
|
32351
32915
|
};
|
|
32352
32916
|
|
|
32353
|
-
var styles$
|
|
32917
|
+
var styles$6 = {"appmenu-button":"appmenu-button-b0UvT","appmenu-layer":"appmenu-layer-0IQpN","appmenu-container":"appmenu-container-gackd","appmenu-scroll":"appmenu-scroll-mPSx0","appmenu-list":"appmenu-list-h7clX","appmenu-list-a":"appmenu-list-a-rGIQN","appmenu-list-a--top":"appmenu-list-a--top-RT-2F","appmenu-list-a--bottom":"appmenu-list-a--bottom-YDeBC","appmenu-close-button":"appmenu-close-button-vv9bV","current-route":"current-route-wpJgz","is-active":"is-active-wy8Bg","routes":"routes-Nmt1S","route":"route-kkknQ","profile-button":"profile-button-a2WO2"};
|
|
32354
32918
|
|
|
32355
32919
|
function AccountSettingsLink({
|
|
32356
32920
|
env
|
|
32357
32921
|
}) {
|
|
32358
32922
|
const accountsName = "accounts";
|
|
32359
32923
|
return /*#__PURE__*/React.createElement("a", {
|
|
32360
|
-
className: classnames(styles$
|
|
32924
|
+
className: classnames(styles$6["appmenu-list-a"], styles$6["appmenu-list-a--bottom"]),
|
|
32361
32925
|
"data-pendo": `app-switcher--change-app--${accountsName}`,
|
|
32362
32926
|
"data-turbolinks": false,
|
|
32363
32927
|
href: pcoUrl$1(env)(accountsName) + getAppsSwitchProductPath(accountsName)
|
|
@@ -32380,7 +32944,7 @@ const CloseMenuButton = ({
|
|
|
32380
32944
|
}) => (/*#__PURE__*/React.createElement("button", {
|
|
32381
32945
|
"aria-label": "Close menu",
|
|
32382
32946
|
type: "button",
|
|
32383
|
-
className: styles$
|
|
32947
|
+
className: styles$6["appmenu-close-button"],
|
|
32384
32948
|
style: style
|
|
32385
32949
|
}, /*#__PURE__*/React.createElement(X, {
|
|
32386
32950
|
fill: color,
|
|
@@ -32441,7 +33005,7 @@ const LegacyAppsSwitcher = ({
|
|
|
32441
33005
|
"aria-haspopup": "true",
|
|
32442
33006
|
"aria-label": `App switcher: ${appName}`,
|
|
32443
33007
|
type: "button",
|
|
32444
|
-
className: styles$
|
|
33008
|
+
className: styles$6["appmenu-button"],
|
|
32445
33009
|
onClick: onTriggerClick
|
|
32446
33010
|
}, /*#__PURE__*/React.createElement(ColorAppIcon, {
|
|
32447
33011
|
appName: appName.replace(/[\s-]/, "")
|
|
@@ -32449,7 +33013,7 @@ const LegacyAppsSwitcher = ({
|
|
|
32449
33013
|
item: "Topbar:Small:Menus:Products:visibility",
|
|
32450
33014
|
render: () => (/*#__PURE__*/React.createElement("div", {
|
|
32451
33015
|
ref: appMenuRef,
|
|
32452
|
-
className: styles$
|
|
33016
|
+
className: styles$6["appmenu-layer"],
|
|
32453
33017
|
style: {
|
|
32454
33018
|
...(visible && {
|
|
32455
33019
|
display: "block"
|
|
@@ -32466,37 +33030,37 @@ const LegacyAppsSwitcher = ({
|
|
|
32466
33030
|
e.stopPropagation();
|
|
32467
33031
|
}
|
|
32468
33032
|
},
|
|
32469
|
-
className: styles$
|
|
33033
|
+
className: styles$6["appmenu-container"]
|
|
32470
33034
|
}, /*#__PURE__*/React.createElement("a", {
|
|
32471
33035
|
"aria-current": appName === (homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) ? "page" : undefined,
|
|
32472
33036
|
"aria-label": homeProduct ? homeProduct.name : appName,
|
|
32473
33037
|
"data-pendo": `app-switcher--change-app--${homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name}`,
|
|
32474
33038
|
"data-turbolinks": false,
|
|
32475
|
-
className: classnames(styles$
|
|
33039
|
+
className: classnames(styles$6["appmenu-list-a"], styles$6["appmenu-list-a--top"]),
|
|
32476
33040
|
href: homeProduct ? pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) : undefined
|
|
32477
33041
|
}, /*#__PURE__*/React.createElement(UserMenuAppLockup, {
|
|
32478
33042
|
appName: homeProduct ? homeProduct.name : appName
|
|
32479
33043
|
})), /*#__PURE__*/React.createElement("div", {
|
|
32480
|
-
className: styles$
|
|
33044
|
+
className: styles$6["appmenu-scroll"]
|
|
32481
33045
|
}, /*#__PURE__*/React.createElement("ul", {
|
|
32482
33046
|
"data-pendo": "app-switcher",
|
|
32483
|
-
className: styles$
|
|
33047
|
+
className: styles$6["appmenu-list"]
|
|
32484
33048
|
}, appsMenuFormatter(restProducts).map(({
|
|
32485
33049
|
name
|
|
32486
33050
|
}) => (/*#__PURE__*/React.createElement("li", {
|
|
32487
33051
|
key: name,
|
|
32488
|
-
className: styles$
|
|
33052
|
+
className: styles$6["appmenu-list-li"]
|
|
32489
33053
|
}, /*#__PURE__*/React.createElement("a", {
|
|
32490
33054
|
"aria-current": name === appName ? "page" : undefined,
|
|
32491
33055
|
"aria-label": name,
|
|
32492
33056
|
"data-pendo": `app-switcher--change-app--${name}`,
|
|
32493
33057
|
"data-turbolinks": false,
|
|
32494
|
-
className: styles$
|
|
33058
|
+
className: styles$6["appmenu-list-a"],
|
|
32495
33059
|
href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(name)
|
|
32496
33060
|
}, /*#__PURE__*/React.createElement(UserMenuAppLockup, {
|
|
32497
33061
|
appName: name
|
|
32498
33062
|
}))))), accountsProduct && (/*#__PURE__*/React.createElement("li", {
|
|
32499
|
-
className: styles$
|
|
33063
|
+
className: styles$6["appmenu-list-li"]
|
|
32500
33064
|
}, /*#__PURE__*/React.createElement(AccountSettingsLink, {
|
|
32501
33065
|
env: env
|
|
32502
33066
|
}))))))))
|
|
@@ -32560,19 +33124,19 @@ const SmallTopbar = ({
|
|
|
32560
33124
|
toggle: toggleProductSwitcher,
|
|
32561
33125
|
visible: productSwitcherVisible
|
|
32562
33126
|
})), routesMenuVisible && (/*#__PURE__*/React.createElement("div", {
|
|
32563
|
-
className: styles$
|
|
33127
|
+
className: styles$6["routes"]
|
|
32564
33128
|
}, props.routes)), /*#__PURE__*/React.createElement("button", {
|
|
32565
33129
|
type: "button",
|
|
32566
|
-
className: classnames(styles$
|
|
32567
|
-
[styles$
|
|
33130
|
+
className: classnames(styles$6["current-route"], {
|
|
33131
|
+
[styles$6["is-active"]]: routesMenuVisible
|
|
32568
33132
|
}),
|
|
32569
33133
|
onClick: props.routes.length > 0 ? () => {
|
|
32570
33134
|
setProductSwitcherVisible(false);
|
|
32571
33135
|
setRoutesMenuVisible(visible => !visible);
|
|
32572
33136
|
} : null
|
|
32573
|
-
}, /*#__PURE__*/React.createElement(CurrentRouteComponent, null, capitalize(currentRouteText)), props.routes.length > 0 && /*#__PURE__*/React.createElement(DisclosureChevronIcon, null)), /*#__PURE__*/React.createElement("button", {
|
|
33137
|
+
}, /*#__PURE__*/React.createElement(CurrentRouteComponent, null, capitalize$1(currentRouteText)), props.routes.length > 0 && /*#__PURE__*/React.createElement(DisclosureChevronIcon, null)), /*#__PURE__*/React.createElement("button", {
|
|
32574
33138
|
type: "button",
|
|
32575
|
-
className: styles$
|
|
33139
|
+
className: styles$6["profile-button"],
|
|
32576
33140
|
onClick: () => {
|
|
32577
33141
|
var _a, _b;
|
|
32578
33142
|
props.requestConnectedPeopleFetch();
|
|
@@ -32588,7 +33152,7 @@ const SmallRoute = ({
|
|
|
32588
33152
|
className,
|
|
32589
33153
|
...props
|
|
32590
33154
|
}) => (/*#__PURE__*/React.createElement("a", {
|
|
32591
|
-
className: classnames(styles$
|
|
33155
|
+
className: classnames(styles$6["route"], className, active && styles$6["is-active"]),
|
|
32592
33156
|
...props
|
|
32593
33157
|
}));
|
|
32594
33158
|
|
|
@@ -32727,7 +33291,7 @@ const topbarTheme = {
|
|
|
32727
33291
|
id: "topbar"
|
|
32728
33292
|
};
|
|
32729
33293
|
|
|
32730
|
-
var styles$
|
|
33294
|
+
var styles$5 = {"debug-pco-resource":"debug-pco-resource-sLteM","debug-pco-resource-header":"debug-pco-resource-header-5fEbe","debug-pco-resource-badge":"debug-pco-resource-badge-J9v1F","debug-pco-resource-title":"debug-pco-resource-title-volWV","debug-pco-resource-field":"debug-pco-resource-field-7Gitp","debug-pco-resource-field-label":"debug-pco-resource-field-label--f1xX","debug-pco-resource-field-value":"debug-pco-resource-field-value-YoX-C","debug-pco-resource-note":"debug-pco-resource-note-N5xyf","debug-pco-resource-json":"debug-pco-resource-json-Rx-JI"};
|
|
32731
33295
|
|
|
32732
33296
|
function isPresent(value) {
|
|
32733
33297
|
return value !== undefined && value.trim() !== "";
|
|
@@ -32756,15 +33320,31 @@ function Field({
|
|
|
32756
33320
|
value
|
|
32757
33321
|
}) {
|
|
32758
33322
|
return /*#__PURE__*/React.createElement("div", {
|
|
32759
|
-
className: styles$
|
|
33323
|
+
className: styles$5["debug-pco-resource-field"]
|
|
32760
33324
|
}, /*#__PURE__*/React.createElement("span", {
|
|
32761
|
-
className: styles$
|
|
33325
|
+
className: styles$5["debug-pco-resource-field-label"]
|
|
32762
33326
|
}, label), /*#__PURE__*/React.createElement("span", {
|
|
32763
|
-
className: styles$
|
|
33327
|
+
className: styles$5["debug-pco-resource-field-value"]
|
|
32764
33328
|
}, value));
|
|
32765
33329
|
}
|
|
33330
|
+
// Only staff ghosting or impersonating a person get pco:staff-access-level,
|
|
33331
|
+
// which keeps the panel away from regular users on staging.
|
|
32766
33332
|
function DebugPcoResource({
|
|
32767
33333
|
env
|
|
33334
|
+
}) {
|
|
33335
|
+
const {
|
|
33336
|
+
"staff-access-level": staffAccessLevel
|
|
33337
|
+
} = usePcoMeta();
|
|
33338
|
+
if (isPresent(staffAccessLevel)) {
|
|
33339
|
+
return /*#__PURE__*/React.createElement(DebugPanel, {
|
|
33340
|
+
env: env
|
|
33341
|
+
});
|
|
33342
|
+
} else {
|
|
33343
|
+
return null;
|
|
33344
|
+
}
|
|
33345
|
+
}
|
|
33346
|
+
function DebugPanel({
|
|
33347
|
+
env
|
|
32768
33348
|
}) {
|
|
32769
33349
|
const {
|
|
32770
33350
|
recently_visitable: recentlyVisitable,
|
|
@@ -32773,12 +33353,17 @@ function DebugPcoResource({
|
|
|
32773
33353
|
const [lookup, setLookup] = React.useState({
|
|
32774
33354
|
status: "idle"
|
|
32775
33355
|
});
|
|
32776
|
-
const
|
|
33356
|
+
const visit = useCurrentVisit();
|
|
33357
|
+
const personId = useCurrentPersonId();
|
|
33358
|
+
const recentlyVisited = useRecentlyVisited({
|
|
33359
|
+
env,
|
|
33360
|
+
personId
|
|
33361
|
+
});
|
|
33362
|
+
const pcrl = (visit === null || visit === void 0 ? void 0 : visit.type) === "resource" ? visit.pcrl : null;
|
|
32777
33363
|
const hasRecentlyVisitable = isPresent(recentlyVisitable);
|
|
33364
|
+
const hasInvalidResource = isPresent(resource) && !isPcrl(resource);
|
|
32778
33365
|
React.useEffect(() => {
|
|
32779
|
-
|
|
32780
|
-
// tags are present.
|
|
32781
|
-
const url = hasResource ? pcrlLookupUrl(resource, env) : null;
|
|
33366
|
+
const url = pcrl ? pcrlLookupUrl(pcrl, env) : null;
|
|
32782
33367
|
if (!url) {
|
|
32783
33368
|
setLookup({
|
|
32784
33369
|
status: "idle"
|
|
@@ -32810,37 +33395,45 @@ function DebugPcoResource({
|
|
|
32810
33395
|
return () => {
|
|
32811
33396
|
cancelled = true;
|
|
32812
33397
|
};
|
|
32813
|
-
}, [
|
|
33398
|
+
}, [pcrl, env]);
|
|
32814
33399
|
return /*#__PURE__*/React.createElement("div", {
|
|
32815
|
-
className: classnames("staff-only", styles$
|
|
33400
|
+
className: classnames("staff-only", styles$5["debug-pco-resource"])
|
|
32816
33401
|
}, /*#__PURE__*/React.createElement("div", {
|
|
32817
|
-
className: styles$
|
|
33402
|
+
className: styles$5["debug-pco-resource-header"]
|
|
32818
33403
|
}, /*#__PURE__*/React.createElement("span", {
|
|
32819
|
-
className: styles$
|
|
33404
|
+
className: styles$5["debug-pco-resource-badge"]
|
|
32820
33405
|
}, "Staff"), /*#__PURE__*/React.createElement("span", {
|
|
32821
|
-
className: styles$
|
|
33406
|
+
className: styles$5["debug-pco-resource-title"]
|
|
32822
33407
|
}, "Recently Visited debug")), /*#__PURE__*/React.createElement(Field, {
|
|
32823
33408
|
label: "pco:resource",
|
|
32824
33409
|
value: describeTag("resource", resource)
|
|
32825
33410
|
}), /*#__PURE__*/React.createElement(Field, {
|
|
32826
33411
|
label: "pco:recently_visitable",
|
|
32827
33412
|
value: describeTag("recently_visitable", recentlyVisitable)
|
|
32828
|
-
}),
|
|
32829
|
-
className: styles$
|
|
32830
|
-
}, "
|
|
32831
|
-
className: styles$
|
|
32832
|
-
}, "
|
|
32833
|
-
className: styles$
|
|
32834
|
-
}, "
|
|
32835
|
-
className: styles$
|
|
32836
|
-
},
|
|
32837
|
-
className: styles$
|
|
32838
|
-
},
|
|
32839
|
-
className: styles$
|
|
32840
|
-
},
|
|
32841
|
-
|
|
32842
|
-
|
|
32843
|
-
|
|
33413
|
+
}), hasInvalidResource && (/*#__PURE__*/React.createElement("p", {
|
|
33414
|
+
className: styles$5["debug-pco-resource-note"]
|
|
33415
|
+
}, "pco:resource isn't a PCRL (product/v2/type/id), so Recently Visited ignores it.")), visit === null && (/*#__PURE__*/React.createElement("p", {
|
|
33416
|
+
className: styles$5["debug-pco-resource-note"]
|
|
33417
|
+
}, "This page is not eligible for Recently Visited")), pcrl && hasRecentlyVisitable && (/*#__PURE__*/React.createElement("p", {
|
|
33418
|
+
className: styles$5["debug-pco-resource-note"]
|
|
33419
|
+
}, "Both tags are present \u2014 pco:resource takes precedence for the Recently Visited entry.")), pcrl && lookup.status === "loading" && (/*#__PURE__*/React.createElement("p", {
|
|
33420
|
+
className: styles$5["debug-pco-resource-note"]
|
|
33421
|
+
}, "Looking up resource\u2026")), pcrl && lookup.status === "error" && (/*#__PURE__*/React.createElement("p", {
|
|
33422
|
+
className: styles$5["debug-pco-resource-note"]
|
|
33423
|
+
}, lookup.message)), pcrl && lookup.status === "loaded" && (/*#__PURE__*/React.createElement("pre", {
|
|
33424
|
+
className: styles$5["debug-pco-resource-json"]
|
|
33425
|
+
}, JSON.stringify(lookup.json, null, 2))), (visit === null || visit === void 0 ? void 0 : visit.type) === "page" && (/*#__PURE__*/React.createElement("p", {
|
|
33426
|
+
className: styles$5["debug-pco-resource-note"]
|
|
33427
|
+
}, "Recently Visited title: ", visit.title)), /*#__PURE__*/React.createElement("span", {
|
|
33428
|
+
className: styles$5["debug-pco-resource-field-label"]
|
|
33429
|
+
}, "Recently Visited"), personId ? (/*#__PURE__*/React.createElement("pre", {
|
|
33430
|
+
className: styles$5["debug-pco-resource-json"]
|
|
33431
|
+
}, JSON.stringify(recentlyVisited, null, 2))) : (/*#__PURE__*/React.createElement("p", {
|
|
33432
|
+
className: styles$5["debug-pco-resource-note"]
|
|
33433
|
+
}, "No current person, so nothing is recorded.")));
|
|
33434
|
+
}
|
|
33435
|
+
|
|
33436
|
+
var styles$4 = {"drawer-wrap":"drawer-wrap-J0rSL","is-visible":"is-visible-KTME8","drawer":"drawer-j7f4l"};
|
|
32844
33437
|
|
|
32845
33438
|
function Drawer({
|
|
32846
33439
|
children,
|
|
@@ -32848,7 +33441,7 @@ function Drawer({
|
|
|
32848
33441
|
}) {
|
|
32849
33442
|
return /*#__PURE__*/React.createElement("div", {
|
|
32850
33443
|
id: id,
|
|
32851
|
-
className: styles$
|
|
33444
|
+
className: styles$4["drawer"]
|
|
32852
33445
|
}, children);
|
|
32853
33446
|
}
|
|
32854
33447
|
function DrawerWrap({
|
|
@@ -32858,7 +33451,7 @@ function DrawerWrap({
|
|
|
32858
33451
|
...props
|
|
32859
33452
|
}) {
|
|
32860
33453
|
return /*#__PURE__*/React.createElement("div", {
|
|
32861
|
-
className: classnames(styles$
|
|
33454
|
+
className: classnames(styles$4["drawer-wrap"], className, isVisible && styles$4["is-visible"]),
|
|
32862
33455
|
...props
|
|
32863
33456
|
}, children);
|
|
32864
33457
|
}
|
|
@@ -33042,15 +33635,50 @@ const SwitchView = ({
|
|
|
33042
33635
|
}));
|
|
33043
33636
|
};
|
|
33044
33637
|
|
|
33045
|
-
|
|
33638
|
+
const collections = {
|
|
33639
|
+
general: general__namespace,
|
|
33640
|
+
toolbar: toolbar__namespace
|
|
33641
|
+
};
|
|
33642
|
+
const paths = collections;
|
|
33643
|
+
function Icon({
|
|
33644
|
+
name,
|
|
33645
|
+
size = 16,
|
|
33646
|
+
style,
|
|
33647
|
+
theme,
|
|
33648
|
+
title,
|
|
33649
|
+
...props
|
|
33650
|
+
}) {
|
|
33651
|
+
const [collection, symbol] = name.split(".");
|
|
33652
|
+
return /*#__PURE__*/React.createElement("svg", {
|
|
33653
|
+
"aria-hidden": title ? undefined : true,
|
|
33654
|
+
fill: "currentColor",
|
|
33655
|
+
height: size,
|
|
33656
|
+
role: title ? "img" : undefined,
|
|
33657
|
+
style: {
|
|
33658
|
+
color: theme && `var(${theme})`,
|
|
33659
|
+
flexShrink: 0,
|
|
33660
|
+
...style
|
|
33661
|
+
},
|
|
33662
|
+
viewBox: "0 0 16 16",
|
|
33663
|
+
width: size,
|
|
33664
|
+
...props
|
|
33665
|
+
}, title && /*#__PURE__*/React.createElement("title", null, title), /*#__PURE__*/React.createElement("path", {
|
|
33666
|
+
d: paths[collection][symbol]
|
|
33667
|
+
}));
|
|
33668
|
+
}
|
|
33669
|
+
|
|
33670
|
+
var styles$3 = {"color-mode-switcher":"color-mode-switcher-JpczO","color-mode-switcher-label":"color-mode-switcher-label-3jPM2","color-mode-switcher-modes":"color-mode-switcher-modes-u-bic"};
|
|
33046
33671
|
|
|
33047
33672
|
const COLOR_MODES = [{
|
|
33673
|
+
icon: "toolbar.sun",
|
|
33048
33674
|
label: "Light",
|
|
33049
33675
|
value: "light"
|
|
33050
33676
|
}, {
|
|
33677
|
+
icon: "toolbar.moon",
|
|
33051
33678
|
label: "Dark",
|
|
33052
33679
|
value: "dark"
|
|
33053
33680
|
}, {
|
|
33681
|
+
icon: "toolbar.display",
|
|
33054
33682
|
label: "System",
|
|
33055
33683
|
value: "system"
|
|
33056
33684
|
}];
|
|
@@ -33062,14 +33690,15 @@ function ColorModeSwitcher() {
|
|
|
33062
33690
|
const updateStore = useUpdateStore();
|
|
33063
33691
|
const [counter] = React.useState(() => COLOR_MODE_INSTANCE_COUNTER++);
|
|
33064
33692
|
return /*#__PURE__*/React.createElement("fieldset", {
|
|
33065
|
-
className: styles$
|
|
33693
|
+
className: styles$3["color-mode-switcher"]
|
|
33066
33694
|
}, /*#__PURE__*/React.createElement("legend", {
|
|
33067
|
-
className: styles$
|
|
33695
|
+
className: styles$3["color-mode-switcher-label"]
|
|
33068
33696
|
}, "Appearance", " "), /*#__PURE__*/React.createElement("div", {
|
|
33069
|
-
className: styles$
|
|
33697
|
+
className: styles$3["color-mode-switcher-modes"]
|
|
33070
33698
|
}, COLOR_MODES.map(({
|
|
33071
|
-
|
|
33072
|
-
label
|
|
33699
|
+
icon,
|
|
33700
|
+
label,
|
|
33701
|
+
value
|
|
33073
33702
|
}) => (/*#__PURE__*/React.createElement("label", {
|
|
33074
33703
|
key: value
|
|
33075
33704
|
}, /*#__PURE__*/React.createElement("input", {
|
|
@@ -33086,8 +33715,9 @@ function ColorModeSwitcher() {
|
|
|
33086
33715
|
document.documentElement.setAttribute(RESOLVED_COLOR_MODE_ATTRIBUTE, resolveColorMode(colorMode));
|
|
33087
33716
|
updateColorMode(colorMode);
|
|
33088
33717
|
}
|
|
33089
|
-
}), /*#__PURE__*/React.createElement(
|
|
33090
|
-
|
|
33718
|
+
}), /*#__PURE__*/React.createElement(Icon, {
|
|
33719
|
+
name: icon,
|
|
33720
|
+
size: 15
|
|
33091
33721
|
}), label)))));
|
|
33092
33722
|
}
|
|
33093
33723
|
function updateColorMode(mode) {
|
|
@@ -33102,28 +33732,6 @@ function updateColorMode(mode) {
|
|
|
33102
33732
|
method: "POST"
|
|
33103
33733
|
});
|
|
33104
33734
|
}
|
|
33105
|
-
const COLOR_MODE_ICON_PATHS = {
|
|
33106
|
-
dark: "M12.66667,9.07579 C11.90047,9.42229 11.04997,9.61514 10.15457,9.61514 C6.78392,9.61514 4.05151,6.88275 4.05151,3.51215 C4.05151,2.61663 4.24438,1.76615 4.59086,1 C2.47336,1.95762 1,4.08858 1,6.56366 C1,9.93423 3.73241,12.66663 7.103,12.66663 C9.57807,12.66663 11.70907,11.19333 12.66667,9.07579 Z",
|
|
33107
|
-
light: "M6.83333,1 L6.83333,2.16667 M6.83333,11.50003 L6.83333,12.66663 M2.16667,6.83334 L1,6.83334 M3.51657,3.51657 L2.69161,2.69161 M10.1501,3.51657 L10.9751,2.69161 M3.51657,10.15253 L2.69161,10.97743 M10.1501,10.15253 L10.9751,10.97743 M12.6667,6.83334 L11.5,6.83334 M9.75,6.83334 C9.75,8.44415 8.44415,9.75 6.83333,9.75 C5.2225,9.75 3.91667,8.44415 3.91667,6.83334 C3.91667,5.22251 5.2225,3.91667 6.83333,3.91667 C8.44415,3.91667 9.75,5.22251 9.75,6.83334 Z",
|
|
33108
|
-
system: "M4.75,12.50003 L9.41667,12.50003 M7.08334,10.16667 L7.08334,12.50003 M4.05,10.16667 L10.11665,10.16667 C11.09675,10.16667 11.58675,10.16667 11.96115,9.97592 C12.29045,9.80815 12.55815,9.54046 12.72595,9.21117 C12.91665,8.83678 12.91665,8.34678 12.91665,7.36667 L12.91665,4.8 C12.91665,3.81991 12.91665,3.32986 12.72595,2.95552 C12.55815,2.62623 12.29045,2.35852 11.96115,2.19074 C11.58675,2 11.09675,2 10.11665,2 L4.05,2 C3.06991,2 2.57986,2 2.20552,2.19074 C1.87623,2.35852 1.60852,2.62623 1.44074,2.95552 C1.25,3.32986 1.25,3.81991 1.25,4.8 L1.25,7.36667 C1.25,8.34678 1.25,8.83678 1.44074,9.21117 C1.60852,9.54046 1.87623,9.80815 2.20552,9.97592 C2.57986,10.16667 3.06991,10.16667 4.05,10.16667 Z"
|
|
33109
|
-
};
|
|
33110
|
-
function ColorModeIcon({
|
|
33111
|
-
mode
|
|
33112
|
-
}) {
|
|
33113
|
-
return /*#__PURE__*/React.createElement("svg", {
|
|
33114
|
-
width: "16",
|
|
33115
|
-
height: "16",
|
|
33116
|
-
viewBox: "0 0 14 14",
|
|
33117
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
33118
|
-
"aria-hidden": "true"
|
|
33119
|
-
}, /*#__PURE__*/React.createElement("path", {
|
|
33120
|
-
d: COLOR_MODE_ICON_PATHS[mode],
|
|
33121
|
-
strokeWidth: "1.5",
|
|
33122
|
-
fill: "none",
|
|
33123
|
-
strokeLinecap: "round",
|
|
33124
|
-
strokeLinejoin: "round"
|
|
33125
|
-
}));
|
|
33126
|
-
}
|
|
33127
33735
|
|
|
33128
33736
|
const requestClearConnectedPeopleCache = () => remove();
|
|
33129
33737
|
function UserProfile({
|
|
@@ -33186,10 +33794,10 @@ function UserProfile({
|
|
|
33186
33794
|
alignment: "center",
|
|
33187
33795
|
spacing: 0.5,
|
|
33188
33796
|
width: "100%"
|
|
33189
|
-
}, /*#__PURE__*/React.createElement(
|
|
33797
|
+
}, /*#__PURE__*/React.createElement(Icon, {
|
|
33190
33798
|
name: mfaConfigured ? "general.lock" : "general.unlock",
|
|
33191
|
-
size:
|
|
33192
|
-
|
|
33799
|
+
size: 12,
|
|
33800
|
+
theme: mfaConfigured ? "--t-icon-color-status-success" : "--t-icon-color-status-error"
|
|
33193
33801
|
}), /*#__PURE__*/React.createElement(tapestryReact.Text, {
|
|
33194
33802
|
size: 4,
|
|
33195
33803
|
weight: 400,
|
|
@@ -33237,9 +33845,10 @@ function UserProfile({
|
|
|
33237
33845
|
}, /*#__PURE__*/React.createElement(tapestryReact.Text, {
|
|
33238
33846
|
color: "var(--text-color-default-secondary)",
|
|
33239
33847
|
size: 3
|
|
33240
|
-
}, "Log out"), /*#__PURE__*/React.createElement(
|
|
33848
|
+
}, "Log out"), /*#__PURE__*/React.createElement(Icon, {
|
|
33241
33849
|
name: "general.intoBox",
|
|
33242
|
-
|
|
33850
|
+
size: 14,
|
|
33851
|
+
theme: "--t-icon-color-secondary"
|
|
33243
33852
|
})))), env !== "production" && /*#__PURE__*/React.createElement(DebugPcoResource, {
|
|
33244
33853
|
env: env
|
|
33245
33854
|
}), hasConnectedPeople && (/*#__PURE__*/React.createElement(tapestryReact.StackView, {
|
|
@@ -33301,26 +33910,42 @@ function UserProfile({
|
|
|
33301
33910
|
|
|
33302
33911
|
var ActionType;
|
|
33303
33912
|
(function (ActionType) {
|
|
33913
|
+
ActionType["clearPending"] = "clearPending";
|
|
33304
33914
|
ActionType["close"] = "close";
|
|
33305
33915
|
ActionType["mount"] = "mount";
|
|
33306
33916
|
ActionType["open"] = "open";
|
|
33917
|
+
ActionType["setPending"] = "setPending";
|
|
33307
33918
|
ActionType["unmount"] = "unmount";
|
|
33308
33919
|
})(ActionType || (ActionType = {}));
|
|
33309
33920
|
const initialState = {
|
|
33310
33921
|
currentContent: null,
|
|
33311
|
-
open: false
|
|
33922
|
+
open: false,
|
|
33923
|
+
pending: null
|
|
33312
33924
|
};
|
|
33313
33925
|
const reducer = (state, action) => {
|
|
33926
|
+
var _a, _b;
|
|
33314
33927
|
switch (action.type) {
|
|
33315
33928
|
case ActionType.mount:
|
|
33316
33929
|
return {
|
|
33317
33930
|
...state,
|
|
33318
|
-
currentContent: action.payload.content
|
|
33931
|
+
currentContent: action.payload.content,
|
|
33932
|
+
pending: ((_a = state.pending) === null || _a === void 0 ? void 0 : _a.tool) === ((_b = action.payload.content) === null || _b === void 0 ? void 0 : _b.name) ? state.pending : null
|
|
33319
33933
|
};
|
|
33320
33934
|
case ActionType.unmount:
|
|
33321
33935
|
return {
|
|
33322
33936
|
...state,
|
|
33323
|
-
currentContent: null
|
|
33937
|
+
currentContent: null,
|
|
33938
|
+
pending: null
|
|
33939
|
+
};
|
|
33940
|
+
case ActionType.setPending:
|
|
33941
|
+
return {
|
|
33942
|
+
...state,
|
|
33943
|
+
pending: action.payload
|
|
33944
|
+
};
|
|
33945
|
+
case ActionType.clearPending:
|
|
33946
|
+
return {
|
|
33947
|
+
...state,
|
|
33948
|
+
pending: null
|
|
33324
33949
|
};
|
|
33325
33950
|
case ActionType.open:
|
|
33326
33951
|
return {
|
|
@@ -33339,13 +33964,181 @@ const useToolbarReducer = overrides => React__namespace.useReducer(reducer, {
|
|
|
33339
33964
|
...overrides
|
|
33340
33965
|
});
|
|
33341
33966
|
|
|
33342
|
-
var styles$
|
|
33967
|
+
var styles$2 = {"toolbar":"toolbar-KHJR5","toolbar-button":"toolbar-button-Ye4h5","is-current":"is-current-DZK47","toolbar-button--avatar":"toolbar-button--avatar-5FUik","toolbar-button--home":"toolbar-button--home-Pthd0"};
|
|
33343
33968
|
|
|
33344
|
-
|
|
33969
|
+
// PCO Box serves TLS by default, and a bare http:// frame inside an https
|
|
33970
|
+
// product page is blocked as mixed content, so follow the page's scheme.
|
|
33971
|
+
const iframeSrc = () => `${window.location.protocol}//pco.test/iframe`;
|
|
33972
|
+
const STAFF_PINK = "var(--t-fill-color-product-staff)";
|
|
33973
|
+
// The pco.test domain is only ever served by a local PCO Box, so the tool
|
|
33974
|
+
// stays hidden in staging and production — and on Topbar's own dev server,
|
|
33975
|
+
// which runs on localhost and has no pco.test to frame.
|
|
33976
|
+
function isPcoTestHost() {
|
|
33977
|
+
const {
|
|
33978
|
+
hostname
|
|
33979
|
+
} = window.location;
|
|
33980
|
+
return hostname === "pco.test" || hostname.endsWith(".pco.test");
|
|
33981
|
+
}
|
|
33982
|
+
// Flattened from the source artwork's two outline paths — its gradients,
|
|
33983
|
+
// filters and clip paths carry nothing at icon size. Those paths are authored
|
|
33984
|
+
// on a 128px canvas, hence the transform: it fits the pair into the same ~24px
|
|
33985
|
+
// optical box the sibling toolbar icons occupy. Filled explicitly rather than
|
|
33986
|
+
// with `currentColor` so the mark stays staff pink instead of inheriting the
|
|
33987
|
+
// toolbar's text color and its hover/active states.
|
|
33988
|
+
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";
|
|
33989
|
+
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";
|
|
33990
|
+
function ToolsIcon() {
|
|
33991
|
+
return /*#__PURE__*/React.createElement("svg", {
|
|
33992
|
+
"aria-hidden": "true",
|
|
33993
|
+
width: "32",
|
|
33994
|
+
height: "32",
|
|
33995
|
+
viewBox: "0 0 32 32",
|
|
33996
|
+
xmlns: "http://www.w3.org/2000/svg"
|
|
33997
|
+
}, /*#__PURE__*/React.createElement("g", {
|
|
33998
|
+
fill: STAFF_PINK,
|
|
33999
|
+
transform: "translate(2.92 2.66) scale(.204)"
|
|
34000
|
+
}, /*#__PURE__*/React.createElement("path", {
|
|
34001
|
+
d: WRENCH
|
|
34002
|
+
}), /*#__PURE__*/React.createElement("path", {
|
|
34003
|
+
d: SCREWDRIVER
|
|
34004
|
+
})));
|
|
34005
|
+
}
|
|
34006
|
+
// A cross-origin frame's own history object is unreachable (touching it throws
|
|
34007
|
+
// a SecurityError), but the frame's navigations do land in the top-level joint
|
|
34008
|
+
// session history, so the *parent's* back/forward traverse them while leaving
|
|
34009
|
+
// the host page in place. Counting frame loads tracks how deep we are, which is
|
|
34010
|
+
// what keeps a click on Back from stepping past the frame's first page and
|
|
34011
|
+
// navigating the host app instead. Loads we caused are attributed to the
|
|
34012
|
+
// traversal that requested them rather than counted as new navigations.
|
|
34013
|
+
function useFrameHistory() {
|
|
34014
|
+
const [steps, setSteps] = React.useState({
|
|
34015
|
+
back: 0,
|
|
34016
|
+
forward: 0
|
|
34017
|
+
});
|
|
34018
|
+
// Traversal has to read the current counts outside of render, so the ref is
|
|
34019
|
+
// the source of truth and state exists to paint the buttons.
|
|
34020
|
+
const stepsRef = React.useRef(steps);
|
|
34021
|
+
const pendingTraversal = React.useRef(null);
|
|
34022
|
+
const frameIsLoaded = React.useRef(false);
|
|
34023
|
+
const moveTo = React.useCallback(next => {
|
|
34024
|
+
stepsRef.current = next;
|
|
34025
|
+
setSteps(next);
|
|
34026
|
+
}, []);
|
|
34027
|
+
const onFrameLoad = React.useCallback(() => {
|
|
34028
|
+
if (!frameIsLoaded.current) {
|
|
34029
|
+
// The frame's first page is the floor, not a step.
|
|
34030
|
+
frameIsLoaded.current = true;
|
|
34031
|
+
} else {
|
|
34032
|
+
const traversal = pendingTraversal.current;
|
|
34033
|
+
pendingTraversal.current = null;
|
|
34034
|
+
const {
|
|
34035
|
+
back,
|
|
34036
|
+
forward
|
|
34037
|
+
} = stepsRef.current;
|
|
34038
|
+
if (traversal === "back") {
|
|
34039
|
+
moveTo({
|
|
34040
|
+
back: back - 1,
|
|
34041
|
+
forward: forward + 1
|
|
34042
|
+
});
|
|
34043
|
+
} else if (traversal === "forward") {
|
|
34044
|
+
moveTo({
|
|
34045
|
+
back: back + 1,
|
|
34046
|
+
forward: forward - 1
|
|
34047
|
+
});
|
|
34048
|
+
} else {
|
|
34049
|
+
moveTo({
|
|
34050
|
+
back: back + 1,
|
|
34051
|
+
forward: 0
|
|
34052
|
+
});
|
|
34053
|
+
}
|
|
34054
|
+
}
|
|
34055
|
+
}, [moveTo]);
|
|
34056
|
+
// Guarded here rather than trusting the button's disabled state: traversing
|
|
34057
|
+
// with no frame step left would navigate the host app out from under us.
|
|
34058
|
+
const traverse = React.useCallback(direction => {
|
|
34059
|
+
if (stepsRef.current[direction] > 0) {
|
|
34060
|
+
pendingTraversal.current = direction;
|
|
34061
|
+
window.history[direction]();
|
|
34062
|
+
}
|
|
34063
|
+
}, []);
|
|
34064
|
+
return {
|
|
34065
|
+
onFrameLoad,
|
|
34066
|
+
steps,
|
|
34067
|
+
traverse
|
|
34068
|
+
};
|
|
34069
|
+
}
|
|
34070
|
+
function NavButton({
|
|
34071
|
+
direction,
|
|
34072
|
+
disabled,
|
|
34073
|
+
onClick
|
|
34074
|
+
}) {
|
|
34075
|
+
return /*#__PURE__*/React.createElement(tapestryReact.Button
|
|
34076
|
+
// Tapestry's `disabled` suppresses the click but emits no ARIA state.
|
|
34077
|
+
, {
|
|
34078
|
+
"aria-disabled": disabled,
|
|
34079
|
+
disabled: disabled,
|
|
34080
|
+
hover: {
|
|
34081
|
+
backgroundColor: "var(--t-fill-color-neutral-070)"
|
|
34082
|
+
},
|
|
34083
|
+
icon: {
|
|
34084
|
+
fill: "var(--t-icon-color-dim)",
|
|
34085
|
+
name: direction === "back" ? "general.leftChevron" : "general.rightChevron",
|
|
34086
|
+
size: "md"
|
|
34087
|
+
},
|
|
34088
|
+
onClick: onClick,
|
|
34089
|
+
opacity: disabled ? 0.35 : 1,
|
|
34090
|
+
theme: false,
|
|
34091
|
+
title: direction === "back" ? "Back" : "Forward"
|
|
34092
|
+
});
|
|
34093
|
+
}
|
|
34094
|
+
function DeveloperToolsComponent({
|
|
34095
|
+
closeDrawer
|
|
34096
|
+
}) {
|
|
34097
|
+
const {
|
|
34098
|
+
onFrameLoad,
|
|
34099
|
+
steps,
|
|
34100
|
+
traverse
|
|
34101
|
+
} = useFrameHistory();
|
|
34102
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(DrawerHeader, {
|
|
34103
|
+
closeDrawer: closeDrawer
|
|
34104
|
+
}, /*#__PURE__*/React.createElement(tapestryReact.StackView, {
|
|
34105
|
+
alignment: "center",
|
|
34106
|
+
axis: "horizontal",
|
|
34107
|
+
spacing: 0.25
|
|
34108
|
+
}, /*#__PURE__*/React.createElement(NavButton, {
|
|
34109
|
+
direction: "back",
|
|
34110
|
+
disabled: steps.back === 0,
|
|
34111
|
+
onClick: () => traverse("back")
|
|
34112
|
+
}), /*#__PURE__*/React.createElement(NavButton, {
|
|
34113
|
+
direction: "forward",
|
|
34114
|
+
disabled: steps.forward === 0,
|
|
34115
|
+
onClick: () => traverse("forward")
|
|
34116
|
+
}), /*#__PURE__*/React.createElement(DrawerHeading, {
|
|
34117
|
+
paddingLeft: 0.5
|
|
34118
|
+
}, "Developer Tools"))), /*#__PURE__*/React.createElement(tapestryReact.Box, {
|
|
34119
|
+
as: "iframe",
|
|
34120
|
+
border: "none",
|
|
34121
|
+
flex: "1",
|
|
34122
|
+
minHeight: 0,
|
|
34123
|
+
onLoad: onFrameLoad,
|
|
34124
|
+
src: iframeSrc(),
|
|
34125
|
+
title: "Developer Tools",
|
|
34126
|
+
width: "100%"
|
|
34127
|
+
}));
|
|
34128
|
+
}
|
|
34129
|
+
const DeveloperTools = {
|
|
34130
|
+
component: DeveloperToolsComponent,
|
|
34131
|
+
icons: {
|
|
34132
|
+
component: ToolsIcon
|
|
34133
|
+
},
|
|
34134
|
+
label: "Developer Tools",
|
|
34135
|
+
// No space: `name` becomes an element id and a pendo attribute.
|
|
34136
|
+
name: "DeveloperTools"
|
|
34137
|
+
};
|
|
33345
34138
|
|
|
33346
|
-
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"};
|
|
34139
|
+
var styles$1 = {"banner":"banner-YfQ37","stacked":"stacked-EwLRh","content":"content-lNjWf","icon":"icon-abz7q","iconShape":"iconShape-6uyot","iconSparkle":"iconSparkle-t0OzK","text":"text--Vlhl","title":"title-zOEOR","subtitle":"subtitle-ufLnk","chevron":"chevron-oOUA-","inlineContent":"inlineContent-h5sVl","sparkle":"sparkle-uuMBp","inlineTitle":"inlineTitle-ru2Rk","action":"action-fSY3n","actionLabel":"actionLabel-MRBjb","actionChevron":"actionChevron-DghlN"};
|
|
33347
34140
|
|
|
33348
|
-
const SPARKLE_PATH = "M3.809 10.1332C3.87511 9.95596 4.12579 9.95588 4.19181 10.1332L4.63712 11.3363C4.64169 11.3487 4.65116 11.359 4.66349 11.3637L5.86661 11.809C6.04452 11.8749 6.04455 12.126 5.86661 12.1918L4.66349 12.6371C4.65134 12.6417 4.64173 12.6513 4.63712 12.6635L4.19181 13.8666C4.12597 14.0445 3.87486 14.0445 3.809 13.8666L3.36368 12.6635C3.35905 12.6512 3.3487 12.6417 3.33634 12.6371L2.13321 12.1918C1.95588 12.1258 1.95596 11.8751 2.13321 11.809L3.33634 11.3637C3.34877 11.3591 3.35908 11.3488 3.36368 11.3363L3.809 10.1332ZM8.52091 3.33341C8.68571 2.88899 9.31518 2.88891 9.47989 3.33341L10.5922 6.34122C10.6037 6.37226 10.6285 6.39712 10.6596 6.4086L13.6664 7.52091C14.1113 7.68553 14.1113 8.31528 13.6664 8.47989L10.6596 9.5922C10.6285 9.6037 10.6037 9.6285 10.5922 9.65958L9.47989 12.6664C9.31528 13.1113 8.68553 13.1113 8.52091 12.6664L7.4086 9.65958C7.39712 9.62853 7.37226 9.60371 7.34122 9.5922L4.33341 8.47989C3.88891 8.31518 3.88899 7.68571 4.33341 7.52091L7.34122 6.4086C7.3723 6.3971 7.3971 6.3723 7.4086 6.34122L8.52091 3.33341ZM3.809 2.13321C3.87511 1.95596 4.12579 1.95588 4.19181 2.13321L4.63712 3.33634C4.64169 3.3487 4.65116 3.35905 4.66349 3.36368L5.86661 3.809C6.04452 3.87486 6.04455 4.12597 5.86661 4.19181L4.66349 4.63712C4.65134 4.64173 4.64173 4.65134 4.63712 4.66349L4.19181 5.86661C4.12597 6.04455 3.87486 6.04452 3.809 5.86661L3.36368 4.66349C3.35905 4.65116 3.3487 4.64169 3.33634 4.63712L2.13321 4.19181C1.95588 4.12579 1.95596 3.87511 2.13321 3.809L3.33634 3.36368C3.34877 3.35908 3.35908 3.34877 3.36368 3.33634L3.809 2.13321Z";
|
|
34141
|
+
const SPARKLE_PATH$1 = "M3.809 10.1332C3.87511 9.95596 4.12579 9.95588 4.19181 10.1332L4.63712 11.3363C4.64169 11.3487 4.65116 11.359 4.66349 11.3637L5.86661 11.809C6.04452 11.8749 6.04455 12.126 5.86661 12.1918L4.66349 12.6371C4.65134 12.6417 4.64173 12.6513 4.63712 12.6635L4.19181 13.8666C4.12597 14.0445 3.87486 14.0445 3.809 13.8666L3.36368 12.6635C3.35905 12.6512 3.3487 12.6417 3.33634 12.6371L2.13321 12.1918C1.95588 12.1258 1.95596 11.8751 2.13321 11.809L3.33634 11.3637C3.34877 11.3591 3.35908 11.3488 3.36368 11.3363L3.809 10.1332ZM8.52091 3.33341C8.68571 2.88899 9.31518 2.88891 9.47989 3.33341L10.5922 6.34122C10.6037 6.37226 10.6285 6.39712 10.6596 6.4086L13.6664 7.52091C14.1113 7.68553 14.1113 8.31528 13.6664 8.47989L10.6596 9.5922C10.6285 9.6037 10.6037 9.6285 10.5922 9.65958L9.47989 12.6664C9.31528 13.1113 8.68553 13.1113 8.52091 12.6664L7.4086 9.65958C7.39712 9.62853 7.37226 9.60371 7.34122 9.5922L4.33341 8.47989C3.88891 8.31518 3.88899 7.68571 4.33341 7.52091L7.34122 6.4086C7.3723 6.3971 7.3971 6.3723 7.4086 6.34122L8.52091 3.33341ZM3.809 2.13321C3.87511 1.95596 4.12579 1.95588 4.19181 2.13321L4.63712 3.33634C4.64169 3.3487 4.65116 3.35905 4.66349 3.36368L5.86661 3.809C6.04452 3.87486 6.04455 4.12597 5.86661 4.19181L4.66349 4.63712C4.65134 4.64173 4.64173 4.65134 4.63712 4.66349L4.19181 5.86661C4.12597 6.04455 3.87486 6.04452 3.809 5.86661L3.36368 4.66349C3.35905 4.65116 3.3487 4.64169 3.33634 4.63712L2.13321 4.19181C1.95588 4.12579 1.95596 3.87511 2.13321 3.809L3.33634 3.36368C3.34877 3.35908 3.35908 3.34877 3.36368 3.33634L3.809 2.13321Z";
|
|
33349
34142
|
// Same stops as the AskAIBanner border gradient, so the two stay in sync.
|
|
33350
34143
|
const GRADIENT_STOPS = [{
|
|
33351
34144
|
color: "var(--t-fill-color-tag-teal-040)",
|
|
@@ -33394,7 +34187,7 @@ function SparkleIcon({
|
|
|
33394
34187
|
stopColor: color
|
|
33395
34188
|
}
|
|
33396
34189
|
})))))), /*#__PURE__*/React.createElement("path", {
|
|
33397
|
-
d: SPARKLE_PATH,
|
|
34190
|
+
d: SPARKLE_PATH$1,
|
|
33398
34191
|
fill: isGradient ? `url(#${gradientId})` : "currentColor"
|
|
33399
34192
|
}));
|
|
33400
34193
|
}
|
|
@@ -33403,8 +34196,8 @@ function AskAIBanner(props) {
|
|
|
33403
34196
|
var _a, _b;
|
|
33404
34197
|
const isStacked = props.variant !== "inline";
|
|
33405
34198
|
return /*#__PURE__*/React.createElement("button", {
|
|
33406
|
-
className: classnames(styles.banner, {
|
|
33407
|
-
[styles.stacked]: isStacked
|
|
34199
|
+
className: classnames(styles$1.banner, {
|
|
34200
|
+
[styles$1.stacked]: isStacked
|
|
33408
34201
|
}),
|
|
33409
34202
|
onClick: () => props.onClick(),
|
|
33410
34203
|
type: "button"
|
|
@@ -33419,15 +34212,15 @@ function StackedContent({
|
|
|
33419
34212
|
title
|
|
33420
34213
|
}) {
|
|
33421
34214
|
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
|
|
33422
|
-
className: styles.content
|
|
34215
|
+
className: styles$1.content
|
|
33423
34216
|
}, /*#__PURE__*/React.createElement(AskAIIcon, null), /*#__PURE__*/React.createElement("span", {
|
|
33424
|
-
className: styles.text
|
|
34217
|
+
className: styles$1.text
|
|
33425
34218
|
}, /*#__PURE__*/React.createElement("span", {
|
|
33426
|
-
className: styles.title
|
|
34219
|
+
className: styles$1.title
|
|
33427
34220
|
}, title), /*#__PURE__*/React.createElement("span", {
|
|
33428
|
-
className: styles.subtitle
|
|
34221
|
+
className: styles$1.subtitle
|
|
33429
34222
|
}, "Support answers tailored to you"))), /*#__PURE__*/React.createElement(tapestryReact.Icon, {
|
|
33430
|
-
className: styles.chevron,
|
|
34223
|
+
className: styles$1.chevron,
|
|
33431
34224
|
name: "general.rightChevron",
|
|
33432
34225
|
size: "16px"
|
|
33433
34226
|
}));
|
|
@@ -33437,18 +34230,18 @@ function InlineContent({
|
|
|
33437
34230
|
title
|
|
33438
34231
|
}) {
|
|
33439
34232
|
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
|
|
33440
|
-
className: styles.inlineContent
|
|
34233
|
+
className: styles$1.inlineContent
|
|
33441
34234
|
}, /*#__PURE__*/React.createElement(SparkleIcon, {
|
|
33442
|
-
className: styles.sparkle,
|
|
34235
|
+
className: styles$1.sparkle,
|
|
33443
34236
|
fill: "currentColor"
|
|
33444
34237
|
}), /*#__PURE__*/React.createElement("span", {
|
|
33445
|
-
className: styles.inlineTitle
|
|
34238
|
+
className: styles$1.inlineTitle
|
|
33446
34239
|
}, title)), /*#__PURE__*/React.createElement("span", {
|
|
33447
|
-
className: styles.action
|
|
34240
|
+
className: styles$1.action
|
|
33448
34241
|
}, /*#__PURE__*/React.createElement("span", {
|
|
33449
|
-
className: styles.actionLabel
|
|
34242
|
+
className: styles$1.actionLabel
|
|
33450
34243
|
}, action), /*#__PURE__*/React.createElement(tapestryReact.Icon, {
|
|
33451
|
-
className: styles.actionChevron,
|
|
34244
|
+
className: styles$1.actionChevron,
|
|
33452
34245
|
name: "general.rightChevron",
|
|
33453
34246
|
size: "14px"
|
|
33454
34247
|
})));
|
|
@@ -33465,9 +34258,9 @@ function AskAIIcon() {
|
|
|
33465
34258
|
const borderA = `${uid}-border-a`;
|
|
33466
34259
|
const borderB = `${uid}-border-b`;
|
|
33467
34260
|
return /*#__PURE__*/React.createElement("span", {
|
|
33468
|
-
className: styles.icon
|
|
34261
|
+
className: styles$1.icon
|
|
33469
34262
|
}, /*#__PURE__*/React.createElement("svg", {
|
|
33470
|
-
className: styles.iconShape,
|
|
34263
|
+
className: styles$1.iconShape,
|
|
33471
34264
|
viewBox: "0 0 32 32",
|
|
33472
34265
|
fill: "none",
|
|
33473
34266
|
"aria-hidden": "true"
|
|
@@ -33638,7 +34431,7 @@ function AskAIIcon() {
|
|
|
33638
34431
|
stopColor: "#D48B91",
|
|
33639
34432
|
stopOpacity: "0"
|
|
33640
34433
|
})))), /*#__PURE__*/React.createElement("svg", {
|
|
33641
|
-
className: styles.iconSparkle,
|
|
34434
|
+
className: styles$1.iconSparkle,
|
|
33642
34435
|
viewBox: "0 0 19.4995 17.9999",
|
|
33643
34436
|
"aria-hidden": "true"
|
|
33644
34437
|
}, /*#__PURE__*/React.createElement("path", {
|
|
@@ -33647,6 +34440,23 @@ function AskAIIcon() {
|
|
|
33647
34440
|
})));
|
|
33648
34441
|
}
|
|
33649
34442
|
|
|
34443
|
+
var styles = {"button":"button-2NVI1","sparkle":"sparkle-ruJGP","label":"label-4NUGB"};
|
|
34444
|
+
|
|
34445
|
+
function AskAIButton({
|
|
34446
|
+
onClick,
|
|
34447
|
+
text = "Ask AI"
|
|
34448
|
+
}) {
|
|
34449
|
+
return /*#__PURE__*/React.createElement("button", {
|
|
34450
|
+
className: styles.button,
|
|
34451
|
+
onClick: () => onClick(),
|
|
34452
|
+
type: "button"
|
|
34453
|
+
}, /*#__PURE__*/React.createElement(SparkleIcon, {
|
|
34454
|
+
className: styles.sparkle
|
|
34455
|
+
}), /*#__PURE__*/React.createElement("span", {
|
|
34456
|
+
className: styles.label
|
|
34457
|
+
}, text));
|
|
34458
|
+
}
|
|
34459
|
+
|
|
33650
34460
|
// The article heading lives in the deepest populated hierarchy level (lvl0 is
|
|
33651
34461
|
// always "Planning Center").
|
|
33652
34462
|
const hitTitle = hierarchy => (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl6) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl5) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl4) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl3) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl2) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl1) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl0) || "";
|
|
@@ -33802,16 +34612,24 @@ const DocumentationSearch = ({
|
|
|
33802
34612
|
// not the path, so updating it is a same-document navigation that doesn't
|
|
33803
34613
|
// reload the iframe, but also lets the route survive a forced reload (e.g.
|
|
33804
34614
|
// Turbo re-parenting the topbar).
|
|
33805
|
-
|
|
34615
|
+
//
|
|
34616
|
+
// `panel` loads picobot as its own toolbar panel rather than inside Helpdesk.
|
|
34617
|
+
function buildPicobotSrc(origin, path, {
|
|
34618
|
+
panel = false
|
|
34619
|
+
} = {}) {
|
|
33806
34620
|
const [rawPath, queryPart] = path.replace(/^\/+/, "").split("?", 2);
|
|
33807
34621
|
const safePath = rawPath.split("/").filter(segment => segment && segment !== "." && segment !== "..").join("/");
|
|
33808
34622
|
const fragment = queryPart !== undefined ? `${safePath}?${queryPart}` : safePath;
|
|
33809
|
-
const
|
|
34623
|
+
const url = new URL("topbar", origin);
|
|
34624
|
+
if (panel) url.searchParams.set("panel", "1");
|
|
34625
|
+
const base = url.toString();
|
|
33810
34626
|
return fragment ? `${base}#${fragment}` : base;
|
|
33811
34627
|
}
|
|
33812
34628
|
|
|
33813
34629
|
const LOAD_TIMEOUT_MS = 3000;
|
|
33814
|
-
function createPicobotIframe(picobotOrigin
|
|
34630
|
+
function createPicobotIframe(picobotOrigin, {
|
|
34631
|
+
panel = false
|
|
34632
|
+
} = {}) {
|
|
33815
34633
|
return /*#__PURE__*/React.forwardRef(function PicobotIframe({
|
|
33816
34634
|
currentPath: iframePath,
|
|
33817
34635
|
onChangePath: onIframeChangePath,
|
|
@@ -33867,7 +34685,9 @@ function createPicobotIframe(picobotOrigin) {
|
|
|
33867
34685
|
(_b = (_a = iframeRef.current) === null || _a === void 0 ? void 0 : _a.contentWindow) === null || _b === void 0 ? void 0 : _b.postMessage(message, picobotOrigin);
|
|
33868
34686
|
}
|
|
33869
34687
|
}), []);
|
|
33870
|
-
const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath
|
|
34688
|
+
const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath, {
|
|
34689
|
+
panel
|
|
34690
|
+
});
|
|
33871
34691
|
return /*#__PURE__*/React.createElement("iframe", {
|
|
33872
34692
|
ref: iframeRef,
|
|
33873
34693
|
src: iframeSrc,
|
|
@@ -33906,22 +34726,28 @@ function HelpdeskComponent({
|
|
|
33906
34726
|
currentPath,
|
|
33907
34727
|
env,
|
|
33908
34728
|
helpdeskConfig,
|
|
33909
|
-
|
|
34729
|
+
isPicobotLoading,
|
|
34730
|
+
onChangePath,
|
|
34731
|
+
openPicobot
|
|
33910
34732
|
}) {
|
|
33911
34733
|
const PicobotIframe = React.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot")), [env]);
|
|
34734
|
+
const loadingFallback = /*#__PURE__*/React.createElement(StackView$1, {
|
|
34735
|
+
flex: 1,
|
|
34736
|
+
axis: "vertical"
|
|
34737
|
+
}, /*#__PURE__*/React.createElement(DrawerHeader, {
|
|
34738
|
+
closeDrawer: closeDrawer
|
|
34739
|
+
}, /*#__PURE__*/React.createElement(StackView$1, {
|
|
34740
|
+
axis: "horizontal",
|
|
34741
|
+
spacing: 1
|
|
34742
|
+
}, /*#__PURE__*/React.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React.createElement(LoadingIndicator$1, null))));
|
|
34743
|
+
// The add-on reads its args once, so mounting before the gate answers
|
|
34744
|
+
// would leave out openPicobot for good.
|
|
34745
|
+
if (isPicobotLoading) return loadingFallback;
|
|
33912
34746
|
return /*#__PURE__*/React.createElement(addOns.UiExtensionFirstPartyComponent, {
|
|
33913
34747
|
grow: 1,
|
|
33914
34748
|
display: "flex",
|
|
33915
34749
|
extensionId: env === "staging" ? "42" : "11",
|
|
33916
|
-
loadingFallback:
|
|
33917
|
-
flex: 1,
|
|
33918
|
-
axis: "vertical"
|
|
33919
|
-
}, /*#__PURE__*/React.createElement(DrawerHeader, {
|
|
33920
|
-
closeDrawer: closeDrawer
|
|
33921
|
-
}, /*#__PURE__*/React.createElement(StackView$1, {
|
|
33922
|
-
axis: "horizontal",
|
|
33923
|
-
spacing: 1
|
|
33924
|
-
}, /*#__PURE__*/React.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React.createElement(LoadingIndicator$1, null)))),
|
|
34750
|
+
loadingFallback: loadingFallback,
|
|
33925
34751
|
additionalComponents: {
|
|
33926
34752
|
AskAIBanner: {
|
|
33927
34753
|
component: AskAIBanner,
|
|
@@ -33941,6 +34767,18 @@ function HelpdeskComponent({
|
|
|
33941
34767
|
}
|
|
33942
34768
|
}
|
|
33943
34769
|
},
|
|
34770
|
+
AskAIButton: {
|
|
34771
|
+
component: AskAIButton,
|
|
34772
|
+
properties: {
|
|
34773
|
+
onClick: {
|
|
34774
|
+
event: false,
|
|
34775
|
+
type: Function
|
|
34776
|
+
},
|
|
34777
|
+
text: {
|
|
34778
|
+
type: String
|
|
34779
|
+
}
|
|
34780
|
+
}
|
|
34781
|
+
},
|
|
33944
34782
|
CalendlyWidget: {
|
|
33945
34783
|
component: CalendlyWidget,
|
|
33946
34784
|
properties: {
|
|
@@ -34019,7 +34857,11 @@ function HelpdeskComponent({
|
|
|
34019
34857
|
currentPageParams: location.search,
|
|
34020
34858
|
currentPath,
|
|
34021
34859
|
helpdeskConfig,
|
|
34022
|
-
onChangePath
|
|
34860
|
+
onChangePath,
|
|
34861
|
+
// Its presence tells helpdesk that Picobot has its own panel.
|
|
34862
|
+
...(openPicobot && {
|
|
34863
|
+
openPicobot
|
|
34864
|
+
})
|
|
34023
34865
|
}
|
|
34024
34866
|
});
|
|
34025
34867
|
}
|
|
@@ -34566,6 +35408,142 @@ const Notifications = {
|
|
|
34566
35408
|
name: "Notifications"
|
|
34567
35409
|
};
|
|
34568
35410
|
|
|
35411
|
+
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";
|
|
35412
|
+
// Helpdesk reads this context off the email path to open its email form the
|
|
35413
|
+
// same way a handoff inside its own drawer does.
|
|
35414
|
+
function emailPathWithContext(context = {}) {
|
|
35415
|
+
const params = new URLSearchParams();
|
|
35416
|
+
for (const key of ["conversationId", "source", "turnIndex"]) {
|
|
35417
|
+
const value = context[key];
|
|
35418
|
+
if (value !== null && value !== undefined) params.set(key, String(value));
|
|
35419
|
+
}
|
|
35420
|
+
return `email?${params}`;
|
|
35421
|
+
}
|
|
35422
|
+
function PicobotComponent({
|
|
35423
|
+
clearPending,
|
|
35424
|
+
closeDrawer,
|
|
35425
|
+
currentPath,
|
|
35426
|
+
env,
|
|
35427
|
+
onChangePath,
|
|
35428
|
+
openTool,
|
|
35429
|
+
pending
|
|
35430
|
+
}) {
|
|
35431
|
+
const PicobotIframe = React.useMemo(() => createPicobotIframe(pcoUrl(env)("picobot"), {
|
|
35432
|
+
panel: true
|
|
35433
|
+
}), [env]);
|
|
35434
|
+
const picobotRef = React.useRef(null);
|
|
35435
|
+
const [isPicobotReady, setIsPicobotReady] = React.useState(false);
|
|
35436
|
+
// Picobot has no listener until it sends READY, so openPicobot's draft or
|
|
35437
|
+
// conversation waits for it.
|
|
35438
|
+
React.useEffect(function handOverPendingOncePicobotIsReady() {
|
|
35439
|
+
var _a, _b;
|
|
35440
|
+
if (!pending || !isPicobotReady) return;
|
|
35441
|
+
const {
|
|
35442
|
+
conversationId,
|
|
35443
|
+
draft,
|
|
35444
|
+
ticketExternalId
|
|
35445
|
+
} = pending;
|
|
35446
|
+
if (conversationId) {
|
|
35447
|
+
(_a = picobotRef.current) === null || _a === void 0 ? void 0 : _a.postMessage({
|
|
35448
|
+
payload: {
|
|
35449
|
+
conversation_id: conversationId,
|
|
35450
|
+
// Picobot asks whether this resolved the ticket, and closes it.
|
|
35451
|
+
...(ticketExternalId && {
|
|
35452
|
+
external_id: ticketExternalId,
|
|
35453
|
+
source: "ticket_submitted"
|
|
35454
|
+
})
|
|
35455
|
+
},
|
|
35456
|
+
type: "LOAD_CONVERSATION"
|
|
35457
|
+
});
|
|
35458
|
+
} else if (draft) {
|
|
35459
|
+
// Not both: Picobot cancels a conversation load when a prefill
|
|
35460
|
+
// starts a new conversation.
|
|
35461
|
+
(_b = picobotRef.current) === null || _b === void 0 ? void 0 : _b.postMessage({
|
|
35462
|
+
payload: {
|
|
35463
|
+
source: "email_form",
|
|
35464
|
+
text: draft
|
|
35465
|
+
},
|
|
35466
|
+
type: "PREFILL_COMPOSER"
|
|
35467
|
+
});
|
|
35468
|
+
}
|
|
35469
|
+
clearPending();
|
|
35470
|
+
}, [pending, isPicobotReady, clearPending]);
|
|
35471
|
+
const [iframeErrored, setIframeErrored] = React.useState(false);
|
|
35472
|
+
const [mountKey, setMountKey] = React.useState(0);
|
|
35473
|
+
const handleRetry = React.useCallback(() => {
|
|
35474
|
+
setIframeErrored(false);
|
|
35475
|
+
setIsPicobotReady(false);
|
|
35476
|
+
setMountKey(key => key + 1);
|
|
35477
|
+
}, []);
|
|
35478
|
+
const handleMessage = React.useCallback(data => {
|
|
35479
|
+
var _a;
|
|
35480
|
+
const message = data;
|
|
35481
|
+
switch (message === null || message === void 0 ? void 0 : message.type) {
|
|
35482
|
+
case "CLOSE_DRAWER":
|
|
35483
|
+
return closeDrawer();
|
|
35484
|
+
case "READY":
|
|
35485
|
+
return setIsPicobotReady(true);
|
|
35486
|
+
case "PICOBOT_SUPPORT_HANDOFF":
|
|
35487
|
+
return openTool("helpdesk", ((_a = message.payload) === null || _a === void 0 ? void 0 : _a.method) === "zoom" ? "phone" : emailPathWithContext(message.payload));
|
|
35488
|
+
case "IFRAME_LOAD_ERROR":
|
|
35489
|
+
console.warn("topbar: Picobot iframe failed to load");
|
|
35490
|
+
return setIframeErrored(true);
|
|
35491
|
+
}
|
|
35492
|
+
}, [closeDrawer, openTool]);
|
|
35493
|
+
if (iframeErrored) {
|
|
35494
|
+
return /*#__PURE__*/React.createElement(LoadError, {
|
|
35495
|
+
onContactSupport: () => openTool("helpdesk", "email"),
|
|
35496
|
+
onRetry: handleRetry
|
|
35497
|
+
});
|
|
35498
|
+
}
|
|
35499
|
+
return /*#__PURE__*/React.createElement(PicobotIframe, {
|
|
35500
|
+
key: mountKey,
|
|
35501
|
+
ref: picobotRef,
|
|
35502
|
+
currentPath: currentPath,
|
|
35503
|
+
onChangePath: onChangePath,
|
|
35504
|
+
onMessage: handleMessage
|
|
35505
|
+
});
|
|
35506
|
+
}
|
|
35507
|
+
function LoadError({
|
|
35508
|
+
onContactSupport,
|
|
35509
|
+
onRetry
|
|
35510
|
+
}) {
|
|
35511
|
+
return /*#__PURE__*/React.createElement(tapestryReact.StackView, {
|
|
35512
|
+
axis: "vertical",
|
|
35513
|
+
flex: 1,
|
|
35514
|
+
alignment: "center",
|
|
35515
|
+
distribution: "center",
|
|
35516
|
+
padding: 3,
|
|
35517
|
+
spacing: 1
|
|
35518
|
+
}, /*#__PURE__*/React.createElement(tapestryReact.Heading, {
|
|
35519
|
+
level: 2
|
|
35520
|
+
}, "Picobot is temporarily unavailable"), /*#__PURE__*/React.createElement(tapestryReact.Text, {
|
|
35521
|
+
size: 5,
|
|
35522
|
+
color: "var(--t-text-color-secondary)"
|
|
35523
|
+
}, "Try again or contact support."), /*#__PURE__*/React.createElement(tapestryReact.Box, {
|
|
35524
|
+
paddingTop: 2
|
|
35525
|
+
}, /*#__PURE__*/React.createElement(tapestryReact.Button, {
|
|
35526
|
+
onClick: onRetry,
|
|
35527
|
+
iconLeft: "general.refresh",
|
|
35528
|
+
theme: "primary",
|
|
35529
|
+
variant: "fill",
|
|
35530
|
+
title: "Retry"
|
|
35531
|
+
})), /*#__PURE__*/React.createElement(tapestryReact.Button, {
|
|
35532
|
+
onClick: onContactSupport,
|
|
35533
|
+
variant: "naked",
|
|
35534
|
+
theme: "primary",
|
|
35535
|
+
title: "Contact support"
|
|
35536
|
+
}));
|
|
35537
|
+
}
|
|
35538
|
+
const Picobot = {
|
|
35539
|
+
component: PicobotComponent,
|
|
35540
|
+
icons: {
|
|
35541
|
+
staticDotPath: SPARKLE_PATH,
|
|
35542
|
+
staticPath: SPARKLE_PATH
|
|
35543
|
+
},
|
|
35544
|
+
name: "Picobot"
|
|
35545
|
+
};
|
|
35546
|
+
|
|
34569
35547
|
const {
|
|
34570
35548
|
LoadingIndicator,
|
|
34571
35549
|
StackView
|
|
@@ -34961,6 +35939,41 @@ const useChatConfig = ({
|
|
|
34961
35939
|
};
|
|
34962
35940
|
};
|
|
34963
35941
|
|
|
35942
|
+
// Picobot's API rejects requests without a version header, and apiRequest
|
|
35943
|
+
// sets its own headers, so the version rides in on the fetch.
|
|
35944
|
+
const picobotFetch = (url, opts) => defaultFetch(url, {
|
|
35945
|
+
...opts,
|
|
35946
|
+
headers: {
|
|
35947
|
+
...(opts === null || opts === void 0 ? void 0 : opts.headers),
|
|
35948
|
+
"X-PCO-API-Version": "2026-08-11"
|
|
35949
|
+
}
|
|
35950
|
+
});
|
|
35951
|
+
// Routes wait on the gate, so a slow Picobot can't hold them up for long.
|
|
35952
|
+
const GATE_TIMEOUT_MS = 3000;
|
|
35953
|
+
const usePicobotConfig = ({
|
|
35954
|
+
isEnabled
|
|
35955
|
+
}) => {
|
|
35956
|
+
// Undefined until Picobot answers.
|
|
35957
|
+
const [panelEnabled, setPanelEnabled] = React.useState();
|
|
35958
|
+
React.useEffect(() => {
|
|
35959
|
+
if (!isEnabled) return undefined;
|
|
35960
|
+
// Whatever settles first wins, so a late answer can't reopen the gate.
|
|
35961
|
+
const settle = enabled => setPanelEnabled(current => current !== null && current !== void 0 ? current : enabled);
|
|
35962
|
+
const timeout = setTimeout(() => settle(false), GATE_TIMEOUT_MS);
|
|
35963
|
+
apiRequest(picobotFetch, `${window.location.origin}/~api/picobot/v2/me`).then(({
|
|
35964
|
+
json
|
|
35965
|
+
}) => {
|
|
35966
|
+
var _a, _b;
|
|
35967
|
+
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);
|
|
35968
|
+
}).catch(() => settle(false)).finally(() => clearTimeout(timeout));
|
|
35969
|
+
return () => clearTimeout(timeout);
|
|
35970
|
+
}, [isEnabled]);
|
|
35971
|
+
return {
|
|
35972
|
+
displayPicobotIcon: panelEnabled === true,
|
|
35973
|
+
isLoading: isEnabled && panelEnabled === undefined
|
|
35974
|
+
};
|
|
35975
|
+
};
|
|
35976
|
+
|
|
34964
35977
|
const Profile = {
|
|
34965
35978
|
component: UserProfile,
|
|
34966
35979
|
icons: {
|
|
@@ -34971,10 +35984,16 @@ const Profile = {
|
|
|
34971
35984
|
};
|
|
34972
35985
|
|
|
34973
35986
|
const DURATION = 100;
|
|
35987
|
+
// Exempt paths match regardless of query, so `email?conversationId=…` is
|
|
35988
|
+
// exempt along with `email`.
|
|
34974
35989
|
function isToolPathExemptFromStorage(toolName, path) {
|
|
34975
35990
|
var _a, _b;
|
|
34976
35991
|
const tool = toolsRouteMap[toolName];
|
|
34977
|
-
|
|
35992
|
+
const [pathWithoutQuery] = path.split("?");
|
|
35993
|
+
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;
|
|
35994
|
+
}
|
|
35995
|
+
function isConversationPath(path) {
|
|
35996
|
+
return Boolean(path === null || path === void 0 ? void 0 : path.startsWith("conversations/"));
|
|
34978
35997
|
}
|
|
34979
35998
|
function scrollToolDrawerToTop(toolName) {
|
|
34980
35999
|
var _a;
|
|
@@ -34987,6 +36006,7 @@ const TOOLS_MAP = {
|
|
|
34987
36006
|
Chat,
|
|
34988
36007
|
Helpdesk,
|
|
34989
36008
|
Notifications,
|
|
36009
|
+
Picobot,
|
|
34990
36010
|
Profile,
|
|
34991
36011
|
Tasks
|
|
34992
36012
|
};
|
|
@@ -35014,7 +36034,8 @@ function Toolbar({
|
|
|
35014
36034
|
});
|
|
35015
36035
|
const {
|
|
35016
36036
|
currentContent,
|
|
35017
|
-
open
|
|
36037
|
+
open,
|
|
36038
|
+
pending
|
|
35018
36039
|
} = state;
|
|
35019
36040
|
const mounted = !!currentContent;
|
|
35020
36041
|
const storeData = useStore();
|
|
@@ -35027,6 +36048,15 @@ function Toolbar({
|
|
|
35027
36048
|
...(env === "development" ? _storeDataOverride !== null && _storeDataOverride !== void 0 ? _storeDataOverride : {} : {})
|
|
35028
36049
|
};
|
|
35029
36050
|
const updateStore = useUpdateStore();
|
|
36051
|
+
const {
|
|
36052
|
+
displayPicobotIcon,
|
|
36053
|
+
isLoading: isPicobotGateLoading
|
|
36054
|
+
} = usePicobotConfig({
|
|
36055
|
+
isEnabled: Boolean(userId)
|
|
36056
|
+
});
|
|
36057
|
+
// Picobot is a kill-switched tool: until its gate says the panel is
|
|
36058
|
+
// enabled, its routes are held, and if it never does, they're ignored.
|
|
36059
|
+
const routableTool = React.useCallback(name => name === Picobot.name.toLowerCase() && !displayPicobotIcon ? undefined : toolsRouteMap[name], [displayPicobotIcon]);
|
|
35030
36060
|
React.useEffect(() => {
|
|
35031
36061
|
if (open) {
|
|
35032
36062
|
document.body.style.overflow = "hidden";
|
|
@@ -35114,7 +36144,9 @@ function Toolbar({
|
|
|
35114
36144
|
});
|
|
35115
36145
|
}, [clearRoute]);
|
|
35116
36146
|
React.useEffect(() => {
|
|
35117
|
-
if (!toolbarFragmentRoute)
|
|
36147
|
+
if (!toolbarFragmentRoute || !routableTool(toolbarFragmentRoute.tool)) {
|
|
36148
|
+
return;
|
|
36149
|
+
}
|
|
35118
36150
|
const isExempt = isToolPathExemptFromStorage(toolbarFragmentRoute.tool, toolbarFragmentRoute.path);
|
|
35119
36151
|
if (isExempt) return;
|
|
35120
36152
|
const nextState = {
|
|
@@ -35124,12 +36156,32 @@ function Toolbar({
|
|
|
35124
36156
|
if (!shallowCompare(nextState, toolStateCookie)) {
|
|
35125
36157
|
setToolStateCookie(nextState);
|
|
35126
36158
|
}
|
|
35127
|
-
}, [toolbarFragmentRoute, toolStateCookie]);
|
|
36159
|
+
}, [toolbarFragmentRoute, toolStateCookie, routableTool]);
|
|
35128
36160
|
React.useEffect(() => {
|
|
35129
|
-
|
|
35130
|
-
|
|
36161
|
+
// Helpdesk conversations may belong to Picobot, so they wait for its gate
|
|
36162
|
+
// rather than flashing open in Helpdesk first.
|
|
36163
|
+
const mayRedirectToPicobot = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path) && (isPicobotGateLoading || displayPicobotIcon);
|
|
36164
|
+
const tool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && !mayRedirectToPicobot && routableTool(toolbarFragmentRoute.tool);
|
|
36165
|
+
if (tool && (currentContent !== tool || !open)) {
|
|
36166
|
+
setToolAndOpenDrawer(tool);
|
|
35131
36167
|
}
|
|
35132
|
-
}, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, currentContent, open]);
|
|
36168
|
+
}, [setToolAndOpenDrawer, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path, currentContent, open, routableTool, isPicobotGateLoading, displayPicobotIcon]);
|
|
36169
|
+
const openTool = React.useCallback((name, path, pendingValue) => {
|
|
36170
|
+
const tool = routableTool(name.toLowerCase());
|
|
36171
|
+
if (!tool) return;
|
|
36172
|
+
dispatch({
|
|
36173
|
+
payload: pendingValue === undefined ? null : {
|
|
36174
|
+
tool: tool.name,
|
|
36175
|
+
value: pendingValue
|
|
36176
|
+
},
|
|
36177
|
+
type: ActionType.setPending
|
|
36178
|
+
});
|
|
36179
|
+
onChangeToolbarFragmentRoute(tool.name.toLowerCase(), path);
|
|
36180
|
+
scrollToolDrawerToTop(tool.name);
|
|
36181
|
+
}, [onChangeToolbarFragmentRoute, routableTool]);
|
|
36182
|
+
const clearPending = React.useCallback(() => dispatch({
|
|
36183
|
+
type: ActionType.clearPending
|
|
36184
|
+
}), []);
|
|
35133
36185
|
const toggleDrawer = content => {
|
|
35134
36186
|
if (!open) {
|
|
35135
36187
|
return openDrawer(content);
|
|
@@ -35146,12 +36198,52 @@ function Toolbar({
|
|
|
35146
36198
|
joltPromise: storeData === null || storeData === void 0 ? void 0 : storeData.joltPromise,
|
|
35147
36199
|
userId
|
|
35148
36200
|
});
|
|
35149
|
-
|
|
36201
|
+
// Picobot conversations moved from Helpdesk to the Picobot panel, so
|
|
36202
|
+
// send old links and saved paths there once the panel is enabled.
|
|
36203
|
+
React.useEffect(() => {
|
|
36204
|
+
if (!displayPicobotIcon) return;
|
|
36205
|
+
if ((toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) === Helpdesk.name.toLowerCase() && isConversationPath(toolbarFragmentRoute.path)) {
|
|
36206
|
+
onChangeToolbarFragmentRoute(Picobot.name.toLowerCase(), toolbarFragmentRoute.path);
|
|
36207
|
+
}
|
|
36208
|
+
}, [displayPicobotIcon, toolbarFragmentRoute, onChangeToolbarFragmentRoute]);
|
|
36209
|
+
React.useEffect(() => {
|
|
36210
|
+
if (!displayPicobotIcon) return;
|
|
36211
|
+
const {
|
|
36212
|
+
helpdesk,
|
|
36213
|
+
picobot
|
|
36214
|
+
} = toolStateCookie;
|
|
36215
|
+
if (!isConversationPath(helpdesk)) return;
|
|
36216
|
+
setToolStateCookie({
|
|
36217
|
+
...toolStateCookie,
|
|
36218
|
+
helpdesk: "",
|
|
36219
|
+
picobot: picobot || helpdesk
|
|
36220
|
+
});
|
|
36221
|
+
}, [displayPicobotIcon, toolStateCookie]);
|
|
36222
|
+
const openPicobot = React.useCallback(options => {
|
|
36223
|
+
var _a;
|
|
36224
|
+
// Moving an open Picobot off its path reloads or navigates the iframe,
|
|
36225
|
+
// which would race the options it's being handed.
|
|
36226
|
+
const path = currentContent === Picobot ? (_a = toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path) !== null && _a !== void 0 ? _a : "" : "";
|
|
36227
|
+
openTool(Picobot.name, path, options);
|
|
36228
|
+
}, [openTool, currentContent, toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path]);
|
|
36229
|
+
React.useEffect(() => {
|
|
36230
|
+
if (!displayPicobotIcon) return undefined;
|
|
36231
|
+
window.topbar = window.topbar || {};
|
|
36232
|
+
window.topbar.openPicobot = openPicobot;
|
|
36233
|
+
return () => {
|
|
36234
|
+
delete window.topbar.openPicobot;
|
|
36235
|
+
};
|
|
36236
|
+
}, [displayPicobotIcon, openPicobot]);
|
|
36237
|
+
const TOOLS = [Notifications, Tasks, displayChatIcon && Chat, displayPicobotIcon && Picobot, isPcoTestHost() && DeveloperTools].filter(Boolean);
|
|
35150
36238
|
const handleToolOnClick = tool => {
|
|
35151
36239
|
toggleDrawer(tool);
|
|
35152
36240
|
scrollToolDrawerToTop(tool.name);
|
|
35153
36241
|
};
|
|
35154
36242
|
const toolSpecificProps = {
|
|
36243
|
+
Helpdesk: {
|
|
36244
|
+
isPicobotLoading: isPicobotGateLoading,
|
|
36245
|
+
openPicobot: displayPicobotIcon ? openPicobot : undefined
|
|
36246
|
+
},
|
|
35155
36247
|
Profile: {
|
|
35156
36248
|
appName,
|
|
35157
36249
|
closeDrawer,
|
|
@@ -35174,6 +36266,7 @@ function Toolbar({
|
|
|
35174
36266
|
const routeIsForCurrentTool = (toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.tool) && toolsRouteMap[toolbarFragmentRoute.tool] === currentContent;
|
|
35175
36267
|
const currentToolProps = {
|
|
35176
36268
|
appName,
|
|
36269
|
+
clearPending,
|
|
35177
36270
|
closeDrawer,
|
|
35178
36271
|
currentPath: routeIsForCurrentTool ? toolbarFragmentRoute === null || toolbarFragmentRoute === void 0 ? void 0 : toolbarFragmentRoute.path : null,
|
|
35179
36272
|
env,
|
|
@@ -35182,6 +36275,8 @@ function Toolbar({
|
|
|
35182
36275
|
joltPromise,
|
|
35183
36276
|
onChangePath,
|
|
35184
36277
|
openDrawer: () => currentContent && openDrawer(currentContent),
|
|
36278
|
+
openTool,
|
|
36279
|
+
pending: (pending === null || pending === void 0 ? void 0 : pending.tool) === (currentContent === null || currentContent === void 0 ? void 0 : currentContent.name) ? pending === null || pending === void 0 ? void 0 : pending.value : undefined,
|
|
35185
36280
|
userId,
|
|
35186
36281
|
...(currentContent ? toolSpecificProps[currentContent.name] : {})
|
|
35187
36282
|
};
|
|
@@ -35192,38 +36287,37 @@ function Toolbar({
|
|
|
35192
36287
|
return /*#__PURE__*/React.createElement(tapestryReact.ThemeProvider, {
|
|
35193
36288
|
theme: topbarTheme
|
|
35194
36289
|
}, /*#__PURE__*/React.createElement(addOns.UiExtensionDataContext, null, /*#__PURE__*/React.createElement("div", {
|
|
35195
|
-
className: styles$
|
|
36290
|
+
className: styles$2["toolbar"],
|
|
35196
36291
|
"data-pendo": "toolbar-profile-support-wrapper"
|
|
35197
36292
|
}, /*#__PURE__*/React.createElement(ToolbarButton, {
|
|
35198
36293
|
"aria-label": `Open ${userName}’s profile drawer`,
|
|
35199
36294
|
onClick: () => toggleDrawer(Profile),
|
|
35200
36295
|
"data-pendo": "toolbar-profile",
|
|
35201
|
-
className: styles$
|
|
36296
|
+
className: styles$2["toolbar-button--avatar"]
|
|
35202
36297
|
}, /*#__PURE__*/React.createElement(Avatar, {
|
|
35203
36298
|
env: env,
|
|
35204
36299
|
url: userAvatarPath
|
|
35205
36300
|
})), /*#__PURE__*/React.createElement(ToolbarButton, {
|
|
35206
|
-
className: styles$
|
|
36301
|
+
className: styles$2["toolbar-button--home"],
|
|
35207
36302
|
onClick: () => closeDrawer()
|
|
35208
36303
|
}, /*#__PURE__*/React.createElement(tapestryReact.Icon, {
|
|
35209
36304
|
name: "toolbar.planningCenter",
|
|
35210
36305
|
size: "24px"
|
|
35211
36306
|
})), [HelpDeskTool, ...TOOLS].map(tool => {
|
|
36307
|
+
var _a;
|
|
35212
36308
|
const current = open && currentContent === tool;
|
|
35213
36309
|
const indicator = indicatorState[tool.name];
|
|
35214
36310
|
return /*#__PURE__*/React.createElement(ToolbarButton, {
|
|
35215
36311
|
current: current,
|
|
35216
36312
|
onClick: () => handleToolOnClick(tool),
|
|
35217
36313
|
id: getDataPendoAttribute(tool.name),
|
|
35218
|
-
"aria-label": `Open ${tool.name} drawer`,
|
|
36314
|
+
"aria-label": `Open ${(_a = tool.label) !== null && _a !== void 0 ? _a : tool.name} drawer`,
|
|
35219
36315
|
"data-pendo": getDataPendoAttribute(tool.name),
|
|
35220
36316
|
key: tool.name
|
|
35221
|
-
},
|
|
35222
|
-
|
|
35223
|
-
|
|
35224
|
-
|
|
35225
|
-
dot: true
|
|
35226
|
-
})));
|
|
36317
|
+
}, /*#__PURE__*/React.createElement(ToolbarToolIcon, {
|
|
36318
|
+
icons: tool.icons,
|
|
36319
|
+
dot: !!indicator
|
|
36320
|
+
}));
|
|
35227
36321
|
})), /*#__PURE__*/React.createElement(DrawerWrap, {
|
|
35228
36322
|
isVisible: open
|
|
35229
36323
|
}, mounted && (/*#__PURE__*/React.createElement(Drawer, {
|
|
@@ -35241,12 +36335,25 @@ const ToolbarButton = ({
|
|
|
35241
36335
|
}) => {
|
|
35242
36336
|
return /*#__PURE__*/React.createElement("button", {
|
|
35243
36337
|
type: "button",
|
|
35244
|
-
className: classnames(styles$
|
|
36338
|
+
className: classnames(styles$2["toolbar-button"], className, current && styles$2["is-current"]),
|
|
35245
36339
|
onClick: onClick,
|
|
35246
36340
|
"data-toolbar-link": "true",
|
|
35247
36341
|
...restProps
|
|
35248
36342
|
}, children);
|
|
35249
36343
|
};
|
|
36344
|
+
function ToolbarToolIcon({
|
|
36345
|
+
dot,
|
|
36346
|
+
icons
|
|
36347
|
+
}) {
|
|
36348
|
+
if ("component" in icons) {
|
|
36349
|
+
return /*#__PURE__*/React.createElement(icons.component, null);
|
|
36350
|
+
} else {
|
|
36351
|
+
return /*#__PURE__*/React.createElement(ToolbarButtonIcon, {
|
|
36352
|
+
path: dot ? icons.staticDotPath : icons.staticPath,
|
|
36353
|
+
dot: dot
|
|
36354
|
+
});
|
|
36355
|
+
}
|
|
36356
|
+
}
|
|
35250
36357
|
function ToolbarButtonIcon({
|
|
35251
36358
|
dot,
|
|
35252
36359
|
path
|