Multiple operations on webhooks (#7807)

fixes #7792 

WIP :)



https://github.com/user-attachments/assets/91f16744-c002-4f24-9cdd-cff79743cab1

---------

Co-authored-by: martmull <martmull@hotmail.fr>
This commit is contained in:
nitin
2024-10-23 21:27:46 +05:30
committed by GitHub
parent 165dd87264
commit 18778c55ac
16 changed files with 257 additions and 68 deletions

View File

@ -3,5 +3,6 @@ export type Webhook = {
targetUrl: string; targetUrl: string;
description?: string; description?: string;
operation: string; operation: string;
operations: string[];
__typename: 'Webhook'; __typename: 'Webhook';
}; };

View File

@ -11,6 +11,7 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
import { SelectHotkeyScope } from '../types/SelectHotkeyScope'; import { SelectHotkeyScope } from '../types/SelectHotkeyScope';
export type SelectOption<Value extends string | number | null> = { export type SelectOption<Value extends string | number | null> = {
@ -73,6 +74,7 @@ const StyledLabel = styled.span`
const StyledControlLabel = styled.div` const StyledControlLabel = styled.div`
align-items: center; align-items: center;
display: flex; display: flex;
overflow: hidden;
gap: ${({ theme }) => theme.spacing(1)}; gap: ${({ theme }) => theme.spacing(1)};
`; `;
@ -136,7 +138,7 @@ export const Select = <Value extends string | number | null>({
stroke={theme.icon.stroke.sm} stroke={theme.icon.stroke.sm}
/> />
)} )}
{selectedOption?.label} <EllipsisDisplay> {selectedOption?.label} </EllipsisDisplay>
</StyledControlLabel> </StyledControlLabel>
<StyledIconChevronDown disabled={isDisabled} size={theme.icon.size.md} /> <StyledIconChevronDown disabled={isDisabled} size={theme.icon.size.md} />
</StyledControlContainer> </StyledControlContainer>

View File

@ -1,7 +1,16 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useState } from 'react'; import { useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { H2Title, IconTrash } from 'twenty-ui'; import {
H2Title,
IconBox,
IconNorthStar,
IconPlus,
IconRefresh,
IconTrash,
isDefined,
useIcons,
} from 'twenty-ui';
import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState'; import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
@ -17,7 +26,8 @@ import { SettingsDevelopersWebhookUsageGraphEffect } from '@/settings/developers
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPath } from '@/types/SettingsPath';
import { Button } from '@/ui/input/button/components/Button'; import { Button } from '@/ui/input/button/components/Button';
import { Select } from '@/ui/input/components/Select'; import { IconButton } from '@/ui/input/button/components/IconButton';
import { Select, SelectOption } from '@/ui/input/components/Select';
import { TextArea } from '@/ui/input/components/TextArea'; 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';
@ -25,11 +35,23 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBa
import { Section } from '@/ui/layout/section/components/Section'; import { Section } from '@/ui/layout/section/components/Section';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { WEBHOOK_EMPTY_OPERATION } from '~/pages/settings/developers/webhooks/constants/WebhookEmptyOperation';
import { WebhookOperationType } from '~/pages/settings/developers/webhooks/types/WebhookOperationsType';
const OBJECT_DROPDOWN_WIDTH = 340;
const ACTION_DROPDOWN_WIDTH = 140;
const StyledFilterRow = styled.div` const StyledFilterRow = styled.div`
display: flex; display: grid;
flex-direction: row; grid-template-columns: ${OBJECT_DROPDOWN_WIDTH}px ${ACTION_DROPDOWN_WIDTH}px auto;
gap: ${({ theme }) => theme.spacing(2)}; gap: ${({ theme }) => theme.spacing(2)};
margin-bottom: ${({ theme }) => theme.spacing(2)};
align-items: center;
`;
const StyledPlaceholder = styled.div`
height: ${({ theme }) => theme.spacing(8)};
width: ${({ theme }) => theme.spacing(8)};
`; `;
export const SettingsDevelopersWebhooksDetail = () => { export const SettingsDevelopersWebhooksDetail = () => {
@ -41,20 +63,33 @@ export const SettingsDevelopersWebhooksDetail = () => {
const [isDeleteWebhookModalOpen, setIsDeleteWebhookModalOpen] = const [isDeleteWebhookModalOpen, setIsDeleteWebhookModalOpen] =
useState(false); useState(false);
const [description, setDescription] = useState<string>(''); const [description, setDescription] = useState<string>('');
const [operationObjectSingularName, setOperationObjectSingularName] = const [operations, setOperations] = useState<WebhookOperationType[]>([
useState<string>(''); WEBHOOK_EMPTY_OPERATION,
const [operationAction, setOperationAction] = useState(''); ]);
const [isDirty, setIsDirty] = useState<boolean>(false); const [isDirty, setIsDirty] = useState<boolean>(false);
const { getIcon } = useIcons();
const { record: webhookData } = useFindOneRecord({ const { record: webhookData } = useFindOneRecord({
objectNameSingular: CoreObjectNameSingular.Webhook, objectNameSingular: CoreObjectNameSingular.Webhook,
objectRecordId: webhookId, objectRecordId: webhookId,
onCompleted: (data) => { onCompleted: (data) => {
setDescription(data?.description ?? ''); setDescription(data?.description ?? '');
setOperationObjectSingularName(data?.operation.split('.')[0] ?? ''); const baseOperations = data?.operations
setOperationAction(data?.operation.split('.')[1] ?? ''); ? data.operations.map((op: string) => {
const [object, action] = op.split('.');
return { object, action };
})
: data?.operation
? [
{
object: data.operation.split('.')[0],
action: data.operation.split('.')[1],
},
]
: [];
setOperations(addEmptyOperationIfNecessary(baseOperations));
setIsDirty(false); setIsDirty(false);
}, },
}); });
@ -72,30 +107,87 @@ export const SettingsDevelopersWebhooksDetail = () => {
const isAnalyticsV2Enabled = useIsFeatureEnabled('IS_ANALYTICS_V2_ENABLED'); const isAnalyticsV2Enabled = useIsFeatureEnabled('IS_ANALYTICS_V2_ENABLED');
const fieldTypeOptions = [ const fieldTypeOptions: SelectOption<string>[] = useMemo(
{ value: '*', label: 'All Objects' }, () => [
...objectMetadataItems.map((item) => ({ { value: '*', label: 'All Objects', Icon: IconNorthStar },
value: item.nameSingular, ...objectMetadataItems.map((item) => ({
label: item.labelSingular, value: item.nameSingular,
})), label: item.labelPlural,
Icon: getIcon(item.icon),
})),
],
[objectMetadataItems, getIcon],
);
const actionOptions: SelectOption<string>[] = [
{ value: '*', label: 'All Actions', Icon: IconNorthStar },
{ value: 'create', label: 'Created', Icon: IconPlus },
{ value: 'update', label: 'Updated', Icon: IconRefresh },
{ value: 'delete', label: 'Deleted', Icon: IconTrash },
]; ];
const { updateOneRecord } = useUpdateOneRecord<Webhook>({ const { updateOneRecord } = useUpdateOneRecord<Webhook>({
objectNameSingular: CoreObjectNameSingular.Webhook, objectNameSingular: CoreObjectNameSingular.Webhook,
}); });
const cleanAndFormatOperations = (operations: WebhookOperationType[]) => {
return Array.from(
new Set(
operations
.filter((op) => isDefined(op.object) && isDefined(op.action))
.map((op) => `${op.object}.${op.action}`),
),
);
};
const handleSave = async () => { const handleSave = async () => {
const cleanedOperations = cleanAndFormatOperations(operations);
setIsDirty(false); setIsDirty(false);
await updateOneRecord({ await updateOneRecord({
idToUpdate: webhookId, idToUpdate: webhookId,
updateOneRecordInput: { updateOneRecordInput: {
operation: `${operationObjectSingularName}.${operationAction}`, operation: cleanedOperations?.[0],
operations: cleanedOperations,
description: description, description: description,
}, },
}); });
navigate(developerPath); navigate(developerPath);
}; };
const addEmptyOperationIfNecessary = (
newOperations: WebhookOperationType[],
) => {
if (
!newOperations.some((op) => op.object === '*' && op.action === '*') &&
!newOperations.some((op) => op.object === null)
) {
return [...newOperations, WEBHOOK_EMPTY_OPERATION];
}
return newOperations;
};
const updateOperation = (
index: number,
field: 'object' | 'action',
value: string | null,
) => {
const newOperations = [...operations];
newOperations[index] = {
...newOperations[index],
[field]: value,
};
setOperations(addEmptyOperationIfNecessary(newOperations));
setIsDirty(true);
};
const removeOperation = (index: number) => {
const newOperations = operations.filter((_, i) => i !== index);
setOperations(addEmptyOperationIfNecessary(newOperations));
setIsDirty(true);
};
if (!webhookData?.targetUrl) { if (!webhookData?.targetUrl) {
return <></>; return <></>;
} }
@ -108,10 +200,7 @@ export const SettingsDevelopersWebhooksDetail = () => {
children: 'Workspace', children: 'Workspace',
href: getSettingsPagePath(SettingsPath.Workspace), href: getSettingsPagePath(SettingsPath.Workspace),
}, },
{ { children: 'Developers', href: developerPath },
children: 'Developers',
href: developerPath,
},
{ children: 'Webhook' }, { children: 'Webhook' },
]} ]}
actionButton={ actionButton={
@ -152,43 +241,50 @@ export const SettingsDevelopersWebhooksDetail = () => {
<Section> <Section>
<H2Title <H2Title
title="Filters" title="Filters"
description="Select the event you wish to send to this endpoint" description="Select the events you wish to send to this endpoint"
/> />
<StyledFilterRow> {operations.map((operation, index) => (
<Select <StyledFilterRow key={index}>
fullWidth <Select
dropdownId="object-webhook-type-select" withSearchInput
value={operationObjectSingularName} dropdownWidth={OBJECT_DROPDOWN_WIDTH}
onChange={(objectSingularName) => { dropdownId={`object-webhook-type-select-${index}`}
setIsDirty(true); value={operation.object}
setOperationObjectSingularName(objectSingularName); onChange={(object) => updateOperation(index, 'object', object)}
}} options={fieldTypeOptions}
options={fieldTypeOptions} emptyOption={{
/> value: null,
<Select label: 'Choose an object',
fullWidth Icon: IconBox,
dropdownId="operation-webhook-type-select" }}
value={operationAction} />
onChange={(operationAction) => {
setIsDirty(true); <Select
setOperationAction(operationAction); dropdownWidth={ACTION_DROPDOWN_WIDTH}
}} dropdownId={`operation-webhook-type-select-${index}`}
options={[ value={operation.action}
{ value: '*', label: 'All Actions' }, onChange={(action) => updateOperation(index, 'action', action)}
{ value: 'create', label: 'Create' }, options={actionOptions}
{ value: 'update', label: 'Update' }, />
{ value: 'delete', label: 'Delete' },
]} {index < operations.length - 1 ? (
/> <IconButton
</StyledFilterRow> onClick={() => removeOperation(index)}
variant="tertiary"
size="medium"
Icon={IconTrash}
/>
) : (
<StyledPlaceholder />
)}
</StyledFilterRow>
))}
</Section> </Section>
{isAnalyticsEnabled && isAnalyticsV2Enabled ? ( {isAnalyticsEnabled && isAnalyticsV2Enabled && (
<> <>
<SettingsDevelopersWebhookUsageGraphEffect webhookId={webhookId} /> <SettingsDevelopersWebhookUsageGraphEffect webhookId={webhookId} />
<SettingsDevelopersWebhookUsageGraph webhookId={webhookId} /> <SettingsDevelopersWebhookUsageGraph webhookId={webhookId} />
</> </>
) : (
<></>
)} )}
<Section> <Section>
<H2Title title="Danger zone" description="Delete this integration" /> <H2Title title="Danger zone" description="Delete this integration" />

View File

@ -19,9 +19,11 @@ export const SettingsDevelopersWebhooksNew = () => {
const [formValues, setFormValues] = useState<{ const [formValues, setFormValues] = useState<{
targetUrl: string; targetUrl: string;
operation: string; operation: string;
operations: string[];
}>({ }>({
targetUrl: '', targetUrl: '',
operation: '*.*', operation: '*.*',
operations: ['*.*'],
}); });
const [isTargetUrlValid, setIsTargetUrlValid] = useState(true); const [isTargetUrlValid, setIsTargetUrlValid] = useState(true);

View File

@ -0,0 +1,6 @@
import { WebhookOperationType } from '~/pages/settings/developers/webhooks/types/WebhookOperationsType';
export const WEBHOOK_EMPTY_OPERATION: WebhookOperationType = {
object: null,
action: '*',
};

View File

@ -0,0 +1,4 @@
export type WebhookOperationType = {
object: string | null;
action: string;
};

View File

@ -0,0 +1,56 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import chalk from 'chalk';
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { BaseCommandOptions } from 'src/database/commands/base.command';
@Command({
name: 'upgrade-0.32:copy-webhook-operation-into-operations',
description:
'Read, transform and copy webhook from deprecated column operation into newly created column operations',
})
export class CopyWebhookOperationIntoOperationsCommand extends ActiveWorkspacesCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {
super(workspaceRepository);
}
async executeActiveWorkspacesCommand(
passedParams: string[],
options: BaseCommandOptions,
activeWorkspaceIds: string[],
): Promise<void> {
this.logger.log('Running command to copy operation to operations');
for (const workspaceId of activeWorkspaceIds) {
this.logger.log(`Running command for workspace ${workspaceId}`);
const webhookRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'webhook',
);
const webhooks = await webhookRepository.find();
for (const webhook of webhooks) {
if ('operation' in webhook) {
await webhookRepository.update(webhook.id, {
operations: [webhook.operation],
});
this.logger.log(
chalk.yellow(`Copied webhook operation to operations`),
);
}
}
}
}
}

View File

@ -10,6 +10,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
import { SearchModule } from 'src/engine/metadata-modules/search/search.module'; import { SearchModule } from 'src/engine/metadata-modules/search/search.module';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module'; import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module';
import { CopyWebhookOperationIntoOperationsCommand } from 'src/database/commands/upgrade-version/0-32/0-32-copy-webhook-operation-into-operations-command';
@Module({ @Module({
imports: [ imports: [
@ -25,6 +26,7 @@ import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manage
providers: [ providers: [
UpgradeTo0_32Command, UpgradeTo0_32Command,
EnforceUniqueConstraintsCommand, EnforceUniqueConstraintsCommand,
CopyWebhookOperationIntoOperationsCommand,
SimplifySearchVectorExpressionCommand, SimplifySearchVectorExpressionCommand,
], ],
}) })

View File

@ -1,6 +1,6 @@
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { Like } from 'typeorm'; import { ArrayContains } from 'typeorm';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
@ -54,10 +54,10 @@ export class CallWebhookJobsJob {
const webhooks = await webhookRepository.find({ const webhooks = await webhookRepository.find({
where: [ where: [
{ operation: Like(`%${eventName}%`) }, { operations: ArrayContains([eventName]) },
{ operation: Like(`%*.${operation}%`) }, { operations: ArrayContains([`*.${operation}`]) },
{ operation: Like(`%${nameSingular}.*%`) }, { operations: ArrayContains([`${nameSingular}.*`]) },
{ operation: Like('%*.*%') }, { operations: ArrayContains(['*.*']) },
], ],
}); });
@ -80,12 +80,9 @@ export class CallWebhookJobsJob {
); );
}); });
if (webhooks.length) { webhooks.length > 0 &&
this.logger.log( this.logger.log(
`CallWebhookJobsJob on eventName '${eventName}' called on webhooks ids [\n"${webhooks `CallWebhookJobsJob on eventName '${eventName}' triggered webhooks with ids [\n"${webhooks.map((webhook) => webhook.id).join('",\n"')}"\n]`,
.map((webhook) => webhook.id)
.join('",\n"')}"\n]`,
); );
}
} }
} }

