@servicetitan/navigation 8.1.6 → 8.2.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/layout.stories.d.ts +1 -0
- package/dist/components/layout.stories.d.ts.map +1 -1
- package/dist/components/layout.stories.js +5 -2
- package/dist/components/layout.stories.js.map +1 -1
- package/dist/components/left-navigation/side-navigation.d.ts.map +1 -1
- package/dist/components/left-navigation/side-navigation.js +38 -5
- package/dist/components/left-navigation/side-navigation.js.map +1 -1
- package/dist/components/left-navigation/side-navigation.module.less +116 -3
- package/dist/components/left-navigation/side-navigation.stories.d.ts +1 -0
- package/dist/components/left-navigation/side-navigation.stories.d.ts.map +1 -1
- package/dist/components/left-navigation/side-navigation.stories.js +18 -2
- package/dist/components/left-navigation/side-navigation.stories.js.map +1 -1
- package/dist/test/data.d.ts +3 -0
- package/dist/test/data.d.ts.map +1 -1
- package/dist/test/data.js +67 -6
- package/dist/test/data.js.map +1 -1
- package/dist/utils/navigation.d.ts +23 -9
- package/dist/utils/navigation.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/layout.stories.tsx +16 -1
- package/src/components/left-navigation/side-navigation.module.less +116 -3
- package/src/components/left-navigation/side-navigation.module.less.d.ts +6 -0
- package/src/components/left-navigation/side-navigation.stories.tsx +30 -2
- package/src/components/left-navigation/side-navigation.tsx +164 -12
- package/src/test/data.tsx +83 -7
- package/src/utils/navigation.ts +31 -13
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { withAnvil, withMemoryRouter } from '../test/data';
|
|
1
|
+
import { LocationInfo, withAnvil, withMemoryRouter } from '../test/data';
|
|
2
2
|
import {
|
|
3
3
|
WithAllMonolithData,
|
|
4
4
|
WithAllMonolithDataCommercial,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
DefaultSideNavigation,
|
|
8
8
|
SideNavigationLinksOnly,
|
|
9
|
+
SideNavigationWithSubmenu,
|
|
9
10
|
} from './left-navigation/side-navigation.stories';
|
|
10
11
|
|
|
11
12
|
export default {
|
|
@@ -49,3 +50,17 @@ export const LeftNavLayoutOnlyLinks = () => {
|
|
|
49
50
|
</div>
|
|
50
51
|
);
|
|
51
52
|
};
|
|
53
|
+
|
|
54
|
+
export const LeftNavLayoutSubmenu = () => {
|
|
55
|
+
return (
|
|
56
|
+
<div className="d-f border flex-column" style={{ height: '800px' }}>
|
|
57
|
+
<WithAllMonolithDataCommercial />
|
|
58
|
+
<div className="flex-grow-1 flex-basis-0 d-f">
|
|
59
|
+
<SideNavigationWithSubmenu />
|
|
60
|
+
<div className="flex-grow-1 flex-basis-0 p-5">
|
|
61
|
+
<LocationInfo />
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
|
|
76
76
|
.navigation-item-icon-wrapper {
|
|
77
77
|
flex: 1;
|
|
78
|
-
padding: @spacing-1;
|
|
78
|
+
padding: @spacing-1 @spacing-half;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
.navigation-item-text {
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
&.navigation-item-icon-switch {
|
|
163
|
-
&.navigation-
|
|
163
|
+
&.navigation-item-active {
|
|
164
164
|
.navigation-icon-inactive[data-anv][data-anv] {
|
|
165
165
|
display: none;
|
|
166
166
|
}
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
display: block;
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
|
-
&:not(.navigation-
|
|
171
|
+
&:not(.navigation-item-active) {
|
|
172
172
|
.navigation-icon-inactive[data-anv][data-anv] {
|
|
173
173
|
display: block;
|
|
174
174
|
}
|
|
@@ -187,6 +187,11 @@
|
|
|
187
187
|
font-weight: @font-weight-semibold;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
.navigation-item-group-toggle[data-anv][data-anv] {
|
|
191
|
+
color: inherit;
|
|
192
|
+
font-weight: @font-weight-semibold;
|
|
193
|
+
}
|
|
194
|
+
|
|
190
195
|
.navigation-icon[data-anv][data-anv] {
|
|
191
196
|
height: 24px;
|
|
192
197
|
width: 24px;
|
|
@@ -199,6 +204,114 @@
|
|
|
199
204
|
}
|
|
200
205
|
}
|
|
201
206
|
|
|
207
|
+
.submenu {
|
|
208
|
+
margin-left: @spacing-3;
|
|
209
|
+
padding-left: @spacing-1;
|
|
210
|
+
padding-right: @spacing-1;
|
|
211
|
+
margin-bottom: @spacing-1;
|
|
212
|
+
position: relative;
|
|
213
|
+
|
|
214
|
+
&:before {
|
|
215
|
+
content: '';
|
|
216
|
+
position: absolute;
|
|
217
|
+
border-left: 1px solid @color-neutral-100;
|
|
218
|
+
width: 1px;
|
|
219
|
+
top: @spacing-2;
|
|
220
|
+
bottom: @spacing-1;
|
|
221
|
+
left: 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.submenu-group-header[data-anv][data-anv] {
|
|
225
|
+
padding-top: @spacing-2;
|
|
226
|
+
padding-bottom: @spacing-half;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.submenu-link {
|
|
230
|
+
padding: @spacing-1;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.submenu-link-active {
|
|
234
|
+
position: relative;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.submenu-link-active:before {
|
|
238
|
+
content: '';
|
|
239
|
+
position: absolute;
|
|
240
|
+
background-color: @text-color-active;
|
|
241
|
+
width: 3px;
|
|
242
|
+
top: @spacing-1;
|
|
243
|
+
bottom: @spacing-1;
|
|
244
|
+
left: -12px;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.submenu-link:before:not(.submenu-link-active) {
|
|
248
|
+
background-color: @bg-color-hover;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
> *,
|
|
252
|
+
> *[data-anv][data-anv] {
|
|
253
|
+
border-left: 3px solid transparent;
|
|
254
|
+
padding-left: @spacing-1;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
> *:last-child {
|
|
258
|
+
margin-bottom: @spacing-0;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.submenu-popover {
|
|
263
|
+
margin-left: -@spacing-1;
|
|
264
|
+
margin-right: -@spacing-1;
|
|
265
|
+
min-width: 240px;
|
|
266
|
+
|
|
267
|
+
.submenu-group-header[data-anv][data-anv] {
|
|
268
|
+
margin-top: @spacing-2;
|
|
269
|
+
padding-bottom: @spacing-half;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.submenu-link {
|
|
273
|
+
padding-top: @spacing-1;
|
|
274
|
+
padding-bottom: @spacing-1;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.submenu-link-active {
|
|
278
|
+
background-color: @bg-color-active;
|
|
279
|
+
}
|
|
280
|
+
.submenu-link:hover:not(.submenu-link-active) {
|
|
281
|
+
background-color: @bg-color-hover;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
> *,
|
|
285
|
+
> *[data-anv][data-anv] {
|
|
286
|
+
padding-left: @spacing-1;
|
|
287
|
+
padding-right: @spacing-1;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.submenu,
|
|
292
|
+
.submenu-popover {
|
|
293
|
+
display: flex;
|
|
294
|
+
flex-direction: column;
|
|
295
|
+
|
|
296
|
+
.submenu-group-header[data-anv][data-anv] {
|
|
297
|
+
color: @color-neutral-70;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.submenu-link {
|
|
301
|
+
color: @text-color;
|
|
302
|
+
font-size: @typescale-2;
|
|
303
|
+
border-radius: @border-radius-2;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.submenu-link-active {
|
|
307
|
+
color: @text-color-active;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.submenu-link:hover:not(.submenu-link-active) {
|
|
311
|
+
background-color: @bg-color-hover;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
202
315
|
.options-item {
|
|
203
316
|
font-family: @base-font-family;
|
|
204
317
|
color: @text-color;
|
|
@@ -6,6 +6,7 @@ export const navigationIconInactive: string;
|
|
|
6
6
|
export const navigationItem: string;
|
|
7
7
|
export const navigationItemActive: string;
|
|
8
8
|
export const navigationItemCounter: string;
|
|
9
|
+
export const navigationItemGroupToggle: string;
|
|
9
10
|
export const navigationItemIconSwitch: string;
|
|
10
11
|
export const navigationItemIconWrapper: string;
|
|
11
12
|
export const navigationItemText: string;
|
|
@@ -19,4 +20,9 @@ export const sideNavContent: string;
|
|
|
19
20
|
export const sideNavExpanded: string;
|
|
20
21
|
export const sideNavSlim: string;
|
|
21
22
|
export const sideNavTop: string;
|
|
23
|
+
export const submenu: string;
|
|
24
|
+
export const submenuGroupHeader: string;
|
|
25
|
+
export const submenuLink: string;
|
|
26
|
+
export const submenuLinkActive: string;
|
|
27
|
+
export const submenuPopover: string;
|
|
22
28
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { ComponentType, useState } from 'react';
|
|
2
|
-
import { items, withAnvil, withMemoryRouter } from '../../test/data';
|
|
2
|
+
import { LocationInfo, items, withAnvil, withMemoryRouter } from '../../test/data';
|
|
3
3
|
import { SideNavigation } from './';
|
|
4
4
|
|
|
5
5
|
const layout = (Story: ComponentType) => {
|
|
6
6
|
return (
|
|
7
7
|
<div className="d-f border" style={{ height: '800px' }}>
|
|
8
8
|
<Story />
|
|
9
|
-
<div className="flex-grow-1 flex-basis-0"
|
|
9
|
+
<div className="flex-grow-1 flex-basis-0 p-5">
|
|
10
|
+
<LocationInfo />
|
|
11
|
+
</div>
|
|
10
12
|
</div>
|
|
11
13
|
);
|
|
12
14
|
};
|
|
@@ -69,3 +71,29 @@ export const SideNavigationLinksOnly = () => {
|
|
|
69
71
|
/>
|
|
70
72
|
);
|
|
71
73
|
};
|
|
74
|
+
|
|
75
|
+
export const SideNavigationWithSubmenu = () => {
|
|
76
|
+
const [expanded, setExpanded] = useState(false);
|
|
77
|
+
return (
|
|
78
|
+
<SideNavigation
|
|
79
|
+
expanded={expanded}
|
|
80
|
+
onExpandedChange={setExpanded}
|
|
81
|
+
items={[
|
|
82
|
+
items.dashboard,
|
|
83
|
+
items.calls,
|
|
84
|
+
items.schedule,
|
|
85
|
+
items.dispatch,
|
|
86
|
+
|
|
87
|
+
items.accountingWithSubmenu,
|
|
88
|
+
items.purchasingWithSubmenu,
|
|
89
|
+
|
|
90
|
+
items.followUps,
|
|
91
|
+
items.reports,
|
|
92
|
+
items.marketing,
|
|
93
|
+
items.priceBook,
|
|
94
|
+
|
|
95
|
+
items.projects,
|
|
96
|
+
]}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
@@ -1,9 +1,27 @@
|
|
|
1
|
-
import { Icon } from '@servicetitan/anvil2';
|
|
1
|
+
import { Icon, Popover, PopoverTriggerProps, Text } from '@servicetitan/anvil2';
|
|
2
|
+
import SvgGroupCollapse from '@servicetitan/anvil2/assets/icons/material/round/expand_less.svg';
|
|
3
|
+
import SvgGroupExpand from '@servicetitan/anvil2/assets/icons/material/round/expand_more.svg';
|
|
2
4
|
import SvgCollapse from '@servicetitan/anvil2/assets/icons/st/gnav_menu_collapse.svg';
|
|
3
5
|
import SvgExpand from '@servicetitan/anvil2/assets/icons/st/gnav_menu_expand.svg';
|
|
6
|
+
import { Collapsible, Headline } from '@servicetitan/design-system';
|
|
7
|
+
|
|
4
8
|
import classNames from 'classnames';
|
|
5
|
-
import {
|
|
6
|
-
|
|
9
|
+
import {
|
|
10
|
+
CSSProperties,
|
|
11
|
+
FC,
|
|
12
|
+
Fragment,
|
|
13
|
+
MouseEvent,
|
|
14
|
+
ReactElement,
|
|
15
|
+
useCallback,
|
|
16
|
+
useContext,
|
|
17
|
+
useState,
|
|
18
|
+
} from 'react';
|
|
19
|
+
import {
|
|
20
|
+
HeaderNavigationItemData,
|
|
21
|
+
HeaderNavigationItemLinkProps,
|
|
22
|
+
HeaderNavigationItemSubmenu,
|
|
23
|
+
NavLinkComponentProps,
|
|
24
|
+
} from '../../utils/navigation';
|
|
7
25
|
import { NavigationComponentContext } from '../../utils/navigation-context';
|
|
8
26
|
import { CounterTag } from '../counter-tag';
|
|
9
27
|
import * as Styles from './side-navigation.module.less';
|
|
@@ -59,14 +77,23 @@ export const SideNavigation: FC<SideNavigationProps> = ({
|
|
|
59
77
|
</Fragment>
|
|
60
78
|
)}
|
|
61
79
|
<div className={Styles.sideNavContent} data-cy="navigation-items">
|
|
62
|
-
{items?.map(item =>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
80
|
+
{items?.map(item =>
|
|
81
|
+
item.submenu ? (
|
|
82
|
+
<SideNavigationGroupItem
|
|
83
|
+
key={item.id}
|
|
84
|
+
expanded={expanded}
|
|
85
|
+
navigationComponent={NavigationComponent}
|
|
86
|
+
{...item}
|
|
87
|
+
/>
|
|
88
|
+
) : (
|
|
89
|
+
<SideNavigationItem
|
|
90
|
+
key={item.id}
|
|
91
|
+
expanded={expanded}
|
|
92
|
+
navigationComponent={NavigationComponent}
|
|
93
|
+
{...item}
|
|
94
|
+
/>
|
|
95
|
+
)
|
|
96
|
+
)}
|
|
70
97
|
</div>
|
|
71
98
|
<div className={Styles.divider} />
|
|
72
99
|
<div className={Styles.sideNavBottom}>
|
|
@@ -79,14 +106,19 @@ export const SideNavigation: FC<SideNavigationProps> = ({
|
|
|
79
106
|
);
|
|
80
107
|
};
|
|
81
108
|
|
|
82
|
-
interface
|
|
109
|
+
interface NavigationComponentProps {
|
|
83
110
|
navigationComponent: FC<NavLinkComponentProps>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface SideNavigationItemProps extends HeaderNavigationItemData, NavigationComponentProps {
|
|
84
114
|
expanded?: boolean;
|
|
115
|
+
submenuExpanded?: boolean;
|
|
85
116
|
}
|
|
86
117
|
|
|
87
118
|
/** Side Navigation menu item */
|
|
88
119
|
const SideNavigationItem: FC<SideNavigationItemProps> = ({
|
|
89
120
|
id,
|
|
121
|
+
submenuExpanded,
|
|
90
122
|
to,
|
|
91
123
|
title,
|
|
92
124
|
hint,
|
|
@@ -101,6 +133,7 @@ const SideNavigationItem: FC<SideNavigationItemProps> = ({
|
|
|
101
133
|
expanded,
|
|
102
134
|
}) => {
|
|
103
135
|
const iconSwitch = !!icon && !!iconActive && !IconComponent;
|
|
136
|
+
const hasSubmenu = submenuExpanded === true || submenuExpanded === false;
|
|
104
137
|
|
|
105
138
|
return (
|
|
106
139
|
<NavigationComponent
|
|
@@ -150,6 +183,12 @@ const SideNavigationItem: FC<SideNavigationItemProps> = ({
|
|
|
150
183
|
{!!counter && (
|
|
151
184
|
<CounterTag data={counter} className={Styles.navigationItemCounter} />
|
|
152
185
|
)}
|
|
186
|
+
{hasSubmenu && !!expanded && (
|
|
187
|
+
<Icon
|
|
188
|
+
svg={submenuExpanded ? SvgGroupCollapse : SvgGroupExpand}
|
|
189
|
+
className={Styles.navigationItemGroupToggle}
|
|
190
|
+
/>
|
|
191
|
+
)}
|
|
153
192
|
</div>
|
|
154
193
|
|
|
155
194
|
{!expanded && <div className={Styles.navigationItemText}>{title}</div>}
|
|
@@ -157,6 +196,119 @@ const SideNavigationItem: FC<SideNavigationItemProps> = ({
|
|
|
157
196
|
);
|
|
158
197
|
};
|
|
159
198
|
|
|
199
|
+
const submenuPopoverStyles = { '--background-color-strong': '#24323C' } as CSSProperties;
|
|
200
|
+
|
|
201
|
+
/** Side Navigation menu item */
|
|
202
|
+
const SideNavigationGroupItem: FC<SideNavigationItemProps> = ({ ...props }) => {
|
|
203
|
+
const [submenuExpanded, setSubmenuExpanded] = useState(false);
|
|
204
|
+
const triggerClick = useCallback(
|
|
205
|
+
(e: MouseEvent<HTMLDivElement>) => {
|
|
206
|
+
e.stopPropagation();
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
|
|
209
|
+
if (props.expanded) {
|
|
210
|
+
setSubmenuExpanded(exp => !exp);
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
[props.expanded]
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
return props.expanded ? (
|
|
217
|
+
<Fragment>
|
|
218
|
+
<div onClickCapture={triggerClick}>
|
|
219
|
+
<SideNavigationItem {...props} submenuExpanded={submenuExpanded} />
|
|
220
|
+
</div>
|
|
221
|
+
<Collapsible open={submenuExpanded} animate>
|
|
222
|
+
<div className={Styles.submenu}>
|
|
223
|
+
<SideNavigationGroupContent
|
|
224
|
+
groups={props.submenu?.groups ?? []}
|
|
225
|
+
navigationComponent={props.navigationComponent}
|
|
226
|
+
/>
|
|
227
|
+
</div>
|
|
228
|
+
</Collapsible>
|
|
229
|
+
</Fragment>
|
|
230
|
+
) : (
|
|
231
|
+
<Popover placement="right-start" openOnHover>
|
|
232
|
+
<Popover.Trigger>
|
|
233
|
+
{(triggerProps: PopoverTriggerProps) => (
|
|
234
|
+
<div {...triggerProps} onClickCapture={triggerClick}>
|
|
235
|
+
<SideNavigationItem {...props} />
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
</Popover.Trigger>
|
|
239
|
+
<Popover.Content style={submenuPopoverStyles}>
|
|
240
|
+
<div className={Styles.submenuPopover}>
|
|
241
|
+
<Headline size="small" className="c-white m-b-half-i m-t-1">
|
|
242
|
+
{props.title}
|
|
243
|
+
</Headline>
|
|
244
|
+
<SideNavigationGroupContent
|
|
245
|
+
groups={props.submenu?.groups ?? []}
|
|
246
|
+
navigationComponent={props.navigationComponent}
|
|
247
|
+
/>
|
|
248
|
+
</div>
|
|
249
|
+
</Popover.Content>
|
|
250
|
+
</Popover>
|
|
251
|
+
);
|
|
252
|
+
};
|
|
253
|
+
const SideNavigationGroupContent: FC<HeaderNavigationItemSubmenu & NavigationComponentProps> = ({
|
|
254
|
+
groups,
|
|
255
|
+
navigationComponent,
|
|
256
|
+
}) => {
|
|
257
|
+
return (
|
|
258
|
+
<Fragment>
|
|
259
|
+
{groups.reduce((out, group) => {
|
|
260
|
+
if (!group.links.length) {
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
out.push(
|
|
265
|
+
<Text
|
|
266
|
+
key=":group:title"
|
|
267
|
+
variant="eyebrow"
|
|
268
|
+
className={Styles.submenuGroupHeader}
|
|
269
|
+
>
|
|
270
|
+
{group.title}
|
|
271
|
+
</Text>
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
out.push(
|
|
275
|
+
...group.links.map(link => (
|
|
276
|
+
<SideNavigationGroupLink
|
|
277
|
+
key={link.id}
|
|
278
|
+
{...link}
|
|
279
|
+
navigationComponent={navigationComponent}
|
|
280
|
+
/>
|
|
281
|
+
))
|
|
282
|
+
);
|
|
283
|
+
return out;
|
|
284
|
+
}, [] as ReactElement[])}
|
|
285
|
+
</Fragment>
|
|
286
|
+
);
|
|
287
|
+
};
|
|
288
|
+
const SideNavigationGroupLink: FC<HeaderNavigationItemLinkProps & NavigationComponentProps> = ({
|
|
289
|
+
id,
|
|
290
|
+
title,
|
|
291
|
+
to,
|
|
292
|
+
isActive,
|
|
293
|
+
navigationComponent: NavigationComponent,
|
|
294
|
+
}) => {
|
|
295
|
+
return (
|
|
296
|
+
<NavigationComponent
|
|
297
|
+
data-cy={`navigation-item-${id}`}
|
|
298
|
+
data-pendo={`navigation-item-${id}`}
|
|
299
|
+
key={id}
|
|
300
|
+
to={to}
|
|
301
|
+
className={classNames(Styles.submenuLink, {
|
|
302
|
+
[Styles.submenuLinkActive]: isActive === true,
|
|
303
|
+
})}
|
|
304
|
+
isActive={typeof isActive === 'function' ? isActive : undefined}
|
|
305
|
+
activeClassName={Styles.submenuLinkActive}
|
|
306
|
+
>
|
|
307
|
+
{title}
|
|
308
|
+
</NavigationComponent>
|
|
309
|
+
);
|
|
310
|
+
};
|
|
311
|
+
|
|
160
312
|
/** Side Navigation options toggle */
|
|
161
313
|
export const SideNavigationOptionsToggle: FC<{
|
|
162
314
|
expanded?: boolean;
|
package/src/test/data.tsx
CHANGED
|
@@ -30,7 +30,7 @@ import SvgSchedule from '@servicetitan/anvil2/assets/icons/st/gnav_schedule_inac
|
|
|
30
30
|
import SvgTasksActive from '@servicetitan/anvil2/assets/icons/st/gnav_tasks_active.svg';
|
|
31
31
|
import SvgTasks from '@servicetitan/anvil2/assets/icons/st/gnav_tasks_inactive.svg';
|
|
32
32
|
|
|
33
|
-
import { Popover } from '@servicetitan/design-system';
|
|
33
|
+
import { BodyText, Popover } from '@servicetitan/design-system';
|
|
34
34
|
|
|
35
35
|
import classNames from 'classnames';
|
|
36
36
|
import { forwardRef, useState } from 'react';
|
|
@@ -38,7 +38,12 @@ import { forwardRef, useState } from 'react';
|
|
|
38
38
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
39
39
|
import { MemoryRouter, useHistory, useLocation } from 'react-router-dom';
|
|
40
40
|
import { HeaderNavigationTrigger } from '../components/links';
|
|
41
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
HeaderNavigationItemData,
|
|
43
|
+
HeaderNavigationItemLinkProps,
|
|
44
|
+
HeaderNavigationItemSubmenuGroup,
|
|
45
|
+
NavLinkComponentProps,
|
|
46
|
+
} from '../utils/navigation';
|
|
42
47
|
import { NavigationComponentContext } from '../utils/navigation-context';
|
|
43
48
|
import * as Styles from './data-stories.module.less';
|
|
44
49
|
|
|
@@ -46,20 +51,21 @@ export const NavLinkMock = forwardRef<any, NavLinkComponentProps>(
|
|
|
46
51
|
({ to, children, activeClassName, className, isActive, ...rest }, ref) => {
|
|
47
52
|
const history = useHistory();
|
|
48
53
|
const location = useLocation();
|
|
49
|
-
const linkActive = location.pathname.
|
|
54
|
+
const linkActive = location.pathname.startsWith(to);
|
|
50
55
|
|
|
51
56
|
return (
|
|
52
57
|
<a
|
|
53
58
|
{...rest}
|
|
54
|
-
className={classNames(className, linkActive ? activeClassName : '')}
|
|
55
|
-
href={to}
|
|
56
59
|
onClick={e => {
|
|
57
60
|
e.preventDefault();
|
|
61
|
+
e.stopPropagation();
|
|
58
62
|
|
|
59
63
|
if (!to.startsWith('http')) {
|
|
60
|
-
history.
|
|
64
|
+
history.replace(to);
|
|
61
65
|
}
|
|
62
66
|
}}
|
|
67
|
+
className={classNames(className, linkActive ? activeClassName : '')}
|
|
68
|
+
href={to}
|
|
63
69
|
ref={ref}
|
|
64
70
|
>
|
|
65
71
|
{children}
|
|
@@ -68,6 +74,12 @@ export const NavLinkMock = forwardRef<any, NavLinkComponentProps>(
|
|
|
68
74
|
}
|
|
69
75
|
);
|
|
70
76
|
|
|
77
|
+
export const LocationInfo = () => {
|
|
78
|
+
const location = useLocation();
|
|
79
|
+
|
|
80
|
+
return <BodyText>current location - {location.pathname}</BodyText>;
|
|
81
|
+
};
|
|
82
|
+
|
|
71
83
|
export const withMemoryRouter = (Story: any) => (
|
|
72
84
|
<MemoryRouter>
|
|
73
85
|
<NavigationComponentContext.Provider value={NavLinkMock}>
|
|
@@ -111,12 +123,42 @@ const getItem = (
|
|
|
111
123
|
data: Partial<HeaderNavigationItemData>
|
|
112
124
|
): HeaderNavigationItemData => ({
|
|
113
125
|
id,
|
|
114
|
-
to: id,
|
|
126
|
+
to: '/' + id,
|
|
115
127
|
title: id[0].toUpperCase() + id.substring(1),
|
|
116
128
|
hint: id,
|
|
117
129
|
icon: undefined,
|
|
118
130
|
iconActive: undefined,
|
|
119
131
|
...(data ?? {}),
|
|
132
|
+
submenu: data.submenu
|
|
133
|
+
? {
|
|
134
|
+
...data.submenu,
|
|
135
|
+
groups: data.submenu.groups.map(group => ({
|
|
136
|
+
...group,
|
|
137
|
+
links: group.links.map(link => ({
|
|
138
|
+
...link,
|
|
139
|
+
to: `/${id}/${link.to}`,
|
|
140
|
+
})),
|
|
141
|
+
})),
|
|
142
|
+
}
|
|
143
|
+
: undefined,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const getSubItem = (
|
|
147
|
+
id: string,
|
|
148
|
+
data: Partial<HeaderNavigationItemLinkProps>
|
|
149
|
+
): HeaderNavigationItemLinkProps => ({
|
|
150
|
+
id,
|
|
151
|
+
to: id,
|
|
152
|
+
title: id[0].toUpperCase() + id.substring(1),
|
|
153
|
+
...(data ?? {}),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const getGroup = (
|
|
157
|
+
title: string,
|
|
158
|
+
links: HeaderNavigationItemLinkProps[]
|
|
159
|
+
): HeaderNavigationItemSubmenuGroup => ({
|
|
160
|
+
title,
|
|
161
|
+
links,
|
|
120
162
|
});
|
|
121
163
|
|
|
122
164
|
export const items = {
|
|
@@ -165,11 +207,45 @@ export const items = {
|
|
|
165
207
|
iconActive: SvgInventoryActive,
|
|
166
208
|
}),
|
|
167
209
|
purchasing: getItem('purchasing', { iconComponent: InventoryIcon }),
|
|
210
|
+
purchasingWithSubmenu: getItem('purchasing', {
|
|
211
|
+
iconName: 'toys',
|
|
212
|
+
icon: SvgInventory,
|
|
213
|
+
iconActive: SvgInventoryActive,
|
|
214
|
+
submenu: {
|
|
215
|
+
groups: [
|
|
216
|
+
getGroup('Purchase', [
|
|
217
|
+
getSubItem('repl', { title: 'Replenishment' }),
|
|
218
|
+
getSubItem('orders', { title: 'Purchase Orders' }),
|
|
219
|
+
getSubItem('receipts', { title: 'Receipts' }),
|
|
220
|
+
getSubItem('returns', { title: 'Returns' }),
|
|
221
|
+
]),
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
168
225
|
accounting: getItem('accounting', {
|
|
169
226
|
iconName: 'assignment',
|
|
170
227
|
icon: SvgAccounting,
|
|
171
228
|
iconActive: SvgAccountingActive,
|
|
172
229
|
}),
|
|
230
|
+
accountingWithSubmenu: getItem('accounting', {
|
|
231
|
+
iconName: 'assignment',
|
|
232
|
+
icon: SvgAccounting,
|
|
233
|
+
iconActive: SvgAccountingActive,
|
|
234
|
+
submenu: {
|
|
235
|
+
groups: [
|
|
236
|
+
getGroup('Accounts Receivable', [
|
|
237
|
+
getSubItem('ar', { title: 'AR Management' }),
|
|
238
|
+
getSubItem('export', { title: 'Batch/Export Transactions' }),
|
|
239
|
+
getSubItem('invoices', { title: 'Invoices' }),
|
|
240
|
+
getSubItem('payments', { title: 'Customer Payments' }),
|
|
241
|
+
getSubItem('deposits', { title: 'Bank Deposits' }),
|
|
242
|
+
]),
|
|
243
|
+
getGroup('Accounts Payable', [getSubItem('bills', { title: 'Bills' })]),
|
|
244
|
+
getGroup('Financing', [getSubItem('dashboard', { title: 'Dashboard' })]),
|
|
245
|
+
getGroup('Others', [getSubItem('at', { title: 'Accounting Audit Trail' })]),
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
}),
|
|
173
249
|
marketing: getItem('marketing', {
|
|
174
250
|
iconName: 'bullhorn',
|
|
175
251
|
icon: SvgMarketing,
|
package/src/utils/navigation.ts
CHANGED
|
@@ -3,22 +3,10 @@ import { IconPropsStrict } from '@servicetitan/design-system';
|
|
|
3
3
|
import { FC, HTMLAttributeAnchorTarget, ReactNode } from 'react';
|
|
4
4
|
import { CounterTagPropsType } from '../components/counter-tag';
|
|
5
5
|
|
|
6
|
-
export interface HeaderNavigationItemData {
|
|
7
|
-
/** link id */
|
|
8
|
-
id: string;
|
|
9
|
-
|
|
10
|
-
/** link href */
|
|
11
|
-
to: string;
|
|
12
|
-
|
|
13
|
-
/** link title */
|
|
14
|
-
title: string;
|
|
15
|
-
|
|
6
|
+
export interface HeaderNavigationItemData extends HeaderNavigationItemLinkProps {
|
|
16
7
|
/** link description */
|
|
17
8
|
hint: string;
|
|
18
9
|
|
|
19
|
-
/** callback to return active state. By default, it compares link href with current pathname */
|
|
20
|
-
isActive?: boolean | ((pathname: string) => boolean);
|
|
21
|
-
|
|
22
10
|
/** flag if the link is not shown (based on FG and/or user permissions) */
|
|
23
11
|
isHidden?: boolean;
|
|
24
12
|
|
|
@@ -42,6 +30,36 @@ export interface HeaderNavigationItemData {
|
|
|
42
30
|
|
|
43
31
|
/** class name of link item */
|
|
44
32
|
className?: string;
|
|
33
|
+
|
|
34
|
+
/** optional submenu of link item */
|
|
35
|
+
submenu?: HeaderNavigationItemSubmenu;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface HeaderNavigationItemLinkProps {
|
|
39
|
+
/** link id */
|
|
40
|
+
id: string;
|
|
41
|
+
|
|
42
|
+
/** link href */
|
|
43
|
+
to: string;
|
|
44
|
+
|
|
45
|
+
/** link title */
|
|
46
|
+
title: string;
|
|
47
|
+
|
|
48
|
+
/** callback to return active state. By default, it compares link href with current pathname */
|
|
49
|
+
isActive?: boolean | ((pathname: string) => boolean);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface HeaderNavigationItemSubmenu {
|
|
53
|
+
/** submenu groups */
|
|
54
|
+
groups: HeaderNavigationItemSubmenuGroup[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface HeaderNavigationItemSubmenuGroup {
|
|
58
|
+
/** submenu group title */
|
|
59
|
+
title: string;
|
|
60
|
+
|
|
61
|
+
/** submenu group links */
|
|
62
|
+
links: HeaderNavigationItemLinkProps[];
|
|
45
63
|
}
|
|
46
64
|
|
|
47
65
|
export interface NavLinkComponentPropsStrict {
|