Rename serverlessFunction to logicFunction (#17494)

## Summary

Rename "Serverless Function" to "Logic Function" across the codebase for
clearer naming.

### Environment Variable Changes

| Old | New |
|-----|-----|
| `SERVERLESS_TYPE` | `LOGIC_FUNCTION_TYPE` |
| `SERVERLESS_LAMBDA_REGION` | `LOGIC_FUNCTION_LAMBDA_REGION` |
| `SERVERLESS_LAMBDA_ROLE` | `LOGIC_FUNCTION_LAMBDA_ROLE` |
| `SERVERLESS_LAMBDA_SUBHOSTING_URL` |
`LOGIC_FUNCTION_LAMBDA_SUBHOSTING_URL` |
| `SERVERLESS_LAMBDA_ACCESS_KEY_ID` |
`LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID` |
| `SERVERLESS_LAMBDA_SECRET_ACCESS_KEY` |
`LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY` |

### Breaking Changes

- Environment variables must be updated in production deployments
- Database migration renames `serverlessFunction` → `logicFunction`
tables
This commit is contained in:
Charles Bochet
2026-01-28 01:42:19 +01:00
committed by GitHub
parent 59d123d2b1
commit da6f1bbef3
351 changed files with 5054 additions and 5139 deletions
@@ -0,0 +1,117 @@
import { useGetAvailablePackages } from '@/settings/logic-functions/hooks/useGetAvailablePackages';
import { type EditorProps, type Monaco } from '@monaco-editor/react';
import { type editor } from 'monaco-editor';
import { AutoTypings } from 'monaco-editor-auto-typings';
import { useParams } from 'react-router-dom';
import { isDefined } from 'twenty-shared/utils';
import { CodeEditor } from 'twenty-ui/input';
export type File = {
language: string;
content: string;
path: string;
};
type SettingsLogicFunctionCodeEditorProps = Omit<EditorProps, 'onChange'> & {
currentFilePath: string;
files: File[];
onChange: (value: string) => void;
};
export const SettingsLogicFunctionCodeEditor = ({
currentFilePath,
files,
onChange,
height = 450,
options = undefined,
}: SettingsLogicFunctionCodeEditorProps) => {
const { logicFunctionId = '' } = useParams();
const { availablePackages } = useGetAvailablePackages({
id: logicFunctionId,
});
const currentFile = files.find((file) => file.path === currentFilePath);
const handleEditorDidMount = async (
editor: editor.IStandaloneCodeEditor,
monaco: Monaco,
) => {
if (files.length > 1) {
files.forEach((file) => {
const model = monaco.editor.getModel(monaco.Uri.file(file.path));
if (!isDefined(model)) {
monaco.editor.createModel(
file.content,
file.language,
monaco.Uri.file(file.path),
);
}
});
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
...monaco.languages.typescript.typescriptDefaults.getCompilerOptions(),
moduleResolution:
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
baseUrl: 'file:///src',
paths: {
'src/*': ['file:///src/*'],
},
allowSyntheticDefaultImports: true,
esModuleInterop: true,
noEmit: true,
target: monaco.languages.typescript.ScriptTarget.ESNext,
});
// TODO load that with proper env variables
const environmentVariables = {};
if (isDefined(environmentVariables)) {
const envTypeDefinitions = Object.keys(environmentVariables)
// eslint-disable-next-line lingui/no-unlocalized-strings
.map((key) => `${key}: string;`)
.join('\n');
const environmentDefinition = `
declare namespace NodeJS {
interface ProcessEnv {
${envTypeDefinitions}
}
}
declare const process: {
env: NodeJS.ProcessEnv;
};
`;
monaco.languages.typescript.typescriptDefaults.setExtraLibs([
{
content: environmentDefinition,
filePath: 'ts:process-env.d.ts',
},
]);
}
await AutoTypings.create(editor, {
monaco,
preloadPackages: true,
onlySpecifiedPackages: true,
versions: availablePackages,
debounceDuration: 0,
});
}
};
return (
isDefined(currentFile) &&
isDefined(availablePackages) && (
<CodeEditor
height={height}
value={currentFile.content}
language={currentFile.language}
onMount={handleEditorDidMount}
onChange={onChange}
options={options}
variant="with-header"
/>
)
);
};
@@ -0,0 +1,36 @@
import { TitleInput } from '@/ui/input/components/TitleInput';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
const StyledHeaderTitle = styled.div`
color: ${({ theme }) => theme.font.color.primary};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
font-size: ${({ theme }) => theme.font.size.lg};
width: fit-content;
max-width: 420px;
& > input:disabled {
color: ${({ theme }) => theme.font.color.primary};
}
`;
type SettingsLogicFunctionLabelContainerProps = {
value: string;
onChange: (value: string) => void;
};
export const SettingsLogicFunctionLabelContainer = ({
value,
onChange,
}: SettingsLogicFunctionLabelContainerProps) => {
return (
<StyledHeaderTitle>
<TitleInput
instanceId="logic-function-name-input"
sizeVariant="md"
value={value}
onChange={onChange}
placeholder={t`Function name`}
/>
</StyledHeaderTitle>
);
};
@@ -0,0 +1,54 @@
import { type LogicFunctionNewFormValues } from '@/settings/logic-functions/hooks/useLogicFunctionUpdateFormState';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { TextArea } from '@/ui/input/components/TextArea';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
const StyledInputsContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(4)};
`;
export const SettingsLogicFunctionNewForm = ({
formValues,
onChange,
readonly = false,
}: {
formValues: LogicFunctionNewFormValues;
onChange: (key: string) => (value: string) => void;
readonly?: boolean;
}) => {
const descriptionTextAreaId = `${formValues.name}-description`;
const nameTextInputId = `${formValues.name}-name`;
return (
<Section>
<H2Title
title={t`About`}
description={t`Name and describe your function`}
/>
<StyledInputsContainer>
<SettingsTextInput
instanceId={nameTextInputId}
placeholder={t`Name`}
fullWidth
autoFocusOnMount
value={formValues.name}
onChange={onChange('name')}
readOnly={readonly}
/>
<TextArea
textAreaId={descriptionTextAreaId}
placeholder={t`Description`}
minRows={4}
value={formValues.description}
onChange={onChange('description')}
readOnly={readonly}
/>
</StyledInputsContainer>
</Section>
);
};
@@ -0,0 +1,36 @@
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { SettingsPath } from 'twenty-shared/types';
import { LinkChip } from 'twenty-ui/components';
import { getSettingsPath } from 'twenty-shared/utils';
import { useParams } from 'react-router-dom';
import { t } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
export const SettingsLogicFunctionTabEnvironmentVariablesSection = () => {
const { applicationId = '' } = useParams<{ applicationId: string }>();
return (
<Section>
<H2Title
title={t`Environment Variables`}
description={t`Accessible in your function via process.env.KEY`}
/>
<Trans>
Environment variables are defined at application level for all
functions. Please check{' '}
<LinkChip
label={t`application detail page`}
to={getSettingsPath(
SettingsPath.ApplicationDetail,
{
applicationId,
},
undefined,
'settings',
)}
/>
.
</Trans>
</Section>
);
};
@@ -0,0 +1,48 @@
import styled from '@emotion/styled';
import { type LogicFunction } from '~/generated-metadata/graphql';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { useTheme } from '@emotion/react';
import { IconChevronRight } from 'twenty-ui/display';
import { StyledTableRow } from '@/settings/logic-functions/components/SettingsLogicFunctionsTable';
const StyledNameTableCell = styled(TableCell)`
color: ${({ theme }) => theme.font.color.primary};
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledRuntimeTableCell = styled(TableCell)`
color: ${({ theme }) => theme.font.color.secondary};
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledIconTableCell = styled(TableCell)`
justify-content: center;
padding-right: ${({ theme }) => theme.spacing(1)};
`;
const StyledIconChevronRight = styled(IconChevronRight)`
color: ${({ theme }) => theme.font.color.tertiary};
`;
export const SettingsLogicFunctionsFieldItemTableRow = ({
logicFunction,
to,
}: {
logicFunction: LogicFunction;
to: string;
}) => {
const theme = useTheme();
return (
<StyledTableRow to={to}>
<StyledNameTableCell>{logicFunction.name}</StyledNameTableCell>
<StyledNameTableCell></StyledNameTableCell>
<StyledRuntimeTableCell>{logicFunction.runtime}</StyledRuntimeTableCell>
<StyledIconTableCell>
<StyledIconChevronRight
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
</StyledIconTableCell>
</StyledTableRow>
);
};
@@ -0,0 +1,56 @@
import { SettingsLogicFunctionsFieldItemTableRow } from '@/settings/logic-functions/components/SettingsLogicFunctionsFieldItemTableRow';
import { Table } from '@/ui/layout/table/components/Table';
import { TableBody } from '@/ui/layout/table/components/TableBody';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import styled from '@emotion/styled';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { type LogicFunction } from '~/generated-metadata/graphql';
import { useLingui } from '@lingui/react/macro';
import { useParams } from 'react-router-dom';
export const StyledTableRow = styled(TableRow)`
grid-template-columns: 164px 1fr 96px 32px;
`;
const StyledTableBody = styled(TableBody)`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
`;
export const SettingsLogicFunctionsTable = ({
logicFunctions,
}: {
logicFunctions: LogicFunction[];
}) => {
const { applicationId = '' } = useParams();
const { t } = useLingui();
if (logicFunctions.length === 0) {
return null;
}
return (
<Table>
<StyledTableRow>
<TableHeader>{t`Name`}</TableHeader>
<TableHeader></TableHeader>
<TableHeader>{t`Runtime`}</TableHeader>
<TableHeader></TableHeader>
</StyledTableRow>
<StyledTableBody>
{logicFunctions.map((logicFunction: LogicFunction) => (
<SettingsLogicFunctionsFieldItemTableRow
key={logicFunction.id}
logicFunction={logicFunction}
to={getSettingsPath(SettingsPath.ApplicationLogicFunctionDetail, {
applicationId,
logicFunctionId: logicFunction.id,
})}
/>
))}
</StyledTableBody>
</Table>
);
};
@@ -0,0 +1,85 @@
import {
type File,
SettingsLogicFunctionCodeEditor,
} from '@/settings/logic-functions/components/SettingsLogicFunctionCodeEditor';
import { SETTINGS_LOGIC_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/settings/logic-functions/constants/SettingsLogicFunctionTabListComponentId';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { H2Title, IconPlayerPlay } from 'twenty-ui/display';
import { Button, CoreEditorHeader } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
const StyledTabList = styled(TabList)`
border-bottom: none;
`;
export const SettingsLogicFunctionCodeEditorTab = ({
files,
handleExecute,
onChange,
isTesting = false,
isManaged = false,
}: {
files: File[];
handleExecute: () => void;
onChange: (filePath: string, value: string) => void;
isTesting?: boolean;
isManaged?: boolean;
}) => {
const activeTabId = useRecoilComponentValue(
activeTabIdComponentState,
SETTINGS_LOGIC_FUNCTION_TAB_LIST_COMPONENT_ID,
);
const TestButton = (
<Button
title={t`Test`}
variant="primary"
accent="blue"
size="small"
Icon={IconPlayerPlay}
disabled={isTesting}
onClick={handleExecute}
/>
);
const HeaderTabList = (
<StyledTabList
tabs={files.map((file) => {
return { id: file.path, title: file.path.split('/').at(-1) || '' };
})}
componentInstanceId={SETTINGS_LOGIC_FUNCTION_TAB_LIST_COMPONENT_ID}
/>
);
return (
<Section>
<H2Title
title={t`Code your function`}
description={t`Write your function (in typescript) below`}
/>
<CoreEditorHeader leftNodes={[HeaderTabList]} rightNodes={[TestButton]} />
{activeTabId && (
<SettingsLogicFunctionCodeEditor
files={files}
currentFilePath={activeTabId}
onChange={(newCodeValue: string) =>
onChange(activeTabId, newCodeValue)
}
options={
isManaged
? {
readOnly: true,
readOnlyMessage: {
value: t`Managed logic functions are not editable`,
},
}
: undefined
}
/>
)}
</Section>
);
};
@@ -0,0 +1,21 @@
import { SettingsLogicFunctionNewForm } from '@/settings/logic-functions/components/SettingsLogicFunctionNewForm';
import { SettingsLogicFunctionTabEnvironmentVariablesSection } from '@/settings/logic-functions/components/SettingsLogicFunctionTabEnvironmentVariablesSection';
import { type LogicFunctionFormValues } from '@/settings/logic-functions/hooks/useLogicFunctionUpdateFormState';
export const SettingsLogicFunctionSettingsTab = ({
formValues,
onChange,
}: {
formValues: LogicFunctionFormValues;
onChange: (key: string) => (value: string) => void;
}) => {
return (
<>
<SettingsLogicFunctionNewForm
formValues={formValues}
onChange={onChange}
/>
<SettingsLogicFunctionTabEnvironmentVariablesSection />
</>
);
};
@@ -0,0 +1,139 @@
import { TextInput } from '@/ui/input/components/TextInput';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import {
IconCheck,
IconDotsVertical,
IconPencil,
IconTrash,
IconX,
OverflowingTextWithTooltip,
} from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import type { ApplicationVariable } from '~/generated/graphql';
const StyledEditModeTableRow = styled(TableRow)`
grid-template-columns: 180px auto 56px;
`;
const StyledTableRow = styled(TableRow)`
grid-template-columns: 180px 300px 32px;
`;
export const SettingsLogicFunctionTabEnvironmentVariableTableRow = ({
envVariable,
onChange,
onDelete,
initialEditMode = false,
}: {
envVariable: ApplicationVariable;
onChange: (newEnvVariable: ApplicationVariable) => void;
onDelete: () => void;
initialEditMode?: boolean;
}) => {
const [editedEnvVariable, setEditedEnvVariable] = useState(envVariable);
const [editMode, setEditMode] = useState(initialEditMode);
const dropDownId = `settings-environment-variable-dropdown-${envVariable.id}`;
const { closeDropdown } = useCloseDropdown();
return editMode ? (
<StyledEditModeTableRow>
<TableCell>
<TextInput
autoFocus
value={editedEnvVariable.key}
onChange={(newKey) =>
setEditedEnvVariable({ ...editedEnvVariable, key: newKey })
}
placeholder={t`Name`}
fullWidth
/>
</TableCell>
<TableCell>
<TextInput
value={editedEnvVariable.value}
onChange={(newValue) =>
setEditedEnvVariable({ ...editedEnvVariable, value: newValue })
}
placeholder={t`Value`}
fullWidth
/>
</TableCell>
<TableCell>
<LightIconButton
accent="tertiary"
Icon={IconX}
onClick={() => {
if (envVariable.key === '' && envVariable.value === '') {
onDelete();
}
setEditedEnvVariable(envVariable);
setEditMode(false);
}}
/>
<LightIconButton
accent="tertiary"
Icon={IconCheck}
disabled={
editedEnvVariable.key === '' || editedEnvVariable.value === ''
}
onClick={() => {
onChange(editedEnvVariable);
setEditMode(false);
}}
/>
</TableCell>
</StyledEditModeTableRow>
) : (
<StyledTableRow onClick={() => setEditMode(true)}>
<TableCell>
<OverflowingTextWithTooltip text={envVariable.key} />
</TableCell>
<TableCell>
<OverflowingTextWithTooltip text={envVariable.value} />
</TableCell>
<TableCell>
<Dropdown
dropdownId={dropDownId}
clickableComponent={
<LightIconButton
aria-label={t`Env Variable Options`}
Icon={IconDotsVertical}
accent="tertiary"
/>
}
dropdownComponents={
<DropdownContent>
<DropdownMenuItemsContainer>
<MenuItem
text={t`Edit`}
LeftIcon={IconPencil}
onClick={() => {
setEditMode(true);
closeDropdown(dropDownId);
}}
/>
<MenuItem
text={t`Delete`}
LeftIcon={IconTrash}
onClick={() => {
onDelete();
closeDropdown(dropDownId);
}}
/>
</DropdownMenuItemsContainer>
</DropdownContent>
}
/>
</TableCell>
</StyledTableRow>
);
};
@@ -0,0 +1,101 @@
import { LogicFunctionExecutionResult } from '@/logic-functions/components/LogicFunctionExecutionResult';
import {
type LogicFunctionTestData,
logicFunctionTestDataFamilyState,
} from '@/workflow/workflow-steps/workflow-actions/code-action/states/logicFunctionTestDataFamilyState';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useRecoilState } from 'recoil';
import { H2Title, IconPlayerPlay } from 'twenty-ui/display';
import { Button, CodeEditor, CoreEditorHeader } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { TextArea } from '@/ui/input/components/TextArea';
const StyledInputsContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(4)};
`;
const StyledCodeEditorContainer = styled.div`
display: flex;
flex-direction: column;
`;
export const SettingsLogicFunctionTestTab = ({
handleExecute,
logicFunctionId,
isTesting = false,
}: {
handleExecute: () => void;
logicFunctionId: string;
isTesting?: boolean;
}) => {
const { t } = useLingui();
const [logicFunctionTestData, setLogicFunctionTestData] =
useRecoilState<LogicFunctionTestData>(
logicFunctionTestDataFamilyState(logicFunctionId),
);
const onChange = (newInput: string) => {
setLogicFunctionTestData((prev) => ({
...prev,
input: JSON.parse(newInput),
}));
};
const testLogsTextAreaId = `${logicFunctionId}-test-logs`;
return (
<Section>
<H2Title
title={t`Test your function`}
description={t`Insert a JSON input, then press "Run" to test your function.`}
/>
<StyledInputsContainer>
<StyledCodeEditorContainer>
<CoreEditorHeader
title={t`Input`}
rightNodes={[
<Button
title={t`Run Function`}
variant="primary"
accent="blue"
size="small"
Icon={IconPlayerPlay}
onClick={handleExecute}
disabled={isTesting}
/>,
]}
/>
<CodeEditor
value={JSON.stringify(logicFunctionTestData.input, null, 4)}
language="json"
height={100}
onChange={onChange}
variant="with-header"
/>
</StyledCodeEditorContainer>
<LogicFunctionExecutionResult
logicFunctionTestData={logicFunctionTestData}
maxHeight={
logicFunctionTestData.output.logs.length > 0 ? 200 : undefined
}
isTesting={isTesting}
/>
{logicFunctionTestData.output.logs.length > 0 && (
<StyledCodeEditorContainer>
<InputLabel>{t`Logs`}</InputLabel>
<TextArea
textAreaId={testLogsTextAreaId}
value={isTesting ? '' : logicFunctionTestData.output.logs}
maxRows={20}
disabled
/>
</StyledCodeEditorContainer>
)}
</StyledInputsContainer>
</Section>
);
};
@@ -0,0 +1,137 @@
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
import { SettingsDatabaseEventsForm } from '@/settings/components/SettingsDatabaseEventsForm';
import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { type LogicFunction } from '~/generated/graphql';
export const StyledRouteTriggerTableRow = styled(TableRow)`
grid-template-columns: 1fr 120px 120px;
`;
// TODO: @Charles put back with new sources
// const StyledTableCell = styled(TableCell)`
// color: ${({ theme }) => theme.font.color.tertiary};
// gap: ${({ theme }) => theme.spacing(2)};
// min-width: 0;
// overflow: hidden;
// `;
const StyledRouteTriggerTableHeaderRow = styled(StyledRouteTriggerTableRow)`
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
const StyledEmptyState = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
font-size: ${({ theme }) => theme.font.size.md};
height: 160px;
justify-content: center;
text-align: center;
`;
export const SettingsLogicFunctionTriggersTab = ({
logicFunction: _logicFunction,
}: {
logicFunction: LogicFunction;
}) => {
const { t } = useLingui();
const cronTriggers: [] = [];
const routeTriggers: [] = [];
const databaseEvents: [] = [];
const hasNoTriggers =
databaseEvents.length === 0 &&
cronTriggers.length === 0 &&
routeTriggers.length === 0;
if (hasNoTriggers) {
return (
<Section>
<H2Title
title={t`Triggers`}
description={t`Configure when this function should be executed`}
/>
<StyledEmptyState>
{t`No triggers configured for this function.`}
</StyledEmptyState>
</Section>
);
}
return (
<>
{databaseEvents.length > 0 && (
<Section>
<H2Title
title={t`Database event`}
description={t`Select the events that should trigger the function`}
/>
<SettingsDatabaseEventsForm events={databaseEvents} disabled />
</Section>
)}
{cronTriggers.length > 0 && (
<Section>
<H2Title
title={t`Cron`}
description={t`Triggers the function at regular intervals`}
/>
{cronTriggers.map((cronTrigger, index) => (
<FormTextFieldInput
key={index}
label={t`Expression`}
placeholder="0 */1 * * *"
hint={t`Format: [Minute] [Hour] [Day of Month] [Month] [Day of Week]`}
onChange={() => {}}
readonly
defaultValue={cronTrigger}
/>
))}
</Section>
)}
{routeTriggers.length > 0 && (
<Section>
<H2Title
title={t`Http`}
description={t`Triggers the function with Http request`}
/>
<Table>
<StyledRouteTriggerTableHeaderRow>
<TableHeader>{t`Path`}</TableHeader>
<TableHeader>{t`Method`}</TableHeader>
<TableHeader>{t`Auth Required`}</TableHeader>
</StyledRouteTriggerTableHeaderRow>
{routeTriggers.map((_, _index) => (
<></>
// <StyledRouteTriggerTableRow key={index}>
// <StyledTableCell>
// <OverflowingTextWithTooltip
// text={`${REACT_APP_SERVER_BASE_URL}/s${routeTrigger.path}`}
// />
// </StyledTableCell>
// <StyledTableCell>{routeTrigger.httpMethod}</StyledTableCell>
// <StyledTableCell>
// <Tag
// text={routeTrigger.isAuthRequired ? t`True` : t`False`}
// color={routeTrigger.isAuthRequired ? 'green' : 'orange'}
// weight="medium"
// />
// </StyledTableCell>
// </StyledRouteTriggerTableRow>
))}
</Table>
</Section>
)}
</>
);
};
@@ -0,0 +1,2 @@
export const SETTINGS_LOGIC_FUNCTION_TAB_LIST_COMPONENT_ID =
'settings-logic-function-editor-tab-list';
@@ -0,0 +1,21 @@
import { gql } from '@apollo/client';
export const LOGIC_FUNCTION_FRAGMENT = gql`
fragment LogicFunctionFields on LogicFunction {
id
name
description
runtime
timeoutSeconds
latestVersion
publishedVersions
sourceHandlerPath
builtHandlerPath
handlerName
toolInputSchema
isTool
applicationId
createdAt
updatedAt
}
`;
@@ -0,0 +1,11 @@
import { gql } from '@apollo/client';
import { LOGIC_FUNCTION_FRAGMENT } from '@/settings/logic-functions/graphql/fragments/logicFunctionFragment';
export const CREATE_ONE_LOGIC_FUNCTION = gql`
${LOGIC_FUNCTION_FRAGMENT}
mutation CreateOneLogicFunctionItem($input: CreateLogicFunctionInput!) {
createOneLogicFunction(input: $input) {
...LogicFunctionFields
}
}
`;
@@ -0,0 +1,11 @@
import { gql } from '@apollo/client';
import { LOGIC_FUNCTION_FRAGMENT } from '@/settings/logic-functions/graphql/fragments/logicFunctionFragment';
export const DELETE_ONE_LOGIC_FUNCTION = gql`
${LOGIC_FUNCTION_FRAGMENT}
mutation DeleteOneLogicFunction($input: LogicFunctionIdInput!) {
deleteOneLogicFunction(input: $input) {
...LogicFunctionFields
}
}
`;
@@ -0,0 +1,13 @@
import { gql } from '@apollo/client';
export const EXECUTE_ONE_LOGIC_FUNCTION = gql`
mutation ExecuteOneLogicFunction($input: ExecuteLogicFunctionInput!) {
executeOneLogicFunction(input: $input) {
data
logs
duration
status
error
}
}
`;
@@ -0,0 +1,11 @@
import { gql } from '@apollo/client';
import { LOGIC_FUNCTION_FRAGMENT } from '@/settings/logic-functions/graphql/fragments/logicFunctionFragment';
export const PUBLISH_ONE_LOGIC_FUNCTION = gql`
${LOGIC_FUNCTION_FRAGMENT}
mutation PublishOneLogicFunction($input: PublishLogicFunctionInput!) {
publishLogicFunction(input: $input) {
...LogicFunctionFields
}
}
`;
@@ -0,0 +1,11 @@
import { gql } from '@apollo/client';
import { LOGIC_FUNCTION_FRAGMENT } from '@/settings/logic-functions/graphql/fragments/logicFunctionFragment';
export const UPDATE_ONE_LOGIC_FUNCTION = gql`
${LOGIC_FUNCTION_FRAGMENT}
mutation UpdateOneLogicFunction($input: UpdateLogicFunctionInput!) {
updateOneLogicFunction(input: $input) {
...LogicFunctionFields
}
}
`;
@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const FIND_MANY_AVAILABLE_PACKAGES = gql`
query FindManyAvailablePackages($input: LogicFunctionIdInput!) {
getAvailablePackages(input: $input)
}
`;
@@ -0,0 +1,11 @@
import { gql } from '@apollo/client';
import { LOGIC_FUNCTION_FRAGMENT } from '@/settings/logic-functions/graphql/fragments/logicFunctionFragment';
export const FIND_MANY_LOGIC_FUNCTIONS = gql`
${LOGIC_FUNCTION_FRAGMENT}
query GetManyLogicFunctions {
findManyLogicFunctions {
...LogicFunctionFields
}
}
`;
@@ -0,0 +1,11 @@
import { gql } from '@apollo/client';
import { LOGIC_FUNCTION_FRAGMENT } from '@/settings/logic-functions/graphql/fragments/logicFunctionFragment';
export const FIND_ONE_LOGIC_FUNCTION = gql`
${LOGIC_FUNCTION_FRAGMENT}
query GetOneLogicFunction($input: LogicFunctionIdInput!) {
findOneLogicFunction(input: $input) {
...LogicFunctionFields
}
}
`;
@@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const FIND_ONE_LOGIC_FUNCTION_SOURCE_CODE = gql`
query FindOneLogicFunctionSourceCode(
$input: GetLogicFunctionSourceCodeInput!
) {
getLogicFunctionSourceCode(input: $input)
}
`;
@@ -0,0 +1,48 @@
import { useLogicFunctionUpdateFormState } from '@/settings/logic-functions/hooks/useLogicFunctionUpdateFormState';
import { renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
jest.mock('@/settings/logic-functions/hooks/useGetOneLogicFunction', () => ({
useGetOneLogicFunction: jest.fn(),
}));
jest.mock(
'@/settings/logic-functions/hooks/useGetOneLogicFunctionSourceCode',
() => ({
useGetOneLogicFunctionSourceCode: jest.fn(),
}),
);
describe('useLogicFunctionUpdateFormState', () => {
test('should return a form', () => {
const logicFunctionId = 'logicFunctionId';
const useGetOneLogicFunctionMock = jest.requireMock(
'@/settings/logic-functions/hooks/useGetOneLogicFunction',
);
useGetOneLogicFunctionMock.useGetOneLogicFunction.mockReturnValue({
logicFunction: { name: 'name' },
});
const useGetOneLogicFunctionSourceCodeMock = jest.requireMock(
'@/settings/logic-functions/hooks/useGetOneLogicFunctionSourceCode',
);
useGetOneLogicFunctionSourceCodeMock.useGetOneLogicFunctionSourceCode.mockReturnValue(
{
code: { src: { 'index.ts': 'export const handler = () => {}' } },
},
);
const { result } = renderHook(
() => useLogicFunctionUpdateFormState({ logicFunctionId }),
{
wrapper: RecoilRoot,
},
);
const { formValues } = result.current;
expect(formValues).toEqual({
name: '',
description: '',
code: { src: { 'index.ts': '' } },
});
});
});
@@ -0,0 +1,27 @@
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { EXECUTE_ONE_LOGIC_FUNCTION } from '@/settings/logic-functions/graphql/mutations/executeOneLogicFunction';
import { useMutation } from '@apollo/client';
import {
type ExecuteOneLogicFunctionMutation,
type ExecuteOneLogicFunctionMutationVariables,
type ExecuteLogicFunctionInput,
} from '~/generated-metadata/graphql';
export const useExecuteOneLogicFunction = () => {
const apolloMetadataClient = useApolloCoreClient();
const [mutate] = useMutation<
ExecuteOneLogicFunctionMutation,
ExecuteOneLogicFunctionMutationVariables
>(EXECUTE_ONE_LOGIC_FUNCTION, {
client: apolloMetadataClient,
});
const executeOneLogicFunction = async (input: ExecuteLogicFunctionInput) => {
return await mutate({
variables: {
input,
},
});
};
return { executeOneLogicFunction };
};
@@ -0,0 +1,24 @@
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useQuery } from '@apollo/client';
import { FIND_MANY_AVAILABLE_PACKAGES } from '@/settings/logic-functions/graphql/queries/findManyAvailablePackages';
import {
type FindManyAvailablePackagesQuery,
type FindManyAvailablePackagesQueryVariables,
type LogicFunctionIdInput,
} from '~/generated-metadata/graphql';
export const useGetAvailablePackages = (input: LogicFunctionIdInput) => {
const apolloMetadataClient = useApolloCoreClient();
const { data } = useQuery<
FindManyAvailablePackagesQuery,
FindManyAvailablePackagesQueryVariables
>(FIND_MANY_AVAILABLE_PACKAGES, {
client: apolloMetadataClient ?? undefined,
variables: {
input,
},
});
return {
availablePackages: data?.getAvailablePackages || null,
};
};
@@ -0,0 +1,24 @@
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { FIND_MANY_LOGIC_FUNCTIONS } from '@/settings/logic-functions/graphql/queries/findManyLogicFunctions';
import { useQuery } from '@apollo/client';
import {
type GetManyLogicFunctionsQuery,
type GetManyLogicFunctionsQueryVariables,
} from '~/generated-metadata/graphql';
export const useGetManyLogicFunctions = () => {
const apolloMetadataClient = useApolloCoreClient();
const { data, loading, error } = useQuery<
GetManyLogicFunctionsQuery,
GetManyLogicFunctionsQueryVariables
>(FIND_MANY_LOGIC_FUNCTIONS, {
client: apolloMetadataClient ?? undefined,
});
return {
logicFunctions: data?.findManyLogicFunctions || [],
loading,
error,
};
};
@@ -0,0 +1,31 @@
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { FIND_ONE_LOGIC_FUNCTION } from '@/settings/logic-functions/graphql/queries/findOneLogicFunction';
import { useQuery } from '@apollo/client';
import {
type GetOneLogicFunctionQuery,
type GetOneLogicFunctionQueryVariables,
type LogicFunctionIdInput,
} from '~/generated-metadata/graphql';
export const useGetOneLogicFunction = ({
id,
onCompleted,
}: LogicFunctionIdInput & {
onCompleted?: (data: GetOneLogicFunctionQuery) => void;
}) => {
const apolloMetadataClient = useApolloCoreClient();
const { data, loading } = useQuery<
GetOneLogicFunctionQuery,
GetOneLogicFunctionQueryVariables
>(FIND_ONE_LOGIC_FUNCTION, {
client: apolloMetadataClient ?? undefined,
variables: {
input: { id },
},
onCompleted,
});
return {
logicFunction: data?.findOneLogicFunction || null,
loading,
};
};
@@ -0,0 +1,31 @@
import { useQuery } from '@apollo/client';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { FIND_ONE_LOGIC_FUNCTION_SOURCE_CODE } from '@/settings/logic-functions/graphql/queries/findOneLogicFunctionSourceCode';
import {
type FindOneLogicFunctionSourceCodeQuery,
type FindOneLogicFunctionSourceCodeQueryVariables,
} from '~/generated-metadata/graphql';
export const useGetOneLogicFunctionSourceCode = ({
id,
version,
onCompleted,
}: {
id: string;
version: string;
onCompleted?: (data: FindOneLogicFunctionSourceCodeQuery) => void;
}) => {
const apolloMetadataClient = useApolloCoreClient();
const { data, loading } = useQuery<
FindOneLogicFunctionSourceCodeQuery,
FindOneLogicFunctionSourceCodeQueryVariables
>(FIND_ONE_LOGIC_FUNCTION_SOURCE_CODE, {
client: apolloMetadataClient ?? undefined,
variables: {
input: { id, version },
},
onCompleted,
fetchPolicy: 'network-only',
});
return { code: data?.getLogicFunctionSourceCode, loading };
};
@@ -0,0 +1,107 @@
import { flattenSources } from '@/logic-functions/utils/flattenSources';
import { getFunctionInputFromSourceCode } from '@/logic-functions/utils/getFunctionInputFromSourceCode';
import { useGetOneLogicFunction } from '@/settings/logic-functions/hooks/useGetOneLogicFunction';
import { useGetOneLogicFunctionSourceCode } from '@/settings/logic-functions/hooks/useGetOneLogicFunctionSourceCode';
import { logicFunctionTestDataFamilyState } from '@/workflow/workflow-steps/workflow-actions/code-action/states/logicFunctionTestDataFamilyState';
import { type Dispatch, type SetStateAction, useState } from 'react';
import { useRecoilState } from 'recoil';
import { type Sources } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
type FindOneLogicFunctionSourceCodeQuery,
type GetOneLogicFunctionQuery,
} from '~/generated-metadata/graphql';
import { type LogicFunction } from '~/generated/graphql';
export type LogicFunctionNewFormValues = {
name: string;
description: string;
};
export type LogicFunctionFormValues = LogicFunctionNewFormValues & {
code: Sources;
};
type SetLogicFunctionFormValues = Dispatch<
SetStateAction<LogicFunctionFormValues>
>;
export const useLogicFunctionUpdateFormState = ({
logicFunctionId,
logicFunctionVersion = 'draft',
}: {
logicFunctionId: string;
logicFunctionVersion?: string;
}): {
formValues: LogicFunctionFormValues;
logicFunction: LogicFunction | null;
setFormValues: SetLogicFunctionFormValues;
loading: boolean;
} => {
const [formValues, setFormValues] = useState<LogicFunctionFormValues>({
name: '',
description: '',
code: { src: { 'index.ts': '' } },
});
const [logicFunctionTestData, setLogicFunctionTestData] = useRecoilState(
logicFunctionTestDataFamilyState(logicFunctionId),
);
const { logicFunction, loading: logicFunctionLoading } =
useGetOneLogicFunction({
id: logicFunctionId,
onCompleted: (data: GetOneLogicFunctionQuery) => {
const fn = data?.findOneLogicFunction;
if (isDefined(fn)) {
setFormValues((prevState) => ({
...prevState,
name: fn.name || '',
description: fn.description || '',
}));
}
},
});
const { loading: logicFunctionSourceCodeLoading } =
useGetOneLogicFunctionSourceCode({
id: logicFunctionId,
version: logicFunctionVersion,
onCompleted: async (data: FindOneLogicFunctionSourceCodeQuery) => {
const code = data?.getLogicFunctionSourceCode;
setFormValues((prevState) => ({
...prevState,
code: code || prevState.code,
}));
if (logicFunctionTestData.shouldInitInput) {
const flattenedCode = flattenSources(code);
const sourceCode = flattenedCode.find(
(flatCode) => flatCode.path === logicFunction?.sourceHandlerPath,
);
if (isDefined(sourceCode)) {
const functionInput = await getFunctionInputFromSourceCode(
sourceCode.content,
);
setLogicFunctionTestData((prev) => ({
...prev,
input: functionInput,
shouldInitInput: false,
}));
}
}
},
});
return {
formValues,
setFormValues,
logicFunction,
loading: logicFunctionSourceCodeLoading || logicFunctionLoading,
};
};
@@ -0,0 +1,173 @@
import { useCallback } from 'react';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
import { CREATE_ONE_LOGIC_FUNCTION } from '@/settings/logic-functions/graphql/mutations/createOneLogicFunction';
import { DELETE_ONE_LOGIC_FUNCTION } from '@/settings/logic-functions/graphql/mutations/deleteOneLogicFunction';
import { UPDATE_ONE_LOGIC_FUNCTION } from '@/settings/logic-functions/graphql/mutations/updateOneLogicFunction';
import { FIND_MANY_LOGIC_FUNCTIONS } from '@/settings/logic-functions/graphql/queries/findManyLogicFunctions';
import { FIND_ONE_LOGIC_FUNCTION_SOURCE_CODE } from '@/settings/logic-functions/graphql/queries/findOneLogicFunctionSourceCode';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError, useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { t } from '@lingui/core/macro';
import { CrudOperationType } from 'twenty-shared/types';
import {
type CreateOneLogicFunctionItemMutation,
type CreateOneLogicFunctionItemMutationVariables,
type DeleteOneLogicFunctionMutation,
type DeleteOneLogicFunctionMutationVariables,
type UpdateOneLogicFunctionMutation,
type UpdateOneLogicFunctionMutationVariables,
} from '~/generated-metadata/graphql';
export const usePersistLogicFunction = () => {
const apolloMetadataClient = useApolloCoreClient();
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
const [createLogicFunctionMutation] = useMutation<
CreateOneLogicFunctionItemMutation,
CreateOneLogicFunctionItemMutationVariables
>(CREATE_ONE_LOGIC_FUNCTION, {
client: apolloMetadataClient,
});
const [updateLogicFunctionMutation] = useMutation<
UpdateOneLogicFunctionMutation,
UpdateOneLogicFunctionMutationVariables
>(UPDATE_ONE_LOGIC_FUNCTION, {
client: apolloMetadataClient,
});
const [deleteLogicFunctionMutation] = useMutation<
DeleteOneLogicFunctionMutation,
DeleteOneLogicFunctionMutationVariables
>(DELETE_ONE_LOGIC_FUNCTION, {
client: apolloMetadataClient,
});
const createLogicFunction = useCallback(
async (
variables: CreateOneLogicFunctionItemMutationVariables,
): Promise<
MetadataRequestResult<
Awaited<ReturnType<typeof createLogicFunctionMutation>>
>
> => {
try {
const result = await createLogicFunctionMutation({
variables,
awaitRefetchQueries: true,
refetchQueries: [getOperationName(FIND_MANY_LOGIC_FUNCTIONS) ?? ''],
});
return {
status: 'successful',
response: result,
};
} catch (error) {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'logicFunction',
operationType: CrudOperationType.CREATE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
}
return {
status: 'failed',
error,
};
}
},
[createLogicFunctionMutation, handleMetadataError, enqueueErrorSnackBar],
);
const updateLogicFunction = useCallback(
async (
variables: UpdateOneLogicFunctionMutationVariables,
): Promise<
MetadataRequestResult<
Awaited<ReturnType<typeof updateLogicFunctionMutation>>
>
> => {
try {
const result = await updateLogicFunctionMutation({
variables,
refetchQueries: [
getOperationName(FIND_ONE_LOGIC_FUNCTION_SOURCE_CODE) ?? '',
],
});
return {
status: 'successful',
response: result,
};
} catch (error) {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'logicFunction',
operationType: CrudOperationType.UPDATE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
}
return {
status: 'failed',
error,
};
}
},
[updateLogicFunctionMutation, handleMetadataError, enqueueErrorSnackBar],
);
const deleteLogicFunction = useCallback(
async (
variables: DeleteOneLogicFunctionMutationVariables,
): Promise<
MetadataRequestResult<
Awaited<ReturnType<typeof deleteLogicFunctionMutation>>
>
> => {
try {
const result = await deleteLogicFunctionMutation({
variables,
awaitRefetchQueries: true,
refetchQueries: [
getOperationName(FIND_ONE_LOGIC_FUNCTION_SOURCE_CODE) ?? '',
],
});
return {
status: 'successful',
response: result,
};
} catch (error) {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'logicFunction',
operationType: CrudOperationType.DELETE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
}
return {
status: 'failed',
error,
};
}
},
[deleteLogicFunctionMutation, handleMetadataError, enqueueErrorSnackBar],
);
return {
createLogicFunction,
updateLogicFunction,
deleteLogicFunction,
};
};
@@ -0,0 +1,34 @@
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { PUBLISH_ONE_LOGIC_FUNCTION } from '@/settings/logic-functions/graphql/mutations/publishOneLogicFunction';
import { FIND_ONE_LOGIC_FUNCTION_SOURCE_CODE } from '@/settings/logic-functions/graphql/queries/findOneLogicFunctionSourceCode';
import { useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import {
type PublishOneLogicFunctionMutation,
type PublishOneLogicFunctionMutationVariables,
type PublishLogicFunctionInput,
} from '~/generated-metadata/graphql';
export const usePublishOneLogicFunction = () => {
const apolloMetadataClient = useApolloCoreClient();
const [mutate] = useMutation<
PublishOneLogicFunctionMutation,
PublishOneLogicFunctionMutationVariables
>(PUBLISH_ONE_LOGIC_FUNCTION, {
client: apolloMetadataClient,
});
const publishOneLogicFunction = async (input: PublishLogicFunctionInput) => {
return await mutate({
variables: {
input,
},
awaitRefetchQueries: true,
refetchQueries: [
getOperationName(FIND_ONE_LOGIC_FUNCTION_SOURCE_CODE) ?? '',
],
});
};
return { publishOneLogicFunction };
};