import styled from '@emotion/styled'; import { useMemo, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { Button, H2Title, IconBox, IconButton, IconNorthStar, IconPlus, IconRefresh, IconTrash, isDefined, useIcons, } from 'twenty-ui'; import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { Webhook } from '@/settings/developers/types/webhook/Webhook'; import { SettingsDevelopersWebhookUsageGraph } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph'; import { SettingsDevelopersWebhookUsageGraphEffect } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { Select, SelectOption } from '@/ui/input/components/Select'; import { TextArea } from '@/ui/input/components/TextArea'; import { TextInput } from '@/ui/input/components/TextInput'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; 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` display: grid; grid-template-columns: ${OBJECT_DROPDOWN_WIDTH}px ${ACTION_DROPDOWN_WIDTH}px auto; 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 = () => { const { objectMetadataItems } = useObjectMetadataItems(); const isAnalyticsEnabled = useRecoilValue(isAnalyticsEnabledState); const navigate = useNavigate(); const { webhookId = '' } = useParams(); const [isDeleteWebhookModalOpen, setIsDeleteWebhookModalOpen] = useState(false); const [description, setDescription] = useState(''); const [operations, setOperations] = useState([ WEBHOOK_EMPTY_OPERATION, ]); const [isDirty, setIsDirty] = useState(false); const { getIcon } = useIcons(); const { record: webhookData } = useFindOneRecord({ objectNameSingular: CoreObjectNameSingular.Webhook, objectRecordId: webhookId, onCompleted: (data) => { setDescription(data?.description ?? ''); 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], }, ] : []; setOperations(addEmptyOperationIfNecessary(baseOperations)); setIsDirty(false); }, }); const { deleteOneRecord: deleteOneWebhook } = useDeleteOneRecord({ objectNameSingular: CoreObjectNameSingular.Webhook, }); const developerPath = getSettingsPagePath(SettingsPath.Developers); const deleteWebhook = () => { deleteOneWebhook(webhookId); navigate(developerPath); }; const isAnalyticsV2Enabled = useIsFeatureEnabled('IS_ANALYTICS_V2_ENABLED'); const fieldTypeOptions: SelectOption[] = useMemo( () => [ { value: '*', label: 'All Objects', Icon: IconNorthStar }, ...objectMetadataItems.map((item) => ({ value: item.nameSingular, label: item.labelPlural, Icon: getIcon(item.icon), })), ], [objectMetadataItems, getIcon], ); const actionOptions: SelectOption[] = [ { 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({ 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 cleanedOperations = cleanAndFormatOperations(operations); setIsDirty(false); await updateOneRecord({ idToUpdate: webhookId, updateOneRecordInput: { operation: cleanedOperations?.[0], operations: cleanedOperations, description: description, }, }); 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) { return <>; } return ( { navigate(developerPath); }} onSave={handleSave} /> } >