@servicetitan/mpa-components 1.11.0 → 2.1.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 (51) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/lib/components/brands/brand-card/actions-button-section.d.ts +6 -0
  3. package/lib/components/brands/brand-card/actions-button-section.d.ts.map +1 -0
  4. package/lib/components/brands/brand-card/actions-button-section.js +22 -0
  5. package/lib/components/brands/brand-card/actions-button-section.js.map +1 -0
  6. package/lib/components/brands/brand-card/brand-card.d.ts +1 -1
  7. package/lib/components/brands/brand-card/brand-card.d.ts.map +1 -1
  8. package/lib/components/brands/brand-card/brand-card.js +8 -55
  9. package/lib/components/brands/brand-card/brand-card.js.map +1 -1
  10. package/lib/components/brands/brand-card/info-section.d.ts +11 -0
  11. package/lib/components/brands/brand-card/info-section.d.ts.map +1 -0
  12. package/lib/components/brands/brand-card/info-section.js +4 -0
  13. package/lib/components/brands/brand-card/info-section.js.map +1 -0
  14. package/lib/components/brands/brand-card/single-action-button.d.ts +6 -0
  15. package/lib/components/brands/brand-card/single-action-button.d.ts.map +1 -0
  16. package/lib/components/brands/brand-card/single-action-button.js +34 -0
  17. package/lib/components/brands/brand-card/single-action-button.js.map +1 -0
  18. package/lib/components/brands/cards-grid/cards-grid.d.ts +2 -1
  19. package/lib/components/brands/cards-grid/cards-grid.d.ts.map +1 -1
  20. package/lib/components/brands/cards-grid/cards-grid.js +2 -3
  21. package/lib/components/brands/cards-grid/cards-grid.js.map +1 -1
  22. package/lib/components/brands/styles.module.less +3 -40
  23. package/lib/enums/brands.d.ts +4 -19
  24. package/lib/enums/brands.d.ts.map +1 -1
  25. package/lib/enums/brands.js +6 -1
  26. package/lib/enums/brands.js.map +1 -1
  27. package/lib/index.d.ts +3 -1
  28. package/lib/index.d.ts.map +1 -1
  29. package/lib/index.js +3 -1
  30. package/lib/index.js.map +1 -1
  31. package/lib/utils/interfaces.d.ts +22 -0
  32. package/lib/utils/interfaces.d.ts.map +1 -0
  33. package/lib/utils/interfaces.js +2 -0
  34. package/lib/utils/interfaces.js.map +1 -0
  35. package/lib/utils/mappers.d.ts +4 -0
  36. package/lib/utils/mappers.d.ts.map +1 -0
  37. package/lib/utils/mappers.js +13 -0
  38. package/lib/utils/mappers.js.map +1 -0
  39. package/package.json +2 -2
  40. package/src/components/brands/brand-card/actions-button-section.tsx +42 -0
  41. package/src/components/brands/brand-card/brand-card.tsx +38 -222
  42. package/src/components/brands/brand-card/info-section.tsx +32 -0
  43. package/src/components/brands/brand-card/single-action-button.tsx +84 -0
  44. package/src/components/brands/cards-grid/cards-grid.tsx +10 -6
  45. package/src/components/brands/styles.module.less +3 -40
  46. package/src/components/brands/styles.module.less.d.ts +1 -6
  47. package/src/enums/brands.ts +4 -22
  48. package/src/index.ts +3 -1
  49. package/src/utils/interfaces.ts +23 -0
  50. package/src/utils/mappers.ts +14 -0
  51. package/tsconfig.tsbuildinfo +1 -1
@@ -1,21 +1,11 @@
1
- import { FC, useMemo, useState } from 'react';
2
- import { Link } from 'react-router-dom';
1
+ import { FC } from 'react';
2
+ import classNames from 'classnames';
3
3
 
