Webhook wip (#6371)

This PR introduces the following changes:
- Add the ability to filter webhooks by objectSingularName and Actions
- Refactor SettingsWebhookDetails edition to not use react-hook-form
(which will be deprecated on the whole project)
- Updating the tests with a complex set of mock (we just need to fix ~30
of them now :D)

<img width="1053" alt="image"
src="https://github.com/user-attachments/assets/4e56d972-f129-4789-8d1c-4b5797a8ffd7">
This commit is contained in:
Charles Bochet
2024-08-05 23:14:29 +02:00
committed by GitHub
parent 48f4e41148
commit 8373dfdc26
10 changed files with 14156 additions and 12287 deletions

View File

@ -1,3 +1,9 @@
import styled from '@emotion/styled';
import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { H2Title, IconSettings, IconTrash } from 'twenty-ui';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
@ -5,68 +11,82 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Webhook } from '@/settings/developers/types/webhook/Webhook';
import { Button } from '@/ui/input/button/components/Button';
import { Select } from '@/ui/input/components/Select';
import { TextArea } from '@/ui/input/components/TextArea';
import { TextInput } from '@/ui/input/components/TextInput';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { useState } from 'react';
import { Controller, FormProvider, useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router-dom';
import { H2Title, IconSettings, IconTrash } from 'twenty-ui';
type SettingsDevelopersWebhooksDetailForm = {
description?: string;
};
const StyledFilterRow = styled.div`
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
`;
export const SettingsDevelopersWebhooksDetail = () => {
const [isDeleteWebhookModalOpen, setIsDeleteWebhookModalOpen] =
useState(false);
const { objectMetadataItems } = useObjectMetadataItems();
const navigate = useNavigate();
const { webhookId = '' } = useParams();
const { enqueueSnackBar } = useSnackBar();
const [isDeleteWebhookModalOpen, setIsDeleteWebhookModalOpen] =
useState(false);
const [description, setDescription] = useState<string>('');
const [operationObjectSingularName, setOperationObjectSingularName] =
useState<string>('');
const [operationAction, setOperationAction] = useState('');
const [isDirty, setIsDirty] = useState<boolean>(false);
const { record: webhookData } = useFindOneRecord({
objectNameSingular: CoreObjectNameSingular.Webhook,
objectRecordId: webhookId,
onCompleted: (data) => {
setDescription(data?.description ?? '');
setOperationObjectSingularName(data?.operation.split('.')[0] ?? '');
setOperationAction(data?.operation.split('.')[1] ?? '');
setIsDirty(false);
},
});
const { deleteOneRecord: deleteOneWebhook } = useDeleteOneRecord({
objectNameSingular: CoreObjectNameSingular.Webhook,
});
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular: CoreObjectNameSingular.Webhook,
});
const deleteWebhook = () => {
deleteOneWebhook(webhookId);
navigate('/settings/developers');
};
const formConfig = useForm<SettingsDevelopersWebhooksDetailForm>();
const { isDirty, isValid, isSubmitting } = formConfig.formState;
const canSave = isDirty && isValid && !isSubmitting;
const fieldTypeOptions = [
{ value: '*', label: 'All Objects' },
...objectMetadataItems.map((item) => ({
value: item.nameSingular,
label: item.labelSingular,
})),
];
const handleSave = async (
formValues: SettingsDevelopersWebhooksDetailForm,
) => {
try {
await updateOneRecord({
idToUpdate: webhookId,
updateOneRecordInput: formValues,
});
navigate('/settings/developers');
} catch (error) {
enqueueSnackBar((error as Error).message, {
variant: SnackBarVariant.Error,
});
}
const { updateOneRecord } = useUpdateOneRecord<Webhook>({
objectNameSingular: CoreObjectNameSingular.Webhook,
});
const handleSave = async () => {
setIsDirty(false);
await updateOneRecord({
idToUpdate: webhookId,
updateOneRecordInput: {
operation: `${operationObjectSingularName}.${operationAction}`,
description: description,
},
});
navigate('/settings/developers');
};
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<FormProvider {...formConfig}>
<>
{webhookData?.targetUrl && (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer>
@ -78,9 +98,11 @@ export const SettingsDevelopersWebhooksDetail = () => {
]}
/>
<SaveAndCancelButtons
onCancel={() => navigate(`/settings/developers`)}
onSave={formConfig.handleSubmit(handleSave)}
isSaveDisabled={!canSave}
isSaveDisabled={!isDirty}
onCancel={() => {
navigate('/settings/developers');
}}
onSave={handleSave}
/>
</SettingsHeaderContainer>
<Section>
@ -100,20 +122,49 @@ export const SettingsDevelopersWebhooksDetail = () => {
title="Description"
description="An optional description"
/>
<Controller
name="description"
control={formConfig.control}
defaultValue={webhookData?.description ?? null}
render={({ field: { onChange, value } }) => (
<TextArea
placeholder="Write a description"
minRows={4}
value={value ?? undefined}
onChange={(nextValue) => onChange(nextValue ?? null)}
/>
)}
<TextArea
placeholder="Write a description"
minRows={4}
value={description}
onChange={(description) => {
setDescription(description);
setIsDirty(true);
}}
/>
</Section>
<Section>
<H2Title
title="Filters"
description="Select the event you wish to send to this endpoint"
/>
<StyledFilterRow>
<Select
fullWidth
dropdownId="object-webhook-type-select"
value={operationObjectSingularName}
onChange={(objectSingularName) => {
setIsDirty(true);
setOperationObjectSingularName(objectSingularName);
}}
options={fieldTypeOptions}
/>
<Select
fullWidth
dropdownId="operation-webhook-type-select"
value={operationAction}
onChange={(operationAction) => {
setIsDirty(true);
setOperationAction(operationAction);
}}
options={[
{ value: '*', label: 'All Actions' },
{ value: 'create', label: 'Create' },
{ value: 'update', label: 'Update' },
{ value: 'delete', label: 'Delete' },
]}
/>
</StyledFilterRow>
</Section>
<Section>
<H2Title
title="Danger zone"
@ -145,6 +196,6 @@ export const SettingsDevelopersWebhooksDetail = () => {
</SettingsPageContainer>
</SubMenuTopBarContainer>
)}
</FormProvider>
</>
);
};