diff --git a/cypress/integration/community/settings/private_invite_link_spec.js b/cypress/integration/community/settings/private_invite_link_spec.js index 48a157128e..89a980df6f 100644 --- a/cypress/integration/community/settings/private_invite_link_spec.js +++ b/cypress/integration/community/settings/private_invite_link_spec.js @@ -43,6 +43,7 @@ describe('private community invite link settings', () => { // grab the input again and compare its previous value // to the current value cy.get('[data-cy="join-link-input"]') + .scrollIntoView() .invoke('val') .should(val2 => { expect(val1).not.to.eq(val2); @@ -51,6 +52,7 @@ describe('private community invite link settings', () => { // disable cy.get('[data-cy="toggle-token-link-invites-checked"]') + .scrollIntoView() .should('be.visible') .click(); diff --git a/package.json b/package.json index 899f82c0d8..ac904ae3d3 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "cors": "^2.8.3", "cryptr": "^3.0.0", "css.escape": "^1.5.1", - "cypress": "^3.1.3", + "cypress": "3.1.5", "datadog-metrics": "^0.8.1", "dataloader": "^1.4.0", "debounce": "^1.2.0", diff --git a/src/components/emailInvitationForm/index.js b/src/components/emailInvitationForm/index.js index 4402961a29..fe7db7aee4 100644 --- a/src/components/emailInvitationForm/index.js +++ b/src/components/emailInvitationForm/index.js @@ -11,13 +11,17 @@ import { Button } from '../buttons'; import { Error } from '../formElements'; import { SectionCardFooter } from 'src/components/settingsViews/style'; import { withCurrentUser } from 'src/components/withCurrentUser'; +import MediaInput from 'src/components/mediaInput'; import { EmailInviteForm, EmailInviteInput, - AddRow, + Action, + ActionAsLabel, + ActionHelpText, RemoveRow, CustomMessageToggle, CustomMessageTextAreaStyles, + HiddenInput, } from './style'; type Props = { @@ -37,16 +41,20 @@ type ContactProps = { type State = { isLoading: boolean, contacts: Array, + importError: string, hasCustomMessage: boolean, customMessageString: string, customMessageError: boolean, + inputValue: ?string, }; class EmailInvitationForm extends React.Component { - constructor() { - super(); + constructor(props) { + super(props); + this.state = { isLoading: false, + importError: '', contacts: [ { email: '', @@ -70,6 +78,7 @@ class EmailInvitationForm extends React.Component { hasCustomMessage: false, customMessageString: '', customMessageError: false, + inputValue: '', }; } @@ -87,7 +96,7 @@ class EmailInvitationForm extends React.Component { this.setState({ isLoading: true }); let validContacts = contacts - .filter(contact => contact.error === false) + .filter(contact => !contact.error) .filter(contact => contact.email !== currentUser.email) .filter(contact => contact.email.length > 0) .filter(contact => isEmail(contact.email)) @@ -234,6 +243,89 @@ class EmailInvitationForm extends React.Component { }); }; + handleFile = evt => { + this.setState({ + importError: '', + }); + + // Only show loading indicator for large files + // where it takes > 200ms to load + const timeout = setTimeout(() => { + this.setState({ + isLoading: true, + }); + }, 200); + + const reader = new FileReader(); + reader.onload = file => { + clearTimeout(timeout); + this.setState({ + isLoading: false, + }); + + let parsed; + try { + if (typeof reader.result !== 'string') return; + parsed = JSON.parse(reader.result); + } catch (err) { + this.setState({ + importError: 'Only .json files are supported for import.', + }); + return; + } + + if (!Array.isArray(parsed)) { + this.setState({ + importError: + 'Your JSON data is in the wrong format. Please provide either an array of emails ["hi@me.com"] or an array of objects with an "email" property and (optionally) a "name" property [{ "email": "hi@me.com", "name": "Me" }].', + }); + return; + } + + const formatted = parsed.map(value => { + if (typeof value === 'string') + return { + email: value, + }; + + return { + email: value.email, + firstName: value.firstName || value.name, + lastName: value.lastName, + }; + }); + + const validated = formatted + .map(value => { + if (!isEmail(value.email)) return { ...value, error: true }; + return value; + }) + .filter(Boolean); + + const consolidated = [ + ...this.state.contacts.filter( + contact => + contact.email.length > 0 || + contact.firstName.length > 0 || + contact.lastName.length > 0 + ), + ...validated, + ]; + + const unique = consolidated.filter( + (obj, i) => + consolidated.findIndex(a => a['email'] === obj['email']) === i + ); + + this.setState({ + contacts: unique, + inputValue: '', + }); + }; + + reader.readAsText(evt.target.files[0]); + }; + render() { const { contacts, @@ -241,10 +333,12 @@ class EmailInvitationForm extends React.Component { hasCustomMessage, customMessageString, customMessageError, + importError, } = this.state; return (
+ {importError && {importError}} {contacts.map((contact, i) => { return ( @@ -270,14 +364,28 @@ class EmailInvitationForm extends React.Component { ); })} - + Add another + + Add row + + + + Import emails + + + Upload a .json file with an array of email addresses. + - + {hasCustomMessage ? 'Remove custom message' : 'Optional: Add a custom message to your invitation'} - + {hasCustomMessage && (