Add description for Developers/webhook page (#6327)

- Add optional description field to webhook page in developer settings.

Fix https://github.com/twentyhq/twenty/issues/6236

---------

Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr>
This commit is contained in:
AbdulQader Qassab
2024-07-31 12:59:30 +04:00
committed by GitHub
parent ee4f1da956
commit 50f1cd352d
6 changed files with 83 additions and 6 deletions

View File

@ -1,6 +1,7 @@
export type Webhook = { export type Webhook = {
id: string; id: string;
targetUrl: string; targetUrl: string;
description?: string;
operation: string; operation: string;
__typename: 'Webhook'; __typename: 'Webhook';
}; };

View File

@ -27,6 +27,7 @@ const meta: Meta<PageDecoratorArgs> = {
id: '1', id: '1',
createdAt: '2021-08-27T12:00:00Z', createdAt: '2021-08-27T12:00:00Z',
targetUrl: 'https://example.com/webhook', targetUrl: 'https://example.com/webhook',
description: 'A Sample Description',
updatedAt: '2021-08-27T12:00:00Z', updatedAt: '2021-08-27T12:00:00Z',
operation: 'create', operation: 'create',
__typename: 'Webhook', __typename: 'Webhook',

View File

@ -1,24 +1,34 @@
import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { H2Title, IconSettings, IconTrash } from 'twenty-ui';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer'; import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; 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 { Button } from '@/ui/input/button/components/Button'; import { Button } from '@/ui/input/button/components/Button';
import { TextArea } from '@/ui/input/components/TextArea';
import { TextInput } from '@/ui/input/components/TextInput'; import { TextInput } from '@/ui/input/components/TextInput';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section'; import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; 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;
};
export const SettingsDevelopersWebhooksDetail = () => { export const SettingsDevelopersWebhooksDetail = () => {
const [isDeleteWebhookModalOpen, setIsDeleteWebhookModalOpen] = const [isDeleteWebhookModalOpen, setIsDeleteWebhookModalOpen] =
useState(false); useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const { webhookId = '' } = useParams(); const { webhookId = '' } = useParams();
const { enqueueSnackBar } = useSnackBar();
const { record: webhookData } = useFindOneRecord({ const { record: webhookData } = useFindOneRecord({
objectNameSingular: CoreObjectNameSingular.Webhook, objectNameSingular: CoreObjectNameSingular.Webhook,
objectRecordId: webhookId, objectRecordId: webhookId,
@ -26,12 +36,37 @@ export const SettingsDevelopersWebhooksDetail = () => {
const { deleteOneRecord: deleteOneWebhook } = useDeleteOneRecord({ const { deleteOneRecord: deleteOneWebhook } = useDeleteOneRecord({
objectNameSingular: CoreObjectNameSingular.Webhook, objectNameSingular: CoreObjectNameSingular.Webhook,
}); });
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular: CoreObjectNameSingular.Webhook,
});
const deleteWebhook = () => { const deleteWebhook = () => {
deleteOneWebhook(webhookId); deleteOneWebhook(webhookId);
navigate('/settings/developers'); navigate('/settings/developers');
}; };
const formConfig = useForm<SettingsDevelopersWebhooksDetailForm>();
const { isDirty, isValid, isSubmitting } = formConfig.formState;
const canSave = isDirty && isValid && !isSubmitting;
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,
});
}
};
return ( return (
<> // eslint-disable-next-line react/jsx-props-no-spreading
<FormProvider {...formConfig}>
{webhookData?.targetUrl && ( {webhookData?.targetUrl && (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings"> <SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer> <SettingsPageContainer>
@ -42,6 +77,11 @@ export const SettingsDevelopersWebhooksDetail = () => {
{ children: 'Webhook' }, { children: 'Webhook' },
]} ]}
/> />
<SaveAndCancelButtons
onCancel={() => navigate(`/settings/developers`)}
onSave={formConfig.handleSubmit(handleSave)}
isSaveDisabled={!canSave}
/>
</SettingsHeaderContainer> </SettingsHeaderContainer>
<Section> <Section>
<H2Title <H2Title
@ -55,6 +95,25 @@ export const SettingsDevelopersWebhooksDetail = () => {
fullWidth fullWidth
/> />
</Section> </Section>
<Section>
<H2Title
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)}
/>
)}
/>
</Section>
<Section> <Section>
<H2Title <H2Title
title="Danger zone" title="Danger zone"
@ -86,6 +145,6 @@ export const SettingsDevelopersWebhooksDetail = () => {
</SettingsPageContainer> </SettingsPageContainer>
</SubMenuTopBarContainer> </SubMenuTopBarContainer>
)} )}
</> </FormProvider>
); );
}; };

View File

@ -22,6 +22,10 @@ export const computeWebhooks = (
type: 'string', type: 'string',
example: 'https://example.com/incomingWebhook', example: 'https://example.com/incomingWebhook',
}, },
description: {
type: 'string',
example: 'A sample description',
},
eventType: { eventType: {
type: 'string', type: 'string',
enum: [ enum: [

View File

@ -325,6 +325,7 @@ export const VIEW_STANDARD_FIELD_IDS = {
export const WEBHOOK_STANDARD_FIELD_IDS = { export const WEBHOOK_STANDARD_FIELD_IDS = {
targetUrl: '20202020-1229-45a8-8cf4-85c9172aae12', targetUrl: '20202020-1229-45a8-8cf4-85c9172aae12',
operation: '20202020-15b7-458e-bf30-74770a54410c', operation: '20202020-15b7-458e-bf30-74770a54410c',
description: '20202020-15b7-458e-bf30-74770a54410d',
}; };
export const WORKFLOW_EVENT_LISTENER_STANDARD_FIELD_IDS = { export const WORKFLOW_EVENT_LISTENER_STANDARD_FIELD_IDS = {

View File

@ -3,6 +3,7 @@ import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator'; import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WEBHOOK_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { WEBHOOK_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
@ -36,4 +37,14 @@ export class WebhookWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconCheckbox', icon: 'IconCheckbox',
}) })
operation: string; operation: string;
@WorkspaceField({
standardId: WEBHOOK_STANDARD_FIELD_IDS.description,
type: FieldMetadataType.TEXT,
label: 'Description',
description: undefined,
icon: 'IconInfo',
})
@WorkspaceIsNullable()
description: string;
} }