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/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/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 6e94245414e..70171135b82 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, {Component} 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' @@ -22,6 +17,11 @@ import NewTeamFormOrgName from './NewTeamFormOrgName' 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' +import useForm from '../../../../hooks/useForm' const StyledForm = styled('form')({ margin: 0, @@ -63,79 +63,26 @@ const StyledButton = styled(PrimaryButton)({ const controlSize = 'medium' -interface Props extends WithMutationProps, WithAtmosphereProps, RouteComponentProps<{}> { - isNewOrganization: boolean +interface Props { + isInitiallyNewOrg: boolean organizations: NewTeamForm_organizations } -interface State { - isNewOrg: boolean - orgId: string - fields: Fields -} - -interface Field { - dirty?: boolean - error: string | undefined - value: string -} - -interface Fields { - orgName: Field - teamName: Field -} +const NewTeamForm = (props: Props) => { + const {isInitiallyNewOrg, organizations} = props + const [isNewOrg, setIsNewOrg] = useState(isInitiallyNewOrg) + const [orgId, setOrgId] = useState('') -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} - } - } - - setOrgId = (orgId: string) => { - this.setState( - { - orgId - }, - () => { - this.validate('teamName') - } - ) - } - - 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 = (orgName: string) => { + return new Legitity(orgName) + .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 rawTeamName = fields.teamName.value + const validateTeamName = (teamName: string) => { let teamNames: string[] = [] if (!isNewOrg) { const org = organizations.find((org) => org.id === orgId) @@ -143,109 +90,50 @@ class NewTeamForm extends Component { teamNames = org.teams.map((team) => team.name) } } - 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?') + return teamNameValidation(teamName, teamNames) } - handleBlur = (e: React.FocusEvent) => { - this.setDirty(e.target.name as FieldName) - } - - setDirty = (name: FieldName) => { - const {fields} = this.state - const field = fields[name] - if (!field.dirty) { - this.setState({ - fields: { - ...fields, - [name]: { - ...field, - dirty: true - } - } - }) + const {fields, onChange, validateField} = useForm({ + orgName: { + getDefault: () => '', + validate: validateOrgName + }, + teamName: { + getDefault: () => '', + validate: validateTeamName } - } + }) - handleInputChange = (e: React.ChangeEvent) => { - const {fields} = this.state - 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) - } - ) + const {submitting, onError, error, onCompleted, submitMutation} = useMutationProps() + const atmosphere = useAtmosphere() + const {history} = useRouter() + + const updateOrgId = (orgId: string) => { + setOrgId(orgId) } - handleIsNewOrgChange = (e: React.ChangeEvent) => { + const handleIsNewOrgChange = (e: 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') - } - ) - // if (isNewOrg) { - // setTimeout(() => { - // this.newOrgInputRef.current && this.newOrgInputRef.current.focus() - // }) - // } + setIsNewOrg(isNewOrg) } - onSubmit = (e: React.FormEvent) => { - const {atmosphere, history, onError, onCompleted, submitMutation, submitting} = this.props + const onSubmit = (e: 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) - 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) { - this.setDirty('orgName') - const {error, value: orgName} = this.validate('orgName') - if (error) return + 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() @@ -253,67 +141,58 @@ 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))), { +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 8c3605c4227..46b57697790 100644 --- a/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormOrgName.tsx +++ b/packages/client/modules/newTeam/components/NewTeamForm/NewTeamFormOrgName.tsx @@ -1,58 +1,41 @@ -import React, {Component} from 'react' +import React, {ChangeEvent} from 'react' import BasicInput from '../../../../components/InputField/BasicInput' import Radio from '../../../../components/Radio/Radio' import {NewTeamFieldBlock} from './NewTeamForm' import NewTeamFormBlock from './NewTeamFormBlock' interface Props { - dirty: boolean error: string | undefined isNewOrg: boolean - onTypeChange: (e: React.ChangeEvent) => void - - onChange(e: React.ChangeEvent): void - + onTypeChange: (e: ChangeEvent) => void + onChange(e: ChangeEvent): void orgName: string placeholder: string - - onBlur(e: React.FocusEvent): void } -class NewTeamFormOrgName extends Component { - render() { - const { - dirty, - error, - isNewOrg, - onChange, - onTypeChange, - orgName, - placeholder, - onBlur - } = this.props - return ( - - { + const {error, isNewOrg, onChange, onTypeChange, orgName, placeholder} = 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..0be7ae9b19e 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} from 'react' import styled from '@emotion/styled' import FieldLabel from '../../../../components/FieldLabel/FieldLabel' import BasicInput from '../../../../components/InputField/BasicInput' @@ -14,34 +14,23 @@ const FormBlockInline = styled(NewTeamFormBlock)({ }) interface Props { - dirty: boolean error: string | undefined - onChange(e: React.ChangeEvent): void + onChange(e: ChangeEvent): void teamName: string - - onBlur(e: React.FocusEvent): void } -class NewTeamFormTeamName extends Component { - render() { - const {dirty, error, onChange, onBlur, teamName} = this.props - return ( - - - - - - - ) - } +const NewTeamFormTeamName = (props: Props) => { + const {error, onChange, 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 18d99672bee..fdb40e6628e 100644 --- a/packages/client/modules/teamDashboard/components/ProviderRow/SlackNotificationList.tsx +++ b/packages/client/modules/teamDashboard/components/ProviderRow/SlackNotificationList.tsx @@ -109,7 +109,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 d89391a8333..3c96bf84ce6 100644 --- a/packages/client/modules/teamDashboard/components/ProviderRow/SlackNotificationRow.tsx +++ b/packages/client/modules/teamDashboard/components/ProviderRow/SlackNotificationRow.tsx @@ -71,7 +71,7 @@ const SlackNotificationRow = (props: Props) => { - {error && {error}} + {error && {error.message}} ) } 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/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 diff --git a/packages/client/validation/templates.js b/packages/client/validation/templates.js index 53e566b53ae..d47aac098c1 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) => @@ -43,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 @@ -63,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 @@ -71,20 +74,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/addOrg.ts b/packages/server/graphql/mutations/addOrg.ts index 45de64e16fa..3f65c8f0cf8 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' } }, diff --git a/packages/server/graphql/mutations/addTeam.ts b/packages/server/graphql/mutations/addTeam.ts index e134d59dc96..d9dcfe6b95e 100644 --- a/packages/server/graphql/mutations/addTeam.ts +++ b/packages/server/graphql/mutations/addTeam.ts @@ -46,7 +46,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 72% rename from packages/server/graphql/mutations/helpers/addOrgValidation.js rename to packages/server/graphql/mutations/helpers/addOrgValidation.ts index 200e2636956..2d8475b2313 100644 --- a/packages/server/graphql/mutations/helpers/addOrgValidation.js +++ b/packages/server/graphql/mutations/helpers/addOrgValidation.ts @@ -1,5 +1,5 @@ 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({ diff --git a/packages/server/graphql/mutations/helpers/addTeamValidation.js b/packages/server/graphql/mutations/helpers/addTeamValidation.ts similarity index 54% rename from packages/server/graphql/mutations/helpers/addTeamValidation.js rename to packages/server/graphql/mutations/helpers/addTeamValidation.ts index 17389a71598..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' +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),