@servicetitan/navigation 11.0.0-canary.237.f2d512b.0 → 11.0.0-canary.237.f834dab.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.
Files changed (91) hide show
  1. package/dist/components/badge-tag.d.ts +1 -1
  2. package/dist/components/badge-tag.d.ts.map +1 -1
  3. package/dist/components/header-navigation/header-navigation-stacked.stories.js +1 -1
  4. package/dist/components/header-navigation/header-navigation-stacked.stories.js.map +1 -1
  5. package/dist/components/header-navigation/header-navigation.stories.js +1 -1
  6. package/dist/components/header-navigation/header-navigation.stories.js.map +1 -1
  7. package/dist/components/left-navigation/header-navigation-tiny.stories.js +2 -2
  8. package/dist/components/left-navigation/header-navigation-tiny.stories.js.map +1 -1
  9. package/dist/components/logo/logo-titan-text.d.ts +1 -1
  10. package/dist/components/logo/logo-titan-text.d.ts.map +1 -1
  11. package/dist/components/profile-dropdown/profile-dropdown.d.ts +12 -7
  12. package/dist/components/profile-dropdown/profile-dropdown.d.ts.map +1 -1
  13. package/dist/components/profile-dropdown/profile-dropdown.js +5 -4
  14. package/dist/components/profile-dropdown/profile-dropdown.js.map +1 -1
  15. package/dist/components/profile-dropdown/profile-dropdown.module.less +3 -0
  16. package/dist/components/titan-layout/layout-context.js +1 -1
  17. package/dist/components/titan-layout/layout-context.js.map +1 -1
  18. package/dist/components/titan-layout/layout-header.d.ts +2 -0
  19. package/dist/components/titan-layout/layout-header.d.ts.map +1 -1
  20. package/dist/components/titan-layout/layout-header.js +3 -4
  21. package/dist/components/titan-layout/layout-header.js.map +1 -1
  22. package/dist/components/titan-layout/layout-header.module.less +60 -21
  23. package/dist/components/titan-layout/layout-logo.d.ts.map +1 -1
  24. package/dist/components/titan-layout/layout-logo.js +2 -1
  25. package/dist/components/titan-layout/layout-logo.js.map +1 -1
  26. package/dist/components/titan-layout/layout-profile.d.ts.map +1 -1
  27. package/dist/components/titan-layout/layout-profile.js +37 -9
  28. package/dist/components/titan-layout/layout-profile.js.map +1 -1
  29. package/dist/components/titan-layout/layout-profile.stories.d.ts.map +1 -1
  30. package/dist/components/titan-layout/layout-profile.stories.js +1 -1
  31. package/dist/components/titan-layout/layout-profile.stories.js.map +1 -1
  32. package/dist/components/titan-layout/layout-sidebar-links-internal.d.ts +2 -2
  33. package/dist/components/titan-layout/layout-sidebar-links-internal.d.ts.map +1 -1
  34. package/dist/components/titan-layout/layout-sidebar-links-internal.js +4 -4
  35. package/dist/components/titan-layout/layout-sidebar-links-internal.js.map +1 -1
  36. package/dist/components/titan-layout/layout-sidebar-links.d.ts.map +1 -1
  37. package/dist/components/titan-layout/layout-sidebar-links.js +12 -5
  38. package/dist/components/titan-layout/layout-sidebar-links.js.map +1 -1
  39. package/dist/components/titan-layout/layout-sidebar.d.ts +2 -0
  40. package/dist/components/titan-layout/layout-sidebar.d.ts.map +1 -1
  41. package/dist/components/titan-layout/layout-sidebar.js +6 -4
  42. package/dist/components/titan-layout/layout-sidebar.js.map +1 -1
  43. package/dist/components/titan-layout/layout-sidebar.module.less +29 -6
  44. package/dist/components/titan-layout/notifications-context.d.ts +13 -0
  45. package/dist/components/titan-layout/notifications-context.d.ts.map +1 -0
  46. package/dist/components/titan-layout/notifications-context.js +23 -0
  47. package/dist/components/titan-layout/notifications-context.js.map +1 -0
  48. package/dist/components/titan-layout/titan-layout.d.ts +10 -6
  49. package/dist/components/titan-layout/titan-layout.d.ts.map +1 -1
  50. package/dist/components/titan-layout/titan-layout.js +104 -21
  51. package/dist/components/titan-layout/titan-layout.js.map +1 -1
  52. package/dist/components/titan-layout/titan-layout.module.less +50 -20
  53. package/dist/components/titan-layout/titan-layout.stories.d.ts +15 -11
  54. package/dist/components/titan-layout/titan-layout.stories.d.ts.map +1 -1
  55. package/dist/components/titan-layout/titan-layout.stories.js +35 -14
  56. package/dist/components/titan-layout/titan-layout.stories.js.map +1 -1
  57. package/dist/test/data.d.ts +4 -1
  58. package/dist/test/data.d.ts.map +1 -1
  59. package/dist/test/data.js +2 -3
  60. package/dist/test/data.js.map +1 -1
  61. package/dist/utils/use-breakpoint.d.ts +1 -0
  62. package/dist/utils/use-breakpoint.d.ts.map +1 -1
  63. package/dist/utils/use-breakpoint.js +3 -2
  64. package/dist/utils/use-breakpoint.js.map +1 -1
  65. package/package.json +2 -2
  66. package/src/components/badge-tag.tsx +1 -1
  67. package/src/components/header-navigation/header-navigation-stacked.stories.tsx +1 -1
  68. package/src/components/header-navigation/header-navigation.stories.tsx +1 -1
  69. package/src/components/left-navigation/header-navigation-tiny.stories.tsx +2 -2
  70. package/src/components/logo/logo-titan-text.tsx +1 -1
  71. package/src/components/profile-dropdown/profile-dropdown.module.less +3 -0
  72. package/src/components/profile-dropdown/profile-dropdown.tsx +23 -6
  73. package/src/components/titan-layout/layout-context.tsx +1 -1
  74. package/src/components/titan-layout/layout-header.module.less +60 -21
  75. package/src/components/titan-layout/layout-header.module.less.d.ts +2 -0
  76. package/src/components/titan-layout/layout-header.tsx +12 -5
  77. package/src/components/titan-layout/layout-logo.tsx +13 -6
  78. package/src/components/titan-layout/layout-profile.stories.tsx +10 -1
  79. package/src/components/titan-layout/layout-profile.tsx +88 -26
  80. package/src/components/titan-layout/layout-sidebar-links-internal.tsx +18 -5
  81. package/src/components/titan-layout/layout-sidebar-links.tsx +21 -5
  82. package/src/components/titan-layout/layout-sidebar.module.less +29 -6
  83. package/src/components/titan-layout/layout-sidebar.module.less.d.ts +1 -0
  84. package/src/components/titan-layout/layout-sidebar.tsx +14 -5
  85. package/src/components/titan-layout/notifications-context.tsx +44 -0
  86. package/src/components/titan-layout/titan-layout.module.less +50 -20
  87. package/src/components/titan-layout/titan-layout.module.less.d.ts +3 -0
  88. package/src/components/titan-layout/titan-layout.stories.tsx +167 -19
  89. package/src/components/titan-layout/titan-layout.tsx +265 -76
  90. package/src/test/data.tsx +2 -3
  91. package/src/utils/use-breakpoint.ts +3 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicetitan/navigation",
