@planningcenter/topbar 12.3.0-qa-899.0 → 12.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/topbar.css +26 -0
- package/dist/topbar.css.map +1 -1
- package/dist/topbar.js +872 -170
- package/dist/topbar.js.map +1 -1
- package/dist/topbar.modern.js +873 -171
- package/dist/topbar.modern.js.map +1 -1
- package/dist/topbar.module.js +873 -171
- package/dist/topbar.module.js.map +1 -1
- package/dist/topbar.umd.js +872 -170
- package/dist/topbar.umd.js.map +1 -1
- package/package.json +2 -3
package/dist/topbar.modern.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Link, Button, Popover, PopoverTrigger, PopoverContent, Modal } from '@planningcenter/tapestry';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import React__default, { useState, useEffect, useMemo, useLayoutEffect, useContext, createContext, useCallback, useRef } from 'react';
|
|
4
|
-
import pcoUrl$1, { pcoApiUrl } from '@planningcenter/url';
|
|
4
|
+
import pcoUrl$1, { pcoUrl as pcoUrl$2, pcoApiUrl } from '@planningcenter/url';
|
|
5
5
|
import * as general from '@planningcenter/icons/paths/general';
|
|
6
6
|
import Jolt from '@planningcenter/jolt-client';
|
|
7
7
|
import { useSyncExternalStore } from 'use-sync-external-store/shim';
|
|
@@ -17,7 +17,7 @@ import { useSearchBox, useHits, InstantSearch, Configure } from 'react-instantse
|
|
|
17
17
|
import { TransitionGroup, Transition } from 'react-transition-group';
|
|
18
18
|
import datetimeFmt from '@planningcenter/datetime-fmt';
|
|
19
19
|
|
|
20
|
-
var styles$
|
|
20
|
+
var styles$l = {"accessibility-toolbar":"accessibility-toolbar-jgjUG"};
|
|
21
21
|
|
|
22
22
|
const AccessibilityToolbar = ({
|
|
23
23
|
children
|
|
@@ -29,7 +29,7 @@ const AccessibilityToolbar = ({
|
|
|
29
29
|
(_a = document.getElementById("main-content")) === null || _a === void 0 ? void 0 : _a.focus();
|
|
30
30
|
};
|
|
31
31
|
return /*#__PURE__*/React__default.createElement("div", {
|
|
32
|
-
className: styles$
|
|
32
|
+
className: styles$l["accessibility-toolbar"]
|
|
33
33
|
}, /*#__PURE__*/React__default.createElement(Link, {
|
|
34
34
|
href: "#main-content",
|
|
35
35
|
kind: "primary",
|
|
@@ -346,11 +346,11 @@ const X = ({
|
|
|
346
346
|
fill: fill
|
|
347
347
|
})));
|
|
348
348
|
|
|
349
|
-
var styles$
|
|
349
|
+
var styles$k = {"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"};
|
|
350
350
|
|
|
351
351
|
function AnnouncementIcon() {
|
|
352
352
|
return /*#__PURE__*/React__default.createElement("svg", {
|
|
353
|
-
className: styles$
|
|
353
|
+
className: styles$k["announcement-icon"],
|
|
354
354
|
width: "16",
|
|
355
355
|
height: "16",
|
|
356
356
|
viewBox: "0 0 16 16",
|
|
@@ -475,7 +475,7 @@ let StyledAnnouncement$1 = class StyledAnnouncement extends React__default.Compo
|
|
|
475
475
|
} = this.props;
|
|
476
476
|
const isPromo = variant === "promo";
|
|
477
477
|
return /*#__PURE__*/React__default.createElement("div", {
|
|
478
|
-
className: classnames(styles$
|
|
478
|
+
className: classnames(styles$k["announcement"], styles$k[`announcement--${variant !== null && variant !== void 0 ? variant : "platform"}`]),
|
|
479
479
|
"data-announcement-variant": variant !== null && variant !== void 0 ? variant : "platform",
|
|
480
480
|
style: isPromo ? {
|
|
481
481
|
backgroundImage: promoBackgroundUrl,
|
|
@@ -484,7 +484,7 @@ let StyledAnnouncement$1 = class StyledAnnouncement extends React__default.Compo
|
|
|
484
484
|
backgroundSize: "cover"
|
|
485
485
|
} : undefined
|
|
486
486
|
}, !isPromo && /*#__PURE__*/React__default.createElement(AnnouncementIcon, null), /*#__PURE__*/React__default.createElement("div", {
|
|
487
|
-
className: styles$
|
|
487
|
+
className: styles$k["announcement-content"],
|
|
488
488
|
style: style,
|
|
489
489
|
...platformProps
|
|
490
490
|
}));
|
|
@@ -533,7 +533,7 @@ function StyledDismissButton$1({
|
|
|
533
533
|
const isPromo = variant === "promo";
|
|
534
534
|
return /*#__PURE__*/React__default.createElement("button", {
|
|
535
535
|
type: "button",
|
|
536
|
-
className: classnames(styles$
|
|
536
|
+
className: classnames(styles$k.dismiss, isPromo && styles$k["dismiss--promo"]),
|
|
537
537
|
...props
|
|
538
538
|
}, /*#__PURE__*/React__default.createElement(X, {
|
|
539
539
|
fill: isPromo ? "currentColor" : "white",
|
|
@@ -613,9 +613,9 @@ class StyledAnnouncement extends React__default.Component {
|
|
|
613
613
|
...platformProps
|
|
614
614
|
} = this.props;
|
|
615
615
|
return /*#__PURE__*/React__default.createElement("div", {
|
|
616
|
-
className: styles$
|
|
616
|
+
className: styles$k["announcement"]
|
|
617
617
|
}, /*#__PURE__*/React__default.createElement(AnnouncementIcon, null), /*#__PURE__*/React__default.createElement("div", {
|
|
618
|
-
className: styles$
|
|
618
|
+
className: styles$k["announcement-content"],
|
|
619
619
|
style: style,
|
|
620
620
|
...platformProps
|
|
621
621
|
}));
|
|
@@ -626,7 +626,7 @@ function StyledDismissButton({
|
|
|
626
626
|
}) {
|
|
627
627
|
return /*#__PURE__*/React__default.createElement("button", {
|
|
628
628
|
type: "button",
|
|
629
|
-
className: styles$
|
|
629
|
+
className: styles$k.dismiss,
|
|
630
630
|
...props
|
|
631
631
|
}, /*#__PURE__*/React__default.createElement(X, {
|
|
632
632
|
fill: "white",
|
|
@@ -1748,7 +1748,7 @@ function MonoAppIcon({
|
|
|
1748
1748
|
})) : (/*#__PURE__*/React__default.createElement("div", null, "appName not supported")));
|
|
1749
1749
|
}
|
|
1750
1750
|
|
|
1751
|
-
var styles$
|
|
1751
|
+
var styles$j = {"icon-slot":"icon-slot-m3ZIk","name":"name-UNqbW"};
|
|
1752
1752
|
|
|
1753
1753
|
const AppRow = ({
|
|
1754
1754
|
appName,
|
|
@@ -1757,24 +1757,24 @@ const AppRow = ({
|
|
|
1757
1757
|
}) => {
|
|
1758
1758
|
if (appName === "Accounts") {
|
|
1759
1759
|
return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("span", {
|
|
1760
|
-
className: styles$
|
|
1760
|
+
className: styles$j["icon-slot"]
|
|
1761
1761
|
}, /*#__PURE__*/React__default.createElement(MonoAppIcon, {
|
|
1762
1762
|
appName: "Accounts",
|
|
1763
1763
|
colors: colors,
|
|
1764
1764
|
size: iconSize
|
|
1765
1765
|
})), /*#__PURE__*/React__default.createElement("span", {
|
|
1766
|
-
className: styles$
|
|
1766
|
+
className: styles$j.name
|
|
1767
1767
|
}, "Account settings"));
|
|
1768
1768
|
}
|
|
1769
1769
|
return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(ColorAppIcon, {
|
|
1770
1770
|
appName: appName.replace(/[\s-]/, ""),
|
|
1771
1771
|
size: iconSize
|
|
1772
1772
|
}), /*#__PURE__*/React__default.createElement("span", {
|
|
1773
|
-
className: styles$
|
|
1773
|
+
className: styles$j.name
|
|
1774
1774
|
}, appName));
|
|
1775
1775
|
};
|
|
1776
1776
|
|
|
1777
|
-
var styles$
|
|
1777
|
+
var styles$i = {"button":"button-XMbeQ","label":"label-884of","chevron":"chevron-RRg9k"};
|
|
1778
1778
|
|
|
1779
1779
|
const AppsButton = /*#__PURE__*/React__default.forwardRef(({
|
|
1780
1780
|
appName,
|
|
@@ -1786,10 +1786,10 @@ const AppsButton = /*#__PURE__*/React__default.forwardRef(({
|
|
|
1786
1786
|
...restProps,
|
|
1787
1787
|
ref: ref,
|
|
1788
1788
|
"aria-label": `App switcher: ${appName}`,
|
|
1789
|
-
className: styles$
|
|
1789
|
+
className: styles$i.button,
|
|
1790
1790
|
kind: "neutral-inline",
|
|
1791
1791
|
label: /*#__PURE__*/React__default.createElement("span", {
|
|
1792
|
-
className: styles$
|
|
1792
|
+
className: styles$i.label
|
|
1793
1793
|
}, isAccounts ? "Account settings" : appName),
|
|
1794
1794
|
prefix: isAccounts ? (/*#__PURE__*/React__default.createElement(MonoAppIcon, {
|
|
1795
1795
|
appName: "Accounts",
|
|
@@ -1800,13 +1800,13 @@ const AppsButton = /*#__PURE__*/React__default.forwardRef(({
|
|
|
1800
1800
|
size: 22
|
|
1801
1801
|
})),
|
|
1802
1802
|
suffix: /*#__PURE__*/React__default.createElement(DisclosureChevronIcon, {
|
|
1803
|
-
className: `${styles$
|
|
1803
|
+
className: `${styles$i.chevron} symbol`
|
|
1804
1804
|
})
|
|
1805
1805
|
});
|
|
1806
1806
|
});
|
|
1807
1807
|
AppsButton.displayName = "AppsButton";
|
|
1808
1808
|
|
|
1809
|
-
var styles$
|
|
1809
|
+
var styles$h = {"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"};
|
|
1810
1810
|
|
|
1811
1811
|
const AppsMenu = ({
|
|
1812
1812
|
appName,
|
|
@@ -1827,8 +1827,8 @@ const AppsMenu = ({
|
|
|
1827
1827
|
}, /*#__PURE__*/React__default.createElement("a", {
|
|
1828
1828
|
"aria-current": isCurrent ? "page" : undefined,
|
|
1829
1829
|
"aria-label": name,
|
|
1830
|
-
className: classnames(styles$
|
|
1831
|
-
[styles$
|
|
1830
|
+
className: classnames(styles$h.link, {
|
|
1831
|
+
[styles$h.current]: isCurrent
|
|
1832
1832
|
}),
|
|
1833
1833
|
"data-pendo": `app-switcher--change-app--${name}`,
|
|
1834
1834
|
"data-turbolinks": false,
|
|
@@ -1844,22 +1844,22 @@ const AppsMenu = ({
|
|
|
1844
1844
|
colors: colors
|
|
1845
1845
|
})), /*#__PURE__*/React__default.createElement(PopoverContent, {
|
|
1846
1846
|
"aria-label": "Switch app",
|
|
1847
|
-
className: styles$
|
|
1847
|
+
className: styles$h["popover-content"],
|
|
1848
1848
|
placement: "bottom start"
|
|
1849
1849
|
}, /*#__PURE__*/React__default.createElement("nav", {
|
|
1850
|
-
className: styles$
|
|
1850
|
+
className: styles$h.nav,
|
|
1851
1851
|
"data-pendo": "app-switcher"
|
|
1852
1852
|
}, /*#__PURE__*/React__default.createElement("ul", {
|
|
1853
|
-
className: styles$
|
|
1853
|
+
className: styles$h.list,
|
|
1854
1854
|
role: "list"
|
|
1855
1855
|
}, homeProduct && renderProductLink(homeProduct.name), restProducts.map(({
|
|
1856
1856
|
name
|
|
1857
1857
|
}) => renderProductLink(name)), accountsProduct && (/*#__PURE__*/React__default.createElement("li", {
|
|
1858
|
-
className: styles$
|
|
1858
|
+
className: styles$h["accounts-item"]
|
|
1859
1859
|
}, /*#__PURE__*/React__default.createElement("a", {
|
|
1860
1860
|
"aria-current": appName === accountsProduct.name ? "page" : undefined,
|
|
1861
1861
|
"aria-label": "Account settings",
|
|
1862
|
-
className: classnames(styles$
|
|
1862
|
+
className: classnames(styles$h.link, styles$h["accounts-link"]),
|
|
1863
1863
|
"data-pendo": "app-switcher--change-app--Accounts",
|
|
1864
1864
|
"data-turbolinks": false,
|
|
1865
1865
|
href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name)
|
|
@@ -2067,7 +2067,7 @@ function MonoAppName({
|
|
|
2067
2067
|
})) : (/*#__PURE__*/React__default.createElement("div", null, "appName not supported"));
|
|
2068
2068
|
}
|
|
2069
2069
|
|
|
2070
|
-
var styles$
|
|
2070
|
+
var styles$g = {"unbutton":"unbutton-lEbTR"};
|
|
2071
2071
|
|
|
2072
2072
|
function Unbutton({
|
|
2073
2073
|
className,
|
|
@@ -2076,7 +2076,7 @@ function Unbutton({
|
|
|
2076
2076
|
...nativeProps
|
|
2077
2077
|
}) {
|
|
2078
2078
|
return /*#__PURE__*/React__default.createElement("button", {
|
|
2079
|
-
className: classnames(styles$
|
|
2079
|
+
className: classnames(styles$g.unbutton, className),
|
|
2080
2080
|
style: {
|
|
2081
2081
|
...style
|
|
2082
2082
|
},
|
|
@@ -2085,7 +2085,7 @@ function Unbutton({
|
|
|
2085
2085
|
});
|
|
2086
2086
|
}
|
|
2087
2087
|
|
|
2088
|
-
var styles$
|
|
2088
|
+
var styles$f = {"button":"button-wsipc","button-accounts":"button-accounts-zQmBM","chevron":"chevron-obyBF","is-expanded":"is-expanded-PoCQ9"};
|
|
2089
2089
|
|
|
2090
2090
|
const LegacyAppsButton = ({
|
|
2091
2091
|
appName,
|
|
@@ -2100,8 +2100,8 @@ const LegacyAppsButton = ({
|
|
|
2100
2100
|
"aria-haspopup": "true",
|
|
2101
2101
|
"aria-label": `App switcher: ${appName}`,
|
|
2102
2102
|
onClick: onClick,
|
|
2103
|
-
className: classnames(styles$
|
|
2104
|
-
[styles$
|
|
2103
|
+
className: classnames(styles$f.button, {
|
|
2104
|
+
[styles$f["is-expanded"]]: expanded
|
|
2105
2105
|
}),
|
|
2106
2106
|
type: "button",
|
|
2107
2107
|
style: style,
|
|
@@ -2110,7 +2110,7 @@ const LegacyAppsButton = ({
|
|
|
2110
2110
|
appName: "Accounts",
|
|
2111
2111
|
colors: colors
|
|
2112
2112
|
}), /*#__PURE__*/React__default.createElement("div", {
|
|
2113
|
-
className: styles$
|
|
2113
|
+
className: styles$f["button-accounts"]
|
|
2114
2114
|
}, "Account settings"))) : (/*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(ColorAppIcon, {
|
|
2115
2115
|
appName: appName.replace(/[\s-]/, "")
|
|
2116
2116
|
}), /*#__PURE__*/React__default.createElement(MonoAppName, {
|
|
@@ -2118,7 +2118,7 @@ const LegacyAppsButton = ({
|
|
|
2118
2118
|
key: "mono-app-name",
|
|
2119
2119
|
size: 24
|
|
2120
2120
|
}))), /*#__PURE__*/React__default.createElement(DisclosureChevronIcon, {
|
|
2121
|
-
className: styles$
|
|
2121
|
+
className: styles$f.chevron,
|
|
2122
2122
|
key: "disclosure-chevron-icon"
|
|
2123
2123
|
}));
|
|
2124
2124
|
};
|
|
@@ -2251,7 +2251,7 @@ class Outsider extends React__default.Component {
|
|
|
2251
2251
|
}
|
|
2252
2252
|
}
|
|
2253
2253
|
|
|
2254
|
-
var styles$
|
|
2254
|
+
var styles$e = {"popup-root":"popup-root-SeL5u","popup":"popup-nQXYW","is-visible":"is-visible-3DAUP"};
|
|
2255
2255
|
|
|
2256
2256
|
const Popup = ({
|
|
2257
2257
|
className,
|
|
@@ -2259,8 +2259,8 @@ const Popup = ({
|
|
|
2259
2259
|
isVisible,
|
|
2260
2260
|
...nativeProps
|
|
2261
2261
|
}) => (/*#__PURE__*/React__default.createElement(C, {
|
|
2262
|
-
className: classnames(styles$
|
|
2263
|
-
[styles$
|
|
2262
|
+
className: classnames(styles$e.popup, {
|
|
2263
|
+
[styles$e["is-visible"]]: isVisible
|
|
2264
2264
|
}, className),
|
|
2265
2265
|
...nativeProps
|
|
2266
2266
|
}));
|
|
@@ -2268,11 +2268,11 @@ const Popup = ({
|
|
|
2268
2268
|
const PopupRoot = ({
|
|
2269
2269
|
...nativeProps
|
|
2270
2270
|
}) => (/*#__PURE__*/React__default.createElement("div", {
|
|
2271
|
-
className: styles$
|
|
2271
|
+
className: styles$e["popup-root"],
|
|
2272
2272
|
...nativeProps
|
|
2273
2273
|
}));
|
|
2274
2274
|
|
|
2275
|
-
var styles$
|
|
2275
|
+
var styles$d = {"popup":"popup-bVhSa"};
|
|
2276
2276
|
|
|
2277
2277
|
const LegacyAppsMenu = ({
|
|
2278
2278
|
appName,
|
|
@@ -2310,7 +2310,7 @@ const LegacyAppsMenu = ({
|
|
|
2310
2310
|
cleanup: !visible,
|
|
2311
2311
|
component: Outsider,
|
|
2312
2312
|
onOutsideClick: toggle,
|
|
2313
|
-
className: styles$
|
|
2313
|
+
className: styles$d.popup,
|
|
2314
2314
|
isVisible: visible
|
|
2315
2315
|
}, /*#__PURE__*/React__default.createElement("menu", {
|
|
2316
2316
|
ref: menuRef,
|
|
@@ -3220,7 +3220,97 @@ const useUpdateStore = () => useCallback(updates => {
|
|
|
3220
3220
|
});
|
|
3221
3221
|
}, []);
|
|
3222
3222
|
|
|
3223
|
+
const isMac = () => /Mac|iPhone|iPad|iPod/.test(navigator.platform);
|
|
3224
|
+
const hasCommandKey = event => isMac() ? event.metaKey : event.ctrlKey;
|
|
3225
|
+
const commandKeyLabel = () => isMac() ? "⌘" : "Ctrl+";
|
|
3226
|
+
|
|
3227
|
+
const STORAGE_KEY = "topbar.product_search.last_product";
|
|
3228
|
+
function readLastProduct() {
|
|
3229
|
+
try {
|
|
3230
|
+
return localStorage.getItem(STORAGE_KEY);
|
|
3231
|
+
} catch {
|
|
3232
|
+
return null;
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
function saveLastProduct(slug) {
|
|
3236
|
+
try {
|
|
3237
|
+
localStorage.setItem(STORAGE_KEY, slug);
|
|
3238
|
+
} catch {
|
|
3239
|
+
// Storage can be full or blocked; search still works without it.
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
var styles$c = {"visuallyHidden":"visuallyHidden-C28ju"};
|
|
3244
|
+
|
|
3245
|
+
var styles$b = {"tab":"tab-lQLDx"};
|
|
3246
|
+
|
|
3247
|
+
// useId isn't available on React 17, which our peer range still supports.
|
|
3248
|
+
let instanceCount$1 = 0;
|
|
3249
|
+
function useInstanceId(prefix) {
|
|
3250
|
+
const [id] = React__default.useState(() => `${prefix}-${++instanceCount$1}`);
|
|
3251
|
+
return id;
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
function ProductTabs({
|
|
3255
|
+
children,
|
|
3256
|
+
onKeyDown,
|
|
3257
|
+
onSelect,
|
|
3258
|
+
products,
|
|
3259
|
+
selected
|
|
3260
|
+
}) {
|
|
3261
|
+
const id = useInstanceId("product-tabs");
|
|
3262
|
+
const tabId = slug => `${id}-tab-${slug}`;
|
|
3263
|
+
const tabRefs = React__default.useRef(new Map());
|
|
3264
|
+
const handleKeyDown = event => {
|
|
3265
|
+
var _a;
|
|
3266
|
+
const offset = {
|
|
3267
|
+
ArrowLeft: -1,
|
|
3268
|
+
ArrowRight: 1
|
|
3269
|
+
}[event.key];
|
|
3270
|
+
if (offset) {
|
|
3271
|
+
event.preventDefault();
|
|
3272
|
+
const index = products.indexOf(selected);
|
|
3273
|
+
const next = products[(index + offset + products.length) % products.length];
|
|
3274
|
+
onSelect(next.slug);
|
|
3275
|
+
(_a = tabRefs.current.get(next.slug)) === null || _a === void 0 ? void 0 : _a.focus();
|
|
3276
|
+
} else {
|
|
3277
|
+
onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
|
|
3278
|
+
}
|
|
3279
|
+
};
|
|
3280
|
+
return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("div", {
|
|
3281
|
+
"aria-label": "Product",
|
|
3282
|
+
onKeyDown: handleKeyDown,
|
|
3283
|
+
role: "tablist"
|
|
3284
|
+
}, products.map(product => (/*#__PURE__*/React__default.createElement("button", {
|
|
3285
|
+
"aria-controls": `${id}-panel`,
|
|
3286
|
+
"aria-selected": product === selected,
|
|
3287
|
+
className: styles$b.tab,
|
|
3288
|
+
id: tabId(product.slug),
|
|
3289
|
+
key: product.slug,
|
|
3290
|
+
onClick: () => onSelect(product.slug),
|
|
3291
|
+
ref: element => {
|
|
3292
|
+
if (element) {
|
|
3293
|
+
tabRefs.current.set(product.slug, element);
|
|
3294
|
+
} else {
|
|
3295
|
+
tabRefs.current.delete(product.slug);
|
|
3296
|
+
}
|
|
3297
|
+
},
|
|
3298
|
+
role: "tab",
|
|
3299
|
+
tabIndex: product === selected ? 0 : -1,
|
|
3300
|
+
type: "button"
|
|
3301
|
+
}, product.appName)))), /*#__PURE__*/React__default.createElement("div", {
|
|
3302
|
+
"aria-labelledby": tabId(selected.slug),
|
|
3303
|
+
id: `${id}-panel`,
|
|
3304
|
+
role: "tabpanel"
|
|
3305
|
+
}, children));
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
var styles$a = {"result":"result-WCnxr"};
|
|
3309
|
+
|
|
3223
3310
|
function SearchResult({
|
|
3311
|
+
highlighted,
|
|
3312
|
+
id,
|
|
3313
|
+
onHighlight,
|
|
3224
3314
|
result
|
|
3225
3315
|
}) {
|
|
3226
3316
|
const {
|
|
@@ -3228,62 +3318,222 @@ function SearchResult({
|
|
|
3228
3318
|
title,
|
|
3229
3319
|
url
|
|
3230
3320
|
} = result.attributes;
|
|
3321
|
+
const ref = React__default.useRef(null);
|
|
3322
|
+
React__default.useEffect(() => {
|
|
3323
|
+
var _a;
|
|
3324
|
+
if (highlighted) (_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({
|
|
3325
|
+
block: "nearest"
|
|
3326
|
+
});
|
|
3327
|
+
}, [highlighted]);
|
|
3328
|
+
// Focus stays in the search input, which points at the highlighted result
|
|
3329
|
+
// with aria-activedescendant, so results are kept out of the tab order.
|
|
3330
|
+
// Mouse move rather than enter: arrowing scrolls results under a resting
|
|
3331
|
+
// pointer, and that shouldn't steal the highlight.
|
|
3231
3332
|
return /*#__PURE__*/React__default.createElement("a", {
|
|
3232
|
-
|
|
3333
|
+
"aria-selected": highlighted,
|
|
3334
|
+
className: styles$a.result,
|
|
3335
|
+
href: url,
|
|
3336
|
+
id: id,
|
|
3337
|
+
onMouseMove: onHighlight,
|
|
3338
|
+
ref: ref,
|
|
3339
|
+
role: "option",
|
|
3340
|
+
tabIndex: -1
|
|
3233
3341
|
}, /*#__PURE__*/React__default.createElement("div", null, title), /*#__PURE__*/React__default.createElement("div", null, summarize(metadata)));
|
|
3234
3342
|
}
|
|
3235
3343
|
function summarize(metadata) {
|
|
3236
3344
|
return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
|
|
3237
3345
|
}
|
|
3238
3346
|
|
|
3239
|
-
const
|
|
3240
|
-
function
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3347
|
+
const resultId = (listboxId, index) => `${listboxId}-result-${index}`;
|
|
3348
|
+
function SearchResults({
|
|
3349
|
+
search,
|
|
3350
|
+
term,
|
|
3351
|
+
...navigation
|
|
3352
|
+
}) {
|
|
3353
|
+
var _a;
|
|
3354
|
+
switch (search.status) {
|
|
3355
|
+
case "idle":
|
|
3356
|
+
return null;
|
|
3357
|
+
case "loading":
|
|
3358
|
+
if ((_a = search.groups) === null || _a === void 0 ? void 0 : _a.length) {
|
|
3359
|
+
return /*#__PURE__*/React__default.createElement(ResultGroups, {
|
|
3360
|
+
busy: true,
|
|
3361
|
+
groups: search.groups,
|
|
3362
|
+
...navigation
|
|
3363
|
+
});
|
|
3364
|
+
}
|
|
3365
|
+
return /*#__PURE__*/React__default.createElement("p", {
|
|
3366
|
+
role: "status"
|
|
3367
|
+
}, "Searching\u2026");
|
|
3368
|
+
case "error":
|
|
3369
|
+
return /*#__PURE__*/React__default.createElement("p", {
|
|
3370
|
+
role: "alert"
|
|
3371
|
+
}, "Search isn\u2019t working right now. Try again.");
|
|
3372
|
+
case "done":
|
|
3373
|
+
if (search.groups.length === 0) {
|
|
3374
|
+
return /*#__PURE__*/React__default.createElement("p", {
|
|
3375
|
+
role: "status"
|
|
3376
|
+
}, "No results for \u201C", term.trim(), "\u201D");
|
|
3377
|
+
}
|
|
3378
|
+
return /*#__PURE__*/React__default.createElement(ResultGroups, {
|
|
3379
|
+
groups: search.groups,
|
|
3380
|
+
...navigation
|
|
3381
|
+
});
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
function ResultGroups({
|
|
3385
|
+
busy = false,
|
|
3386
|
+
groups,
|
|
3387
|
+
highlightedIndex,
|
|
3388
|
+
id,
|
|
3389
|
+
label,
|
|
3390
|
+
onHighlight
|
|
3391
|
+
}) {
|
|
3392
|
+
let index = 0;
|
|
3393
|
+
return /*#__PURE__*/React__default.createElement("div", {
|
|
3394
|
+
"aria-busy": busy,
|
|
3395
|
+
"aria-label": label,
|
|
3396
|
+
id: id,
|
|
3397
|
+
role: "listbox"
|
|
3398
|
+
}, groups.map(({
|
|
3399
|
+
results,
|
|
3400
|
+
type
|
|
3401
|
+
}, groupIndex) => {
|
|
3402
|
+
const headingId = `${id}-group-${groupIndex}`;
|
|
3403
|
+
return /*#__PURE__*/React__default.createElement("div", {
|
|
3404
|
+
"aria-labelledby": headingId,
|
|
3405
|
+
key: type,
|
|
3406
|
+
role: "group"
|
|
3407
|
+
}, /*#__PURE__*/React__default.createElement("div", {
|
|
3408
|
+
id: headingId
|
|
3409
|
+
}, type), results.map(result => {
|
|
3410
|
+
const resultIndex = index++;
|
|
3411
|
+
return /*#__PURE__*/React__default.createElement(SearchResult, {
|
|
3412
|
+
highlighted: resultIndex === highlightedIndex,
|
|
3413
|
+
id: resultId(id, resultIndex),
|
|
3414
|
+
key: result.id,
|
|
3415
|
+
onHighlight: () => onHighlight(resultIndex),
|
|
3416
|
+
result: result
|
|
3417
|
+
});
|
|
3418
|
+
}));
|
|
3419
|
+
}));
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
const CACHE_TTL_MS = 60000;
|
|
3423
|
+
const MAX_ENTRIES$1 = 50;
|
|
3424
|
+
const cache = new Map();
|
|
3425
|
+
function cachedRequest(url) {
|
|
3426
|
+
const cached = cache.get(url);
|
|
3427
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
3428
|
+
// Re-inserting keeps the Map in recently-used order for eviction.
|
|
3429
|
+
cache.delete(url);
|
|
3430
|
+
cache.set(url, cached);
|
|
3431
|
+
return cached.promise;
|
|
3432
|
+
} else {
|
|
3433
|
+
return fetchIntoCache(url);
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
function fetchIntoCache(url) {
|
|
3437
|
+
const promise = apiRequest(defaultFetch, url).then(({
|
|
3438
|
+
json,
|
|
3439
|
+
ok
|
|
3440
|
+
}) => {
|
|
3441
|
+
if (!ok || json === null) throw new Error(`Request failed: ${url}`);
|
|
3442
|
+
return json;
|
|
3245
3443
|
});
|
|
3444
|
+
// In-flight entries never expire, so every caller shares the one fetch.
|
|
3445
|
+
const entry = {
|
|
3446
|
+
expiresAt: Infinity,
|
|
3447
|
+
promise
|
|
3448
|
+
};
|
|
3449
|
+
cache.set(url, entry);
|
|
3450
|
+
promise.then(() => entry.expiresAt = Date.now() + CACHE_TTL_MS, () => cache.get(url) === entry && cache.delete(url));
|
|
3451
|
+
if (cache.size > MAX_ENTRIES$1) cache.delete(cache.keys().next().value);
|
|
3452
|
+
return promise;
|
|
3453
|
+
}
|
|
3454
|
+
function useCachedRequest(url) {
|
|
3455
|
+
const [settled, setSettled] = React__default.useState();
|
|
3246
3456
|
React__default.useEffect(() => {
|
|
3247
|
-
if (
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3457
|
+
if (url) {
|
|
3458
|
+
let current = true;
|
|
3459
|
+
cachedRequest(url).then(data => current && setSettled({
|
|
3460
|
+
data,
|
|
3461
|
+
url
|
|
3462
|
+
}), () => current && setSettled({
|
|
3463
|
+
failed: true,
|
|
3464
|
+
url
|
|
3465
|
+
}));
|
|
3466
|
+
return () => {
|
|
3467
|
+
current = false;
|
|
3468
|
+
};
|
|
3469
|
+
} else {
|
|
3251
3470
|
return undefined;
|
|
3252
3471
|
}
|
|
3253
|
-
|
|
3254
|
-
|
|
3472
|
+
}, [url]);
|
|
3473
|
+
if (!url) {
|
|
3474
|
+
return {
|
|
3475
|
+
status: "idle"
|
|
3476
|
+
};
|
|
3477
|
+
} else if ((settled === null || settled === void 0 ? void 0 : settled.url) !== url) {
|
|
3478
|
+
return {
|
|
3479
|
+
data: settled && "data" in settled ? settled.data : undefined,
|
|
3255
3480
|
status: "loading"
|
|
3256
|
-
});
|
|
3257
|
-
apiRequest(defaultFetch, productSearchUrl(debouncedQuery)).then(({
|
|
3258
|
-
json,
|
|
3259
|
-
ok
|
|
3260
|
-
}) => {
|
|
3261
|
-
if (!current) return;
|
|
3262
|
-
setSearch(ok && json ? {
|
|
3263
|
-
groups: groupByType(json),
|
|
3264
|
-
status: "done"
|
|
3265
|
-
} : {
|
|
3266
|
-
status: "error"
|
|
3267
|
-
});
|
|
3268
|
-
});
|
|
3269
|
-
return () => {
|
|
3270
|
-
current = false;
|
|
3271
3481
|
};
|
|
3272
|
-
}
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3482
|
+
} else if ("data" in settled) {
|
|
3483
|
+
return {
|
|
3484
|
+
data: settled.data,
|
|
3485
|
+
status: "done"
|
|
3486
|
+
};
|
|
3487
|
+
} else {
|
|
3488
|
+
return {
|
|
3489
|
+
status: "error"
|
|
3490
|
+
};
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
const SEARCH_DEBOUNCE_MS = 300;
|
|
3495
|
+
function useProductSearch(product, term) {
|
|
3496
|
+
const query = term.trim();
|
|
3497
|
+
const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
|
|
3498
|
+
const request = useCachedRequest(debouncedQuery ? productSearchUrl(product, debouncedQuery) : null);
|
|
3499
|
+
const response = "data" in request ? request.data : undefined;
|
|
3500
|
+
const settledProduct = useSettledProduct(product, request.status);
|
|
3501
|
+
// While a request loads, the previous response stays on screen, but only
|
|
3502
|
+
// if it was for this product: People results must never show under the
|
|
3503
|
+
// Services tab.
|
|
3504
|
+
const visibleResponse = request.status === "done" || settledProduct === product ? response : undefined;
|
|
3505
|
+
const groups = React__default.useMemo(() => visibleResponse && groupByType(visibleResponse), [visibleResponse]);
|
|
3506
|
+
if (!query) {
|
|
3507
|
+
return {
|
|
3508
|
+
status: "idle"
|
|
3509
|
+
};
|
|
3510
|
+
} else if (query !== debouncedQuery) {
|
|
3511
|
+
return {
|
|
3512
|
+
groups,
|
|
3513
|
+
status: "loading"
|
|
3514
|
+
};
|
|
3515
|
+
} else if (request.status === "done") {
|
|
3516
|
+
return {
|
|
3517
|
+
groups: groups !== null && groups !== void 0 ? groups : [],
|
|
3518
|
+
status: "done"
|
|
3519
|
+
};
|
|
3520
|
+
} else if (request.status === "error") {
|
|
3521
|
+
return {
|
|
3522
|
+
status: "error"
|
|
3523
|
+
};
|
|
3524
|
+
} else {
|
|
3525
|
+
return {
|
|
3526
|
+
groups,
|
|
3527
|
+
status: "loading"
|
|
3528
|
+
};
|
|
3529
|
+
}
|
|
3280
3530
|
}
|
|
3281
|
-
function productSearchUrl(query) {
|
|
3531
|
+
function productSearchUrl(product, query) {
|
|
3282
3532
|
const params = new URLSearchParams({
|
|
3283
3533
|
per_page: "100",
|
|
3284
3534
|
"where[term]": query
|
|
3285
3535
|
});
|
|
3286
|
-
return `${window.location.origin}/~api/
|
|
3536
|
+
return `${window.location.origin}/~api/${product}/v2/product_search_results?${params}`;
|
|
3287
3537
|
}
|
|
3288
3538
|
function groupByType({
|
|
3289
3539
|
data,
|
|
@@ -3308,6 +3558,14 @@ function groupByType({
|
|
|
3308
3558
|
};
|
|
3309
3559
|
return [...groups.values()].sort((a, b) => rank(a.type) - rank(b.type));
|
|
3310
3560
|
}
|
|
3561
|
+
// The product the most recent finished request was for.
|
|
3562
|
+
function useSettledProduct(product, status) {
|
|
3563
|
+
const [settledProduct, setSettledProduct] = React__default.useState();
|
|
3564
|
+
React__default.useEffect(() => {
|
|
3565
|
+
if (status === "done") setSettledProduct(product);
|
|
3566
|
+
}, [product, status]);
|
|
3567
|
+
return settledProduct;
|
|
3568
|
+
}
|
|
3311
3569
|
function useDebouncedValue(value, delay) {
|
|
3312
3570
|
const [debounced, setDebounced] = React__default.useState(value);
|
|
3313
3571
|
React__default.useEffect(() => {
|
|
@@ -3317,53 +3575,199 @@ function useDebouncedValue(value, delay) {
|
|
|
3317
3575
|
return debounced;
|
|
3318
3576
|
}
|
|
3319
3577
|
|
|
3320
|
-
|
|
3321
|
-
|
|
3578
|
+
const COPIED_FEEDBACK_MS = 1500;
|
|
3579
|
+
const verticalStep = key => ({
|
|
3580
|
+
ArrowDown: 1,
|
|
3581
|
+
ArrowUp: -1
|
|
3582
|
+
})[key];
|
|
3583
|
+
function useResultNavigation(results) {
|
|
3584
|
+
// Remembering which results the index belongs to puts the highlight back on
|
|
3585
|
+
// the first result whenever new ones arrive, without an effect to reset it.
|
|
3586
|
+
const [highlight, setHighlight] = React__default.useState({
|
|
3587
|
+
index: 0,
|
|
3588
|
+
results
|
|
3589
|
+
});
|
|
3590
|
+
const highlightedIndex = highlight.results === results ? highlight.index : 0;
|
|
3591
|
+
const highlighted = results[highlightedIndex];
|
|
3592
|
+
const [copiedUrl, setCopiedUrl] = React__default.useState(null);
|
|
3593
|
+
React__default.useEffect(() => {
|
|
3594
|
+
if (copiedUrl) {
|
|
3595
|
+
const timeout = setTimeout(() => setCopiedUrl(null), COPIED_FEEDBACK_MS);
|
|
3596
|
+
return () => clearTimeout(timeout);
|
|
3597
|
+
} else {
|
|
3598
|
+
return undefined;
|
|
3599
|
+
}
|
|
3600
|
+
}, [copiedUrl]);
|
|
3601
|
+
const highlightIndex = React__default.useCallback(index => setHighlight(current => current.results === results && current.index === index ? current : {
|
|
3602
|
+
index,
|
|
3603
|
+
results
|
|
3604
|
+
}), [results]);
|
|
3605
|
+
const handleKeyDown = event => {
|
|
3606
|
+
const step = verticalStep(event.key);
|
|
3607
|
+
if (step && results.length > 0) {
|
|
3608
|
+
event.preventDefault();
|
|
3609
|
+
highlightIndex((highlightedIndex + step + results.length) % results.length);
|
|
3610
|
+
} else if (event.key === "Enter" && highlighted && !event.nativeEvent.isComposing) {
|
|
3611
|
+
event.preventDefault();
|
|
3612
|
+
open(highlighted.attributes.url, {
|
|
3613
|
+
newTab: hasCommandKey(event)
|
|
3614
|
+
});
|
|
3615
|
+
} else if (isCopyShortcut(event) && highlighted && !hasSelectedText(event.currentTarget)) {
|
|
3616
|
+
event.preventDefault();
|
|
3617
|
+
const {
|
|
3618
|
+
url
|
|
3619
|
+
} = highlighted.attributes;
|
|
3620
|
+
navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
|
|
3621
|
+
}
|
|
3622
|
+
};
|
|
3623
|
+
return {
|
|
3624
|
+
copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.attributes.url),
|
|
3625
|
+
handleKeyDown,
|
|
3626
|
+
highlightIndex,
|
|
3627
|
+
highlighted,
|
|
3628
|
+
highlightedIndex: highlighted ? highlightedIndex : null
|
|
3629
|
+
};
|
|
3630
|
+
}
|
|
3631
|
+
function open(url, {
|
|
3632
|
+
newTab
|
|
3322
3633
|
}) {
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3634
|
+
if (newTab) {
|
|
3635
|
+
window.open(url, "_blank", "noopener");
|
|
3636
|
+
} else {
|
|
3637
|
+
window.location.assign(url);
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
const isCopyShortcut = event => event.key.toLowerCase() === "c" && !event.altKey && !event.shiftKey && hasCommandKey(event);
|
|
3641
|
+
// With text selected, ⌘C should copy that text as usual, not the link.
|
|
3642
|
+
function hasSelectedText(element) {
|
|
3643
|
+
var _a, _b;
|
|
3644
|
+
const selectedInField = element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
|
|
3645
|
+
return selectedInField || ((_b = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "") !== "";
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
const SEARCHABLE_PRODUCTS = [{
|
|
3649
|
+
appName: "Groups",
|
|
3650
|
+
slug: "groups"
|
|
3651
|
+
}, {
|
|
3652
|
+
appName: "People",
|
|
3653
|
+
slug: "people"
|
|
3654
|
+
}, {
|
|
3655
|
+
appName: "Services",
|
|
3656
|
+
slug: "services"
|
|
3657
|
+
}];
|
|
3658
|
+
const PERMISSION_CHECKS = {
|
|
3659
|
+
people: () => fetchMe("people", "people_permissions,site_administrator").then(({
|
|
3660
|
+
people_permissions,
|
|
3661
|
+
site_administrator
|
|
3662
|
+
}) => site_administrator || people_permissions !== null),
|
|
3663
|
+
services: () => fetchMe("services", "can_search").then(({
|
|
3664
|
+
can_search
|
|
3665
|
+
}) => can_search)
|
|
3666
|
+
};
|
|
3667
|
+
function fetchMe(product, fields) {
|
|
3668
|
+
const params = new URLSearchParams({
|
|
3669
|
+
"fields[Person]": fields
|
|
3670
|
+
});
|
|
3671
|
+
return apiRequest(defaultFetch, `${window.location.origin}/~api/${product}/v2/me?${params}`).then(({
|
|
3672
|
+
json,
|
|
3673
|
+
ok
|
|
3674
|
+
}) => {
|
|
3675
|
+
if (!ok || json === null) throw new Error("Permission check failed");
|
|
3676
|
+
return json.data.attributes;
|
|
3677
|
+
});
|
|
3678
|
+
}
|
|
3679
|
+
// Permissions rarely change mid-session, so a successful check is kept until
|
|
3680
|
+
// the page reloads. A failed one is dropped so a remount tries again.
|
|
3681
|
+
const permissionCache = new Map();
|
|
3682
|
+
function canSearch(slug) {
|
|
3683
|
+
const check = PERMISSION_CHECKS[slug];
|
|
3684
|
+
const cached = permissionCache.get(slug);
|
|
3685
|
+
if (!check) {
|
|
3686
|
+
return Promise.resolve(true);
|
|
3687
|
+
} else if (cached) {
|
|
3688
|
+
return cached;
|
|
3689
|
+
} else {
|
|
3690
|
+
const promise = check().catch(() => {
|
|
3691
|
+
permissionCache.delete(slug);
|
|
3692
|
+
return false;
|
|
3693
|
+
});
|
|
3694
|
+
permissionCache.set(slug, promise);
|
|
3695
|
+
return promise;
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
function useSearchableProducts(apps) {
|
|
3699
|
+
// Keyed by name because providers hand over a fresh `apps` array on renders
|
|
3700
|
+
// where nothing changed.
|
|
3701
|
+
const accessibleKey = inSwitcherOrder(apps).map(({
|
|
3702
|
+
slug
|
|
3703
|
+
}) => slug).join(",");
|
|
3704
|
+
const [settled, setSettled] = React__default.useState();
|
|
3705
|
+
React__default.useEffect(() => {
|
|
3706
|
+
let current = true;
|
|
3707
|
+
const accessible = accessibleKey.split(",").flatMap(slug => SEARCHABLE_PRODUCTS.filter(product => product.slug === slug));
|
|
3708
|
+
Promise.all(accessible.map(({
|
|
3709
|
+
slug
|
|
3710
|
+
}) => canSearch(slug))).then(allowed => {
|
|
3711
|
+
if (current) {
|
|
3712
|
+
setSettled({
|
|
3713
|
+
accessibleKey,
|
|
3714
|
+
products: accessible.filter((_, index) => allowed[index])
|
|
3715
|
+
});
|
|
3340
3716
|
}
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3717
|
+
});
|
|
3718
|
+
return () => {
|
|
3719
|
+
current = false;
|
|
3720
|
+
};
|
|
3721
|
+
}, [accessibleKey]);
|
|
3722
|
+
if ((settled === null || settled === void 0 ? void 0 : settled.accessibleKey) === accessibleKey) {
|
|
3723
|
+
return {
|
|
3724
|
+
products: settled.products,
|
|
3725
|
+
status: "done"
|
|
3726
|
+
};
|
|
3727
|
+
} else {
|
|
3728
|
+
return {
|
|
3729
|
+
status: "loading"
|
|
3730
|
+
};
|
|
3352
3731
|
}
|
|
3353
3732
|
}
|
|
3733
|
+
function inSwitcherOrder(apps) {
|
|
3734
|
+
return groupApps(apps).restProducts.flatMap(app => SEARCHABLE_PRODUCTS.filter(product => product.appName === app.name));
|
|
3735
|
+
}
|
|
3354
3736
|
|
|
3355
|
-
const
|
|
3356
|
-
|
|
3357
|
-
|
|
3737
|
+
const isSearchShortcut = event => event.key.toLowerCase() === "k" && !event.altKey && !event.shiftKey && hasCommandKey(event);
|
|
3738
|
+
// Products opened their own search with `/` before this one replaced it.
|
|
3739
|
+
const isLegacySearchShortcut = event => event.key === "/" && !event.altKey && !event.ctrlKey && !event.metaKey && !isTypingIn(event.target);
|
|
3740
|
+
const isTypingKey = event => event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey;
|
|
3741
|
+
const isTypingIn = target => target instanceof Element && target.closest('input, select, textarea, [contenteditable]:not([contenteditable="false"])') !== null;
|
|
3742
|
+
function ProductSearch({
|
|
3743
|
+
apps
|
|
3744
|
+
}) {
|
|
3745
|
+
const searchable = useSearchableProducts(apps);
|
|
3746
|
+
if (searchable.status === "done" && searchable.products.length > 0) {
|
|
3747
|
+
return /*#__PURE__*/React__default.createElement(SearchButtonAndDialog, {
|
|
3748
|
+
products: searchable.products
|
|
3749
|
+
});
|
|
3750
|
+
} else {
|
|
3751
|
+
return null;
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
function SearchButtonAndDialog({
|
|
3755
|
+
products
|
|
3756
|
+
}) {
|
|
3358
3757
|
const [open, setOpen] = React__default.useState(false);
|
|
3359
3758
|
React__default.useEffect(() => {
|
|
3759
|
+
// Rich text editors like Trix bind ⌘K to "insert link"; capturing at the
|
|
3760
|
+
// window and stopping propagation keeps them from ever seeing it.
|
|
3360
3761
|
const handleKeyDown = event => {
|
|
3361
|
-
if (
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
event
|
|
3366
|
-
|
|
3762
|
+
if (isSearchShortcut(event)) {
|
|
3763
|
+
event.preventDefault();
|
|
3764
|
+
event.stopPropagation();
|
|
3765
|
+
setOpen(wasOpen => !wasOpen);
|
|
3766
|
+
} else if (isLegacySearchShortcut(event)) {
|
|
3767
|
+
event.preventDefault();
|
|
3768
|
+
event.stopPropagation();
|
|
3769
|
+
setOpen(true);
|
|
3770
|
+
}
|
|
3367
3771
|
};
|
|
3368
3772
|
window.addEventListener("keydown", handleKeyDown, {
|
|
3369
3773
|
capture: true
|
|
@@ -3373,7 +3777,7 @@ function ProductSearch() {
|
|
|
3373
3777
|
});
|
|
3374
3778
|
}, []);
|
|
3375
3779
|
return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("button", {
|
|
3376
|
-
"aria-keyshortcuts": isMac() ? "Meta+K" : "Control+K"
|
|
3780
|
+
"aria-keyshortcuts": `${isMac() ? "Meta+K" : "Control+K"} /`,
|
|
3377
3781
|
"aria-label": "Search",
|
|
3378
3782
|
onClick: () => setOpen(true),
|
|
3379
3783
|
type: "button"
|
|
@@ -3384,21 +3788,357 @@ function ProductSearch() {
|
|
|
3384
3788
|
dismissible: true,
|
|
3385
3789
|
onClose: () => setOpen(false),
|
|
3386
3790
|
open: open
|
|
3387
|
-
}, /*#__PURE__*/React__default.createElement(SearchPanel,
|
|
3791
|
+
}, /*#__PURE__*/React__default.createElement(SearchPanel, {
|
|
3792
|
+
products: products
|
|
3793
|
+
})));
|
|
3388
3794
|
}
|
|
3389
|
-
function SearchPanel(
|
|
3795
|
+
function SearchPanel({
|
|
3796
|
+
products
|
|
3797
|
+
}) {
|
|
3798
|
+
var _a, _b;
|
|
3799
|
+
const listboxId = useInstanceId("product-search-results");
|
|
3390
3800
|
const [term, setTerm] = React__default.useState("");
|
|
3801
|
+
const [selectedSlug, setSelectedSlug] = React__default.useState(() => initialProduct(products).slug);
|
|
3802
|
+
const product = (_a = products.find(({
|
|
3803
|
+
slug
|
|
3804
|
+
}) => slug === selectedSlug)) !== null && _a !== void 0 ? _a : products[0];
|
|
3805
|
+
const hasTerm = term.trim() !== "";
|
|
3806
|
+
const search = useProductSearch(product.slug, term);
|
|
3807
|
+
const groups = "groups" in search ? search.groups : undefined;
|
|
3808
|
+
const results = React__default.useMemo(() => {
|
|
3809
|
+
var _a;
|
|
3810
|
+
return (_a = groups === null || groups === void 0 ? void 0 : groups.flatMap(group => group.results)) !== null && _a !== void 0 ? _a : [];
|
|
3811
|
+
}, [groups]);
|
|
3812
|
+
const navigation = useResultNavigation(results);
|
|
3813
|
+
const {
|
|
3814
|
+
highlightedIndex
|
|
3815
|
+
} = navigation;
|
|
3816
|
+
const inputRef = React__default.useRef(null);
|
|
3817
|
+
const [announceHighlight, setAnnounceHighlight] = React__default.useState(false);
|
|
3818
|
+
// The tabs keep focus while arrowing through results, but only the focused
|
|
3819
|
+
// input can point screen readers at the highlight, so from the tabs it's
|
|
3820
|
+
// announced instead. Typing goes back to the input, keystroke and all.
|
|
3821
|
+
const handleTabsKeyDown = event => {
|
|
3822
|
+
var _a;
|
|
3823
|
+
if (isTypingKey(event)) {
|
|
3824
|
+
(_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
3825
|
+
} else if (verticalStep(event.key)) {
|
|
3826
|
+
setAnnounceHighlight(true);
|
|
3827
|
+
navigation.handleKeyDown(event);
|
|
3828
|
+
} else {
|
|
3829
|
+
navigation.handleKeyDown(event);
|
|
3830
|
+
}
|
|
3831
|
+
};
|
|
3832
|
+
React__default.useEffect(() => {
|
|
3833
|
+
if (hasTerm) saveLastProduct(product.slug);
|
|
3834
|
+
}, [hasTerm, product.slug]);
|
|
3391
3835
|
return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("input", {
|
|
3836
|
+
"aria-activedescendant": highlightedIndex === null ? undefined : resultId(listboxId, highlightedIndex),
|
|
3837
|
+
"aria-autocomplete": "list",
|
|
3838
|
+
"aria-controls": results.length > 0 ? listboxId : undefined,
|
|
3839
|
+
"aria-expanded": results.length > 0,
|
|
3392
3840
|
"aria-label": "Search",
|
|
3393
3841
|
autoFocus: true,
|
|
3394
3842
|
onChange: event => setTerm(event.target.value),
|
|
3843
|
+
onFocus: () => setAnnounceHighlight(false),
|
|
3844
|
+
onKeyDown: navigation.handleKeyDown,
|
|
3395
3845
|
placeholder: "Search",
|
|
3846
|
+
ref: inputRef,
|
|
3847
|
+
role: "combobox",
|
|
3396
3848
|
type: "search",
|
|
3397
3849
|
value: term
|
|
3398
|
-
}), /*#__PURE__*/React__default.createElement(
|
|
3850
|
+
}), /*#__PURE__*/React__default.createElement(ProductTabs, {
|
|
3851
|
+
onKeyDown: handleTabsKeyDown,
|
|
3852
|
+
onSelect: setSelectedSlug,
|
|
3853
|
+
products: products,
|
|
3854
|
+
selected: product
|
|
3855
|
+
}, /*#__PURE__*/React__default.createElement(SearchResults, {
|
|
3856
|
+
highlightedIndex: highlightedIndex,
|
|
3857
|
+
id: listboxId,
|
|
3858
|
+
label: `${product.appName} results`,
|
|
3859
|
+
onHighlight: navigation.highlightIndex,
|
|
3860
|
+
search: search,
|
|
3399
3861
|
term: term
|
|
3862
|
+
})), /*#__PURE__*/React__default.createElement("div", {
|
|
3863
|
+
"aria-live": "polite",
|
|
3864
|
+
className: styles$c.visuallyHidden
|
|
3865
|
+
}, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.attributes.title : null), /*#__PURE__*/React__default.createElement(KeyboardHints, {
|
|
3866
|
+
copied: navigation.copied
|
|
3400
3867
|
}));
|
|
3401
3868
|
}
|
|
3869
|
+
function KeyboardHints({
|
|
3870
|
+
copied
|
|
3871
|
+
}) {
|
|
3872
|
+
const command = commandKeyLabel();
|
|
3873
|
+
return /*#__PURE__*/React__default.createElement("p", null, "\u2191\u2193 Select \u00B7 \u21B5 Open \u00B7 ", command, "\u21B5 Open in new tab \u00B7 ", command, "C", " ", /*#__PURE__*/React__default.createElement("span", {
|
|
3874
|
+
role: "status"
|
|
3875
|
+
}, copied ? "Copied!" : "Copy link"), " \u00B7 ", command, "K Open search");
|
|
3876
|
+
}
|
|
3877
|
+
function initialProduct(products) {
|
|
3878
|
+
var _a, _b, _c;
|
|
3879
|
+
const lastProduct = readLastProduct();
|
|
3880
|
+
const bySlug = slug => products.find(product => product.slug === slug);
|
|
3881
|
+
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];
|
|
3882
|
+
}
|
|
3883
|
+
|
|
3884
|
+
function parseValue(element) {
|
|
3885
|
+
const datatype = element.getAttribute("datatype");
|
|
3886
|
+
switch (datatype) {
|
|
3887
|
+
case "boolean":
|
|
3888
|
+
return element.content === "true" || element.content === "1";
|
|
3889
|
+
default:
|
|
3890
|
+
return element.content;
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
function getAllPcoMeta() {
|
|
3894
|
+
const elements = document.head.querySelectorAll(`meta[property^="pco:"]`);
|
|
3895
|
+
const meta = {};
|
|
3896
|
+
elements.forEach(e => {
|
|
3897
|
+
const property = e.getAttribute("property");
|
|
3898
|
+
meta[property.substring("pco:".length)] = parseValue(e);
|
|
3899
|
+
});
|
|
3900
|
+
return meta;
|
|
3901
|
+
}
|
|
3902
|
+
// usePcoMeta only reads each tag's content. Recently Visited also needs the
|
|
3903
|
+
// pco:recently_visitable tag's `metadata` attribute, which can change while
|
|
3904
|
+
// the content stays the same, so it is tracked in its own store.
|
|
3905
|
+
function getRecentlyVisitableMetadata() {
|
|
3906
|
+
var _a, _b;
|
|
3907
|
+
return (_b = (_a = document.head.querySelector(`meta[property="pco:recently_visitable"]`)) === null || _a === void 0 ? void 0 : _a.getAttribute("metadata")) !== null && _b !== void 0 ? _b : null;
|
|
3908
|
+
}
|
|
3909
|
+
const store = createStore(getAllPcoMeta());
|
|
3910
|
+
const recentlyVisitableMetadataStore = createStore({
|
|
3911
|
+
metadata: getRecentlyVisitableMetadata()
|
|
3912
|
+
});
|
|
3913
|
+
function sync() {
|
|
3914
|
+
const nextState = getAllPcoMeta();
|
|
3915
|
+
if (!shallowCompare(store.getState(), nextState)) store.setState(nextState);
|
|
3916
|
+
const metadata = getRecentlyVisitableMetadata();
|
|
3917
|
+
if (recentlyVisitableMetadataStore.getState().metadata !== metadata) {
|
|
3918
|
+
recentlyVisitableMetadataStore.setState({
|
|
3919
|
+
metadata
|
|
3920
|
+
});
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
const observer = new MutationObserver(sync);
|
|
3924
|
+
let hasTriggeredObserve = false;
|
|
3925
|
+
function observe() {
|
|
3926
|
+
if (hasTriggeredObserve) return;
|
|
3927
|
+
hasTriggeredObserve = true;
|
|
3928
|
+
// The module-load snapshot above can predate meta tags that appear later
|
|
3929
|
+
// in <head> (e.g. pco:resource, yielded right before </head>) if this
|
|
3930
|
+
// module's own script tag isn't deferred and runs mid-parse. Those tags
|
|
3931
|
+
// never mutate afterward, so the observer alone would never catch them.
|
|
3932
|
+
// Re-read once now, since observe() is only called once a consumer is
|
|
3933
|
+
// actually mounting, which is always after the document has fully parsed.
|
|
3934
|
+
sync();
|
|
3935
|
+
observer.observe(document.head, {
|
|
3936
|
+
attributes: true,
|
|
3937
|
+
childList: true,
|
|
3938
|
+
subtree: true
|
|
3939
|
+
});
|
|
3940
|
+
}
|
|
3941
|
+
function usePcoMeta() {
|
|
3942
|
+
observe();
|
|
3943
|
+
return useSyncExternalStore(store.subscribe, () => store.getState());
|
|
3944
|
+
}
|
|
3945
|
+
function useRecentlyVisitableMetadata() {
|
|
3946
|
+
observe();
|
|
3947
|
+
return useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
const MAX_ENTRIES = 20;
|
|
3951
|
+
const FORMAT_VERSION = 1;
|
|
3952
|
+
const MAX_TEXT_LENGTH = 200;
|
|
3953
|
+
const PRODUCTS = ["accounts", "calendar", "check-ins", "giving", "groups", "home", "login", "people", "publishing", "registrations", "services"];
|
|
3954
|
+
function isId(value) {
|
|
3955
|
+
return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
|
|
3956
|
+
}
|
|
3957
|
+
// A PCRL looks like `<product>/v2/<resource_type>/<id>`.
|
|
3958
|
+
function isPcrl(value) {
|
|
3959
|
+
const segments = value.split("/");
|
|
3960
|
+
return segments.length === 4 && PRODUCTS.some(product => product === segments[0]) && isId(segments[3]);
|
|
3961
|
+
}
|
|
3962
|
+
function isNonBlankString(value) {
|
|
3963
|
+
return typeof value === "string" && value.trim() !== "";
|
|
3964
|
+
}
|
|
3965
|
+
function isRecord(value) {
|
|
3966
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3967
|
+
}
|
|
3968
|
+
function entryKey(entry) {
|
|
3969
|
+
if (entry.type === "resource") {
|
|
3970
|
+
return `resource:${entry.pcrl}`;
|
|
3971
|
+
} else {
|
|
3972
|
+
return `page:${entry.product}:${entry.path}`;
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3975
|
+
function isEntry(value) {
|
|
3976
|
+
if (!isRecord(value)) {
|
|
3977
|
+
return false;
|
|
3978
|
+
} else if (value.type === "resource") {
|
|
3979
|
+
return typeof value.pcrl === "string" && isPcrl(value.pcrl);
|
|
3980
|
+
} else if (value.type === "page") {
|
|
3981
|
+
return isNonBlankString(value.product) && isNonBlankString(value.title) && isNonBlankString(value.path) && typeof value.details === "string";
|
|
3982
|
+
} else {
|
|
3983
|
+
return false;
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
// Cuts on code points so a surrogate pair is never split.
|
|
3987
|
+
function clampText(value) {
|
|
3988
|
+
return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
|
|
3989
|
+
}
|
|
3990
|
+
function clampEntry(entry) {
|
|
3991
|
+
if (entry.type === "resource") {
|
|
3992
|
+
return entry;
|
|
3993
|
+
} else {
|
|
3994
|
+
return {
|
|
3995
|
+
...entry,
|
|
3996
|
+
details: clampText(entry.details),
|
|
3997
|
+
title: clampText(entry.title)
|
|
3998
|
+
};
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
// Recents are disposable: anything unexpected reads as an empty list (or, for
|
|
4002
|
+
// a single bad entry, a list without it) rather than throwing.
|
|
4003
|
+
function parseRecentlyVisited(value, personId) {
|
|
4004
|
+
let parsed = null;
|
|
4005
|
+
try {
|
|
4006
|
+
parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
|
|
4007
|
+
} catch {
|
|
4008
|
+
parsed = null;
|
|
4009
|
+
}
|
|
4010
|
+
if (isRecord(parsed) && parsed.v === FORMAT_VERSION && parsed.person === personId && Array.isArray(parsed.entries)) {
|
|
4011
|
+
return parsed.entries.filter(isEntry);
|
|
4012
|
+
} else {
|
|
4013
|
+
return [];
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
function addVisit(entries, visit) {
|
|
4017
|
+
const key = entryKey(visit);
|
|
4018
|
+
return [clampEntry(visit), ...entries.filter(entry => entryKey(entry) !== key)].slice(0, MAX_ENTRIES);
|
|
4019
|
+
}
|
|
4020
|
+
function serializeRecentlyVisited(entries, personId) {
|
|
4021
|
+
return JSON.stringify({
|
|
4022
|
+
entries,
|
|
4023
|
+
person: personId,
|
|
4024
|
+
v: FORMAT_VERSION
|
|
4025
|
+
});
|
|
4026
|
+
}
|
|
4027
|
+
|
|
4028
|
+
// The product whose URL, in this environment, has the current page's host.
|
|
4029
|
+
// Reversing pcoUrl handles every environment @planningcenter/url knows about,
|
|
4030
|
+
// and guarantees the stored product rebuilds a working link.
|
|
4031
|
+
function currentProduct() {
|
|
4032
|
+
var _a;
|
|
4033
|
+
return (_a = PRODUCTS.find(product => new URL(pcoUrl$2(product)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
|
|
4034
|
+
}
|
|
4035
|
+
function detailsFromMetadata(metadata) {
|
|
4036
|
+
var _a;
|
|
4037
|
+
try {
|
|
4038
|
+
const details = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) === null || _a === void 0 ? void 0 : _a.details;
|
|
4039
|
+
return typeof details === "string" ? details : "";
|
|
4040
|
+
} catch {
|
|
4041
|
+
return "";
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
// pco:resource is the more specific signal, so it wins when both tags are
|
|
4045
|
+
// present. A resource that isn't a PCRL is ignored rather than recorded.
|
|
4046
|
+
function visitFromPage({
|
|
4047
|
+
metadata,
|
|
4048
|
+
resource,
|
|
4049
|
+
title
|
|
4050
|
+
}) {
|
|
4051
|
+
const product = currentProduct();
|
|
4052
|
+
if (resource && isPcrl(resource)) {
|
|
4053
|
+
return {
|
|
4054
|
+
pcrl: resource,
|
|
4055
|
+
type: "resource"
|
|
4056
|
+
};
|
|
4057
|
+
} else if (title && title.trim() !== "" && product) {
|
|
4058
|
+
return {
|
|
4059
|
+
details: detailsFromMetadata(metadata),
|
|
4060
|
+
path: window.location.pathname,
|
|
4061
|
+
product,
|
|
4062
|
+
title,
|
|
4063
|
+
type: "page"
|
|
4064
|
+
};
|
|
4065
|
+
} else {
|
|
4066
|
+
return null;
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
// The entry Recently Visited would record for the current page, or null when
|
|
4070
|
+
// the page isn't eligible. Shared by the recorder and the debug panel so the
|
|
4071
|
+
// panel always shows what actually gets stored.
|
|
4072
|
+
function useCurrentVisit() {
|
|
4073
|
+
const {
|
|
4074
|
+
recently_visitable: title,
|
|
4075
|
+
resource
|
|
4076
|
+
} = usePcoMeta();
|
|
4077
|
+
const metadata = useRecentlyVisitableMetadata();
|
|
4078
|
+
return useMemo(() => visitFromPage({
|
|
4079
|
+
metadata,
|
|
4080
|
+
resource,
|
|
4081
|
+
title
|
|
4082
|
+
}), [metadata, resource, title]);
|
|
4083
|
+
}
|
|
4084
|
+
// Products hand the person's ID to AppsProvider, not to the topbar itself,
|
|
4085
|
+
// and several pass it as a number.
|
|
4086
|
+
function useCurrentPersonId() {
|
|
4087
|
+
const {
|
|
4088
|
+
userId
|
|
4089
|
+
} = useStore();
|
|
4090
|
+
return userId == null ? undefined : String(userId);
|
|
4091
|
+
}
|
|
4092
|
+
|
|
4093
|
+
// Per-origin localStorage is a stopgap: each product keeps its own list until
|
|
4094
|
+
// PCA-409 moves this to storage shared across product subdomains. Only this
|
|
4095
|
+
// file should need to change when it does.
|
|
4096
|
+
function storageKey({
|
|
4097
|
+
env,
|
|
4098
|
+
personId
|
|
4099
|
+
}) {
|
|
4100
|
+
return `topbar.recently_visited.${env}.${personId}`;
|
|
4101
|
+
}
|
|
4102
|
+
// localStorage doesn't notify the tab that wrote it, so readers in this tab
|
|
4103
|
+
// learn about new visits from this counter instead.
|
|
4104
|
+
const changes = createStore({
|
|
4105
|
+
version: 0
|
|
4106
|
+
});
|
|
4107
|
+
function readRecentlyVisited(scope) {
|
|
4108
|
+
try {
|
|
4109
|
+
return parseRecentlyVisited(localStorage.getItem(storageKey(scope)), scope.personId);
|
|
4110
|
+
} catch {
|
|
4111
|
+
return [];
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
4114
|
+
function recordVisit(visit, scope) {
|
|
4115
|
+
if (isEntry(visit)) {
|
|
4116
|
+
try {
|
|
4117
|
+
localStorage.setItem(storageKey(scope), serializeRecentlyVisited(addVisit(readRecentlyVisited(scope), visit), scope.personId));
|
|
4118
|
+
changes.setState({
|
|
4119
|
+
version: changes.getState().version + 1
|
|
4120
|
+
});
|
|
4121
|
+
} catch {
|
|
4122
|
+
// Storage can be full or blocked; recents are disposable.
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
function useRecordRecentlyVisited({
|
|
4128
|
+
enabled,
|
|
4129
|
+
env
|
|
4130
|
+
}) {
|
|
4131
|
+
const personId = useCurrentPersonId();
|
|
4132
|
+
const visit = useCurrentVisit();
|
|
4133
|
+
useEffect(() => {
|
|
4134
|
+
if (enabled && personId && visit) {
|
|
4135
|
+
recordVisit(visit, {
|
|
4136
|
+
env,
|
|
4137
|
+
personId
|
|
4138
|
+
});
|
|
4139
|
+
}
|
|
4140
|
+
}, [enabled, env, personId, visit]);
|
|
4141
|
+
}
|
|
3402
4142
|
|
|
3403
4143
|
function StyledRoot({
|
|
3404
4144
|
children,
|
|
@@ -3628,6 +4368,10 @@ const NotSmallTopbar = ({
|
|
|
3628
4368
|
const updateStore = useUpdateStore();
|
|
3629
4369
|
const useRedesignedSwitcher = useFeature(FEATURE_FLAGS.APP_SWITCHER_REDESIGN, props.features);
|
|
3630
4370
|
const productSearchEnabled = useFeature(FEATURE_FLAGS.PRODUCT_SEARCH, props.features);
|
|
4371
|
+
useRecordRecentlyVisited({
|
|
4372
|
+
enabled: productSearchEnabled,
|
|
4373
|
+
env: props.env
|
|
4374
|
+
});
|
|
3631
4375
|
React__default.useEffect(() => {
|
|
3632
4376
|
const handleToolbarClick = event => {
|
|
3633
4377
|
const target = event.target;
|
|
@@ -3668,7 +4412,9 @@ const NotSmallTopbar = ({
|
|
|
3668
4412
|
style: {
|
|
3669
4413
|
margin: "auto"
|
|
3670
4414
|
}
|
|
3671
|
-
}), !notificationsEnabled && notifications(), productSearchEnabled ? (/*#__PURE__*/React__default.createElement(ProductSearch,
|
|
4415
|
+
}), !notificationsEnabled && notifications(), productSearchEnabled ? (/*#__PURE__*/React__default.createElement(ProductSearch, {
|
|
4416
|
+
apps: props.apps
|
|
4417
|
+
})) : search({
|
|
3672
4418
|
hideRoutes: () => setRoutesVisible(false),
|
|
3673
4419
|
showRoutes: () => setRoutesVisible(true)
|
|
3674
4420
|
}), /*#__PURE__*/React__default.createElement(UserMenuTrigger, {
|
|
@@ -31744,6 +32490,11 @@ const SmallTopbar = ({
|
|
|
31744
32490
|
const [routesMenuVisible, setRoutesMenuVisible] = React__default.useState(false);
|
|
31745
32491
|
const [productSwitcherVisible, setProductSwitcherVisible] = React__default.useState(false);
|
|
31746
32492
|
const useRedesignedSwitcher = useFeature(FEATURE_FLAGS.APP_SWITCHER_REDESIGN, props.features);
|
|
32493
|
+
const productSearchEnabled = useFeature(FEATURE_FLAGS.PRODUCT_SEARCH, props.features);
|
|
32494
|
+
useRecordRecentlyVisited({
|
|
32495
|
+
enabled: productSearchEnabled,
|
|
32496
|
+
env: props.env
|
|
32497
|
+
});
|
|
31747
32498
|
const activeRoute = props.routes.filter(({
|
|
31748
32499
|
props: {
|
|
31749
32500
|
active
|
|
@@ -31952,55 +32703,6 @@ const topbarTheme = {
|
|
|
31952
32703
|
id: "topbar"
|
|
31953
32704
|
};
|
|
31954
32705
|
|
|
31955
|
-
function parseValue(element) {
|
|
31956
|
-
const datatype = element.getAttribute("datatype");
|
|
31957
|
-
switch (datatype) {
|
|
31958
|
-
case "boolean":
|
|
31959
|
-
return element.content === "true" || element.content === "1";
|
|
31960
|
-
default:
|
|
31961
|
-
return element.content;
|
|
31962
|
-
}
|
|
31963
|
-
}
|
|
31964
|
-
function getAllPcoMeta() {
|
|
31965
|
-
const elements = document.head.querySelectorAll(`meta[property^="pco:"]`);
|
|
31966
|
-
const meta = {};
|
|
31967
|
-
elements.forEach(e => {
|
|
31968
|
-
const property = e.getAttribute("property");
|
|
31969
|
-
meta[property.substring("pco:".length)] = parseValue(e);
|
|
31970
|
-
});
|
|
31971
|
-
return meta;
|
|
31972
|
-
}
|
|
31973
|
-
const store = createStore(getAllPcoMeta());
|
|
31974
|
-
const observer = new MutationObserver(() => {
|
|
31975
|
-
const prevState = store.getState();
|
|
31976
|
-
const nextState = getAllPcoMeta();
|
|
31977
|
-
if (!shallowCompare(prevState, nextState)) store.setState(nextState);
|
|
31978
|
-
});
|
|
31979
|
-
let hasTriggeredObserve = false;
|
|
31980
|
-
function observe() {
|
|
31981
|
-
if (hasTriggeredObserve) return;
|
|
31982
|
-
hasTriggeredObserve = true;
|
|
31983
|
-
// The module-load snapshot above can predate meta tags that appear later
|
|
31984
|
-
// in <head> (e.g. pco:resource, yielded right before </head>) if this
|
|
31985
|
-
// module's own script tag isn't deferred and runs mid-parse. Those tags
|
|
31986
|
-
// never mutate afterward, so the observer alone would never catch them.
|
|
31987
|
-
// Re-read once now, since observe() is only called once a consumer is
|
|
31988
|
-
// actually mounting, which is always after the document has fully parsed.
|
|
31989
|
-
const freshState = getAllPcoMeta();
|
|
31990
|
-
if (!shallowCompare(store.getState(), freshState)) {
|
|
31991
|
-
store.setState(freshState);
|
|
31992
|
-
}
|
|
31993
|
-
observer.observe(document.head, {
|
|
31994
|
-
attributes: true,
|
|
31995
|
-
childList: true,
|
|
31996
|
-
subtree: true
|
|
31997
|
-
});
|
|
31998
|
-
}
|
|
31999
|
-
function usePcoMeta() {
|
|
32000
|
-
observe();
|
|
32001
|
-
return useSyncExternalStore(store.subscribe, () => store.getState());
|
|
32002
|
-
}
|
|
32003
|
-
|
|
32004
32706
|
var styles$4 = {"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"};
|
|
32005
32707
|
|
|
32006
32708
|
function isPresent(value) {
|