Files
twenty/packages/twenty-front/src/modules/settings/developers/components/SettingsDevelopersWebhookForm.tsx
T
Raphaël Bosi 8034c7725f Reorganize twenty-ui into best-practice component domains and per-component folders (#21745)
Reorganizes `twenty-ui`'s component organization to follow how the best
UI libraries (MUI, Mantine, Base UI, Polaris) structure their source,
now that the package has stabilized.

**Taxonomy** — dissolves the meaningless `components/` junk-drawer and
the 107-file `display/` mega-category. New domains/subpaths:
`data-display`, `typography`, `icon`, `surfaces`; `feedback` and
`layout` absorb the rest (banners/callout/info + placeholders →
feedback; modal/card → surfaces; motion + separators → layout).

**Per-component layout** — every component is now
`<domain>/<ComponentName>/<ComponentName>.tsx` with colocated
styles/stories/types, `internal/` for private helpers and `parts/` for
re-exported compound sub-parts. The redundant inner `/components/` is
gone. `icon` and `json-visualizer` are kept as cohesive subsystems.

**Also:** adds a tree-shakeable root barrel (`import { Button } from
'twenty-ui'`), the generator now owns `individual-entry.ts`, and a real
barrel-leak bug is fixed (private `internals/` parts were leaking into
the public API).

Consumer imports (~1.2k files) and the `twenty-sdk` UI aggregator were
updated by codemod. The change is **export-neutral** except 16
intentionally-removed private internals symbols (all verified
unconsumed). Gates green: typecheck, lint, build, size-limit, storybook.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21745?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-18 10:31:29 +02:00

216 lines
7.1 KiB
TypeScript

import { Controller, FormProvider } from 'react-hook-form';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
import { type WebhookFormMode } from '@/settings/developers/constants/WebhookFormMode';
import { useWebhookForm } from '@/settings/developers/hooks/useWebhookForm';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { TextArea } from '@/ui/input/components/TextArea';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { Trans, useLingui } from '@lingui/react/macro';
import { SettingsPath } from 'twenty-shared/types';
import {
getSettingsPath,
getUrlHostnameOrThrow,
isDefined,
isValidUrl,
} from 'twenty-shared/utils';
import { IconTrash } from 'twenty-ui/icon';
import { H2Title } from 'twenty-ui/typography';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { SettingsDatabaseEventsForm } from '@/settings/components/SettingsDatabaseEventsForm';
const DELETE_WEBHOOK_MODAL_ID = 'delete-webhook-modal';
type SettingsDevelopersWebhookFormProps = {
webhookId?: string;
mode: WebhookFormMode;
};
export const SettingsDevelopersWebhookForm = ({
webhookId,
mode,
}: SettingsDevelopersWebhookFormProps) => {
const { t } = useLingui();
const navigate = useNavigateSettings();
const { openModal } = useModal();
const {
formConfig,
loading,
canSave,
handleSave,
updateOperation,
removeOperation,
handleDelete,
isCreationMode,
error,
} = useWebhookForm({ webhookId, mode });
const getTitle = () => {
if (isCreationMode) {
return t`New Webhook`;
}
const targetUrl = formConfig.watch('targetUrl');
if (isDefined(targetUrl) && isValidUrl(targetUrl.trim())) {
return getUrlHostnameOrThrow(targetUrl);
}
};
if ((loading && !isCreationMode) || isDefined(error)) {
return <SettingsSkeletonLoader />;
}
const descriptionTextAreaId = `${webhookId}-description`;
const targetUrlTextInputId = `${webhookId}-target-url`;
const secretTextInputId = `${webhookId}-secret`;
return (
// oxlint-disable-next-line react/jsx-props-no-spreading
<FormProvider {...formConfig}>
<SettingsPageLayout
title={getTitle()}
links={[
{
children: t`Workspace`,
href: getSettingsPath(SettingsPath.General),
},
{
children: t`APIs & Webhooks`,
href: getSettingsPath(SettingsPath.ApiWebhooks),
},
{ children: isCreationMode ? t`New` : getTitle() },
]}
actionButton={
<SaveAndCancelButtons
isSaveDisabled={!canSave}
isCancelDisabled={formConfig.formState.isSubmitting}
onCancel={() => navigate(SettingsPath.ApiWebhooks)}
onSave={formConfig.handleSubmit(handleSave)}
/>
}
>
<SettingsPageContainer>
<Section>
<H2Title
title={t`Endpoint URL`}
description={t`We will send a POST request to this endpoint for each new event in application/json format`}
/>
<Controller
name="targetUrl"
control={formConfig.control}
render={({
field: { onChange, value },
fieldState: { error },
}) => {
return (
<SettingsTextInput
instanceId={targetUrlTextInputId}
placeholder={t`https://example.com/webhook`}
value={value}
onChange={onChange}
error={error?.message}
fullWidth
autoFocus={isCreationMode}
/>
);
}}
/>
</Section>
<Section>
<H2Title
title={t`Description`}
description={t`We will send a POST request to this endpoint for each new event in application/json format.`}
/>
<Controller
name="description"
control={formConfig.control}
render={({ field: { onChange, value } }) => (
<TextArea
textAreaId={descriptionTextAreaId}
placeholder={t`Write a description`}
minRows={4}
maxRows={5}
value={value || ''}
onChange={onChange}
/>
)}
/>
</Section>
<Section>
<H2Title
title={t`Filters`}
description={t`Select the events you wish to send to this endpoint`}
/>
<Controller
name="operations"
control={formConfig.control}
render={({ field: { value } }) => (
<SettingsDatabaseEventsForm
events={value}
updateOperation={updateOperation}
removeOperation={removeOperation}
/>
)}
/>
</Section>
<Section>
<H2Title
title={t`Secret`}
description={t`Optional secret used to compute the HMAC signature for webhook payloads`}
/>
<Controller
name="secret"
control={formConfig.control}
render={({ field: { onChange, value } }) => (
<SettingsTextInput
instanceId={secretTextInputId}
placeholder={t`Secret (optional)`}
value={value || ''}
onChange={onChange}
fullWidth
/>
)}
/>
</Section>
{!isCreationMode && (
<Section>
<H2Title
title={t`Danger zone`}
description={t`Delete this webhook`}
/>
<Button
accent="danger"
variant="secondary"
title={t`Delete`}
Icon={IconTrash}
onClick={() => openModal(DELETE_WEBHOOK_MODAL_ID)}
/>
</Section>
)}
</SettingsPageContainer>
</SettingsPageLayout>
{!isCreationMode && (
<ConfirmationModal
confirmationPlaceholder={t`yes`}
confirmationValue={t`yes`}
modalInstanceId={DELETE_WEBHOOK_MODAL_ID}
title={t`Delete webhook`}
subtitle={
<Trans>
Please type "yes" to confirm you want to delete this webhook.
</Trans>
}
onConfirmClick={handleDelete}
confirmButtonText={t`Delete`}
/>
)}
</FormProvider>
);
};