Skip to content

Commit

Permalink
Add validations to groups
Browse files Browse the repository at this point in the history
  • Loading branch information
Anthony M committed Jun 10, 2022
1 parent 71056d4 commit 6721aa5
Showing 1 changed file with 219 additions and 49 deletions.
268 changes: 219 additions & 49 deletions packages/groups/src/components/Form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,47 @@ import { useServices } from '@kernel/common'

import AppConfig from 'App.config'

const MODES = { create: 'create', update: 'update' }
const KEYS = ['name', 'memberIdsText']
const STATE_KEYS = ['group', 'groups', 'member', 'members', 'error', 'status', 'taskService']
const INITIAL_STATE = STATE_KEYS.concat(KEYS)
.reduce((acc, k) => Object.assign(acc, { [k]: '' }), {})
const MODES = {
create: 'create',
update: 'update'
}

const NAME_STATUSES = {
blank: 'blank',
clean: 'clean',
valid: 'valid'
}

const MEMBER_IDS_TEXT_STATUS = {
blank: 'blank',
clean: 'clean',
invalidFormat: 'invalidFormat',
invalidIds: 'invalidIds',
valid: 'valid'
}

const FORM_STATUSES = {
clean: 'clean',
submitting: 'submitting',
success: 'success',
error: 'error',
valid: 'valid'
}

const INITIAL_STATE = {
name: '',
memberIdsText: '',
nameStatus: NAME_STATUSES.clean,
memberIdsTextStatus: MEMBER_IDS_TEXT_STATUS.clean,
group: '',
groups: '',
invalidIds: [],
member: '',
members: '',
errorMessage: '',
formStatus: FORM_STATUSES.clean,
taskService: ''
}

