Skip to content

Commit

Permalink
Webhook wip (twentyhq#6371)
Browse files Browse the repository at this point in the history
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">
  • Loading branch information
charlesBochet authored Aug 5, 2024
1 parent 48f4e41 commit 8373dfd
Show file tree
Hide file tree
Showing 10 changed files with 10,268 additions and 8,399 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,15 @@ describe('filterOutInvalidTimelineActivities', () => {
] as TimelineActivity[];

const mainObjectMetadataItem = {
nameSingular: 'objectNameSingular',
namePlural: 'objectNamePlural',
fields: [{ name: 'field1' }, { name: 'field2' }, { name: 'field3' }],
} as ObjectMetadataItem;

const filteredEvents = filterOutInvalidTimelineActivities(
events,
mainObjectMetadataItem,
'objectNameSingular',
[mainObjectMetadataItem],
);

expect(filteredEvents).toEqual([
Expand Down Expand Up @@ -85,7 +88,8 @@ describe('filterOutInvalidTimelineActivities', () => {

const filteredEvents = filterOutInvalidTimelineActivities(
events,
mainObjectMetadataItem,
'objectNameSingular',
[mainObjectMetadataItem],
);

expect(filteredEvents).toEqual([]);
Expand All @@ -109,7 +113,8 @@ describe('filterOutInvalidTimelineActivities', () => {

const filteredEvents = filterOutInvalidTimelineActivities(
events,
mainObjectMetadataItem,
'objectNameSingular',
[mainObjectMetadataItem],
);

expect(filteredEvents).toEqual(events);
Expand Down
18,478 changes: 10,148 additions & 8,330 deletions packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { ReactNode } from 'react';
import { renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil';

import {
phoneFieldDefinition,
ratingfieldDefinition,
ratingFieldDefinition,
} from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
Expand All @@ -28,7 +28,7 @@ const getWrapper =
</FieldContext.Provider>
);

const RatingWrapper = getWrapper(ratingfieldDefinition);
const RatingWrapper = getWrapper(ratingFieldDefinition);
const PhoneWrapper = getWrapper(phoneFieldDefinition);

describe('useIsFieldInputOnly', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Controller, useFormContext } from 'react-hook-form';
import omit from 'lodash.omit';
import { Controller, useFormContext } from 'react-hook-form';
import { z } from 'zod';

import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@ describe('getFieldPreviewValue', () => {

// Then
expect(result).toBe(2000);
expect(result).toBe(
getSettingsFieldTypeConfig(FieldMetadataType.Number)?.defaultValue,
);
expect(result).toBe(getSettingsFieldTypeConfig(FieldMetadataType.Number));
});

it('returns null if the field is supported in Settings but has no pre-configured placeholder defaultValue', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { Link } from 'react-router-dom';

const StyledUndecoratedLink = styled(Link)`
text-decoration: none;
width: 100%;
`;

type UndecoratedLinkProps = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,72 +1,92 @@
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';
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>
Expand All @@ -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>
Expand All @@ -100,19 +122,48 @@ 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
Expand Down Expand Up @@ -145,6 +196,6 @@ export const SettingsDevelopersWebhooksDetail = () => {
</SettingsPageContainer>
</SubMenuTopBarContainer>
)}
</FormProvider>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
InternalServerErrorException,
NotFoundException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';

Expand All @@ -16,6 +15,7 @@ import {
AppTokenType,
} from 'src/engine/core-modules/app-token/app-token.entity';
import { JwtAuthStrategy } from 'src/engine/core-modules/auth/strategies/jwt.auth.strategy';
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { EmailService } from 'src/engine/integrations/email/email.service';
Expand All @@ -34,10 +34,8 @@ describe('TokenService', () => {
providers: [
TokenService,
{
provide: JwtService,
useValue: {
sign: jest.fn().mockReturnValue('mock-jwt-token'),
},
provide: JwtWrapperService,
useValue: {},
},
{
provide: JwtAuthStrategy,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing';

import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';

Expand All @@ -22,7 +22,7 @@ describe('FileService', () => {
useValue: {},
},
{
provide: TokenService,
provide: JwtWrapperService,
useValue: {},
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export class WorkspaceMigrationRunnerService {

await queryRunner.commitTransaction();
} catch (error) {
this.logger.error('Error executing migration', error);
console.error('Error executing migration', error);
await queryRunner.rollbackTransaction();
throw error;
} finally {
Expand Down

0 comments on commit 8373dfd

Please sign in to comment.