Skip to content

Commit

Permalink
Regression: Fixed room edit custom field validation (#28078)
Browse files Browse the repository at this point in the history
  • Loading branch information
aleksandernsilva committed Mar 1, 2023
1 parent d4b3cb8 commit ee55b3e
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 111 deletions.
78 changes: 78 additions & 0 deletions apps/meteor/client/components/CustomFieldsFormV2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/* eslint-disable react/no-multi-comp */
import type { SelectOption } from '@rocket.chat/fuselage';
import { Field, Select, TextInput } from '@rocket.chat/fuselage';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React from 'react';
import type { Control, FieldValues } from 'react-hook-form';
import { Controller, get } from 'react-hook-form';

export type CustomFieldMetadata = {
name: string;
label: string;
type: 'select' | 'text';
required?: boolean;
defaultValue?: any;
options?: SelectOption[];
};

type CustomFieldFormProps<T extends FieldValues> = {
metadata: CustomFieldMetadata[];
formControl: Control<T>;
formName: string;
};

type CustomFieldProps<T extends FieldValues> = Omit<CustomFieldMetadata, 'name'> & {
control: Control<T>;
name: string;
};

const FIELD_TYPES = {
select: Select,
text: TextInput,
} as const;

export const CustomField = <T extends FieldValues>({
name,
type,
control,
label,
required,
defaultValue,
options = [],
...props
}: CustomFieldProps<T>) => {
const t = useTranslation();
const Component = FIELD_TYPES[type] ?? null;

return (
<Controller<T, any>
name={name}
control={control}
defaultValue={defaultValue ?? ''}
rules={{ required: t('The_field_is_required', label || name) }}
render={({ field, formState: { errors } }) => (
<Field>
<Field.Label>
{label || t(name as TranslationKey)}
{required && '*'}
</Field.Label>
<Field.Row>
<Component {...props} {...field} options={options} error={get(errors, name) as string} flexGrow={1} />
</Field.Row>
<Field.Error>{get(errors, name)?.message}</Field.Error>
</Field>
)}
/>
);
};

CustomField.displayName = 'CustomField';

export const CustomFieldsForm = <T extends FieldValues>({ formName, formControl, metadata }: CustomFieldFormProps<T>) => (
<>
{metadata.map(({ name: fieldName, ...props }) => (
<CustomField key={fieldName} name={`${formName}.${fieldName}`} control={formControl} {...props} />
))}
</>
);
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import type { ILivechatVisitor, IOmnichannelRoom, Serialized } from '@rocket.chat/core-typings';
import { Field, TextInput, ButtonGroup, Button } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useToastMessageDispatch, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import React, { useCallback } from 'react';
import { useController, useForm } from 'react-hook-form';

import { hasAtLeastOnePermission } from '../../../../../../../app/authorization/client';
import { useOmnichannelPriorities } from '../../../../../../../ee/client/omnichannel/hooks/useOmnichannelPriorities';
import CustomFieldsForm from '../../../../../../components/CustomFieldsForm';
import { CustomFieldsForm } from '../../../../../../components/CustomFieldsFormV2';
import Tags from '../../../../../../components/Omnichannel/Tags';
import VerticalBar from '../../../../../../components/VerticalBar';
import { useFormsSubscription } from '../../../../additionalForms';
Expand Down Expand Up @@ -64,71 +63,53 @@ function RoomEdit({ room, visitor, reload, reloadInfo, onClose }: RoomEditProps)

const {
register,
getValues: getFormValues,
control,
formState: { isDirty: isFormDirty, isValid: isFormValid, errors },
setError,
formState: { isDirty: isFormDirty, isValid: isFormValid },
handleSubmit,
trigger,
} = useForm({
mode: 'onBlur',
mode: 'onChange',
defaultValues: getInitialValuesRoom(room),
});

const { field: livechatDataField } = useController({ control, name: 'livechatData' });
const { field: tagsField } = useController({ control, name: 'tags' });
const { field: slaIdField } = useController({ control, name: 'slaId' });
const { field: priorityIdField } = useController({ control, name: 'priorityId' });

const handleSave = useMutableCallback(async (e) => {
e.preventDefault();

if (!isFormValid) {
return;
}

const { topic, tags, livechatData, slaId, priorityId } = getFormValues();

const guestData = {
_id: visitor._id,
};

const roomData = {
_id: room._id,
topic,
tags: tags.sort(),
livechatData,
priorityId,
...(slaId && { slaId }),
};

try {
await saveRoom({ guestData, roomData });
await queryClient.invalidateQueries(['/v1/rooms.info', room._id]);

dispatchToastMessage({ type: 'success', message: t('Saved') });
reload?.();
reloadInfo?.();
onClose();
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
});

const handleCustomFieldsError = useMutableCallback((validator) => {
const { livechatData } = errors;
const formattedErrors = livechatData ? Object.keys(livechatData).map((name) => ({ name })) : [];
const customFormErrors = validator(formattedErrors);

if (!customFormErrors.length) {
trigger('livechatData');
return;
}

customFormErrors.forEach(({ name }: { name: string }) => {
setError(`livechatData.${name}`, { type: 'custom' });
});
});
const handleSave = useCallback(
async (data) => {
if (!isFormValid) {
return;
}

const { topic, tags, livechatData, slaId, priorityId } = data;

const guestData = {
_id: visitor._id,
};

const roomData = {
_id: room._id,
topic,
tags: tags.sort(),
livechatData,
priorityId,
...(slaId && { slaId }),
};

try {
await saveRoom({ guestData, roomData });
await queryClient.invalidateQueries(['/v1/rooms.info', room._id]);

dispatchToastMessage({ type: 'success', message: t('Saved') });
reload?.();
reloadInfo?.();
onClose();
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
},
[dispatchToastMessage, isFormValid, onClose, queryClient, reload, reloadInfo, room._id, saveRoom, t, visitor._id],
);

if (isCustomFieldsLoading || isSlaPoliciesLoading || isPrioritiesLoading) {
return (
Expand All @@ -142,12 +123,7 @@ function RoomEdit({ room, visitor, reload, reloadInfo, onClose }: RoomEditProps)
<>
<VerticalBar.ScrollableContent is='form' onSubmit={handleSubmit(handleSave)}>
{canViewCustomFields && customFieldsMetadata && (
<CustomFieldsForm
jsonCustomFields={customFieldsMetadata}
customFieldsData={livechatDataField.value}
setCustomFieldsData={livechatDataField.onChange}
setCustomFieldsError={handleCustomFieldsError}
/>
<CustomFieldsForm formName='livechatData' formControl={control} metadata={customFieldsMetadata} />
)}

<Field>
Expand Down Expand Up @@ -176,7 +152,7 @@ function RoomEdit({ room, visitor, reload, reloadInfo, onClose }: RoomEditProps)
{t('Cancel')}
</Button>

<Button mie='none' flexGrow={1} onClick={handleSave} disabled={!isFormValid || !isFormDirty} primary>
<Button mie='none' flexGrow={1} onClick={handleSubmit(handleSave)} disabled={!isFormValid || !isFormDirty} primary>
{t('Save')}
</Button>
</ButtonGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { Field, TextInput, ButtonGroup, Button } from '@rocket.chat/fuselage';
import { useToastMessageDispatch, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React, { useState, useEffect } from 'react';
import { useController, useForm } from 'react-hook-form';
import { useForm } from 'react-hook-form';

import { hasAtLeastOnePermission } from '../../../../../../app/authorization/client';
import { validateEmail } from '../../../../../../lib/emailValidator';
import { withDebouncing } from '../../../../../../lib/utils/highOrderFunctions';
import CustomFieldsForm from '../../../../../components/CustomFieldsForm';
import { CustomFieldsForm } from '../../../../../components/CustomFieldsFormV2';
import VerticalBar from '../../../../../components/VerticalBar';
import { createToken } from '../../../../../lib/utils/createToken';
import { useFormsSubscription } from '../../../additionalForms';
Expand Down Expand Up @@ -72,7 +72,7 @@ export const ContactNewEdit = ({ id, data, close }: ContactNewEditProps): ReactE
const getContactBy = useEndpoint('GET', '/v1/omnichannel/contact.search');
const getUserData = useEndpoint('GET', '/v1/users.info');

const { data: customFieldsMetadata = {}, isInitialLoading: isLoadingCustomFields } = useCustomFieldsMetadata({
const { data: customFieldsMetadata = [], isInitialLoading: isLoadingCustomFields } = useCustomFieldsMetadata({
scope: 'visitor',
enabled: canViewCustomFields(),
});
Expand All @@ -85,7 +85,6 @@ export const ContactNewEdit = ({ id, data, close }: ContactNewEditProps): ReactE
formState: { errors, isValid: isFormValid, isDirty },
control,
setValue,
getValues,
handleSubmit,
trigger,
} = useForm<ContactFormData>({
Expand All @@ -94,15 +93,7 @@ export const ContactNewEdit = ({ id, data, close }: ContactNewEditProps): ReactE
defaultValues: initialValue,
});

const {
field: { onChange: handleLivechatData, value: customFields },
} = useController({
name: 'customFields',
control,
});

const [customFieldsErrors, setCustomFieldsErrors] = useState([]);
const isValid = isDirty && isFormValid && customFieldsErrors.length === 0;
const isValid = isDirty && isFormValid;

useEffect(() => {
if (!initialUsername) {
Expand Down Expand Up @@ -152,8 +143,8 @@ export const ContactNewEdit = ({ id, data, close }: ContactNewEditProps): ReactE

const validate = (fieldName: keyof ContactFormData): (() => void) => withDebouncing({ wait: 500 })(() => trigger(fieldName));

const handleSave = async (): Promise<void> => {
const { name, phone, email, customFields, username, token } = getValues();
const handleSave = async (data: ContactFormData): Promise<void> => {
const { name, phone, email, customFields, username, token } = data;

const payload = {
name,
Expand All @@ -180,7 +171,7 @@ export const ContactNewEdit = ({ id, data, close }: ContactNewEditProps): ReactE

return (
<>
<VerticalBar.ScrollableContent is='form'>
<VerticalBar.ScrollableContent is='form' onSubmit={handleSubmit(handleSave)}>
<Field>
<Field.Label>{t('Name')}*</Field.Label>
<Field.Row>
Expand Down Expand Up @@ -210,14 +201,7 @@ export const ContactNewEdit = ({ id, data, close }: ContactNewEditProps): ReactE
</Field.Row>
<Field.Error>{errors.phone?.message}</Field.Error>
</Field>
{canViewCustomFields() && customFields && (
<CustomFieldsForm
jsonCustomFields={customFieldsMetadata}
customFieldsData={customFields}
setCustomFieldsData={handleLivechatData}
setCustomFieldsError={setCustomFieldsErrors}
/>
)}
{canViewCustomFields() && <CustomFieldsForm formName='customFields' formControl={control} metadata={customFieldsMetadata} />}
{ContactManager && <ContactManager value={userId} handler={handleContactManagerChange} />}
</VerticalBar.ScrollableContent>
<VerticalBar.Footer>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,23 @@
import type { ILivechatCustomField, Serialized } from '@rocket.chat/core-typings';

type CustomFieldsMetadata = Record<
string,
{
label: string;
type: 'select' | 'text';
required?: boolean;
defaultValue?: unknown;
options?: string[];
}
>;
import type { CustomFieldMetadata } from '../../../../components/CustomFieldsFormV2';

export const formatCustomFieldsMetadata = (
customFields: Serialized<ILivechatCustomField>[],
scope: 'visitor' | 'room',
): CustomFieldsMetadata => {
): CustomFieldMetadata[] => {
if (!customFields) {
return {};
return [];
}

return customFields
.filter((field) => field.visibility === 'visible' && field.scope === scope)
.reduce((obj, { _id, label, options, defaultValue, required }) => {
obj[_id] = {
label,
type: options ? 'select' : 'text',
required,
defaultValue,
options: options?.split(',').map((item) => item.trim()),
};
return obj;
}, {} as CustomFieldsMetadata);
.map(({ _id, label, options, defaultValue, required }) => ({
name: _id,
label,
type: options ? 'select' : 'text',
required,
defaultValue,
options: options?.split(',').map((item) => [item.trim(), item.trim()]),
}));
};

0 comments on commit ee55b3e

Please sign in to comment.