4
- import {
5
- ActionMenu,
6
- BodyText,
7
- Button,
8
- Card,
9
- Headline,
10
- Icon,
11
- Stack,
12
- Tag,
13
- Tooltip,
14
- } from '@servicetitan/design-system';
15
- import { DomainValidationStatus } from '@servicetitan/marketing-services-api/dist/settings/brand.mrk.api';
16
- import { tokens } from '@servicetitan/tokens/core';
4
+ import { Flex, Card, Avatar } from '@servicetitan/anvil2';
17
5
 
18
- import { BrandAction, BrandCardProps } from '../../../enums/brands';
6
+ import { BrandCardProps } from '../../../utils/interfaces';
7
+ import { ActionsButtonSection } from './actions-button-section';
8
+ import { InfoSection } from './info-section';
19
9
 
20
10
  import * as Styles from '../styles.module.less';
21
11
 
@@ -23,223 +13,49 @@ const BRAND_NAME_PLACEHOLDER = 'Assign Brand Name';
23
13
 
24
14
  export const BrandCard: FC<BrandCardProps> = ({
25
15
  actions,
26
- domainValidationStatus,
27
16
  email,
28
17
  isDefault,
29
18
  isShared,
30
19
  logo,
31
20
  name,
21
+ hasError,
32
22
  }) => {
33
- const [isOpen, setOpen] = useState(false);
34
-
35
- const actionsButtonSection = useMemo(() => {
36
- if (!Array.isArray(actions)) {
37
- return (
38
- <SingleActionButtonSection
39
- action={actions}
40
- domainValidationStatus={domainValidationStatus}
41
- />
42
- );
43
- }
44
-
45
- const trigger = (
46
- <Button outline size="small" onClick={() => setOpen(true)}>
47
- Actions <Icon name="keyboard_arrow_down" size={16} />
48
- </Button>
49
- );
50
-
51
- return (
52
- <ActionMenu
53
- trigger={trigger}
54
- open={isOpen}
55
- onClickOutside={() => setOpen(false)}
56
- direction="bl"
57
- >
58
- {actions.map(action => {
59
- if (action.href) {
60
- return (
61
- <Link to={action.href} key={action.name}>
62
- <ActionMenu.Item key={action.name} disabled={action.disabled}>
63
- <BodyText subdued={action.disabled}>{action.name}</BodyText>
64
- </ActionMenu.Item>
65
- </Link>
66
- );
67
- }
68
-
69
- const actionClick = () => {
70
- setOpen(false);
71
- if (action.action) {
72
- action.action();
73
- }
74
- };
75
-
76
- return (
77
- <ActionMenu.Item
78
- disabled={action.disabled}
79
- key={action.name}
80
- onClick={actionClick}
81
- >
82
- <BodyText subdued={action.disabled}>{action.name}</BodyText>
83
- </ActionMenu.Item>
84
- );
85
- })}
86
- </ActionMenu>
87
- );
88
- }, [isOpen, setOpen, actions, domainValidationStatus]);
89
-
90
23
  const nameToShow = name || BRAND_NAME_PLACEHOLDER;
91
24
 
92
25
  return (
93
26
  <Card
94
- className={Styles.brandCard}
95
- status={domainValidationStatus === DomainValidationStatus.Error ? 'error' : 'default'}
27
+ className={classNames(
28
+ {
29
+ [Styles.cardWithError]: hasError,
30
+ },
31
+ Styles.brandCard,
32
+ 'qa-brand-card',
33
+ )}
34
+ padding="large"
96
35
  >
97
- <Stack direction="row" spacing={2} className={Styles.cardContent}>
98
- <Stack.Item fill>
99
- <Stack direction="column">
100
- {logo && nameToShow !== 'Corporate Brand 2' ? (
101
- <img className={Styles.brandImg} src={logo} alt={name} />
102
- ) : (
103
- <div className={Styles.brandImgPlaceholder}>
104
- <Icon name="image" className="c-neutral-90" size={24} />
105
- </div>
106
- )}
107
-
108
- <Stack direction="column" className="m-t-3">
109
- <Headline
110
- el="p"
111
- subdued={!name}
112
- className="t-truncate-i m-0"
113
- title={nameToShow}
114
- >
115
- {nameToShow}
116
- </Headline>
117
- {email && (
118
- <BodyText subdued title={email} className="t-truncate-i">
119
- {email}
120
- </BodyText>
121
- )}
122
- </Stack>
123
- </Stack>
124
- </Stack.Item>
125
- <Stack direction="column">
126
- {actionsButtonSection}
127
- <CardTags isDefault={isDefault} isShared={isShared} />
128
- </Stack>
129
- </Stack>
36
+ <Flex
37
+ className={Styles.cardContent}
38
+ justifyContent="space-between"
39
+ direction="column"
40
+ gap="6"
41
+ >
42
+ <Flex
43
+ justifyContent="space-between"
44
+ alignItems="flex-start"
45
+ className="qa-brand-card-left-section"
46
+ >
47
+ <Avatar image={logo} name={nameToShow} size="large" />
48
+ <ActionsButtonSection actions={actions} />
49
+ </Flex>
50
+
51
+ <InfoSection
52
+ name={name}
53
+ nameToShow={nameToShow}
54
+ email={email}
55
+ isDefault={isDefault}
56
+ isShared={isShared}
57
+ />
58
+ </Flex>
130
59
  </Card>
131
60
  );
132
61
  };
