@ -26,7 +26,7 @@ type PartialObjectRecordWithId = Partial<ObjectRecord> & {
|
|||||||
type useCreateManyRecordsProps = {
|
type useCreateManyRecordsProps = {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||||
skipPostOptmisticEffect?: boolean;
|
skipPostOptimisticEffect?: boolean;
|
||||||
shouldMatchRootQueryFilter?: boolean;
|
shouldMatchRootQueryFilter?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ export const useCreateManyRecords = <
|
|||||||
>({
|
>({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
recordGqlFields,
|
recordGqlFields,
|
||||||
skipPostOptmisticEffect = false,
|
skipPostOptimisticEffect = false,
|
||||||
shouldMatchRootQueryFilter,
|
shouldMatchRootQueryFilter,
|
||||||
}: useCreateManyRecordsProps) => {
|
}: useCreateManyRecordsProps) => {
|
||||||
const apolloClient = useApolloClient();
|
const apolloClient = useApolloClient();
|
||||||
@ -135,7 +135,7 @@ export const useCreateManyRecords = <
|
|||||||
update: (cache, { data }) => {
|
update: (cache, { data }) => {
|
||||||
const records = data?.[mutationResponseField];
|
const records = data?.[mutationResponseField];
|
||||||
|
|
||||||
if (!isDefined(records?.length) || skipPostOptmisticEffect) return;
|
if (!isDefined(records?.length) || skipPostOptimisticEffect) return;
|
||||||
|
|
||||||
triggerCreateRecordsOptimisticEffect({
|
triggerCreateRecordsOptimisticEffect({
|
||||||
cache,
|
cache,
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import { isDefined } from 'twenty-shared';
|
|||||||
type useCreateOneRecordProps = {
|
type useCreateOneRecordProps = {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||||
skipPostOptmisticEffect?: boolean;
|
skipPostOptimisticEffect?: boolean;
|
||||||
shouldMatchRootQueryFilter?: boolean;
|
shouldMatchRootQueryFilter?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ export const useCreateOneRecord = <
|
|||||||
>({
|
>({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
recordGqlFields,
|
recordGqlFields,
|
||||||
skipPostOptmisticEffect = false,
|
skipPostOptimisticEffect = false,
|
||||||
shouldMatchRootQueryFilter,
|
shouldMatchRootQueryFilter,
|
||||||
}: useCreateOneRecordProps) => {
|
}: useCreateOneRecordProps) => {
|
||||||
const apolloClient = useApolloClient();
|
const apolloClient = useApolloClient();
|
||||||
@ -95,7 +95,7 @@ export const useCreateOneRecord = <
|
|||||||
computeReferences: false,
|
computeReferences: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (optimisticRecordNode !== null) {
|
if (skipPostOptimisticEffect === false && optimisticRecordNode !== null) {
|
||||||
triggerCreateRecordsOptimisticEffect({
|
triggerCreateRecordsOptimisticEffect({
|
||||||
cache: apolloClient.cache,
|
cache: apolloClient.cache,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
@ -117,7 +117,7 @@ export const useCreateOneRecord = <
|
|||||||
},
|
},
|
||||||
update: (cache, { data }) => {
|
update: (cache, { data }) => {
|
||||||
const record = data?.[mutationResponseField];
|
const record = data?.[mutationResponseField];
|
||||||
if (skipPostOptmisticEffect === false && isDefined(record)) {
|
if (skipPostOptimisticEffect === false && isDefined(record)) {
|
||||||
triggerCreateRecordsOptimisticEffect({
|
triggerCreateRecordsOptimisticEffect({
|
||||||
cache,
|
cache,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
|
|||||||
@ -139,7 +139,7 @@ export const FormNumberFieldInput = ({
|
|||||||
</FormFieldInputRowContainer>
|
</FormFieldInputRowContainer>
|
||||||
|
|
||||||
{hint ? <InputHint>{hint}</InputHint> : null}
|
{hint ? <InputHint>{hint}</InputHint> : null}
|
||||||
{error && <InputErrorHelper>{error}</InputErrorHelper>}
|
<InputErrorHelper>{error}</InputErrorHelper>
|
||||||
</FormFieldInputContainer>
|
</FormFieldInputContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -91,7 +91,7 @@ export const FormTextFieldInput = ({
|
|||||||
) : null}
|
) : null}
|
||||||
</FormFieldInputRowContainer>
|
</FormFieldInputRowContainer>
|
||||||
{hint && <InputHint>{hint}</InputHint>}
|
{hint && <InputHint>{hint}</InputHint>}
|
||||||
{error && <InputErrorHelper>{error}</InputErrorHelper>}
|
<InputErrorHelper>{error}</InputErrorHelper>
|
||||||
</FormFieldInputContainer>
|
</FormFieldInputContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { IconChevronRight } from 'twenty-ui';
|
|||||||
import { Webhook } from '@/settings/developers/types/webhook/Webhook';
|
import { Webhook } from '@/settings/developers/types/webhook/Webhook';
|
||||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||||
|
import { getUrlHostname } from '~/utils/url/getUrlHostname';
|
||||||
|
|
||||||
export const StyledApisFieldTableRow = styled(TableRow)`
|
export const StyledApisFieldTableRow = styled(TableRow)`
|
||||||
grid-template-columns: 1fr 28px;
|
grid-template-columns: 1fr 28px;
|
||||||
@ -37,7 +38,9 @@ export const SettingsDevelopersWebhookTableRow = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledApisFieldTableRow to={to}>
|
<StyledApisFieldTableRow to={to}>
|
||||||
<StyledUrlTableCell>{fieldItem.targetUrl}</StyledUrlTableCell>
|
<StyledUrlTableCell>
|
||||||
|
{getUrlHostname(fieldItem.targetUrl, { keepPath: true })}
|
||||||
|
</StyledUrlTableCell>
|
||||||
<StyledIconTableCell>
|
<StyledIconTableCell>
|
||||||
<StyledIconChevronRight
|
<StyledIconChevronRight
|
||||||
size={theme.icon.size.md}
|
size={theme.icon.size.md}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
|||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
|
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||||
import { Webhook } from '@/settings/developers/types/webhook/Webhook';
|
import { Webhook } from '@/settings/developers/types/webhook/Webhook';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
import { WebhookOperationType } from '~/pages/settings/developers/webhooks/types/WebhookOperationsType';
|
import { WebhookOperationType } from '~/pages/settings/developers/webhooks/types/WebhookOperationsType';
|
||||||
@ -11,6 +12,7 @@ import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
|||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||||
import { isValidUrl } from '~/utils/url/isValidUrl';
|
import { isValidUrl } from '~/utils/url/isValidUrl';
|
||||||
|
import { getUrlHostname } from '~/utils/url/getUrlHostname';
|
||||||
|
|
||||||
type WebhookFormData = {
|
type WebhookFormData = {
|
||||||
targetUrl: string;
|
targetUrl: string;
|
||||||
@ -19,10 +21,18 @@ type WebhookFormData = {
|
|||||||
secret?: string;
|
secret?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useWebhookUpdateForm = ({ webhookId }: { webhookId: string }) => {
|
export const useWebhookUpdateForm = ({
|
||||||
|
webhookId,
|
||||||
|
isCreationMode,
|
||||||
|
}: {
|
||||||
|
webhookId: string;
|
||||||
|
isCreationMode: boolean;
|
||||||
|
}) => {
|
||||||
const navigate = useNavigateSettings();
|
const navigate = useNavigateSettings();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [isCreated, setIsCreated] = useState(!isCreationMode);
|
||||||
|
const [loading, setLoading] = useState(!isCreationMode);
|
||||||
|
const [title, setTitle] = useState(isCreationMode ? 'New Webhook' : '');
|
||||||
|
|
||||||
const [formData, setFormData] = useState<WebhookFormData>({
|
const [formData, setFormData] = useState<WebhookFormData>({
|
||||||
targetUrl: '',
|
targetUrl: '',
|
||||||
@ -37,6 +47,10 @@ export const useWebhookUpdateForm = ({ webhookId }: { webhookId: string }) => {
|
|||||||
objectNameSingular: CoreObjectNameSingular.Webhook,
|
objectNameSingular: CoreObjectNameSingular.Webhook,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { createOneRecord } = useCreateOneRecord<Webhook>({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.Webhook,
|
||||||
|
});
|
||||||
|
|
||||||
const addEmptyOperationIfNecessary = (
|
const addEmptyOperationIfNecessary = (
|
||||||
newOperations: WebhookOperationType[],
|
newOperations: WebhookOperationType[],
|
||||||
) => {
|
) => {
|
||||||
@ -49,34 +63,6 @@ export const useWebhookUpdateForm = ({ webhookId }: { webhookId: string }) => {
|
|||||||
return newOperations;
|
return newOperations;
|
||||||
};
|
};
|
||||||
|
|
||||||
useFindOneRecord({
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const cleanAndFormatOperations = (operations: WebhookOperationType[]) => {
|
const cleanAndFormatOperations = (operations: WebhookOperationType[]) => {
|
||||||
return Array.from(
|
return Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
@ -90,6 +76,19 @@ export const useWebhookUpdateForm = ({ webhookId }: { webhookId: string }) => {
|
|||||||
const handleSave = useDebouncedCallback(async () => {
|
const handleSave = useDebouncedCallback(async () => {
|
||||||
const cleanedOperations = cleanAndFormatOperations(formData.operations);
|
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({
|
await updateOneRecord({
|
||||||
idToUpdate: webhookId,
|
idToUpdate: webhookId,
|
||||||
updateOneRecordInput: {
|
updateOneRecordInput: {
|
||||||
@ -104,7 +103,13 @@ export const useWebhookUpdateForm = ({ webhookId }: { webhookId: string }) => {
|
|||||||
const validateData = (data: Partial<WebhookFormData>) => {
|
const validateData = (data: Partial<WebhookFormData>) => {
|
||||||
if (isDefined(data?.targetUrl)) {
|
if (isDefined(data?.targetUrl)) {
|
||||||
const trimmedUrl = data.targetUrl.trim();
|
const trimmedUrl = data.targetUrl.trim();
|
||||||
setIsTargetUrlValid(isValidUrl(trimmedUrl));
|
const isTargetUrlValid = isValidUrl(trimmedUrl);
|
||||||
|
setIsTargetUrlValid(isTargetUrlValid);
|
||||||
|
if (isTargetUrlValid) {
|
||||||
|
setTitle(
|
||||||
|
getUrlHostname(trimmedUrl, { keepPath: true }) || 'New Webhook',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -147,8 +152,41 @@ export const useWebhookUpdateForm = ({ webhookId }: { webhookId: string }) => {
|
|||||||
navigate(SettingsPath.Developers);
|
navigate(SettingsPath.Developers);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
setTitle(
|
||||||
|
getUrlHostname(data.targetUrl, { keepPath: true }) || 'New Webhook',
|
||||||
|
);
|
||||||
|
setLoading(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
formData,
|
formData,
|
||||||
|
title,
|
||||||
isTargetUrlValid,
|
isTargetUrlValid,
|
||||||
updateWebhook,
|
updateWebhook,
|
||||||
updateOperation,
|
updateOperation,
|
||||||
|
|||||||
@ -4,12 +4,25 @@ import styled from '@emotion/styled';
|
|||||||
const StyledInputErrorHelper = styled.div`
|
const StyledInputErrorHelper = styled.div`
|
||||||
color: ${({ theme }) => theme.color.red};
|
color: ${({ theme }) => theme.color.red};
|
||||||
font-size: ${({ theme }) => theme.font.size.xs};
|
font-size: ${({ theme }) => theme.font.size.xs};
|
||||||
|
position: absolute;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledErrorContainer = styled.div`
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const InputErrorHelper = ({
|
export const InputErrorHelper = ({
|
||||||
children,
|
children,
|
||||||
|
isVisible = true,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
isVisible?: boolean;
|
||||||
}) => (
|
}) => (
|
||||||
<StyledInputErrorHelper aria-live="polite">{children}</StyledInputErrorHelper>
|
<StyledErrorContainer>
|
||||||
|
{children && isVisible && (
|
||||||
|
<StyledInputErrorHelper aria-live="polite">
|
||||||
|
{children}
|
||||||
|
</StyledInputErrorHelper>
|
||||||
|
)}
|
||||||
|
</StyledErrorContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -76,18 +76,14 @@ const StyledInput = styled.input<
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
${({ theme }) => {
|
${({ theme, error }) => {
|
||||||
return `
|
return `
|
||||||
border-color: ${theme.color.blue};
|
border-color: ${error ? theme.border.color.danger : theme.color.blue};
|
||||||
`;
|
`;
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledErrorHelper = styled(InputErrorHelper)`
|
|
||||||
padding: ${({ theme }) => theme.spacing(1)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledLeftIconContainer = styled.div<{ sizeVariant: TextInputV2Size }>`
|
const StyledLeftIconContainer = styled.div<{ sizeVariant: TextInputV2Size }>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -270,9 +266,7 @@ const TextInputV2Component = (
|
|||||||
)}
|
)}
|
||||||
</StyledTrailingIconContainer>
|
</StyledTrailingIconContainer>
|
||||||
</StyledInputContainer>
|
</StyledInputContainer>
|
||||||
{error && !noErrorHelper && (
|
<InputErrorHelper isVisible={!noErrorHelper}>{error}</InputErrorHelper>
|
||||||
<StyledErrorHelper>{error}</StyledErrorHelper>
|
|
||||||
)}
|
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -111,7 +111,7 @@ export const WorkflowEditTriggerCronForm = ({
|
|||||||
placeholder="0 */1 * * *"
|
placeholder="0 */1 * * *"
|
||||||
error={errorMessagesVisible ? errorMessages.CUSTOM : undefined}
|
error={errorMessagesVisible ? errorMessages.CUSTOM : undefined}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
hint="Format: [Second] [Minute] [Hour] [Day of Month] [Month] [Day of Week]"
|
hint="Format: [Minute] [Hour] [Day of Month] [Month] [Day of Week]"
|
||||||
readonly={triggerOptions.readonly}
|
readonly={triggerOptions.readonly}
|
||||||
defaultValue={trigger.settings.pattern}
|
defaultValue={trigger.settings.pattern}
|
||||||
onPersist={(newPattern: string) => {
|
onPersist={(newPattern: string) => {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { v4 } from 'uuid';
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { SettingsApiKeysTable } from '@/settings/developers/components/SettingsApiKeysTable';
|
import { SettingsApiKeysTable } from '@/settings/developers/components/SettingsApiKeysTable';
|
||||||
import { SettingsReadDocumentationButton } from '@/settings/developers/components/SettingsReadDocumentationButton';
|
import { SettingsReadDocumentationButton } from '@/settings/developers/components/SettingsReadDocumentationButton';
|
||||||
@ -9,10 +10,6 @@ import styled from '@emotion/styled';
|
|||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import { Button, H2Title, IconPlus, MOBILE_VIEWPORT, Section } from 'twenty-ui';
|
import { Button, H2Title, IconPlus, MOBILE_VIEWPORT, Section } from 'twenty-ui';
|
||||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
|
||||||
import { Webhook } from '@/settings/developers/types/webhook/Webhook';
|
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
|
||||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
|
||||||
|
|
||||||
const StyledButtonContainer = styled.div`
|
const StyledButtonContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -32,24 +29,7 @@ const StyledContainer = styled.div<{ isMobile: boolean }>`
|
|||||||
|
|
||||||
export const SettingsDevelopers = () => {
|
export const SettingsDevelopers = () => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const navigate = useNavigateSettings();
|
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const { createOneRecord: createOneWebhook } = useCreateOneRecord<Webhook>({
|
|
||||||
objectNameSingular: CoreObjectNameSingular.Webhook,
|
|
||||||
});
|
|
||||||
|
|
||||||
const createNewWebhook = async () => {
|
|
||||||
const newWebhook = await createOneWebhook({
|
|
||||||
targetUrl: '',
|
|
||||||
operations: ['*.*'],
|
|
||||||
});
|
|
||||||
if (!newWebhook) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
navigate(SettingsPath.DevelopersNewWebhookDetail, {
|
|
||||||
webhookId: newWebhook.id,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer
|
<SubMenuTopBarContainer
|
||||||
@ -93,7 +73,15 @@ export const SettingsDevelopers = () => {
|
|||||||
title={t`Create Webhook`}
|
title={t`Create Webhook`}
|
||||||
size="small"
|
size="small"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={createNewWebhook}
|
to={getSettingsPath(
|
||||||
|
SettingsPath.DevelopersNewWebhookDetail,
|
||||||
|
{
|
||||||
|
webhookId: v4(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
creationMode: true,
|
||||||
|
},
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</StyledButtonContainer>
|
</StyledButtonContainer>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams, useSearchParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
H2Title,
|
H2Title,
|
||||||
@ -33,6 +33,7 @@ import { useRecoilValue } from 'recoil';
|
|||||||
import { FeatureFlagKey } from '~/generated/graphql';
|
import { FeatureFlagKey } from '~/generated/graphql';
|
||||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||||
import { useWebhookUpdateForm } from '@/settings/developers/hooks/useWebhookUpdateForm';
|
import { useWebhookUpdateForm } from '@/settings/developers/hooks/useWebhookUpdateForm';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
const OBJECT_DROPDOWN_WIDTH = 340;
|
const OBJECT_DROPDOWN_WIDTH = 340;
|
||||||
const ACTION_DROPDOWN_WIDTH = 140;
|
const ACTION_DROPDOWN_WIDTH = 140;
|
||||||
@ -68,8 +69,13 @@ export const SettingsDevelopersWebhooksDetail = () => {
|
|||||||
|
|
||||||
const { webhookId = '' } = useParams();
|
const { webhookId = '' } = useParams();
|
||||||
|
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const isCreationMode = isDefined(searchParams.get('creationMode'));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
formData,
|
formData,
|
||||||
|
title,
|
||||||
loading,
|
loading,
|
||||||
isTargetUrlValid,
|
isTargetUrlValid,
|
||||||
updateWebhook,
|
updateWebhook,
|
||||||
@ -78,6 +84,7 @@ export const SettingsDevelopersWebhooksDetail = () => {
|
|||||||
deleteWebhook,
|
deleteWebhook,
|
||||||
} = useWebhookUpdateForm({
|
} = useWebhookUpdateForm({
|
||||||
webhookId,
|
webhookId,
|
||||||
|
isCreationMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isDeleteWebhookModalOpen, setIsDeleteWebhookModalOpen] =
|
const [isDeleteWebhookModalOpen, setIsDeleteWebhookModalOpen] =
|
||||||
@ -114,7 +121,7 @@ export const SettingsDevelopersWebhooksDetail = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer
|
<SubMenuTopBarContainer
|
||||||
title={formData.targetUrl}
|
title={title}
|
||||||
reserveTitleSpace
|
reserveTitleSpace
|
||||||
links={[
|
links={[
|
||||||
{
|
{
|
||||||
@ -220,7 +227,7 @@ export const SettingsDevelopersWebhooksDetail = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
{isAnalyticsEnabled && isAnalyticsV2Enabled && (
|
{!isCreationMode && isAnalyticsEnabled && isAnalyticsV2Enabled && (
|
||||||
<AnalyticsGraphDataInstanceContext.Provider
|
<AnalyticsGraphDataInstanceContext.Provider
|
||||||
value={{ instanceId: `webhook-${webhookId}-analytics` }}
|
value={{ instanceId: `webhook-${webhookId}-analytics` }}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
import { getAbsoluteUrl } from '~/utils/url/getAbsoluteUrl';
|
import { getAbsoluteUrl } from '~/utils/url/getAbsoluteUrl';
|
||||||
|
|
||||||
export const getUrlHostname = (url: string) => {
|
export const getUrlHostname = (
|
||||||
|
url: string,
|
||||||
|
options?: { keepPath?: boolean },
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const absoluteUrl = getAbsoluteUrl(url);
|
const parsedUrl = new URL(getAbsoluteUrl(url));
|
||||||
return new URL(absoluteUrl).hostname.replace(/^www\./i, '');
|
return `${parsedUrl.hostname.replace(/^www\./i, '')}${options?.keepPath && parsedUrl.pathname !== '/' ? parsedUrl.pathname : ''}`;
|
||||||
} catch {
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -88,40 +88,65 @@ function assertCronTriggerSettingsAreValid(settings: any) {
|
|||||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (settings.type === 'CUSTOM' && !settings.pattern) {
|
switch (settings.type) {
|
||||||
throw new WorkflowTriggerException(
|
case 'CUSTOM': {
|
||||||
'No pattern provided in CUSTOM cron trigger',
|
if (!settings.pattern) {
|
||||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
throw new WorkflowTriggerException(
|
||||||
);
|
'No pattern provided in CUSTOM cron trigger',
|
||||||
}
|
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||||
if (!settings.schedule) {
|
);
|
||||||
throw new WorkflowTriggerException(
|
}
|
||||||
'No schedule provided in cron trigger',
|
|
||||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (settings.type === 'HOURS' && settings.schedule.hour <= 0) {
|
|
||||||
throw new WorkflowTriggerException(
|
|
||||||
'Invalid hour value. Should be integer greater than 1',
|
|
||||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
return;
|
||||||
settings.type === 'HOURS' &&
|
}
|
||||||
(settings.schedule.minute < 0 || settings.schedule.minute > 59)
|
|
||||||
) {
|
|
||||||
throw new WorkflowTriggerException(
|
|
||||||
'Invalid minute value. Should be integer between 0 and 59',
|
|
||||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.type === 'MINUTES' && settings.schedule.minute <= 0) {
|
case 'HOURS': {
|
||||||
throw new WorkflowTriggerException(
|
if (!settings.schedule) {
|
||||||
'Invalid minute value. Should be integer greater than 1',
|
throw new WorkflowTriggerException(
|
||||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
'No schedule provided in cron trigger',
|
||||||
);
|
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (settings.schedule.hour <= 0) {
|
||||||
|
throw new WorkflowTriggerException(
|
||||||
|
'Invalid hour value. Should be integer greater than 1',
|
||||||
|
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.schedule.minute < 0 || settings.schedule.minute > 59) {
|
||||||
|
throw new WorkflowTriggerException(
|
||||||
|
'Invalid minute value. Should be integer between 0 and 59',
|
||||||
|
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'MINUTES': {
|
||||||
|
if (!settings.schedule) {
|
||||||
|
throw new WorkflowTriggerException(
|
||||||
|
'No schedule provided in cron trigger',
|
||||||
|
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.schedule.minute <= 0) {
|
||||||
|
throw new WorkflowTriggerException(
|
||||||
|
'Invalid minute value. Should be integer greater than 1',
|
||||||
|
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new WorkflowTriggerException(
|
||||||
|
'Invalid setting type provided in cron trigger',
|
||||||
|
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user