diff --git a/packages/groups/src/components/Form.jsx b/packages/groups/src/components/Form.jsx index 2985716..73ef820 100644 --- a/packages/groups/src/components/Form.jsx +++ b/packages/groups/src/components/Form.jsx @@ -12,11 +12,45 @@ 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', + 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: '', + member: '', + members: '', + errorMessage: '', + formStatus: FORM_STATUSES.clean, + taskService: '' +} const actions = {} Object.keys(INITIAL_STATE) @@ -52,49 +86,146 @@ 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() - resetAlerts(dispatch) - const { group, groups, memberIdsText, name, taskService } = state - const groupId = group.id - const memberIds = textToArray(memberIdsText) - try { - if (group.data.name !== name) { - await groups.patch(groupId, { name }) + + const name = e.target.value + + validateName(name, state, dispatch) +} + +const onMemberIdsTextBlur = (state, dispatch, e) => { + e.preventDefault() + + 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 + } + + dispatch({ type: 'memberIdsTextStatus', payload: MEMBER_IDS_TEXT_STATUS.valid }) + 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
{text}
+} + +const ValidationMessage = ({ fieldName, fieldStatus }) => { + if (fieldName === 'name') { + switch (fieldStatus) { + case NAME_STATUSES.blank: + return + default: + return null + } + } + + if (fieldName === 'memberIdsText') { + switch (fieldStatus) { + case MEMBER_IDS_TEXT_STATUS.blank: + return + case MEMBER_IDS_TEXT_STATUS.invalidFormat: + return + 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) @@ -116,7 +247,7 @@ 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' }) @@ -141,9 +272,19 @@ 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 (
@@ -151,36 +292,44 @@ const Form = () => { Name + - {state && state.status && - } - {state && state.error && + {state && state.errorMessage && }