@servicetitan/mpa-components 0.1.0 → 0.2.1

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 (41) hide show
  1. package/lib/components/settings/company-details/company-details-form.stories.js +1 -1
  2. package/lib/components/settings/company-details/company-details-form.stories.js.map +1 -1
  3. package/lib/components/settings/email-validation/email-validation.stories.d.ts +11 -0
  4. package/lib/components/settings/email-validation/email-validation.stories.d.ts.map +1 -0
  5. package/lib/components/settings/email-validation/email-validation.stories.js +84 -0
  6. package/lib/components/settings/email-validation/email-validation.stories.js.map +1 -0
  7. package/lib/components/settings/email-validation/index.d.ts +32 -0
  8. package/lib/components/settings/email-validation/index.d.ts.map +1 -0
  9. package/lib/components/settings/email-validation/index.js +39 -0
  10. package/lib/components/settings/email-validation/index.js.map +1 -0
  11. package/lib/components/settings/result-definitions-modal/header.d.ts +3 -0
  12. package/lib/components/settings/result-definitions-modal/header.d.ts.map +1 -0
  13. package/lib/components/settings/result-definitions-modal/header.js +5 -0
  14. package/lib/components/settings/result-definitions-modal/header.js.map +1 -0
  15. package/lib/components/settings/result-definitions-modal/index.d.ts +7 -0
  16. package/lib/components/settings/result-definitions-modal/index.d.ts.map +1 -0
  17. package/lib/components/settings/result-definitions-modal/index.js +8 -0
  18. package/lib/components/settings/result-definitions-modal/index.js.map +1 -0
  19. package/lib/components/settings/result-definitions-modal/result-definitions-modal.module.less +17 -0
  20. package/lib/components/settings/result-definitions-modal/row.d.ts +14 -0
  21. package/lib/components/settings/result-definitions-modal/row.d.ts.map +1 -0
  22. package/lib/components/settings/result-definitions-modal/row.js +21 -0
  23. package/lib/components/settings/result-definitions-modal/row.js.map +1 -0
  24. package/lib/components/settings/settings-section/index.d.ts +1 -2
  25. package/lib/components/settings/settings-section/index.d.ts.map +1 -1
  26. package/lib/components/settings/settings-section/settings-section.stories.d.ts +9 -0
  27. package/lib/components/settings/settings-section/settings-section.stories.d.ts.map +1 -0
  28. package/lib/components/settings/settings-section/settings-section.stories.js +12 -0
  29. package/lib/components/settings/settings-section/settings-section.stories.js.map +1 -0
  30. package/package.json +2 -2
  31. package/src/components/settings/company-details/company-details-form.stories.tsx +1 -1
  32. package/src/components/settings/email-validation/email-validation.stories.tsx +89 -0
  33. package/src/components/settings/email-validation/index.tsx +267 -0
  34. package/src/components/settings/result-definitions-modal/header.tsx +27 -0
  35. package/src/components/settings/result-definitions-modal/index.tsx +127 -0
  36. package/src/components/settings/result-definitions-modal/result-definitions-modal.module.less +17 -0
  37. package/src/components/settings/result-definitions-modal/result-definitions-modal.module.less.d.ts +5 -0
  38. package/src/components/settings/result-definitions-modal/row.tsx +48 -0
  39. package/src/components/settings/settings-section/index.tsx +1 -1
  40. package/src/components/settings/settings-section/settings-section.stories.tsx +16 -0
  41. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,267 @@