133
-
134
- const CardTags: FC<{ isDefault: boolean; isShared: boolean }> = ({ isDefault, isShared }) => {
135
- if (!isDefault && !isShared) {
136
- return null;
137
- }
138
-
139
- return (
140
- <Stack direction="column" alignItems="flex-end" className={Styles.tags}>
141
- {isShared && (
142
- <Tag className={Styles.corporateTag} color="info" subtle compact>
143
- Corporate HQ
144
- </Tag>
145
- )}
146
- {isDefault && (
147
- <Tag className={Styles.defaultTag} compact>
148
- Default
149
- </Tag>
150
- )}
151
- </Stack>
152
- );
153
- };
154
-
155
- const SingleActionButtonSection: FC<{
156
- action: BrandAction;
157
- domainValidationStatus?: DomainValidationStatus;
158
- }> = ({ domainValidationStatus, action }) => {
159
- const validationStatusIcon = useMemo(() => {
160
- return <ValidationStatusIcon status={domainValidationStatus} />;
161
- }, [domainValidationStatus]);
162
-
163
- if (domainValidationStatus === DomainValidationStatus.Pending) {
164
- return (
165
- <Stack spacing={1} alignItems="center">
166
- <BodyText size="small">Validation In Process</BodyText>
167
- {validationStatusIcon}
168
- </Stack>
169
- );
170
- }
171
-
172
- return (
173
- <Stack spacing={2} alignItems="center">
174
- {validationStatusIcon}
175
- {action.isConfigured && (
176
- <Icon name="check_circle" color={tokens.colorGreen500} size={24} />
177
- )}
178
-
179
- {action.inProgress ? (
180
- <InProgressButton action={action} />
181
- ) : (
182
- <StandardButton action={action} />
183
- )}
184
- </Stack>
185
- );
186
- };
187
-
188
- const StandardButton: FC<{ action: BrandAction }> = ({ action }) => (
189
- <Tooltip text={action.disabled ? action.tooltipMessage : ''}>
190
- {action.href ? (
191
- <Link to={action.href}>
192
- <Button size="small" primary outline disabled={action.disabled}>
193
- {action.name}
194
- </Button>
195
- </Link>
196
- ) : (
197
- <Button size="small" primary onClick={action.action} outline disabled={action.disabled}>
198
- {action.name}
199
- </Button>
200
- )}
201
- </Tooltip>
202
- );
203
-
204
- const InProgressButton: FC<{ action: BrandAction }> = ({ action }) => (
205
- <Tooltip text={action.disabled ? action.tooltipMessage : ''}>
206
- {action.href ? (
207
- <Link to={action.href}>
208
- <Button size="small" primary text className="m-0 p-1">
209
- <BodyText size="small" className="m-r-half">
210
- Registration In Progress
211
- </BodyText>
212
- <Icon name="hourglass_full" color={tokens.colorBlue} size={16} />
213
- </Button>
214
- </Link>
215
- ) : (
216
- <Button size="small" primary text className="m-0 p-1" onClick={action.action}>
217
- <BodyText size="small" className="m-r-1">
218
- Registration In Progress
219
- </BodyText>
220
- <Icon name="hourglass_full" color={tokens.colorBlue} size={16} />
221
- </Button>
222
- )}
223
- </Tooltip>
224
- );
225
-
226
- const ValidationStatusIcon: FC<{ status?: DomainValidationStatus }> = ({ status }) => {
227
- switch (status) {
228
- case DomainValidationStatus.Error:
229
- return (
230
- <Tooltip
231
- direction="t"
232
- text="Error is detected in processing the Sender Domain validation. Please check and try again."
233
- className="cursor-pointer"
234
- >
235
- <Icon name="warning" color={tokens.colorRed500} size={24} />
236
- </Tooltip>
237
- );
238
- case DomainValidationStatus.Validated:
239
- return <Icon name="check_circle" color={tokens.colorGreen500} size={24} />;
240
- case DomainValidationStatus.Pending:
241
- return <Icon name="hourglass_full" color={tokens.colorBlue500} size={24} />;
242
- default:
243
- return null;
244
- }
245
- };
@@ -0,0 +1,32 @@
1
+ import type { FC } from 'react';
2
+
3
+ import { Chip, Flex, Text } from '@servicetitan/anvil2';
4
+
5
+ interface InfoSectionProps {
6
+ nameToShow: string;
7
+ name?: string;
8
+ email?: string;
9
+ isDefault?: boolean;
10
+ isShared?: boolean;
11
+ }
12
+
13
+ export const InfoSection: FC<InfoSectionProps> = ({ nameToShow, email, isDefault, isShared }) => (
14
+ <Flex direction="column" gap="4">
15
+ <Flex direction="column" gap="1">
16
+ <Flex gap="2" alignItems="center">
17
+ <Text className="t-truncate-i m-0" title={nameToShow} variant="headline" el="h3">
18
+ {nameToShow}
19
+ </Text>
20
+ <Flex gap="1">
21
+ {isDefault && <Chip label="Default" />}
22
+ {isShared && <Chip color="#b5deff" label="Corporate HQ" />}
23
+ </Flex>
24
+ </Flex>
25
+ {email && (
26
+ <Text subdued title={email} className="t-truncate-i">
27
+ {email}
28
+ </Text>
29
+ )}
30
+ </Flex>
31
+ </Flex>
32
+ );
@@ -0,0 +1,84 @@
1
+ import { FC, PropsWithChildren } from 'react';
2
+ import { useHistory } from 'react-router-dom';
3
+
4
+ import HelpIcon from '@servicetitan/anvil2/assets/icons/material/round/help.svg';
5
+ import { Chip, Flex, Tooltip as TooltipA2, Button } from '@servicetitan/anvil2';
6
+ import { ButtonAppearance } from '@servicetitan/hammer-react/dist/types/props';
7
+
8
+ import { BrandAction } from '../../../utils/interfaces';
9
+ import { BrandActionChipColor, BrandActionChipLabel } from '../../../utils/mappers';
10
+ import { ActionStatus } from '../../../enums/brands';
11
+
12
+ export const SingleActionButtonSection: FC<{
13
+ action: BrandAction;
14
+ }> = ({ action }) => {
15
+ const history = useHistory();
16
+ const handleClick = () => {
17
+ if (action.action) {
18
+ action.action();
19
+ }
20
+
21
+ if (action.href) {
22
+ history.push(action.href);
23
+ }
24
+ };
25
+
26
+ let buttonAppearance: ButtonAppearance | undefined;
27
+ if (action.status === undefined) {
28
+ buttonAppearance = 'primary';
29
+ } else if (!action.disabled) {
30
+ buttonAppearance = 'secondary';
31
+ }
32
+
33
+ return (
34
+ <Flex gap="2" alignItems="center" className="qa-brand-card-single-action">
35
+ <ActionChip status={action.status} statusTooltip={action.statusTooltip} />
36
+ <Button
37
+ size="small"
38
+ onClick={handleClick}
39
+ disabled={action.disabled}
40
+ appearance={buttonAppearance}
41
+ >
42
+ {action.name}
43
+ </Button>
44
+ {action.disabled && (
45
+ <DisabledActionHelper>{action.disableTooltipContent?.()}</DisabledActionHelper>
46
+ )}
47
+ </Flex>
48
+ );
49
+ };
50
+
51
+ const ActionChip: FC<{ status?: ActionStatus; statusTooltip?: string }> = ({
52
+ status,
53
+ statusTooltip,
54
+ }) => {
55
+ if (status === undefined) {
56
+ return null;
57
+ }
58
+
59
+ const chip = (
60
+ <Chip
61
+ label={BrandActionChipLabel[status]}
62
+ color={BrandActionChipColor[status]}
63
+ size="small"
64
+ />
65
+ );
66
+
67
+ return statusTooltip ? (
68
+ <TooltipA2>
69
+ <TooltipA2.Trigger>{chip}</TooltipA2.Trigger>
70
+ <TooltipA2.Content>{statusTooltip}</TooltipA2.Content>
71
+ </TooltipA2>
72
+ ) : (
73
+ chip
74
+ );
75
+ };
76
+
77
+ const DisabledActionHelper: FC<PropsWithChildren> = ({ children }) => (
78
+ <TooltipA2>
79
+ <TooltipA2.Trigger>
80
+ <Button icon={HelpIcon} size="small" />
81
+ </TooltipA2.Trigger>
82
+ <TooltipA2.Content>{children}</TooltipA2.Content>
83
+ </TooltipA2>
84
+ );
@@ -1,16 +1,20 @@
1
1
  import { FC } from 'react';
