@servicetitan/navigation 14.3.0 → 14.5.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/components/titan-layout/layout-sidebar.d.ts.map +1 -1
- package/dist/components/titan-layout/layout-sidebar.js +30 -11
- package/dist/components/titan-layout/layout-sidebar.js.map +1 -1
- package/dist/components/titan-layout/layout-sidebar.module.less +1 -1
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.js +1 -1
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.js.map +1 -1
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog.d.ts.map +1 -1
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog.js +69 -107
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog.js.map +1 -1
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog.module.less +70 -127
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog.module.less.d.ts +2 -0
- package/dist/utils/__tests__/catalog-groups.test.d.ts +2 -0
- package/dist/utils/__tests__/catalog-groups.test.d.ts.map +1 -0
- package/dist/utils/__tests__/side-nav.test.d.ts +2 -0
- package/dist/utils/__tests__/side-nav.test.d.ts.map +1 -0
- package/dist/utils/catalog-groups.d.ts +11 -0
- package/dist/utils/catalog-groups.d.ts.map +1 -0
- package/dist/utils/catalog-groups.js +66 -0
- package/dist/utils/catalog-groups.js.map +1 -0
- package/dist/utils/side-nav.d.ts +17 -0
- package/dist/utils/side-nav.d.ts.map +1 -1
- package/dist/utils/side-nav.js +32 -0
- package/dist/utils/side-nav.js.map +1 -1
- package/package.json +2 -2
- package/src/components/titan-layout/__tests__/navigation-catalog-reset.test.tsx +7 -3
- package/src/components/titan-layout/layout-sidebar.module.less +1 -1
- package/src/components/titan-layout/layout-sidebar.tsx +34 -10
- package/src/components/titan-layout/navigation-catalog/navigation-catalog-trigger.tsx +1 -1
- package/src/components/titan-layout/navigation-catalog/navigation-catalog.module.less +70 -127
- package/src/components/titan-layout/navigation-catalog/navigation-catalog.module.less.d.ts +2 -0
- package/src/components/titan-layout/navigation-catalog/navigation-catalog.tsx +107 -134
- package/src/utils/__tests__/catalog-groups.test.ts +97 -0
- package/src/utils/__tests__/side-nav.test.ts +77 -0
- package/src/utils/catalog-groups.ts +57 -0
- package/src/utils/side-nav.ts +42 -0
|
@@ -5,26 +5,12 @@ import IconStar from '@servicetitan/anvil2/assets/icons/material/round/star.svg'
|
|
|
5
5
|
import IconStarBorder from '@servicetitan/anvil2/assets/icons/material/round/star_border.svg';
|
|
6
6
|
import classNames from 'classnames';
|
|
7
7
|
import { Fragment, useMemo, useRef, useState } from 'react';
|
|
8
|
+
import { filterCatalogGroups, getCatalogChildren, getCatalogSubmenuGroups } from '../../../utils/catalog-groups';
|
|
8
9
|
import { getCounterTag } from '../../../utils/side-nav';
|
|
9
10
|
import { BadgeTag } from '../../badge-tag';
|
|
10
11
|
import { useTitanLayoutContext } from '../layout-context';
|
|
11
12
|
import { NavigationCatalogFooter } from './navigation-catalog-footer';
|
|
12
13
|
import * as styles from './navigation-catalog.module.less';
|
|
13
|
-
/** The pages beneath an item, flattened out of its submenu groups. */ const childrenOf = (item)=>{
|
|
14
|
-
var _ref;
|
|
15
|
-
var _item_submenu;
|
|
16
|
-
const seen = new Set();
|
|
17
|
-
return ((_ref = (_item_submenu = item.submenu) === null || _item_submenu === void 0 ? void 0 : _item_submenu.groups) !== null && _ref !== void 0 ? _ref : []).flatMap((group)=>{
|
|
18
|
-
var _group_links;
|
|
19
|
-
return ((_group_links = group.links) !== null && _group_links !== void 0 ? _group_links : []).filter((link)=>{
|
|
20
|
-
if (seen.has(link.id)) {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
seen.add(link.id);
|
|
24
|
-
return true;
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
};
|
|
28
14
|
/** Shown in place of the pin control for a destination the user may not unpin. */ const LockedPin = ()=>/*#__PURE__*/ _jsxs(Tooltip, {
|
|
29
15
|
children: [
|
|
30
16
|
/*#__PURE__*/ _jsx(Tooltip.Trigger, {
|
|
@@ -34,6 +20,9 @@ import * as styles from './navigation-catalog.module.less';
|
|
|
34
20
|
})
|
|
35
21
|
}),
|
|
36
22
|
/*#__PURE__*/ _jsx(Tooltip.Content, {
|
|
23
|
+
style: {
|
|
24
|
+
pointerEvents: 'none'
|
|
25
|
+
},
|
|
37
26
|
children: "Cannot be removed from Favorites"
|
|
38
27
|
})
|
|
39
28
|
]
|
|
@@ -52,28 +41,32 @@ const PinToggle = ({ pinned, label, onToggle })=>{
|
|
|
52
41
|
children: /*#__PURE__*/ _jsx(Button, {
|
|
53
42
|
appearance: "ghost",
|
|
54
43
|
size: "small",
|
|
55
|
-
icon: pinned ? IconStar : IconStarBorder,
|
|
56
|
-
// Only a control that covers nothing but pinned rows is lit.
|
|
44
|
+
icon: pinned === true ? IconStar : IconStarBorder,
|
|
57
45
|
className: classNames(styles.star, pinned && styles.starActive),
|
|
58
46
|
"aria-label": label,
|
|
59
47
|
"aria-pressed": pinned,
|
|
48
|
+
"data-pendo": "global-nav-favorite-star",
|
|
60
49
|
onClick: toggle
|
|
61
50
|
})
|
|
62
51
|
}),
|
|
63
52
|
/*#__PURE__*/ _jsx(Tooltip.Content, {
|
|
64
|
-
|
|
53
|
+
style: {
|
|
54
|
+
pointerEvents: 'none'
|
|
55
|
+
},
|
|
56
|
+
children: pinned === 'mixed' ? 'Add this module to Favorites' : pinned ? 'Remove from Favorites' : 'Add to Favorites'
|
|
65
57
|
})
|
|
66
58
|
]
|
|
67
59
|
});
|
|
68
60
|
};
|
|
69
61
|
const CatalogRows = ({ item, scopeIds, isPinned, isLocked, onTogglePin, onToggleAll, onNavigate })=>{
|
|
70
62
|
const { NavigationComponent } = useTitanLayoutContext();
|
|
71
|
-
const
|
|
63
|
+
const submenuGroups = getCatalogSubmenuGroups(item);
|
|
72
64
|
/*
|
|
73
65
|
* Minus anything locked — locked destinations are always present, so the module's control
|
|
74
66
|
* neither reports on them nor acts on them.
|
|
75
67
|
*/ const everything = scopeIds.filter((id)=>!isLocked(id));
|
|
76
68
|
const anyPinned = everything.some(isPinned);
|
|
69
|
+
const allPinned = everything.length > 0 && everything.every(isPinned);
|
|
77
70
|
const tag = getCounterTag(item.counter, item.tag);
|
|
78
71
|
return /*#__PURE__*/ _jsxs("div", {
|
|
79
72
|
className: styles.moduleGroup,
|
|
@@ -89,11 +82,12 @@ const CatalogRows = ({ item, scopeIds, isPinned, isLocked, onTogglePin, onToggle
|
|
|
89
82
|
children: [
|
|
90
83
|
item.icon && /*#__PURE__*/ _jsx(Icon, {
|
|
91
84
|
svg: item.icon,
|
|
92
|
-
size: "
|
|
85
|
+
size: "medium",
|
|
93
86
|
className: styles.moduleIcon
|
|
94
87
|
}),
|
|
95
88
|
/*#__PURE__*/ _jsx(Text, {
|
|
96
89
|
inline: true,
|
|
90
|
+
size: "small",
|
|
97
91
|
className: styles.moduleTitle,
|
|
98
92
|
children: item.title
|
|
99
93
|
}),
|
|
@@ -104,96 +98,63 @@ const CatalogRows = ({ item, scopeIds, isPinned, isLocked, onTogglePin, onToggle
|
|
|
104
98
|
]
|
|
105
99
|
}),
|
|
106
100
|
isLocked(item.id) ? /*#__PURE__*/ _jsx(LockedPin, {}) : /*#__PURE__*/ _jsx(PinToggle, {
|
|
107
|
-
pinned: anyPinned,
|
|
108
|
-
label:
|
|
109
|
-
onToggle: ()=>onToggleAll(everything, !
|
|
101
|
+
pinned: allPinned ? true : anyPinned ? 'mixed' : false,
|
|
102
|
+
label: allPinned ? `Unpin ${item.title} from the navigation` : `Pin ${item.title} to the navigation`,
|
|
103
|
+
onToggle: ()=>onToggleAll(everything, !allPinned)
|
|
110
104
|
})
|
|
111
105
|
]
|
|
112
106
|
}),
|
|
113
|
-
|
|
114
|
-
/*
|
|
115
|
-
* Page titles are only unique within their item — "Settings" and "Reports" recur
|
|
116
|
-
* across the catalog — and the row that tells them apart is a separate element, so
|
|
117
|
-
* it is not part of any of these names. Anything reading the rows on their own, a
|
|
118
|
-
* screen reader running through the links or a voice command naming one, would
|
|
119
|
-
* otherwise be given the same name several times.
|
|
120
|
-
*/ const qualifiedTitle = `${item.title}: ${child.title}`;
|
|
121
|
-
const childPinned = isPinned(child.id);
|
|
122
|
-
const childTag = getCounterTag(child.counter, child.tag);
|
|
123
|
-
return /*#__PURE__*/ _jsxs("div", {
|
|
124
|
-
className: styles.row,
|
|
107
|
+
submenuGroups.map((group)=>/*#__PURE__*/ _jsxs("div", {
|
|
125
108
|
children: [
|
|
126
|
-
/*#__PURE__*/
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
children: [
|
|
132
|
-
/*#__PURE__*/ _jsx(Text, {
|
|
133
|
-
inline: true,
|
|
134
|
-
className: styles.childTitle,
|
|
135
|
-
children: child.title
|
|
136
|
-
}),
|
|
137
|
-
!!childTag && /*#__PURE__*/ _jsx(BadgeTag, {
|
|
138
|
-
data: childTag,
|
|
139
|
-
className: styles.badge
|
|
140
|
-
})
|
|
141
|
-
]
|
|
109
|
+
group.title && /*#__PURE__*/ _jsx(Text, {
|
|
110
|
+
variant: "eyebrow",
|
|
111
|
+
size: "small",
|
|
112
|
+
className: styles.submenuTitle,
|
|
113
|
+
children: group.title
|
|
142
114
|
}),
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
115
|
+
group.links.map((child)=>{
|
|
116
|
+
/*
|
|
117
|
+
* Page titles are only unique within their item — "Settings" and "Reports" recur
|
|
118
|
+
* across the catalog — and the row that tells them apart is a separate element, so
|
|
119
|
+
* it is not part of any of these names. Anything reading the rows on their own, a
|
|
120
|
+
* screen reader running through the links or a voice command naming one, would
|
|
121
|
+
* otherwise be given the same name several times.
|
|
122
|
+
*/ const qualifiedTitle = `${item.title}: ${child.title}`;
|
|
123
|
+
const childPinned = isPinned(child.id);
|
|
124
|
+
const childTag = getCounterTag(child.counter, child.tag);
|
|
125
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
126
|
+
className: styles.row,
|
|
127
|
+
children: [
|
|
128
|
+
/*#__PURE__*/ _jsxs(NavigationComponent, {
|
|
129
|
+
to: child.to,
|
|
130
|
+
className: classNames(styles.link, styles.childLink),
|
|
131
|
+
onClick: onNavigate,
|
|
132
|
+
"aria-label": qualifiedTitle,
|
|
133
|
+
children: [
|
|
134
|
+
/*#__PURE__*/ _jsx(Text, {
|
|
135
|
+
inline: true,
|
|
136
|
+
className: styles.childTitle,
|
|
137
|
+
children: child.title
|
|
138
|
+
}),
|
|
139
|
+
!!childTag && /*#__PURE__*/ _jsx(BadgeTag, {
|
|
140
|
+
data: childTag,
|
|
141
|
+
className: styles.badge
|
|
142
|
+
})
|
|
143
|
+
]
|
|
144
|
+
}),
|
|
145
|
+
isLocked(child.id) ? /*#__PURE__*/ _jsx(LockedPin, {}) : /*#__PURE__*/ _jsx(PinToggle, {
|
|
146
|
+
pinned: childPinned,
|
|
147
|
+
label: childPinned ? `Unpin ${qualifiedTitle} from the navigation` : `Pin ${qualifiedTitle} to the navigation`,
|
|
148
|
+
onToggle: ()=>onTogglePin(child.id, item.id)
|
|
149
|
+
})
|
|
150
|
+
]
|
|
151
|
+
}, child.id);
|
|
147
152
|
})
|
|
148
153
|
]
|
|
149
|
-
},
|
|
150
|
-
})
|
|
154
|
+
}, group.links[0].id))
|
|
151
155
|
]
|
|
152
156
|
});
|
|
153
157
|
};
|
|
154
|
-
/**
|
|
155
|
-
* Narrows the catalog to the entries matching `query`.
|
|
156
|
-
*
|
|
157
|
-
* Matching is literal and never cascades: a row survives only when its own title matches. An item
|
|
158
|
-
* whose title matches is kept as a bare row without the pages beneath it, and a section is kept
|
|
159
|
-
* only for whatever survived under it, so the list only ever shrinks as the query grows.
|
|
160
|
-
*/ const filterGroups = (groups, query)=>{
|
|
161
|
-
const needle = query.trim().toLowerCase();
|
|
162
|
-
if (!needle) {
|
|
163
|
-
return groups;
|
|
164
|
-
}
|
|
165
|
-
return groups.reduce((out, group)=>{
|
|
166
|
-
const items = group.items.reduce((kept, item)=>{
|
|
167
|
-
const matchingChildren = childrenOf(item).filter((child)=>child.title.toLowerCase().includes(needle));
|
|
168
|
-
if (matchingChildren.length > 0) {
|
|
169
|
-
kept.push({
|
|
170
|
-
...item,
|
|
171
|
-
submenu: {
|
|
172
|
-
groups: [
|
|
173
|
-
{
|
|
174
|
-
title: '',
|
|
175
|
-
links: matchingChildren
|
|
176
|
-
}
|
|
177
|
-
]
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
} else if (item.title.toLowerCase().includes(needle)) {
|
|
181
|
-
kept.push({
|
|
182
|
-
...item,
|
|
183
|
-
submenu: undefined
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
return kept;
|
|
187
|
-
}, []);
|
|
188
|
-
if (items.length > 0) {
|
|
189
|
-
out.push({
|
|
190
|
-
...group,
|
|
191
|
-
items
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
return out;
|
|
195
|
-
}, []);
|
|
196
|
-
};
|
|
197
158
|
/**
|
|
198
159
|
* Every destination the host offers, grouped into sections, with a star on every row.
|
|
199
160
|
*
|
|
@@ -201,6 +162,7 @@ const CatalogRows = ({ item, scopeIds, isPinned, isLocked, onTogglePin, onToggle
|
|
|
201
162
|
* user may reach, so the catalog cannot offer a destination they do not have.
|
|
202
163
|
*/ export const NavigationCatalog = ({ open, onClose, groups, pinnedIds, lockedIds, onPinnedIdsChange, onResetToDefaults })=>{
|
|
203
164
|
const [query, setQuery] = useState('');
|
|
165
|
+
const isSearching = query.trim().length > 0;
|
|
204
166
|
const searchRef = useRef(null);
|
|
205
167
|
/*
|
|
206
168
|
* Resolved here rather than by the host, so a catalog that costs requests to enumerate is paid
|
|
@@ -209,7 +171,7 @@ const CatalogRows = ({ item, scopeIds, isPinned, isLocked, onTogglePin, onToggle
|
|
|
209
171
|
*/ const resolvedGroups = useMemo(()=>groups(), [
|
|
210
172
|
groups
|
|
211
173
|
]);
|
|
212
|
-
const visibleGroups = useMemo(()=>
|
|
174
|
+
const visibleGroups = useMemo(()=>filterCatalogGroups(resolvedGroups, query), [
|
|
213
175
|
resolvedGroups,
|
|
214
176
|
query
|
|
215
177
|
]);
|
|
@@ -220,7 +182,7 @@ const CatalogRows = ({ item, scopeIds, isPinned, isLocked, onTogglePin, onToggle
|
|
|
220
182
|
item.id,
|
|
221
183
|
[
|
|
222
184
|
item.id,
|
|
223
|
-
...
|
|
185
|
+
...getCatalogChildren(item).map((child)=>child.id)
|
|
224
186
|
]
|
|
225
187
|
])), [
|
|
226
188
|
resolvedGroups
|
|
@@ -322,12 +284,12 @@ const CatalogRows = ({ item, scopeIds, isPinned, isLocked, onTogglePin, onToggle
|
|
|
322
284
|
"aria-label": "Go to"
|
|
323
285
|
}),
|
|
324
286
|
/*#__PURE__*/ _jsx("div", {
|
|
325
|
-
className: styles.results,
|
|
287
|
+
className: classNames(styles.results, isSearching && styles.searchResults),
|
|
326
288
|
children: visibleGroups.length === 0 ? /*#__PURE__*/ _jsx("div", {
|
|
327
289
|
className: styles.empty,
|
|
328
290
|
children: /*#__PURE__*/ _jsx(Text, {
|
|
329
291
|
subdued: true,
|
|
330
|
-
children:
|
|
292
|
+
children: isSearching ? /*#__PURE__*/ _jsxs(Fragment, {
|
|
331
293
|
children: [
|
|
332
294
|
"No navigation items match “",
|
|
333
295
|
query,
|
|
@@ -336,11 +298,11 @@ const CatalogRows = ({ item, scopeIds, isPinned, isLocked, onTogglePin, onToggle
|
|
|
336
298
|
}) : 'There are no navigation items to show.'
|
|
337
299
|
})
|
|
338
300
|
}) : /*#__PURE__*/ _jsx("div", {
|
|
339
|
-
className: styles.columns,
|
|
301
|
+
className: isSearching ? undefined : styles.columns,
|
|
340
302
|
children: visibleGroups.map((group)=>/*#__PURE__*/ _jsxs("div", {
|
|
341
|
-
className: styles.group,
|
|
303
|
+
className: isSearching ? undefined : styles.group,
|
|
342
304
|
children: [
|
|
343
|
-
/*#__PURE__*/ _jsx(Text, {
|
|
305
|
+
!isSearching && /*#__PURE__*/ _jsx(Text, {
|
|
344
306
|
variant: "eyebrow",
|
|
345
307
|
size: "medium",
|
|
346
308
|
className: styles.groupTitle,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/components/titan-layout/navigation-catalog/navigation-catalog.tsx"],"sourcesContent":["import { Button, Dialog, Icon, SearchField, Text, Tooltip } from '@servicetitan/anvil2';\nimport IconLock from '@servicetitan/anvil2/assets/icons/material/round/lock.svg';\nimport IconStar from '@servicetitan/anvil2/assets/icons/material/round/star.svg';\nimport IconStarBorder from '@servicetitan/anvil2/assets/icons/material/round/star_border.svg';\nimport classNames from 'classnames';\nimport { FC, Fragment, MouseEvent, useMemo, useRef, useState } from 'react';\nimport { NavigationItemData } from '../../../utils/navigation';\nimport { getCounterTag } from '../../../utils/side-nav';\nimport { BadgeTag } from '../../badge-tag';\nimport { NavigationCatalogGroup, PinnedNavigationConfig } from '../interface';\nimport { useTitanLayoutContext } from '../layout-context';\nimport { NavigationCatalogFooter } from './navigation-catalog-footer';\nimport * as styles from './navigation-catalog.module.less';\n\n/** The pages beneath an item, flattened out of its submenu groups. */\nconst childrenOf = (item: NavigationItemData) => {\n const seen = new Set<string>();\n\n return (item.submenu?.groups ?? []).flatMap(group =>\n (group.links ?? []).filter(link => {\n if (seen.has(link.id)) {\n return false;\n }\n\n seen.add(link.id);\n return true;\n })\n );\n};\n\n/** Shown in place of the pin control for a destination the user may not unpin. */\nconst LockedPin: FC = () => (\n <Tooltip>\n <Tooltip.Trigger>\n <Icon svg={IconLock} className={styles.lock} />\n </Tooltip.Trigger>\n <Tooltip.Content>Cannot be removed from Favorites</Tooltip.Content>\n </Tooltip>\n);\n\ninterface PinToggleProps {\n pinned: boolean;\n label: string;\n onToggle: () => void;\n}\n\nconst PinToggle: FC<PinToggleProps> = ({ pinned, label, onToggle }) => {\n const toggle = (event: MouseEvent) => {\n // The control sits inside the row but is not a navigation affordance.\n event.preventDefault();\n event.stopPropagation();\n onToggle();\n };\n\n return (\n <Tooltip delay={300}>\n <Tooltip.Trigger>\n <Button\n appearance=\"ghost\"\n size=\"small\"\n icon={pinned ? IconStar : IconStarBorder}\n // Only a control that covers nothing but pinned rows is lit.\n className={classNames(styles.star, pinned && styles.starActive)}\n aria-label={label}\n aria-pressed={pinned}\n onClick={toggle}\n />\n </Tooltip.Trigger>\n <Tooltip.Content>\n {pinned ? 'Remove from Favorites' : 'Add to Favorites'}\n </Tooltip.Content>\n </Tooltip>\n );\n};\n\ninterface CatalogRowsProps {\n item: NavigationItemData;\n /**\n * Every id the module's own control covers, taken from the unfiltered item.\n *\n * `item` may have been narrowed by the search, so deriving this from it would let the control\n * act on only the pages that happen to be visible — silently stranding the rest.\n */\n scopeIds: string[];\n isPinned: (id: string) => boolean;\n isLocked: (id: string) => boolean;\n /** `ownerId` is the module the page belongs to; omitted for the module's own row. */\n onTogglePin: (id: string, ownerId?: string) => void;\n onToggleAll: (ids: string[], shouldPin: boolean) => void;\n onNavigate: () => void;\n}\n\nconst CatalogRows: FC<CatalogRowsProps> = ({\n item,\n scopeIds,\n isPinned,\n isLocked,\n onTogglePin,\n onToggleAll,\n onNavigate,\n}) => {\n const { NavigationComponent } = useTitanLayoutContext();\n const children = childrenOf(item);\n\n /*\n * Minus anything locked — locked destinations are always present, so the module's control\n * neither reports on them nor acts on them.\n */\n const everything = scopeIds.filter(id => !isLocked(id));\n const anyPinned = everything.some(isPinned);\n const tag = getCounterTag(item.counter, item.tag);\n\n return (\n <div className={styles.moduleGroup}>\n <div className={styles.row}>\n <NavigationComponent\n to={item.to}\n className={styles.link}\n onClick={onNavigate}\n aria-label={item.title}\n >\n {item.icon && (\n <Icon svg={item.icon} size=\"small\" className={styles.moduleIcon} />\n )}\n <Text inline className={styles.moduleTitle}>\n {item.title}\n </Text>\n {/*\n * The same counter the rail shows, so a destination that is asking for\n * attention still says so here — the catalog is the only place an unfavourited\n * one is visible at all.\n */}\n {!!tag && <BadgeTag data={tag} className={styles.badge} />}\n </NavigationComponent>\n {/*\n * One control for the whole module: favouriting it takes its pages with it, and\n * unfavouriting it takes them away again.\n *\n * Lit when *any* page under it is favourited, because that is exactly when the\n * module is on the rail — the star reports whether the module is there, not\n * whether every page under it is. Reporting \"all\" instead would leave an unlit\n * star on a module the user can plainly see in the navigation.\n */}\n {isLocked(item.id) ? (\n <LockedPin />\n ) : (\n <PinToggle\n pinned={anyPinned}\n label={\n anyPinned\n ? `Unpin ${item.title} from the navigation`\n : `Pin ${item.title} to the navigation`\n }\n onToggle={() => onToggleAll(everything, !anyPinned)}\n />\n )}\n </div>\n {children.map(child => {\n /*\n * Page titles are only unique within their item — \"Settings\" and \"Reports\" recur\n * across the catalog — and the row that tells them apart is a separate element, so\n * it is not part of any of these names. Anything reading the rows on their own, a\n * screen reader running through the links or a voice command naming one, would\n * otherwise be given the same name several times.\n */\n const qualifiedTitle = `${item.title}: ${child.title}`;\n const childPinned = isPinned(child.id);\n const childTag = getCounterTag(child.counter, child.tag);\n\n return (\n <div key={child.id} className={styles.row}>\n <NavigationComponent\n to={child.to}\n className={classNames(styles.link, styles.childLink)}\n onClick={onNavigate}\n aria-label={qualifiedTitle}\n >\n <Text inline className={styles.childTitle}>\n {child.title}\n </Text>\n {!!childTag && <BadgeTag data={childTag} className={styles.badge} />}\n </NavigationComponent>\n {isLocked(child.id) ? (\n <LockedPin />\n ) : (\n <PinToggle\n pinned={childPinned}\n label={\n childPinned\n ? `Unpin ${qualifiedTitle} from the navigation`\n : `Pin ${qualifiedTitle} to the navigation`\n }\n onToggle={() => onTogglePin(child.id, item.id)}\n />\n )}\n </div>\n );\n })}\n </div>\n );\n};\n\n/**\n * Narrows the catalog to the entries matching `query`.\n *\n * Matching is literal and never cascades: a row survives only when its own title matches. An item\n * whose title matches is kept as a bare row without the pages beneath it, and a section is kept\n * only for whatever survived under it, so the list only ever shrinks as the query grows.\n */\nconst filterGroups = (groups: NavigationCatalogGroup[], query: string) => {\n const needle = query.trim().toLowerCase();\n\n if (!needle) {\n return groups;\n }\n\n return groups.reduce<NavigationCatalogGroup[]>((out, group) => {\n const items = group.items.reduce<NavigationItemData[]>((kept, item) => {\n const matchingChildren = childrenOf(item).filter(child =>\n child.title.toLowerCase().includes(needle)\n );\n\n if (matchingChildren.length > 0) {\n kept.push({\n ...item,\n submenu: { groups: [{ title: '', links: matchingChildren }] },\n });\n } else if (item.title.toLowerCase().includes(needle)) {\n kept.push({ ...item, submenu: undefined });\n }\n\n return kept;\n }, []);\n\n if (items.length > 0) {\n out.push({ ...group, items });\n }\n\n return out;\n }, []);\n};\n\nexport interface NavigationCatalogProps extends PinnedNavigationConfig {\n open: boolean;\n onClose: () => void;\n}\n\n/**\n * Every destination the host offers, grouped into sections, with a star on every row.\n *\n * The catalog shows what it is given and nothing more — the host has already resolved what this\n * user may reach, so the catalog cannot offer a destination they do not have.\n */\nexport const NavigationCatalog: FC<NavigationCatalogProps> = ({\n open,\n onClose,\n groups,\n pinnedIds,\n lockedIds,\n onPinnedIdsChange,\n onResetToDefaults,\n}) => {\n const [query, setQuery] = useState('');\n const searchRef = useRef<HTMLInputElement>(null);\n\n /*\n * Resolved here rather than by the host, so a catalog that costs requests to enumerate is paid\n * for on opening the dialog and not on every page load. This component is mounted only while\n * open, so the call happens once per open unless the host changes the function.\n */\n const resolvedGroups = useMemo(() => groups(), [groups]);\n\n const visibleGroups = useMemo(\n () => filterGroups(resolvedGroups, query),\n [resolvedGroups, query]\n );\n\n /*\n * The pin scope of every item, keyed by id and built before the search narrows anything, so a\n * module's control still covers the pages a query is currently hiding.\n */\n const scopeById = useMemo(\n () =>\n new Map(\n resolvedGroups\n .flatMap(group => group.items)\n .map(item => [item.id, [item.id, ...childrenOf(item).map(child => child.id)]])\n ),\n [resolvedGroups]\n );\n const pinnedSet = useMemo(() => new Set(pinnedIds), [pinnedIds]);\n const lockedSet = useMemo(() => new Set(lockedIds ?? []), [lockedIds]);\n\n const isPinned = (id: string) => pinnedSet.has(id);\n const isLocked = (id: string) => lockedSet.has(id);\n\n /**\n * Favourites or unfavourites one row, carrying its module with it.\n *\n * A page is reached through its module, so the two travel together: favouriting a page brings\n * the module along when it is not already there, and unfavouriting the last one takes the\n * module with it. Leaving the module behind on its own would not read as an emptier\n * navigation — it would put the module back carrying every page it has.\n *\n * Locked ids never reach a proposal. The controls that would move them are not rendered, so\n * that part is belt and braces — but the host's list is the thing that gets persisted, and a\n * locked destination has no business appearing in or disappearing from it.\n */\n const togglePin = (id: string, ownerId?: string) => {\n if (lockedSet.has(id)) {\n return;\n }\n\n /** The module, when it is a separate row that this page can legitimately move. */\n const owner = ownerId !== id && ownerId && !lockedSet.has(ownerId) ? ownerId : undefined;\n\n if (pinnedSet.has(id)) {\n const removing = new Set([id]);\n\n if (owner) {\n const siblings = (scopeById.get(owner) ?? []).filter(\n scoped => scoped !== owner && scoped !== id\n );\n\n if (!siblings.some(sibling => pinnedSet.has(sibling))) {\n removing.add(owner);\n }\n }\n\n onPinnedIdsChange(pinnedIds.filter(pinned => !removing.has(pinned)));\n return;\n }\n\n // Module first, so it reads ahead of its own page in the saved order.\n const additions = owner && !pinnedSet.has(owner) ? [owner, id] : [id];\n\n onPinnedIdsChange([...pinnedIds, ...additions]);\n };\n\n /*\n * One proposal for the whole set rather than one per row, so a host that persists on every\n * change writes once, and every intermediate list — states the user never asked for — never\n * exists.\n */\n const toggleAll = (ids: string[], shouldPin: boolean) => {\n const actionable = ids.filter(id => !lockedSet.has(id));\n\n if (!shouldPin) {\n const removing = new Set(actionable);\n\n onPinnedIdsChange(pinnedIds.filter(pinned => !removing.has(pinned)));\n return;\n }\n\n const additions = actionable.filter(id => !pinnedSet.has(id));\n\n if (additions.length > 0) {\n onPinnedIdsChange([...pinnedIds, ...additions]);\n }\n };\n\n const close = () => {\n setQuery('');\n onClose();\n };\n\n return (\n <Dialog\n open={open}\n onClose={close}\n size=\"xlarge\"\n aria-label=\"All Navigation\"\n className={styles.dialog}\n data-cy=\"navigation-catalog-dialog\"\n >\n <Dialog.Content className={styles.content}>\n <div className={styles.body}>\n <SearchField\n ref={searchRef}\n size=\"small\"\n className={styles.search}\n placeholder=\"Go to ...\"\n value={query}\n onChange={event => setQuery(event.target.value)}\n onClear={() => setQuery('')}\n aria-label=\"Go to\"\n />\n <div className={styles.results}>\n {visibleGroups.length === 0 ? (\n <div className={styles.empty}>\n {/*\n * Two different nothings: a search that matched no row, and a host\n * that offered no rows at all. The second is a real state — the\n * catalog is reachable whether or not there is anything in it — and\n * quoting an empty query at the user does not describe it.\n */}\n <Text subdued>\n {query.trim() ? (\n <Fragment>\n No navigation items match “{query}”.\n </Fragment>\n ) : (\n 'There are no navigation items to show.'\n )}\n </Text>\n </div>\n ) : (\n <div className={styles.columns}>\n {visibleGroups.map(group => (\n <div key={group.title} className={styles.group}>\n {/*\n * `medium` is 0.75rem, a step below the destinations under\n * it. A heading that matches or beats its contents in size\n * competes with them, and this one does not need the room:\n * it is already set apart by case, tracking and a grey that\n * nothing else on the surface uses. `small` is 0.625rem,\n * too small to take in at a glance once uppercased and\n * letter-spaced.\n */}\n <Text\n variant=\"eyebrow\"\n size=\"medium\"\n className={styles.groupTitle}\n >\n {group.title}\n </Text>\n {group.items.map(item => (\n <CatalogRows\n key={item.id}\n item={item}\n scopeIds={scopeById.get(item.id) ?? [item.id]}\n isPinned={isPinned}\n isLocked={isLocked}\n onTogglePin={togglePin}\n onToggleAll={toggleAll}\n onNavigate={close}\n />\n ))}\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n </Dialog.Content>\n <NavigationCatalogFooter\n onRestore={\n onResetToDefaults &&\n (() => {\n onResetToDefaults();\n // The control disappears with the host's reset; keep focus in the dialog.\n searchRef.current?.focus();\n })\n }\n />\n </Dialog>\n );\n};\n"],"names":["Button","Dialog","Icon","SearchField","Text","Tooltip","IconLock","IconStar","IconStarBorder","classNames","Fragment","useMemo","useRef","useState","getCounterTag","BadgeTag","useTitanLayoutContext","NavigationCatalogFooter","styles","childrenOf","item","seen","Set","submenu","groups","flatMap","group","links","filter","link","has","id","add","LockedPin","Trigger","svg","className","lock","Content","PinToggle","pinned","label","onToggle","toggle","event","preventDefault","stopPropagation","delay","appearance","size","icon","star","starActive","aria-label","aria-pressed","onClick","CatalogRows","scopeIds","isPinned","isLocked","onTogglePin","onToggleAll","onNavigate","NavigationComponent","children","everything","anyPinned","some","tag","counter","div","moduleGroup","row","to","title","moduleIcon","inline","moduleTitle","data","badge","map","child","qualifiedTitle","childPinned","childTag","childLink","childTitle","filterGroups","query","needle","trim","toLowerCase","reduce","out","items","kept","matchingChildren","includes","length","push","undefined","NavigationCatalog","open","onClose","pinnedIds","lockedIds","onPinnedIdsChange","onResetToDefaults","setQuery","searchRef","resolvedGroups","visibleGroups","scopeById","Map","pinnedSet","lockedSet","togglePin","ownerId","owner","removing","siblings","get","scoped","sibling","additions","toggleAll","ids","shouldPin","actionable","close","dialog","data-cy","content","body","ref","search","placeholder","value","onChange","target","onClear","results","empty","subdued","columns","variant","groupTitle","onRestore","current","focus"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../src/components/titan-layout/navigation-catalog/navigation-catalog.tsx"],"sourcesContent":["import { Button, Dialog, Icon, SearchField, Text, Tooltip } from '@servicetitan/anvil2';\nimport IconLock from '@servicetitan/anvil2/assets/icons/material/round/lock.svg';\nimport IconStar from '@servicetitan/anvil2/assets/icons/material/round/star.svg';\nimport IconStarBorder from '@servicetitan/anvil2/assets/icons/material/round/star_border.svg';\nimport classNames from 'classnames';\nimport { FC, Fragment, MouseEvent, useMemo, useRef, useState } from 'react';\nimport {\n filterCatalogGroups,\n getCatalogChildren,\n getCatalogSubmenuGroups,\n} from '../../../utils/catalog-groups';\nimport { NavigationItemData } from '../../../utils/navigation';\nimport { getCounterTag } from '../../../utils/side-nav';\nimport { BadgeTag } from '../../badge-tag';\nimport { PinnedNavigationConfig } from '../interface';\nimport { useTitanLayoutContext } from '../layout-context';\nimport { NavigationCatalogFooter } from './navigation-catalog-footer';\nimport * as styles from './navigation-catalog.module.less';\n\n/** Shown in place of the pin control for a destination the user may not unpin. */\nconst LockedPin: FC = () => (\n <Tooltip>\n <Tooltip.Trigger>\n <Icon svg={IconLock} className={styles.lock} />\n </Tooltip.Trigger>\n <Tooltip.Content style={{ pointerEvents: 'none' }}>\n Cannot be removed from Favorites\n </Tooltip.Content>\n </Tooltip>\n);\n\ninterface PinToggleProps {\n /** Mixed modules stay highlighted but use an outline until all pages are pinned. */\n pinned: boolean | 'mixed';\n label: string;\n onToggle: () => void;\n}\n\nconst PinToggle: FC<PinToggleProps> = ({ pinned, label, onToggle }) => {\n const toggle = (event: MouseEvent) => {\n // The control sits inside the row but is not a navigation affordance.\n event.preventDefault();\n event.stopPropagation();\n onToggle();\n };\n\n return (\n <Tooltip delay={300}>\n <Tooltip.Trigger>\n <Button\n appearance=\"ghost\"\n size=\"small\"\n icon={pinned === true ? IconStar : IconStarBorder}\n className={classNames(styles.star, pinned && styles.starActive)}\n aria-label={label}\n aria-pressed={pinned}\n data-pendo=\"global-nav-favorite-star\"\n onClick={toggle}\n />\n </Tooltip.Trigger>\n <Tooltip.Content style={{ pointerEvents: 'none' }}>\n {pinned === 'mixed'\n ? 'Add this module to Favorites'\n : pinned\n ? 'Remove from Favorites'\n : 'Add to Favorites'}\n </Tooltip.Content>\n </Tooltip>\n );\n};\n\ninterface CatalogRowsProps {\n item: NavigationItemData;\n /**\n * Every id the module's own control covers, taken from the unfiltered item.\n *\n * `item` may have been narrowed by the search, so deriving this from it would let the control\n * act on only the pages that happen to be visible — silently stranding the rest.\n */\n scopeIds: string[];\n isPinned: (id: string) => boolean;\n isLocked: (id: string) => boolean;\n /** `ownerId` is the module the page belongs to; omitted for the module's own row. */\n onTogglePin: (id: string, ownerId?: string) => void;\n onToggleAll: (ids: string[], shouldPin: boolean) => void;\n onNavigate: () => void;\n}\n\nconst CatalogRows: FC<CatalogRowsProps> = ({\n item,\n scopeIds,\n isPinned,\n isLocked,\n onTogglePin,\n onToggleAll,\n onNavigate,\n}) => {\n const { NavigationComponent } = useTitanLayoutContext();\n const submenuGroups = getCatalogSubmenuGroups(item);\n\n /*\n * Minus anything locked — locked destinations are always present, so the module's control\n * neither reports on them nor acts on them.\n */\n const everything = scopeIds.filter(id => !isLocked(id));\n const anyPinned = everything.some(isPinned);\n const allPinned = everything.length > 0 && everything.every(isPinned);\n const tag = getCounterTag(item.counter, item.tag);\n\n return (\n <div className={styles.moduleGroup}>\n <div className={styles.row}>\n <NavigationComponent\n to={item.to}\n className={styles.link}\n onClick={onNavigate}\n aria-label={item.title}\n >\n {item.icon && (\n <Icon svg={item.icon} size=\"medium\" className={styles.moduleIcon} />\n )}\n <Text inline size=\"small\" className={styles.moduleTitle}>\n {item.title}\n </Text>\n {/*\n * The same counter the rail shows, so a destination that is asking for\n * attention still says so here — the catalog is the only place an unfavourited\n * one is visible at all.\n */}\n {!!tag && <BadgeTag data={tag} className={styles.badge} />}\n </NavigationComponent>\n {/*\n * One control for the whole module: favouriting it takes its pages with it, and\n * unfavouriting it takes them away again.\n *\n * A gold outline marks a partially favourited module. Fill the star only when\n * its full, unfiltered scope is favourited, including pages hidden by search.\n * Clicking the outline adds the remaining pages; clicking the filled star removes all.\n */}\n {isLocked(item.id) ? (\n <LockedPin />\n ) : (\n <PinToggle\n pinned={allPinned ? true : anyPinned ? 'mixed' : false}\n label={\n allPinned\n ? `Unpin ${item.title} from the navigation`\n : `Pin ${item.title} to the navigation`\n }\n onToggle={() => onToggleAll(everything, !allPinned)}\n />\n )}\n </div>\n {submenuGroups.map(group => (\n <div key={group.links[0].id}>\n {group.title && (\n <Text variant=\"eyebrow\" size=\"small\" className={styles.submenuTitle}>\n {group.title}\n </Text>\n )}\n {group.links.map(child => {\n /*\n * Page titles are only unique within their item — \"Settings\" and \"Reports\" recur\n * across the catalog — and the row that tells them apart is a separate element, so\n * it is not part of any of these names. Anything reading the rows on their own, a\n * screen reader running through the links or a voice command naming one, would\n * otherwise be given the same name several times.\n */\n const qualifiedTitle = `${item.title}: ${child.title}`;\n const childPinned = isPinned(child.id);\n const childTag = getCounterTag(child.counter, child.tag);\n\n return (\n <div key={child.id} className={styles.row}>\n <NavigationComponent\n to={child.to}\n className={classNames(styles.link, styles.childLink)}\n onClick={onNavigate}\n aria-label={qualifiedTitle}\n >\n <Text inline className={styles.childTitle}>\n {child.title}\n </Text>\n {!!childTag && (\n <BadgeTag data={childTag} className={styles.badge} />\n )}\n </NavigationComponent>\n {isLocked(child.id) ? (\n <LockedPin />\n ) : (\n <PinToggle\n pinned={childPinned}\n label={\n childPinned\n ? `Unpin ${qualifiedTitle} from the navigation`\n : `Pin ${qualifiedTitle} to the navigation`\n }\n onToggle={() => onTogglePin(child.id, item.id)}\n />\n )}\n </div>\n );\n })}\n </div>\n ))}\n </div>\n );\n};\n\nexport interface NavigationCatalogProps extends PinnedNavigationConfig {\n open: boolean;\n onClose: () => void;\n}\n\n/**\n * Every destination the host offers, grouped into sections, with a star on every row.\n *\n * The catalog shows what it is given and nothing more — the host has already resolved what this\n * user may reach, so the catalog cannot offer a destination they do not have.\n */\nexport const NavigationCatalog: FC<NavigationCatalogProps> = ({\n open,\n onClose,\n groups,\n pinnedIds,\n lockedIds,\n onPinnedIdsChange,\n onResetToDefaults,\n}) => {\n const [query, setQuery] = useState('');\n const isSearching = query.trim().length > 0;\n const searchRef = useRef<HTMLInputElement>(null);\n\n /*\n * Resolved here rather than by the host, so a catalog that costs requests to enumerate is paid\n * for on opening the dialog and not on every page load. This component is mounted only while\n * open, so the call happens once per open unless the host changes the function.\n */\n const resolvedGroups = useMemo(() => groups(), [groups]);\n\n const visibleGroups = useMemo(\n () => filterCatalogGroups(resolvedGroups, query),\n [resolvedGroups, query]\n );\n\n /*\n * The pin scope of every item, keyed by id and built before the search narrows anything, so a\n * module's control still covers the pages a query is currently hiding.\n */\n const scopeById = useMemo(\n () =>\n new Map(\n resolvedGroups\n .flatMap(group => group.items)\n .map(item => [\n item.id,\n [item.id, ...getCatalogChildren(item).map(child => child.id)],\n ])\n ),\n [resolvedGroups]\n );\n const pinnedSet = useMemo(() => new Set(pinnedIds), [pinnedIds]);\n const lockedSet = useMemo(() => new Set(lockedIds ?? []), [lockedIds]);\n\n const isPinned = (id: string) => pinnedSet.has(id);\n const isLocked = (id: string) => lockedSet.has(id);\n\n /**\n * Favourites or unfavourites one row, carrying its module with it.\n *\n * A page is reached through its module, so the two travel together: favouriting a page brings\n * the module along when it is not already there, and unfavouriting the last one takes the\n * module with it. Leaving the module behind on its own would not read as an emptier\n * navigation — it would put the module back carrying every page it has.\n *\n * Locked ids never reach a proposal. The controls that would move them are not rendered, so\n * that part is belt and braces — but the host's list is the thing that gets persisted, and a\n * locked destination has no business appearing in or disappearing from it.\n */\n const togglePin = (id: string, ownerId?: string) => {\n if (lockedSet.has(id)) {\n return;\n }\n\n /** The module, when it is a separate row that this page can legitimately move. */\n const owner = ownerId !== id && ownerId && !lockedSet.has(ownerId) ? ownerId : undefined;\n\n if (pinnedSet.has(id)) {\n const removing = new Set([id]);\n\n if (owner) {\n const siblings = (scopeById.get(owner) ?? []).filter(\n scoped => scoped !== owner && scoped !== id\n );\n\n if (!siblings.some(sibling => pinnedSet.has(sibling))) {\n removing.add(owner);\n }\n }\n\n onPinnedIdsChange(pinnedIds.filter(pinned => !removing.has(pinned)));\n return;\n }\n\n // Module first, so it reads ahead of its own page in the saved order.\n const additions = owner && !pinnedSet.has(owner) ? [owner, id] : [id];\n\n onPinnedIdsChange([...pinnedIds, ...additions]);\n };\n\n /*\n * One proposal for the whole set rather than one per row, so a host that persists on every\n * change writes once, and every intermediate list — states the user never asked for — never\n * exists.\n */\n const toggleAll = (ids: string[], shouldPin: boolean) => {\n const actionable = ids.filter(id => !lockedSet.has(id));\n\n if (!shouldPin) {\n const removing = new Set(actionable);\n\n onPinnedIdsChange(pinnedIds.filter(pinned => !removing.has(pinned)));\n return;\n }\n\n const additions = actionable.filter(id => !pinnedSet.has(id));\n\n if (additions.length > 0) {\n onPinnedIdsChange([...pinnedIds, ...additions]);\n }\n };\n\n const close = () => {\n setQuery('');\n onClose();\n };\n\n return (\n <Dialog\n open={open}\n onClose={close}\n size=\"xlarge\"\n aria-label=\"All Navigation\"\n className={styles.dialog}\n data-cy=\"navigation-catalog-dialog\"\n >\n <Dialog.Content className={styles.content}>\n <div className={styles.body}>\n <SearchField\n ref={searchRef}\n size=\"small\"\n className={styles.search}\n placeholder=\"Go to ...\"\n value={query}\n onChange={event => setQuery(event.target.value)}\n onClear={() => setQuery('')}\n aria-label=\"Go to\"\n />\n <div\n className={classNames(styles.results, isSearching && styles.searchResults)}\n >\n {visibleGroups.length === 0 ? (\n <div className={styles.empty}>\n {/*\n * Two different nothings: a search that matched no row, and a host\n * that offered no rows at all. The second is a real state — the\n * catalog is reachable whether or not there is anything in it — and\n * quoting an empty query at the user does not describe it.\n */}\n <Text subdued>\n {isSearching ? (\n <Fragment>\n No navigation items match “{query}”.\n </Fragment>\n ) : (\n 'There are no navigation items to show.'\n )}\n </Text>\n </div>\n ) : (\n <div className={isSearching ? undefined : styles.columns}>\n {visibleGroups.map(group => (\n <div\n key={group.title}\n className={isSearching ? undefined : styles.group}\n >\n {/*\n * Browse uses titled columns. Search stacks matches at full\n * width, keeping only each module's own submenu headings.\n */}\n {!isSearching && (\n <Text\n variant=\"eyebrow\"\n size=\"medium\"\n className={styles.groupTitle}\n >\n {group.title}\n </Text>\n )}\n {group.items.map(item => (\n <CatalogRows\n key={item.id}\n item={item}\n scopeIds={scopeById.get(item.id) ?? [item.id]}\n isPinned={isPinned}\n isLocked={isLocked}\n onTogglePin={togglePin}\n onToggleAll={toggleAll}\n onNavigate={close}\n />\n ))}\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n </Dialog.Content>\n <NavigationCatalogFooter\n onRestore={\n onResetToDefaults &&\n (() => {\n onResetToDefaults();\n // The control disappears with the host's reset; keep focus in the dialog.\n searchRef.current?.focus();\n })\n }\n />\n </Dialog>\n );\n};\n"],"names":["Button","Dialog","Icon","SearchField","Text","Tooltip","IconLock","IconStar","IconStarBorder","classNames","Fragment","useMemo","useRef","useState","filterCatalogGroups","getCatalogChildren","getCatalogSubmenuGroups","getCounterTag","BadgeTag","useTitanLayoutContext","NavigationCatalogFooter","styles","LockedPin","Trigger","svg","className","lock","Content","style","pointerEvents","PinToggle","pinned","label","onToggle","toggle","event","preventDefault","stopPropagation","delay","appearance","size","icon","star","starActive","aria-label","aria-pressed","data-pendo","onClick","CatalogRows","item","scopeIds","isPinned","isLocked","onTogglePin","onToggleAll","onNavigate","NavigationComponent","submenuGroups","everything","filter","id","anyPinned","some","allPinned","length","every","tag","counter","div","moduleGroup","row","to","link","title","moduleIcon","inline","moduleTitle","data","badge","map","group","variant","submenuTitle","links","child","qualifiedTitle","childPinned","childTag","childLink","childTitle","NavigationCatalog","open","onClose","groups","pinnedIds","lockedIds","onPinnedIdsChange","onResetToDefaults","query","setQuery","isSearching","trim","searchRef","resolvedGroups","visibleGroups","scopeById","Map","flatMap","items","pinnedSet","Set","lockedSet","has","togglePin","ownerId","owner","undefined","removing","siblings","get","scoped","sibling","add","additions","toggleAll","ids","shouldPin","actionable","close","dialog","data-cy","content","body","ref","search","placeholder","value","onChange","target","onClear","results","searchResults","empty","subdued","columns","groupTitle","onRestore","current","focus"],"mappings":"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"}
|