View File

@ -189,3 +189,9 @@ export class FieldMetadataDefaultValuePhones {
@IsObject() @IsObject()
additionalPhones: object | null; additionalPhones: object | null;
} }
export class FieldMetadataDefaultArray {
@ValidateIf((_object, value) => value !== null)
@IsArray()
value: string[] | null;
}

View File

@ -1,5 +1,6 @@
import { import {
FieldMetadataDefaultActor, FieldMetadataDefaultActor,
FieldMetadataDefaultArray,
FieldMetadataDefaultValueAddress, FieldMetadataDefaultValueAddress,
FieldMetadataDefaultValueBoolean, FieldMetadataDefaultValueBoolean,
FieldMetadataDefaultValueCurrency, FieldMetadataDefaultValueCurrency,
@ -48,6 +49,7 @@ type FieldMetadataDefaultValueMapping = {
[FieldMetadataType.RAW_JSON]: FieldMetadataDefaultValueRawJson; [FieldMetadataType.RAW_JSON]: FieldMetadataDefaultValueRawJson;
[FieldMetadataType.RICH_TEXT]: FieldMetadataDefaultValueRichText; [FieldMetadataType.RICH_TEXT]: FieldMetadataDefaultValueRichText;
[FieldMetadataType.ACTOR]: FieldMetadataDefaultActor; [FieldMetadataType.ACTOR]: FieldMetadataDefaultActor;
[FieldMetadataType.ARRAY]: FieldMetadataDefaultArray;
}; };
export type FieldMetadataClassValidation = export type FieldMetadataClassValidation =

View File

@ -400,6 +400,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',
operations: '20202020-15b7-458e-bf30-74770a54411c',
description: '20202020-15b7-458e-bf30-74770a54410d', description: '20202020-15b7-458e-bf30-74770a54410d',
}; };