1
+ import classnames from 'classnames';
2
+ import { action } from 'mobx';
3
+ import { FC, useCallback } from 'react';
4
+ import { observer, useLocalStore } from 'mobx-react';
5
+
6
+ import {
7
+ BodyText,
8
+ Button,
9
+ Card,
10
+ Container,
11
+ Form,
12
+ Grid,
13
+ Layout,
14
+ Link,
15
+ Stack,
16
+ Text,
17
+ Tooltip,
18
+ } from '@servicetitan/design-system';
19
+ import {
20
+ DateRangeColumnMenuFilter,
21
+ multiSelectColumnMenuFilter,
22
+ StandardColumnMenuFilter,
23
+ Table,
24
+ TableColumn,
25
+ TableState,
26
+ TableCellProps,
27
+ } from '@servicetitan/table';
28
+
29
+ import { ResultDefinitionsModal } from '../result-definitions-modal';
30
+
31
+ interface EmailValidationLocalStore {
32
+ open: boolean;
33
+ setOpen(state: boolean): void;
34
+ }
35
+
36
+ export interface EmailValidationTableRecord<T> {
37
+ id: number;
38
+ active: boolean;
39
+ createdOn: Date;
40
+ email?: string | undefined;
41
+ result: T;
42
+ dateAdded?: Date | undefined;
43
+ }
44
+
45
+ export interface EmailValidationProps<
46
+ TEmailValidationType extends string = string,
47
+ TEmailValidationResult extends string = string
48
+ > {
49
+ className?: string;
50
+ loading: boolean;
51
+ resultGrid: TableState<EmailValidationTableRecord<TEmailValidationResult>>;
52
+ validationRiskType: TEmailValidationType;
53
+ handleDownload(): void;
54
+ setValidationRiskType(type: TEmailValidationType): void;
55
+ }
56
+
57
+ interface EmailValidationHocOptions<
58
+ TEmailValidationType extends string = string,
59
+ TEmailValidationResult extends string = string
60
+ > {
61
+ resultToText: Map<TEmailValidationResult, string>;
62
+ levelTypeMap: {
63
+ high: TEmailValidationType;
64
+ medium: TEmailValidationType;
65
+ low: TEmailValidationType;
66
+ };
67
+ levelsResultMap: Map<TEmailValidationType, TEmailValidationResult[]>;
68
+ resultToTooltipText: Map<TEmailValidationResult, string>;
69
+ TenantDateCell: FC<TableCellProps>;
70
+ }
71
+
72
+ export function emailValidationHoc<
73
+ TEmailValidationType extends string = string,
74
+ TEmailValidationResult extends string = string
75
+ >({
76
+ resultToText,
77
+ levelTypeMap,
78
+ resultToTooltipText,
79
+ levelsResultMap,
80
+ TenantDateCell,
81
+ }: EmailValidationHocOptions<TEmailValidationType, TEmailValidationResult>) {
82
+ const ResultColumnMenuFilter = multiSelectColumnMenuFilter(
83
+ [...resultToText.keys()],
84
+ result => resultToText.get(result)!
85
+ );
86
+
87
+ const ResultCell: FC<TableCellProps> = props => {
88
+ const { result } = props.dataItem as { result: TEmailValidationResult };
89
+
90
+ return (
91
+ <td className="qa-result-cell of-visible-i">
92
+ <Tooltip text={resultToTooltipText.get(result)} className="qa-result-cell-tooltip">
93
+ {resultToText.get(result)}
94
+ </Tooltip>
95
+ </td>
96
+ );
97
+ };
98
+
99
+ function EmailValidationImpl({
100
+ className,
101
+ handleDownload,
102
+ loading,
103
+ resultGrid,
104
+ setValidationRiskType,
105
+ validationRiskType,
106
+ }: EmailValidationProps<TEmailValidationType>) {
107
+ const localStore: EmailValidationLocalStore = useLocalStore(() => ({
108
+ open: false,
109
+ setOpen: action(function (this: EmailValidationLocalStore, state: boolean) {
110
+ this.open = state;
111
+ }),
112
+ }));
113
+
114
+ const handleCheck = useCallback(
115
+ (riskType: TEmailValidationType) => () => {
116
+ setValidationRiskType(riskType);
117
+ },
118
+ [setValidationRiskType]
119
+ );
120
+
121
+ const handleClickLearnMore = () => {
122
+ localStore.setOpen(true);
123
+ };
124
+
125
+ const handleClose = () => {
126
+ localStore.setOpen(false);
127
+ };
128
+
129
+ const getResultsItemsForLevel = (level: TEmailValidationType) => {
130
+ return levelsResultMap.get(level)?.map(r => <div key={r}>{r}</div>);
131
+ };
132
+
133
+ return (
134
+ <Container className={classnames(className, 'p-b-5 qa-email-validation')}>
135
+ <Stack justifyContent="space-between" alignItems="center" className="m-b-2">
136
+ <Text size={4} className="m-r-3">
137
+ Email Validation
138
+ </Text>
139
+ <Button
140
+ small
141
+ primary
142
+ onClick={handleDownload}
143
+ className="qa-email-validation-download-csv"
144
+ iconName="file_download"
145
+ iconPosition="left"
146
+ >
147
+ Download CSV
148
+ </Button>
149
+ </Stack>
150
+ <Layout type="support" direction="right">
151
+ <Layout.Section>
152
+ <Text size={2} className="m-b-4 c-neutral-90">
153
+ Email Validation acts like a filter that will block emails from going to
154
+ bad email addresses. You won't necessarily see these emails in your
155
+ suppression list, but it will effect your number “Sent”.
156
+ </Text>
157
+ </Layout.Section>
158
+ <Layout.Section />
159
+ </Layout>
160
+ <Grid relaxed>
161
+ <Grid.Column width={3}>
162
+ <Card>
163
+ <Card.Section light>
164
+ <Text bold size={3} className="m-b-1">
165
+ Set Your Preference
166
+ </Text>
167
+ <Text size={2} className="m-b-1">
168
+ Choose what level you want to enable for auto-suppression.
169
+ </Text>
170
+ <Link
171
+ primary
172
+ onClick={handleClickLearnMore}
173
+ className="qa-email-validation-learn-more"
174
+ >
175
+ Learn More
176
+ </Link>
177
+ </Card.Section>
178
+ <Card.Section>
179
+ <Form.Radio
180
+ label={
181
+ <div className="m-l-1">
182
+ <Text bold size={3} className="m-b-1">
183
+ High
184
+ <span className="c-neutral-90 fs-1 m-l-1">
185
+ (recommended)
186
+ </span>
187
+ </Text>
188
+ <BodyText size="small" className="c-neutral-90">
189
+ {getResultsItemsForLevel(levelTypeMap.high)}
190
+ </BodyText>
191
+ </div>
192
+ }
193
+ checked={validationRiskType === levelTypeMap.high}
194
+ onChange={handleCheck(levelTypeMap.high)}
195
+ className="qa-email-validation-high"
196
+ />
197
+ </Card.Section>
198
+ <Card.Section>
199
+ <Form.Radio
200
+ label={
201
+ <div className="m-l-1">
202
+ <Text bold size={3} className="m-b-1">
203
+ Medium
204
+ </Text>
205
+ <BodyText size="small" className="c-neutral-90">
206
+ {getResultsItemsForLevel(levelTypeMap.medium)}
207
+ </BodyText>
208
+ </div>
209
+ }
210
+ checked={validationRiskType === levelTypeMap.medium}
211
+ onChange={handleCheck(levelTypeMap.medium)}
212
+ className="qa-email-validation-medium"
213
+ />
214
+ </Card.Section>
215
+ <Card.Section>
216
+ <Form.Radio
217
+ label={
218
+ <div className="m-l-1">
219
+ <Text bold size={3} className="m-b-1">
220
+ Low
221
+ </Text>
222
+ <BodyText size="small" className="c-neutral-90">
223
+ {getResultsItemsForLevel(levelTypeMap.low)}
224
+ </BodyText>
225
+ </div>
226
+ }
227
+ checked={validationRiskType === levelTypeMap.low}
228
+ onChange={handleCheck(levelTypeMap.low)}
229
+ className="qa-email-validation-low"
230
+ />
231
+ </Card.Section>
232
+ </Card>
233
+ </Grid.Column>
234
+ <Grid.Column width={9}>
235
+ <Table
236
+ tableState={resultGrid}
237
+ sortable
238
+ loading={loading}
239
+ className="qa-email-validation-table"
240
+ scrollable="none"
241
+ >
242
+ <TableColumn
243
+ field="email"
244
+ title="Email Address"
245
+ columnMenu={StandardColumnMenuFilter}
246
+ />
247
+ <TableColumn
248
+ field="result"
249
+ title="Result"
250
+ cell={ResultCell}
251
+ columnMenu={ResultColumnMenuFilter}
252
+ />
253
+ <TableColumn
254
+ field="dateAdded"
255
+ title="Date Added"
256
+ cell={TenantDateCell}
257
+ columnMenu={DateRangeColumnMenuFilter}
258
+ />
259
+ </Table>
260
+ </Grid.Column>
261
+ </Grid>
262
+ {localStore.open && <ResultDefinitionsModal onClose={handleClose} />}
263
+ </Container>
264
+ );
265
+ }
266
+ return observer(EmailValidationImpl);
267
+ }
@@ -0,0 +1,27 @@
1
+ import { FC } from 'react';
2
+ import { Grid, Text } from '@servicetitan/design-system';
3
+
4
+ import * as Styles from './result-definitions-modal.module.less';
5
+
6
+ export const Header: FC = () => (
7
+ <div className="position-sticky position-t-0 z-popover">
8
+ <Grid columns={3} className="p-y-1 bg-white">
9
+ <Grid.Column>
10
+ <Text size={1} className="c-neutral-90 tt-uppercase">
11
+ Result
12
+ </Text>
13
+ </Grid.Column>
14
+ <Grid.Column>
15
+ <Text size={1} className="c-neutral-90 tt-uppercase">
16
+ Risk
17
+ </Text>
18
+ </Grid.Column>
19
+ <Grid.Column>
20
+ <Text size={1} className="c-neutral-90 tt-uppercase">
21
+ Recommendation
22
+ </Text>
23
+ </Grid.Column>
24
+ </Grid>
25
+ <hr className={Styles.divider} />
26
+ </div>
27
+ );
@@ -0,0 +1,127 @@
1
+ import { FC } from 'react';
2
+ import classnames from 'classnames';
3
+ import { Modal, Text } from '@servicetitan/design-system';
4
+
5
+ import { Header } from './header';
6
+ import { Row, RiskType } from './row';
7
+
8
+ import * as Styles from './result-definitions-modal.module.less';
9
+
10
+ interface ResultDefinitionsModalProps {
11
+ onClose(): void;
12
+ }
13
+
14
+ export const ResultDefinitionsModal: FC<ResultDefinitionsModalProps> = ({ onClose }) => (
15
+ <Modal
16
+ open
17
+ closable
18
+ title="Email Validation Result Definitions"
19
+ onClose={onClose}
20
+ className={classnames(Styles.modal, 'qa-result-definitions-modal')}
21
+ >
22
+ <Header />
23
+ <div className={Styles.wall} />
24
+ <Row result="Undeliverable" risk={RiskType.VeryHigh} recommendation="Suppress" />
25
+ <hr className={Styles.divider} />
26
+ <Text size={2} className="m-y-2">
27
+ Address was determined invalid after performing multiple checks including MX, SMTP,
28
+ etc., likely due to:
29
+ <ul className="m-y-0">
30
+ <li>The receiving recipient domain is not configured to accept email</li>
31
+ <li>
32
+ The SMTP commands issued resulted in a hard bounce, i.e., "user does not exist"
33
+ or "mailbox full" errors
34
+ </li>
35
+ <li>
36
+ The recipient address historically hard-bounced or appeared on suppression lists
37
+ </li>
38
+ <li>
39
+ Mailbox providers require specific standard for an email address including
40
+ min/max number of characters, max number of periods, etc.
41
+ </li>
42
+ </ul>
43
+ </Text>
44
+ <hr className={Styles.divider} />
45
+ <Row result="Risky" risk={RiskType.Medium} recommendation="Proceed With Caution" />
46
+ <hr className={Styles.divider} />
47
+ <Text size={2} className="m-y-2">
48
+ Address was determined risky based on historical analysis or problematic results, likely
49
+ due to:
50
+ <ul className="m-y-0">
51
+ <li>
52
+ The receiving address may be valid, but was found on one or more suppression
53
+ lists Addresses matching a known spam trap are not ideal for sending campaigns
54
+ to and can impact reputation
55
+ </li>
56
+ </ul>
57
+ </Text>
58
+ <hr className={Styles.divider} />
59
+ <Row result="Typo" risk={RiskType.High} recommendation="Suppress" />
60
+ <hr className={Styles.divider} />
61
+ <Text size={2} className="m-y-2">
62
+ Address was possibly typed incorrectly during sign-up, or matches known typo domains,
63
+ likely due to:
64
+ <ul className="m-y-0">
65
+ <li>
66
+ Typos are mistyped address domains like "gmil.com" instead of "gmail.com," and
67
+ are often undeliverable. Address is checked against database of common domain
68
+ misspellings. These kinds of addresses are usually not good for sending.
69
+ </li>
70
+ </ul>
71
+ </Text>
72
+ <hr className={Styles.divider} />
73
+ <Row result="Accept-All" risk={RiskType.Medium} recommendation="Proceed With Caution" />
74
+ <hr className={Styles.divider} />
75
+ <Text size={2} className="m-y-2">
76
+ The receiving domain of the address accepts all mail, indicating it may be a spam trap,
77
+ likely due to:
78
+ <ul className="m-y-0">
79
+ <li>
80
+ Some domains do not distinguish between existing and non-existing addresses and
81
+ accept all mail
82
+ </li>
83
+ </ul>
84
+ </Text>
85
+ <hr className={Styles.divider} />
86
+ <Row result="Dispose" risk={RiskType.Medium} recommendation="Proceed With Caution" />
87
+ <hr className={Styles.divider} />
88
+ <Text size={2} className="m-y-2">
89
+ The receiving domain of the address accepts all mail, indicating it may be a spam trap,
90
+ likely due to:
91
+ <ul className="m-y-0">
92
+ <li>
93
+ Some domains do not distinguish between existing and non-existing addresses and
94
+ accept all mail
95
+ </li>
96
+ </ul>
97
+ </Text>
98
+ <hr className={Styles.divider} />
99
+ <Row result="Role" risk={RiskType.Medium} recommendation="Proceed With Caution" />
100
+ <hr className={Styles.divider} />
101
+ <Text size={2} className="m-y-2">
102
+ Address likely is not a particular person, but rather a group or department, likely due
103
+ to:
104
+ <ul className="m-y-0">
105
+ <li>
106
+ Role addresses belong to the end user and are deliverable, but include multiple
107
+ end recipients as part of a larger distribution list. These kinds of addreses
108
+ are usually not good for sending campaigns
109
+ </li>
110
+ </ul>
111
+ </Text>
112
+ <hr className={Styles.divider} />
113
+ <Row result="Unknown" risk={RiskType.Medium} recommendation="Proceed With Caution" />
114
+ <hr className={Styles.divider} />
115
+ <Text size={2} className="m-y-2">
116
+ A connection problem or unresponsive SMTP server prevented validation, likely due to:
117
+ <ul className="m-y-0">
118
+ <li>Unable to connect and resolve to service during SMTP connection</li>
119
+ <li>Unable to successfully get a response from SMTP server</li>
120
+ <li>
121
+ Address was outside known result definitions and criteria and was unable to be
122
+ classified
123
+ </li>
124
+ </ul>
125
+ </Text>
126
+ </Modal>
127
+ );
@@ -0,0 +1,17 @@
1
+ @import (reference) '@servicetitan/tokens/dist/tokens.less';
2
+
3
+ .modal {
4
+ :global(.Modal__content) {
5
+ padding-top: @spacing-0;
6
+ }
7
+
8
+ .divider {
9
+ margin: @spacing-1 @spacing-0;
10
+ border: 0;
11
+ border-bottom: 1px solid @color-neutral-60;
12
+ }
13
+
14
+ .wall {
15
+ height: 1px;
16
+ }
17
+ }
@@ -0,0 +1,5 @@
1
+ export const __esModule: true;
2
+ export const modal: string;
3
+ export const divider: string;
4
+ export const wall: string;
5
+
@@ -0,0 +1,48 @@
1
+ import { FC } from 'react';
2
+
3
+ import { Grid, Text, Tag } from '@servicetitan/design-system';
4
+ import tokens from '@servicetitan/tokens';
5
+
6
+ export enum RiskType {
7
+ Medium,
8
+ High,
9
+ VeryHigh,
10
+ }
11
+
12
+ const riskTypeToColor = new Map<RiskType, string>([
13
+ [RiskType.Medium, 'warning'],
14
+ [RiskType.High, tokens.colorRed500],
15
+ [RiskType.VeryHigh, tokens.colorRed600],
16
+ ]);
17
+
18
+ const riskTypeToText = new Map<RiskType, string>([
19
+ [RiskType.Medium, 'Medium'],
20
+ [RiskType.High, 'High'],
21
+ [RiskType.VeryHigh, 'Very High'],
22
+ ]);
23
+
24
+ interface RowProps {
25
+ result: string;
26
+ risk: RiskType;
27
+ recommendation: string;
28
+ }
29
+
30
+ export const Row: FC<RowProps> = ({ result, risk, recommendation }) => (
31
+ <Grid columns={3} className="m-y-1">
32
+ <Grid.Column>
33
+ <Text size={2} bold>
34
+ {result}
35
+ </Text>
36
+ </Grid.Column>
37
+ <Grid.Column>
38
+ <Tag color={riskTypeToColor.get(risk)} compact>
39
+ {riskTypeToText.get(risk)}
40
+ </Tag>
41
+ </Grid.Column>
42
+ <Grid.Column>
43
+ <Text size={2} bold>
44
+ {recommendation}
45
+ </Text>
46
+ </Grid.Column>
47
+ </Grid>
48
+ );
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react';
4
4
 
5
5
  import { Text, Card, Layout } from '@servicetitan/design-system';
6
6
 
7
- interface SectionProps {
7
+ export interface SectionProps {
8
8
  title: string;
9
9
  text?: JSX.Element | string;
10
10
  children: ReactNode;
@@ -0,0 +1,16 @@
1
+ import { Headline } from '@servicetitan/design-system';
2
+ import { SettingsSection as Component } from '.';
3
+
4
+ export default {
5
+ title: 'MPA Components/settings/SettingsSection',
6
+ component: Component,
7
+ parameters: {},
8
+ };
9
+
10
+ export const SettingsSection = () => {
11
+ return (
12
+ <Component title="Hey! The test">
13
+ <Headline>The body</Headline>
14
+ </Component>
15
+ );
16
+ };