mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-22 19:41:53 +03:00
8726 workflow add a test button in workflow code step (#9016)
- add test button to workflow code step - add test tab to workflow code step https://github.com/user-attachments/assets/e180a827-7321-49a2-8026-88490c557da2 ![image](https://github.com/user-attachments/assets/cacbd756-de3f-4141-a84c-8e1853f6556b) ![image](https://github.com/user-attachments/assets/ee170d81-8a22-4178-bd6d-11a0e8c73365)
This commit is contained in:
parent
07aaf0801c
commit
b10d831371
2
nx.json
2
nx.json
@ -112,7 +112,7 @@
|
|||||||
"outputs": ["{projectRoot}/{options.output-dir}"],
|
"outputs": ["{projectRoot}/{options.output-dir}"],
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "{projectRoot}",
|
"cwd": "{projectRoot}",
|
||||||
"command": "VITE_DISABLE_ESLINT_CHECKER=true storybook build",
|
"command": "VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true storybook build",
|
||||||
"output-dir": "storybook-static",
|
"output-dir": "storybook-static",
|
||||||
"config-dir": ".storybook"
|
"config-dir": ".storybook"
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@
|
|||||||
"test": {},
|
"test": {},
|
||||||
"storybook:build": {
|
"storybook:build": {
|
||||||
"options": {
|
"options": {
|
||||||
"env": { "NODE_OPTIONS": "--max_old_space_size=7000" }
|
"env": { "NODE_OPTIONS": "--max_old_space_size=8000" }
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"docs": { "env": { "STORYBOOK_SCOPE": "ui-docs" } },
|
"docs": { "env": { "STORYBOOK_SCOPE": "ui-docs" } },
|
||||||
|
@ -1569,8 +1569,6 @@ export type UpdateServerlessFunctionInput = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateWorkflowVersionStepInput = {
|
export type UpdateWorkflowVersionStepInput = {
|
||||||
/** Boolean to check if we need to update stepOutput */
|
|
||||||
shouldUpdateStepOutput?: InputMaybe<Scalars['Boolean']['input']>;
|
|
||||||
/** Step to update in JSON format */
|
/** Step to update in JSON format */
|
||||||
step: Scalars['JSON']['input'];
|
step: Scalars['JSON']['input'];
|
||||||
/** Workflow version ID */
|
/** Workflow version ID */
|
||||||
|
@ -1277,8 +1277,6 @@ export type UpdateServerlessFunctionInput = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateWorkflowVersionStepInput = {
|
export type UpdateWorkflowVersionStepInput = {
|
||||||
/** Boolean to check if we need to update stepOutput */
|
|
||||||
shouldUpdateStepOutput?: InputMaybe<Scalars['Boolean']>;
|
|
||||||
/** Step to update in JSON format */
|
/** Step to update in JSON format */
|
||||||
step: Scalars['JSON'];
|
step: Scalars['JSON'];
|
||||||
/** Workflow version ID */
|
/** Workflow version ID */
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
|
||||||
|
|
||||||
export const usePreventOverlapCallback = (
|
|
||||||
callback: () => Promise<void>,
|
|
||||||
wait?: number,
|
|
||||||
) => {
|
|
||||||
const [isRunning, setIsRunning] = useState(false);
|
|
||||||
const [pendingRun, setPendingRun] = useState(false);
|
|
||||||
|
|
||||||
const handleCallback = async () => {
|
|
||||||
if (isRunning) {
|
|
||||||
setPendingRun(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsRunning(true);
|
|
||||||
try {
|
|
||||||
await callback();
|
|
||||||
} finally {
|
|
||||||
setIsRunning(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isRunning && pendingRun) {
|
|
||||||
setPendingRun(false);
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}, [callback, isRunning, pendingRun, setPendingRun]);
|
|
||||||
|
|
||||||
return useDebouncedCallback(handleCallback, wait);
|
|
||||||
};
|
|
@ -0,0 +1,30 @@
|
|||||||
|
import { Button } from 'twenty-ui';
|
||||||
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
import { Key } from 'ts-key-enum';
|
||||||
|
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||||
|
|
||||||
|
export const CmdEnterActionButton = ({
|
||||||
|
title,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}) => {
|
||||||
|
useScopedHotkeys(
|
||||||
|
[`${Key.Control}+${Key.Enter}`, `${Key.Meta}+${Key.Enter}`],
|
||||||
|
() => onClick(),
|
||||||
|
RightDrawerHotkeyScope.RightDrawer,
|
||||||
|
[onClick],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
title={title}
|
||||||
|
variant="primary"
|
||||||
|
accent="blue"
|
||||||
|
size="medium"
|
||||||
|
onClick={onClick}
|
||||||
|
shortcut={'⌘⏎'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -1,5 +1,4 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
import {
|
import {
|
||||||
AnimatedPlaceholder,
|
AnimatedPlaceholder,
|
||||||
AnimatedPlaceholderEmptyContainer,
|
AnimatedPlaceholderEmptyContainer,
|
||||||
@ -43,8 +42,7 @@ export const TaskGroups = ({ targetableObjects }: TaskGroupsProps) => {
|
|||||||
activityObjectNameSingular: CoreObjectNameSingular.Task,
|
activityObjectNameSingular: CoreObjectNameSingular.Task,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { activeTabIdState } = useTabList(TASKS_TAB_LIST_COMPONENT_ID);
|
const { activeTabId } = useTabList(TASKS_TAB_LIST_COMPONENT_ID);
|
||||||
const activeTabId = useRecoilValue(activeTabIdState);
|
|
||||||
|
|
||||||
const isLoading =
|
const isLoading =
|
||||||
(activeTabId !== 'done' && tasksLoading) ||
|
(activeTabId !== 'done' && tasksLoading) ||
|
||||||
|
@ -83,11 +83,11 @@ const SettingsServerlessFunctions = lazy(() =>
|
|||||||
).then((module) => ({ default: module.SettingsServerlessFunctions })),
|
).then((module) => ({ default: module.SettingsServerlessFunctions })),
|
||||||
);
|
);
|
||||||
|
|
||||||
const SettingsServerlessFunctionDetailWrapper = lazy(() =>
|
const SettingsServerlessFunctionDetail = lazy(() =>
|
||||||
import(
|
import(
|
||||||
'~/pages/settings/serverless-functions/SettingsServerlessFunctionDetailWrapper'
|
'~/pages/settings/serverless-functions/SettingsServerlessFunctionDetail'
|
||||||
).then((module) => ({
|
).then((module) => ({
|
||||||
default: module.SettingsServerlessFunctionDetailWrapper,
|
default: module.SettingsServerlessFunctionDetail,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -353,7 +353,7 @@ export const SettingsRoutes = ({
|
|||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={SettingsPath.ServerlessFunctionDetail}
|
path={SettingsPath.ServerlessFunctionDetail}
|
||||||
element={<SettingsServerlessFunctionDetailWrapper />}
|
element={<SettingsServerlessFunctionDetail />}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { FormCountrySelectInput } from '@/object-record/record-field/form-types/components/FormCountrySelectInput';
|
import { FormCountrySelectInput } from '@/object-record/record-field/form-types/components/FormCountrySelectInput';
|
||||||
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
||||||
import { StyledFormCompositeFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormCompositeFieldInputContainer';
|
import { FormNestedFieldInputContainer } from '@/object-record/record-field/form-types/components/FormNestedFieldInputContainer';
|
||||||
import { StyledFormFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputContainer';
|
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
|
||||||
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
||||||
import { FieldAddressDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
|
import { FieldAddressDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
|
||||||
import { FieldAddressValue } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldAddressValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
@ -39,9 +39,9 @@ export const FormAddressFieldInput = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledFormFieldInputContainer>
|
<FormFieldInputContainer>
|
||||||
{label ? <InputLabel>{label}</InputLabel> : null}
|
{label ? <InputLabel>{label}</InputLabel> : null}
|
||||||
<StyledFormCompositeFieldInputContainer>
|
<FormNestedFieldInputContainer>
|
||||||
<FormTextFieldInput
|
<FormTextFieldInput
|
||||||
label="Address 1"
|
label="Address 1"
|
||||||
defaultValue={defaultValue?.addressStreet1 ?? ''}
|
defaultValue={defaultValue?.addressStreet1 ?? ''}
|
||||||
@ -88,7 +88,7 @@ export const FormAddressFieldInput = ({
|
|||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
VariablePicker={VariablePicker}
|
VariablePicker={VariablePicker}
|
||||||
/>
|
/>
|
||||||
</StyledFormCompositeFieldInputContainer>
|
</FormNestedFieldInputContainer>
|
||||||
</StyledFormFieldInputContainer>
|
</FormFieldInputContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { StyledFormFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputContainer';
|
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
|
||||||
import { StyledFormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputInputContainer';
|
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
|
||||||
import { StyledFormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputRowContainer';
|
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
|
||||||
import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip';
|
import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip';
|
||||||
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
||||||
import { BooleanInput } from '@/ui/field/input/components/BooleanInput';
|
import { BooleanInput } from '@/ui/field/input/components/BooleanInput';
|
||||||
@ -80,11 +80,11 @@ export const FormBooleanFieldInput = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledFormFieldInputContainer>
|
<FormFieldInputContainer>
|
||||||
{label ? <InputLabel>{label}</InputLabel> : null}
|
{label ? <InputLabel>{label}</InputLabel> : null}
|
||||||
|
|
||||||
<StyledFormFieldInputRowContainer>
|
<FormFieldInputRowContainer>
|
||||||
<StyledFormFieldInputInputContainer
|
<FormFieldInputInputContainer
|
||||||
hasRightElement={isDefined(VariablePicker)}
|
hasRightElement={isDefined(VariablePicker)}
|
||||||
>
|
>
|
||||||
{draftValue.type === 'static' ? (
|
{draftValue.type === 'static' ? (
|
||||||
@ -101,7 +101,7 @@ export const FormBooleanFieldInput = ({
|
|||||||
onRemove={handleUnlinkVariable}
|
onRemove={handleUnlinkVariable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</StyledFormFieldInputInputContainer>
|
</FormFieldInputInputContainer>
|
||||||
|
|
||||||
{VariablePicker ? (
|
{VariablePicker ? (
|
||||||
<VariablePicker
|
<VariablePicker
|
||||||
@ -109,7 +109,7 @@ export const FormBooleanFieldInput = ({
|
|||||||
onVariableSelect={handleVariableTagInsert}
|
onVariableSelect={handleVariableTagInsert}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</StyledFormFieldInputRowContainer>
|
</FormFieldInputRowContainer>
|
||||||
</StyledFormFieldInputContainer>
|
</FormFieldInputContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
const StyledFormFieldInputContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FormFieldInputContainer = StyledFormFieldInputContainer;
|
@ -1,7 +1,7 @@
|
|||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const StyledFormFieldInputInputContainer = styled.div<{
|
const StyledFormFieldInputInputContainer = styled.div<{
|
||||||
hasRightElement: boolean;
|
hasRightElement: boolean;
|
||||||
multiline?: boolean;
|
multiline?: boolean;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
@ -29,3 +29,5 @@ export const StyledFormFieldInputInputContainer = styled.div<{
|
|||||||
overflow: ${({ multiline }) => (multiline ? 'auto' : 'hidden')};
|
overflow: ${({ multiline }) => (multiline ? 'auto' : 'hidden')};
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const FormFieldInputInputContainer = StyledFormFieldInputInputContainer;
|
@ -3,7 +3,7 @@ import styled from '@emotion/styled';
|
|||||||
|
|
||||||
export const LINE_HEIGHT = 24;
|
export const LINE_HEIGHT = 24;
|
||||||
|
|
||||||
export const StyledFormFieldInputRowContainer = styled.div<{
|
const StyledFormFieldInputRowContainer = styled.div<{
|
||||||
multiline?: boolean;
|
multiline?: boolean;
|
||||||
}>`
|
}>`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -21,3 +21,5 @@ export const StyledFormFieldInputRowContainer = styled.div<{
|
|||||||
height: 32px;
|
height: 32px;
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const FormFieldInputRowContainer = StyledFormFieldInputRowContainer;
|
@ -1,6 +1,6 @@
|
|||||||
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
||||||
import { StyledFormCompositeFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormCompositeFieldInputContainer';
|
import { FormNestedFieldInputContainer } from '@/object-record/record-field/form-types/components/FormNestedFieldInputContainer';
|
||||||
import { StyledFormFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputContainer';
|
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
|
||||||
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
||||||
import { FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/FirstNamePlaceholder';
|
import { FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/FirstNamePlaceholder';
|
||||||
import { LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/LastNamePlaceholder';
|
import { LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/LastNamePlaceholder';
|
||||||
@ -37,9 +37,9 @@ export const FormFullNameFieldInput = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledFormFieldInputContainer>
|
<FormFieldInputContainer>
|
||||||
{label ? <InputLabel>{label}</InputLabel> : null}
|
{label ? <InputLabel>{label}</InputLabel> : null}
|
||||||
<StyledFormCompositeFieldInputContainer>
|
<FormNestedFieldInputContainer>
|
||||||
<FormTextFieldInput
|
<FormTextFieldInput
|
||||||
label="First Name"
|
label="First Name"
|
||||||
defaultValue={defaultValue?.firstName}
|
defaultValue={defaultValue?.firstName}
|
||||||
@ -60,7 +60,7 @@ export const FormFullNameFieldInput = ({
|
|||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
VariablePicker={VariablePicker}
|
VariablePicker={VariablePicker}
|
||||||
/>
|
/>
|
||||||
</StyledFormCompositeFieldInputContainer>
|
</FormNestedFieldInputContainer>
|
||||||
</StyledFormFieldInputContainer>
|
</FormFieldInputContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const StyledFormCompositeFieldInputContainer = styled.div`
|
const StyledFormNestedFieldInputContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: ${({ theme }) => theme.background.secondary};
|
background: ${({ theme }) => theme.background.secondary};
|
||||||
@ -9,3 +9,6 @@ export const StyledFormCompositeFieldInputContainer = styled.div`
|
|||||||
gap: ${({ theme }) => theme.spacing(2)};
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
padding: ${({ theme }) => theme.spacing(2)};
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const FormNestedFieldInputContainer =
|
||||||
|
StyledFormNestedFieldInputContainer;
|
@ -1,6 +1,6 @@
|
|||||||
import { StyledFormFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputContainer';
|
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
|
||||||
import { StyledFormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputInputContainer';
|
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
|
||||||
import { StyledFormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputRowContainer';
|
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
|
||||||
import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip';
|
import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip';
|
||||||
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
||||||
import { TextInput } from '@/ui/field/input/components/TextInput';
|
import { TextInput } from '@/ui/field/input/components/TextInput';
|
||||||
@ -94,11 +94,11 @@ export const FormNumberFieldInput = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledFormFieldInputContainer>
|
<FormFieldInputContainer>
|
||||||
{label ? <InputLabel htmlFor={inputId}>{label}</InputLabel> : null}
|
{label ? <InputLabel htmlFor={inputId}>{label}</InputLabel> : null}
|
||||||
|
|
||||||
<StyledFormFieldInputRowContainer>
|
<FormFieldInputRowContainer>
|
||||||
<StyledFormFieldInputInputContainer
|
<FormFieldInputInputContainer
|
||||||
hasRightElement={isDefined(VariablePicker)}
|
hasRightElement={isDefined(VariablePicker)}
|
||||||
>
|
>
|
||||||
{draftValue.type === 'static' ? (
|
{draftValue.type === 'static' ? (
|
||||||
@ -116,7 +116,7 @@ export const FormNumberFieldInput = ({
|
|||||||
onRemove={handleUnlinkVariable}
|
onRemove={handleUnlinkVariable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</StyledFormFieldInputInputContainer>
|
</FormFieldInputInputContainer>
|
||||||
|
|
||||||
{VariablePicker ? (
|
{VariablePicker ? (
|
||||||
<VariablePicker
|
<VariablePicker
|
||||||
@ -124,7 +124,7 @@ export const FormNumberFieldInput = ({
|
|||||||
onVariableSelect={handleVariableTagInsert}
|
onVariableSelect={handleVariableTagInsert}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</StyledFormFieldInputRowContainer>
|
</FormFieldInputRowContainer>
|
||||||
</StyledFormFieldInputContainer>
|
</FormFieldInputContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { StyledFormFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputContainer';
|
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
|
||||||
import { StyledFormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputInputContainer';
|
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
|
||||||
import { StyledFormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputRowContainer';
|
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
|
||||||
import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip';
|
import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip';
|
||||||
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
||||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||||
@ -204,11 +204,11 @@ export const FormSelectFieldInput = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledFormFieldInputContainer>
|
<FormFieldInputContainer>
|
||||||
{label ? <InputLabel>{label}</InputLabel> : null}
|
{label ? <InputLabel>{label}</InputLabel> : null}
|
||||||
|
|
||||||
<StyledFormFieldInputRowContainer>
|
<FormFieldInputRowContainer>
|
||||||
<StyledFormFieldInputInputContainer
|
<FormFieldInputInputContainer
|
||||||
hasRightElement={isDefined(VariablePicker)}
|
hasRightElement={isDefined(VariablePicker)}
|
||||||
>
|
>
|
||||||
{draftValue.type === 'static' ? (
|
{draftValue.type === 'static' ? (
|
||||||
@ -234,7 +234,7 @@ export const FormSelectFieldInput = ({
|
|||||||
onRemove={handleUnlinkVariable}
|
onRemove={handleUnlinkVariable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</StyledFormFieldInputInputContainer>
|
</FormFieldInputInputContainer>
|
||||||
<StyledSelectInputContainer>
|
<StyledSelectInputContainer>
|
||||||
{draftValue.type === 'static' &&
|
{draftValue.type === 'static' &&
|
||||||
draftValue.editingMode === 'edit' && (
|
draftValue.editingMode === 'edit' && (
|
||||||
@ -260,7 +260,7 @@ export const FormSelectFieldInput = ({
|
|||||||
onVariableSelect={handleVariableTagInsert}
|
onVariableSelect={handleVariableTagInsert}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</StyledFormFieldInputRowContainer>
|
</FormFieldInputRowContainer>
|
||||||
</StyledFormFieldInputContainer>
|
</FormFieldInputContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { StyledFormFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputContainer';
|
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
|
||||||
import { StyledFormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputInputContainer';
|
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
|
||||||
import { StyledFormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputRowContainer';
|
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
|
||||||
import { TextVariableEditor } from '@/object-record/record-field/form-types/components/TextVariableEditor';
|
import { TextVariableEditor } from '@/object-record/record-field/form-types/components/TextVariableEditor';
|
||||||
import { useTextVariableEditor } from '@/object-record/record-field/form-types/hooks/useTextVariableEditor';
|
import { useTextVariableEditor } from '@/object-record/record-field/form-types/hooks/useTextVariableEditor';
|
||||||
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
||||||
@ -58,11 +58,11 @@ export const FormTextFieldInput = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledFormFieldInputContainer>
|
<FormFieldInputContainer>
|
||||||
{label ? <InputLabel>{label}</InputLabel> : null}
|
{label ? <InputLabel>{label}</InputLabel> : null}
|
||||||
|
|
||||||
<StyledFormFieldInputRowContainer multiline={multiline}>
|
<FormFieldInputRowContainer multiline={multiline}>
|
||||||
<StyledFormFieldInputInputContainer
|
<FormFieldInputInputContainer
|
||||||
hasRightElement={isDefined(VariablePicker)}
|
hasRightElement={isDefined(VariablePicker)}
|
||||||
multiline={multiline}
|
multiline={multiline}
|
||||||
>
|
>
|
||||||
@ -71,7 +71,7 @@ export const FormTextFieldInput = ({
|
|||||||
multiline={multiline}
|
multiline={multiline}
|
||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
/>
|
/>
|
||||||
</StyledFormFieldInputInputContainer>
|
</FormFieldInputInputContainer>
|
||||||
|
|
||||||
{VariablePicker ? (
|
{VariablePicker ? (
|
||||||
<VariablePicker
|
<VariablePicker
|
||||||
@ -80,7 +80,7 @@ export const FormTextFieldInput = ({
|
|||||||
onVariableSelect={handleVariableTagInsert}
|
onVariableSelect={handleVariableTagInsert}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</StyledFormFieldInputRowContainer>
|
</FormFieldInputRowContainer>
|
||||||
</StyledFormFieldInputContainer>
|
</FormFieldInputContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
export const StyledFormFieldInputContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
@ -58,11 +58,13 @@ export const useTextVariableEditor = ({
|
|||||||
const { tr } = state;
|
const { tr } = state;
|
||||||
|
|
||||||
// Insert hard break using the view's state and dispatch
|
// Insert hard break using the view's state and dispatch
|
||||||
const transaction = tr.replaceSelectionWith(
|
if (multiline === true) {
|
||||||
state.schema.nodes.hardBreak.create(),
|
const transaction = tr.replaceSelectionWith(
|
||||||
);
|
state.schema.nodes.hardBreak.create(),
|
||||||
|
);
|
||||||
|
|
||||||
view.dispatch(transaction);
|
view.dispatch(transaction);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,73 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import { ServerlessFunctionExecutionStatus } from '~/generated-metadata/graphql';
|
||||||
|
import {
|
||||||
|
CodeEditor,
|
||||||
|
CoreEditorHeader,
|
||||||
|
IconSquareRoundedCheck,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton';
|
||||||
|
import {
|
||||||
|
DEFAULT_OUTPUT_VALUE,
|
||||||
|
ServerlessFunctionTestData,
|
||||||
|
} from '@/workflow/states/serverlessFunctionTestDataFamilyState';
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledOutput = styled.div<{ status?: ServerlessFunctionExecutionStatus }>`
|
||||||
|
align-items: center;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
color: ${({ theme, status }) =>
|
||||||
|
status === ServerlessFunctionExecutionStatus.Success
|
||||||
|
? theme.color.turquoise
|
||||||
|
: theme.color.red};
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ServerlessFunctionExecutionResult = ({
|
||||||
|
serverlessFunctionTestData,
|
||||||
|
}: {
|
||||||
|
serverlessFunctionTestData: ServerlessFunctionTestData;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const result =
|
||||||
|
serverlessFunctionTestData.output.data ||
|
||||||
|
serverlessFunctionTestData.output.error ||
|
||||||
|
'';
|
||||||
|
|
||||||
|
const leftNode =
|
||||||
|
serverlessFunctionTestData.output.data === DEFAULT_OUTPUT_VALUE ? (
|
||||||
|
'Output'
|
||||||
|
) : (
|
||||||
|
<StyledOutput status={serverlessFunctionTestData.output.status}>
|
||||||
|
<IconSquareRoundedCheck size={theme.icon.size.md} />
|
||||||
|
{serverlessFunctionTestData.output.status ===
|
||||||
|
ServerlessFunctionExecutionStatus.Success
|
||||||
|
? '200 OK'
|
||||||
|
: '500 Error'}
|
||||||
|
{' - '}
|
||||||
|
{serverlessFunctionTestData.output.duration}ms
|
||||||
|
</StyledOutput>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<CoreEditorHeader
|
||||||
|
leftNodes={[leftNode]}
|
||||||
|
rightNodes={[<LightCopyIconButton copyText={result} />]}
|
||||||
|
/>
|
||||||
|
<CodeEditor
|
||||||
|
value={result}
|
||||||
|
language={serverlessFunctionTestData.language}
|
||||||
|
height={serverlessFunctionTestData.height}
|
||||||
|
options={{ readOnly: true, domReadOnly: true }}
|
||||||
|
withHeader
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export const INDEX_FILE_PATH = 'src/index.ts';
|
@ -0,0 +1,51 @@
|
|||||||
|
import { useExecuteOneServerlessFunction } from '@/settings/serverless-functions/hooks/useExecuteOneServerlessFunction';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const useTestServerlessFunction = (
|
||||||
|
serverlessFunctionId: string,
|
||||||
|
callback?: (testResult: object) => void,
|
||||||
|
) => {
|
||||||
|
const { executeOneServerlessFunction } = useExecuteOneServerlessFunction();
|
||||||
|
const [serverlessFunctionTestData, setServerlessFunctionTestData] =
|
||||||
|
useRecoilState(serverlessFunctionTestDataFamilyState(serverlessFunctionId));
|
||||||
|
|
||||||
|
const testServerlessFunction = async () => {
|
||||||
|
const result = await executeOneServerlessFunction({
|
||||||
|
id: serverlessFunctionId,
|
||||||
|
payload: serverlessFunctionTestData.input,
|
||||||
|
version: 'draft',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isDefined(result?.data?.executeOneServerlessFunction?.data)) {
|
||||||
|
callback?.(result?.data?.executeOneServerlessFunction?.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
setServerlessFunctionTestData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
language: 'json',
|
||||||
|
height: 300,
|
||||||
|
output: {
|
||||||
|
data: result?.data?.executeOneServerlessFunction?.data
|
||||||
|
? JSON.stringify(
|
||||||
|
result?.data?.executeOneServerlessFunction?.data,
|
||||||
|
null,
|
||||||
|
4,
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
duration: result?.data?.executeOneServerlessFunction?.duration,
|
||||||
|
status: result?.data?.executeOneServerlessFunction?.status,
|
||||||
|
error: result?.data?.executeOneServerlessFunction?.error
|
||||||
|
? JSON.stringify(
|
||||||
|
result?.data?.executeOneServerlessFunction?.error,
|
||||||
|
null,
|
||||||
|
4,
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return { testServerlessFunction };
|
||||||
|
};
|
@ -0,0 +1,44 @@
|
|||||||
|
import { InputSchema } from '@/workflow/types/InputSchema';
|
||||||
|
import { getDefaultFunctionInputFromInputSchema } from '@/serverless-functions/utils/getDefaultFunctionInputFromInputSchema';
|
||||||
|
|
||||||
|
describe('getDefaultFunctionInputFromInputSchema', () => {
|
||||||
|
it('should init function input properly', () => {
|
||||||
|
const inputSchema = [
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
a: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
c: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
},
|
||||||
|
d: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
da: { type: 'string', enum: ['my', 'enum'] },
|
||||||
|
db: { type: 'number' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
e: { type: 'object' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as InputSchema;
|
||||||
|
const expectedResult = [
|
||||||
|
{
|
||||||
|
a: null,
|
||||||
|
b: null,
|
||||||
|
c: [],
|
||||||
|
d: { da: 'my', db: null },
|
||||||
|
e: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(getDefaultFunctionInputFromInputSchema(inputSchema)).toEqual(
|
||||||
|
expectedResult,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,50 @@
|
|||||||
|
import { getFunctionInputFromSourceCode } from '@/serverless-functions/utils/getFunctionInputFromSourceCode';
|
||||||
|
|
||||||
|
describe('getFunctionInputFromSourceCode', () => {
|
||||||
|
it('should return empty input if not parameter', () => {
|
||||||
|
const fileContent = 'function testFunction() { return }';
|
||||||
|
const result = getFunctionInputFromSourceCode(fileContent);
|
||||||
|
expect(result).toEqual({});
|
||||||
|
});
|
||||||
|
it('should return first input if multiple parameters', () => {
|
||||||
|
const fileContent =
|
||||||
|
'function testFunction(params1: {}, params2: {}) { return }';
|
||||||
|
const result = getFunctionInputFromSourceCode(fileContent);
|
||||||
|
expect(result).toEqual({});
|
||||||
|
});
|
||||||
|
it('should return empty input if wrong parameter', () => {
|
||||||
|
const fileContent = 'function testFunction(params: string) { return }';
|
||||||
|
const result = getFunctionInputFromSourceCode(fileContent);
|
||||||
|
expect(result).toEqual({});
|
||||||
|
});
|
||||||
|
it('should return input from source code', () => {
|
||||||
|
const fileContent = `
|
||||||
|
function testFunction(
|
||||||
|
params: {
|
||||||
|
param1: string;
|
||||||
|
param2: number;
|
||||||
|
param3: boolean;
|
||||||
|
param4: object;
|
||||||
|
param5: { subParam1: string };
|
||||||
|
param6: "my" | "enum";
|
||||||
|
param7: string[];
|
||||||
|
}
|
||||||
|
): void {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = getFunctionInputFromSourceCode(fileContent);
|
||||||
|
expect(result).toEqual({
|
||||||
|
param1: null,
|
||||||
|
param2: null,
|
||||||
|
param3: null,
|
||||||
|
param4: {},
|
||||||
|
param5: {
|
||||||
|
subParam1: null,
|
||||||
|
},
|
||||||
|
param6: 'my',
|
||||||
|
param7: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,67 @@
|
|||||||
|
import { getFunctionInputSchema } from '@/serverless-functions/utils/getFunctionInputSchema';
|
||||||
|
|
||||||
|
describe('getFunctionInputSchema', () => {
|
||||||
|
it('should analyze a simple function correctly', () => {
|
||||||
|
const fileContent = `
|
||||||
|
function testFunction(param1: string, param2: number): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const result = getFunctionInputSchema(fileContent);
|
||||||
|
|
||||||
|
expect(result).toEqual([{ type: 'string' }, { type: 'number' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should analyze a arrow function correctly', () => {
|
||||||
|
const fileContent = `
|
||||||
|
export const main = async (
|
||||||
|
param1: string,
|
||||||
|
param2: number,
|
||||||
|
): Promise<object> => {
|
||||||
|
return param1;
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
const result = getFunctionInputSchema(fileContent);
|
||||||
|
|
||||||
|
expect(result).toEqual([{ type: 'string' }, { type: 'number' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should analyze a complex function correctly', () => {
|
||||||
|
const fileContent = `
|
||||||
|
function testFunction(
|
||||||
|
params: {
|
||||||
|
param1: string;
|
||||||
|
param2: number;
|
||||||
|
param3: boolean;
|
||||||
|
param4: object;
|
||||||
|
param5: { subParam1: string };
|
||||||
|
param6: "my" | "enum";
|
||||||
|
param7: string[];
|
||||||
|
}
|
||||||
|
): void {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const result = getFunctionInputSchema(fileContent);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
param1: { type: 'string' },
|
||||||
|
param2: { type: 'number' },
|
||||||
|
param3: { type: 'boolean' },
|
||||||
|
param4: { type: 'object' },
|
||||||
|
param5: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
subParam1: { type: 'string' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
param6: { type: 'string', enum: ['my', 'enum'] },
|
||||||
|
param7: { type: 'array', items: { type: 'string' } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,24 @@
|
|||||||
|
import { getFunctionOutputSchema } from '@/serverless-functions/utils/getFunctionOutputSchema';
|
||||||
|
|
||||||
|
describe('getFunctionOutputSchema', () => {
|
||||||
|
it('should compute outputSchema properly', () => {
|
||||||
|
const testResult = {
|
||||||
|
a: null,
|
||||||
|
b: 'b',
|
||||||
|
c: { cc: 1 },
|
||||||
|
d: true,
|
||||||
|
e: [1, 2, 3],
|
||||||
|
};
|
||||||
|
const expectedOutputSchema = {
|
||||||
|
a: { isLeaf: true, type: 'unknown', value: null },
|
||||||
|
b: { isLeaf: true, type: 'string', value: 'b' },
|
||||||
|
c: {
|
||||||
|
isLeaf: false,
|
||||||
|
value: { cc: { isLeaf: true, type: 'number', value: 1 } },
|
||||||
|
},
|
||||||
|
d: { isLeaf: true, type: 'boolean', value: true },
|
||||||
|
e: { isLeaf: true, type: 'array', value: [1, 2, 3] },
|
||||||
|
};
|
||||||
|
expect(getFunctionOutputSchema(testResult)).toEqual(expectedOutputSchema);
|
||||||
|
});
|
||||||
|
});
|
@ -2,19 +2,25 @@ import { mergeDefaultFunctionInputAndFunctionInput } from '../mergeDefaultFuncti
|
|||||||
|
|
||||||
describe('mergeDefaultFunctionInputAndFunctionInput', () => {
|
describe('mergeDefaultFunctionInputAndFunctionInput', () => {
|
||||||
it('should merge properly', () => {
|
it('should merge properly', () => {
|
||||||
const defaultFunctionInput = {
|
const newInput = {
|
||||||
params: { a: null, b: null, c: { cc: null } },
|
a: null,
|
||||||
};
|
b: null,
|
||||||
const functionInput = {
|
c: { cc: null },
|
||||||
params: { a: 'a', c: 'c' },
|
d: null,
|
||||||
|
e: { ee: null },
|
||||||
};
|
};
|
||||||
|
const oldInput = { a: 'a', c: 'c', d: { da: null }, e: { ee: 'ee' } };
|
||||||
const expectedResult = {
|
const expectedResult = {
|
||||||
params: { a: 'a', b: null, c: { cc: null } },
|
a: 'a',
|
||||||
|
b: null,
|
||||||
|
c: { cc: null },
|
||||||
|
d: null,
|
||||||
|
e: { ee: 'ee' },
|
||||||
};
|
};
|
||||||
expect(
|
expect(
|
||||||
mergeDefaultFunctionInputAndFunctionInput({
|
mergeDefaultFunctionInputAndFunctionInput({
|
||||||
defaultFunctionInput,
|
newInput: newInput,
|
||||||
functionInput,
|
oldInput: oldInput,
|
||||||
}),
|
}),
|
||||||
).toEqual(expectedResult);
|
).toEqual(expectedResult);
|
||||||
});
|
});
|
@ -0,0 +1,24 @@
|
|||||||
|
import { InputSchema } from '@/workflow/types/InputSchema';
|
||||||
|
import { FunctionInput } from '@/workflow/types/FunctionInput';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
export const getDefaultFunctionInputFromInputSchema = (
|
||||||
|
inputSchema: InputSchema,
|
||||||
|
): FunctionInput => {
|
||||||
|
return inputSchema.map((param) => {
|
||||||
|
if (['string', 'number', 'boolean'].includes(param.type)) {
|
||||||
|
return param.enum && param.enum.length > 0 ? param.enum[0] : null;
|
||||||
|
} else if (param.type === 'object') {
|
||||||
|
const result: FunctionInput = {};
|
||||||
|
if (isDefined(param.properties)) {
|
||||||
|
Object.entries(param.properties).forEach(([key, val]) => {
|
||||||
|
result[key] = getDefaultFunctionInputFromInputSchema([val])[0];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else if (param.type === 'array' && isDefined(param.items)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,23 @@
|
|||||||
|
import { getDefaultFunctionInputFromInputSchema } from '@/serverless-functions/utils/getDefaultFunctionInputFromInputSchema';
|
||||||
|
import { getFunctionInputSchema } from '@/serverless-functions/utils/getFunctionInputSchema';
|
||||||
|
import { FunctionInput } from '@/workflow/types/FunctionInput';
|
||||||
|
import { isObject } from '@sniptt/guards';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const getFunctionInputFromSourceCode = (
|
||||||
|
sourceCode?: string,
|
||||||
|
): FunctionInput => {
|
||||||
|
if (!isDefined(sourceCode)) {
|
||||||
|
throw new Error('Source code is not defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
const functionInputSchema = getFunctionInputSchema(sourceCode);
|
||||||
|
|
||||||
|
const result = getDefaultFunctionInputFromInputSchema(functionInputSchema)[0];
|
||||||
|
|
||||||
|
if (!isObject(result)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
@ -3,12 +3,14 @@ import {
|
|||||||
ArrowFunction,
|
ArrowFunction,
|
||||||
createSourceFile,
|
createSourceFile,
|
||||||
FunctionDeclaration,
|
FunctionDeclaration,
|
||||||
|
FunctionLikeDeclaration,
|
||||||
LiteralTypeNode,
|
LiteralTypeNode,
|
||||||
PropertySignature,
|
PropertySignature,
|
||||||
ScriptTarget,
|
ScriptTarget,
|
||||||
StringLiteral,
|
StringLiteral,
|
||||||
SyntaxKind,
|
SyntaxKind,
|
||||||
TypeNode,
|
TypeNode,
|
||||||
|
Node,
|
||||||
UnionTypeNode,
|
UnionTypeNode,
|
||||||
VariableStatement,
|
VariableStatement,
|
||||||
} from 'typescript';
|
} from 'typescript';
|
||||||
@ -31,7 +33,7 @@ const getTypeString = (typeNode: TypeNode): InputSchemaProperty => {
|
|||||||
case SyntaxKind.ObjectKeyword:
|
case SyntaxKind.ObjectKeyword:
|
||||||
return { type: 'object' };
|
return { type: 'object' };
|
||||||
case SyntaxKind.TypeLiteral: {
|
case SyntaxKind.TypeLiteral: {
|
||||||
const properties: InputSchema = {};
|
const properties: InputSchemaProperty['properties'] = {};
|
||||||
|
|
||||||
(typeNode as any).members.forEach((member: PropertySignature) => {
|
(typeNode as any).members.forEach((member: PropertySignature) => {
|
||||||
if (isDefined(member.name) && isDefined(member.type)) {
|
if (isDefined(member.name) && isDefined(member.type)) {
|
||||||
@ -74,6 +76,42 @@ const getTypeString = (typeNode: TypeNode): InputSchemaProperty => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const computeFunctionParameters = (
|
||||||
|
funcNode: FunctionDeclaration | FunctionLikeDeclaration | ArrowFunction,
|
||||||
|
schema: InputSchema,
|
||||||
|
): InputSchema => {
|
||||||
|
const params = funcNode.parameters;
|
||||||
|
|
||||||
|
return params.reduce((updatedSchema, param) => {
|
||||||
|
const typeNode = param.type;
|
||||||
|
|
||||||
|
if (isDefined(typeNode)) {
|
||||||
|
return [...updatedSchema, getTypeString(typeNode)];
|
||||||
|
} else {
|
||||||
|
return [...updatedSchema, { type: 'unknown' }];
|
||||||
|
}
|
||||||
|
}, schema);
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractFunctions = (node: Node): FunctionLikeDeclaration[] => {
|
||||||
|
if (node.kind === SyntaxKind.FunctionDeclaration) {
|
||||||
|
return [node as FunctionDeclaration];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.kind === SyntaxKind.VariableStatement) {
|
||||||
|
const varStatement = node as VariableStatement;
|
||||||
|
return varStatement.declarationList.declarations
|
||||||
|
.filter(
|
||||||
|
(declaration) =>
|
||||||
|
isDefined(declaration.initializer) &&
|
||||||
|
declaration.initializer.kind === SyntaxKind.ArrowFunction,
|
||||||
|
)
|
||||||
|
.map((declaration) => declaration.initializer as ArrowFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
export const getFunctionInputSchema = (fileContent: string): InputSchema => {
|
export const getFunctionInputSchema = (fileContent: string): InputSchema => {
|
||||||
const sourceFile = createSourceFile(
|
const sourceFile = createSourceFile(
|
||||||
'temp.ts',
|
'temp.ts',
|
||||||
@ -81,46 +119,16 @@ export const getFunctionInputSchema = (fileContent: string): InputSchema => {
|
|||||||
ScriptTarget.ESNext,
|
ScriptTarget.ESNext,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
let schema: InputSchema = [];
|
||||||
const schema: InputSchema = {};
|
|
||||||
|
|
||||||
sourceFile.forEachChild((node) => {
|
sourceFile.forEachChild((node) => {
|
||||||
if (node.kind === SyntaxKind.FunctionDeclaration) {
|
if (
|
||||||
const funcNode = node as FunctionDeclaration;
|
node.kind === SyntaxKind.FunctionDeclaration ||
|
||||||
const params = funcNode.parameters;
|
node.kind === SyntaxKind.VariableStatement
|
||||||
|
) {
|
||||||
params.forEach((param) => {
|
const functions = extractFunctions(node);
|
||||||
const paramName = param.name.getText();
|
functions.forEach((func) => {
|
||||||
const typeNode = param.type;
|
schema = computeFunctionParameters(func, schema);
|
||||||
|
|
||||||
if (isDefined(typeNode)) {
|
|
||||||
schema[paramName] = getTypeString(typeNode);
|
|
||||||
} else {
|
|
||||||
schema[paramName] = { type: 'unknown' };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (node.kind === SyntaxKind.VariableStatement) {
|
|
||||||
const varStatement = node as VariableStatement;
|
|
||||||
|
|
||||||
varStatement.declarationList.declarations.forEach((declaration) => {
|
|
||||||
if (
|
|
||||||
isDefined(declaration.initializer) &&
|
|
||||||
declaration.initializer.kind === SyntaxKind.ArrowFunction
|
|
||||||
) {
|
|
||||||
const arrowFunction = declaration.initializer as ArrowFunction;
|
|
||||||
const params = arrowFunction.parameters;
|
|
||||||
|
|
||||||
params.forEach((param: any) => {
|
|
||||||
const paramName = param.name.text;
|
|
||||||
const typeNode = param.type;
|
|
||||||
|
|
||||||
if (isDefined(typeNode)) {
|
|
||||||
schema[paramName] = getTypeString(typeNode);
|
|
||||||
} else {
|
|
||||||
schema[paramName] = { type: 'unknown' };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
@ -0,0 +1,49 @@
|
|||||||
|
import { BaseOutputSchema } from '@/workflow/search-variables/types/StepOutputSchema';
|
||||||
|
import { isObject } from '@sniptt/guards';
|
||||||
|
import { InputSchemaPropertyType } from '@/workflow/types/InputSchema';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
|
const getValueType = (value: any): InputSchemaPropertyType => {
|
||||||
|
if (!isDefined(value) || value === null) {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return 'string';
|
||||||
|
}
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return 'number';
|
||||||
|
}
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return 'boolean';
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return 'array';
|
||||||
|
}
|
||||||
|
if (isObject(value)) {
|
||||||
|
return 'object';
|
||||||
|
}
|
||||||
|
return 'unknown';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFunctionOutputSchema = (testResult: object) => {
|
||||||
|
return testResult
|
||||||
|
? Object.entries(testResult).reduce(
|
||||||
|
(acc: BaseOutputSchema, [key, value]) => {
|
||||||
|
if (isObject(value) && !Array.isArray(value)) {
|
||||||
|
acc[key] = {
|
||||||
|
isLeaf: false,
|
||||||
|
value: getFunctionOutputSchema(value),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
acc[key] = {
|
||||||
|
isLeaf: true,
|
||||||
|
value,
|
||||||
|
type: getValueType(value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
: {};
|
||||||
|
};
|
@ -0,0 +1,32 @@
|
|||||||
|
import { FunctionInput } from '@/workflow/types/FunctionInput';
|
||||||
|
import { isObject } from '@sniptt/guards';
|
||||||
|
|
||||||
|
export const mergeDefaultFunctionInputAndFunctionInput = ({
|
||||||
|
newInput,
|
||||||
|
oldInput,
|
||||||
|
}: {
|
||||||
|
newInput: FunctionInput;
|
||||||
|
oldInput: FunctionInput;
|
||||||
|
}): FunctionInput => {
|
||||||
|
const result: FunctionInput = {};
|
||||||
|
|
||||||
|
for (const key of Object.keys(newInput)) {
|
||||||
|
const newValue = newInput[key];
|
||||||
|
const oldValue = oldInput[key];
|
||||||
|
|
||||||
|
if (!(key in oldInput)) {
|
||||||
|
result[key] = newValue;
|
||||||
|
} else if (newValue === null && isObject(oldValue)) {
|
||||||
|
result[key] = null;
|
||||||
|
} else if (isObject(newValue)) {
|
||||||
|
result[key] = mergeDefaultFunctionInputAndFunctionInput({
|
||||||
|
newInput: newValue,
|
||||||
|
oldInput: isObject(oldValue) ? oldValue : {},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result[key] = oldValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
@ -19,10 +19,9 @@ const StyledCalenderContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const SettingsAccountsCalendarChannelsContainer = () => {
|
export const SettingsAccountsCalendarChannelsContainer = () => {
|
||||||
const { activeTabIdState } = useTabList(
|
const { activeTabId } = useTabList(
|
||||||
SETTINGS_ACCOUNT_CALENDAR_CHANNELS_TAB_LIST_COMPONENT_ID,
|
SETTINGS_ACCOUNT_CALENDAR_CHANNELS_TAB_LIST_COMPONENT_ID,
|
||||||
);
|
);
|
||||||
const activeTabId = useRecoilValue(activeTabIdState);
|
|
||||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||||
|
|
||||||
const { records: accounts } = useFindManyRecords<ConnectedAccount>({
|
const { records: accounts } = useFindManyRecords<ConnectedAccount>({
|
||||||
|
@ -18,10 +18,9 @@ const StyledMessageContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const SettingsAccountsMessageChannelsContainer = () => {
|
export const SettingsAccountsMessageChannelsContainer = () => {
|
||||||
const { activeTabIdState } = useTabList(
|
const { activeTabId } = useTabList(
|
||||||
SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID,
|
SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID,
|
||||||
);
|
);
|
||||||
const activeTabId = useRecoilValue(activeTabIdState);
|
|
||||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||||
|
|
||||||
const { records: accounts } = useFindManyRecords<ConnectedAccount>({
|
const { records: accounts } = useFindManyRecords<ConnectedAccount>({
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
import {
|
|
||||||
DEFAULT_OUTPUT_VALUE,
|
|
||||||
settingsServerlessFunctionOutputState,
|
|
||||||
} from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { IconSquareRoundedCheck } from 'twenty-ui';
|
|
||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import { ServerlessFunctionExecutionStatus } from '~/generated-metadata/graphql';
|
|
||||||
|
|
||||||
const StyledOutput = styled.div<{ status?: ServerlessFunctionExecutionStatus }>`
|
|
||||||
align-items: center;
|
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
|
||||||
color: ${({ theme, status }) =>
|
|
||||||
status === ServerlessFunctionExecutionStatus.Success
|
|
||||||
? theme.color.turquoise
|
|
||||||
: theme.color.red};
|
|
||||||
display: flex;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const SettingsServerlessFunctionsOutputMetadataInfo = () => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const settingsServerlessFunctionOutput = useRecoilValue(
|
|
||||||
settingsServerlessFunctionOutputState,
|
|
||||||
);
|
|
||||||
return settingsServerlessFunctionOutput.data === DEFAULT_OUTPUT_VALUE ? (
|
|
||||||
'Output'
|
|
||||||
) : (
|
|
||||||
<StyledOutput status={settingsServerlessFunctionOutput.status}>
|
|
||||||
<IconSquareRoundedCheck size={theme.icon.size.md} />
|
|
||||||
{settingsServerlessFunctionOutput.status ===
|
|
||||||
ServerlessFunctionExecutionStatus.Success
|
|
||||||
? '200 OK'
|
|
||||||
: '500 Error'}
|
|
||||||
{' - '}
|
|
||||||
{settingsServerlessFunctionOutput.duration}ms
|
|
||||||
</StyledOutput>
|
|
||||||
);
|
|
||||||
};
|
|
@ -11,7 +11,6 @@ import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
|||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -47,10 +46,9 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
|
|||||||
onChange: (filePath: string, value: string) => void;
|
onChange: (filePath: string, value: string) => void;
|
||||||
setIsCodeValid: (isCodeValid: boolean) => void;
|
setIsCodeValid: (isCodeValid: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { activeTabIdState } = useTabList(
|
const { activeTabId } = useTabList(
|
||||||
SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID,
|
SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID,
|
||||||
);
|
);
|
||||||
const activeTabId = useRecoilValue(activeTabIdState);
|
|
||||||
const TestButton = (
|
const TestButton = (
|
||||||
<Button
|
<Button
|
||||||
title="Test"
|
title="Test"
|
||||||
|
@ -7,20 +7,17 @@ import {
|
|||||||
Section,
|
Section,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
|
|
||||||
import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton';
|
|
||||||
import { SettingsServerlessFunctionsOutputMetadataInfo } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsOutputMetadataInfo';
|
|
||||||
import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/settings/serverless-functions/states/settingsServerlessFunctionCodeEditorOutputParamsState';
|
|
||||||
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
|
||||||
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
|
||||||
import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope';
|
import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope';
|
||||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
|
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
|
||||||
|
import { ServerlessFunctionExecutionResult } from '@/serverless-functions/components/ServerlessFunctionExecutionResult';
|
||||||
|
import { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState';
|
||||||
|
|
||||||
const StyledInputsContainer = styled.div`
|
const StyledInputsContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -28,24 +25,27 @@ const StyledInputsContainer = styled.div`
|
|||||||
gap: ${({ theme }) => theme.spacing(4)};
|
gap: ${({ theme }) => theme.spacing(4)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledCodeEditorContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
export const SettingsServerlessFunctionTestTab = ({
|
export const SettingsServerlessFunctionTestTab = ({
|
||||||
handleExecute,
|
handleExecute,
|
||||||
|
serverlessFunctionId,
|
||||||
}: {
|
}: {
|
||||||
handleExecute: () => void;
|
handleExecute: () => void;
|
||||||
|
serverlessFunctionId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const settingsServerlessFunctionCodeEditorOutputParams = useRecoilValue(
|
const [serverlessFunctionTestData, setServerlessFunctionTestData] =
|
||||||
settingsServerlessFunctionCodeEditorOutputParamsState,
|
useRecoilState(serverlessFunctionTestDataFamilyState(serverlessFunctionId));
|
||||||
);
|
|
||||||
const settingsServerlessFunctionOutput = useRecoilValue(
|
|
||||||
settingsServerlessFunctionOutputState,
|
|
||||||
);
|
|
||||||
const [settingsServerlessFunctionInput, setSettingsServerlessFunctionInput] =
|
|
||||||
useRecoilState(settingsServerlessFunctionInputState);
|
|
||||||
|
|
||||||
const result =
|
const onChange = (newInput: string) => {
|
||||||
settingsServerlessFunctionOutput.data ||
|
setServerlessFunctionTestData((prev) => ({
|
||||||
settingsServerlessFunctionOutput.error ||
|
...prev,
|
||||||
'';
|
input: JSON.parse(newInput),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
useHotkeyScopeOnMount(
|
useHotkeyScopeOnMount(
|
||||||
@ -67,7 +67,7 @@ export const SettingsServerlessFunctionTestTab = ({
|
|||||||
description='Insert a JSON input, then press "Run" to test your function.'
|
description='Insert a JSON input, then press "Run" to test your function.'
|
||||||
/>
|
/>
|
||||||
<StyledInputsContainer>
|
<StyledInputsContainer>
|
||||||
<div>
|
<StyledCodeEditorContainer>
|
||||||
<CoreEditorHeader
|
<CoreEditorHeader
|
||||||
title={'Input'}
|
title={'Input'}
|
||||||
rightNodes={[
|
rightNodes={[
|
||||||
@ -82,26 +82,16 @@ export const SettingsServerlessFunctionTestTab = ({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={settingsServerlessFunctionInput}
|
value={JSON.stringify(serverlessFunctionTestData.input, null, 4)}
|
||||||
language="json"
|
language="json"
|
||||||
height={200}
|
height={200}
|
||||||
onChange={setSettingsServerlessFunctionInput}
|
onChange={onChange}
|
||||||
withHeader
|
withHeader
|
||||||
/>
|
/>
|
||||||
</div>
|
</StyledCodeEditorContainer>
|
||||||
<div>
|
<ServerlessFunctionExecutionResult
|
||||||
<CoreEditorHeader
|
serverlessFunctionTestData={serverlessFunctionTestData}
|
||||||
leftNodes={[<SettingsServerlessFunctionsOutputMetadataInfo />]}
|
/>
|
||||||
rightNodes={[<LightCopyIconButton copyText={result} />]}
|
|
||||||
/>
|
|
||||||
<CodeEditor
|
|
||||||
value={result}
|
|
||||||
language={settingsServerlessFunctionCodeEditorOutputParams.language}
|
|
||||||
height={settingsServerlessFunctionCodeEditorOutputParams.height}
|
|
||||||
options={{ readOnly: true, domReadOnly: true }}
|
|
||||||
withHeader
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</StyledInputsContainer>
|
</StyledInputsContainer>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
|
||||||
import {
|
|
||||||
DEFAULT_OUTPUT_VALUE,
|
|
||||||
settingsServerlessFunctionOutputState,
|
|
||||||
} from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
|
||||||
import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/settings/serverless-functions/states/settingsServerlessFunctionCodeEditorOutputParamsState';
|
|
||||||
|
|
||||||
export const SettingsServerlessFunctionTestTabEffect = () => {
|
|
||||||
const settingsServerlessFunctionOutput = useRecoilValue(
|
|
||||||
settingsServerlessFunctionOutputState,
|
|
||||||
);
|
|
||||||
const setSettingsServerlessFunctionCodeEditorOutputParams = useSetRecoilState(
|
|
||||||
settingsServerlessFunctionCodeEditorOutputParamsState,
|
|
||||||
);
|
|
||||||
useEffect(() => {
|
|
||||||
if (settingsServerlessFunctionOutput.data !== DEFAULT_OUTPUT_VALUE) {
|
|
||||||
setSettingsServerlessFunctionCodeEditorOutputParams({
|
|
||||||
language: 'json',
|
|
||||||
height: 300,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
settingsServerlessFunctionOutput.data,
|
|
||||||
setSettingsServerlessFunctionCodeEditorOutputParams,
|
|
||||||
]);
|
|
||||||
return <></>;
|
|
||||||
};
|
|
@ -2,6 +2,10 @@ import { useGetOneServerlessFunction } from '@/settings/serverless-functions/hoo
|
|||||||
import { useGetOneServerlessFunctionSourceCode } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunctionSourceCode';
|
import { useGetOneServerlessFunctionSourceCode } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunctionSourceCode';
|
||||||
import { Dispatch, SetStateAction, useState } from 'react';
|
import { Dispatch, SetStateAction, useState } from 'react';
|
||||||
import { FindOneServerlessFunctionSourceCodeQuery } from '~/generated-metadata/graphql';
|
import { FindOneServerlessFunctionSourceCodeQuery } from '~/generated-metadata/graphql';
|
||||||
|
import { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState';
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
import { getFunctionInputFromSourceCode } from '@/serverless-functions/utils/getFunctionInputFromSourceCode';
|
||||||
|
import { INDEX_FILE_PATH } from '@/serverless-functions/constants/IndexFilePath';
|
||||||
|
|
||||||
export type ServerlessFunctionNewFormValues = {
|
export type ServerlessFunctionNewFormValues = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -29,6 +33,10 @@ export const useServerlessFunctionUpdateFormState = (
|
|||||||
code: undefined,
|
code: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setServerlessFunctionTestData = useSetRecoilState(
|
||||||
|
serverlessFunctionTestDataFamilyState(serverlessFunctionId),
|
||||||
|
);
|
||||||
|
|
||||||
const { serverlessFunction } = useGetOneServerlessFunction({
|
const { serverlessFunction } = useGetOneServerlessFunction({
|
||||||
id: serverlessFunctionId,
|
id: serverlessFunctionId,
|
||||||
});
|
});
|
||||||
@ -46,6 +54,12 @@ export const useServerlessFunctionUpdateFormState = (
|
|||||||
...prevState,
|
...prevState,
|
||||||
...newState,
|
...newState,
|
||||||
}));
|
}));
|
||||||
|
const sourceCode =
|
||||||
|
data?.getServerlessFunctionSourceCode?.[INDEX_FILE_PATH];
|
||||||
|
setServerlessFunctionTestData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
input: getFunctionInputFromSourceCode(sourceCode),
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import { createState } from 'twenty-ui';
|
|
||||||
|
|
||||||
export const settingsServerlessFunctionCodeEditorOutputParamsState =
|
|
||||||
createState<{ language: string; height: number }>({
|
|
||||||
key: 'settingsServerlessFunctionCodeEditorOutputParamsState',
|
|
||||||
defaultValue: { language: 'plaintext', height: 64 },
|
|
||||||
});
|
|
@ -1,6 +0,0 @@
|
|||||||
import { createState } from 'twenty-ui';
|
|
||||||
|
|
||||||
export const settingsServerlessFunctionInputState = createState<string>({
|
|
||||||
key: 'settingsServerlessFunctionInputState',
|
|
||||||
defaultValue: '{}',
|
|
||||||
});
|
|
@ -1,18 +0,0 @@
|
|||||||
import { createState } from 'twenty-ui';
|
|
||||||
import { ServerlessFunctionExecutionStatus } from '~/generated-metadata/graphql';
|
|
||||||
|
|
||||||
type settingsServerlessFunctionOutput = {
|
|
||||||
data?: string;
|
|
||||||
duration?: number;
|
|
||||||
status?: ServerlessFunctionExecutionStatus;
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DEFAULT_OUTPUT_VALUE =
|
|
||||||
'Enter an input above then press "run Function"';
|
|
||||||
|
|
||||||
export const settingsServerlessFunctionOutputState =
|
|
||||||
createState<settingsServerlessFunctionOutput>({
|
|
||||||
key: 'settingsServerlessFunctionOutputState',
|
|
||||||
defaultValue: { data: DEFAULT_OUTPUT_VALUE },
|
|
||||||
});
|
|
@ -1,5 +1,7 @@
|
|||||||
import { getHighlightedDates } from '@/ui/input/components/internal/date/utils/getHighlightedDates';
|
import { getHighlightedDates } from '@/ui/input/components/internal/date/utils/getHighlightedDates';
|
||||||
|
|
||||||
|
jest.useFakeTimers().setSystemTime(new Date('2024-10-01T00:00:00.000Z'));
|
||||||
|
|
||||||
describe('getHighlightedDates', () => {
|
describe('getHighlightedDates', () => {
|
||||||
it('should should return empty if range is undefined', () => {
|
it('should should return empty if range is undefined', () => {
|
||||||
const dateRange = undefined;
|
const dateRange = undefined;
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { Fragment } from 'react';
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
background: ${({ theme }) => theme.background.secondary};
|
||||||
|
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
bottom: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)};
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type RightDrawerFooterProps = {
|
||||||
|
actions: React.ReactNode[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RightDrawerFooter = ({ actions }: RightDrawerFooterProps) => {
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
{actions.map((action, index) => (
|
||||||
|
<Fragment key={index}>{action}</Fragment>
|
||||||
|
))}
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -13,6 +13,7 @@ import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
|||||||
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 { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
|
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
|
||||||
|
|
||||||
const StyledShowPageRightContainer = styled.div<{ isMobile: boolean }>`
|
const StyledShowPageRightContainer = styled.div<{ isMobile: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -34,19 +35,6 @@ const StyledTabListContainer = styled.div<{ shouldDisplay: boolean }>`
|
|||||||
height: 40px;
|
height: 40px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledButtonContainer = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
background: ${({ theme }) => theme.background.secondary};
|
|
||||||
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
|
||||||
bottom: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding: ${({ theme }) => theme.spacing(2)};
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledContentContainer = styled.div<{ isInRightDrawer: boolean }>`
|
const StyledContentContainer = styled.div<{ isInRightDrawer: boolean }>`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@ -57,7 +45,7 @@ const StyledContentContainer = styled.div<{ isInRightDrawer: boolean }>`
|
|||||||
export const TAB_LIST_COMPONENT_ID = 'show-page-right-tab-list';
|
export const TAB_LIST_COMPONENT_ID = 'show-page-right-tab-list';
|
||||||
|
|
||||||
type ShowPageSubContainerProps = {
|
type ShowPageSubContainerProps = {
|
||||||
layout: RecordLayout;
|
layout?: RecordLayout;
|
||||||
tabs: SingleTabProps[];
|
tabs: SingleTabProps[];
|
||||||
targetableObject: Pick<
|
targetableObject: Pick<
|
||||||
ActivityTargetableObject,
|
ActivityTargetableObject,
|
||||||
@ -76,10 +64,9 @@ export const ShowPageSubContainer = ({
|
|||||||
isInRightDrawer = false,
|
isInRightDrawer = false,
|
||||||
isNewRightDrawerItemLoading = false,
|
isNewRightDrawerItemLoading = false,
|
||||||
}: ShowPageSubContainerProps) => {
|
}: ShowPageSubContainerProps) => {
|
||||||
const { activeTabIdState } = useTabList(
|
const { activeTabId } = useTabList(
|
||||||
`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}`,
|
`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}`,
|
||||||
);
|
);
|
||||||
const activeTabId = useRecoilValue(activeTabIdState);
|
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
@ -125,9 +112,12 @@ export const ShowPageSubContainer = ({
|
|||||||
|
|
||||||
const visibleTabs = tabs.filter((tab) => !tab.hide);
|
const visibleTabs = tabs.filter((tab) => !tab.hide);
|
||||||
|
|
||||||
|
const displaySummaryAndFields =
|
||||||
|
layout && !layout.hideSummaryAndFields && !isMobile && !isInRightDrawer;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!layout.hideSummaryAndFields && !isMobile && !isInRightDrawer && (
|
{displaySummaryAndFields && (
|
||||||
<ShowPageLeftContainer forceMobile={isMobile}>
|
<ShowPageLeftContainer forceMobile={isMobile}>
|
||||||
{summaryCard}
|
{summaryCard}
|
||||||
{fieldsCard}
|
{fieldsCard}
|
||||||
@ -147,9 +137,7 @@ export const ShowPageSubContainer = ({
|
|||||||
{renderActiveTabContent()}
|
{renderActiveTabContent()}
|
||||||
</StyledContentContainer>
|
</StyledContentContainer>
|
||||||
{isInRightDrawer && recordFromStore && !recordFromStore.deletedAt && (
|
{isInRightDrawer && recordFromStore && !recordFromStore.deletedAt && (
|
||||||
<StyledButtonContainer>
|
<RightDrawerFooter actions={[<RecordShowRightDrawerActionMenu />]} />
|
||||||
<RecordShowRightDrawerActionMenu />
|
|
||||||
</StyledButtonContainer>
|
|
||||||
)}
|
)}
|
||||||
</StyledShowPageRightContainer>
|
</StyledShowPageRightContainer>
|
||||||
</>
|
</>
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
import { IconComponent } from 'twenty-ui';
|
import { IconComponent } from 'twenty-ui';
|
||||||
|
|
||||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||||
import { TabListScope } from '@/ui/layout/tab/scopes/TabListScope';
|
import { TabListScope } from '@/ui/layout/tab/scopes/TabListScope';
|
||||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
|
||||||
|
|
||||||
import { TabListFromUrlOptionalEffect } from '@/ui/layout/tab/components/TabListFromUrlOptionalEffect';
|
import { TabListFromUrlOptionalEffect } from '@/ui/layout/tab/components/TabListFromUrlOptionalEffect';
|
||||||
import { LayoutCard } from '@/ui/layout/tab/types/LayoutCard';
|
import { LayoutCard } from '@/ui/layout/tab/types/LayoutCard';
|
||||||
import { Tab } from './Tab';
|
import { Tab } from './Tab';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
export type SingleTabProps = {
|
export type SingleTabProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@ -30,13 +29,17 @@ type TabListProps = {
|
|||||||
behaveAsLinks?: boolean;
|
behaveAsLinks?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledTabsContainer = styled.div`
|
||||||
border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`};
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: ${({ theme }) => theme.spacing(2)};
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
height: 40px;
|
height: 40px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TabList = ({
|
export const TabList = ({
|
||||||
@ -50,11 +53,9 @@ export const TabList = ({
|
|||||||
|
|
||||||
const initialActiveTabId = visibleTabs[0]?.id || '';
|
const initialActiveTabId = visibleTabs[0]?.id || '';
|
||||||
|
|
||||||
const { activeTabIdState, setActiveTabId } = useTabList(tabListInstanceId);
|
const { activeTabId, setActiveTabId } = useTabList(tabListInstanceId);
|
||||||
|
|
||||||
const activeTabId = useRecoilValue(activeTabIdState);
|
useEffect(() => {
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
setActiveTabId(initialActiveTabId);
|
setActiveTabId(initialActiveTabId);
|
||||||
}, [initialActiveTabId, setActiveTabId]);
|
}, [initialActiveTabId, setActiveTabId]);
|
||||||
|
|
||||||
@ -63,13 +64,13 @@ export const TabList = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabListScope tabListScopeId={tabListInstanceId}>
|
<StyledContainer className={className}>
|
||||||
<TabListFromUrlOptionalEffect
|
<TabListScope tabListScopeId={tabListInstanceId}>
|
||||||
componentInstanceId={tabListInstanceId}
|
<TabListFromUrlOptionalEffect
|
||||||
tabListIds={tabs.map((tab) => tab.id)}
|
componentInstanceId={tabListInstanceId}
|
||||||
/>
|
tabListIds={tabs.map((tab) => tab.id)}
|
||||||
<ScrollWrapper enableYScroll={false} contextProviderName="tabList">
|
/>
|
||||||
<StyledContainer className={className}>
|
<StyledTabsContainer>
|
||||||
{visibleTabs.map((tab) => (
|
{visibleTabs.map((tab) => (
|
||||||
<Tab
|
<Tab
|
||||||
id={tab.id}
|
id={tab.id}
|
||||||
@ -88,8 +89,8 @@ export const TabList = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</StyledContainer>
|
</StyledTabsContainer>
|
||||||
</ScrollWrapper>
|
</TabListScope>
|
||||||
</TabListScope>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
|
|
||||||
type TabListFromUrlOptionalEffectProps = {
|
type TabListFromUrlOptionalEffectProps = {
|
||||||
componentInstanceId: string;
|
componentInstanceId: string;
|
||||||
@ -13,11 +12,9 @@ export const TabListFromUrlOptionalEffect = ({
|
|||||||
tabListIds,
|
tabListIds,
|
||||||
}: TabListFromUrlOptionalEffectProps) => {
|
}: TabListFromUrlOptionalEffectProps) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { activeTabIdState } = useTabList(componentInstanceId);
|
const { activeTabId, setActiveTabId } = useTabList(componentInstanceId);
|
||||||
const { setActiveTabId } = useTabList(componentInstanceId);
|
|
||||||
|
|
||||||
const hash = location.hash.replace('#', '');
|
const hash = location.hash.replace('#', '');
|
||||||
const activeTabId = useRecoilValue(activeTabIdState);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hash === activeTabId) {
|
if (hash === activeTabId) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
import { RecoilRoot, useRecoilValue } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
import { useTabList } from '../useTabList';
|
import { useTabList } from '../useTabList';
|
||||||
|
|
||||||
@ -8,9 +8,7 @@ describe('useTabList', () => {
|
|||||||
it('Should update the activeTabId state', async () => {
|
it('Should update the activeTabId state', async () => {
|
||||||
const { result } = renderHook(
|
const { result } = renderHook(
|
||||||
() => {
|
() => {
|
||||||
const { activeTabIdState, setActiveTabId } =
|
const { activeTabId, setActiveTabId } = useTabList('TEST_TAB_LIST_ID');
|
||||||
useTabList('TEST_TAB_LIST_ID');
|
|
||||||
const activeTabId = useRecoilValue(activeTabIdState);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeTabId,
|
activeTabId,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useSetRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { useTabListStates } from '@/ui/layout/tab/hooks/internal/useTabListStates';
|
import { useTabListStates } from '@/ui/layout/tab/hooks/internal/useTabListStates';
|
||||||
|
|
||||||
@ -7,10 +7,10 @@ export const useTabList = (tabListId?: string) => {
|
|||||||
tabListScopeId: tabListId,
|
tabListScopeId: tabListId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const setActiveTabId = useSetRecoilState(activeTabIdState);
|
const [activeTabId, setActiveTabId] = useRecoilState(activeTabIdState);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeTabIdState,
|
activeTabId,
|
||||||
setActiveTabId,
|
setActiveTabId,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||||
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
import { WorkflowStepHeader } from '@/workflow/components/WorkflowStepHeader';
|
||||||
import { OBJECT_EVENT_TRIGGERS } from '@/workflow/constants/ObjectEventTriggers';
|
import { OBJECT_EVENT_TRIGGERS } from '@/workflow/constants/ObjectEventTriggers';
|
||||||
import { WorkflowDatabaseEventTrigger } from '@/workflow/types/Workflow';
|
import { WorkflowDatabaseEventTrigger } from '@/workflow/types/Workflow';
|
||||||
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
|
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import { IconPlaylistAdd, isDefined } from 'twenty-ui';
|
import { IconPlaylistAdd, isDefined } from 'twenty-ui';
|
||||||
|
import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
|
||||||
|
|
||||||
type WorkflowEditTriggerDatabaseEventFormProps = {
|
type WorkflowEditTriggerDatabaseEventFormProps = {
|
||||||
trigger: WorkflowDatabaseEventTrigger;
|
trigger: WorkflowDatabaseEventTrigger;
|
||||||
@ -60,88 +61,91 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
|
|||||||
: '-';
|
: '-';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkflowEditGenericFormBase
|
<>
|
||||||
onTitleChange={(newName: string) => {
|
<WorkflowStepHeader
|
||||||
if (triggerOptions.readonly === true) {
|
onTitleChange={(newName: string) => {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
triggerOptions.onTriggerUpdate({
|
|
||||||
...trigger,
|
|
||||||
name: newName,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
Icon={IconPlaylistAdd}
|
|
||||||
iconColor={theme.font.color.tertiary}
|
|
||||||
initialTitle={headerTitle}
|
|
||||||
headerType={headerType}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
dropdownId="workflow-edit-trigger-record-type"
|
|
||||||
label="Record Type"
|
|
||||||
fullWidth
|
|
||||||
disabled={triggerOptions.readonly}
|
|
||||||
value={triggerEvent?.objectType}
|
|
||||||
emptyOption={{ label: 'Select an option', value: '' }}
|
|
||||||
options={availableMetadata}
|
|
||||||
onChange={(updatedRecordType) => {
|
|
||||||
if (triggerOptions.readonly === true) {
|
if (triggerOptions.readonly === true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerOptions.onTriggerUpdate(
|
triggerOptions.onTriggerUpdate({
|
||||||
isDefined(trigger) && isDefined(triggerEvent)
|
...trigger,
|
||||||
? {
|
name: newName,
|
||||||
...trigger,
|
});
|
||||||
settings: {
|
|
||||||
...trigger.settings,
|
|
||||||
eventName: `${updatedRecordType}.${triggerEvent.event}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
name: headerTitle,
|
|
||||||
type: 'DATABASE_EVENT',
|
|
||||||
settings: {
|
|
||||||
eventName: `${updatedRecordType}.${OBJECT_EVENT_TRIGGERS[0].value}`,
|
|
||||||
outputSchema: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
|
Icon={IconPlaylistAdd}
|
||||||
|
iconColor={theme.font.color.tertiary}
|
||||||
|
initialTitle={headerTitle}
|
||||||
|
headerType={headerType}
|
||||||
/>
|
/>
|
||||||
<Select
|
<WorkflowStepBody>
|
||||||
dropdownId="workflow-edit-trigger-event-type"
|
<Select
|
||||||
label="Event type"
|
dropdownId="workflow-edit-trigger-record-type"
|
||||||
fullWidth
|
label="Record Type"
|
||||||
value={triggerEvent?.event}
|
fullWidth
|
||||||
emptyOption={{ label: 'Select an option', value: '' }}
|
disabled={triggerOptions.readonly}
|
||||||
options={OBJECT_EVENT_TRIGGERS}
|
value={triggerEvent?.objectType}
|
||||||
disabled={triggerOptions.readonly}
|
emptyOption={{ label: 'Select an option', value: '' }}
|
||||||
onChange={(updatedEvent) => {
|
options={availableMetadata}
|
||||||
if (triggerOptions.readonly === true) {
|
onChange={(updatedRecordType) => {
|
||||||
return;
|
if (triggerOptions.readonly === true) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
triggerOptions.onTriggerUpdate(
|
triggerOptions.onTriggerUpdate(
|
||||||
isDefined(trigger) && isDefined(triggerEvent)
|
isDefined(trigger) && isDefined(triggerEvent)
|
||||||
? {
|
? {
|
||||||
...trigger,
|
...trigger,
|
||||||
settings: {
|
settings: {
|
||||||
...trigger.settings,
|
...trigger.settings,
|
||||||
eventName: `${triggerEvent.objectType}.${updatedEvent}`,
|
eventName: `${updatedRecordType}.${triggerEvent.event}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
name: headerTitle,
|
||||||
|
type: 'DATABASE_EVENT',
|
||||||
|
settings: {
|
||||||
|
eventName: `${updatedRecordType}.${OBJECT_EVENT_TRIGGERS[0].value}`,
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
);
|
||||||
: {
|
}}
|
||||||
name: headerTitle,
|
/>
|
||||||
type: 'DATABASE_EVENT',
|
<Select
|
||||||
settings: {
|
dropdownId="workflow-edit-trigger-event-type"
|
||||||
eventName: `${availableMetadata[0].value}.${updatedEvent}`,
|
label="Event type"
|
||||||
outputSchema: {},
|
fullWidth
|
||||||
|
value={triggerEvent?.event}
|
||||||
|
emptyOption={{ label: 'Select an option', value: '' }}
|
||||||
|
options={OBJECT_EVENT_TRIGGERS}
|
||||||
|
disabled={triggerOptions.readonly}
|
||||||
|
onChange={(updatedEvent) => {
|
||||||
|
if (triggerOptions.readonly === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerOptions.onTriggerUpdate(
|
||||||
|
isDefined(trigger) && isDefined(triggerEvent)
|
||||||
|
? {
|
||||||
|
...trigger,
|
||||||
|
settings: {
|
||||||
|
...trigger.settings,
|
||||||
|
eventName: `${triggerEvent.objectType}.${updatedEvent}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
name: headerTitle,
|
||||||
|
type: 'DATABASE_EVENT',
|
||||||
|
settings: {
|
||||||
|
eventName: `${availableMetadata?.[0].value}.${updatedEvent}`,
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
);
|
||||||
);
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</WorkflowStepBody>
|
||||||
</WorkflowEditGenericFormBase>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||||
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
import { WorkflowStepHeader } from '@/workflow/components/WorkflowStepHeader';
|
||||||
import { MANUAL_TRIGGER_AVAILABILITY_OPTIONS } from '@/workflow/constants/ManualTriggerAvailabilityOptions';
|
import { MANUAL_TRIGGER_AVAILABILITY_OPTIONS } from '@/workflow/constants/ManualTriggerAvailabilityOptions';
|
||||||
import {
|
import {
|
||||||
WorkflowManualTrigger,
|
WorkflowManualTrigger,
|
||||||
@ -9,6 +9,7 @@ import {
|
|||||||
import { getManualTriggerDefaultSettings } from '@/workflow/utils/getManualTriggerDefaultSettings';
|
import { getManualTriggerDefaultSettings } from '@/workflow/utils/getManualTriggerDefaultSettings';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import { IconHandMove, isDefined, useIcons } from 'twenty-ui';
|
import { IconHandMove, isDefined, useIcons } from 'twenty-ui';
|
||||||
|
import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
|
||||||
|
|
||||||
type WorkflowEditTriggerManualFormProps = {
|
type WorkflowEditTriggerManualFormProps = {
|
||||||
trigger: WorkflowManualTrigger;
|
trigger: WorkflowManualTrigger;
|
||||||
@ -47,67 +48,70 @@ export const WorkflowEditTriggerManualForm = ({
|
|||||||
const headerTitle = isDefined(trigger.name) ? trigger.name : 'Manual Trigger';
|
const headerTitle = isDefined(trigger.name) ? trigger.name : 'Manual Trigger';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkflowEditGenericFormBase
|
<>
|
||||||
onTitleChange={(newName: string) => {
|
<WorkflowStepHeader
|
||||||
if (triggerOptions.readonly === true) {
|
onTitleChange={(newName: string) => {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
triggerOptions.onTriggerUpdate({
|
|
||||||
...trigger,
|
|
||||||
name: newName,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
Icon={IconHandMove}
|
|
||||||
iconColor={theme.font.color.tertiary}
|
|
||||||
initialTitle={headerTitle}
|
|
||||||
headerType="Trigger · Manual"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
dropdownId="workflow-edit-manual-trigger-availability"
|
|
||||||
label="Available"
|
|
||||||
fullWidth
|
|
||||||
disabled={triggerOptions.readonly}
|
|
||||||
value={manualTriggerAvailability}
|
|
||||||
options={MANUAL_TRIGGER_AVAILABILITY_OPTIONS}
|
|
||||||
onChange={(updatedTriggerType) => {
|
|
||||||
if (triggerOptions.readonly === true) {
|
if (triggerOptions.readonly === true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerOptions.onTriggerUpdate({
|
triggerOptions.onTriggerUpdate({
|
||||||
...trigger,
|
...trigger,
|
||||||
settings: getManualTriggerDefaultSettings({
|
name: newName,
|
||||||
availability: updatedTriggerType,
|
|
||||||
activeObjectMetadataItems,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
Icon={IconHandMove}
|
||||||
|
iconColor={theme.font.color.tertiary}
|
||||||
|
initialTitle={headerTitle}
|
||||||
|
headerType="Trigger · Manual"
|
||||||
/>
|
/>
|
||||||
|
<WorkflowStepBody>
|
||||||
{manualTriggerAvailability === 'WHEN_RECORD_SELECTED' ? (
|
|
||||||
<Select
|
<Select
|
||||||
dropdownId="workflow-edit-manual-trigger-object"
|
dropdownId="workflow-edit-manual-trigger-availability"
|
||||||
label="Object"
|
label="Available"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={trigger.settings.objectType}
|
|
||||||
options={availableMetadata}
|
|
||||||
disabled={triggerOptions.readonly}
|
disabled={triggerOptions.readonly}
|
||||||
onChange={(updatedObject) => {
|
value={manualTriggerAvailability}
|
||||||
|
options={MANUAL_TRIGGER_AVAILABILITY_OPTIONS}
|
||||||
|
onChange={(updatedTriggerType) => {
|
||||||
if (triggerOptions.readonly === true) {
|
if (triggerOptions.readonly === true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerOptions.onTriggerUpdate({
|
triggerOptions.onTriggerUpdate({
|
||||||
...trigger,
|
...trigger,
|
||||||
settings: {
|
settings: getManualTriggerDefaultSettings({
|
||||||
objectType: updatedObject,
|
availability: updatedTriggerType,
|
||||||
outputSchema: {},
|
activeObjectMetadataItems,
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
|
||||||
</WorkflowEditGenericFormBase>
|
{manualTriggerAvailability === 'WHEN_RECORD_SELECTED' ? (
|
||||||
|
<Select
|
||||||
|
dropdownId="workflow-edit-manual-trigger-object"
|
||||||
|
label="Object"
|
||||||
|
fullWidth
|
||||||
|
value={trigger.settings.objectType}
|
||||||
|
options={availableMetadata}
|
||||||
|
disabled={triggerOptions.readonly}
|
||||||
|
onChange={(updatedObject) => {
|
||||||
|
if (triggerOptions.readonly === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerOptions.onTriggerUpdate({
|
||||||
|
...trigger,
|
||||||
|
settings: {
|
||||||
|
objectType: updatedObject,
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</WorkflowStepBody>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -6,8 +6,8 @@ import {
|
|||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
|
|
||||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
import { StyledFormFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputContainer';
|
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
|
||||||
import { StyledFormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputRowContainer';
|
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
|
||||||
import { SingleRecordSelect } from '@/object-record/relation-picker/components/SingleRecordSelect';
|
import { SingleRecordSelect } from '@/object-record/relation-picker/components/SingleRecordSelect';
|
||||||
import { useRecordPicker } from '@/object-record/relation-picker/hooks/useRecordPicker';
|
import { useRecordPicker } from '@/object-record/relation-picker/hooks/useRecordPicker';
|
||||||
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
|
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||||
@ -137,9 +137,9 @@ export const WorkflowSingleRecordPicker = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledFormFieldInputContainer>
|
<FormFieldInputContainer>
|
||||||
{label ? <InputLabel>{label}</InputLabel> : null}
|
{label ? <InputLabel>{label}</InputLabel> : null}
|
||||||
<StyledFormFieldInputRowContainer>
|
<FormFieldInputRowContainer>
|
||||||
<StyledFormSelectContainer>
|
<StyledFormSelectContainer>
|
||||||
<WorkflowSingleRecordFieldChip
|
<WorkflowSingleRecordFieldChip
|
||||||
draftValue={draftValue}
|
draftValue={draftValue}
|
||||||
@ -193,7 +193,7 @@ export const WorkflowSingleRecordPicker = ({
|
|||||||
objectNameSingularToSelect={objectNameSingular}
|
objectNameSingularToSelect={objectNameSingular}
|
||||||
/>
|
/>
|
||||||
</StyledSearchVariablesDropdownContainer>
|
</StyledSearchVariablesDropdownContainer>
|
||||||
</StyledFormFieldInputRowContainer>
|
</FormFieldInputRowContainer>
|
||||||
</StyledFormFieldInputContainer>
|
</FormFieldInputContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
const StyledWorkflowStepBody = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: scroll;
|
||||||
|
padding: ${({ theme }) => theme.spacing(6)};
|
||||||
|
row-gap: ${({ theme }) => theme.spacing(6)};
|
||||||
|
flex: 1 1 auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export { StyledWorkflowStepBody as WorkflowStepBody };
|
@ -10,8 +10,8 @@ import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrTh
|
|||||||
import { WorkflowEditActionFormCreateRecord } from '@/workflow/workflow-actions/components/WorkflowEditActionFormCreateRecord';
|
import { WorkflowEditActionFormCreateRecord } from '@/workflow/workflow-actions/components/WorkflowEditActionFormCreateRecord';
|
||||||
import { WorkflowEditActionFormDeleteRecord } from '@/workflow/workflow-actions/components/WorkflowEditActionFormDeleteRecord';
|
import { WorkflowEditActionFormDeleteRecord } from '@/workflow/workflow-actions/components/WorkflowEditActionFormDeleteRecord';
|
||||||
import { WorkflowEditActionFormSendEmail } from '@/workflow/workflow-actions/components/WorkflowEditActionFormSendEmail';
|
import { WorkflowEditActionFormSendEmail } from '@/workflow/workflow-actions/components/WorkflowEditActionFormSendEmail';
|
||||||
|
import { Suspense, lazy } from 'react';
|
||||||
import { WorkflowEditActionFormUpdateRecord } from '@/workflow/workflow-actions/components/WorkflowEditActionFormUpdateRecord';
|
import { WorkflowEditActionFormUpdateRecord } from '@/workflow/workflow-actions/components/WorkflowEditActionFormUpdateRecord';
|
||||||
import { lazy, Suspense } from 'react';
|
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
import { RightDrawerSkeletonLoader } from '~/loading/components/RightDrawerSkeletonLoader';
|
import { RightDrawerSkeletonLoader } from '~/loading/components/RightDrawerSkeletonLoader';
|
||||||
|
|
||||||
|
@ -43,27 +43,18 @@ const StyledHeaderIconContainer = styled.div`
|
|||||||
padding: ${({ theme }) => theme.spacing(2)};
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledContentContainer = styled.div`
|
export const WorkflowStepHeader = ({
|
||||||
padding: ${({ theme }) => theme.spacing(6)};
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
row-gap: ${({ theme }) => theme.spacing(4)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const WorkflowEditGenericFormBase = ({
|
|
||||||
onTitleChange,
|
onTitleChange,
|
||||||
Icon,
|
Icon,
|
||||||
iconColor,
|
iconColor,
|
||||||
initialTitle,
|
initialTitle,
|
||||||
headerType,
|
headerType,
|
||||||
children,
|
|
||||||
}: {
|
}: {
|
||||||
onTitleChange: (newTitle: string) => void;
|
onTitleChange: (newTitle: string) => void;
|
||||||
Icon: IconComponent;
|
Icon: IconComponent;
|
||||||
iconColor: string;
|
iconColor: string;
|
||||||
initialTitle: string;
|
initialTitle: string;
|
||||||
headerType: string;
|
headerType: string;
|
||||||
children: React.ReactNode;
|
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [title, setTitle] = useState(initialTitle);
|
const [title, setTitle] = useState(initialTitle);
|
||||||
@ -74,33 +65,30 @@ export const WorkflowEditGenericFormBase = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<StyledHeader>
|
||||||
<StyledHeader>
|
<StyledHeaderIconContainer>
|
||||||
<StyledHeaderIconContainer>
|
{
|
||||||
{
|
<Icon
|
||||||
<Icon
|
color={iconColor}
|
||||||
color={iconColor}
|
stroke={theme.icon.stroke.sm}
|
||||||
stroke={theme.icon.stroke.sm}
|
size={theme.icon.size.lg}
|
||||||
size={theme.icon.size.lg}
|
/>
|
||||||
/>
|
}
|
||||||
}
|
</StyledHeaderIconContainer>
|
||||||
</StyledHeaderIconContainer>
|
<StyledHeaderInfo>
|
||||||
<StyledHeaderInfo>
|
<StyledHeaderTitle>
|
||||||
<StyledHeaderTitle>
|
<TextInput
|
||||||
<TextInput
|
value={title}
|
||||||
value={title}
|
copyButton={false}
|
||||||
copyButton={false}
|
hotkeyScope="workflow-step-title"
|
||||||
hotkeyScope="workflow-step-title"
|
onEnter={onTitleChange}
|
||||||
onEnter={onTitleChange}
|
onEscape={onTitleChange}
|
||||||
onEscape={onTitleChange}
|
onChange={handleChange}
|
||||||
onChange={handleChange}
|
shouldTrim={false}
|
||||||
shouldTrim={false}
|
/>
|
||||||
/>
|
</StyledHeaderTitle>
|
||||||
</StyledHeaderTitle>
|
<StyledHeaderType>{headerType}</StyledHeaderType>
|
||||||
<StyledHeaderType>{headerType}</StyledHeaderType>
|
</StyledHeaderInfo>
|
||||||
</StyledHeaderInfo>
|
</StyledHeader>
|
||||||
</StyledHeader>
|
|
||||||
<StyledContentContainer>{children}</StyledContentContainer>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -14,10 +14,7 @@ export const useUpdateStep = ({
|
|||||||
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
|
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
|
||||||
const { updateWorkflowVersionStep } = useUpdateWorkflowVersionStep();
|
const { updateWorkflowVersionStep } = useUpdateWorkflowVersionStep();
|
||||||
|
|
||||||
const updateStep = async <T extends WorkflowStep>(
|
const updateStep = async <T extends WorkflowStep>(updatedStep: T) => {
|
||||||
updatedStep: T,
|
|
||||||
shouldUpdateStepOutput = true,
|
|
||||||
) => {
|
|
||||||
if (!isDefined(workflow.currentVersion)) {
|
if (!isDefined(workflow.currentVersion)) {
|
||||||
throw new Error('Can not update an undefined workflow version.');
|
throw new Error('Can not update an undefined workflow version.');
|
||||||
}
|
}
|
||||||
@ -26,7 +23,6 @@ export const useUpdateStep = ({
|
|||||||
await updateWorkflowVersionStep({
|
await updateWorkflowVersionStep({
|
||||||
workflowVersionId: workflowVersion.id,
|
workflowVersionId: workflowVersion.id,
|
||||||
step: updatedStep,
|
step: updatedStep,
|
||||||
shouldUpdateStepOutput,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
import { ServerlessFunctionExecutionStatus } from '~/generated-metadata/graphql';
|
||||||
|
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
|
||||||
|
|
||||||
|
export type ServerlessFunctionTestData = {
|
||||||
|
input: { [field: string]: any };
|
||||||
|
output: {
|
||||||
|
data?: string;
|
||||||
|
duration?: number;
|
||||||
|
status?: ServerlessFunctionExecutionStatus;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
language: 'plaintext' | 'json';
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_OUTPUT_VALUE =
|
||||||
|
'Enter an input above then press "run Function"';
|
||||||
|
|
||||||
|
export const serverlessFunctionTestDataFamilyState = createFamilyState<
|
||||||
|
ServerlessFunctionTestData,
|
||||||
|
string
|
||||||
|
>({
|
||||||
|
key: 'serverlessFunctionTestDataFamilyState',
|
||||||
|
defaultValue: {
|
||||||
|
language: 'plaintext',
|
||||||
|
height: 64,
|
||||||
|
input: {},
|
||||||
|
output: { data: DEFAULT_OUTPUT_VALUE },
|
||||||
|
},
|
||||||
|
});
|
@ -13,9 +13,11 @@ export type InputSchemaProperty = {
|
|||||||
type: InputSchemaPropertyType;
|
type: InputSchemaPropertyType;
|
||||||
enum?: string[];
|
enum?: string[];
|
||||||
items?: InputSchemaProperty;
|
items?: InputSchemaProperty;
|
||||||
properties?: InputSchema;
|
properties?: Properties;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InputSchema = {
|
type Properties = {
|
||||||
[name: string]: InputSchemaProperty;
|
[name: string]: InputSchemaProperty;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type InputSchema = InputSchemaProperty[];
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
import { getDefaultFunctionInputFromInputSchema } from '@/workflow/utils/getDefaultFunctionInputFromInputSchema';
|
|
||||||
import { InputSchema } from '@/workflow/types/InputSchema';
|
|
||||||
|
|
||||||
describe('getDefaultFunctionInputFromInputSchema', () => {
|
|
||||||
it('should init function input properly', () => {
|
|
||||||
const inputSchema = {
|
|
||||||
params: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
a: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
b: {
|
|
||||||
type: 'number',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as InputSchema;
|
|
||||||
const expectedResult = {
|
|
||||||
params: {
|
|
||||||
a: null,
|
|
||||||
b: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
expect(getDefaultFunctionInputFromInputSchema(inputSchema)).toEqual(
|
|
||||||
expectedResult,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,22 +0,0 @@
|
|||||||
import { InputSchema } from '@/workflow/types/InputSchema';
|
|
||||||
import { FunctionInput } from '@/workflow/types/FunctionInput';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
export const getDefaultFunctionInputFromInputSchema = (
|
|
||||||
inputSchema: InputSchema | undefined,
|
|
||||||
): FunctionInput => {
|
|
||||||
return isDefined(inputSchema)
|
|
||||||
? Object.entries(inputSchema).reduce((acc, [key, value]) => {
|
|
||||||
if (['string', 'number', 'boolean'].includes(value.type)) {
|
|
||||||
acc[key] = null;
|
|
||||||
} else if (value.type === 'object') {
|
|
||||||
acc[key] = isDefined(value.properties)
|
|
||||||
? getDefaultFunctionInputFromInputSchema(value.properties)
|
|
||||||
: {};
|
|
||||||
} else if (value.type === 'array' && isDefined(value.items)) {
|
|
||||||
acc[key] = [];
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {} as FunctionInput)
|
|
||||||
: {};
|
|
||||||
};
|
|
@ -3,7 +3,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
|
|||||||
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
|
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
|
||||||
import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput';
|
import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput';
|
||||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||||
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
import { WorkflowStepHeader } from '@/workflow/components/WorkflowStepHeader';
|
||||||
import { WorkflowVariablePicker } from '@/workflow/components/WorkflowVariablePicker';
|
import { WorkflowVariablePicker } from '@/workflow/components/WorkflowVariablePicker';
|
||||||
import { WorkflowCreateRecordAction } from '@/workflow/types/Workflow';
|
import { WorkflowCreateRecordAction } from '@/workflow/types/Workflow';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
@ -17,6 +17,7 @@ import {
|
|||||||
import { JsonValue } from 'type-fest';
|
import { JsonValue } from 'type-fest';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
|
import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
|
||||||
|
|
||||||
type WorkflowEditActionFormCreateRecordProps = {
|
type WorkflowEditActionFormCreateRecordProps = {
|
||||||
action: WorkflowCreateRecordAction;
|
action: WorkflowCreateRecordAction;
|
||||||
@ -136,58 +137,61 @@ export const WorkflowEditActionFormCreateRecord = ({
|
|||||||
const headerTitle = isDefined(action.name) ? action.name : `Create Record`;
|
const headerTitle = isDefined(action.name) ? action.name : `Create Record`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkflowEditGenericFormBase
|
<>
|
||||||
onTitleChange={(newName: string) => {
|
<WorkflowStepHeader
|
||||||
if (actionOptions.readonly === true) {
|
onTitleChange={(newName: string) => {
|
||||||
return;
|
if (actionOptions.readonly === true) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
actionOptions.onActionUpdate({
|
actionOptions.onActionUpdate({
|
||||||
...action,
|
...action,
|
||||||
name: newName,
|
name: newName,
|
||||||
});
|
});
|
||||||
}}
|
|
||||||
Icon={IconAddressBook}
|
|
||||||
iconColor={theme.font.color.tertiary}
|
|
||||||
initialTitle={headerTitle}
|
|
||||||
headerType="Action"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
dropdownId="workflow-edit-action-record-create-object-name"
|
|
||||||
label="Object"
|
|
||||||
fullWidth
|
|
||||||
disabled={isFormDisabled}
|
|
||||||
value={formData.objectName}
|
|
||||||
emptyOption={{ label: 'Select an option', value: '' }}
|
|
||||||
options={availableMetadata}
|
|
||||||
onChange={(updatedObjectName) => {
|
|
||||||
const newFormData: CreateRecordFormData = {
|
|
||||||
objectName: updatedObjectName,
|
|
||||||
};
|
|
||||||
|
|
||||||
setFormData(newFormData);
|
|
||||||
|
|
||||||
saveAction(newFormData);
|
|
||||||
}}
|
}}
|
||||||
|
Icon={IconAddressBook}
|
||||||
|
iconColor={theme.font.color.tertiary}
|
||||||
|
initialTitle={headerTitle}
|
||||||
|
headerType="Action"
|
||||||
/>
|
/>
|
||||||
|
<WorkflowStepBody>
|
||||||
|
<Select
|
||||||
|
dropdownId="workflow-edit-action-record-create-object-name"
|
||||||
|
label="Object"
|
||||||
|
fullWidth
|
||||||
|
disabled={isFormDisabled}
|
||||||
|
value={formData.objectName}
|
||||||
|
emptyOption={{ label: 'Select an option', value: '' }}
|
||||||
|
options={availableMetadata}
|
||||||
|
onChange={(updatedObjectName) => {
|
||||||
|
const newFormData: CreateRecordFormData = {
|
||||||
|
objectName: updatedObjectName,
|
||||||
|
};
|
||||||
|
|
||||||
<HorizontalSeparator noMargin />
|
setFormData(newFormData);
|
||||||
|
|
||||||
{inlineFieldDefinitions.map((field) => {
|
saveAction(newFormData);
|
||||||
const currentValue = formData[field.metadata.fieldName] as JsonValue;
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
return (
|
<HorizontalSeparator noMargin />
|
||||||
<FormFieldInput
|
|
||||||
key={field.metadata.fieldName}
|
{inlineFieldDefinitions.map((field) => {
|
||||||
defaultValue={currentValue}
|
const currentValue = formData[field.metadata.fieldName] as JsonValue;
|
||||||
field={field}
|
|
||||||
onPersist={(value) => {
|
return (
|
||||||
handleFieldChange(field.metadata.fieldName, value);
|
<FormFieldInput
|
||||||
}}
|
key={field.metadata.fieldName}
|
||||||
VariablePicker={WorkflowVariablePicker}
|
defaultValue={currentValue}
|
||||||
/>
|
field={field}
|
||||||
);
|
onPersist={(value) => {
|
||||||
})}
|
handleFieldChange(field.metadata.fieldName, value);
|
||||||
</WorkflowEditGenericFormBase>
|
}}
|
||||||
|
VariablePicker={WorkflowVariablePicker}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</WorkflowStepBody>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||||
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
import { WorkflowStepHeader } from '@/workflow/components/WorkflowStepHeader';
|
||||||
import { WorkflowSingleRecordPicker } from '@/workflow/components/WorkflowSingleRecordPicker';
|
import { WorkflowSingleRecordPicker } from '@/workflow/components/WorkflowSingleRecordPicker';
|
||||||
import { WorkflowDeleteRecordAction } from '@/workflow/types/Workflow';
|
import { WorkflowDeleteRecordAction } from '@/workflow/types/Workflow';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
@ -14,6 +14,7 @@ import {
|
|||||||
|
|
||||||
import { JsonValue } from 'type-fest';
|
import { JsonValue } from 'type-fest';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
|
||||||
|
|
||||||
type WorkflowEditActionFormDeleteRecordProps = {
|
type WorkflowEditActionFormDeleteRecordProps = {
|
||||||
action: WorkflowDeleteRecordAction;
|
action: WorkflowDeleteRecordAction;
|
||||||
@ -118,52 +119,55 @@ export const WorkflowEditActionFormDeleteRecord = ({
|
|||||||
const headerTitle = isDefined(action.name) ? action.name : `Delete Record`;
|
const headerTitle = isDefined(action.name) ? action.name : `Delete Record`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkflowEditGenericFormBase
|
<>
|
||||||
onTitleChange={(newName: string) => {
|
<WorkflowStepHeader
|
||||||
if (actionOptions.readonly === true) {
|
onTitleChange={(newName: string) => {
|
||||||
return;
|
if (actionOptions.readonly === true) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
actionOptions.onActionUpdate({
|
actionOptions.onActionUpdate({
|
||||||
...action,
|
...action,
|
||||||
name: newName,
|
name: newName,
|
||||||
});
|
});
|
||||||
}}
|
|
||||||
Icon={IconAddressBook}
|
|
||||||
iconColor={theme.font.color.tertiary}
|
|
||||||
initialTitle={headerTitle}
|
|
||||||
headerType="Action"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
dropdownId="workflow-edit-action-record-delete-object-name"
|
|
||||||
label="Object"
|
|
||||||
fullWidth
|
|
||||||
disabled={isFormDisabled}
|
|
||||||
value={formData.objectName}
|
|
||||||
emptyOption={{ label: 'Select an option', value: '' }}
|
|
||||||
options={availableMetadata}
|
|
||||||
onChange={(objectName) => {
|
|
||||||
const newFormData: DeleteRecordFormData = {
|
|
||||||
objectName,
|
|
||||||
objectRecordId: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
setFormData(newFormData);
|
|
||||||
|
|
||||||
saveAction(newFormData);
|
|
||||||
}}
|
}}
|
||||||
|
Icon={IconAddressBook}
|
||||||
|
iconColor={theme.font.color.tertiary}
|
||||||
|
initialTitle={headerTitle}
|
||||||
|
headerType="Action"
|
||||||
/>
|
/>
|
||||||
|
<WorkflowStepBody>
|
||||||
|
<Select
|
||||||
|
dropdownId="workflow-edit-action-record-delete-object-name"
|
||||||
|
label="Object"
|
||||||
|
fullWidth
|
||||||
|
disabled={isFormDisabled}
|
||||||
|
value={formData.objectName}
|
||||||
|
emptyOption={{ label: 'Select an option', value: '' }}
|
||||||
|
options={availableMetadata}
|
||||||
|
onChange={(objectName) => {
|
||||||
|
const newFormData: DeleteRecordFormData = {
|
||||||
|
objectName,
|
||||||
|
objectRecordId: '',
|
||||||
|
};
|
||||||
|
|
||||||
<HorizontalSeparator noMargin />
|
setFormData(newFormData);
|
||||||
|
|
||||||
<WorkflowSingleRecordPicker
|
saveAction(newFormData);
|
||||||
label="Record"
|
}}
|
||||||
onChange={(objectRecordId) =>
|
/>
|
||||||
handleFieldChange('objectRecordId', objectRecordId)
|
|
||||||
}
|
<HorizontalSeparator noMargin />
|
||||||
objectNameSingular={formData.objectName}
|
|
||||||
defaultValue={formData.objectRecordId}
|
<WorkflowSingleRecordPicker
|
||||||
/>
|
label="Record"
|
||||||
</WorkflowEditGenericFormBase>
|
onChange={(objectRecordId) =>
|
||||||
|
handleFieldChange('objectRecordId', objectRecordId)
|
||||||
|
}
|
||||||
|
objectNameSingular={formData.objectName}
|
||||||
|
defaultValue={formData.objectRecordId}
|
||||||
|
/>
|
||||||
|
</WorkflowStepBody>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,7 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
|||||||
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
||||||
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
|
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
|
||||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||||
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
import { WorkflowStepHeader } from '@/workflow/components/WorkflowStepHeader';
|
||||||
import { WorkflowVariablePicker } from '@/workflow/components/WorkflowVariablePicker';
|
import { WorkflowVariablePicker } from '@/workflow/components/WorkflowVariablePicker';
|
||||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||||
import { WorkflowSendEmailAction } from '@/workflow/types/Workflow';
|
import { WorkflowSendEmailAction } from '@/workflow/types/Workflow';
|
||||||
@ -15,6 +15,7 @@ import { Controller, useForm } from 'react-hook-form';
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { IconMail, IconPlus, isDefined } from 'twenty-ui';
|
import { IconMail, IconPlus, isDefined } from 'twenty-ui';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
|
||||||
|
|
||||||
type WorkflowEditActionFormSendEmailProps = {
|
type WorkflowEditActionFormSendEmailProps = {
|
||||||
action: WorkflowSendEmailAction;
|
action: WorkflowSendEmailAction;
|
||||||
@ -171,99 +172,104 @@ export const WorkflowEditActionFormSendEmail = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
!loading && (
|
!loading && (
|
||||||
<WorkflowEditGenericFormBase
|
<>
|
||||||
onTitleChange={(newName: string) => {
|
<WorkflowStepHeader
|
||||||
if (actionOptions.readonly === true) {
|
onTitleChange={(newName: string) => {
|
||||||
return;
|
if (actionOptions.readonly === true) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
actionOptions.onActionUpdate({
|
actionOptions.onActionUpdate({
|
||||||
...action,
|
...action,
|
||||||
name: newName,
|
name: newName,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
Icon={IconMail}
|
Icon={IconMail}
|
||||||
iconColor={theme.color.blue}
|
iconColor={theme.color.blue}
|
||||||
initialTitle={headerTitle}
|
initialTitle={headerTitle}
|
||||||
headerType="Email"
|
headerType="Email"
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
name="connectedAccountId"
|
|
||||||
control={form.control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Select
|
|
||||||
dropdownId="select-connected-account-id"
|
|
||||||
label="Account"
|
|
||||||
fullWidth
|
|
||||||
emptyOption={emptyOption}
|
|
||||||
value={field.value}
|
|
||||||
options={connectedAccountOptions}
|
|
||||||
callToActionButton={{
|
|
||||||
onClick: () =>
|
|
||||||
triggerApisOAuth('google', { redirectLocation: redirectUrl }),
|
|
||||||
Icon: IconPlus,
|
|
||||||
text: 'Add account',
|
|
||||||
}}
|
|
||||||
onChange={(connectedAccountId) => {
|
|
||||||
field.onChange(connectedAccountId);
|
|
||||||
handleSave(true);
|
|
||||||
}}
|
|
||||||
disabled={actionOptions.readonly}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<Controller
|
<WorkflowStepBody>
|
||||||
name="email"
|
<Controller
|
||||||
control={form.control}
|
name="connectedAccountId"
|
||||||
render={({ field }) => (
|
control={form.control}
|
||||||
<FormTextFieldInput
|
render={({ field }) => (
|
||||||
label="Email"
|
<Select
|
||||||
placeholder="Enter receiver email"
|
dropdownId="select-connected-account-id"
|
||||||
readonly={actionOptions.readonly}
|
label="Account"
|
||||||
defaultValue={field.value}
|
fullWidth
|
||||||
onPersist={(value) => {
|
emptyOption={emptyOption}
|
||||||
field.onChange(value);
|
value={field.value}
|
||||||
handleSave();
|
options={connectedAccountOptions}
|
||||||
}}
|
callToActionButton={{
|
||||||
VariablePicker={WorkflowVariablePicker}
|
onClick: () =>
|
||||||
/>
|
triggerApisOAuth('google', {
|
||||||
)}
|
redirectLocation: redirectUrl,
|
||||||
/>
|
}),
|
||||||
<Controller
|
Icon: IconPlus,
|
||||||
name="subject"
|
text: 'Add account',
|
||||||
control={form.control}
|
}}
|
||||||
render={({ field }) => (
|
onChange={(connectedAccountId) => {
|
||||||
<FormTextFieldInput
|
field.onChange(connectedAccountId);
|
||||||
label="Subject"
|
handleSave(true);
|
||||||
placeholder="Enter email subject"
|
}}
|
||||||
readonly={actionOptions.readonly}
|
disabled={actionOptions.readonly}
|
||||||
defaultValue={field.value}
|
/>
|
||||||
onPersist={(value) => {
|
)}
|
||||||
field.onChange(value);
|
/>
|
||||||
handleSave();
|
<Controller
|
||||||
}}
|
name="email"
|
||||||
VariablePicker={WorkflowVariablePicker}
|
control={form.control}
|
||||||
/>
|
render={({ field }) => (
|
||||||
)}
|
<FormTextFieldInput
|
||||||
/>
|
label="Email"
|
||||||
<Controller
|
placeholder="Enter receiver email"
|
||||||
name="body"
|
readonly={actionOptions.readonly}
|
||||||
control={form.control}
|
defaultValue={field.value}
|
||||||
render={({ field }) => (
|
onPersist={(value) => {
|
||||||
<FormTextFieldInput
|
field.onChange(value);
|
||||||
label="Body"
|
handleSave();
|
||||||
placeholder="Enter email body"
|
}}
|
||||||
readonly={actionOptions.readonly}
|
VariablePicker={WorkflowVariablePicker}
|
||||||
defaultValue={field.value}
|
/>
|
||||||
onPersist={(value) => {
|
)}
|
||||||
field.onChange(value);
|
/>
|
||||||
handleSave();
|
<Controller
|
||||||
}}
|
name="subject"
|
||||||
VariablePicker={WorkflowVariablePicker}
|
control={form.control}
|
||||||
/>
|
render={({ field }) => (
|
||||||
)}
|
<FormTextFieldInput
|
||||||
/>
|
label="Subject"
|
||||||
</WorkflowEditGenericFormBase>
|
placeholder="Enter email subject"
|
||||||
|
readonly={actionOptions.readonly}
|
||||||
|
defaultValue={field.value}
|
||||||
|
onPersist={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
handleSave();
|
||||||
|
}}
|
||||||
|
VariablePicker={WorkflowVariablePicker}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="body"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormTextFieldInput
|
||||||
|
label="Body"
|
||||||
|
placeholder="Enter email body"
|
||||||
|
readonly={actionOptions.readonly}
|
||||||
|
defaultValue={field.value}
|
||||||
|
onPersist={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
handleSave();
|
||||||
|
}}
|
||||||
|
VariablePicker={WorkflowVariablePicker}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</WorkflowStepBody>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,48 +1,53 @@
|
|||||||
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
|
||||||
import { StyledFormCompositeFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormCompositeFieldInputContainer';
|
|
||||||
import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages';
|
import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages';
|
||||||
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||||
import { useUpdateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useUpdateOneServerlessFunction';
|
import { useUpdateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useUpdateOneServerlessFunction';
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { WorkflowStepHeader } from '@/workflow/components/WorkflowStepHeader';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
|
||||||
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
|
||||||
import { WorkflowVariablePicker } from '@/workflow/components/WorkflowVariablePicker';
|
|
||||||
import { useGetUpdatableWorkflowVersion } from '@/workflow/hooks/useGetUpdatableWorkflowVersion';
|
import { useGetUpdatableWorkflowVersion } from '@/workflow/hooks/useGetUpdatableWorkflowVersion';
|
||||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||||
import { FunctionInput } from '@/workflow/types/FunctionInput';
|
|
||||||
import { WorkflowCodeAction } from '@/workflow/types/Workflow';
|
import { WorkflowCodeAction } from '@/workflow/types/Workflow';
|
||||||
import { getDefaultFunctionInputFromInputSchema } from '@/workflow/utils/getDefaultFunctionInputFromInputSchema';
|
|
||||||
import { getFunctionInputSchema } from '@/workflow/utils/getFunctionInputSchema';
|
|
||||||
import { setNestedValue } from '@/workflow/utils/setNestedValue';
|
import { setNestedValue } from '@/workflow/utils/setNestedValue';
|
||||||
import { mergeDefaultFunctionInputAndFunctionInput } from '@/workflow/workflow-actions/utils/mergeDefaultFunctionInputAndFunctionInput';
|
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { Monaco } from '@monaco-editor/react';
|
import { Monaco } from '@monaco-editor/react';
|
||||||
import { editor } from 'monaco-editor';
|
import { editor } from 'monaco-editor';
|
||||||
import { AutoTypings } from 'monaco-editor-auto-typings';
|
import { AutoTypings } from 'monaco-editor-auto-typings';
|
||||||
import { Fragment, ReactNode, useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import {
|
import { CodeEditor, IconCode, isDefined, IconPlayerPlay } from 'twenty-ui';
|
||||||
CodeEditor,
|
|
||||||
HorizontalSeparator,
|
|
||||||
IconCode,
|
|
||||||
isDefined,
|
|
||||||
} from 'twenty-ui';
|
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
import { usePreventOverlapCallback } from '~/hooks/usePreventOverlapCallback';
|
import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
|
||||||
|
import { TabList } from '@/ui/layout/tab/components/TabList';
|
||||||
|
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||||
|
import { WorkflowVariablePicker } from '@/workflow/components/WorkflowVariablePicker';
|
||||||
|
import { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState';
|
||||||
|
import { ServerlessFunctionExecutionResult } from '@/serverless-functions/components/ServerlessFunctionExecutionResult';
|
||||||
|
import { INDEX_FILE_PATH } from '@/serverless-functions/constants/IndexFilePath';
|
||||||
|
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||||
|
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
|
||||||
|
import { CmdEnterActionButton } from '@/action-menu/components/CmdEnterActionButton';
|
||||||
|
import { useTestServerlessFunction } from '@/serverless-functions/hooks/useTestServerlessFunction';
|
||||||
|
import { getFunctionOutputSchema } from '@/serverless-functions/utils/getFunctionOutputSchema';
|
||||||
|
import { getFunctionInputFromSourceCode } from '@/serverless-functions/utils/getFunctionInputFromSourceCode';
|
||||||
|
import { mergeDefaultFunctionInputAndFunctionInput } from '@/serverless-functions/utils/mergeDefaultFunctionInputAndFunctionInput';
|
||||||
|
import { WorkflowEditActionFormServerlessFunctionFields } from '@/workflow/workflow-actions/components/WorkflowEditActionFormServerlessFunctionFields';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledCodeEditorContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledLabel = styled.div`
|
const StyledTabList = styled(TabList)`
|
||||||
color: ${({ theme }) => theme.font.color.light};
|
background: ${({ theme }) => theme.background.secondary};
|
||||||
font-size: ${({ theme }) => theme.font.size.md};
|
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
|
||||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type WorkflowEditActionFormServerlessFunctionProps = {
|
type WorkflowEditActionFormServerlessFunctionProps = {
|
||||||
@ -53,10 +58,7 @@ type WorkflowEditActionFormServerlessFunctionProps = {
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
readonly?: false;
|
readonly?: false;
|
||||||
onActionUpdate: (
|
onActionUpdate: (action: WorkflowCodeAction) => void;
|
||||||
action: WorkflowCodeAction,
|
|
||||||
shouldUpdateStepOutput?: boolean,
|
|
||||||
) => void;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -64,14 +66,14 @@ type ServerlessFunctionInputFormData = {
|
|||||||
[field: string]: string | ServerlessFunctionInputFormData;
|
[field: string]: string | ServerlessFunctionInputFormData;
|
||||||
};
|
};
|
||||||
|
|
||||||
const INDEX_FILE_PATH = 'src/index.ts';
|
const TAB_LIST_COMPONENT_ID = 'serverless-function-code-step';
|
||||||
|
|
||||||
export const WorkflowEditActionFormServerlessFunction = ({
|
export const WorkflowEditActionFormServerlessFunction = ({
|
||||||
action,
|
action,
|
||||||
actionOptions,
|
actionOptions,
|
||||||
}: WorkflowEditActionFormServerlessFunctionProps) => {
|
}: WorkflowEditActionFormServerlessFunctionProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { activeTabId, setActiveTabId } = useTabList(TAB_LIST_COMPONENT_ID);
|
||||||
const { updateOneServerlessFunction } = useUpdateOneServerlessFunction();
|
const { updateOneServerlessFunction } = useUpdateOneServerlessFunction();
|
||||||
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
|
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
|
||||||
const serverlessFunctionId = action.settings.input.serverlessFunctionId;
|
const serverlessFunctionId = action.settings.input.serverlessFunctionId;
|
||||||
@ -81,6 +83,9 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
|||||||
id: serverlessFunctionId,
|
id: serverlessFunctionId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [serverlessFunctionTestData, setServerlessFunctionTestData] =
|
||||||
|
useRecoilState(serverlessFunctionTestDataFamilyState(serverlessFunctionId));
|
||||||
|
|
||||||
const [functionInput, setFunctionInput] =
|
const [functionInput, setFunctionInput] =
|
||||||
useState<ServerlessFunctionInputFormData>(
|
useState<ServerlessFunctionInputFormData>(
|
||||||
action.settings.input.serverlessFunctionInput,
|
action.settings.input.serverlessFunctionInput,
|
||||||
@ -89,83 +94,80 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
|||||||
const { formValues, setFormValues, loading } =
|
const { formValues, setFormValues, loading } =
|
||||||
useServerlessFunctionUpdateFormState(serverlessFunctionId);
|
useServerlessFunctionUpdateFormState(serverlessFunctionId);
|
||||||
|
|
||||||
const headerTitle = action.name || 'Code - Serverless Function';
|
const updateOutputSchemaFromTestResult = async (testResult: object) => {
|
||||||
|
if (actionOptions.readonly === true) {
|
||||||
const save = async () => {
|
return;
|
||||||
try {
|
|
||||||
await updateOneServerlessFunction({
|
|
||||||
id: serverlessFunctionId,
|
|
||||||
name: formValues.name,
|
|
||||||
description: formValues.description,
|
|
||||||
code: formValues.code,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
enqueueSnackBar(
|
|
||||||
(err as Error)?.message || 'An error occurred while updating function',
|
|
||||||
{
|
|
||||||
variant: SnackBarVariant.Error,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
const newOutputSchema = getFunctionOutputSchema(testResult);
|
||||||
|
updateAction({
|
||||||
|
...action,
|
||||||
|
settings: { ...action.settings, outputSchema: newOutputSchema },
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = usePreventOverlapCallback(save, 1000);
|
const { testServerlessFunction } = useTestServerlessFunction(
|
||||||
|
serverlessFunctionId,
|
||||||
|
updateOutputSchemaFromTestResult,
|
||||||
|
);
|
||||||
|
|
||||||
const onCodeChange = async (value: string) => {
|
const handleSave = useDebouncedCallback(async () => {
|
||||||
|
await updateOneServerlessFunction({
|
||||||
|
id: serverlessFunctionId,
|
||||||
|
name: formValues.name,
|
||||||
|
description: formValues.description,
|
||||||
|
code: formValues.code,
|
||||||
|
});
|
||||||
|
}, 1_000);
|
||||||
|
|
||||||
|
const onCodeChange = async (newCode: string) => {
|
||||||
if (actionOptions.readonly === true) {
|
if (actionOptions.readonly === true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setFormValues((prevState) => ({
|
setFormValues((prevState) => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
code: { ...prevState.code, [INDEX_FILE_PATH]: value },
|
code: { ...prevState.code, [INDEX_FILE_PATH]: newCode },
|
||||||
}));
|
}));
|
||||||
await handleSave();
|
await handleSave();
|
||||||
await handleUpdateFunctionInputSchema();
|
await handleUpdateFunctionInputSchema(newCode);
|
||||||
};
|
|
||||||
|
|
||||||
const updateFunctionInputSchema = async () => {
|
|
||||||
if (actionOptions.readonly === true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const sourceCode = formValues.code?.[INDEX_FILE_PATH];
|
|
||||||
if (!isDefined(sourceCode)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const functionInputSchema = getFunctionInputSchema(sourceCode);
|
|
||||||
const newMergedInputSchema = mergeDefaultFunctionInputAndFunctionInput({
|
|
||||||
defaultFunctionInput:
|
|
||||||
getDefaultFunctionInputFromInputSchema(functionInputSchema),
|
|
||||||
functionInput: action.settings.input.serverlessFunctionInput,
|
|
||||||
});
|
|
||||||
|
|
||||||
setFunctionInput(newMergedInputSchema);
|
|
||||||
await updateFunctionInput(newMergedInputSchema);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateFunctionInputSchema = useDebouncedCallback(
|
const handleUpdateFunctionInputSchema = useDebouncedCallback(
|
||||||
updateFunctionInputSchema,
|
async (sourceCode: string) => {
|
||||||
100,
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateFunctionInput = useDebouncedCallback(
|
|
||||||
async (newFunctionInput: object, shouldUpdateStepOutput = true) => {
|
|
||||||
if (actionOptions.readonly === true) {
|
if (actionOptions.readonly === true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
actionOptions.onActionUpdate(
|
if (!isDefined(sourceCode)) {
|
||||||
{
|
return;
|
||||||
...action,
|
}
|
||||||
settings: {
|
|
||||||
...action.settings,
|
const newFunctionInput = getFunctionInputFromSourceCode(sourceCode);
|
||||||
input: {
|
const newMergedInput = mergeDefaultFunctionInputAndFunctionInput({
|
||||||
...action.settings.input,
|
newInput: newFunctionInput,
|
||||||
serverlessFunctionInput: newFunctionInput,
|
oldInput: action.settings.input.serverlessFunctionInput,
|
||||||
},
|
});
|
||||||
|
const newMergedTestInput = mergeDefaultFunctionInputAndFunctionInput({
|
||||||
|
newInput: newFunctionInput,
|
||||||
|
oldInput: serverlessFunctionTestData.input,
|
||||||
|
});
|
||||||
|
|
||||||
|
setFunctionInput(newMergedInput);
|
||||||
|
setServerlessFunctionTestData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
input: newMergedTestInput,
|
||||||
|
}));
|
||||||
|
|
||||||
|
updateAction({
|
||||||
|
...action,
|
||||||
|
settings: {
|
||||||
|
...action.settings,
|
||||||
|
outputSchema: {},
|
||||||
|
input: {
|
||||||
|
...action.settings.input,
|
||||||
|
serverlessFunctionInput: newMergedInput,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
shouldUpdateStepOutput,
|
});
|
||||||
);
|
|
||||||
},
|
},
|
||||||
1_000,
|
1_000,
|
||||||
);
|
);
|
||||||
@ -175,63 +177,33 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
|||||||
|
|
||||||
setFunctionInput(updatedFunctionInput);
|
setFunctionInput(updatedFunctionInput);
|
||||||
|
|
||||||
await updateFunctionInput(updatedFunctionInput, false);
|
updateAction({
|
||||||
|
...action,
|
||||||
|
settings: {
|
||||||
|
...action.settings,
|
||||||
|
input: {
|
||||||
|
...action.settings.input,
|
||||||
|
serverlessFunctionInput: updatedFunctionInput,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderFields = (
|
const handleTestInputChange = async (value: any, path: string[]) => {
|
||||||
functionInput: FunctionInput,
|
const updatedTestFunctionInput = setNestedValue(
|
||||||
path: string[] = [],
|
serverlessFunctionTestData.input,
|
||||||
isRoot = true,
|
path,
|
||||||
): ReactNode[] => {
|
value,
|
||||||
const displaySeparator = (functionInput: FunctionInput) => {
|
);
|
||||||
const keys = Object.keys(functionInput);
|
setServerlessFunctionTestData((prev) => ({
|
||||||
if (keys.length > 1) {
|
...prev,
|
||||||
return true;
|
input: updatedTestFunctionInput,
|
||||||
}
|
}));
|
||||||
if (keys.length === 1) {
|
};
|
||||||
const subKeys = Object.keys(functionInput[keys[0]]);
|
|
||||||
return subKeys.length > 0;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
return Object.entries(functionInput).map(([inputKey, inputValue]) => {
|
const handleRunFunction = async () => {
|
||||||
const currentPath = [...path, inputKey];
|
await testServerlessFunction();
|
||||||
const pathKey = currentPath.join('.');
|
setActiveTabId('test');
|
||||||
|
|
||||||
if (inputValue !== null && typeof inputValue === 'object') {
|
|
||||||
if (isRoot) {
|
|
||||||
return (
|
|
||||||
<Fragment key={pathKey}>
|
|
||||||
{displaySeparator(functionInput) && (
|
|
||||||
<HorizontalSeparator noMargin />
|
|
||||||
)}
|
|
||||||
{renderFields(inputValue, currentPath, false)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<StyledContainer key={pathKey}>
|
|
||||||
<StyledLabel>{inputKey}</StyledLabel>
|
|
||||||
<StyledFormCompositeFieldInputContainer>
|
|
||||||
{renderFields(inputValue, currentPath, false)}
|
|
||||||
</StyledFormCompositeFieldInputContainer>
|
|
||||||
</StyledContainer>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<FormTextFieldInput
|
|
||||||
key={pathKey}
|
|
||||||
label={inputKey}
|
|
||||||
placeholder="Enter value"
|
|
||||||
defaultValue={inputValue ? `${inputValue}` : ''}
|
|
||||||
readonly={actionOptions.readonly}
|
|
||||||
onPersist={(value) => handleInputChange(value, currentPath)}
|
|
||||||
VariablePicker={WorkflowVariablePicker}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditorDidMount = async (
|
const handleEditorDidMount = async (
|
||||||
@ -247,58 +219,103 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onActionUpdate = (actionUpdate: Partial<WorkflowCodeAction>) => {
|
const updateAction = useDebouncedCallback(
|
||||||
if (actionOptions.readonly === true) {
|
(actionUpdate: Partial<WorkflowCodeAction>) => {
|
||||||
return;
|
if (actionOptions.readonly === true) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
actionOptions?.onActionUpdate(
|
actionOptions.onActionUpdate({
|
||||||
{
|
|
||||||
...action,
|
...action,
|
||||||
...actionUpdate,
|
...actionUpdate,
|
||||||
},
|
});
|
||||||
false,
|
},
|
||||||
);
|
500,
|
||||||
};
|
);
|
||||||
|
|
||||||
const checkWorkflowUpdatable = async () => {
|
const handleCodeChange = async (value: string) => {
|
||||||
if (actionOptions.readonly === true || !isDefined(workflow)) {
|
if (actionOptions.readonly === true || !isDefined(workflow)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await getUpdatableWorkflowVersion(workflow);
|
await getUpdatableWorkflowVersion(workflow);
|
||||||
|
await onCodeChange(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: 'code', title: 'Code', Icon: IconCode },
|
||||||
|
{ id: 'test', title: 'Test', Icon: IconPlayerPlay },
|
||||||
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFunctionInput(action.settings.input.serverlessFunctionInput);
|
setFunctionInput(action.settings.input.serverlessFunctionInput);
|
||||||
}, [action]);
|
}, [action]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!loading && (
|
!loading && (
|
||||||
<WorkflowEditGenericFormBase
|
<StyledContainer>
|
||||||
onTitleChange={(newName: string) => {
|
<StyledTabList
|
||||||
onActionUpdate({ name: newName });
|
tabListInstanceId={TAB_LIST_COMPONENT_ID}
|
||||||
}}
|
tabs={tabs}
|
||||||
Icon={IconCode}
|
behaveAsLinks={false}
|
||||||
iconColor={theme.color.orange}
|
|
||||||
initialTitle={headerTitle}
|
|
||||||
headerType="Code"
|
|
||||||
>
|
|
||||||
<CodeEditor
|
|
||||||
height={340}
|
|
||||||
value={formValues.code?.[INDEX_FILE_PATH]}
|
|
||||||
language={'typescript'}
|
|
||||||
onChange={async (value) => {
|
|
||||||
await checkWorkflowUpdatable();
|
|
||||||
await onCodeChange(value);
|
|
||||||
}}
|
|
||||||
onMount={handleEditorDidMount}
|
|
||||||
options={{
|
|
||||||
readOnly: actionOptions.readonly,
|
|
||||||
domReadOnly: actionOptions.readonly,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{renderFields(functionInput)}
|
<WorkflowStepHeader
|
||||||
</WorkflowEditGenericFormBase>
|
onTitleChange={(newName: string) => {
|
||||||
|
updateAction({ name: newName });
|
||||||
|
}}
|
||||||
|
Icon={IconCode}
|
||||||
|
iconColor={theme.color.orange}
|
||||||
|
initialTitle={action.name || 'Code - Serverless Function'}
|
||||||
|
headerType="Code"
|
||||||
|
/>
|
||||||
|
<WorkflowStepBody>
|
||||||
|
{activeTabId === 'code' && (
|
||||||
|
<>
|
||||||
|
<WorkflowEditActionFormServerlessFunctionFields
|
||||||
|
functionInput={functionInput}
|
||||||
|
VariablePicker={WorkflowVariablePicker}
|
||||||
|
onInputChange={handleInputChange}
|
||||||
|
readonly={actionOptions.readonly}
|
||||||
|
/>
|
||||||
|
<StyledCodeEditorContainer>
|
||||||
|
<InputLabel>Code</InputLabel>
|
||||||
|
<CodeEditor
|
||||||
|
height={343}
|
||||||
|
value={formValues.code?.[INDEX_FILE_PATH]}
|
||||||
|
language={'typescript'}
|
||||||
|
onChange={handleCodeChange}
|
||||||
|
onMount={handleEditorDidMount}
|
||||||
|
options={{
|
||||||
|
readOnly: actionOptions.readonly,
|
||||||
|
domReadOnly: actionOptions.readonly,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StyledCodeEditorContainer>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{activeTabId === 'test' && (
|
||||||
|
<>
|
||||||
|
<WorkflowEditActionFormServerlessFunctionFields
|
||||||
|
functionInput={serverlessFunctionTestData.input}
|
||||||
|
onInputChange={handleTestInputChange}
|
||||||
|
readonly={actionOptions.readonly}
|
||||||
|
/>
|
||||||
|
<StyledCodeEditorContainer>
|
||||||
|
<InputLabel>Result</InputLabel>
|
||||||
|
<ServerlessFunctionExecutionResult
|
||||||
|
serverlessFunctionTestData={serverlessFunctionTestData}
|
||||||
|
/>
|
||||||
|
</StyledCodeEditorContainer>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</WorkflowStepBody>
|
||||||
|
{activeTabId === 'test' && (
|
||||||
|
<RightDrawerFooter
|
||||||
|
actions={[
|
||||||
|
<CmdEnterActionButton title="Test" onClick={handleRunFunction} />,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</StyledContainer>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
import { FunctionInput } from '@/workflow/types/FunctionInput';
|
||||||
|
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
||||||
|
import { isObject } from '@sniptt/guards';
|
||||||
|
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||||
|
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { FormNestedFieldInputContainer } from '@/object-record/record-field/form-types/components/FormNestedFieldInputContainer';
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const WorkflowEditActionFormServerlessFunctionFields = ({
|
||||||
|
functionInput,
|
||||||
|
path = [],
|
||||||
|
VariablePicker,
|
||||||
|
onInputChange,
|
||||||
|
readonly = false,
|
||||||
|
}: {
|
||||||
|
functionInput: FunctionInput;
|
||||||
|
path?: string[];
|
||||||
|
VariablePicker?: VariablePickerComponent;
|
||||||
|
onInputChange: (value: any, path: string[]) => void;
|
||||||
|
readonly?: boolean;
|
||||||
|
}) => {
|
||||||
|
const renderFields = ({
|
||||||
|
functionInput,
|
||||||
|
path = [],
|
||||||
|
VariablePicker,
|
||||||
|
onInputChange,
|
||||||
|
readonly = false,
|
||||||
|
}: {
|
||||||
|
functionInput: FunctionInput;
|
||||||
|
path?: string[];
|
||||||
|
VariablePicker?: VariablePickerComponent;
|
||||||
|
onInputChange: (value: any, path: string[]) => void;
|
||||||
|
readonly?: boolean;
|
||||||
|
}): ReactNode[] => {
|
||||||
|
return Object.entries(functionInput).map(([inputKey, inputValue]) => {
|
||||||
|
const currentPath = [...path, inputKey];
|
||||||
|
const pathKey = currentPath.join('.');
|
||||||
|
if (inputValue !== null && isObject(inputValue)) {
|
||||||
|
return (
|
||||||
|
<StyledContainer key={pathKey}>
|
||||||
|
<InputLabel>{inputKey}</InputLabel>
|
||||||
|
<FormNestedFieldInputContainer>
|
||||||
|
{renderFields({
|
||||||
|
functionInput: inputValue,
|
||||||
|
path: currentPath,
|
||||||
|
VariablePicker,
|
||||||
|
onInputChange,
|
||||||
|
})}
|
||||||
|
</FormNestedFieldInputContainer>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<FormTextFieldInput
|
||||||
|
key={pathKey}
|
||||||
|
label={inputKey}
|
||||||
|
placeholder="Enter value"
|
||||||
|
defaultValue={inputValue ? `${inputValue}` : ''}
|
||||||
|
readonly={readonly}
|
||||||
|
onPersist={(value) => onInputChange(value, currentPath)}
|
||||||
|
VariablePicker={VariablePicker}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderFields({
|
||||||
|
functionInput,
|
||||||
|
path,
|
||||||
|
VariablePicker,
|
||||||
|
onInputChange,
|
||||||
|
readonly,
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||||
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
import { WorkflowStepHeader } from '@/workflow/components/WorkflowStepHeader';
|
||||||
import { WorkflowSingleRecordPicker } from '@/workflow/components/WorkflowSingleRecordPicker';
|
import { WorkflowSingleRecordPicker } from '@/workflow/components/WorkflowSingleRecordPicker';
|
||||||
import { WorkflowUpdateRecordAction } from '@/workflow/types/Workflow';
|
import { WorkflowUpdateRecordAction } from '@/workflow/types/Workflow';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
@ -14,6 +14,7 @@ import {
|
|||||||
|
|
||||||
import { JsonValue } from 'type-fest';
|
import { JsonValue } from 'type-fest';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
|
||||||
|
|
||||||
type WorkflowEditActionFormUpdateRecordProps = {
|
type WorkflowEditActionFormUpdateRecordProps = {
|
||||||
action: WorkflowUpdateRecordAction;
|
action: WorkflowUpdateRecordAction;
|
||||||
@ -123,52 +124,55 @@ export const WorkflowEditActionFormUpdateRecord = ({
|
|||||||
const headerTitle = isDefined(action.name) ? action.name : `Update Record`;
|
const headerTitle = isDefined(action.name) ? action.name : `Update Record`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkflowEditGenericFormBase
|
<>
|
||||||
onTitleChange={(newName: string) => {
|
<WorkflowStepHeader
|
||||||
if (actionOptions.readonly === true) {
|
onTitleChange={(newName: string) => {
|
||||||
return;
|
if (actionOptions.readonly === true) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
actionOptions.onActionUpdate({
|
actionOptions.onActionUpdate({
|
||||||
...action,
|
...action,
|
||||||
name: newName,
|
name: newName,
|
||||||
});
|
});
|
||||||
}}
|
|
||||||
Icon={IconAddressBook}
|
|
||||||
iconColor={theme.font.color.tertiary}
|
|
||||||
initialTitle={headerTitle}
|
|
||||||
headerType="Action"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
dropdownId="workflow-edit-action-record-update-object-name"
|
|
||||||
label="Object"
|
|
||||||
fullWidth
|
|
||||||
disabled={isFormDisabled}
|
|
||||||
value={formData.objectName}
|
|
||||||
emptyOption={{ label: 'Select an option', value: '' }}
|
|
||||||
options={availableMetadata}
|
|
||||||
onChange={(updatedObjectName) => {
|
|
||||||
const newFormData: UpdateRecordFormData = {
|
|
||||||
objectName: updatedObjectName,
|
|
||||||
objectRecordId: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
setFormData(newFormData);
|
|
||||||
|
|
||||||
saveAction(newFormData);
|
|
||||||
}}
|
}}
|
||||||
|
Icon={IconAddressBook}
|
||||||
|
iconColor={theme.font.color.tertiary}
|
||||||
|
initialTitle={headerTitle}
|
||||||
|
headerType="Action"
|
||||||
/>
|
/>
|
||||||
|
<WorkflowStepBody>
|
||||||
|
<Select
|
||||||
|
dropdownId="workflow-edit-action-record-update-object-name"
|
||||||
|
label="Object"
|
||||||
|
fullWidth
|
||||||
|
disabled={isFormDisabled}
|
||||||
|
value={formData.objectName}
|
||||||
|
emptyOption={{ label: 'Select an option', value: '' }}
|
||||||
|
options={availableMetadata}
|
||||||
|
onChange={(updatedObjectName) => {
|
||||||
|
const newFormData: UpdateRecordFormData = {
|
||||||
|
objectName: updatedObjectName,
|
||||||
|
objectRecordId: '',
|
||||||
|
};
|
||||||
|
|
||||||
<HorizontalSeparator noMargin />
|
setFormData(newFormData);
|
||||||
|
|
||||||
<WorkflowSingleRecordPicker
|
saveAction(newFormData);
|
||||||
label="Record"
|
}}
|
||||||
onChange={(objectRecordId) =>
|
/>
|
||||||
handleFieldChange('objectRecordId', objectRecordId)
|
|
||||||
}
|
<HorizontalSeparator noMargin />
|
||||||
objectNameSingular={formData.objectName}
|
|
||||||
defaultValue={formData.objectRecordId}
|
<WorkflowSingleRecordPicker
|
||||||
/>
|
label="Record"
|
||||||
</WorkflowEditGenericFormBase>
|
onChange={(objectRecordId) =>
|
||||||
|
handleFieldChange('objectRecordId', objectRecordId)
|
||||||
|
}
|
||||||
|
objectNameSingular={formData.objectName}
|
||||||
|
defaultValue={formData.objectRecordId}
|
||||||
|
/>
|
||||||
|
</WorkflowStepBody>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
import { FunctionInput } from '@/workflow/types/FunctionInput';
|
|
||||||
|
|
||||||
export const mergeDefaultFunctionInputAndFunctionInput = ({
|
|
||||||
defaultFunctionInput,
|
|
||||||
functionInput,
|
|
||||||
}: {
|
|
||||||
defaultFunctionInput: FunctionInput;
|
|
||||||
functionInput: FunctionInput;
|
|
||||||
}): FunctionInput => {
|
|
||||||
const result: FunctionInput = {};
|
|
||||||
|
|
||||||
for (const key of Object.keys(defaultFunctionInput)) {
|
|
||||||
if (!(key in functionInput)) {
|
|
||||||
result[key] = defaultFunctionInput[key];
|
|
||||||
} else {
|
|
||||||
if (
|
|
||||||
defaultFunctionInput[key] !== null &&
|
|
||||||
typeof defaultFunctionInput[key] === 'object'
|
|
||||||
) {
|
|
||||||
result[key] = mergeDefaultFunctionInputAndFunctionInput({
|
|
||||||
defaultFunctionInput: defaultFunctionInput[key],
|
|
||||||
functionInput:
|
|
||||||
typeof functionInput[key] === 'object' ? functionInput[key] : {},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
result[key] = functionInput[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
@ -154,7 +154,9 @@ export const PasswordReset = () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
logError(err);
|
logError(err);
|
||||||
enqueueSnackBar(
|
enqueueSnackBar(
|
||||||
(err as Error)?.message || 'An error occurred while updating password',
|
err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: 'An error occurred while updating password',
|
||||||
{
|
{
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
},
|
},
|
||||||
|
@ -14,7 +14,6 @@ import { TableRow } from '@/ui/layout/table/components/TableRow';
|
|||||||
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
|
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
getImageAbsoluteURI,
|
getImageAbsoluteURI,
|
||||||
@ -68,10 +67,9 @@ const StyledContentContainer = styled.div`
|
|||||||
export const SettingsAdminFeatureFlags = () => {
|
export const SettingsAdminFeatureFlags = () => {
|
||||||
const [userIdentifier, setUserIdentifier] = useState('');
|
const [userIdentifier, setUserIdentifier] = useState('');
|
||||||
|
|
||||||
const { activeTabIdState, setActiveTabId } = useTabList(
|
const { activeTabId, setActiveTabId } = useTabList(
|
||||||
SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID,
|
SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID,
|
||||||
);
|
);
|
||||||
const activeTabId = useRecoilValue(activeTabIdState);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
userLookupResult,
|
userLookupResult,
|
||||||
|
@ -78,10 +78,9 @@ export const SettingsObjectDetailPage = () => {
|
|||||||
findActiveObjectMetadataItemBySlug(objectSlug) ??
|
findActiveObjectMetadataItemBySlug(objectSlug) ??
|
||||||
findActiveObjectMetadataItemBySlug(updatedObjectSlug);
|
findActiveObjectMetadataItemBySlug(updatedObjectSlug);
|
||||||
|
|
||||||
const { activeTabIdState } = useTabList(
|
const { activeTabId } = useTabList(
|
||||||
SETTINGS_OBJECT_DETAIL_TABS.COMPONENT_INSTANCE_ID,
|
SETTINGS_OBJECT_DETAIL_TABS.COMPONENT_INSTANCE_ID,
|
||||||
);
|
);
|
||||||
const activeTabId = useRecoilValue(activeTabIdState);
|
|
||||||
|
|
||||||
const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState);
|
const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState);
|
||||||
const isUniqueIndexesEnabled = useIsFeatureEnabled(
|
const isUniqueIndexesEnabled = useIsFeatureEnabled(
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/settings/serverless-functions/states/settingsServerlessFunctionCodeEditorOutputParamsState';
|
|
||||||
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
|
||||||
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
|
||||||
import { useResetRecoilState } from 'recoil';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
export const ResetServerlessFunctionStatesEffect = () => {
|
|
||||||
const resetSettingsServerlessFunctionInput = useResetRecoilState(
|
|
||||||
settingsServerlessFunctionInputState,
|
|
||||||
);
|
|
||||||
const resetSettingsServerlessFunctionOutput = useResetRecoilState(
|
|
||||||
settingsServerlessFunctionOutputState,
|
|
||||||
);
|
|
||||||
const resetSettingsServerlessFunctionCodeEditorOutputParamsState =
|
|
||||||
useResetRecoilState(settingsServerlessFunctionCodeEditorOutputParamsState);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
resetSettingsServerlessFunctionInput();
|
|
||||||
resetSettingsServerlessFunctionOutput();
|
|
||||||
resetSettingsServerlessFunctionCodeEditorOutputParamsState();
|
|
||||||
}, [
|
|
||||||
resetSettingsServerlessFunctionInput,
|
|
||||||
resetSettingsServerlessFunctionOutput,
|
|
||||||
resetSettingsServerlessFunctionCodeEditorOutputParamsState,
|
|
||||||
]);
|
|
||||||
return <></>;
|
|
||||||
};
|
|
@ -4,14 +4,10 @@ import { SettingsServerlessFunctionCodeEditorTab } from '@/settings/serverless-f
|
|||||||
import { SettingsServerlessFunctionMonitoringTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionMonitoringTab';
|
import { SettingsServerlessFunctionMonitoringTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionMonitoringTab';
|
||||||
import { SettingsServerlessFunctionSettingsTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab';
|
import { SettingsServerlessFunctionSettingsTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab';
|
||||||
import { SettingsServerlessFunctionTestTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab';
|
import { SettingsServerlessFunctionTestTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab';
|
||||||
import { SettingsServerlessFunctionTestTabEffect } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTabEffect';
|
|
||||||
import { useExecuteOneServerlessFunction } from '@/settings/serverless-functions/hooks/useExecuteOneServerlessFunction';
|
|
||||||
import { useGetOneServerlessFunctionSourceCode } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunctionSourceCode';
|
import { useGetOneServerlessFunctionSourceCode } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunctionSourceCode';
|
||||||
import { usePublishOneServerlessFunction } from '@/settings/serverless-functions/hooks/usePublishOneServerlessFunction';
|
import { usePublishOneServerlessFunction } from '@/settings/serverless-functions/hooks/usePublishOneServerlessFunction';
|
||||||
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||||
import { useUpdateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useUpdateOneServerlessFunction';
|
import { useUpdateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useUpdateOneServerlessFunction';
|
||||||
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
|
||||||
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
|
||||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
@ -22,63 +18,39 @@ import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
|||||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import {
|
import { IconCode, IconGauge, IconSettings, IconTestPipe } from 'twenty-ui';
|
||||||
IconCode,
|
|
||||||
IconGauge,
|
|
||||||
IconSettings,
|
|
||||||
IconTestPipe,
|
|
||||||
Section,
|
|
||||||
} from 'twenty-ui';
|
|
||||||
import { usePreventOverlapCallback } from '~/hooks/usePreventOverlapCallback';
|
|
||||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { useTestServerlessFunction } from '@/serverless-functions/hooks/useTestServerlessFunction';
|
||||||
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
const TAB_LIST_COMPONENT_ID = 'serverless-function-detail';
|
const TAB_LIST_COMPONENT_ID = 'serverless-function-detail';
|
||||||
|
|
||||||
export const SettingsServerlessFunctionDetail = () => {
|
export const SettingsServerlessFunctionDetail = () => {
|
||||||
const { serverlessFunctionId = '' } = useParams();
|
const { serverlessFunctionId = '' } = useParams();
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
const { activeTabIdState, setActiveTabId } = useTabList(
|
const { activeTabId, setActiveTabId } = useTabList(TAB_LIST_COMPONENT_ID);
|
||||||
TAB_LIST_COMPONENT_ID,
|
|
||||||
);
|
|
||||||
const activeTabId = useRecoilValue(activeTabIdState);
|
|
||||||
const [isCodeValid, setIsCodeValid] = useState(true);
|
const [isCodeValid, setIsCodeValid] = useState(true);
|
||||||
const { executeOneServerlessFunction } = useExecuteOneServerlessFunction();
|
|
||||||
const { updateOneServerlessFunction } = useUpdateOneServerlessFunction();
|
const { updateOneServerlessFunction } = useUpdateOneServerlessFunction();
|
||||||
const { publishOneServerlessFunction } = usePublishOneServerlessFunction();
|
const { publishOneServerlessFunction } = usePublishOneServerlessFunction();
|
||||||
const { formValues, setFormValues, loading } =
|
const { formValues, setFormValues, loading } =
|
||||||
useServerlessFunctionUpdateFormState(serverlessFunctionId);
|
useServerlessFunctionUpdateFormState(serverlessFunctionId);
|
||||||
|
const { testServerlessFunction } =
|
||||||
|
useTestServerlessFunction(serverlessFunctionId);
|
||||||
const { code: latestVersionCode } = useGetOneServerlessFunctionSourceCode({
|
const { code: latestVersionCode } = useGetOneServerlessFunctionSourceCode({
|
||||||
id: serverlessFunctionId,
|
id: serverlessFunctionId,
|
||||||
version: 'latest',
|
version: 'latest',
|
||||||
});
|
});
|
||||||
const setSettingsServerlessFunctionOutput = useSetRecoilState(
|
|
||||||
settingsServerlessFunctionOutputState,
|
|
||||||
);
|
|
||||||
const settingsServerlessFunctionInput = useRecoilValue(
|
|
||||||
settingsServerlessFunctionInputState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const save = async () => {
|
const handleSave = useDebouncedCallback(async () => {
|
||||||
try {
|
await updateOneServerlessFunction({
|
||||||
await updateOneServerlessFunction({
|
id: serverlessFunctionId,
|
||||||
id: serverlessFunctionId,
|
name: formValues.name,
|
||||||
name: formValues.name,
|
description: formValues.description,
|
||||||
description: formValues.description,
|
code: formValues.code,
|
||||||
code: formValues.code,
|
});
|
||||||
});
|
}, 1_000);
|
||||||
} catch (err) {
|
|
||||||
enqueueSnackBar(
|
|
||||||
(err as Error)?.message || 'An error occurred while updating function',
|
|
||||||
{
|
|
||||||
variant: SnackBarVariant.Error,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = usePreventOverlapCallback(save, 1000);
|
|
||||||
|
|
||||||
const onChange = (key: string) => {
|
const onChange = (key: string) => {
|
||||||
return async (value: string) => {
|
return async (value: string) => {
|
||||||
@ -143,41 +115,11 @@ export const SettingsServerlessFunctionDetail = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExecute = async () => {
|
const handleTestFunction = async () => {
|
||||||
try {
|
await testServerlessFunction();
|
||||||
const result = await executeOneServerlessFunction({
|
|
||||||
id: serverlessFunctionId,
|
|
||||||
payload: JSON.parse(settingsServerlessFunctionInput),
|
|
||||||
version: 'draft',
|
|
||||||
});
|
|
||||||
setSettingsServerlessFunctionOutput({
|
|
||||||
data: result?.data?.executeOneServerlessFunction?.data
|
|
||||||
? JSON.stringify(
|
|
||||||
result?.data?.executeOneServerlessFunction?.data,
|
|
||||||
null,
|
|
||||||
4,
|
|
||||||
)
|
|
||||||
: undefined,
|
|
||||||
duration: result?.data?.executeOneServerlessFunction?.duration,
|
|
||||||
status: result?.data?.executeOneServerlessFunction?.status,
|
|
||||||
error: result?.data?.executeOneServerlessFunction?.error
|
|
||||||
? JSON.stringify(
|
|
||||||
result?.data?.executeOneServerlessFunction?.error,
|
|
||||||
null,
|
|
||||||
4,
|
|
||||||
)
|
|
||||||
: undefined,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
enqueueSnackBar(
|
|
||||||
(err as Error)?.message || 'An error occurred while executing function',
|
|
||||||
{
|
|
||||||
variant: SnackBarVariant.Error,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
setActiveTabId('test');
|
setActiveTabId('test');
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAnalyticsEnabled = useRecoilValue(isAnalyticsEnabledState);
|
const isAnalyticsEnabled = useRecoilValue(isAnalyticsEnabledState);
|
||||||
|
|
||||||
const isAnalyticsV2Enabled = useIsFeatureEnabled('IS_ANALYTICS_V2_ENABLED');
|
const isAnalyticsV2Enabled = useIsFeatureEnabled('IS_ANALYTICS_V2_ENABLED');
|
||||||
@ -209,7 +151,7 @@ export const SettingsServerlessFunctionDetail = () => {
|
|||||||
return (
|
return (
|
||||||
<SettingsServerlessFunctionCodeEditorTab
|
<SettingsServerlessFunctionCodeEditorTab
|
||||||
files={files}
|
files={files}
|
||||||
handleExecute={handleExecute}
|
handleExecute={handleTestFunction}
|
||||||
handlePublish={handlePublish}
|
handlePublish={handlePublish}
|
||||||
handleReset={handleReset}
|
handleReset={handleReset}
|
||||||
resetDisabled={resetDisabled}
|
resetDisabled={resetDisabled}
|
||||||
@ -220,10 +162,10 @@ export const SettingsServerlessFunctionDetail = () => {
|
|||||||
);
|
);
|
||||||
case 'test':
|
case 'test':
|
||||||
return (
|
return (
|
||||||
<>
|
<SettingsServerlessFunctionTestTab
|
||||||
<SettingsServerlessFunctionTestTabEffect />
|
serverlessFunctionId={serverlessFunctionId}
|
||||||
<SettingsServerlessFunctionTestTab handleExecute={handleExecute} />
|
handleExecute={handleTestFunction}
|
||||||
</>
|
/>
|
||||||
);
|
);
|
||||||
case 'settings':
|
case 'settings':
|
||||||
return (
|
return (
|
||||||
@ -262,9 +204,11 @@ export const SettingsServerlessFunctionDetail = () => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<Section>
|
<TabList
|
||||||
<TabList tabListInstanceId={TAB_LIST_COMPONENT_ID} tabs={tabs} />
|
tabListInstanceId={TAB_LIST_COMPONENT_ID}
|
||||||
</Section>
|
tabs={tabs}
|
||||||
|
behaveAsLinks={false}
|
||||||
|
/>
|
||||||
{renderActiveTabContent()}
|
{renderActiveTabContent()}
|
||||||
</SettingsPageContainer>
|
</SettingsPageContainer>
|
||||||
</SubMenuTopBarContainer>
|
</SubMenuTopBarContainer>
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
import { ResetServerlessFunctionStatesEffect } from '~/pages/settings/serverless-functions/ResetServerlessFunctionStatesEffect';
|
|
||||||
import { SettingsServerlessFunctionDetail } from '~/pages/settings/serverless-functions/SettingsServerlessFunctionDetail';
|
|
||||||
|
|
||||||
export const SettingsServerlessFunctionDetailWrapper = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ResetServerlessFunctionStatesEffect />
|
|
||||||
<SettingsServerlessFunctionDetail />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -55,7 +55,7 @@ export class BaseGraphQLError extends GraphQLError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (extensions?.extensions) {
|
if (extensions?.extensions) {
|
||||||
throw Error(
|
throw new Error(
|
||||||
'Pass extensions directly as the third argument of the ApolloError constructor: `new ' +
|
'Pass extensions directly as the third argument of the ApolloError constructor: `new ' +
|
||||||
'ApolloError(message, code, {myExt: value})`, not `new ApolloError(message, code, ' +
|
'ApolloError(message, code, {myExt: value})`, not `new ApolloError(message, code, ' +
|
||||||
'{extensions: {myExt: value}})`',
|
'{extensions: {myExt: value}})`',
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
export const BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA = {
|
||||||
|
a: null,
|
||||||
|
b: null,
|
||||||
|
};
|
@ -17,11 +17,4 @@ export class UpdateWorkflowVersionStepInput {
|
|||||||
nullable: false,
|
nullable: false,
|
||||||
})
|
})
|
||||||
step: WorkflowAction;
|
step: WorkflowAction;
|
||||||
|
|
||||||
@Field(() => Boolean, {
|
|
||||||
description: 'Boolean to check if we need to update stepOutput',
|
|
||||||
nullable: true,
|
|
||||||
defaultValue: true,
|
|
||||||
})
|
|
||||||
shouldUpdateStepOutput: boolean;
|
|
||||||
}
|
}
|
||||||
|
@ -37,17 +37,12 @@ export class WorkflowVersionStepResolver {
|
|||||||
async updateWorkflowVersionStep(
|
async updateWorkflowVersionStep(
|
||||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
@Args('input')
|
@Args('input')
|
||||||
{
|
{ step, workflowVersionId }: UpdateWorkflowVersionStepInput,
|
||||||
step,
|
|
||||||
workflowVersionId,
|
|
||||||
shouldUpdateStepOutput,
|
|
||||||
}: UpdateWorkflowVersionStepInput,
|
|
||||||
): Promise<WorkflowActionDTO> {
|
): Promise<WorkflowActionDTO> {
|
||||||
return this.workflowVersionStepWorkspaceService.updateWorkflowVersionStep({
|
return this.workflowVersionStepWorkspaceService.updateWorkflowVersionStep({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
workflowVersionId,
|
workflowVersionId,
|
||||||
step,
|
step,
|
||||||
shouldUpdateStepOutput,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ import GraphQLJSON from 'graphql-type-json';
|
|||||||
|
|
||||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||||
import { ServerlessFunctionSyncStatus } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
import { ServerlessFunctionSyncStatus } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||||
import { InputSchema } from 'src/modules/code-introspection/types/input-schema.type';
|
import { InputSchema } from 'src/modules/workflow/workflow-builder/types/input-schema.type';
|
||||||
|
|
||||||
registerEnumType(ServerlessFunctionSyncStatus, {
|
registerEnumType(ServerlessFunctionSyncStatus, {
|
||||||
name: 'ServerlessFunctionSyncStatus',
|
name: 'ServerlessFunctionSyncStatus',
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import { InputSchema } from 'src/modules/code-introspection/types/input-schema.type';
|
import { InputSchema } from 'src/modules/workflow/workflow-builder/types/input-schema.type';
|
||||||
|
|
||||||
export enum ServerlessFunctionSyncStatus {
|
export enum ServerlessFunctionSyncStatus {
|
||||||
NOT_READY = 'NOT_READY',
|
NOT_READY = 'NOT_READY',
|
||||||
|
@ -1,138 +0,0 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
|
|
||||||
import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service';
|
|
||||||
|
|
||||||
describe('CodeIntrospectionService', () => {
|
|
||||||
let service: CodeIntrospectionService;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [CodeIntrospectionService],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
service = module.get<CodeIntrospectionService>(CodeIntrospectionService);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(service).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getFunctionInputSchema', () => {
|
|
||||||
it('should analyze a simple function correctly', () => {
|
|
||||||
const fileContent = `
|
|
||||||
function testFunction(param1: string, param2: number): void {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
const result = service.getFunctionInputSchema(fileContent);
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
param1: { type: 'string' },
|
|
||||||
param2: { type: 'number' },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should analyze a arrow function correctly', () => {
|
|
||||||
const fileContent = `
|
|
||||||
export const main = async (
|
|
||||||
param1: string,
|
|
||||||
param2: number,
|
|
||||||
): Promise<object> => {
|
|
||||||
return params;
|
|
||||||
};
|
|
||||||
`;
|
|
||||||
const result = service.getFunctionInputSchema(fileContent);
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
param1: { type: 'string' },
|
|
||||||
param2: { type: 'number' },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should analyze a complex function correctly', () => {
|
|
||||||
const fileContent = `
|
|
||||||
function testFunction(
|
|
||||||
params: {
|
|
||||||
param1: string;
|
|
||||||
param2: number;
|
|
||||||
param3: boolean;
|
|
||||||
param4: object;
|
|
||||||
param5: { subParam1: string };
|
|
||||||
param6: "my" | "enum";
|
|
||||||
param7: string[];
|
|
||||||
}
|
|
||||||
): void {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
const result = service.getFunctionInputSchema(fileContent);
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
params: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
param1: { type: 'string' },
|
|
||||||
param2: { type: 'number' },
|
|
||||||
param3: { type: 'boolean' },
|
|
||||||
param4: { type: 'object' },
|
|
||||||
param5: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
subParam1: { type: 'string' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
param6: { type: 'string', enum: ['my', 'enum'] },
|
|
||||||
param7: { type: 'array', items: { type: 'string' } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('generateInputData', () => {
|
|
||||||
it('should generate fake data for simple function', () => {
|
|
||||||
const fileContent = `
|
|
||||||
function testFunction(param1: string, param2: number): void {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
const inputSchema = service.getFunctionInputSchema(fileContent);
|
|
||||||
const result = service.generateInputData(inputSchema);
|
|
||||||
|
|
||||||
expect(result).toEqual({ param1: 'generated-string-value', param2: 1 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should generate fake data for complex function', () => {
|
|
||||||
const fileContent = `
|
|
||||||
function testFunction(
|
|
||||||
params: {
|
|
||||||
param1: string;
|
|
||||||
param2: number;
|
|
||||||
param3: boolean;
|
|
||||||
param4: object;
|
|
||||||
param5: { subParam1: string };
|
|
||||||
param6: "my" | "enum";
|
|
||||||
param7: string[];
|
|
||||||
}
|
|
||||||
): void {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const inputSchema = service.getFunctionInputSchema(fileContent);
|
|
||||||
const result = service.generateInputData(inputSchema);
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
params: {
|
|
||||||
param1: 'generated-string-value',
|
|
||||||
param2: 1,
|
|
||||||
param3: true,
|
|
||||||
param4: {},
|
|
||||||
param5: { subParam1: 'generated-string-value' },
|
|
||||||
param6: 'my',
|
|
||||||
param7: ['generated-string-value'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,12 +0,0 @@
|
|||||||
import { CustomException } from 'src/utils/custom-exception';
|
|
||||||
|
|
||||||
export class CodeIntrospectionException extends CustomException {
|
|
||||||
code: CodeIntrospectionExceptionCode;
|
|
||||||
constructor(message: string, code: CodeIntrospectionExceptionCode) {
|
|
||||||
super(message, code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum CodeIntrospectionExceptionCode {
|
|
||||||
ONLY_ONE_FUNCTION_ALLOWED = 'ONLY_ONE_FUNCTION_ALLOWED',
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
providers: [CodeIntrospectionService],
|
|
||||||
exports: [CodeIntrospectionService],
|
|
||||||
})
|
|
||||||
export class CodeIntrospectionModule {}
|
|
@ -1,157 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ArrayTypeNode,
|
|
||||||
createSourceFile,
|
|
||||||
LiteralTypeNode,
|
|
||||||
PropertySignature,
|
|
||||||
ScriptTarget,
|
|
||||||
StringLiteral,
|
|
||||||
SyntaxKind,
|
|
||||||
TypeNode,
|
|
||||||
UnionTypeNode,
|
|
||||||
VariableStatement,
|
|
||||||
ArrowFunction,
|
|
||||||
FunctionDeclaration,
|
|
||||||
} from 'typescript';
|
|
||||||
|
|
||||||
import { generateFakeValue } from 'src/engine/utils/generate-fake-value';
|
|
||||||
import { isDefined } from 'src/utils/is-defined';
|
|
||||||
import {
|
|
||||||
InputSchema,
|
|
||||||
InputSchemaProperty,
|
|
||||||
} from 'src/modules/code-introspection/types/input-schema.type';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class CodeIntrospectionService {
|
|
||||||
public generateInputData(inputSchema: InputSchema, setNullValue = false) {
|
|
||||||
return Object.entries(inputSchema).reduce((acc, [key, value]) => {
|
|
||||||
if (isDefined(value.enum)) {
|
|
||||||
acc[key] = value.enum?.[0];
|
|
||||||
} else if (['string', 'number', 'boolean'].includes(value.type)) {
|
|
||||||
acc[key] = setNullValue ? null : generateFakeValue(value.type);
|
|
||||||
} else if (value.type === 'object') {
|
|
||||||
acc[key] = isDefined(value.properties)
|
|
||||||
? this.generateInputData(value.properties, setNullValue)
|
|
||||||
: {};
|
|
||||||
} else if (value.type === 'array' && isDefined(value.items)) {
|
|
||||||
acc[key] = [generateFakeValue(value.items.type)];
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
public getFunctionInputSchema(fileContent: string): InputSchema {
|
|
||||||
const sourceFile = createSourceFile(
|
|
||||||
'temp.ts',
|
|
||||||
fileContent,
|
|
||||||
ScriptTarget.ESNext,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
const schema: InputSchema = {};
|
|
||||||
|
|
||||||
sourceFile.forEachChild((node) => {
|
|
||||||
if (node.kind === SyntaxKind.FunctionDeclaration) {
|
|
||||||
const funcNode = node as FunctionDeclaration;
|
|
||||||
const params = funcNode.parameters;
|
|
||||||
|
|
||||||
params.forEach((param) => {
|
|
||||||
const paramName = param.name.getText();
|
|
||||||
const typeNode = param.type;
|
|
||||||
|
|
||||||
if (typeNode) {
|
|
||||||
schema[paramName] = this.getTypeString(typeNode);
|
|
||||||
} else {
|
|
||||||
schema[paramName] = { type: 'unknown' };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (node.kind === SyntaxKind.VariableStatement) {
|
|
||||||
const varStatement = node as VariableStatement;
|
|
||||||
|
|
||||||
varStatement.declarationList.declarations.forEach((declaration) => {
|
|
||||||
if (
|
|
||||||
declaration.initializer &&
|
|
||||||
declaration.initializer.kind === SyntaxKind.ArrowFunction
|
|
||||||
) {
|
|
||||||
const arrowFunction = declaration.initializer as ArrowFunction;
|
|
||||||
const params = arrowFunction.parameters;
|
|
||||||
|
|
||||||
params.forEach((param: any) => {
|
|
||||||
const paramName = param.name.text;
|
|
||||||
const typeNode = param.type;
|
|
||||||
|
|
||||||
if (typeNode) {
|
|
||||||
schema[paramName] = this.getTypeString(typeNode);
|
|
||||||
} else {
|
|
||||||
schema[paramName] = { type: 'unknown' };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getTypeString(typeNode: TypeNode): InputSchemaProperty {
|
|
||||||
switch (typeNode.kind) {
|
|
||||||
case SyntaxKind.NumberKeyword:
|
|
||||||
return { type: 'number' };
|
|
||||||
case SyntaxKind.StringKeyword:
|
|
||||||
return { type: 'string' };
|
|
||||||
case SyntaxKind.BooleanKeyword:
|
|
||||||
return { type: 'boolean' };
|
|
||||||
case SyntaxKind.ArrayType:
|
|
||||||
return {
|
|
||||||
type: 'array',
|
|
||||||
items: this.getTypeString((typeNode as ArrayTypeNode).elementType),
|
|
||||||
};
|
|
||||||
case SyntaxKind.ObjectKeyword:
|
|
||||||
return { type: 'object' };
|
|
||||||
case SyntaxKind.TypeLiteral: {
|
|
||||||
const properties: InputSchema = {};
|
|
||||||
|
|
||||||
(typeNode as any).members.forEach((member: PropertySignature) => {
|
|
||||||
if (member.name && member.type) {
|
|
||||||
const memberName = (member.name as any).text;
|
|
||||||
|
|
||||||
properties[memberName] = this.getTypeString(member.type);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { type: 'object', properties };
|
|
||||||
}
|
|
||||||
case SyntaxKind.UnionType: {
|
|
||||||
const unionNode = typeNode as UnionTypeNode;
|
|
||||||
const enumValues: string[] = [];
|
|
||||||
|
|
||||||
let isEnum = true;
|
|
||||||
|
|
||||||
unionNode.types.forEach((subType) => {
|
|
||||||
if (subType.kind === SyntaxKind.LiteralType) {
|
|
||||||
const literal = (subType as LiteralTypeNode).literal;
|
|
||||||
|
|
||||||
if (literal.kind === SyntaxKind.StringLiteral) {
|
|
||||||
enumValues.push((literal as StringLiteral).text);
|
|
||||||
} else {
|
|
||||||
isEnum = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isEnum = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isEnum) {
|
|
||||||
return { type: 'string', enum: enumValues };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { type: 'unknown' };
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return { type: 'unknown' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,7 +9,6 @@ import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/common
|
|||||||
import { WorkflowBuilderModule } from 'src/modules/workflow/workflow-builder/workflow-builder.module';
|
import { WorkflowBuilderModule } from 'src/modules/workflow/workflow-builder/workflow-builder.module';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
|
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
|
||||||
import { CodeIntrospectionModule } from 'src/modules/code-introspection/code-introspection.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -17,7 +16,6 @@ import { CodeIntrospectionModule } from 'src/modules/code-introspection/code-int
|
|||||||
WorkflowCommandModule,
|
WorkflowCommandModule,
|
||||||
WorkflowBuilderModule,
|
WorkflowBuilderModule,
|
||||||
ServerlessFunctionModule,
|
ServerlessFunctionModule,
|
||||||
CodeIntrospectionModule,
|
|
||||||
NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
|
NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
@ -1,17 +1,13 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
|
|
||||||
import { WorkflowActionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-step.dto';
|
import { WorkflowActionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-step.dto';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||||
import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service';
|
|
||||||
import {
|
import {
|
||||||
WorkflowVersionStepException,
|
WorkflowVersionStepException,
|
||||||
WorkflowVersionStepExceptionCode,
|
WorkflowVersionStepExceptionCode,
|
||||||
@ -24,6 +20,7 @@ import {
|
|||||||
WorkflowActionType,
|
WorkflowActionType,
|
||||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||||
import { isDefined } from 'src/utils/is-defined';
|
import { isDefined } from 'src/utils/is-defined';
|
||||||
|
import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema';
|
||||||
|
|
||||||
const TRIGGER_STEP_ID = 'trigger';
|
const TRIGGER_STEP_ID = 'trigger';
|
||||||
|
|
||||||
@ -46,7 +43,6 @@ export class WorkflowVersionStepWorkspaceService {
|
|||||||
private readonly twentyORMManager: TwentyORMManager,
|
private readonly twentyORMManager: TwentyORMManager,
|
||||||
private readonly workflowBuilderWorkspaceService: WorkflowBuilderWorkspaceService,
|
private readonly workflowBuilderWorkspaceService: WorkflowBuilderWorkspaceService,
|
||||||
private readonly serverlessFunctionService: ServerlessFunctionService,
|
private readonly serverlessFunctionService: ServerlessFunctionService,
|
||||||
private readonly codeIntrospectionService: CodeIntrospectionService,
|
|
||||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||||
) {}
|
) {}
|
||||||
@ -78,21 +74,6 @@ export class WorkflowVersionStepWorkspaceService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceCode = (
|
|
||||||
await this.serverlessFunctionService.getServerlessFunctionSourceCode(
|
|
||||||
workspaceId,
|
|
||||||
newServerlessFunction.id,
|
|
||||||
'draft',
|
|
||||||
)
|
|
||||||
)?.[join('src', INDEX_FILE_NAME)];
|
|
||||||
|
|
||||||
const inputSchema = isDefined(sourceCode)
|
|
||||||
? this.codeIntrospectionService.getFunctionInputSchema(sourceCode)
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const serverlessFunctionInput =
|
|
||||||
this.codeIntrospectionService.generateInputData(inputSchema, true);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: newStepId,
|
id: newStepId,
|
||||||
name: 'Code - Serverless Function',
|
name: 'Code - Serverless Function',
|
||||||
@ -103,7 +84,7 @@ export class WorkflowVersionStepWorkspaceService {
|
|||||||
input: {
|
input: {
|
||||||
serverlessFunctionId: newServerlessFunction.id,
|
serverlessFunctionId: newServerlessFunction.id,
|
||||||
serverlessFunctionVersion: 'draft',
|
serverlessFunctionVersion: 'draft',
|
||||||
serverlessFunctionInput,
|
serverlessFunctionInput: BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -201,6 +182,11 @@ export class WorkflowVersionStepWorkspaceService {
|
|||||||
step: WorkflowAction;
|
step: WorkflowAction;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
}): Promise<WorkflowAction> {
|
}): Promise<WorkflowAction> {
|
||||||
|
// We don't enrich on the fly for code workflow action. OutputSchema is computed and updated when testing the serverless function
|
||||||
|
if (step.type === WorkflowActionType.CODE) {
|
||||||
|
return step;
|
||||||
|
}
|
||||||
|
|
||||||
const result = { ...step };
|
const result = { ...step };
|
||||||
const outputSchema =
|
const outputSchema =
|
||||||
await this.workflowBuilderWorkspaceService.computeStepOutputSchema({
|
await this.workflowBuilderWorkspaceService.computeStepOutputSchema({
|
||||||
@ -262,12 +248,10 @@ export class WorkflowVersionStepWorkspaceService {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
workflowVersionId,
|
workflowVersionId,
|
||||||
step,
|
step,
|
||||||
shouldUpdateStepOutput,
|
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
workflowVersionId: string;
|
workflowVersionId: string;
|
||||||
step: WorkflowAction;
|
step: WorkflowAction;
|
||||||
shouldUpdateStepOutput: boolean;
|
|
||||||
}): Promise<WorkflowAction> {
|
}): Promise<WorkflowAction> {
|
||||||
const workflowVersionRepository =
|
const workflowVersionRepository =
|
||||||
await this.twentyORMManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
await this.twentyORMManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||||
@ -294,12 +278,10 @@ export class WorkflowVersionStepWorkspaceService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const enrichedNewStep = shouldUpdateStepOutput
|
const enrichedNewStep = await this.enrichOutputSchema({
|
||||||
? await this.enrichOutputSchema({
|
step,
|
||||||
step,
|
workspaceId,
|
||||||
workspaceId,
|
});
|
||||||
})
|
|
||||||
: step;
|
|
||||||
|
|
||||||
const updatedSteps = workflowVersion.steps.map((existingStep) => {
|
const updatedSteps = workflowVersion.steps.map((existingStep) => {
|
||||||
if (existingStep.id === step.id) {
|
if (existingStep.id === step.id) {
|
||||||
|
@ -13,9 +13,11 @@ export type InputSchemaProperty = {
|
|||||||
type: InputSchemaPropertyType;
|
type: InputSchemaPropertyType;
|
||||||
enum?: string[];
|
enum?: string[];
|
||||||
items?: InputSchemaProperty; // used to describe array type elements
|
items?: InputSchemaProperty; // used to describe array type elements
|
||||||
properties?: InputSchema; // used to describe object type elements
|
properties?: Properties; // used to describe object type elements
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InputSchema = {
|
type Properties = {
|
||||||
[name: string]: InputSchemaProperty;
|
[name: string]: InputSchemaProperty;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type InputSchema = InputSchemaProperty[];
|
@ -1,9 +1,9 @@
|
|||||||
import { InputSchemaPropertyType } from 'src/modules/code-introspection/types/input-schema.type';
|
import { InputSchemaPropertyType } from 'src/modules/workflow/workflow-builder/types/input-schema.type';
|
||||||
|
|
||||||
export type Leaf = {
|
export type Leaf = {
|
||||||
isLeaf: true;
|
isLeaf: true;
|
||||||
icon?: string;
|
|
||||||
type?: InputSchemaPropertyType;
|
type?: InputSchemaPropertyType;
|
||||||
|
icon?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
value: any;
|
value: any;
|
||||||
};
|
};
|
||||||
|
@ -3,14 +3,12 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
|
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
|
||||||
import { CodeIntrospectionModule } from 'src/modules/code-introspection/code-introspection.module';
|
|
||||||
import { WorkflowBuilderWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-builder.workspace-service';
|
import { WorkflowBuilderWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-builder.workspace-service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
|
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
|
||||||
ServerlessFunctionModule,
|
ServerlessFunctionModule,
|
||||||
CodeIntrospectionModule,
|
|
||||||
],
|
],
|
||||||
providers: [WorkflowBuilderWorkspaceService],
|
providers: [WorkflowBuilderWorkspaceService],
|
||||||
exports: [WorkflowBuilderWorkspaceService],
|
exports: [WorkflowBuilderWorkspaceService],
|
||||||
|
@ -1,23 +1,14 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
import { checkStringIsDatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/utils/check-string-is-database-event-action';
|
import { checkStringIsDatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/utils/check-string-is-database-event-action';
|
||||||
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
|
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||||
import { generateFakeValue } from 'src/engine/utils/generate-fake-value';
|
import { generateFakeValue } from 'src/engine/utils/generate-fake-value';
|
||||||
import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service';
|
import { OutputSchema } from 'src/modules/workflow/workflow-builder/types/output-schema.type';
|
||||||
import { InputSchemaPropertyType } from 'src/modules/code-introspection/types/input-schema.type';
|
|
||||||
import {
|
|
||||||
Leaf,
|
|
||||||
Node,
|
|
||||||
OutputSchema,
|
|
||||||
} from 'src/modules/workflow/workflow-builder/types/output-schema.type';
|
|
||||||
import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/utils/generate-fake-object-record';
|
import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/utils/generate-fake-object-record';
|
||||||
import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-builder/utils/generate-fake-object-record-event';
|
import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-builder/utils/generate-fake-object-record-event';
|
||||||
import {
|
import {
|
||||||
@ -34,7 +25,6 @@ import { isDefined } from 'src/utils/is-defined';
|
|||||||
export class WorkflowBuilderWorkspaceService {
|
export class WorkflowBuilderWorkspaceService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly serverlessFunctionService: ServerlessFunctionService,
|
private readonly serverlessFunctionService: ServerlessFunctionService,
|
||||||
private readonly codeIntrospectionService: CodeIntrospectionService,
|
|
||||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||||
) {}
|
) {}
|
||||||
@ -72,18 +62,6 @@ export class WorkflowBuilderWorkspaceService {
|
|||||||
case WorkflowActionType.SEND_EMAIL: {
|
case WorkflowActionType.SEND_EMAIL: {
|
||||||
return this.computeSendEmailActionOutputSchema();
|
return this.computeSendEmailActionOutputSchema();
|
||||||
}
|
}
|
||||||
case WorkflowActionType.CODE: {
|
|
||||||
const { serverlessFunctionId, serverlessFunctionVersion } =
|
|
||||||
step.settings.input;
|
|
||||||
|
|
||||||
return this.computeCodeActionOutputSchema({
|
|
||||||
serverlessFunctionId,
|
|
||||||
serverlessFunctionVersion,
|
|
||||||
workspaceId,
|
|
||||||
serverlessFunctionService: this.serverlessFunctionService,
|
|
||||||
codeIntrospectionService: this.codeIntrospectionService,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
case WorkflowActionType.CREATE_RECORD:
|
case WorkflowActionType.CREATE_RECORD:
|
||||||
case WorkflowActionType.UPDATE_RECORD:
|
case WorkflowActionType.UPDATE_RECORD:
|
||||||
case WorkflowActionType.DELETE_RECORD:
|
case WorkflowActionType.DELETE_RECORD:
|
||||||
@ -98,6 +76,7 @@ export class WorkflowBuilderWorkspaceService {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
objectMetadataRepository: this.objectMetadataRepository,
|
objectMetadataRepository: this.objectMetadataRepository,
|
||||||
});
|
});
|
||||||
|
case WorkflowActionType.CODE: // StepOutput schema is computed on serverlessFunction draft execution
|
||||||
default:
|
default:
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@ -194,63 +173,4 @@ export class WorkflowBuilderWorkspaceService {
|
|||||||
private computeSendEmailActionOutputSchema(): OutputSchema {
|
private computeSendEmailActionOutputSchema(): OutputSchema {
|
||||||
return { success: { isLeaf: true, type: 'boolean', value: true } };
|
return { success: { isLeaf: true, type: 'boolean', value: true } };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async computeCodeActionOutputSchema({
|
|
||||||
serverlessFunctionId,
|
|
||||||
serverlessFunctionVersion,
|
|
||||||
workspaceId,
|
|
||||||
serverlessFunctionService,
|
|
||||||
codeIntrospectionService,
|
|
||||||
}: {
|
|
||||||
serverlessFunctionId: string;
|
|
||||||
serverlessFunctionVersion: string;
|
|
||||||
workspaceId: string;
|
|
||||||
serverlessFunctionService: ServerlessFunctionService;
|
|
||||||
codeIntrospectionService: CodeIntrospectionService;
|
|
||||||
}): Promise<OutputSchema> {
|
|
||||||
if (serverlessFunctionId === '') {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceCode = (
|
|
||||||
await serverlessFunctionService.getServerlessFunctionSourceCode(
|
|
||||||
workspaceId,
|
|
||||||
serverlessFunctionId,
|
|
||||||
serverlessFunctionVersion,
|
|
||||||
)
|
|
||||||
)?.[join('src', INDEX_FILE_NAME)];
|
|
||||||
|
|
||||||
if (!isDefined(sourceCode)) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputSchema =
|
|
||||||
codeIntrospectionService.getFunctionInputSchema(sourceCode);
|
|
||||||
|
|
||||||
const fakeFunctionInput =
|
|
||||||
codeIntrospectionService.generateInputData(inputSchema);
|
|
||||||
|
|
||||||
const resultFromFakeInput =
|
|
||||||
await serverlessFunctionService.executeOneServerlessFunction(
|
|
||||||
serverlessFunctionId,
|
|
||||||
workspaceId,
|
|
||||||
Object.values(fakeFunctionInput)?.[0] || {},
|
|
||||||
serverlessFunctionVersion,
|
|
||||||
);
|
|
||||||
|
|
||||||
return resultFromFakeInput.data
|
|
||||||
? Object.entries(resultFromFakeInput.data).reduce(
|
|
||||||
(acc: Record<string, Leaf | Node>, [key, value]) => {
|
|
||||||
acc[key] = {
|
|
||||||
isLeaf: true,
|
|
||||||
value,
|
|
||||||
type: typeof value as InputSchemaPropertyType,
|
|
||||||
};
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
)
|
|
||||||
: {};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ export class CodeWorkflowAction implements WorkflowAction {
|
|||||||
await this.serverlessFunctionService.executeOneServerlessFunction(
|
await this.serverlessFunctionService.executeOneServerlessFunction(
|
||||||
workflowActionInput.serverlessFunctionId,
|
workflowActionInput.serverlessFunctionId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
Object.values(workflowActionInput.serverlessFunctionInput)?.[0] || {},
|
workflowActionInput.serverlessFunctionInput,
|
||||||
workflowActionInput.serverlessFunctionVersion,
|
workflowActionInput.serverlessFunctionVersion,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -367,7 +367,7 @@ const StyledSeparator = styled.div<{
|
|||||||
background: ${({ theme, accent }) => {
|
background: ${({ theme, accent }) => {
|
||||||
switch (accent) {
|
switch (accent) {
|
||||||
case 'blue':
|
case 'blue':
|
||||||
return theme.color.blue30;
|
return theme.border.color.blue;
|
||||||
case 'danger':
|
case 'danger':
|
||||||
return theme.border.color.danger;
|
return theme.border.color.danger;
|
||||||
default:
|
default:
|
||||||
@ -387,7 +387,7 @@ const StyledShortcutLabel = styled.div<{
|
|||||||
color: ${({ theme, variant, accent }) => {
|
color: ${({ theme, variant, accent }) => {
|
||||||
switch (accent) {
|
switch (accent) {
|
||||||
case 'blue':
|
case 'blue':
|
||||||
return theme.color.blue30;
|
return theme.border.color.blue;
|
||||||
case 'danger':
|
case 'danger':
|
||||||
return variant === 'primary'
|
return variant === 'primary'
|
||||||
? theme.border.color.danger
|
? theme.border.color.danger
|
||||||
|
@ -43,36 +43,34 @@ export const CodeEditor = ({
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<StyledEditor
|
||||||
<StyledEditor
|
height={height}
|
||||||
height={height}
|
withHeader={withHeader}
|
||||||
withHeader={withHeader}
|
value={value}
|
||||||
value={value}
|
language={language}
|
||||||
language={language}
|
onMount={(editor, monaco) => {
|
||||||
onMount={(editor, monaco) => {
|
monaco.editor.defineTheme('codeEditorTheme', codeEditorTheme(theme));
|
||||||
monaco.editor.defineTheme('codeEditorTheme', codeEditorTheme(theme));
|
monaco.editor.setTheme('codeEditorTheme');
|
||||||
monaco.editor.setTheme('codeEditorTheme');
|
|
||||||
|
|
||||||
onMount?.(editor, monaco);
|
onMount?.(editor, monaco);
|
||||||
}}
|
}}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
if (isDefined(value)) {
|
if (isDefined(value)) {
|
||||||
onChange?.(value);
|
onChange?.(value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onValidate={onValidate}
|
onValidate={onValidate}
|
||||||
options={{
|
options={{
|
||||||
overviewRulerLanes: 0,
|
overviewRulerLanes: 0,
|
||||||
scrollbar: {
|
scrollbar: {
|
||||||
vertical: 'hidden',
|
vertical: 'hidden',
|
||||||
horizontal: 'hidden',
|
horizontal: 'hidden',
|
||||||
},
|
},
|
||||||
minimap: {
|
minimap: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -10,6 +10,7 @@ export const BORDER_DARK = {
|
|||||||
secondaryInverted: GRAY_SCALE.gray35,
|
secondaryInverted: GRAY_SCALE.gray35,
|
||||||
inverted: GRAY_SCALE.gray20,
|
inverted: GRAY_SCALE.gray20,
|
||||||
danger: COLOR.red70,
|
danger: COLOR.red70,
|
||||||
|
blue: COLOR.blue30,
|
||||||
},
|
},
|
||||||
...BORDER_COMMON,
|
...BORDER_COMMON,
|
||||||
};
|
};
|
||||||
|
@ -10,6 +10,7 @@ export const BORDER_LIGHT = {
|
|||||||
secondaryInverted: GRAY_SCALE.gray50,
|
secondaryInverted: GRAY_SCALE.gray50,
|
||||||
inverted: GRAY_SCALE.gray60,
|
inverted: GRAY_SCALE.gray60,
|
||||||
danger: COLOR.red20,
|
danger: COLOR.red20,
|
||||||
|
blue: COLOR.blue30,
|
||||||
},
|
},
|
||||||
...BORDER_COMMON,
|
...BORDER_COMMON,
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user