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:
@ -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';
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { WebhookOperationType } from '~/pages/settings/developers/webhooks/types/WebhookOperationsType';
|
||||||
|
|
||||||
|
export const WEBHOOK_EMPTY_OPERATION: WebhookOperationType = {
|
||||||
|
object: null,
|
||||||
|
action: '*',
|
||||||
|
};
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
export type WebhookOperationType = {
|
||||||
|
object: string | null;
|
||||||
|
action: string;
|
||||||
|
};
|
||||||
@ -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`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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]`,
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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 =
|
||||||
|
|||||||
@ -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',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user