Skip to content
This repository was archived by the owner on Oct 11, 2022. It is now read-only.

Commit 5e0a2fa

Browse files
authored
Merge pull request #4621 from withspectrum/import-user-emails
Import emails to invite to community
2 parents 5ec44ec + f85fa1a commit 5e0a2fa

File tree

8 files changed

+181
-44
lines changed

8 files changed

+181
-44
lines changed

cypress/integration/community/settings/private_invite_link_spec.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ describe('private community invite link settings', () => {
4343
// grab the input again and compare its previous value
4444
// to the current value
4545
cy.get('[data-cy="join-link-input"]')
46+
.scrollIntoView()
4647
.invoke('val')
4748
.should(val2 => {
4849
expect(val1).not.to.eq(val2);
@@ -51,6 +52,7 @@ describe('private community invite link settings', () => {
5152

5253
// disable
5354
cy.get('[data-cy="toggle-token-link-invites-checked"]')
55+
.scrollIntoView()
5456
.should('be.visible')
5557
.click();
5658

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
"cors": "^2.8.3",
7878
"cryptr": "^3.0.0",
7979
"css.escape": "^1.5.1",
80-
"cypress": "^3.1.3",
80+
"cypress": "3.1.5",
8181
"datadog-metrics": "^0.8.1",
8282
"dataloader": "^1.4.0",
8383
"debounce": "^1.2.0",

src/components/emailInvitationForm/index.js

Lines changed: 120 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,17 @@ import { Button } from '../buttons';
1111
import { Error } from '../formElements';
1212
import { SectionCardFooter } from 'src/components/settingsViews/style';
1313
import { withCurrentUser } from 'src/components/withCurrentUser';
14+
import MediaInput from 'src/components/mediaInput';
1415
import {
1516
EmailInviteForm,
1617
EmailInviteInput,
17-
AddRow,
18+
Action,
19+
ActionAsLabel,
20+
ActionHelpText,
1821
RemoveRow,
1922
CustomMessageToggle,
2023
CustomMessageTextAreaStyles,
24+
HiddenInput,
2125
} from './style';
2226

2327
type Props = {
@@ -37,16 +41,20 @@ type ContactProps = {
3741
type State = {
3842
isLoading: boolean,
3943
contacts: Array<ContactProps>,
44+
importError: string,
4045
hasCustomMessage: boolean,
4146
customMessageString: string,
4247
customMessageError: boolean,
48+
inputValue: ?string,
4349
};
4450

4551
class EmailInvitationForm extends React.Component<Props, State> {
46-
constructor() {
47-
super();
52+
constructor(props) {
53+
super(props);
54+
4855
this.state = {
4956
isLoading: false,
57+
importError: '',
5058
contacts: [
5159
{
5260
email: '',
@@ -70,6 +78,7 @@ class EmailInvitationForm extends React.Component<Props, State> {
7078
hasCustomMessage: false,
7179
customMessageString: '',
7280
customMessageError: false,
81+
inputValue: '',
7382
};
7483
}
7584

@@ -87,7 +96,7 @@ class EmailInvitationForm extends React.Component<Props, State> {
8796
this.setState({ isLoading: true });
8897

8998
let validContacts = contacts
90-
.filter(contact => contact.error === false)
99+
.filter(contact => !contact.error)
91100
.filter(contact => contact.email !== currentUser.email)
92101
.filter(contact => contact.email.length > 0)
93102
.filter(contact => isEmail(contact.email))
@@ -234,17 +243,102 @@ class EmailInvitationForm extends React.Component<Props, State> {
234243
});
235244
};
236245

246+
handleFile = evt => {
247+
this.setState({
248+
importError: '',
249+
});
250+
251+
// Only show loading indicator for large files
252+
// where it takes > 200ms to load
253+
const timeout = setTimeout(() => {
254+
this.setState({
255+
isLoading: true,
256+
});
257+
}, 200);
258+
259+
const reader = new FileReader();
260+
reader.onload = file => {
261+
clearTimeout(timeout);
262+
this.setState({
263+
isLoading: false,
264+
});
265+
266+
let parsed;
267+
try {
268+
if (typeof reader.result !== 'string') return;
269+
parsed = JSON.parse(reader.result);
270+
} catch (err) {
271+
this.setState({
272+
importError: 'Only .json files are supported for import.',
273+
});
274+
return;
275+
}
276+
277+
if (!Array.isArray(parsed)) {
278+
this.setState({
279+
importError:
280+
'Your JSON data is in the wrong format. Please provide either an array of emails ["[email protected]"] or an array of objects with an "email" property and (optionally) a "name" property [{ "email": "[email protected]", "name": "Me" }].',
281+
});
282+
return;
283+
}
284+
285+
const formatted = parsed.map(value => {
286+
if (typeof value === 'string')
287+
return {
288+
email: value,
289+
};
290+
291+
return {
292+
email: value.email,
293+
firstName: value.firstName || value.name,
294+
lastName: value.lastName,
295+
};
296+
});
297+
298+
const validated = formatted
299+
.map(value => {
300+
if (!isEmail(value.email)) return { ...value, error: true };
301+
return value;
302+
})
303+
.filter(Boolean);
304+
305+
const consolidated = [
306+
...this.state.contacts.filter(
307+
contact =>
308+
contact.email.length > 0 ||
309+
contact.firstName.length > 0 ||
310+
contact.lastName.length > 0
311+
),
312+
...validated,
313+
];
314+
315+
const unique = consolidated.filter(
316+
(obj, i) =>
317+
consolidated.findIndex(a => a['email'] === obj['email']) === i
318+
);
319+
320+
this.setState({
321+
contacts: unique,
322+
inputValue: '',
323+
});
324+
};
325+
326+
reader.readAsText(evt.target.files[0]);
327+
};
328+
237329
render() {
238330
const {
239331
contacts,
240332
isLoading,
241333
hasCustomMessage,
242334
customMessageString,
243335
customMessageError,
336+
importError,
244337
} = this.state;
245338

246339
return (
247340
<div>
341+
{importError && <Error>{importError}</Error>}
248342
{contacts.map((contact, i) => {
249343
return (
250344
<EmailInviteForm key={i}>
@@ -270,14 +364,28 @@ class EmailInvitationForm extends React.Component<Props, State> {
270364
);
271365
})}
272366

273-
<AddRow onClick={this.addRow}>+ Add another</AddRow>
367+
<Action onClick={this.addRow}>
368+
<Icon glyph="plus" size={20} /> Add row
369+
</Action>
370+
<ActionAsLabel mb="8px">
371+
<HiddenInput
372+
value={this.state.inputValue}
373+
type="file"
374+
accept=".json"
375+
onChange={this.handleFile}
376+
/>
377+
<Icon size={20} glyph="upload" /> Import emails
378+
</ActionAsLabel>
379+
<ActionHelpText>
380+
Upload a .json file with an array of email addresses.
381+
</ActionHelpText>
274382

275-
<CustomMessageToggle onClick={this.toggleCustomMessage}>
383+
<Action onClick={this.toggleCustomMessage}>
276384
<Icon glyph={hasCustomMessage ? 'view-close' : 'post'} size={20} />
277385
{hasCustomMessage
278386
? 'Remove custom message'
279387
: 'Optional: Add a custom message to your invitation'}
280-
</CustomMessageToggle>
388+
</Action>
281389

282390
{hasCustomMessage && (
283391
<Textarea
@@ -294,12 +402,11 @@ class EmailInvitationForm extends React.Component<Props, State> {
294402
/>
295403
)}
296404

297-
{hasCustomMessage &&
298-
customMessageError && (
299-
<Error>
300-
Your custom invitation message can be up to 500 characters.
301-
</Error>
302-
)}
405+
{hasCustomMessage && customMessageError && (
406+
<Error>
407+
Your custom invitation message can be up to 500 characters.
408+
</Error>
409+
)}
303410

304411
<SectionCardFooter>
305412
<Button

src/components/emailInvitationForm/style.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,25 +40,43 @@ export const EmailInviteInput = styled.input`
4040
}
4141
`;
4242

43-
export const AddRow = styled.div`
43+
export const HiddenInput = styled.input`
44+
display: none;
45+
`;
46+
47+
export const Action = styled.button`
4448
display: flex;
4549
width: 100%;
4650
justify-content: center;
4751
padding: 8px;
4852
background: ${theme.bg.wash};
4953
margin-top: 8px;
50-
margin-bottom: 16px;
54+
margin-bottom: ${props => props.mb || '16px'};
5155
font-size: 14px;
5256
color: ${theme.text.alt};
5357
font-weight: 500;
5458
border-radius: 4px;
5559
60+
.icon {
61+
margin-right: 4px;
62+
}
63+
5664
&:hover {
57-
color: ${theme.text.default};
65+
color: ${theme.text.secondary};
5866
cursor: pointer;
5967
}
6068
`;
6169

70+
export const ActionAsLabel = Action.withComponent('label');
71+
72+
export const ActionHelpText = styled.div`
73+
color: ${theme.text.alt};
74+
font-size: 14px;
75+
text-align: center;
76+
margin-top: 8px;
77+
margin-bottom: 24px;
78+
`;
79+
6280
export const RemoveRow = styled.div`
6381
margin-left: 4px;
6482
color: ${theme.text.alt};
@@ -86,6 +104,10 @@ export const CustomMessageToggle = styled.h4`
86104
}
87105
`;
88106

107+
export const FileUploadWrapper = styled.div`
108+
margin-right: 16px;
109+
`;
110+
89111
export const CustomMessageTextAreaStyles = {
90112
width: '100%',
91113
borderRadius: '8px',

src/components/icons/index.js

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/mediaInput/index.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ export default ({
66
onChange,
77
accept = '.png, .jpg, .jpeg, .gif, .mp4',
88
multiple = false,
9-
tipLocation,
9+
tipLocation = 'top-right',
10+
tipText = 'Upload photo',
11+
glyph = 'photo',
1012
}) => (
1113
<MediaLabel>
1214
<MediaInput
@@ -15,10 +17,6 @@ export default ({
1517
multiple={multiple}
1618
onChange={onChange}
1719
/>
18-
<Icon
19-
glyph="photo"
20-
tipLocation={tipLocation ? tipLocation : 'top-right'}
21-
tipText="Upload photo"
22-
/>
20+
<Icon glyph={glyph} tipLocation={tipLocation} tipText={tipText} />
2321
</MediaLabel>
2422
);

upload.svg

Lines changed: 5 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)