2
+ import classNames from 'classnames';
2
3
 
3
4
  import { Grid } from '@servicetitan/anvil2';
4
5
 
5
6
  import { BrandCard } from '../brand-card/brand-card';
6
- import { BrandCardProps } from '../../../enums/brands';
7
+ import { BrandCardProps } from '../../../utils/interfaces';
7
8
 
8
9
  import * as Styles from '../styles.module.less';
9
10
 
10
- export const CardsGrid: FC<{ brands: BrandCardProps[] }> = ({ brands }) => (
11
- <Grid className={Styles.grid} gap="6">
12
- {brands.map(brand => {
13
- return <BrandCard key={brand.id} {...brand} />;
14
- })}
11
+ export const CardsGrid: FC<{
12
+ brands: BrandCardProps[];
13
+ qaIdentifier: string;
14
+ }> = ({ brands, qaIdentifier }) => (
15
+ <Grid className={classNames(Styles.grid, `qa-${qaIdentifier}-cards-grid`)} gap="6">
16
+ {brands.map(brand => (
17
+ <BrandCard key={brand.id} {...brand} />
18
+ ))}
15
19
  </Grid>
16
20
  );
@@ -5,10 +5,6 @@
5
5
  @card-max-width: 700px;
6
6
  @max-number-of-cards: 3;
7
7
 
8
- .delete-tooltip {
9
- min-width: 327px;
10
- }
11
-
12
8
  .grid {
13
9
  grid-template-columns: repeat(auto-fill, minmax(@card-min-width, 1fr));
14
10
  max-width: calc(@card-max-width * @max-number-of-cards + @spacing-3 * 2);
@@ -27,47 +23,14 @@
27
23
  }
28
24
 
29
25
  .brand-card {
30
- height: 200px;
31
26
  min-width: @card-min-width;
32
27
  max-width: @card-max-width;
33
- box-sizing: border-box;
34
- background: inherit !important;
35
-
36
- :global(.CardSection) {
37
- height: 150px;
38
- background-color: @color-white;
39
- }
40
28
  }
41
29
 
42
30
  .card-content {
43
- height: 139px;
44
- }
45
-
46
- .brand-img {
47
- width: @spacing-6;
48
- height: @spacing-6;
49
- min-width: @spacing-6;
50
- min-height: @spacing-6;
51
- border-radius: @border-radius-circular;
52
- border: solid 1px @color-neutral-60;
53
- }
54
-
55
- .brand-img-placeholder {
56
- .brand-img();
57
- background-color: @color-neutral-60;
58
- }
59
-
60
- .tags {
61
- height: 41px;
62
- margin-bottom: 2px;
63
- margin-top: auto;
64
- }
65
-
66
- .default-tag {
67
- width: 53px;
31
+ width: 100%;
68
32
  }
69
33
 
70
- .corporate-tag {
71
- min-width: 86px;
72
- margin-bottom: @spacing-half;
34
+ .card-with-error {
35
+ border-color: @color-red-500;
73
36
  }
@@ -1,11 +1,6 @@
1
1
  export const __esModule: true;
2
2
  export const brandCard: string;
3
- export const brandImg: string;
4
- export const brandImgPlaceholder: string;
5
3
  export const cardContent: string;
6
- export const corporateTag: string;
7
- export const defaultTag: string;
8
- export const deleteTooltip: string;
4
+ export const cardWithError: string;
9
5
  export const grid: string;
10
- export const tags: string;
11
6
 
@@ -1,23 +1,5 @@
1
- import { DomainValidationStatus } from '@servicetitan/marketing-services-api/dist/settings/brand.mrk.api';
2
-
3
- export interface BrandAction {
4
- name: string;
5
- disabled?: boolean;
6
-
7
- tooltipMessage?: string;
8
- inProgress?: boolean;
9
- isConfigured?: boolean;
10
- href?: string;
11
- action?: () => void;
12
- }
13
-
14
- export interface BrandCardProps {
15
- id: string;
16
- actions: BrandAction | BrandAction[];
17
- logo?: string;
18
- name?: string;
19
- isDefault: boolean;
20
- email?: string;
21
- domainValidationStatus?: DomainValidationStatus;
22
- isShared: boolean;
1
+ export enum ActionStatus {
2
+ Error,
3
+ Pending,
4
+ Approved,
23
5
  }
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from './components/settings';
2
2
  export * from './components/campaign-actions';
3
- export * from './enums/brands';
3
+ export * from './components/settings';
4
4
  export * from './utils/helpers';
5
+ export * from './utils/interfaces';
6
+ export * from './enums/brands';
@@ -0,0 +1,23 @@
1
+ import { JSX } from 'react';
2
+ import { ActionStatus } from '../enums/brands';
3
+
4
+ export interface BrandAction {
5
+ name: string;
6
+ disabled?: boolean;
7
+ disableTooltipContent?: () => JSX.Element | string;
8
+ href?: string;
9
+ status?: ActionStatus;
10
+ statusTooltip?: string;
11
+ action?: () => void;
12
+ }
13
+
14
+ export interface BrandCardProps {
15
+ id: string;
16
+ hasError?: boolean;
17
+ actions: BrandAction | BrandAction[];
18
+ logo?: string;
19
+ name?: string;
20
+ isDefault: boolean;
21
+ email?: string;
22
+ isShared?: boolean;
23
+ }
@@ -0,0 +1,14 @@
1
+ import { ActionStatus } from '../enums/brands';
2
+ import { tokens } from '@servicetitan/tokens/core';
3
+
4
+ export const BrandActionChipLabel: Record<ActionStatus, string> = {
5
+ [ActionStatus.Pending]: 'Pending',
6
+ [ActionStatus.Error]: 'Error',
7
+ [ActionStatus.Approved]: 'Complete',
8
+ };
9
+
10
+ export const BrandActionChipColor: Record<ActionStatus, string> = {
11
+ [ActionStatus.Pending]: tokens.colorOrange200,
12
+ [ActionStatus.Error]: tokens.colorRed500,
13
+ [ActionStatus.Approved]: tokens.colorGreen200,
14
+ };