3
- "version": "11.0.0-canary.237.f2d512b.0",
3
+ "version": "11.0.0-canary.237.f834dab.0",
4
4
  "description": "Navigation components",
5
5
  "repository": {
6
6
  "type": "git",
@@ -42,5 +42,5 @@
42
42
  "less": true,
43
43
  "webpack": false
44
44
  },
45
- "gitHead": "f2d512b061480a072b2b840832515edd05b6844d"
45
+ "gitHead": "f834dab984458f0c3c0d7df80f64443bfee2f7f6"
46
46
  }
@@ -5,7 +5,7 @@ import { CounterTagData } from '../utils/counter-tag';
5
5
 
6
6
  export interface BadgeTagProps extends CounterTagData {
7
7
  [key: string]: any;
8
- className: string;
8
+ className?: string;
9
9
  }
10
10
 
11
11
  export const BadgeTag: FC<{
@@ -130,7 +130,7 @@ export const WithAllMonolithData = () => (
130
130
  Task Management
131
131
  </ProfileDropdown.Link>
132
132
  <ProfileDropdown.Divider />
133
- <ProfileDropdown.Link id="sign-out" to="https://googgle.com">
133
+ <ProfileDropdown.Link id="sign-out" to="https://googgle.com" text="Sign Out">
134
134
  Sign Out{' '}
135
135
  <span className="c-neutral-100 m-l-half t-truncate">James Bond</span>
136
136
  </ProfileDropdown.Link>
@@ -154,7 +154,7 @@ export const WithAllMonolithData = () => (
154
154
  Task Management
155
155
  </ProfileDropdown.Link>
156
156
  <ProfileDropdown.Divider />
157
- <ProfileDropdown.Link id="sign-out" to="https://googgle.com">
157
+ <ProfileDropdown.Link id="sign-out" to="https://googgle.com" text="Sign Out">
158
158
  Sign Out <span className="c-neutral-100 m-l-half t-truncate">James Bond</span>
159
159
  </ProfileDropdown.Link>
160
160
  <ProfileDropdown.Section id="userid" className="c-neutral-100 fs-1">
@@ -93,7 +93,7 @@ export const WithAllMonolithData = () => (
93
93
  Task Management
94
94
  </ProfileDropdown.Link>
95
95
  <ProfileDropdown.Divider />
96
- <ProfileDropdown.Link id="sign-out" to="https://googgle.com">
96
+ <ProfileDropdown.Link id="sign-out" to="https://googgle.com" text="Sign Out">
97
97
  Sign Out{' '}
98
98
  <span className="c-neutral-100 m-l-half t-truncate">James Bond</span>
99
99
  </ProfileDropdown.Link>
@@ -162,7 +162,7 @@ export const WithAllMonolithDataCommercial = () => (
162
162
  Task Management
163
163
  </ProfileDropdown.Link>
164
164
  <ProfileDropdown.Divider />
165
- <ProfileDropdown.Link id="sign-out" to="https://googgle.com">
165
+ <ProfileDropdown.Link id="sign-out" to="https://googgle.com" text="Sign Out">
166
166
  Sign Out{' '}
167
167
  <span className="c-neutral-100 m-l-half t-truncate">James Bond</span>
168
168
  </ProfileDropdown.Link>
@@ -3,7 +3,7 @@ import { CSSProperties, FC, ReactNode } from 'react';
3
3
  import { LogoTitanSvg } from './logo-titan';
4
4
  import * as Styles from './logo-titan-text.module.less';
5
5
 
6
- export type WrapperProps = FC<{ className: string; children: ReactNode; style?: CSSProperties }>;
6
+ export type WrapperProps = FC<{ className?: string; children: ReactNode; style?: CSSProperties }>;
7
7
 
8
8
  export interface LogoTitanProps {
9
9
  mantleFill?: string;
@@ -123,6 +123,7 @@
123
123
 
124
124
  .dropdown-section {
125
125
  padding: @spacing-1 @spacing-2;
126
+ position: relative;
126
127
  }
127
128
 
128
129
  .dropdown-link {
@@ -143,6 +144,8 @@
143
144
  position: absolute;
144
145
  top: @spacing-half;
145
146
  margin-left: @spacing-half;
147
+ min-width: 12px !important;
148
+ min-height: 12px !important;
146
149
  }
147
150
  }
148
151
 
@@ -6,6 +6,7 @@ import SvgAccountInactive from '@servicetitan/anvil2/assets/icons/st/gnav_accoun
6
6
  import { BodyText, Popover, PopoverPropsStrict } from '@servicetitan/design-system';
7
7
  import classNames from 'classnames';
8
8
  import {
9
+ ComponentPropsWithoutRef,
9
10
  FC,
10
11
  HTMLAttributeAnchorTarget,
11
12
  MouseEvent,
@@ -147,22 +148,31 @@ const ProfileDropdownTrigger: FC<ProfileDropdownTriggerProps> = ({
147
148
  );
148
149
  };
149
150
 
151
+ export type ProfileItemContent =
152
+ | { children: string; text?: string }
153
+ | { children: ReactNode; text: string };
154
+
150
155
  export interface ProfileDropdownSectionPropsStrict {
151
156
  children: ReactNode;
152
157
  id: string;
153
158
  tooltip?: string;
154
159
  className?: string;
160
+ tag?: CounterTagData;
161
+ counter?: CounterTagValue;
155
162
  onClick?(e: MouseEvent): void;
156
163
  }
157
164
 
158
- export interface ProfileDropdownSectionProps extends ProfileDropdownSectionPropsStrict {
159
- [key: string]: any;
160
- }
165
+ export type ProfileDropdownSectionProps = Omit<ComponentPropsWithoutRef<'div'>, 'children'> &
166
+ ProfileDropdownSectionPropsStrict &
167
+ ProfileItemContent;
161
168
 
162
169
  export const ProfileDropdownSection: FC<ProfileDropdownSectionProps> = ({
163
170
  children,
164
171
  className,
172
+ counter,
165
173
  id,
174
+ tag,
175
+ text,
166
176
  tooltip,
167
177
  onClick,
168
178
  ...rest
@@ -175,6 +185,11 @@ export const ProfileDropdownSection: FC<ProfileDropdownSectionProps> = ({
175
185
  }
176
186
  };
177
187
 
188
+ const tagElement = useMemo(
189
+ () => <CounterTag data={getCounterTag(counter, tag)} className={Styles.counter} />,
190
+ [counter, tag]
191
+ );
192
+
178
193
  return withTooltip(
179
194
  <div
180
195
  className={classNames(Styles.dropdownSection, className, {
@@ -186,6 +201,7 @@ export const ProfileDropdownSection: FC<ProfileDropdownSectionProps> = ({
186
201
  {...rest}
187
202
  >
188
203
  {children}
204
+ {tagElement}
189
205
  </div>,
190
206
  tooltip,
191
207
  'left'
@@ -206,9 +222,9 @@ export interface ProfileDropdownLinkPropsStrict {
206
222
  counter?: CounterTagValue;
207
223
  }
208
224
 
209
- export interface ProfileDropdownLinkProps extends ProfileDropdownLinkPropsStrict {
210
- [key: string]: any;
211
- }
225
+ export type ProfileDropdownLinkProps = Omit<ComponentPropsWithoutRef<'a'>, 'children'> &
226
+ ProfileDropdownLinkPropsStrict &
227
+ ProfileItemContent;
212
228
 
213
229
  export const ProfileDropdownLink: FC<ProfileDropdownLinkProps> = ({
214
230
  children,
@@ -218,6 +234,7 @@ export const ProfileDropdownLink: FC<ProfileDropdownLinkProps> = ({
218
234
  counter,
219
235
  tag,
220
236
  target,
237
+ text,
221
238
  to,
222
239
  tooltip,
223
240
  onClick,
@@ -19,7 +19,7 @@ export interface TitanLayoutContextType {
19
19
 
20
20
  export const LayoutContext = createContext<TitanLayoutContextType>({
21
21
  NavigationComponent: DefaultNavLinkComponent,
22
- breakpoint: { name: 'lg', isMobile: false },
22
+ breakpoint: { name: 'lg', isMobile: false, width: 0 },
23
23
  isTitanLayout: false,
24
24
  sidebar: { styles: { popoverContent: {} } },
25
25
  });
@@ -4,29 +4,29 @@
4
4
  @size-links-tiny: 24px;
5
5
 
6
6
  .header {
7
- display: grid;
7
+ --nav-top-content-height: 32px;
8
+ display: flex;
9
+ justify-content: space-between;
8
10
 
9
11
  background-color: @color-white;
10
12
  color: @color-black;
11
- border-bottom: 1px solid @color-neutral-60;
13
+ box-sizing: border-box;
14
+ outline: 1px solid @color-neutral-60;
12
15
 
13
16
  & > * {
14
17
  overflow-y: hidden;
15
18
  }
16
19
 
17
20
  .he-top-left {
18
- grid-column: span 1;
19
21
  display: flex;
20
22
  align-items: center;
21
23
  }
22
24
 
23
25
  .he-top-center {
24
- grid-column: span 1;
26
+ overflow: hidden;
25
27
  }
26
28
 
27
29
  .he-top-right {
28
- grid-column: span 1;
29
-
30
30
  & > * {
31
31
  color: @color-black;
32
32
  }
@@ -90,33 +90,71 @@
90
90
  }
91
91
  }
92
92
 
93
+ .header-mobile {
94
+ padding: @spacing-2 @spacing-0;
95
+ height: var(--nav-offset-top);
96
+
97
+ --nav-top-content-height: 40px;
98
+
99
+ .navigation-link {
100
+ padding: 10px;
101
+ }
102
+
103
+ .he-top-center {
104
+ max-width: unset;
105
+ flex: 1;
106
+ margin-left: @spacing-3;
107
+ margin-right: @spacing-3;
108
+ }
109
+
110
+ .he-top-left {
111
+ margin-left: @spacing-half;
112
+ }
113
+
114
+ .he-top-right {
115
+ margin-right: @spacing-half;
116
+ }
117
+ }
118
+
93
119
  // desktop
94
- @media only screen and (min-width: 768px) {
95
- .header {
96
- grid-template-columns: repeat(3, 1fr);
97
- grid-template-rows: 48px;
120
+ .header-desktop {
121
+ height: var(--nav-offset-top);
122
+ .navigation-link {
123
+ margin: 6px 2px;
124
+ padding: 6px 6px;
98
125
 
99
- .navigation-link {
100
- margin: 6px 2px;
101
- padding: 6px 6px;
126
+ .navigation-item-counter {
127
+ min-width: 12px !important;
128
+ height: 12px !important;
102
129
  }
103
130
  }
104
131
 
105
132
  .he-top-left {
106
133
  padding-left: @spacing-1;
107
134
  }
135
+ .he-top-center {
136
+ flex: 1;
137
+ margin-left: @spacing-2;
138
+ margin-right: @spacing-2;
139
+ max-width: 400px;
140
+ }
108
141
  }
109
142
 
110
- // mobile
111
- @media only screen and (max-width: 768px) {
112
- .header {
143
+ // desktop wide
144
+ @media only screen and (min-width: 1200px) {
145
+ .header-desktop {
146
+ display: grid;
113
147
  grid-template-columns: repeat(3, 1fr);
114
- grid-template-rows: 44px;
148
+ grid-template-rows: 48px;
115
149
 
116
- padding: @spacing-1 @spacing-half;
150
+ .he-top-left,
151
+ .he-top-center,
152
+ .he-top-right {
153
+ grid-column: span 1;
154
+ }
117
155
 
118
- .navigation-link {
119
- padding: 10px;
156
+ .he-top-center {
157
+ max-width: unset;
120
158
  }
121
159
  }
122
160
  }
@@ -134,7 +172,8 @@
134
172
  color: @color-white;
135
173
  font-weight: @font-weight-semibold;
136
174
  font-size: 8px !important;
137
- min-width: 12px;
175
+ min-width: 16px !important;
176
+ height: 16px !important;
138
177
  position: absolute;
139
178
  top: 4px;
140
179
  right: -2px;
@@ -4,6 +4,8 @@ export const heTopLeft: string;
4
4
  export const heTopRight: string;
5
5
  export const heTopRightText: string;
6
6
  export const header: string;
7
+ export const headerDesktop: string;
8
+ export const headerMobile: string;
7
9
  export const navigationIcon: string;
8
10
  export const navigationIconActive: string;
9
11
  export const navigationItemActive: string;
@@ -1,7 +1,7 @@
1
1
  import SvgBurgerMenu from '@servicetitan/anvil2/assets/icons/material/round/menu.svg';
2
2
  import classNames from 'classnames';
3
3
  import { ComponentPropsWithoutRef, FC, ReactElement, ReactNode } from 'react';
4
- import { LayoutPlacementContext, useTitanLayoutContext } from './layout-context';
4
+ import { LayoutPlacementContext } from './layout-context';
5
5
  import { LayoutHeaderNavigationTrigger } from './layout-header-links';
6
6
  import * as Styles from './layout-header.module.less';
7
7
  import { TitanLayoutLogoProps } from './layout-logo';
@@ -25,6 +25,8 @@ export type LayoutHeaderProps = Omit<ComponentPropsWithoutRef<'div'>, 'children'
25
25
  logo: ReactElement<TitanLayoutLogoProps>;
26
26
  profile?: ReactElement;
27
27
 
28
+ isMobile: boolean;
29
+ hasNotifications: boolean;
28
30
  onBurgerClick?: (e: MouseEvent) => void;
29
31
  };
30
32
 
@@ -35,22 +37,26 @@ export const LayoutHeader: FC<LayoutHeaderProps> = ({
35
37
  rightClassName,
36
38
  center,
37
39
  centerClassName,
40
+ isMobile,
41
+ hasNotifications,
38
42
  logo,
39
43
  profile,
40
44
  onBurgerClick,
41
45
  ...rest
42
46
  }) => {
43
- const { breakpoint } = useTitanLayoutContext();
44
-
45
47
  return (
46
48
  <LayoutPlacementContext.Provider value="top">
47
49
  <div
48
- className={classNames(Styles.header, className)}
50
+ className={classNames(
51
+ Styles.header,
52
+ isMobile ? Styles.headerMobile : Styles.headerDesktop,
53
+ className
54
+ )}
49
55
  {...rest}
50
56
  data-cy="header-navigation"
51
57
  >
52
58
  <div className={classNames(Styles.heTopLeft)} data-cy="navigation-left">
53
- {breakpoint.isMobile && (
59
+ {isMobile && (
54
60
  <LayoutHeaderNavigationTrigger
55
61
  id="burger"
56
62
  title=""
@@ -58,6 +64,7 @@ export const LayoutHeader: FC<LayoutHeaderProps> = ({
58
64
  iconActive={SvgBurgerMenu}
59
65
  className="m-r-1"
60
66
  onClick={onBurgerClick}
67
+ tag={{ value: hasNotifications }}
61
68
  />
62
69
  )}
63
70
  {logo}
@@ -33,20 +33,27 @@ export const TitanLayoutLogo: FC<TitanLayoutLogoProps> = ({
33
33
  const Wrapper = logoWrapper;
34
34
  const logoSize = isMobile ? 44 : 56;
35
35
  const logoCompanySize = 48;
36
+ const showCompanyTitle = title === true && !isMobile;
36
37
 
37
38
  return (
38
- <div className={classNames('d-f align-items-center', className)}>
39
- {typeof title === 'string' ? (
39
+ <div
40
+ className={classNames(
41
+ 'd-f align-items-center',
42
+ { 'p-t-half': showCompanyTitle },
43
+ className
44
+ )}
45
+ >
46
+ {showCompanyTitle ? (
47
+ <Wrapper>
48
+ <LogoCompanyTitle height={logoCompanySize} />
49
+ </Wrapper>
50
+ ) : typeof title === 'string' ? (
40
51
  <Fragment>
41
52
  <LogoTitan size={logoSize} mantleFill={mantleFill} logoWrapper={Wrapper} />
42
53
  {!isMobile && (
43
54
  <LogoTitanTitle className="c-inherit m-l-1">{title}</LogoTitanTitle>
44
55
  )}
45
56
  </Fragment>
46
- ) : title === true && !isMobile ? (
47
- <Wrapper className="">
48
- <LogoCompanyTitle height={logoCompanySize} />
49
- </Wrapper>
50
57
  ) : (
51
58
  <LogoTitan size={logoSize} mantleFill={mantleFill} logoWrapper={Wrapper} />
52
59
  )}
@@ -18,7 +18,7 @@ export default {
18
18
 
19
19
  export const ProfileDefault = withTitanLayout(
20
20
  <ProfileDropdown>
21
- <ProfileDropdown.Link id="first" to="https://google.com">
21
+ <ProfileDropdown.Link id="first" to="https://google.com" external>
22
22
  first link
23
23
  </ProfileDropdown.Link>
24
24
  <ProfileDropdown.Section id="second" onClick={() => alert('second click')}>
@@ -33,5 +33,14 @@ export const ProfileDefault = withTitanLayout(
33
33
  third link
34
34
  </ProfileDropdown.Link>
35
35
  <ProfileDropdown.Divider />
36
+ <ProfileDropdown.Section
37
+ id="forth"
38
+ onClick={() => alert('forth click')}
39
+ text="Sign Out user"
40
+ >
41
+ Sign Out
42
+ <span className="c-neutral-60 m-l-1">user</span>
43
+ </ProfileDropdown.Section>
44
+ <ProfileDropdown.Divider />
36
45
  </ProfileDropdown>
37
46
  );
@@ -1,8 +1,8 @@
1
1
  import SvgAccountActive from '@servicetitan/anvil2/assets/icons/st/gnav_account_active.svg';
2
2
  import SvgAccountInactive from '@servicetitan/anvil2/assets/icons/st/gnav_account_inactive.svg';
3
3
 
4
- import { FC, useState } from 'react';
5
- import { NavigationComponentContext } from '../../utils/navigation-context';
4
+ import { FC, MouseEvent, useEffect, useState } from 'react';
5
+ import { NavLinkComponentProps, NavigationComponentContext } from '../../utils/navigation-context';
6
6
  import {
7
7
  ProfileDropdown as DesktopProfileDropdown,
8
8
  ProfileDropdownLinkProps,
@@ -17,6 +17,7 @@ import {
17
17
  InternalSideNavigationGroupLink,
18
18
  InternalSideNavigationGroupTrigger,
19
19
  } from './layout-sidebar-links-internal';
20
+ import { useNotificationsContext, useNotificationsState } from './notifications-context';
20
21
 
21
22
  export type {
22
23
  ProfileDropdownProps,
@@ -24,6 +25,18 @@ export type {
24
25
  ProfileDropdownLinkProps,
25
26
  } from '../profile-dropdown/profile-dropdown';
26
27
 
28
+ const ExternalNavComponent: FC<NavLinkComponentProps> = ({
29
+ children,
30
+ isActive,
31
+ to,
32
+ activeClassName,
33
+ ...props
34
+ }) => (
35
+ <a {...props} href={to}>
36
+ {children}
37
+ </a>
38
+ );
39
+
27
40
  const ProfileDropdownContent: FC<ProfileDropdownProps> = props => {
28
41
  const { breakpoint, NavigationComponent } = useTitanLayoutContext();
29
42
  return breakpoint.isMobile ? (
@@ -40,24 +53,37 @@ const MobileProfileDropdown: FC<ProfileDropdownProps & NavigationComponentProps>
40
53
  children,
41
54
  ...props
42
55
  }) => {
56
+ const id = '__profile';
43
57
  const [expanded, setExpanded] = useState(false);
44
- const onExpandToggle = () => setExpanded(!expanded);
58
+ const { hasNotifications, NotificationsContextProvider } = useNotificationsState();
59
+ const { onNotificationsUpdate } = useNotificationsContext();
60
+ const onExpandToggle = (e: MouseEvent<never>) => {
61
+ e.stopPropagation();
62
+ setExpanded(!expanded);
63
+ };
64
+
65
+ useEffect(() => {
66
+ onNotificationsUpdate(id, hasNotifications);
67
+ }, [hasNotifications, onNotificationsUpdate]);
68
+
45
69
  return (
46
- <InternalSideNavigationGroup
47
- id="__profile"
48
- to={undefined}
49
- title="Profile"
50
- icon={SvgAccountInactive}
51
- iconActive={SvgAccountActive}
52
- isActive={expanded}
53
- {...props}
54
- submenuExpanded={expanded}
55
- onExpandToggle={onExpandToggle}
56
- onClick={onExpandToggle}
57
- tag={undefined}
58
- >
59
- {children}
60
- </InternalSideNavigationGroup>
70
+ <NotificationsContextProvider>
71
+ <InternalSideNavigationGroup
72
+ id={id}
73
+ to={undefined}
74
+ title="Profile"
75
+ icon={SvgAccountInactive}
76
+ iconActive={SvgAccountActive}
77
+ isActive={expanded}
78
+ {...props}
79
+ submenuExpanded={expanded}
80
+ onExpandToggle={onExpandToggle}
81
+ onClick={onExpandToggle}
82
+ tag={{ value: hasNotifications }}
83
+ >
84
+ {children}
85
+ </InternalSideNavigationGroup>
86
+ </NotificationsContextProvider>
61
87
  );
62
88
  };
63
89
 
@@ -70,6 +96,25 @@ const ProfileDropdownDivider: FC = () => {
70
96
  );
71
97
  };
72
98
 
99
+ const getText = (children: any, text: any): string | undefined => {
100
+ if (typeof children === 'string') {
101
+ return children;
102
+ }
103
+
104
+ if (typeof text === 'string') {
105
+ return text;
106
+ }
107
+
108
+ return undefined;
109
+ };
110
+
111
+ const getTag = (
112
+ tag: ProfileDropdownLinkProps['tag'],
113
+ counter: ProfileDropdownLinkProps['counter']
114
+ ): boolean => {
115
+ return !!tag?.value || !!counter;
116
+ };
117
+
73
118
  const ProfileDropdownSection: FC<ProfileDropdownSectionProps> = props => {
74
119
  const { breakpoint } = useTitanLayoutContext();
75
120
  return breakpoint.isMobile ? (
@@ -78,10 +123,19 @@ const ProfileDropdownSection: FC<ProfileDropdownSectionProps> = props => {
78
123
  <DesktopProfileDropdown.Section {...props} />
79
124
  );
80
125
  };
81
- const MobileProfileDropdownSection: FC<ProfileDropdownSectionProps> = props => {
82
- const title = typeof props.children === 'string' ? props.children : undefined;
83
- return title ? (
84
- <InternalSideNavigationGroupTrigger id={props.id} title={title} onClick={props.onClick} />
126
+ const MobileProfileDropdownSection: FC<ProfileDropdownSectionProps> = ({
127
+ children,
128
+ text,
129
+ tooltip,
130
+ title,
131
+ ...props
132
+ }) => {
133
+ const sectionText = getText(children, text);
134
+ const { onNotificationsUpdate } = useNotificationsContext();
135
+ onNotificationsUpdate(props.id, getTag(props.tag, props.counter));
136
+
137
+ return sectionText ? (
138
+ <InternalSideNavigationGroupTrigger {...props} title={sectionText} />
85
139
  ) : null;
86
140
  };
87
141
 
@@ -94,17 +148,25 @@ const ProfileDropdownLink: FC<ProfileDropdownLinkProps> = props => {
94
148
  );
95
149
  };
96
150
  const MobileProfileDropdownLink: FC<ProfileDropdownLinkProps & NavigationComponentProps> = ({
151
+ external,
97
152
  to,
153
+ tooltip,
154
+ text,
155
+ children,
98
156
  navigationComponent,
99
157
  ...props
100
158
  }) => {
101
- const title = typeof props.children === 'string' ? props.children : undefined;
102
- return title ? (
159
+ const { onNotificationsUpdate } = useNotificationsContext();
160
+ const linkText = getText(children, text);
161
+ const isExternalLink = external ?? to?.startsWith('http');
162
+ onNotificationsUpdate(props.id, getTag(props.tag, props.counter));
163
+
164
+ return linkText ? (
103
165
  <InternalSideNavigationGroupLink
104
166
  {...props}
105
167
  to={to}
106
- title={title}
107
- navigationComponent={navigationComponent}
168
+ title={linkText}
169
+ navigationComponent={isExternalLink ? ExternalNavComponent : navigationComponent}
108
170
  />
109
171
  ) : null;
110
172
  };
@@ -156,7 +156,9 @@ export const InternalSideNavigationLink: FC<InternalSideNavigationLinkProps> = (
156
156
 
157
157
  /** Side Navigation menu trigger (for internal usage) */
158
158
  export const InternalSideNavigationTrigger: FC<
159
- Omit<InternalSideNavigationLinkProps, 'to' | 'navigationComponent'> & { onClick?: () => void }
159
+ Omit<InternalSideNavigationLinkProps, 'to' | 'navigationComponent'> & {
160
+ onClick?: (e: MouseEvent<never>) => void;
161
+ }
160
162
  > = ({ className, dataPrefix, isActive, submenuExpanded, onExpandToggle, onClick, ...props }) => {
161
163
  return (
162
164
  <div
@@ -180,12 +182,22 @@ export const InternalSideNavigationTrigger: FC<
180
182
 
181
183
  export const InternalSideNavigationGroupLink: FC<
182
184
  NavigationSubmenuItemData & NavigationComponentProps
183
- > = ({ id, counter, tag, title, to, isActive, navigationComponent: NavigationComponent }) => {
185
+ > = ({
186
+ id,
187
+ counter,
188
+ tag,
189
+ title,
190
+ to,
191
+ isActive,
192
+ navigationComponent: NavigationComponent,
193
+ ...rest
194
+ }) => {
184
195
  return (
185
196
  <NavigationComponent
197
+ key={id}
186
198
  data-cy={`navigation-item-${id}`}
187
199
  data-pendo={`navigation-item-${id}`}
188
- key={id}
200
+ {...rest}
189
201
  to={to}
190
202
  className={classNames(Styles.submenuItem, Styles.submenuLink, {
191
203
  [Styles.submenuLinkActive]: isActive === true,
@@ -201,12 +213,13 @@ export const InternalSideNavigationGroupLink: FC<
201
213
 
202
214
  export const InternalSideNavigationGroupTrigger: FC<
203
215
  Omit<NavigationSubmenuItemData, 'to'> & { onClick?: (e: MouseEvent<any>) => void }
204
- > = ({ id, counter, onClick, tag, title, isActive }) => {
216
+ > = ({ id, counter, onClick, tag, title, isActive, ...rest }) => {
205
217
  return (
206
218
  <div
207
219
  data-cy={`navigation-item-${id}`}
208
220
  data-pendo={`navigation-item-${id}`}
209
221
  key={id}
222
+ {...rest}
210
223
  className={classNames(Styles.submenuItem, {
211
224
  [Styles.submenuLink]: !!onClick,
212
225
  [Styles.submenuLinkActive]: isActive === true,
@@ -231,7 +244,7 @@ export const InternalSideNavigationGroup: FC<
231
244
  onExpandToggle?: (e: MouseEvent<never>) => void;
232
245
  tag: BadgeTagProps | undefined;
233
246
  to: NavigationItemData['to'] | undefined;
234
- onClick?: () => void;
247
+ onClick?: (e: MouseEvent<never>) => void;
235
248
  }
236
249
  > = ({ children, submenuExpanded, to, onExpandToggle, onClick, ...props }) => {
237
250
  return (