Skip to content

Commit

Permalink
fix(zimbra): required form field has error on init
Browse files Browse the repository at this point in the history
ref: MANAGER-16496

Signed-off-by: Tristan WAGNER <[email protected]>
  • Loading branch information
tristanwagner committed Dec 17, 2024
1 parent 0b0f33c commit ca6a26b
Show file tree
Hide file tree
Showing 11 changed files with 235 additions and 336 deletions.
82 changes: 82 additions & 0 deletions packages/manager/apps/zimbra/src/hooks/useForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useState } from 'react';

export type FieldType = {
value: string;
touched: boolean;
defaultValue?: string;
hasError?: boolean;
required?: boolean;
validate?: ((value: string) => boolean) | RegExp;
};

export interface FormTypeInterface {
[key: string]: FieldType;
}

export const validateField = (
name: string,
value: string,
form: FormTypeInterface,
) => {
const field = form[name];

if (!field) {
throw new Error(`validateField field is not defined for name "${name}"`);
}

if (!field.required && !value) {
return true;
}

if (typeof field.validate === 'function') {
return field.validate(value);
}

if (field.validate instanceof RegExp) {
return field.validate.test(String(value));
}

return !field.required || !!value;
};

export const validateForm = (form: FormTypeInterface) => {
const touched = Object.values(form).find((field) => field.touched);
const error = Object.values(form).find(
(field) => field.hasError || (field.required && field.value === ''),
);
return touched && !error;
};

type FormTypeOptions = {
onValueChange?: (
state: FormTypeInterface,
name: string,
value: string,
) => FormTypeInterface;
};

export const useForm = (
initialForm: FormTypeInterface = {},
options: FormTypeOptions = {},
) => {
const [isFormValid, setIsFormValid] = useState(false);
const [form, setForm] = useState<FormTypeInterface>(initialForm);

const setValue = (name: string, value: string, isBlur = false) => {
let newForm = form;
if (value !== form[name].value || isBlur) {
newForm[name] = {
...form[name],
value,
touched: true,
hasError: !validateField(name, value, form),
};
if (typeof options?.onValueChange === 'function') {
newForm = options.onValueChange(newForm, name, value);
}
setForm((oldForm) => ({ ...oldForm, ...newForm }));
setIsFormValid(validateForm(form));
}
};
return { isFormValid, form, setValue, setForm };
};
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,9 @@ import {
useDomains,
useGenerateUrl,
} from '@/hooks';
import {
ACCOUNT_REGEX,
checkValidityField,
checkValidityForm,
FormTypeInterface,
makeDateFromDDMMYYYY,
} from '@/utils';
import { ACCOUNT_REGEX, makeDateFromDDMMYYYY } from '@/utils';
import Loading from '@/components/Loading/Loading';
import { FormTypeInterface, useForm } from '@/hooks/useForm';

