@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.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$l = {"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$l["accessibility-toolbar"]
|
|
57
57
|
}, /*#__PURE__*/React.createElement(tapestry.Link, {
|
|
58
58
|
href: "#main-content",
|
|
59
59
|
kind: "primary",
|
|
@@ -370,11 +370,11 @@ const X = ({
|
|
|
370
370
|
fill: fill
|
|
371
371
|
})));
|
|
372
372
|
|
|
373
|
-
var styles$
|
|
373
|
+
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"};
|
|
374
374
|
|
|
375
375
|
function AnnouncementIcon() {
|
|
376
376
|
return /*#__PURE__*/React.createElement("svg", {
|
|
377
|
-
className: styles$
|
|
377
|
+
className: styles$k["announcement-icon"],
|
|
378
378
|
width: "16",
|
|
379
379
|
height: "16",
|
|
380
380
|
viewBox: "0 0 16 16",
|
|
@@ -499,7 +499,7 @@ let StyledAnnouncement$1 = class StyledAnnouncement extends React.Component {
|
|
|
499
499
|
} = this.props;
|
|
500
500
|
const isPromo = variant === "promo";
|
|
501
501
|
return /*#__PURE__*/React.createElement("div", {
|
|
502
|
-
className: classnames(styles$
|
|
502
|
+
className: classnames(styles$k["announcement"], styles$k[`announcement--${variant !== null && variant !== void 0 ? variant : "platform"}`]),
|
|
503
503
|
"data-announcement-variant": variant !== null && variant !== void 0 ? variant : "platform",
|
|
504
504
|
style: isPromo ? {
|
|
505
505
|
backgroundImage: promoBackgroundUrl,
|
|
@@ -508,7 +508,7 @@ let StyledAnnouncement$1 = class StyledAnnouncement extends React.Component {
|
|
|
508
508
|
backgroundSize: "cover"
|
|
509
509
|
} : undefined
|
|
510
510
|
}, !isPromo && /*#__PURE__*/React.createElement(AnnouncementIcon, null), /*#__PURE__*/React.createElement("div", {
|
|
511
|
-
className: styles$
|
|
511
|
+
className: styles$k["announcement-content"],
|
|
512
512
|
style: style,
|
|
513
513
|
...platformProps
|
|
514
514
|
}));
|
|
@@ -557,7 +557,7 @@ function StyledDismissButton$1({
|
|
|
557
557
|
const isPromo = variant === "promo";
|
|
558
558
|
return /*#__PURE__*/React.createElement("button", {
|
|
559
559
|
type: "button",
|
|
560
|
-
className: classnames(styles$
|
|
560
|
+
className: classnames(styles$k.dismiss, isPromo && styles$k["dismiss--promo"]),
|
|
561
561
|
...props
|
|
562
562
|
}, /*#__PURE__*/React.createElement(X, {
|
|
563
563
|
fill: isPromo ? "currentColor" : "white",
|
|
@@ -637,9 +637,9 @@ class StyledAnnouncement extends React.Component {
|
|
|
637
637
|
...platformProps
|
|
638
638
|
} = this.props;
|
|
639
639
|
return /*#__PURE__*/React.createElement("div", {
|
|
640
|
-
className: styles$
|
|
640
|
+
className: styles$k["announcement"]
|
|
641
641
|
}, /*#__PURE__*/React.createElement(AnnouncementIcon, null), /*#__PURE__*/React.createElement("div", {
|
|
642
|
-
className: styles$
|
|
642
|
+
className: styles$k["announcement-content"],
|
|
643
643
|
style: style,
|
|
644
644
|
...platformProps
|
|
645
645
|
}));
|
|
@@ -650,7 +650,7 @@ function StyledDismissButton({
|
|
|
650
650
|
}) {
|
|
651
651
|
return /*#__PURE__*/React.createElement("button", {
|
|
652
652
|
type: "button",
|
|
653
|
-
className: styles$
|
|
653
|
+
className: styles$k.dismiss,
|
|
654
654
|
...props
|
|
655
655
|
}, /*#__PURE__*/React.createElement(X, {
|
|
656
656
|
fill: "white",
|
|
@@ -1772,7 +1772,7 @@ function MonoAppIcon({
|
|
|
1772
1772
|
})) : (/*#__PURE__*/React.createElement("div", null, "appName not supported")));
|
|
1773
1773
|
}
|
|
1774
1774
|
|
|
1775
|
-
var styles$
|
|
1775
|
+
var styles$j = {"icon-slot":"icon-slot-m3ZIk","name":"name-UNqbW"};
|
|
1776
1776
|
|
|
1777
1777
|
const AppRow = ({
|
|
1778
1778
|
appName,
|
|
@@ -1781,24 +1781,24 @@ const AppRow = ({
|
|
|
1781
1781
|
}) => {
|
|
1782
1782
|
if (appName === "Accounts") {
|
|
1783
1783
|
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
|
|
1784
|
-
className: styles$
|
|
1784
|
+
className: styles$j["icon-slot"]
|
|
1785
1785
|
}, /*#__PURE__*/React.createElement(MonoAppIcon, {
|
|
1786
1786
|
appName: "Accounts",
|
|
1787
1787
|
colors: colors,
|
|
1788
1788
|
size: iconSize
|
|
1789
1789
|
})), /*#__PURE__*/React.createElement("span", {
|
|
1790
|
-
className: styles$
|
|
1790
|
+
className: styles$j.name
|
|
1791
1791
|
}, "Account settings"));
|
|
1792
1792
|
}
|
|
1793
1793
|
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ColorAppIcon, {
|
|
1794
1794
|
appName: appName.replace(/[\s-]/, ""),
|
|
1795
1795
|
size: iconSize
|
|
1796
1796
|
}), /*#__PURE__*/React.createElement("span", {
|
|
1797
|
-
className: styles$
|
|
1797
|
+
className: styles$j.name
|
|
1798
1798
|
}, appName));
|
|
1799
1799
|
};
|
|
1800
1800
|
|
|
1801
|
-
var styles$
|
|
1801
|
+
var styles$i = {"button":"button-XMbeQ","label":"label-884of","chevron":"chevron-RRg9k"};
|
|
1802
1802
|
|
|
1803
1803
|
const AppsButton = /*#__PURE__*/React.forwardRef(({
|
|
1804
1804
|
appName,
|
|
@@ -1810,10 +1810,10 @@ const AppsButton = /*#__PURE__*/React.forwardRef(({
|
|
|
1810
1810
|
...restProps,
|
|
1811
1811
|
ref: ref,
|
|
1812
1812
|
"aria-label": `App switcher: ${appName}`,
|
|
1813
|
-
className: styles$
|
|
1813
|
+
className: styles$i.button,
|
|
1814
1814
|
kind: "neutral-inline",
|
|
1815
1815
|
label: /*#__PURE__*/React.createElement("span", {
|
|
1816
|
-
className: styles$
|
|
1816
|
+
className: styles$i.label
|
|
1817
1817
|
}, isAccounts ? "Account settings" : appName),
|
|
1818
1818
|
prefix: isAccounts ? (/*#__PURE__*/React.createElement(MonoAppIcon, {
|
|
1819
1819
|
appName: "Accounts",
|
|
@@ -1824,13 +1824,13 @@ const AppsButton = /*#__PURE__*/React.forwardRef(({
|
|
|
1824
1824
|
size: 22
|
|
1825
1825
|
})),
|
|
1826
1826
|
suffix: /*#__PURE__*/React.createElement(DisclosureChevronIcon, {
|
|
1827
|
-
className: `${styles$
|
|
1827
|
+
className: `${styles$i.chevron} symbol`
|
|
1828
1828
|
})
|
|
1829
1829
|
});
|
|
1830
1830
|
});
|
|
1831
1831
|
AppsButton.displayName = "AppsButton";
|
|
1832
1832
|
|
|
1833
|
-
var styles$
|
|
1833
|
+
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"};
|
|
1834
1834
|
|
|
1835
1835
|
const AppsMenu = ({
|
|
1836
1836
|
appName,
|
|
@@ -1851,8 +1851,8 @@ const AppsMenu = ({
|
|
|
1851
1851
|
}, /*#__PURE__*/React.createElement("a", {
|
|
1852
1852
|
"aria-current": isCurrent ? "page" : undefined,
|
|
1853
1853
|
"aria-label": name,
|
|
1854
|
-
className: classnames(styles$
|
|
1855
|
-
[styles$
|
|
1854
|
+
className: classnames(styles$h.link, {
|
|
1855
|
+
[styles$h.current]: isCurrent
|
|
1856
1856
|
}),
|
|
1857
1857
|
"data-pendo": `app-switcher--change-app--${name}`,
|
|
1858
1858
|
"data-turbolinks": false,
|
|
@@ -1868,22 +1868,22 @@ const AppsMenu = ({
|
|
|
1868
1868
|
colors: colors
|
|
1869
1869
|
})), /*#__PURE__*/React.createElement(tapestry.PopoverContent, {
|
|
1870
1870
|
"aria-label": "Switch app",
|
|
1871
|
-
className: styles$
|
|
1871
|
+
className: styles$h["popover-content"],
|
|
1872
1872
|
placement: "bottom start"
|
|
1873
1873
|
}, /*#__PURE__*/React.createElement("nav", {
|
|
1874
|
-
className: styles$
|
|
1874
|
+
className: styles$h.nav,
|
|
1875
1875
|
"data-pendo": "app-switcher"
|
|
1876
1876
|
}, /*#__PURE__*/React.createElement("ul", {
|
|
1877
|
-
className: styles$
|
|
1877
|
+
className: styles$h.list,
|
|
1878
1878
|
role: "list"
|
|
1879
1879
|
}, homeProduct && renderProductLink(homeProduct.name), restProducts.map(({
|
|
1880
1880
|
name
|
|
1881
1881
|
}) => renderProductLink(name)), accountsProduct && (/*#__PURE__*/React.createElement("li", {
|
|
1882
|
-
className: styles$
|
|
1882
|
+
className: styles$h["accounts-item"]
|
|
1883
1883
|
}, /*#__PURE__*/React.createElement("a", {
|
|
1884
1884
|
"aria-current": appName === accountsProduct.name ? "page" : undefined,
|
|
1885
1885
|
"aria-label": "Account settings",
|
|
1886
|
-
className: classnames(styles$
|
|
1886
|
+
className: classnames(styles$h.link, styles$h["accounts-link"]),
|
|
1887
1887
|
"data-pendo": "app-switcher--change-app--Accounts",
|
|
1888
1888
|
"data-turbolinks": false,
|
|
1889
1889
|
href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name)
|
|
@@ -2091,7 +2091,7 @@ function MonoAppName({
|
|
|
2091
2091
|
})) : (/*#__PURE__*/React.createElement("div", null, "appName not supported"));
|
|
2092
2092
|
}
|
|
2093
2093
|
|
|
2094
|
-
var styles$
|
|
2094
|
+
var styles$g = {"unbutton":"unbutton-lEbTR"};
|
|
2095
2095
|
|
|
2096
2096
|
function Unbutton({
|
|
2097
2097
|
className,
|
|
@@ -2100,7 +2100,7 @@ function Unbutton({
|
|
|
2100
2100
|
...nativeProps
|
|
2101
2101
|
}) {
|
|
2102
2102
|
return /*#__PURE__*/React.createElement("button", {
|
|
2103
|
-
className: classnames(styles$
|
|
2103
|
+
className: classnames(styles$g.unbutton, className),
|
|
2104
2104
|
style: {
|
|
2105
2105
|
...style
|
|
2106
2106
|
},
|
|
@@ -2109,7 +2109,7 @@ function Unbutton({
|
|
|
2109
2109
|
});
|
|
2110
2110
|
}
|
|
2111
2111
|
|
|
2112
|
-
var styles$
|
|
2112
|
+
var styles$f = {"button":"button-wsipc","button-accounts":"button-accounts-zQmBM","chevron":"chevron-obyBF","is-expanded":"is-expanded-PoCQ9"};
|
|
2113
2113
|
|
|
2114
2114
|
const LegacyAppsButton = ({
|
|
2115
2115
|
appName,
|
|
@@ -2124,8 +2124,8 @@ const LegacyAppsButton = ({
|
|
|
2124
2124
|
"aria-haspopup": "true",
|
|
2125
2125
|
"aria-label": `App switcher: ${appName}`,
|
|
2126
2126
|
onClick: onClick,
|
|
2127
|
-
className: classnames(styles$
|
|
2128
|
-
[styles$
|
|
2127
|
+
className: classnames(styles$f.button, {
|
|
2128
|
+
[styles$f["is-expanded"]]: expanded
|
|
2129
2129
|
}),
|
|
2130
2130
|
type: "button",
|
|
2131
2131
|
style: style,
|
|
@@ -2134,7 +2134,7 @@ const LegacyAppsButton = ({
|
|
|
2134
2134
|
appName: "Accounts",
|
|
2135
2135
|
colors: colors
|
|
2136
2136
|
}), /*#__PURE__*/React.createElement("div", {
|
|
2137
|
-
className: styles$
|
|
2137
|
+
className: styles$f["button-accounts"]
|
|
2138
2138
|
}, "Account settings"))) : (/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ColorAppIcon, {
|
|
2139
2139
|
appName: appName.replace(/[\s-]/, "")
|
|
2140
2140
|
}), /*#__PURE__*/React.createElement(MonoAppName, {
|
|
@@ -2142,7 +2142,7 @@ const LegacyAppsButton = ({
|
|
|
2142
2142
|
key: "mono-app-name",
|
|
2143
2143
|
size: 24
|
|
2144
2144
|
}))), /*#__PURE__*/React.createElement(DisclosureChevronIcon, {
|
|
2145
|
-
className: styles$
|
|
2145
|
+
className: styles$f.chevron,
|
|
2146
2146
|
key: "disclosure-chevron-icon"
|
|
2147
2147
|
}));
|
|
2148
2148
|
};
|
|
@@ -2275,7 +2275,7 @@ class Outsider extends React.Component {
|
|
|
2275
2275
|
}
|
|
2276
2276
|
}
|
|
2277
2277
|
|
|
2278
|
-
var styles$
|
|
2278
|
+
var styles$e = {"popup-root":"popup-root-SeL5u","popup":"popup-nQXYW","is-visible":"is-visible-3DAUP"};
|
|
2279
2279
|
|
|
2280
2280
|
const Popup = ({
|
|
2281
2281
|
className,
|
|
@@ -2283,8 +2283,8 @@ const Popup = ({
|
|
|
2283
2283
|
isVisible,
|
|
2284
2284
|
...nativeProps
|
|
2285
2285
|
}) => (/*#__PURE__*/React.createElement(C, {
|
|
2286
|
-
className: classnames(styles$
|
|
2287
|
-
[styles$
|
|
2286
|
+
className: classnames(styles$e.popup, {
|
|
2287
|
+
[styles$e["is-visible"]]: isVisible
|
|
2288
2288
|
}, className),
|
|
2289
2289
|
...nativeProps
|
|
2290
2290
|
}));
|
|
@@ -2292,11 +2292,11 @@ const Popup = ({
|
|
|
2292
2292
|
const PopupRoot = ({
|
|
2293
2293
|
...nativeProps
|
|
2294
2294
|
}) => (/*#__PURE__*/React.createElement("div", {
|
|
2295
|
-
className: styles$
|
|
2295
|
+
className: styles$e["popup-root"],
|
|
2296
2296
|
...nativeProps
|
|
2297
2297
|
}));
|
|
2298
2298
|
|
|
2299
|
-
var styles$
|
|
2299
|
+
var styles$d = {"popup":"popup-bVhSa"};
|
|
2300
2300
|
|
|
2301
2301
|
const LegacyAppsMenu = ({
|
|
2302
2302
|
appName,
|
|
@@ -2334,7 +2334,7 @@ const LegacyAppsMenu = ({
|
|
|
2334
2334
|
cleanup: !visible,
|
|
2335
2335
|
component: Outsider,
|
|
2336
2336
|
onOutsideClick: toggle,
|
|
2337
|
-
className: styles$
|
|
2337
|
+
className: styles$d.popup,
|
|
2338
2338
|
isVisible: visible
|
|
2339
2339
|
}, /*#__PURE__*/React.createElement("menu", {
|
|
2340
2340
|
ref: menuRef,
|
|
@@ -3244,7 +3244,97 @@ const useUpdateStore = () => React.useCallback(updates => {
|
|
|
3244
3244
|
});
|
|
3245
3245
|
}, []);
|
|
3246
3246
|
|
|
3247
|
+
const isMac = () => /Mac|iPhone|iPad|iPod/.test(navigator.platform);
|
|
3248
|
+
const hasCommandKey = event => isMac() ? event.metaKey : event.ctrlKey;
|
|
3249
|
+
const commandKeyLabel = () => isMac() ? "⌘" : "Ctrl+";
|
|
3250
|
+
|
|
3251
|
+
const STORAGE_KEY = "topbar.product_search.last_product";
|
|
3252
|
+
function readLastProduct() {
|
|
3253
|
+
try {
|
|
3254
|
+
return localStorage.getItem(STORAGE_KEY);
|
|
3255
|
+
} catch {
|
|
3256
|
+
return null;
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
function saveLastProduct(slug) {
|
|
3260
|
+
try {
|
|
3261
|
+
localStorage.setItem(STORAGE_KEY, slug);
|
|
3262
|
+
} catch {
|
|
3263
|
+
// Storage can be full or blocked; search still works without it.
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
var styles$c = {"visuallyHidden":"visuallyHidden-C28ju"};
|
|
3268
|
+
|
|
3269
|
+
var styles$b = {"tab":"tab-lQLDx"};
|
|
3270
|
+
|
|
3271
|
+
// useId isn't available on React 17, which our peer range still supports.
|
|
3272
|
+
let instanceCount$1 = 0;
|
|
3273
|
+
function useInstanceId(prefix) {
|
|
3274
|
+
const [id] = React.useState(() => `${prefix}-${++instanceCount$1}`);
|
|
3275
|
+
return id;
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
function ProductTabs({
|
|
3279
|
+
children,
|
|
3280
|
+
onKeyDown,
|
|
3281
|
+
onSelect,
|
|
3282
|
+
products,
|
|
3283
|
+
selected
|
|
3284
|
+
}) {
|
|
3285
|
+
const id = useInstanceId("product-tabs");
|
|
3286
|
+
const tabId = slug => `${id}-tab-${slug}`;
|
|
3287
|
+
const tabRefs = React.useRef(new Map());
|
|
3288
|
+
const handleKeyDown = event => {
|
|
3289
|
+
var _a;
|
|
3290
|
+
const offset = {
|
|
3291
|
+
ArrowLeft: -1,
|
|
3292
|
+
ArrowRight: 1
|
|
3293
|
+
}[event.key];
|
|
3294
|
+
if (offset) {
|
|
3295
|
+
event.preventDefault();
|
|
3296
|
+
const index = products.indexOf(selected);
|
|
3297
|
+
const next = products[(index + offset + products.length) % products.length];
|
|
3298
|
+
onSelect(next.slug);
|
|
3299
|
+
(_a = tabRefs.current.get(next.slug)) === null || _a === void 0 ? void 0 : _a.focus();
|
|
3300
|
+
} else {
|
|
3301
|
+
onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
|
|
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
|
+
|
|
3247
3334
|
function SearchResult({
|
|
3335
|
+
highlighted,
|
|
3336
|
+
id,
|
|
3337
|
+
onHighlight,
|
|
3248
3338
|
result
|
|
3249
3339
|
}) {
|
|
3250
3340
|
const {
|
|
@@ -3252,62 +3342,222 @@ function SearchResult({
|
|
|
3252
3342
|
title,
|
|
3253
3343
|
url
|
|
3254
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.
|
|
3255
3356
|
return /*#__PURE__*/React.createElement("a", {
|
|
3256
|
-
|
|
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
|
|
3257
3365
|
}, /*#__PURE__*/React.createElement("div", null, title), /*#__PURE__*/React.createElement("div", null, summarize(metadata)));
|
|
3258
3366
|
}
|
|
3259
3367
|
function summarize(metadata) {
|
|
3260
3368
|
return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
|
|
3261
3369
|
}
|
|
3262
3370
|
|
|
3263
|
-
const
|
|
3264
|
-
function
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
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
|
+
});
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
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
|
+
|
|
3446
|
+
const CACHE_TTL_MS = 60000;
|
|
3447
|
+
const MAX_ENTRIES$1 = 50;
|
|
3448
|
+
const cache = new Map();
|
|
3449
|
+
function cachedRequest(url) {
|
|
3450
|
+
const cached = cache.get(url);
|
|
3451
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
3452
|
+
// Re-inserting keeps the Map in recently-used order for eviction.
|
|
3453
|
+
cache.delete(url);
|
|
3454
|
+
cache.set(url, cached);
|
|
3455
|
+
return cached.promise;
|
|
3456
|
+
} else {
|
|
3457
|
+
return fetchIntoCache(url);
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
function fetchIntoCache(url) {
|
|
3461
|
+
const promise = apiRequest(defaultFetch, url).then(({
|
|
3462
|
+
json,
|
|
3463
|
+
ok
|
|
3464
|
+
}) => {
|
|
3465
|
+
if (!ok || json === null) throw new Error(`Request failed: ${url}`);
|
|
3466
|
+
return json;
|
|
3269
3467
|
});
|
|
3468
|
+
// In-flight entries never expire, so every caller shares the one fetch.
|
|
3469
|
+
const entry = {
|
|
3470
|
+
expiresAt: Infinity,
|
|
3471
|
+
promise
|
|
3472
|
+
};
|
|
3473
|
+
cache.set(url, entry);
|
|
3474
|
+
promise.then(() => entry.expiresAt = Date.now() + CACHE_TTL_MS, () => cache.get(url) === entry && cache.delete(url));
|
|
3475
|
+
if (cache.size > MAX_ENTRIES$1) cache.delete(cache.keys().next().value);
|
|
3476
|
+
return promise;
|
|
3477
|
+
}
|
|
3478
|
+
function useCachedRequest(url) {
|
|
3479
|
+
const [settled, setSettled] = React.useState();
|
|
3270
3480
|
React.useEffect(() => {
|
|
3271
|
-
if (
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3481
|
+
if (url) {
|
|
3482
|
+
let current = true;
|
|
3483
|
+
cachedRequest(url).then(data => current && setSettled({
|
|
3484
|
+
data,
|
|
3485
|
+
url
|
|
3486
|
+
}), () => current && setSettled({
|
|
3487
|
+
failed: true,
|
|
3488
|
+
url
|
|
3489
|
+
}));
|
|
3490
|
+
return () => {
|
|
3491
|
+
current = false;
|
|
3492
|
+
};
|
|
3493
|
+
} else {
|
|
3275
3494
|
return undefined;
|
|
3276
3495
|
}
|
|
3277
|
-
|
|
3278
|
-
|
|
3496
|
+
}, [url]);
|
|
3497
|
+
if (!url) {
|
|
3498
|
+
return {
|
|
3499
|
+
status: "idle"
|
|
3500
|
+
};
|
|
3501
|
+
} else if ((settled === null || settled === void 0 ? void 0 : settled.url) !== url) {
|
|
3502
|
+
return {
|
|
3503
|
+
data: settled && "data" in settled ? settled.data : undefined,
|
|
3279
3504
|
status: "loading"
|
|
3280
|
-
});
|
|
3281
|
-
apiRequest(defaultFetch, productSearchUrl(debouncedQuery)).then(({
|
|
3282
|
-
json,
|
|
3283
|
-
ok
|
|
3284
|
-
}) => {
|
|
3285
|
-
if (!current) return;
|
|
3286
|
-
setSearch(ok && json ? {
|
|
3287
|
-
groups: groupByType(json),
|
|
3288
|
-
status: "done"
|
|
3289
|
-
} : {
|
|
3290
|
-
status: "error"
|
|
3291
|
-
});
|
|
3292
|
-
});
|
|
3293
|
-
return () => {
|
|
3294
|
-
current = false;
|
|
3295
3505
|
};
|
|
3296
|
-
}
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3506
|
+
} else if ("data" in settled) {
|
|
3507
|
+
return {
|
|
3508
|
+
data: settled.data,
|
|
3509
|
+
status: "done"
|
|
3510
|
+
};
|
|
3511
|
+
} else {
|
|
3512
|
+
return {
|
|
3513
|
+
status: "error"
|
|
3514
|
+
};
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
const SEARCH_DEBOUNCE_MS = 300;
|
|
3519
|
+
function useProductSearch(product, term) {
|
|
3520
|
+
const query = term.trim();
|
|
3521
|
+
const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
|
|
3522
|
+
const request = useCachedRequest(debouncedQuery ? productSearchUrl(product, debouncedQuery) : null);
|
|
3523
|
+
const response = "data" in request ? request.data : undefined;
|
|
3524
|
+
const settledProduct = useSettledProduct(product, request.status);
|
|
3525
|
+
// While a request loads, the previous response stays on screen, but only
|
|
3526
|
+
// if it was for this product: People results must never show under the
|
|
3527
|
+
// Services tab.
|
|
3528
|
+
const visibleResponse = request.status === "done" || settledProduct === product ? response : undefined;
|
|
3529
|
+
const groups = React.useMemo(() => visibleResponse && groupByType(visibleResponse), [visibleResponse]);
|
|
3530
|
+
if (!query) {
|
|
3531
|
+
return {
|
|
3532
|
+
status: "idle"
|
|
3533
|
+
};
|
|
3534
|
+
} else if (query !== debouncedQuery) {
|
|
3535
|
+
return {
|
|
3536
|
+
groups,
|
|
3537
|
+
status: "loading"
|
|
3538
|
+
};
|
|
3539
|
+
} else if (request.status === "done") {
|
|
3540
|
+
return {
|
|
3541
|
+
groups: groups !== null && groups !== void 0 ? groups : [],
|
|
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"
|
|
3552
|
+
};
|
|
3553
|
+
}
|
|
3304
3554
|
}
|
|
3305
|
-
function productSearchUrl(query) {
|
|
3555
|
+
function productSearchUrl(product, query) {
|
|
3306
3556
|
const params = new URLSearchParams({
|
|
3307
3557
|
per_page: "100",
|
|
3308
3558
|
"where[term]": query
|
|
3309
3559
|
});
|
|
3310
|
-
return `${window.location.origin}/~api/
|
|
3560
|
+
return `${window.location.origin}/~api/${product}/v2/product_search_results?${params}`;
|
|
3311
3561
|
}
|
|
3312
3562
|
function groupByType({
|
|
3313
3563
|
data,
|
|
@@ -3332,6 +3582,14 @@ function groupByType({
|
|
|
3332
3582
|
};
|
|
3333
3583
|
return [...groups.values()].sort((a, b) => rank(a.type) - rank(b.type));
|
|
3334
3584
|
}
|
|
3585
|
+
// The product the most recent finished request was for.
|
|
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
|
+
}
|
|
3335
3593
|
function useDebouncedValue(value, delay) {
|
|
3336
3594
|
const [debounced, setDebounced] = React.useState(value);
|
|
3337
3595
|
React.useEffect(() => {
|
|
@@ -3341,53 +3599,199 @@ function useDebouncedValue(value, delay) {
|
|
|
3341
3599
|
return debounced;
|
|
3342
3600
|
}
|
|
3343
3601
|
|
|
3344
|
-
|
|
3345
|
-
|
|
3602
|
+
const COPIED_FEEDBACK_MS = 1500;
|
|
3603
|
+
const verticalStep = key => ({
|
|
3604
|
+
ArrowDown: 1,
|
|
3605
|
+
ArrowUp: -1
|
|
3606
|
+
})[key];
|
|
3607
|
+
function useResultNavigation(results) {
|
|
3608
|
+
// Remembering which results the index belongs to puts the highlight back on
|
|
3609
|
+
// the first result whenever new ones arrive, without an effect to reset it.
|
|
3610
|
+
const [highlight, setHighlight] = React.useState({
|
|
3611
|
+
index: 0,
|
|
3612
|
+
results
|
|
3613
|
+
});
|
|
3614
|
+
const highlightedIndex = highlight.results === results ? highlight.index : 0;
|
|
3615
|
+
const highlighted = results[highlightedIndex];
|
|
3616
|
+
const [copiedUrl, setCopiedUrl] = React.useState(null);
|
|
3617
|
+
React.useEffect(() => {
|
|
3618
|
+
if (copiedUrl) {
|
|
3619
|
+
const timeout = setTimeout(() => setCopiedUrl(null), COPIED_FEEDBACK_MS);
|
|
3620
|
+
return () => clearTimeout(timeout);
|
|
3621
|
+
} else {
|
|
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
|
|
3346
3657
|
}) {
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
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
|
+
});
|
|
3364
3740
|
}
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3741
|
+
});
|
|
3742
|
+
return () => {
|
|
3743
|
+
current = false;
|
|
3744
|
+
};
|
|
3745
|
+
}, [accessibleKey]);
|
|
3746
|
+
if ((settled === null || settled === void 0 ? void 0 : settled.accessibleKey) === accessibleKey) {
|
|
3747
|
+
return {
|
|
3748
|
+
products: settled.products,
|
|
3749
|
+
status: "done"
|
|
3750
|
+
};
|
|
3751
|
+
} else {
|
|
3752
|
+
return {
|
|
3753
|
+
status: "loading"
|
|
3754
|
+
};
|
|
3376
3755
|
}
|
|
3377
3756
|
}
|
|
3757
|
+
function inSwitcherOrder(apps) {
|
|
3758
|
+
return groupApps(apps).restProducts.flatMap(app => SEARCHABLE_PRODUCTS.filter(product => product.appName === app.name));
|
|
3759
|
+
}
|
|
3378
3760
|
|
|
3379
|
-
const
|
|
3380
|
-
|
|
3381
|
-
|
|
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
|
+
}) {
|
|
3382
3781
|
const [open, setOpen] = React.useState(false);
|
|
3383
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.
|
|
3384
3785
|
const handleKeyDown = event => {
|
|
3385
|
-
if (
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
event
|
|
3390
|
-
|
|
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
|
+
}
|
|
3391
3795
|
};
|
|
3392
3796
|
window.addEventListener("keydown", handleKeyDown, {
|
|
3393
3797
|
capture: true
|
|
@@ -3397,7 +3801,7 @@ function ProductSearch() {
|
|
|
3397
3801
|
});
|
|
3398
3802
|
}, []);
|
|
3399
3803
|
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
|
|
3400
|
-
"aria-keyshortcuts": isMac() ? "Meta+K" : "Control+K"
|
|
3804
|
+
"aria-keyshortcuts": `${isMac() ? "Meta+K" : "Control+K"} /`,
|
|
3401
3805
|
"aria-label": "Search",
|
|
3402
3806
|
onClick: () => setOpen(true),
|
|
3403
3807
|
type: "button"
|
|
@@ -3408,21 +3812,357 @@ function ProductSearch() {
|
|
|
3408
3812
|
dismissible: true,
|
|
3409
3813
|
onClose: () => setOpen(false),
|
|
3410
3814
|
open: open
|
|
3411
|
-
}, /*#__PURE__*/React.createElement(SearchPanel,
|
|
3815
|
+
}, /*#__PURE__*/React.createElement(SearchPanel, {
|
|
3816
|
+
products: products
|
|
3817
|
+
})));
|
|
3412
3818
|
}
|
|
3413
|
-
function SearchPanel(
|
|
3819
|
+
function SearchPanel({
|
|
3820
|
+
products
|
|
3821
|
+
}) {
|
|
3822
|
+
var _a, _b;
|
|
3823
|
+
const listboxId = useInstanceId("product-search-results");
|
|
3414
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
|
+
};
|
|
3856
|
+
React.useEffect(() => {
|
|
3857
|
+
if (hasTerm) saveLastProduct(product.slug);
|
|
3858
|
+
}, [hasTerm, product.slug]);
|
|
3415
3859
|
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("input", {
|
|
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,
|
|
3416
3864
|
"aria-label": "Search",
|
|
3417
3865
|
autoFocus: true,
|
|
3418
3866
|
onChange: event => setTerm(event.target.value),
|
|
3867
|
+
onFocus: () => setAnnounceHighlight(false),
|
|
3868
|
+
onKeyDown: navigation.handleKeyDown,
|
|
3419
3869
|
placeholder: "Search",
|
|
3870
|
+
ref: inputRef,
|
|
3871
|
+
role: "combobox",
|
|
3420
3872
|
type: "search",
|
|
3421
3873
|
value: term
|
|
3422
|
-
}), /*#__PURE__*/React.createElement(
|
|
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,
|
|
3423
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
|
|
3424
3891
|
}));
|
|
3425
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];
|
|
3906
|
+
}
|
|
3907
|
+
|
|
3908
|
+
function parseValue(element) {
|
|
3909
|
+
const datatype = element.getAttribute("datatype");
|
|
3910
|
+
switch (datatype) {
|
|
3911
|
+
case "boolean":
|
|
3912
|
+
return element.content === "true" || element.content === "1";
|
|
3913
|
+
default:
|
|
3914
|
+
return element.content;
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
function getAllPcoMeta() {
|
|
3918
|
+
const elements = document.head.querySelectorAll(`meta[property^="pco:"]`);
|
|
3919
|
+
const meta = {};
|
|
3920
|
+
elements.forEach(e => {
|
|
3921
|
+
const property = e.getAttribute("property");
|
|
3922
|
+
meta[property.substring("pco:".length)] = parseValue(e);
|
|
3923
|
+
});
|
|
3924
|
+
return meta;
|
|
3925
|
+
}
|
|
3926
|
+
// usePcoMeta only reads each tag's content. Recently Visited also needs the
|
|
3927
|
+
// pco:recently_visitable tag's `metadata` attribute, which can change while
|
|
3928
|
+
// the content stays the same, so it is tracked in its own store.
|
|
3929
|
+
function getRecentlyVisitableMetadata() {
|
|
3930
|
+
var _a, _b;
|
|
3931
|
+
return (_b = (_a = document.head.querySelector(`meta[property="pco:recently_visitable"]`)) === null || _a === void 0 ? void 0 : _a.getAttribute("metadata")) !== null && _b !== void 0 ? _b : null;
|
|
3932
|
+
}
|
|
3933
|
+
const store = createStore(getAllPcoMeta());
|
|
3934
|
+
const recentlyVisitableMetadataStore = createStore({
|
|
3935
|
+
metadata: getRecentlyVisitableMetadata()
|
|
3936
|
+
});
|
|
3937
|
+
function sync() {
|
|
3938
|
+
const nextState = getAllPcoMeta();
|
|
3939
|
+
if (!shallowCompare(store.getState(), nextState)) store.setState(nextState);
|
|
3940
|
+
const metadata = getRecentlyVisitableMetadata();
|
|
3941
|
+
if (recentlyVisitableMetadataStore.getState().metadata !== metadata) {
|
|
3942
|
+
recentlyVisitableMetadataStore.setState({
|
|
3943
|
+
metadata
|
|
3944
|
+
});
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
const observer = new MutationObserver(sync);
|
|
3948
|
+
let hasTriggeredObserve = false;
|
|
3949
|
+
function observe() {
|
|
3950
|
+
if (hasTriggeredObserve) return;
|
|
3951
|
+
hasTriggeredObserve = true;
|
|
3952
|
+
// The module-load snapshot above can predate meta tags that appear later
|
|
3953
|
+
// in <head> (e.g. pco:resource, yielded right before </head>) if this
|
|
3954
|
+
// module's own script tag isn't deferred and runs mid-parse. Those tags
|
|
3955
|
+
// never mutate afterward, so the observer alone would never catch them.
|
|
3956
|
+
// Re-read once now, since observe() is only called once a consumer is
|
|
3957
|
+
// actually mounting, which is always after the document has fully parsed.
|
|
3958
|
+
sync();
|
|
3959
|
+
observer.observe(document.head, {
|
|
3960
|
+
attributes: true,
|
|
3961
|
+
childList: true,
|
|
3962
|
+
subtree: true
|
|
3963
|
+
});
|
|
3964
|
+
}
|
|
3965
|
+
function usePcoMeta() {
|
|
3966
|
+
observe();
|
|
3967
|
+
return shim.useSyncExternalStore(store.subscribe, () => store.getState());
|
|
3968
|
+
}
|
|
3969
|
+
function useRecentlyVisitableMetadata() {
|
|
3970
|
+
observe();
|
|
3971
|
+
return shim.useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
const MAX_ENTRIES = 20;
|
|
3975
|
+
const FORMAT_VERSION = 1;
|
|
3976
|
+
const MAX_TEXT_LENGTH = 200;
|
|
3977
|
+
const PRODUCTS = ["accounts", "calendar", "check-ins", "giving", "groups", "home", "login", "people", "publishing", "registrations", "services"];
|
|
3978
|
+
function isId(value) {
|
|
3979
|
+
return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
|
|
3980
|
+
}
|
|
3981
|
+
// A PCRL looks like `<product>/v2/<resource_type>/<id>`.
|
|
3982
|
+
function isPcrl(value) {
|
|
3983
|
+
const segments = value.split("/");
|
|
3984
|
+
return segments.length === 4 && PRODUCTS.some(product => product === segments[0]) && isId(segments[3]);
|
|
3985
|
+
}
|
|
3986
|
+
function isNonBlankString(value) {
|
|
3987
|
+
return typeof value === "string" && value.trim() !== "";
|
|
3988
|
+
}
|
|
3989
|
+
function isRecord(value) {
|
|
3990
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3991
|
+
}
|
|
3992
|
+
function entryKey(entry) {
|
|
3993
|
+
if (entry.type === "resource") {
|
|
3994
|
+
return `resource:${entry.pcrl}`;
|
|
3995
|
+
} else {
|
|
3996
|
+
return `page:${entry.product}:${entry.path}`;
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
function isEntry(value) {
|
|
4000
|
+
if (!isRecord(value)) {
|
|
4001
|
+
return false;
|
|
4002
|
+
} else if (value.type === "resource") {
|
|
4003
|
+
return typeof value.pcrl === "string" && isPcrl(value.pcrl);
|
|
4004
|
+
} else if (value.type === "page") {
|
|
4005
|
+
return isNonBlankString(value.product) && isNonBlankString(value.title) && isNonBlankString(value.path) && typeof value.details === "string";
|
|
4006
|
+
} else {
|
|
4007
|
+
return false;
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
// Cuts on code points so a surrogate pair is never split.
|
|
4011
|
+
function clampText(value) {
|
|
4012
|
+
return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
|
|
4013
|
+
}
|
|
4014
|
+
function clampEntry(entry) {
|
|
4015
|
+
if (entry.type === "resource") {
|
|
4016
|
+
return entry;
|
|
4017
|
+
} else {
|
|
4018
|
+
return {
|
|
4019
|
+
...entry,
|
|
4020
|
+
details: clampText(entry.details),
|
|
4021
|
+
title: clampText(entry.title)
|
|
4022
|
+
};
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
// Recents are disposable: anything unexpected reads as an empty list (or, for
|
|
4026
|
+
// a single bad entry, a list without it) rather than throwing.
|
|
4027
|
+
function parseRecentlyVisited(value, personId) {
|
|
4028
|
+
let parsed = null;
|
|
4029
|
+
try {
|
|
4030
|
+
parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
|
|
4031
|
+
} catch {
|
|
4032
|
+
parsed = null;
|
|
4033
|
+
}
|
|
4034
|
+
if (isRecord(parsed) && parsed.v === FORMAT_VERSION && parsed.person === personId && Array.isArray(parsed.entries)) {
|
|
4035
|
+
return parsed.entries.filter(isEntry);
|
|
4036
|
+
} else {
|
|
4037
|
+
return [];
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
function addVisit(entries, visit) {
|
|
4041
|
+
const key = entryKey(visit);
|
|
4042
|
+
return [clampEntry(visit), ...entries.filter(entry => entryKey(entry) !== key)].slice(0, MAX_ENTRIES);
|
|
4043
|
+
}
|
|
4044
|
+
function serializeRecentlyVisited(entries, personId) {
|
|
4045
|
+
return JSON.stringify({
|
|
4046
|
+
entries,
|
|
4047
|
+
person: personId,
|
|
4048
|
+
v: FORMAT_VERSION
|
|
4049
|
+
});
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4052
|
+
// The product whose URL, in this environment, has the current page's host.
|
|
4053
|
+
// Reversing pcoUrl handles every environment @planningcenter/url knows about,
|
|
4054
|
+
// and guarantees the stored product rebuilds a working link.
|
|
4055
|
+
function currentProduct() {
|
|
4056
|
+
var _a;
|
|
4057
|
+
return (_a = PRODUCTS.find(product => new URL(pcoUrl$1.pcoUrl(product)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
|
|
4058
|
+
}
|
|
4059
|
+
function detailsFromMetadata(metadata) {
|
|
4060
|
+
var _a;
|
|
4061
|
+
try {
|
|
4062
|
+
const details = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) === null || _a === void 0 ? void 0 : _a.details;
|
|
4063
|
+
return typeof details === "string" ? details : "";
|
|
4064
|
+
} catch {
|
|
4065
|
+
return "";
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
4068
|
+
// pco:resource is the more specific signal, so it wins when both tags are
|
|
4069
|
+
// present. A resource that isn't a PCRL is ignored rather than recorded.
|
|
4070
|
+
function visitFromPage({
|
|
4071
|
+
metadata,
|
|
4072
|
+
resource,
|
|
4073
|
+
title
|
|
4074
|
+
}) {
|
|
4075
|
+
const product = currentProduct();
|
|
4076
|
+
if (resource && isPcrl(resource)) {
|
|
4077
|
+
return {
|
|
4078
|
+
pcrl: resource,
|
|
4079
|
+
type: "resource"
|
|
4080
|
+
};
|
|
4081
|
+
} else if (title && title.trim() !== "" && product) {
|
|
4082
|
+
return {
|
|
4083
|
+
details: detailsFromMetadata(metadata),
|
|
4084
|
+
path: window.location.pathname,
|
|
4085
|
+
product,
|
|
4086
|
+
title,
|
|
4087
|
+
type: "page"
|
|
4088
|
+
};
|
|
4089
|
+
} else {
|
|
4090
|
+
return null;
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
// The entry Recently Visited would record for the current page, or null when
|
|
4094
|
+
// the page isn't eligible. Shared by the recorder and the debug panel so the
|
|
4095
|
+
// panel always shows what actually gets stored.
|
|
4096
|
+
function useCurrentVisit() {
|
|
4097
|
+
const {
|
|
4098
|
+
recently_visitable: title,
|
|
4099
|
+
resource
|
|
4100
|
+
} = usePcoMeta();
|
|
4101
|
+
const metadata = useRecentlyVisitableMetadata();
|
|
4102
|
+
return React.useMemo(() => visitFromPage({
|
|
4103
|
+
metadata,
|
|
4104
|
+
resource,
|
|
4105
|
+
title
|
|
4106
|
+
}), [metadata, resource, title]);
|
|
4107
|
+
}
|
|
4108
|
+
// Products hand the person's ID to AppsProvider, not to the topbar itself,
|
|
4109
|
+
// and several pass it as a number.
|
|
4110
|
+
function useCurrentPersonId() {
|
|
4111
|
+
const {
|
|
4112
|
+
userId
|
|
4113
|
+
} = useStore();
|
|
4114
|
+
return userId == null ? undefined : String(userId);
|
|
4115
|
+
}
|
|
4116
|
+
|
|
4117
|
+
// Per-origin localStorage is a stopgap: each product keeps its own list until
|
|
4118
|
+
// PCA-409 moves this to storage shared across product subdomains. Only this
|
|
4119
|
+
// file should need to change when it does.
|
|
4120
|
+
function storageKey({
|
|
4121
|
+
env,
|
|
4122
|
+
personId
|
|
4123
|
+
}) {
|
|
4124
|
+
return `topbar.recently_visited.${env}.${personId}`;
|
|
4125
|
+
}
|
|
4126
|
+
// localStorage doesn't notify the tab that wrote it, so readers in this tab
|
|
4127
|
+
// learn about new visits from this counter instead.
|
|
4128
|
+
const changes = createStore({
|
|
4129
|
+
version: 0
|
|
4130
|
+
});
|
|
4131
|
+
function readRecentlyVisited(scope) {
|
|
4132
|
+
try {
|
|
4133
|
+
return parseRecentlyVisited(localStorage.getItem(storageKey(scope)), scope.personId);
|
|
4134
|
+
} catch {
|
|
4135
|
+
return [];
|
|
4136
|
+
}
|
|
4137
|
+
}
|
|
4138
|
+
function recordVisit(visit, scope) {
|
|
4139
|
+
if (isEntry(visit)) {
|
|
4140
|
+
try {
|
|
4141
|
+
localStorage.setItem(storageKey(scope), serializeRecentlyVisited(addVisit(readRecentlyVisited(scope), visit), scope.personId));
|
|
4142
|
+
changes.setState({
|
|
4143
|
+
version: changes.getState().version + 1
|
|
4144
|
+
});
|
|
4145
|
+
} catch {
|
|
4146
|
+
// Storage can be full or blocked; recents are disposable.
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4151
|
+
function useRecordRecentlyVisited({
|
|
4152
|
+
enabled,
|
|
4153
|
+
env
|
|
4154
|
+
}) {
|
|
4155
|
+
const personId = useCurrentPersonId();
|
|
4156
|
+
const visit = useCurrentVisit();
|
|
4157
|
+
React.useEffect(() => {
|
|
4158
|
+
if (enabled && personId && visit) {
|
|
4159
|
+
recordVisit(visit, {
|
|
4160
|
+
env,
|
|
4161
|
+
personId
|
|
4162
|
+
});
|
|
4163
|
+
}
|
|
4164
|
+
}, [enabled, env, personId, visit]);
|
|
4165
|
+
}
|
|
3426
4166
|
|
|
3427
4167
|
function StyledRoot({
|
|
3428
4168
|
children,
|
|
@@ -3652,6 +4392,10 @@ const NotSmallTopbar = ({
|
|
|
3652
4392
|
const updateStore = useUpdateStore();
|
|
3653
4393
|
const useRedesignedSwitcher = useFeature(FEATURE_FLAGS.APP_SWITCHER_REDESIGN, props.features);
|
|
3654
4394
|
const productSearchEnabled = useFeature(FEATURE_FLAGS.PRODUCT_SEARCH, props.features);
|
|
4395
|
+
useRecordRecentlyVisited({
|
|
4396
|
+
enabled: productSearchEnabled,
|
|
4397
|
+
env: props.env
|
|
4398
|
+
});
|
|
3655
4399
|
React.useEffect(() => {
|
|
3656
4400
|
const handleToolbarClick = event => {
|
|
3657
4401
|
const target = event.target;
|
|
@@ -3692,7 +4436,9 @@ const NotSmallTopbar = ({
|
|
|
3692
4436
|
style: {
|
|
3693
4437
|
margin: "auto"
|
|
3694
4438
|
}
|
|
3695
|
-
}), !notificationsEnabled && notifications(), productSearchEnabled ? (/*#__PURE__*/React.createElement(ProductSearch,
|
|
4439
|
+
}), !notificationsEnabled && notifications(), productSearchEnabled ? (/*#__PURE__*/React.createElement(ProductSearch, {
|
|
4440
|
+
apps: props.apps
|
|
4441
|
+
})) : search({
|
|
3696
4442
|
hideRoutes: () => setRoutesVisible(false),
|
|
3697
4443
|
showRoutes: () => setRoutesVisible(true)
|
|
3698
4444
|
}), /*#__PURE__*/React.createElement(UserMenuTrigger, {
|
|
@@ -31768,6 +32514,11 @@ const SmallTopbar = ({
|
|
|
31768
32514
|
const [routesMenuVisible, setRoutesMenuVisible] = React.useState(false);
|
|
31769
32515
|
const [productSwitcherVisible, setProductSwitcherVisible] = React.useState(false);
|
|
31770
32516
|
const useRedesignedSwitcher = useFeature(FEATURE_FLAGS.APP_SWITCHER_REDESIGN, props.features);
|
|
32517
|
+
const productSearchEnabled = useFeature(FEATURE_FLAGS.PRODUCT_SEARCH, props.features);
|
|
32518
|
+
useRecordRecentlyVisited({
|
|
32519
|
+
enabled: productSearchEnabled,
|
|
32520
|
+
env: props.env
|
|
32521
|
+
});
|
|
31771
32522
|
const activeRoute = props.routes.filter(({
|
|
31772
32523
|
props: {
|
|
31773
32524
|
active
|
|
@@ -31976,55 +32727,6 @@ const topbarTheme = {
|
|
|
31976
32727
|
id: "topbar"
|
|
31977
32728
|
};
|
|
31978
32729
|
|
|
31979
|
-
function parseValue(element) {
|
|
31980
|
-
const datatype = element.getAttribute("datatype");
|
|
31981
|
-
switch (datatype) {
|
|
31982
|
-
case "boolean":
|
|
31983
|
-
return element.content === "true" || element.content === "1";
|
|
31984
|
-
default:
|
|
31985
|
-
return element.content;
|
|
31986
|
-
}
|
|
31987
|
-
}
|
|
31988
|
-
function getAllPcoMeta() {
|
|
31989
|
-
const elements = document.head.querySelectorAll(`meta[property^="pco:"]`);
|
|
31990
|
-
const meta = {};
|
|
31991
|
-
elements.forEach(e => {
|
|
31992
|
-
const property = e.getAttribute("property");
|
|
31993
|
-
meta[property.substring("pco:".length)] = parseValue(e);
|
|
31994
|
-
});
|
|
31995
|
-
return meta;
|
|
31996
|
-
}
|
|
31997
|
-
const store = createStore(getAllPcoMeta());
|
|
31998
|
-
const observer = new MutationObserver(() => {
|
|
31999
|
-
const prevState = store.getState();
|
|
32000
|
-
const nextState = getAllPcoMeta();
|
|
32001
|
-
if (!shallowCompare(prevState, nextState)) store.setState(nextState);
|
|
32002
|
-
});
|
|
32003
|
-
let hasTriggeredObserve = false;
|
|
32004
|
-
function observe() {
|
|
32005
|
-
if (hasTriggeredObserve) return;
|
|
32006
|
-
hasTriggeredObserve = true;
|
|
32007
|
-
// The module-load snapshot above can predate meta tags that appear later
|
|
32008
|
-
// in <head> (e.g. pco:resource, yielded right before </head>) if this
|
|
32009
|
-
// module's own script tag isn't deferred and runs mid-parse. Those tags
|
|
32010
|
-
// never mutate afterward, so the observer alone would never catch them.
|
|
32011
|
-
// Re-read once now, since observe() is only called once a consumer is
|
|
32012
|
-
// actually mounting, which is always after the document has fully parsed.
|
|
32013
|
-
const freshState = getAllPcoMeta();
|
|
32014
|
-
if (!shallowCompare(store.getState(), freshState)) {
|
|
32015
|
-
store.setState(freshState);
|
|
32016
|
-
}
|
|
32017
|
-
observer.observe(document.head, {
|
|
32018
|
-
attributes: true,
|
|
32019
|
-
childList: true,
|
|
32020
|
-
subtree: true
|
|
32021
|
-
});
|
|
32022
|
-
}
|
|
32023
|
-
function usePcoMeta() {
|
|
32024
|
-
observe();
|
|
32025
|
-
return shim.useSyncExternalStore(store.subscribe, () => store.getState());
|
|
32026
|
-
}
|
|
32027
|
-
|
|
32028
32730
|
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"};
|
|
32029
32731
|
|
|
32030
32732
|
function isPresent(value) {
|