const actions = {}
Object.keys(INITIAL_STATE)
Expand Down Expand Up @@ -52,49 +88,165 @@ const value = (state, type) => {
// dedupe, sort
const textToArray = (s) => [...new Set(s.split(',').map((e) => e.trim()))].sort()
const arrayToText = (arr) => arr.join(', ')
const resetAlerts = (dispatch) => {
dispatch({ type: 'error', payload: '' })
dispatch({ type: 'status', payload: 'submitting' })
const setFormSubmitting = (dispatch) => {
dispatch({ type: 'errorMessage', payload: '' })
dispatch({ type: 'formStatus', payload: FORM_STATUSES.submitting })
}

const create = async (state, dispatch, e) => {
e.preventDefault()
resetAlerts(dispatch)
const { groups, memberIdsText, name, taskService } = state
const memberIds = textToArray(memberIdsText)
if (!name.length || !memberIdsText.length) {
dispatch({ type: 'error', payload: 'name and member ids are required' })
return
setFormSubmitting(dispatch)

const formIsValid = validateFields(state, dispatch)

if (formIsValid) {
const {groups, memberIdsText, name, taskService} = state
const memberIds = textToArray(memberIdsText)

try {
const group = await groups.create({ name })
const groupId = group.id
await groups.updateMeta(groupId, { owner: groupId })
await taskService.syncGroupMembers({ groupId, memberIds })
dispatch({type: 'formStatus', payload: FORM_STATUSES.success})
} catch (error) {
dispatch({ type: 'formStatus', payload: FORM_STATUSES.error })
dispatch({ type: 'errorMessage', payload: error.message })
}
}
try {
const group = await groups.create({ name })
}

const update = async (state, dispatch, e) => {
e.preventDefault()
setFormSubmitting(dispatch)

const formIsValid = validateFields(state, dispatch)

if (formIsValid) {
const { group, groups, memberIdsText, name, taskService } = state
const groupId = group.id
await groups.updateMeta(groupId, { owner: groupId })
await taskService.syncGroupMembers({ groupId, memberIds })
dispatch({ type: 'status', payload: 'Group creation submitted' })
} catch (error) {
dispatch({ type: 'error', payload: error.message })
const memberIds = textToArray(memberIdsText)

try {
if (group.data.name !== name) {
await groups.patch(groupId, { name })
}
await taskService.syncGroupMembers({ groupId, memberIds })
dispatch({ type: 'formStatus', payload: FORM_STATUSES.success })
} catch (error) {
dispatch({ type: 'formStatus', payload: FORM_STATUSES.error })
dispatch({ type: 'errorMessage', payload: error.message })
}
}
}

const update = async (state, dispatch, e) => {
const onNameBlur = (state, dispatch, e) => {
e.preventDefault()

const name = e.target.value

validateName(name, state, dispatch)
}

const onMemberIdsTextBlur = (state, dispatch, e) => {
e.preventDefault()
resetAlerts(dispatch)
const { group, groups, memberIdsText, name, taskService } = state
const groupId = group.id

const memberIdsText = e.target.value

validateMemberIdsText(memberIdsText, state, dispatch)
}

const validateName = (name, _state, dispatch) => {
if (name.length === 0) {
dispatch({type: 'nameStatus', payload: NAME_STATUSES.blank})
return false
}

dispatch({type: 'nameStatus', payload: NAME_STATUSES.valid})
return true
}

const validateMemberIdsText = async (memberIdsText, state, dispatch) => {
if (memberIdsText.length === 0) {
dispatch({type: 'memberIdsTextStatus', payload: MEMBER_IDS_TEXT_STATUS.blank})
return false
}

if (memberIdsText.match(/[^\ \d\,]/)) {
dispatch({type: 'memberIdsTextStatus', payload: MEMBER_IDS_TEXT_STATUS.invalidFormat})
return false
}

const memberIds = textToArray(memberIdsText)
try {
if (group.data.name !== name) {
await groups.patch(groupId, { name })
let invalidIds = []
memberIds.forEach(async (memberId) => {
const memberExists = await state.members.exists(memberId)
if (!memberExists) {
invalidIds.push(memberId)
}
})

if (invalidIds.length > 0) {
dispatch({type: 'memberIdsTextStatus', payload: MEMBER_IDS_TEXT_STATUS.invalidIds})
dispatch({type: 'invalidIds', payload: invalidIds})
return false
}

dispatch({type: 'memberIdsTextStatus', payload: MEMBER_IDS_TEXT_STATUS.valid})
dispatch({type: 'invalidIds', payload: []})
return true
}

const validateFields = async (state, dispatch) => {
const { name, memberIdsText } = state

const nameIsValid = validateName(name, state, dispatch)
const memberIdsTextIsValid = await validateMemberIdsText(memberIdsText, state, dispatch)
const formIsValid = nameIsValid && memberIdsTextIsValid

if (!formIsValid) {
dispatch({type: 'formStatus', payload: FORM_STATUSES.error})
dispatch({type: 'errorMessage', payload: 'Check for errors above.'})
} else {
dispatch({type: 'formStatus', payload: FORM_STATUSES.valid})
dispatch({ type: 'errorMessage', payload: '' })
}

return formIsValid
}

const ErrorMessage = ({text}) => {
return <div className='mt-2 text-sm text-red-500'>{text}</div>
}

const ValidationMessage = ({fieldName, fieldStatus}) => {
if (fieldName === 'name') {
switch (fieldStatus) {
case NAME_STATUSES.blank:
return <ErrorMessage text="This field is required." />
default:
return null
}
}

if (fieldName === 'memberIdsText') {
switch (fieldStatus) {
case MEMBER_IDS_TEXT_STATUS.blank:
return <ErrorMessage text="This field is required." />
case MEMBER_IDS_TEXT_STATUS.invalidFormat:
return <ErrorMessage text="Group IDs must be numbers separated by commas" />
case MEMBER_IDS_TEXT_STATUS.invalidIds:
const idsText = global.state.invalidIds.join(", ")
return <ErrorMessage text={`${idsText} are not valid member IDs`} />
default:
return null
}
await taskService.syncGroupMembers({ groupId, memberIds })
dispatch({ type: 'status', payload: 'Group update submitted' })
} catch (error) {
dispatch({ type: 'error', payload: error.message })
}
}

const formClass = 'mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50'
const submitButtonBaseClass = 'mt-6 mb-4 px-6 py-4 text-kernel-white bg-kernel-green-dark w-full rounded font-bold capitalize'
const submitButtonDisabledClass = `${submitButtonBaseClass} bg-kernel-green-light cursor-not-allowed`

const Form = () => {
const [state, dispatch] = useReducer(reducer, INITIAL_STATE)
Expand All @@ -116,12 +268,12 @@ const Form = () => {

useEffect(() => {
(async () => {
dispatch({ type: 'status', payload: 'Loading' })
dispatch({ type: 'formStatus', payload: FORM_STATUSES.submitting })
const { entityFactory, taskService } = await services()
dispatch({ type: 'taskService', payload: taskService })
const members = await entityFactory({ resource: 'member' })
const member = await members.get(user.iss)
const groups = await entityFactory({ resource: 'group' })
const groups = await entityFactory({resource: 'group'})
dispatch({ type: 'members', payload: members })
dispatch({ type: 'member', payload: member })
dispatch({ type: 'groups', payload: groups })
Expand All @@ -141,46 +293,64 @@ const Form = () => {
dispatch({ type, payload })
})
}
dispatch({ type: 'status', payload: '' })
dispatch({ type: 'formStatus', payload: FORM_STATUSES.clean })
})()
}, [services, user.iss, mode, group])
}, [services, user, mode, group])

const handleOnClickSubmit = () => {
if (mode === MODES.create) {
create.bind(null, state, dispatch)
} else {
update.bind(null, state, dispatch)
}
}

const isSubmitDisabled = state.formStatus === FORM_STATUSES.submitting

return (
<form className='grid grid-cols-1 gap-6'>
<label className='block'>
<span className='text-gray-700'>Name</span>
<input
type='text' className={formClass}
value={value(state, 'name')} onChange={change.bind(null, dispatch, 'name')}
value={value(state, 'name')}
onChange={change.bind(null, dispatch, 'name')}
onBlur={onNameBlur.bind(null, state, dispatch)}
/>
<ValidationMessage
fieldName='name'
fieldStatus={value(state, 'nameStatus')}
value={value(state, 'name')}
/>
</label>
<label className='block'>
<span className='text-gray-700'>Member Ids (comma separated)</span>
<input
type='text' multiple className={formClass}
value={value(state, 'memberIdsText')} onChange={change.bind(null, dispatch, 'memberIdsText')}
value={value(state, 'memberIdsText')}
onChange={change.bind(null, dispatch, 'memberIdsText')}
onBlur={onMemberIdsTextBlur.bind(null, state, dispatch)}
/>
<ValidationMessage
fieldName='memberIdsText'
fieldStatus={value(state, 'memberIdsTextStatus')}
value={value(state, 'memberIdsText')}
/>
</label>
<label className='block'>
<button
onClick={mode === MODES.create ? create.bind(null, state, dispatch) : update.bind(null, state, dispatch)}
className='mt-6 mb-4 px-6 py-4 text-kernel-white bg-kernel-green-dark w-full rounded font-bold capitalize'
disabled={isSubmitDisabled}
onClick={handleOnClickSubmit}
className={isSubmitDisabled ? submitButtonDisabledClass : submitButtonBaseClass}
>
{mode}
</button>
</label>
{state && state.status &&
<label className='block'>
<span className='text-gray-700'>Status</span>
<div className={formClass}>
{state.status}
</div>
</label>}
{state && state.error &&
{state && state.errorMessage &&
<label className='block'>
<span className='text-gray-700'>Error</span>
<div className={formClass}>
{state.error}
{state.errorMessage}
</div>
</label>}
</form>
Expand Down

0 comments on commit 6721aa5

Please sign in to comment.