From a5a5d9bbc763a61b75692fd6b0f66c1f58d05340 Mon Sep 17 00:00:00 2001 From: nickoferrall Date: Tue, 23 Feb 2021 12:34:39 -0500 Subject: [PATCH 1/9] add link check in teamNameValidation --- .../modules/newTeam/components/NewTeamForm/NewTeamForm.tsx | 5 ----- packages/client/validation/teamNameValidation.ts | 6 ++++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx index 6e94245414e..b5ddf690a77 100644 --- a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx +++ b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx @@ -215,11 +215,6 @@ class NewTeamForm extends Component { this.validate('teamName') } ) - // if (isNewOrg) { - // setTimeout(() => { - // this.newOrgInputRef.current && this.newOrgInputRef.current.focus() - // }) - // } } onSubmit = (e: React.FormEvent) => { diff --git a/packages/client/validation/teamNameValidation.ts b/packages/client/validation/teamNameValidation.ts index a74bfeccd39..74acab7d3f4 100644 --- a/packages/client/validation/teamNameValidation.ts +++ b/packages/client/validation/teamNameValidation.ts @@ -1,12 +1,14 @@ +import linkify from '../utils/linkify' import Legitity from './Legitity' -const teamNameValidation = (rawTeamName, teamNames) => { +const teamNameValidation = (rawTeamName: string, teamNames: string[]) => { return new Legitity(rawTeamName) .trim() .required('“The nameless wonder” is better than nothing') .min(2, 'The “A Team” had a longer name than that') .max(50, 'That isn’t very memorable. Maybe shorten it up?') - .test((val) => teamNames.includes(val) && 'That name is already taken') + .test((val) => (teamNames.includes(val) ? 'That name is already taken' : undefined)) + .test((val) => (linkify.match(val) ? 'Try using a name, not a link!' : undefined)) } export default teamNameValidation From 72575babff44e91a8d74decbe23f73db68a01d75 Mon Sep 17 00:00:00 2001 From: nickoferrall Date: Tue, 23 Feb 2021 12:47:22 -0500 Subject: [PATCH 2/9] verify team name on the server too --- packages/client/validation/templates.js | 21 +++++++++---------- .../mutations/helpers/addTeamValidation.js | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/client/validation/templates.js b/packages/client/validation/templates.js index 53e566b53ae..2458101cc4b 100644 --- a/packages/client/validation/templates.js +++ b/packages/client/validation/templates.js @@ -1,5 +1,6 @@ import {TASK_MAX_CHARS} from '../utils/constants' import {compositeIdRegex, emailRegex, idRegex} from './regex' +import linkify from '../utils/linkify' export const avatar = { size: (value) => @@ -71,20 +72,18 @@ export const makeTeamNameSchema = (teamNames) => (value) => .min(2, 'The “A Team” had a longer name than that') .max(50, 'That isn’t very memorable. Maybe shorten it up?') .test((val) => teamNames.includes(val) && 'That name is already taken') + .test((val) => (linkify.match(val) ? 'Try using a name, not a link!' : undefined)) export const optionalUrl = (value) => value .trim() - .test( - (value) => { - if (value) { - try { - new URL(value) - } catch (e) { - return e.message - } + .test((value) => { + if (value) { + try { + new URL(value) + } catch (e) { + return e.message } - }, - 'that url doesn’t look quite right' - ) + } + }, 'that url doesn’t look quite right') .max(2000, 'please use a shorter url') diff --git a/packages/server/graphql/mutations/helpers/addTeamValidation.js b/packages/server/graphql/mutations/helpers/addTeamValidation.js index 17389a71598..8887258ceba 100644 --- a/packages/server/graphql/mutations/helpers/addTeamValidation.js +++ b/packages/server/graphql/mutations/helpers/addTeamValidation.js @@ -1,5 +1,5 @@ import legitify from 'parabol-client/validation/legitify' -import { makeTeamNameSchema, requiredId } from 'parabol-client/validation/templates' +import {makeTeamNameSchema, requiredId} from 'parabol-client/validation/templates' export default function addTeamValidation(teamNames) { return legitify({ From 488f13a203aa813315aadcbb6184532cc91270ec Mon Sep 17 00:00:00 2001 From: nickoferrall Date: Tue, 23 Feb 2021 16:15:47 -0500 Subject: [PATCH 3/9] add org name link validation --- .../modules/newTeam/components/NewTeamForm/NewTeamForm.tsx | 2 ++ packages/client/mutations/AddOrgMutation.ts | 4 +++- packages/client/validation/templates.js | 2 ++ packages/server/graphql/mutations/addTeam.ts | 5 ++++- .../helpers/{addOrgValidation.js => addOrgValidation.ts} | 6 ++++-- .../helpers/{addTeamValidation.js => addTeamValidation.ts} | 2 +- 6 files changed, 16 insertions(+), 5 deletions(-) rename packages/server/graphql/mutations/helpers/{addOrgValidation.js => addOrgValidation.ts} (62%) rename packages/server/graphql/mutations/helpers/{addTeamValidation.js => addTeamValidation.ts} (79%) diff --git a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx index b5ddf690a77..2c7ee99fde4 100644 --- a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx +++ b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx @@ -22,6 +22,7 @@ import NewTeamFormOrgName from './NewTeamFormOrgName' import NewTeamFormTeamName from './NewTeamFormTeamName' import StyledError from '../../../../components/StyledError' import DashHeaderTitle from '../../../../components/DashHeaderTitle' +import linkify from '../../../../utils/linkify' const StyledForm = styled('form')({ margin: 0, @@ -154,6 +155,7 @@ class NewTeamForm extends Component { .required('Your new org needs a name!') .min(2, 'C’mon, you call that an organization?') .max(100, 'Maybe just the legal name?') + .test((val) => (linkify.match(val) ? 'Try using a name, not a link!' : undefined)) } handleBlur = (e: React.FocusEvent) => { diff --git a/packages/client/mutations/AddOrgMutation.ts b/packages/client/mutations/AddOrgMutation.ts index 713b689c8b7..7088aa9f59f 100644 --- a/packages/client/mutations/AddOrgMutation.ts +++ b/packages/client/mutations/AddOrgMutation.ts @@ -11,6 +11,7 @@ import { } from '../types/relayMutations' import {AddOrgMutation_organization} from '../__generated__/AddOrgMutation_organization.graphql' import {AddOrgMutation as TAddOrgMutation} from '../__generated__/AddOrgMutation.graphql' +import getGraphQLError from '../utils/relay/getGraphQLError' graphql` fragment AddOrgMutation_organization on AddOrgPayload { @@ -100,7 +101,8 @@ const AddOrgMutation: StandardMutation = ( onCompleted(res, errors) } const {addOrg} = res - if (!errors) { + const error = getGraphQLError(res, errors) + if (!error) { const {authToken} = addOrg atmosphere.setAuthToken(authToken) popOrganizationCreatedToast(addOrg, {atmosphere}) diff --git a/packages/client/validation/templates.js b/packages/client/validation/templates.js index 2458101cc4b..d47aac098c1 100644 --- a/packages/client/validation/templates.js +++ b/packages/client/validation/templates.js @@ -44,6 +44,7 @@ export const orgName = (value) => .required('Your new org needs a name!') .min(2, 'C’mon, you call that an organization?') .max(100, 'Maybe just the legal name?') + .test((val) => (linkify.match(val) ? 'Try using a name, not a link!' : undefined)) export const preferredName = (value) => value @@ -64,6 +65,7 @@ export const teamName = (value) => .required('“The nameless wonder” is better than nothing') .min(2, 'The “A Team” had a longer name than that') .max(50, 'That isn’t very memorable. Maybe shorten it up?') + .test((val) => (linkify.match(val) ? 'Try using a name, not a link!' : undefined)) export const makeTeamNameSchema = (teamNames) => (value) => value diff --git a/packages/server/graphql/mutations/addTeam.ts b/packages/server/graphql/mutations/addTeam.ts index 73f9f8ca82c..89197fe4e1c 100644 --- a/packages/server/graphql/mutations/addTeam.ts +++ b/packages/server/graphql/mutations/addTeam.ts @@ -45,7 +45,10 @@ export default { .getAll(orgId, {index: 'orgId'}) .run() - const orgTeamNames = orgTeams.map((team) => !team.isArchived && team.name) + const orgTeamNames = [] as string[] + orgTeams.forEach((team) => { + if (!team.isArchived) orgTeamNames.push(team.name) + }) const { data: {newTeam}, errors diff --git a/packages/server/graphql/mutations/helpers/addOrgValidation.js b/packages/server/graphql/mutations/helpers/addOrgValidation.ts similarity index 62% rename from packages/server/graphql/mutations/helpers/addOrgValidation.js rename to packages/server/graphql/mutations/helpers/addOrgValidation.ts index 200e2636956..6da9712e886 100644 --- a/packages/server/graphql/mutations/helpers/addOrgValidation.js +++ b/packages/server/graphql/mutations/helpers/addOrgValidation.ts @@ -1,11 +1,13 @@ import legitify from 'parabol-client/validation/legitify' -import { orgName, teamName } from 'parabol-client/validation/templates' +import {orgName, teamName} from 'parabol-client/validation/templates' export default function addOrgValidation() { return legitify({ newTeam: { name: teamName }, - orgName + orgName: { + orgName + } }) } diff --git a/packages/server/graphql/mutations/helpers/addTeamValidation.js b/packages/server/graphql/mutations/helpers/addTeamValidation.ts similarity index 79% rename from packages/server/graphql/mutations/helpers/addTeamValidation.js rename to packages/server/graphql/mutations/helpers/addTeamValidation.ts index 8887258ceba..8097ed4f74c 100644 --- a/packages/server/graphql/mutations/helpers/addTeamValidation.js +++ b/packages/server/graphql/mutations/helpers/addTeamValidation.ts @@ -1,7 +1,7 @@ import legitify from 'parabol-client/validation/legitify' import {makeTeamNameSchema, requiredId} from 'parabol-client/validation/templates' -export default function addTeamValidation(teamNames) { +export default function addTeamValidation(teamNames: string[]) { return legitify({ newTeam: { name: makeTeamNameSchema(teamNames), From f9ca889a483c2dcb7ba3e87bd71f14704e325863 Mon Sep 17 00:00:00 2001 From: nickoferrall Date: Tue, 23 Feb 2021 17:23:07 -0500 Subject: [PATCH 4/9] refactor to functional components and fix error object bugs --- .../components/StageTimerModalTimeLimit.tsx | 2 +- .../components/NewTeamForm/NewTeamForm.tsx | 294 ++++++++---------- .../NewTeamForm/NewTeamFormOrgName.tsx | 65 ++-- .../NewTeamForm/NewTeamFormTeamName.tsx | 40 ++- .../ProviderRow/SlackNotificationList.tsx | 4 +- .../ProviderRow/SlackNotificationRow.tsx | 6 +- .../mutations/helpers/addOrgValidation.ts | 4 +- 7 files changed, 183 insertions(+), 232 deletions(-) diff --git a/packages/client/components/StageTimerModalTimeLimit.tsx b/packages/client/components/StageTimerModalTimeLimit.tsx index 3593b9695cb..8cab4f75b19 100644 --- a/packages/client/components/StageTimerModalTimeLimit.tsx +++ b/packages/client/components/StageTimerModalTimeLimit.tsx @@ -107,7 +107,7 @@ const StageTimerModalTimeLimit = (props: Props) => { {scheduledEndTime ? 'Add Time' : `Start ${MeetingLabels.TIMER}`} - {error && {error}} + {error && {error.message}} ) } diff --git a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx index 2c7ee99fde4..6e6229b911d 100644 --- a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx +++ b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx @@ -1,5 +1,5 @@ import {NewTeamForm_organizations} from '../../../../__generated__/NewTeamForm_organizations.graphql' -import React, {Component} from 'react' +import React, {useEffect, useState} from 'react' import styled from '@emotion/styled' import {createFragmentContainer} from 'react-relay' import graphql from 'babel-plugin-relay/macro' @@ -23,6 +23,9 @@ import NewTeamFormTeamName from './NewTeamFormTeamName' import StyledError from '../../../../components/StyledError' import DashHeaderTitle from '../../../../components/DashHeaderTitle' import linkify from '../../../../utils/linkify' +import useMutationProps from '../../../../hooks/useMutationProps' +import useAtmosphere from '../../../../hooks/useAtmosphere' +import useRouter from '../../../../hooks/useRouter' const StyledForm = styled('form')({ margin: 0, @@ -69,12 +72,6 @@ interface Props extends WithMutationProps, WithAtmosphereProps, RouteComponentPr organizations: NewTeamForm_organizations } -interface State { - isNewOrg: boolean - orgId: string - fields: Fields -} - interface Field { dirty?: boolean error: string | undefined @@ -89,53 +86,33 @@ interface Fields { type FieldName = 'orgName' | 'teamName' const DEFAULT_FIELD = {value: '', error: undefined, dirty: false} -class NewTeamForm extends Component { - state = { - isNewOrg: this.props.isNewOrganization, - orgId: '', - fields: { - orgName: {...DEFAULT_FIELD}, - teamName: {...DEFAULT_FIELD} - } - } +const NewTeamForm = (props: Props) => { + const {isNewOrganization, organizations} = props + const [isNewOrg, setIsNewOrg] = useState(isNewOrganization) + const [orgId, setOrgId] = useState('') + const [fields, setFields] = useState({ + orgName: {...DEFAULT_FIELD}, + teamName: {...DEFAULT_FIELD} + }) + const {submitting, onError, error, onCompleted, submitMutation} = useMutationProps() + const atmosphere = useAtmosphere() + const {history} = useRouter() - setOrgId = (orgId: string) => { - this.setState( - { - orgId - }, - () => { - this.validate('teamName') - } - ) + const updateOrgId = (orgId: string) => { + setOrgId(orgId) } - validate = (name: FieldName) => { - const validators = { - teamName: this.validateTeamName, - orgName: this.validateOrgName - } - const res: Legitity = validators[name]() - - const {fields} = this.state - const field = fields[name] - if (res.error !== field.error) { - this.setState({ - fields: { - ...fields, - [name]: { - ...field, - error: res.error - } - } - }) - } - return res + const validateOrgName = () => { + const rawOrgName = fields.orgName.value + return new Legitity(rawOrgName) + .trim() + .required('Your new org needs a name!') + .min(2, 'C’mon, you call that an organization?') + .max(100, 'Maybe just the legal name?') + .test((val) => (linkify.match(val) ? 'Try using a name, not a link!' : undefined)) } - validateTeamName = () => { - const {organizations} = this.props - const {isNewOrg, orgId, fields} = this.state + const validateTeamName = () => { const rawTeamName = fields.teamName.value let teamNames: string[] = [] if (!isNewOrg) { @@ -147,92 +124,88 @@ class NewTeamForm extends Component { return teamNameValidation(rawTeamName, teamNames) } - validateOrgName = () => { - const {fields} = this.state - const rawOrgName = fields.orgName.value - return new Legitity(rawOrgName) - .trim() - .required('Your new org needs a name!') - .min(2, 'C’mon, you call that an organization?') - .max(100, 'Maybe just the legal name?') - .test((val) => (linkify.match(val) ? 'Try using a name, not a link!' : undefined)) + const validate = (name: FieldName) => { + const validators = { + teamName: validateTeamName, + orgName: validateOrgName + } + const res: Legitity = validators[name]() + const field = fields[name] + if (res.error !== field.error) { + setFields({ + ...fields, + [name]: { + ...field, + error: res.error + } + }) + } + return res } - handleBlur = (e: React.FocusEvent) => { - this.setDirty(e.target.name as FieldName) + useEffect(() => { + validate('orgName') + }, [fields.orgName]) + + useEffect(() => { + validate('teamName') + }, [fields.teamName]) + + const handleBlur = (e: React.FocusEvent) => { + setDirty(e.target.name as FieldName) } - setDirty = (name: FieldName) => { - const {fields} = this.state + const setDirty = (name: FieldName) => { const field = fields[name] if (!field.dirty) { - this.setState({ - fields: { - ...fields, - [name]: { - ...field, - dirty: true - } + setFields({ + ...fields, + [name]: { + ...field, + dirty: true } }) } } - handleInputChange = (e: React.ChangeEvent) => { - const {fields} = this.state + const handleInputChange = (e: React.ChangeEvent) => { const {value} = e.target const name = e.target.name as FieldName const field = fields[name] - this.setState( - { - fields: { - ...fields, - [name]: { - ...field, - value - } - } - }, - () => { - this.validate(name) + setFields({ + ...fields, + [name]: { + ...field, + value } - ) + }) } - handleIsNewOrgChange = (e: React.ChangeEvent) => { + const handleIsNewOrgChange = (e: React.ChangeEvent) => { const isNewOrg = e.target.value === 'true' - this.setState( - { - isNewOrg, - fields: { - ...this.state.fields, - orgName: { - ...this.state.fields.orgName, - dirty: false, - error: undefined - } - } - }, - () => { - this.validate('teamName') + setIsNewOrg(isNewOrg) + setFields({ + ...fields, + orgName: { + ...fields.orgName, + dirty: false, + error: undefined } - ) + }) } - onSubmit = (e: React.FormEvent) => { - const {atmosphere, history, onError, onCompleted, submitMutation, submitting} = this.props + const onSubmit = (e: React.FormEvent) => { e.preventDefault() if (submitting) return - const {isNewOrg, orgId} = this.state const fieldNames: FieldName[] = ['teamName'] - fieldNames.forEach(this.setDirty) - const fieldRes = fieldNames.map(this.validate) + fieldNames.forEach(setDirty) + const fieldRes = fieldNames.map(validate) const hasError = fieldRes.reduce((err: boolean, val) => err || !!val.error, false) if (hasError) return const [teamRes] = fieldRes if (isNewOrg) { - this.setDirty('orgName') - const {error, value: orgName} = this.validate('orgName') + setDirty('orgName') + const {error, value: orgName} = validate('orgName') if (error) return const newTeam = { name: teamRes.value @@ -250,64 +223,59 @@ class NewTeamForm extends Component { } } - render() { - const {fields, isNewOrg, orgId} = this.state - const {error, submitting, organizations} = this.props - - return ( - -
- {'Create a New Team'} -
- - - - - - - - - - - - - +
+ {'Create a New Team'} +
+ + + + + + + + + + + + + - - {isNewOrg ? 'Create Team & Org' : 'Create Team'} - - {error && {error}} - - -
- ) - } + + {isNewOrg ? 'Create Team & Org' : 'Create Team'} + + {error && {error.message}} + + + + ) } export default createFragmentContainer(withAtmosphere(withRouter(withMutationProps(NewTeamForm))), { diff --git a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormOrgName.tsx b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormOrgName.tsx index 8c3605c4227..d2be9110841 100644 --- a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormOrgName.tsx +++ b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormOrgName.tsx @@ -1,4 +1,4 @@ -import React, {Component} from 'react' +import React, {ChangeEvent, FocusEvent} from 'react' import BasicInput from '../../../../components/InputField/BasicInput' import Radio from '../../../../components/Radio/Radio' import {NewTeamFieldBlock} from './NewTeamForm' @@ -8,51 +8,40 @@ interface Props { dirty: boolean error: string | undefined isNewOrg: boolean - onTypeChange: (e: React.ChangeEvent) => void + onTypeChange: (e: ChangeEvent) => void - onChange(e: React.ChangeEvent): void + onChange(e: ChangeEvent): void orgName: string placeholder: string - onBlur(e: React.FocusEvent): void + onBlur(e: FocusEvent): void } -class NewTeamFormOrgName extends Component { - render() { - const { - dirty, - error, - isNewOrg, - onChange, - onTypeChange, - orgName, - placeholder, - onBlur - } = this.props - return ( - - { + const {dirty, error, isNewOrg, onChange, onTypeChange, orgName, placeholder, onBlur} = props + return ( + + + + - - - - - ) - } + + + ) } export default NewTeamFormOrgName diff --git a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormTeamName.tsx b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormTeamName.tsx index 51f9d89c86a..9c6de4ada4e 100644 --- a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormTeamName.tsx +++ b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormTeamName.tsx @@ -1,4 +1,4 @@ -import React, {Component} from 'react' +import React, {ChangeEvent, FocusEvent} from 'react' import styled from '@emotion/styled' import FieldLabel from '../../../../components/FieldLabel/FieldLabel' import BasicInput from '../../../../components/InputField/BasicInput' @@ -17,31 +17,29 @@ interface Props { dirty: boolean error: string | undefined - onChange(e: React.ChangeEvent): void + onChange(e: ChangeEvent): void teamName: string - onBlur(e: React.FocusEvent): void + onBlur(e: FocusEvent): void } -class NewTeamFormTeamName extends Component { - render() { - const {dirty, error, onChange, onBlur, teamName} = this.props - return ( - - - - - - - ) - } +const NewTeamFormTeamName = (props: Props) => { + const {dirty, error, onChange, onBlur, teamName} = props + return ( + + + + + + + ) } export default NewTeamFormTeamName diff --git a/packages/client/modules/teamDashboard/components/ProviderRow/SlackNotificationList.tsx b/packages/client/modules/teamDashboard/components/ProviderRow/SlackNotificationList.tsx index eed8bdadcdc..f1613743ca7 100644 --- a/packages/client/modules/teamDashboard/components/ProviderRow/SlackNotificationList.tsx +++ b/packages/client/modules/teamDashboard/components/ProviderRow/SlackNotificationList.tsx @@ -110,7 +110,7 @@ const SlackNotificationList = (props: Props) => { teamId={teamId} /> - {error && {error}} + {error && {error.message}} {TEAM_EVENTS.map((event) => { return ( { Private Notifications {'@Parabol'} - {error && {error}} + {error && {error.message}} {localPrivateChannelId && USER_EVENTS.map((event) => { return ( diff --git a/packages/client/modules/teamDashboard/components/ProviderRow/SlackNotificationRow.tsx b/packages/client/modules/teamDashboard/components/ProviderRow/SlackNotificationRow.tsx index 913b48046b7..473fa83dd49 100644 --- a/packages/client/modules/teamDashboard/components/ProviderRow/SlackNotificationRow.tsx +++ b/packages/client/modules/teamDashboard/components/ProviderRow/SlackNotificationRow.tsx @@ -45,9 +45,7 @@ const SlackNotificationRow = (props: Props) => { const notifications = slack?.notifications ?? [] const label = labelLookup[event] const atmosphere = useAtmosphere() - const existingNotification = notifications.find( - (notification) => notification.event === event - ) + const existingNotification = notifications.find((notification) => notification.event === event) const active = !!(existingNotification && existingNotification.channelId) const {error, submitMutation, onCompleted, onError, submitting} = useMutationProps() const onClick = () => { @@ -71,7 +69,7 @@ const SlackNotificationRow = (props: Props) => { - {error && {error}} + {error && {error.message}} ) } diff --git a/packages/server/graphql/mutations/helpers/addOrgValidation.ts b/packages/server/graphql/mutations/helpers/addOrgValidation.ts index 6da9712e886..2d8475b2313 100644 --- a/packages/server/graphql/mutations/helpers/addOrgValidation.ts +++ b/packages/server/graphql/mutations/helpers/addOrgValidation.ts @@ -6,8 +6,6 @@ export default function addOrgValidation() { newTeam: { name: teamName }, - orgName: { - orgName - } + orgName }) } From a9c02cfcb10e88e441eaf48be930343ad31b5f4a Mon Sep 17 00:00:00 2001 From: nickoferrall Date: Tue, 23 Feb 2021 17:26:34 -0500 Subject: [PATCH 5/9] make org name non nullable --- .../newTeam/components/NewTeamForm/NewTeamForm.tsx | 10 +++++----- packages/server/graphql/mutations/addOrg.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx index 6e6229b911d..9d5a35b1120 100644 --- a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx +++ b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx @@ -1,5 +1,5 @@ import {NewTeamForm_organizations} from '../../../../__generated__/NewTeamForm_organizations.graphql' -import React, {useEffect, useState} from 'react' +import React, {useEffect, useState, ChangeEvent, FormEvent, FocusEvent} from 'react' import styled from '@emotion/styled' import {createFragmentContainer} from 'react-relay' import graphql from 'babel-plugin-relay/macro' @@ -151,7 +151,7 @@ const NewTeamForm = (props: Props) => { validate('teamName') }, [fields.teamName]) - const handleBlur = (e: React.FocusEvent) => { + const handleBlur = (e: FocusEvent) => { setDirty(e.target.name as FieldName) } @@ -168,7 +168,7 @@ const NewTeamForm = (props: Props) => { } } - const handleInputChange = (e: React.ChangeEvent) => { + const handleInputChange = (e: ChangeEvent) => { const {value} = e.target const name = e.target.name as FieldName const field = fields[name] @@ -181,7 +181,7 @@ const NewTeamForm = (props: Props) => { }) } - const handleIsNewOrgChange = (e: React.ChangeEvent) => { + const handleIsNewOrgChange = (e: ChangeEvent) => { const isNewOrg = e.target.value === 'true' setIsNewOrg(isNewOrg) setFields({ @@ -194,7 +194,7 @@ const NewTeamForm = (props: Props) => { }) } - const onSubmit = (e: React.FormEvent) => { + const onSubmit = (e: FormEvent) => { e.preventDefault() if (submitting) return const fieldNames: FieldName[] = ['teamName'] diff --git a/packages/server/graphql/mutations/addOrg.ts b/packages/server/graphql/mutations/addOrg.ts index 69974c57961..80795602617 100644 --- a/packages/server/graphql/mutations/addOrg.ts +++ b/packages/server/graphql/mutations/addOrg.ts @@ -27,7 +27,7 @@ export default { description: 'The new team object with exactly 1 team member' }, orgName: { - type: GraphQLString, + type: new GraphQLNonNull(GraphQLString), description: 'The name of the new team' } }, From caeaac4d970165fede6d82e111f967ee44897665 Mon Sep 17 00:00:00 2001 From: nickoferrall Date: Mon, 1 Mar 2021 11:23:43 -0500 Subject: [PATCH 6/9] use isInitiallyNewOrg --- packages/client/modules/newTeam/NewTeam.tsx | 2 +- .../modules/newTeam/components/NewTeamForm/NewTeamForm.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/client/modules/newTeam/NewTeam.tsx b/packages/client/modules/newTeam/NewTeam.tsx index 888b6830e9e..bd588fc02bc 100644 --- a/packages/client/modules/newTeam/NewTeam.tsx +++ b/packages/client/modules/newTeam/NewTeam.tsx @@ -74,7 +74,7 @@ const NewTeam = (props: Props) => { return ( - + {isDesktop && ( diff --git a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx index 9d5a35b1120..17f0c4f7c97 100644 --- a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx +++ b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx @@ -68,7 +68,7 @@ const StyledButton = styled(PrimaryButton)({ const controlSize = 'medium' interface Props extends WithMutationProps, WithAtmosphereProps, RouteComponentProps<{}> { - isNewOrganization: boolean + isInitiallyNewOrg: boolean organizations: NewTeamForm_organizations } @@ -87,8 +87,8 @@ type FieldName = 'orgName' | 'teamName' const DEFAULT_FIELD = {value: '', error: undefined, dirty: false} const NewTeamForm = (props: Props) => { - const {isNewOrganization, organizations} = props - const [isNewOrg, setIsNewOrg] = useState(isNewOrganization) + const {isInitiallyNewOrg, organizations} = props + const [isNewOrg, setIsNewOrg] = useState(isInitiallyNewOrg) const [orgId, setOrgId] = useState('') const [fields, setFields] = useState({ orgName: {...DEFAULT_FIELD}, From 4b569149cfa94e7a3ead0fff55a938806d0f32d7 Mon Sep 17 00:00:00 2001 From: nickoferrall Date: Tue, 2 Mar 2021 17:07:11 -0500 Subject: [PATCH 7/9] refactor newTeamForm to use useForm --- .../components/NewTeamForm/NewTeamForm.tsx | 134 ++++-------------- .../NewTeamForm/NewTeamFormOrgName.tsx | 7 +- .../NewTeamForm/NewTeamFormTeamName.tsx | 4 +- 3 files changed, 32 insertions(+), 113 deletions(-) diff --git a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx index 17f0c4f7c97..bcd225f74ec 100644 --- a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx +++ b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx @@ -1,20 +1,15 @@ import {NewTeamForm_organizations} from '../../../../__generated__/NewTeamForm_organizations.graphql' -import React, {useEffect, useState, ChangeEvent, FormEvent, FocusEvent} from 'react' +import React, {useState, ChangeEvent, FormEvent} from 'react' import styled from '@emotion/styled' import {createFragmentContainer} from 'react-relay' import graphql from 'babel-plugin-relay/macro' -import {RouteComponentProps, withRouter} from 'react-router-dom' import FieldLabel from '../../../../components/FieldLabel/FieldLabel' import Panel from '../../../../components/Panel/Panel' import PrimaryButton from '../../../../components/PrimaryButton' import Radio from '../../../../components/Radio/Radio' -import withAtmosphere, { - WithAtmosphereProps -} from '../../../../decorators/withAtmosphere/withAtmosphere' import NewTeamOrgPicker from '../../../team/components/NewTeamOrgPicker' import AddOrgMutation from '../../../../mutations/AddOrgMutation' import AddTeamMutation from '../../../../mutations/AddTeamMutation' -import withMutationProps, {WithMutationProps} from '../../../../utils/relay/withMutationProps' import Legitity from '../../../../validation/Legitity' import teamNameValidation from '../../../../validation/teamNameValidation' import NewTeamFormBlock from './NewTeamFormBlock' @@ -26,6 +21,7 @@ import linkify from '../../../../utils/linkify' import useMutationProps from '../../../../hooks/useMutationProps' import useAtmosphere from '../../../../hooks/useAtmosphere' import useRouter from '../../../../hooks/useRouter' +import useForm from '../../../../hooks/useForm' const StyledForm = styled('form')({ margin: 0, @@ -67,40 +63,15 @@ const StyledButton = styled(PrimaryButton)({ const controlSize = 'medium' -interface Props extends WithMutationProps, WithAtmosphereProps, RouteComponentProps<{}> { +interface Props { isInitiallyNewOrg: boolean organizations: NewTeamForm_organizations } -interface Field { - dirty?: boolean - error: string | undefined - value: string -} - -interface Fields { - orgName: Field - teamName: Field -} - -type FieldName = 'orgName' | 'teamName' -const DEFAULT_FIELD = {value: '', error: undefined, dirty: false} - const NewTeamForm = (props: Props) => { const {isInitiallyNewOrg, organizations} = props const [isNewOrg, setIsNewOrg] = useState(isInitiallyNewOrg) const [orgId, setOrgId] = useState('') - const [fields, setFields] = useState({ - orgName: {...DEFAULT_FIELD}, - teamName: {...DEFAULT_FIELD} - }) - const {submitting, onError, error, onCompleted, submitMutation} = useMutationProps() - const atmosphere = useAtmosphere() - const {history} = useRouter() - - const updateOrgId = (orgId: string) => { - setOrgId(orgId) - } const validateOrgName = () => { const rawOrgName = fields.orgName.value @@ -123,99 +94,50 @@ const NewTeamForm = (props: Props) => { } return teamNameValidation(rawTeamName, teamNames) } - - const validate = (name: FieldName) => { - const validators = { - teamName: validateTeamName, - orgName: validateOrgName - } - const res: Legitity = validators[name]() - const field = fields[name] - if (res.error !== field.error) { - setFields({ - ...fields, - [name]: { - ...field, - error: res.error - } - }) + const {fields, onChange, setDirtyField, validateField} = useForm({ + orgName: { + getDefault: () => '', + validate: validateOrgName + }, + teamName: { + getDefault: () => '', + validate: validateTeamName } - return res - } - - useEffect(() => { - validate('orgName') - }, [fields.orgName]) - - useEffect(() => { - validate('teamName') - }, [fields.teamName]) + }) - const handleBlur = (e: FocusEvent) => { - setDirty(e.target.name as FieldName) - } + const {submitting, onError, error, onCompleted, submitMutation} = useMutationProps() + const atmosphere = useAtmosphere() + const {history} = useRouter() - const setDirty = (name: FieldName) => { - const field = fields[name] - if (!field.dirty) { - setFields({ - ...fields, - [name]: { - ...field, - dirty: true - } - }) - } + const updateOrgId = (orgId: string) => { + setOrgId(orgId) } - const handleInputChange = (e: ChangeEvent) => { - const {value} = e.target - const name = e.target.name as FieldName - const field = fields[name] - setFields({ - ...fields, - [name]: { - ...field, - value - } - }) - } + const handleBlur = () => setDirtyField() const handleIsNewOrgChange = (e: ChangeEvent) => { const isNewOrg = e.target.value === 'true' setIsNewOrg(isNewOrg) - setFields({ - ...fields, - orgName: { - ...fields.orgName, - dirty: false, - error: undefined - } - }) } const onSubmit = (e: FormEvent) => { e.preventDefault() if (submitting) return - const fieldNames: FieldName[] = ['teamName'] - fieldNames.forEach(setDirty) - const fieldRes = fieldNames.map(validate) - const hasError = fieldRes.reduce((err: boolean, val) => err || !!val.error, false) - if (hasError) return - const [teamRes] = fieldRes + const {error: teamErr, value: teamName} = validateField('teamName') + if (teamErr) return if (isNewOrg) { - setDirty('orgName') - const {error, value: orgName} = validate('orgName') - if (error) return + setDirtyField('orgName') + const {error: orgErr, value: orgName} = validateField('orgName') + if (orgErr) return const newTeam = { - name: teamRes.value + name: teamName } const variables = {newTeam, orgName} submitMutation() AddOrgMutation(atmosphere, variables, {history, onError, onCompleted}) } else { const newTeam = { - name: teamRes.value, + name: teamName, orgId } submitMutation() @@ -252,7 +174,7 @@ const NewTeamForm = (props: Props) => { { @@ -278,7 +200,7 @@ const NewTeamForm = (props: Props) => { ) } -export default createFragmentContainer(withAtmosphere(withRouter(withMutationProps(NewTeamForm))), { +export default createFragmentContainer(NewTeamForm, { organizations: graphql` fragment NewTeamForm_organizations on Organization @relay(plural: true) { ...NewTeamOrgPicker_organizations diff --git a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormOrgName.tsx b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormOrgName.tsx index d2be9110841..1a90e4ab172 100644 --- a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormOrgName.tsx +++ b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormOrgName.tsx @@ -1,4 +1,4 @@ -import React, {ChangeEvent, FocusEvent} from 'react' +import React, {ChangeEvent} from 'react' import BasicInput from '../../../../components/InputField/BasicInput' import Radio from '../../../../components/Radio/Radio' import {NewTeamFieldBlock} from './NewTeamForm' @@ -9,13 +9,10 @@ interface Props { error: string | undefined isNewOrg: boolean onTypeChange: (e: ChangeEvent) => void - onChange(e: ChangeEvent): void - orgName: string placeholder: string - - onBlur(e: FocusEvent): void + onBlur(): void } const NewTeamFormOrgName = (props: Props) => { diff --git a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormTeamName.tsx b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormTeamName.tsx index 9c6de4ada4e..bb489f29c64 100644 --- a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormTeamName.tsx +++ b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormTeamName.tsx @@ -1,4 +1,4 @@ -import React, {ChangeEvent, FocusEvent} from 'react' +import React, {ChangeEvent} from 'react' import styled from '@emotion/styled' import FieldLabel from '../../../../components/FieldLabel/FieldLabel' import BasicInput from '../../../../components/InputField/BasicInput' @@ -21,7 +21,7 @@ interface Props { teamName: string - onBlur(e: FocusEvent): void + onBlur(): void } const NewTeamFormTeamName = (props: Props) => { From 13b9e4e4482440ee47aca5070db8dcfd8b6b1fc4 Mon Sep 17 00:00:00 2001 From: nickoferrall Date: Tue, 16 Mar 2021 10:31:25 -0500 Subject: [PATCH 8/9] remove dirty check and show error when typing --- packages/client/hooks/useForm.tsx | 2 +- .../newTeam/components/NewTeamForm/NewTeamForm.tsx | 12 ++++-------- .../components/NewTeamForm/NewTeamFormOrgName.tsx | 5 ++--- .../components/NewTeamForm/NewTeamFormTeamName.tsx | 5 ++--- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/packages/client/hooks/useForm.tsx b/packages/client/hooks/useForm.tsx index 6474e3a326e..865fccae501 100644 --- a/packages/client/hooks/useForm.tsx +++ b/packages/client/hooks/useForm.tsx @@ -109,7 +109,7 @@ const useForm = (fieldInputDict: FieldInputDict, deps: any[] = []) => { const validate = useEventCallback((name: string, value: any) => { const validateField = fieldInputDict[name].validate - if (!validateField) return {error: undefined, value: state[name].value} + if (!validateField) return {error: undefined, value} const res: Legitity = validateField(value) dispatch({type: 'setError', name, error: res.error}) return res diff --git a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx index bcd225f74ec..602f6e92300 100644 --- a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx +++ b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx @@ -73,9 +73,8 @@ const NewTeamForm = (props: Props) => { const [isNewOrg, setIsNewOrg] = useState(isInitiallyNewOrg) const [orgId, setOrgId] = useState('') - const validateOrgName = () => { - const rawOrgName = fields.orgName.value - return new Legitity(rawOrgName) + const validateOrgName = (orgName: string) => { + return new Legitity(orgName) .trim() .required('Your new org needs a name!') .min(2, 'C’mon, you call that an organization?') @@ -83,8 +82,7 @@ const NewTeamForm = (props: Props) => { .test((val) => (linkify.match(val) ? 'Try using a name, not a link!' : undefined)) } - const validateTeamName = () => { - const rawTeamName = fields.teamName.value + const validateTeamName = (teamName: string) => { let teamNames: string[] = [] if (!isNewOrg) { const org = organizations.find((org) => org.id === orgId) @@ -92,7 +90,7 @@ const NewTeamForm = (props: Props) => { teamNames = org.teams.map((team) => team.name) } } - return teamNameValidation(rawTeamName, teamNames) + return teamNameValidation(teamName, teamNames) } const {fields, onChange, setDirtyField, validateField} = useForm({ orgName: { @@ -177,13 +175,11 @@ const NewTeamForm = (props: Props) => { onChange={onChange} onTypeChange={handleIsNewOrgChange} orgName={fields.orgName.value} - dirty={!!fields.orgName.dirty} error={fields.orgName.error} placeholder='My new organization' onBlur={handleBlur} /> ) => void @@ -16,7 +15,7 @@ interface Props { } const NewTeamFormOrgName = (props: Props) => { - const {dirty, error, isNewOrg, onChange, onTypeChange, orgName, placeholder, onBlur} = props + const {error, isNewOrg, onChange, onTypeChange, orgName, placeholder, onBlur} = props return ( { ): void @@ -25,13 +24,13 @@ interface Props { } const NewTeamFormTeamName = (props: Props) => { - const {dirty, error, onChange, onBlur, teamName} = props + const {error, onChange, onBlur, teamName} = props return ( Date: Tue, 16 Mar 2021 10:38:41 -0500 Subject: [PATCH 9/9] remove setDirtyField and onBlur --- .../newTeam/components/NewTeamForm/NewTeamForm.tsx | 8 ++------ .../components/NewTeamForm/NewTeamFormOrgName.tsx | 4 +--- .../components/NewTeamForm/NewTeamFormTeamName.tsx | 12 ++---------- 3 files changed, 5 insertions(+), 19 deletions(-) diff --git a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx index 602f6e92300..70171135b82 100644 --- a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx +++ b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamForm.tsx @@ -92,7 +92,8 @@ const NewTeamForm = (props: Props) => { } return teamNameValidation(teamName, teamNames) } - const {fields, onChange, setDirtyField, validateField} = useForm({ + + const {fields, onChange, validateField} = useForm({ orgName: { getDefault: () => '', validate: validateOrgName @@ -111,8 +112,6 @@ const NewTeamForm = (props: Props) => { setOrgId(orgId) } - const handleBlur = () => setDirtyField() - const handleIsNewOrgChange = (e: ChangeEvent) => { const isNewOrg = e.target.value === 'true' setIsNewOrg(isNewOrg) @@ -124,7 +123,6 @@ const NewTeamForm = (props: Props) => { const {error: teamErr, value: teamName} = validateField('teamName') if (teamErr) return if (isNewOrg) { - setDirtyField('orgName') const {error: orgErr, value: orgName} = validateField('orgName') if (orgErr) return const newTeam = { @@ -177,13 +175,11 @@ const NewTeamForm = (props: Props) => { orgName={fields.orgName.value} error={fields.orgName.error} placeholder='My new organization' - onBlur={handleBlur} /> diff --git a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormOrgName.tsx b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormOrgName.tsx index 1f10d8d4f97..46b57697790 100644 --- a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormOrgName.tsx +++ b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormOrgName.tsx @@ -11,11 +11,10 @@ interface Props { onChange(e: ChangeEvent): void orgName: string placeholder: string - onBlur(): void } const NewTeamFormOrgName = (props: Props) => { - const {error, isNewOrg, onChange, onTypeChange, orgName, placeholder, onBlur} = props + const {error, isNewOrg, onChange, onTypeChange, orgName, placeholder} = props return ( { error={error} name='orgName' placeholder={placeholder} - onBlur={onBlur} onChange={onChange} value={orgName} /> diff --git a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormTeamName.tsx b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormTeamName.tsx index 3e04ad3a42a..0be7ae9b19e 100644 --- a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormTeamName.tsx +++ b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormTeamName.tsx @@ -19,23 +19,15 @@ interface Props { onChange(e: ChangeEvent): void teamName: string - - onBlur(): void } const NewTeamFormTeamName = (props: Props) => { - const {error, onChange, onBlur, teamName} = props + const {error, onChange, teamName} = props return ( - + )