Api keys and webhook migration to core (#13011)

TODO: check Zapier trigger records work as expected

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
nitin
2025-07-09 20:33:54 +05:30
committed by GitHub
parent 18792f9f74
commit 484c267aa6
113 changed files with 4563 additions and 1060 deletions
@@ -1,13 +1,13 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { WebhookFormMode } from '@/settings/developers/constants/WebhookFormMode';
import { Webhook } from '@/settings/developers/types/webhook/Webhook';
import { addEmptyOperationIfNecessary } from '@/settings/developers/utils/addEmptyOperationIfNecessary';
import {
createWebhookCreateInput,
createWebhookUpdateInput,
} from '@/settings/developers/utils/createWebhookInput';
import { parseOperationsFromStrings } from '@/settings/developers/utils/parseOperationsFromStrings';
import {
webhookFormSchema,
WebhookFormValues,
@@ -16,100 +16,66 @@ import { SettingsPath } from '@/types/SettingsPath';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import {
useCreateWebhookMutation,
useDeleteWebhookMutation,
useGetWebhookQuery,
useUpdateWebhookMutation,
} from '~/generated-metadata/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { WEBHOOK_EMPTY_OPERATION } from '~/pages/settings/developers/webhooks/constants/WebhookEmptyOperation';
import { WebhookOperationType } from '~/pages/settings/developers/webhooks/types/WebhookOperationsType';
type UseWebhookFormProps = {
webhookId?: string;
mode: WebhookFormMode;
};
const DEFAULT_FORM_VALUES: WebhookFormValues = {
targetUrl: '',
description: '',
operations: [{ object: '*', action: '*' }],
secret: '',
};
export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => {
const navigate = useNavigateSettings();
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const isCreationMode = mode === WebhookFormMode.Create;
const { createOneRecord } = useCreateOneRecord<Webhook>({
objectNameSingular: CoreObjectNameSingular.Webhook,
});
const { updateOneRecord } = useUpdateOneRecord<Webhook>({
objectNameSingular: CoreObjectNameSingular.Webhook,
});
const { deleteOneRecord: deleteOneWebhook } = useDeleteOneRecord({
objectNameSingular: CoreObjectNameSingular.Webhook,
});
const [createWebhook] = useCreateWebhookMutation();
const [updateWebhook] = useUpdateWebhookMutation();
const [deleteWebhook] = useDeleteWebhookMutation();
const formConfig = useForm<WebhookFormValues>({
mode: isCreationMode ? 'onSubmit' : 'onTouched',
resolver: zodResolver(webhookFormSchema),
defaultValues: {
targetUrl: '',
description: '',
operations: [
{
object: '*',
action: '*',
},
],
secret: '',
},
defaultValues: DEFAULT_FORM_VALUES,
});
const addEmptyOperationIfNecessary = (
newOperations: WebhookOperationType[],
): WebhookOperationType[] => {
if (
!newOperations.some((op) => op.object === '*' && op.action === '*') &&
!newOperations.some((op) => op.object === null)
) {
return [...newOperations, WEBHOOK_EMPTY_OPERATION];
}
return newOperations;
};
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 { loading, error } = useFindOneRecord({
skip: isCreationMode,
objectNameSingular: CoreObjectNameSingular.Webhook,
objectRecordId: webhookId || '',
const { loading, error } = useGetWebhookQuery({
skip: isCreationMode || !webhookId,
variables: {
input: { id: webhookId || '' },
},
onCompleted: (data) => {
if (!data) return;
const webhook = data.webhook;
if (!webhook) return;
const baseOperations = data?.operations
? 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],
},
]
: [];
const baseOperations = webhook?.operations?.length
? parseOperationsFromStrings(webhook.operations)
: [];
const operations = addEmptyOperationIfNecessary(baseOperations);
formConfig.reset({
targetUrl: data.targetUrl || '',
description: data.description || '',
targetUrl: webhook.targetUrl || '',
description: webhook.description || '',
operations,
secret: data.secret || '',
secret: webhook.secret || '',
});
},
onError: () => {
enqueueErrorSnackBar({
message: t`Failed to load webhook`,
});
},
});
@@ -121,19 +87,9 @@ export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => {
const handleCreate = async (formValues: WebhookFormValues) => {
try {
const cleanedOperations = cleanAndFormatOperations(formValues.operations);
const webhookData = {
targetUrl: formValues.targetUrl.trim(),
operations: cleanedOperations,
description: formValues.description,
secret: formValues.secret,
};
const createdWebhook = await createOneRecord({
id: v4(),
...webhookData,
});
const input = createWebhookCreateInput(formValues);
const { data } = await createWebhook({ variables: { input } });
const createdWebhook = data?.createWebhook;
const targetUrl = createdWebhook?.targetUrl
? `${createdWebhook?.targetUrl}`
@@ -163,23 +119,15 @@ export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => {
}
try {
const cleanedOperations = cleanAndFormatOperations(formValues.operations);
const webhookData = {
targetUrl: formValues.targetUrl.trim(),
operations: cleanedOperations,
description: formValues.description,
secret: formValues.secret,
};
await updateOneRecord({
idToUpdate: webhookId,
updateOneRecordInput: webhookData,
});
const input = createWebhookUpdateInput(formValues, webhookId);
const { data } = await updateWebhook({ variables: { input } });
const updatedWebhook = data?.updateWebhook;
formConfig.reset(formValues);
const targetUrl = webhookData.targetUrl ? `${webhookData.targetUrl}` : '';
const targetUrl = updatedWebhook?.targetUrl
? `${updatedWebhook.targetUrl}`
: '';
enqueueSuccessSnackBar({
message: t`Webhook ${targetUrl} updated successfully`,
@@ -224,7 +172,7 @@ export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => {
);
};
const deleteWebhook = async () => {
const handleDelete = async () => {
if (!webhookId) {
enqueueErrorSnackBar({
message: t`Webhook ID is required for deletion`,
@@ -233,7 +181,9 @@ export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => {
}
try {
await deleteOneWebhook(webhookId);
await deleteWebhook({
variables: { input: { id: webhookId } },
});
enqueueSuccessSnackBar({
message: t`Webhook deleted successfully`,
});
@@ -253,7 +203,7 @@ export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => {
handleSave,
updateOperation,
removeOperation,
deleteWebhook,
handleDelete,
isCreationMode,
error,
};