Files
twenty/packages/twenty-front/src/modules/settings/developers/hooks/useWebhookUpdateForm.ts
T
Paul Rastoin 9ad8287dbc [REFACTOR] twenty-shared multi barrel and CJS/ESM build with preconstruct (#11083)
# Introduction

In this PR we've migrated `twenty-shared` from a `vite` app
[libary-mode](https://vite.dev/guide/build#library-mode) to a
[preconstruct](https://preconstruct.tools/) "atomic" application ( in
the future would like to introduce preconstruct to handle of all our
atomic dependencies such as `twenty-emails` `twenty-ui` etc it will be
integrated at the monorepo's root directly, would be to invasive in the
first, starting incremental via `twenty-shared`)

For more information regarding the motivations please refer to nor:
- https://github.com/twentyhq/core-team-issues/issues/587
-
https://github.com/twentyhq/core-team-issues/issues/281#issuecomment-2630949682

close https://github.com/twentyhq/core-team-issues/issues/589
close https://github.com/twentyhq/core-team-issues/issues/590

## How to test
In order to ease the review this PR will ship all the codegen at the
very end, the actual meaning full diff is `+2,411 −114`
In order to migrate existing dependent packages to `twenty-shared` multi
barrel new arch you need to run in local:
```sh
yarn tsx packages/twenty-shared/scripts/migrateFromSingleToMultiBarrelImport.ts && \
npx nx run-many -t lint --fix -p twenty-front twenty-ui twenty-server twenty-emails twenty-shared twenty-zapier
```
Note that `migrateFromSingleToMultiBarrelImport` is idempotent, it's atm
included in the PR but should not be merged. ( such as codegen will be
added before merging this script will be removed )

## Misc
- related opened issue preconstruct
https://github.com/preconstruct/preconstruct/issues/617

## Closed related PR
- https://github.com/twentyhq/twenty/pull/11028
- https://github.com/twentyhq/twenty/pull/10993
- https://github.com/twentyhq/twenty/pull/10960

## Upcoming enhancement: ( in others dedicated PRs )
- 1/ refactor generate barrel to export atomic module instead of `*`
- 2/ generate barrel own package with several files and tests
- 3/ Migration twenty-ui the same way
- 4/ Use `preconstruct` at monorepo global level

## Conclusion
As always any suggestions are welcomed !
2025-03-22 19:16:06 +01:00

219 lines
5.9 KiB
TypeScript

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 { Webhook } from '@/settings/developers/types/webhook/Webhook';
import { SettingsPath } from '@/types/SettingsPath';
import { useState } from 'react';
import { useDebouncedCallback } from 'use-debounce';
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';
import {
getUrlHostnameOrThrow,
isDefined,
isValidUrl,
} from 'twenty-shared/utils';
type WebhookFormData = {
targetUrl: string;
description?: string;
operations: WebhookOperationType[];
secret?: string;
};
export const useWebhookUpdateForm = ({
webhookId,
isCreationMode,
}: {
webhookId: string;
isCreationMode: boolean;
}) => {
const navigate = useNavigateSettings();
const [isCreated, setIsCreated] = useState(!isCreationMode);
const [loading, setLoading] = useState(!isCreationMode);
const [title, setTitle] = useState(isCreationMode ? 'New Webhook' : '');
const [formData, setFormData] = useState<WebhookFormData>({
targetUrl: '',
description: '',
operations: [
{
object: '*',
action: '*',
},
],
secret: '',
});
const [isTargetUrlValid, setIsTargetUrlValid] = useState(true);
const { updateOneRecord } = useUpdateOneRecord<Webhook>({
objectNameSingular: CoreObjectNameSingular.Webhook,
});
const { createOneRecord } = useCreateOneRecord<Webhook>({
objectNameSingular: CoreObjectNameSingular.Webhook,
});
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 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 = useDebouncedCallback(async () => {
const cleanedOperations = cleanAndFormatOperations(formData.operations);
const webhookData = {
...(isTargetUrlValid && { targetUrl: formData.targetUrl.trim() }),
operations: cleanedOperations,
description: formData.description,
secret: formData.secret,
};
if (!isCreated) {
await createOneRecord({ id: webhookId, ...webhookData });
setIsCreated(true);
return;
}
await updateOneRecord({
idToUpdate: webhookId,
updateOneRecordInput: {
...(isTargetUrlValid && { targetUrl: formData.targetUrl.trim() }),
operations: cleanedOperations,
description: formData.description,
secret: formData.secret,
},
});
}, 300);
const isFormValidAndSetErrors = () => {
const { targetUrl } = formData;
if (isDefined(targetUrl)) {
const trimmedUrl = targetUrl.trim();
const isValid = isValidUrl(trimmedUrl);
if (!isValid) {
setIsTargetUrlValid(false);
return false;
}
setIsTargetUrlValid(true);
}
return true;
};
const updateWebhook = async (data: Partial<WebhookFormData>) => {
setFormData((prev) => ({ ...prev, ...data }));
if (!isFormValidAndSetErrors()) {
return;
}
if (isDefined(data?.targetUrl)) {
setTitle(getUrlHostnameOrThrow(data.targetUrl) || 'New Webhook');
}
await handleSave();
};
const updateOperation = async (
index: number,
field: 'object' | 'action',
value: string | null,
) => {
const newOperations = [...formData.operations];
newOperations[index] = {
...newOperations[index],
[field]: value,
};
await updateWebhook({
operations: addEmptyOperationIfNecessary(newOperations),
});
};
const removeOperation = async (index: number) => {
const newOperations = formData.operations.filter((_, i) => i !== index);
await updateWebhook({
operations: addEmptyOperationIfNecessary(newOperations),
});
};
const { deleteOneRecord: deleteOneWebhook } = useDeleteOneRecord({
objectNameSingular: CoreObjectNameSingular.Webhook,
});
const deleteWebhook = async () => {
await deleteOneWebhook(webhookId);
navigate(SettingsPath.Webhooks);
};
useFindOneRecord({
skip: isCreationMode,
objectNameSingular: CoreObjectNameSingular.Webhook,
objectRecordId: webhookId,
onCompleted: (data) => {
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 operations = addEmptyOperationIfNecessary(baseOperations);
setFormData({
targetUrl: data.targetUrl,
description: data.description,
operations,
secret: data.secret,
});
if (isValidUrl(data.targetUrl)) {
setTitle(getUrlHostnameOrThrow(data.targetUrl));
}
setLoading(false);
},
});
return {
formData,
title,
isTargetUrlValid,
updateWebhook,
updateOperation,
removeOperation,
deleteWebhook,
loading,
};
};