View File

@ -7,6 +7,7 @@ import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-
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';
import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.decorator';
@WorkspaceEntity({ @WorkspaceEntity({
standardId: STANDARD_OBJECT_IDS.webhook, standardId: STANDARD_OBJECT_IDS.webhook,
@ -36,8 +37,19 @@ export class WebhookWorkspaceEntity extends BaseWorkspaceEntity {
description: 'Webhook operation', description: 'Webhook operation',
icon: 'IconCheckbox', icon: 'IconCheckbox',
}) })
@WorkspaceIsDeprecated()
operation: string; operation: string;
@WorkspaceField({
standardId: WEBHOOK_STANDARD_FIELD_IDS.operations,
type: FieldMetadataType.ARRAY,
label: 'Operations',
description: 'Webhook operations',
icon: 'IconCheckbox',
defaultValue: ['*.*'],
})
operations: string[];
@WorkspaceField({ @WorkspaceField({
standardId: WEBHOOK_STANDARD_FIELD_IDS.description, standardId: WEBHOOK_STANDARD_FIELD_IDS.description,
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,

View File

@ -12,7 +12,7 @@ describe('searchWebhooksResolver (e2e)', () => {
node { node {
id id
targetUrl targetUrl
operation operations
description description
createdAt createdAt
updatedAt updatedAt
@ -46,7 +46,7 @@ describe('searchWebhooksResolver (e2e)', () => {
expect(searchWebhooks).toHaveProperty('id'); expect(searchWebhooks).toHaveProperty('id');
expect(searchWebhooks).toHaveProperty('targetUrl'); expect(searchWebhooks).toHaveProperty('targetUrl');
expect(searchWebhooks).toHaveProperty('operation'); expect(searchWebhooks).toHaveProperty('operations');
expect(searchWebhooks).toHaveProperty('description'); expect(searchWebhooks).toHaveProperty('description');
expect(searchWebhooks).toHaveProperty('createdAt'); expect(searchWebhooks).toHaveProperty('createdAt');
expect(searchWebhooks).toHaveProperty('updatedAt'); expect(searchWebhooks).toHaveProperty('updatedAt');

View File

@ -12,7 +12,7 @@ describe('webhooksResolver (e2e)', () => {
node { node {
id id
targetUrl targetUrl
operation operations
description description
createdAt createdAt
updatedAt updatedAt
@ -46,7 +46,7 @@ describe('webhooksResolver (e2e)', () => {
expect(webhooks).toHaveProperty('id'); expect(webhooks).toHaveProperty('id');
expect(webhooks).toHaveProperty('targetUrl'); expect(webhooks).toHaveProperty('targetUrl');
expect(webhooks).toHaveProperty('operation'); expect(webhooks).toHaveProperty('operations');
expect(webhooks).toHaveProperty('description'); expect(webhooks).toHaveProperty('description');
expect(webhooks).toHaveProperty('createdAt'); expect(webhooks).toHaveProperty('createdAt');
expect(webhooks).toHaveProperty('updatedAt'); expect(webhooks).toHaveProperty('updatedAt');

View File

@ -134,6 +134,7 @@ export {
IconH1, IconH1,
IconH2, IconH2,
IconH3, IconH3,
IconHandClick,
IconHeadphones, IconHeadphones,
IconHeart, IconHeart,
IconHeartOff, IconHeartOff,
@ -166,6 +167,7 @@ export {
IconMoneybag, IconMoneybag,
IconMoodSmile, IconMoodSmile,
IconMouse2, IconMouse2,
IconNorthStar,
IconNotes, IconNotes,
IconNumbers, IconNumbers,
IconPaperclip, IconPaperclip,