export enum AutoReplyTypes {
LINKED = 'linked',
Expand Down Expand Up @@ -82,7 +77,6 @@ export default function AddAutoReply() {
const params = Object.fromEntries(searchParams.entries());
const organizationId = searchParams.get('organizationId');
const editEmailAccountId = searchParams.get('editEmailAccountId');
const [isFormValid, setIsFormValid] = useState(false);
const [selectedOrganizationId, setSelectedOrganizationId] = useState(
organizationId,
);
Expand All @@ -91,53 +85,71 @@ export default function AddAutoReply() {

const goBack = () => navigate(goBackUrl);

const [form, setForm] = useState<FormTypeInterface>({
...{
account: {
value: '',
defaultValue: '',
touched: false,
required: true,
validate: ACCOUNT_REGEX,
},
domain: {
value: '',
touched: false,
required: true,
},
duration: {
value: AutoReplyDurations.TEMPORARY,
touched: false,
required: true,
},
from: {
value: '',
touched: false,
required: true,
},
until: {
value: '',
touched: false,
required: true,
},
sendCopy: {
value: '',
touched: false,
required: false,
},
sendCopyTo: {
value: '',
touched: false,
required: false,
const { form, isFormValid, setValue } = useForm(
{
...{
account: {
value: '',
defaultValue: '',
touched: false,
required: true,
validate: ACCOUNT_REGEX,
},
domain: {
value: '',
touched: false,
required: true,
},
duration: {
value: AutoReplyDurations.TEMPORARY,
touched: false,
required: true,
},
from: {
value: '',
touched: false,
required: true,
},
until: {
value: '',
touched: false,
required: true,
},
sendCopy: {
value: '',
touched: false,
required: false,
},
sendCopyTo: {
value: '',
touched: false,
required: false,
},
message: {
value: '',
defaultValue: '',
touched: false,
required: true,
},
},
message: {
value: '',
defaultValue: '',
touched: false,
required: true,
},
{
onValueChange: (currentForm, name) => {
const newForm = currentForm;
if (name === 'sendCopy') {
newForm.sendCopyTo.required = !!newForm.sendCopy.value;
newForm.sendCopyTo.hasError = false;
}
if (name === 'duration') {
newForm.from.required =
newForm.duration.value === AutoReplyDurations.TEMPORARY;
newForm.until.required =
newForm.duration.value === AutoReplyDurations.TEMPORARY;
}
return newForm;
},
},
});
);

const fromDate = useMemo(() => {
return form.from.value ? makeDateFromDDMMYYYY(form.from.value) : undefined;
Expand Down Expand Up @@ -202,33 +214,11 @@ export default function AddAutoReply() {
enabled: !!editEmailAccountId,
});

const handleFormChange = (name: string, value: string) => {
const newForm: FormTypeInterface = form;
newForm[name] = {
...form[name],
value,
touched: true,
hasError: !checkValidityField(name, value, form),
};
if (name === 'sendCopy') {
newForm.sendCopyTo.required = !!newForm.sendCopy.value;
newForm.sendCopyTo.hasError = false;
}
if (name === 'duration') {
newForm.from.required =
newForm.duration.value === AutoReplyDurations.TEMPORARY;
newForm.until.required =
newForm.duration.value === AutoReplyDurations.TEMPORARY;
}
setForm((oldForm) => ({ ...oldForm, ...newForm }));
setIsFormValid(checkValidityForm(form));
};

useEffect(() => {
if (account) {
const [head, tail] = (account.currentState?.email || '@').split('@');
handleFormChange('account', head);
handleFormChange('domain', tail);
setValue('account', head);
setValue('domain', tail);
setSelectedOrganizationId(account.currentState?.organizationId);
}
}, [account]);
Expand Down Expand Up @@ -317,16 +307,10 @@ export default function AddAutoReply() {
data-testid="input-account"
isDisabled={editEmailAccountId ? true : null}
onOdsBlur={(event) =>
handleFormChange(
event.target.name,
event.target.value.toString(),
)
setValue(event.target.name, event.target.value.toString(), true)
}
onOdsChange={(event) => {
handleFormChange(
event.detail.name,
event.detail.value.toString(),
);
setValue(event.detail.name, event.detail.value.toString());
}}
>
{domainAccounts && (
Expand Down Expand Up @@ -354,7 +338,7 @@ export default function AddAutoReply() {
hasError={form.from.hasError}
isDisabled={isLoading || editEmailAccountId ? true : null}
onOdsChange={(event) =>
handleFormChange('domain', event.detail.value as string)
setValue('domain', event.detail.value as string)
}
data-testid="select-domain"
placeholder={t(
Expand Down Expand Up @@ -382,9 +366,7 @@ export default function AddAutoReply() {
name={value}
value={value}
isChecked={form.duration.value === value}
onOdsChange={(event) =>
handleFormChange('duration', event.detail.value)
}
onOdsChange={(event) => setValue('duration', event.detail.value)}
data-testid={value}
className="cursor-pointer"
></OdsRadio>
Expand All @@ -409,7 +391,7 @@ export default function AddAutoReply() {
value={fromDate}
min={now}
onOdsChange={(event) => {
handleFormChange('from', event.detail.formattedValue);
setValue('from', event.detail.formattedValue);
}}
></OdsDatepicker>
</OdsFormField>
Expand All @@ -428,7 +410,7 @@ export default function AddAutoReply() {
value={untilDate}
min={fromDate || now}
onOdsChange={(event) =>
handleFormChange('until', event.detail.formattedValue)
setValue('until', event.detail.formattedValue)
}
></OdsDatepicker>
</OdsFormField>
Expand All @@ -443,7 +425,7 @@ export default function AddAutoReply() {
data-testid="sendCopy"
isChecked={form.sendCopy.value === 'checked'}
onClick={() =>
handleFormChange(
setValue(
'sendCopy',
form.sendCopy.value === 'checked' ? '' : 'checked',
)
Expand Down Expand Up @@ -471,9 +453,7 @@ export default function AddAutoReply() {
? true
: null
}
onOdsChange={(event) =>
handleFormChange('sendCopyTo', event.detail.value)
}
onOdsChange={(event) => setValue('sendCopyTo', event.detail.value)}
>
{orgAccounts?.map(({ currentState: acc }) => (
<option key={acc.email} value={acc.email}>
Expand All @@ -495,9 +475,7 @@ export default function AddAutoReply() {
defaultValue={form.message.defaultValue}
placeholder={t('zimbra_auto_replies_add_message_placeholder')}
hasError={form.message.hasError}
onOdsChange={(event) =>
handleFormChange('message', event.target.value)
}
onOdsChange={(event) => setValue('message', event.target.value)}
></OdsTextarea>
<OdsText preset={ODS_TEXT_PRESET.caption}>
{t('zimbra_auto_replies_add_message_helper')}
Expand Down
Loading

0 comments on commit ca6a26b

Please sign in to comment.