diff --git a/packages/twenty-front/src/modules/serverless-functions/utils/__tests__/getFunctionOutputSchema.test.ts b/packages/twenty-front/src/modules/serverless-functions/utils/__tests__/getFunctionOutputSchema.test.ts index d263c7e8b..db5842d8c 100644 --- a/packages/twenty-front/src/modules/serverless-functions/utils/__tests__/getFunctionOutputSchema.test.ts +++ b/packages/twenty-front/src/modules/serverless-functions/utils/__tests__/getFunctionOutputSchema.test.ts @@ -10,14 +10,22 @@ describe('getFunctionOutputSchema', () => { e: [1, 2, 3], }; const expectedOutputSchema = { - a: { isLeaf: true, type: 'unknown', value: null }, - b: { isLeaf: true, type: 'string', value: 'b' }, + a: { isLeaf: true, type: 'unknown', value: null, icon: 'IconVariable' }, + b: { isLeaf: true, type: 'string', value: 'b', icon: 'IconVariable' }, c: { isLeaf: false, - value: { cc: { isLeaf: true, type: 'number', value: 1 } }, + icon: 'IconVariable', + value: { + cc: { isLeaf: true, type: 'number', value: 1, icon: 'IconVariable' }, + }, + }, + d: { isLeaf: true, type: 'boolean', value: true, icon: 'IconVariable' }, + e: { + isLeaf: true, + type: 'array', + value: [1, 2, 3], + icon: 'IconVariable', }, - d: { isLeaf: true, type: 'boolean', value: true }, - e: { isLeaf: true, type: 'array', value: [1, 2, 3] }, }; expect(getFunctionOutputSchema(testResult)).toEqual(expectedOutputSchema); }); diff --git a/packages/twenty-front/src/modules/serverless-functions/utils/getFunctionOutputSchema.ts b/packages/twenty-front/src/modules/serverless-functions/utils/getFunctionOutputSchema.ts index d0e9084bc..5505ce0c6 100644 --- a/packages/twenty-front/src/modules/serverless-functions/utils/getFunctionOutputSchema.ts +++ b/packages/twenty-front/src/modules/serverless-functions/utils/getFunctionOutputSchema.ts @@ -32,6 +32,7 @@ export const getFunctionOutputSchema = (testResult: object) => { if (isObject(value) && !Array.isArray(value)) { acc[key] = { isLeaf: false, + icon: 'IconVariable', value: getFunctionOutputSchema(value), }; } else { @@ -39,6 +40,7 @@ export const getFunctionOutputSchema = (testResult: object) => { isLeaf: true, value, type: getValueType(value), + icon: 'IconVariable', }; } return acc; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useUpdateOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useUpdateOneServerlessFunction.ts index 3de1b4bac..0c0284570 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useUpdateOneServerlessFunction.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useUpdateOneServerlessFunction.ts @@ -8,9 +8,15 @@ import { UpdateOneServerlessFunctionMutationVariables, UpdateServerlessFunctionInput, } from '~/generated-metadata/graphql'; +import { useEffect, useState } from 'react'; +import { FIND_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunction'; +import { sleep } from '~/utils/sleep'; -export const useUpdateOneServerlessFunction = () => { +export const useUpdateOneServerlessFunction = ( + serverlessFunctionId: string, +) => { const apolloMetadataClient = useApolloMetadataClient(); + const [isReady, setIsReady] = useState(false); const [mutate] = useMutation< UpdateOneServerlessFunctionMutation, UpdateOneServerlessFunctionMutationVariables @@ -19,18 +25,48 @@ export const useUpdateOneServerlessFunction = () => { }); const updateOneServerlessFunction = async ( - input: UpdateServerlessFunctionInput, + input: Omit, ) => { - return await mutate({ + const result = await mutate({ variables: { - input, + input: { ...input, id: serverlessFunctionId }, }, awaitRefetchQueries: true, refetchQueries: [ getOperationName(FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE) ?? '', ], }); + setIsReady(false); + return result; }; - return { updateOneServerlessFunction }; + useEffect(() => { + let isMounted = true; + + const pollFunctionStatus = async () => { + while (isMounted && !isReady) { + const { data } = await apolloMetadataClient.query({ + query: FIND_ONE_SERVERLESS_FUNCTION, + variables: { input: { id: serverlessFunctionId } }, + fetchPolicy: 'network-only', // Always fetch fresh data + }); + + const serverlessFunction = data?.findOneServerlessFunction; + + if (serverlessFunction?.syncStatus === 'READY') { + setIsReady(true); + break; + } + await sleep(500); + } + }; + + pollFunctionStatus(); + + return () => { + isMounted = false; // Cleanup when the component unmounts + }; + }, [serverlessFunctionId, apolloMetadataClient, isReady]); + + return { updateOneServerlessFunction, isReady }; }; diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx index 71f2d5203..9e7267779 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx @@ -4,6 +4,7 @@ import styled from '@emotion/styled'; import { ReactElement } from 'react'; import { Link } from 'react-router-dom'; import { IconComponent, Pill } from 'twenty-ui'; +import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay'; type TabProps = { id: string; @@ -93,7 +94,7 @@ export const Tab = ({ {logo && } {Icon && } - {title} + {title} {pill && typeof pill === 'string' ? : pill} diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx index 72458d3be..f9fada64c 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx @@ -37,6 +37,10 @@ const StyledTabsContainer = styled.div` height: 40px; user-select: none; margin-bottom: -1px; + overflow-y: scroll; + ::-webkit-scrollbar { + display: none; + } `; const StyledContainer = styled.div` @@ -52,10 +56,10 @@ export const TabList = ({ }: TabListProps) => { const visibleTabs = tabs.filter((tab) => !tab.hide); - const initialActiveTabId = visibleTabs[0]?.id || ''; - const { activeTabId, setActiveTabId } = useTabList(tabListInstanceId); + const initialActiveTabId = activeTabId || visibleTabs[0]?.id || ''; + useEffect(() => { setActiveTabId(initialActiveTabId); }, [initialActiveTabId, setActiveTabId]); diff --git a/packages/twenty-front/src/modules/workflow/hooks/__tests__/useTriggerNodeSelection.test.tsx b/packages/twenty-front/src/modules/workflow/hooks/__tests__/useTriggerNodeSelection.test.tsx index 5453118be..1cb5e62cd 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/__tests__/useTriggerNodeSelection.test.tsx +++ b/packages/twenty-front/src/modules/workflow/hooks/__tests__/useTriggerNodeSelection.test.tsx @@ -13,11 +13,11 @@ const wrapper = ({ children }: { children: React.ReactNode }) => ( ); describe('useTriggerNodeSelection', () => { - const mockUpdateNode = jest.fn(); + const mockSetNodes = jest.fn(); beforeEach(() => { (useReactFlow as jest.Mock).mockReturnValue({ - updateNode: mockUpdateNode, + setNodes: mockSetNodes, }); }); @@ -51,7 +51,6 @@ describe('useTriggerNodeSelection', () => { result.current.setWorkflowDiagramTriggerNodeSelection(mockNodeId); }); - expect(mockUpdateNode).toHaveBeenCalledWith(mockNodeId, { selected: true }); expect(result.current.workflowDiagramTriggerNodeSelection).toBeUndefined(); }); @@ -61,6 +60,6 @@ describe('useTriggerNodeSelection', () => { }); // Ensure updateNode is not called when state is undefined - expect(mockUpdateNode).not.toHaveBeenCalled(); + expect(mockSetNodes).not.toHaveBeenCalled(); }); }); diff --git a/packages/twenty-front/src/modules/workflow/hooks/useCreateStep.ts b/packages/twenty-front/src/modules/workflow/hooks/useCreateStep.ts index 820f155c9..8a36b6524 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useCreateStep.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useCreateStep.ts @@ -1,7 +1,6 @@ import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; import { workflowCreateStepFromParentStepIdState } from '@/workflow/states/workflowCreateStepFromParentStepIdState'; -import { workflowDiagramTriggerNodeSelectionState } from '@/workflow/states/workflowDiagramTriggerNodeSelectionState'; import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState'; import { WorkflowStepType, @@ -25,10 +24,6 @@ export const useCreateStep = ({ workflowCreateStepFromParentStepIdState, ); - const setWorkflowDiagramTriggerNodeSelection = useSetRecoilState( - workflowDiagramTriggerNodeSelectionState, - ); - const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion(); const createStep = async (newStepType: WorkflowStepType) => { @@ -51,15 +46,6 @@ export const useCreateStep = ({ setWorkflowSelectedNode(createdStep.id); openRightDrawer(RightDrawerPages.WorkflowStepEdit); - - /** - * After the step has been created, select it. - * As the `insertNodeAndSave` function mutates the cached workflow before resolving, - * we are sure that the new node will have been created at this stage. - * - * Selecting the node will cause a right drawer to open in order to edit the step. - */ - setWorkflowDiagramTriggerNodeSelection(createdStep.id); }; return { diff --git a/packages/twenty-front/src/modules/workflow/hooks/useTriggerNodeSelection.ts b/packages/twenty-front/src/modules/workflow/hooks/useTriggerNodeSelection.ts index df09679da..0f7e62cd4 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useTriggerNodeSelection.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useTriggerNodeSelection.ts @@ -21,9 +21,12 @@ export const useTriggerNodeSelection = () => { return; } - reactflow.updateNode(workflowDiagramTriggerNodeSelection, { - selected: true, - }); + reactflow.setNodes((nodes) => + nodes.map((node) => ({ + ...node, + selected: workflowDiagramTriggerNodeSelection === node.id, + })), + ); setWorkflowDiagramTriggerNodeSelection(undefined); }, [ diff --git a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdown.tsx b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdown.tsx index 64eebb382..b446ce060 100644 --- a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdown.tsx +++ b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdown.tsx @@ -28,7 +28,8 @@ const StyledDropdownVariableButtonContainer = styled( `; const StyledDropdownComponentsContainer = styled(DropdownMenuItemsContainer)` - background-color: ${({ theme }) => theme.background.transparent.light}; + display: flex; + flex-direction: column; `; const SearchVariablesDropdown = ({ diff --git a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownFieldItems.tsx b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownFieldItems.tsx index ac274e131..413909ca2 100644 --- a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownFieldItems.tsx +++ b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownFieldItems.tsx @@ -2,6 +2,7 @@ import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenu import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { BaseOutputSchema, + LinkOutputSchema, OutputSchema, StepOutputSchema, } from '@/workflow/search-variables/types/StepOutputSchema'; @@ -13,10 +14,17 @@ import { useState } from 'react'; import { HorizontalSeparator, IconChevronLeft, + isDefined, MenuItemSelect, OverflowingTextWithTooltip, useIcons, } from 'twenty-ui'; +import { useSetRecoilState } from 'recoil'; +import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState'; +import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; +import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-actions/constants/WorkflowServerlessFunctionTabListComponentId'; +import { isLinkOutputSchema } from '@/workflow/search-variables/utils/isLinkOutputSchema'; +import { workflowDiagramTriggerNodeSelectionState } from '@/workflow/states/workflowDiagramTriggerNodeSelectionState'; type SearchVariablesDropdownFieldItemsProps = { step: StepOutputSchema; @@ -33,6 +41,13 @@ export const SearchVariablesDropdownFieldItems = ({ const [currentPath, setCurrentPath] = useState([]); const [searchInputValue, setSearchInputValue] = useState(''); const { getIcon } = useIcons(); + const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState); + const { setActiveTabId } = useTabList( + WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID, + ); + const setWorkflowDiagramTriggerNodeSelection = useSetRecoilState( + workflowDiagramTriggerNodeSelectionState, + ); const getCurrentSubStep = (): OutputSchema => { let currentSubStep = step.outputSchema; @@ -51,7 +66,9 @@ export const SearchVariablesDropdownFieldItems = ({ const getDisplayedSubStepFields = () => { const currentSubStep = getCurrentSubStep(); - if (isRecordOutputSchema(currentSubStep)) { + if (isLinkOutputSchema(currentSubStep)) { + return { link: currentSubStep.link }; + } else if (isRecordOutputSchema(currentSubStep)) { return currentSubStep.fields; } else if (isBaseOutputSchema(currentSubStep)) { return currentSubStep; @@ -60,6 +77,7 @@ export const SearchVariablesDropdownFieldItems = ({ const handleSelectField = (key: string) => { const currentSubStep = getCurrentSubStep(); + const handleSelectBaseOutputSchema = ( baseOutputSchema: BaseOutputSchema, ) => { @@ -71,7 +89,19 @@ export const SearchVariablesDropdownFieldItems = ({ } }; - if (isRecordOutputSchema(currentSubStep)) { + const handleSelectLinkOutputSchema = ( + linkOutputSchema: LinkOutputSchema, + ) => { + setWorkflowSelectedNode(step.id); + setWorkflowDiagramTriggerNodeSelection(step.id); + if (isDefined(linkOutputSchema.link.tab)) { + setActiveTabId(linkOutputSchema.link.tab); + } + }; + + if (isLinkOutputSchema(currentSubStep)) { + handleSelectLinkOutputSchema(currentSubStep); + } else if (isRecordOutputSchema(currentSubStep)) { handleSelectBaseOutputSchema(currentSubStep.fields); } else if (isBaseOutputSchema(currentSubStep)) { handleSelectBaseOutputSchema(currentSubStep); diff --git a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownObjectItems.tsx b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownObjectItems.tsx index b48cf0bff..3767b2f2b 100644 --- a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownObjectItems.tsx +++ b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownObjectItems.tsx @@ -60,6 +60,10 @@ export const SearchVariablesDropdownObjectItems = ({ const getDisplayedSubStepObject = () => { const currentSubStep = getCurrentSubStep(); + if (!isRecordOutputSchema(currentSubStep)) { + return; + } + return currentSubStep.object; }; diff --git a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownWorkflowStepItems.tsx b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownWorkflowStepItems.tsx index b4a390c73..30dfef721 100644 --- a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownWorkflowStepItems.tsx +++ b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownWorkflowStepItems.tsx @@ -10,6 +10,7 @@ import { MenuItem, MenuItemSelect, OverflowingTextWithTooltip, + useIcons, } from 'twenty-ui'; type SearchVariablesDropdownWorkflowStepItemsProps = { @@ -24,6 +25,7 @@ export const SearchVariablesDropdownWorkflowStepItems = ({ onSelect, }: SearchVariablesDropdownWorkflowStepItemsProps) => { const theme = useTheme(); + const { getIcon } = useIcons(); const [searchInputValue, setSearchInputValue] = useState(''); const { closeDropdown } = useDropdown(dropdownId); @@ -60,7 +62,7 @@ export const SearchVariablesDropdownWorkflowStepItems = ({ hovered={false} onClick={() => onSelect(item.id)} text={item.name} - LeftIcon={undefined} + LeftIcon={item.icon ? getIcon(item.icon) : undefined} hasSubMenu /> )) diff --git a/packages/twenty-front/src/modules/workflow/search-variables/hooks/useAvailableVariablesInWorkflowStep.ts b/packages/twenty-front/src/modules/workflow/search-variables/hooks/useAvailableVariablesInWorkflowStep.ts index 10c0a9964..85f8c340e 100644 --- a/packages/twenty-front/src/modules/workflow/search-variables/hooks/useAvailableVariablesInWorkflowStep.ts +++ b/packages/twenty-front/src/modules/workflow/search-variables/hooks/useAvailableVariablesInWorkflowStep.ts @@ -82,6 +82,7 @@ export const useAvailableVariablesInWorkflowStep = ({ id: previousStep.id, name: previousStep.name, outputSchema: filteredOutputSchema, + ...(previousStep.type === 'CODE' ? { icon: 'IconCode' } : {}), }); } }); diff --git a/packages/twenty-front/src/modules/workflow/search-variables/types/StepOutputSchema.ts b/packages/twenty-front/src/modules/workflow/search-variables/types/StepOutputSchema.ts index 6268184ac..ddf98169b 100644 --- a/packages/twenty-front/src/modules/workflow/search-variables/types/StepOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/search-variables/types/StepOutputSchema.ts @@ -1,6 +1,6 @@ import { InputSchemaPropertyType } from '@/workflow/types/InputSchema'; -export type Leaf = { +type Leaf = { isLeaf: true; type?: InputSchemaPropertyType; icon?: string; @@ -8,13 +8,20 @@ export type Leaf = { value: any; }; -export type Node = { +type Node = { isLeaf: false; icon?: string; label?: string; value: OutputSchema; }; +type Link = { + isLeaf: true; + tab?: string; + icon?: string; + label?: string; +}; + export type BaseOutputSchema = Record; export type RecordOutputSchema = { @@ -23,10 +30,19 @@ export type RecordOutputSchema = { _outputSchemaType: 'RECORD'; }; -export type OutputSchema = BaseOutputSchema | RecordOutputSchema; +export type LinkOutputSchema = { + link: Link; + _outputSchemaType: 'LINK'; +}; + +export type OutputSchema = + | BaseOutputSchema + | RecordOutputSchema + | LinkOutputSchema; export type StepOutputSchema = { id: string; name: string; + icon?: string; outputSchema: OutputSchema; }; diff --git a/packages/twenty-front/src/modules/workflow/search-variables/utils/filterOutputSchema.ts b/packages/twenty-front/src/modules/workflow/search-variables/utils/filterOutputSchema.ts index 1129d6df7..6ed52f4a6 100644 --- a/packages/twenty-front/src/modules/workflow/search-variables/utils/filterOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/search-variables/utils/filterOutputSchema.ts @@ -6,6 +6,7 @@ import { import { isBaseOutputSchema } from '@/workflow/search-variables/utils/isBaseOutputSchema'; import { isRecordOutputSchema } from '@/workflow/search-variables/utils/isRecordOutputSchema'; import { isDefined } from 'twenty-ui'; +import { isLinkOutputSchema } from '@/workflow/search-variables/utils/isLinkOutputSchema'; const isValidRecordOutputSchema = ( outputSchema: RecordOutputSchema, @@ -105,7 +106,9 @@ export const filterOutputSchema = ( return outputSchema; } - if (isRecordOutputSchema(outputSchema)) { + if (isLinkOutputSchema(outputSchema)) { + return outputSchema; + } else if (isRecordOutputSchema(outputSchema)) { return filterRecordOutputSchema(outputSchema, objectNameSingularToSelect); } else if (isBaseOutputSchema(outputSchema)) { return filterBaseOutputSchema(outputSchema, objectNameSingularToSelect); diff --git a/packages/twenty-front/src/modules/workflow/search-variables/utils/isLinkOutputSchema.ts b/packages/twenty-front/src/modules/workflow/search-variables/utils/isLinkOutputSchema.ts new file mode 100644 index 000000000..e1dd24a21 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/search-variables/utils/isLinkOutputSchema.ts @@ -0,0 +1,10 @@ +import { + OutputSchema, + LinkOutputSchema, +} from '@/workflow/search-variables/types/StepOutputSchema'; + +export const isLinkOutputSchema = ( + outputSchema: OutputSchema, +): outputSchema is LinkOutputSchema => { + return outputSchema._outputSchemaType === 'LINK'; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-actions/components/WorkflowEditActionFormServerlessFunction.tsx b/packages/twenty-front/src/modules/workflow/workflow-actions/components/WorkflowEditActionFormServerlessFunction.tsx index 64e0adf80..eb4f888b1 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-actions/components/WorkflowEditActionFormServerlessFunction.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-actions/components/WorkflowEditActionFormServerlessFunction.tsx @@ -15,7 +15,7 @@ import { editor } from 'monaco-editor'; import { AutoTypings } from 'monaco-editor-auto-typings'; import { useEffect, useState } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; -import { CodeEditor, IconCode, isDefined, IconPlayerPlay } from 'twenty-ui'; +import { CodeEditor, IconCode, IconPlayerPlay, isDefined } from 'twenty-ui'; import { useDebouncedCallback } from 'use-debounce'; import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody'; import { TabList } from '@/ui/layout/tab/components/TabList'; @@ -32,6 +32,7 @@ import { getFunctionOutputSchema } from '@/serverless-functions/utils/getFunctio import { getFunctionInputFromSourceCode } from '@/serverless-functions/utils/getFunctionInputFromSourceCode'; import { mergeDefaultFunctionInputAndFunctionInput } from '@/serverless-functions/utils/mergeDefaultFunctionInputAndFunctionInput'; import { WorkflowEditActionFormServerlessFunctionFields } from '@/workflow/workflow-actions/components/WorkflowEditActionFormServerlessFunctionFields'; +import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-actions/constants/WorkflowServerlessFunctionTabListComponentId'; const StyledContainer = styled.div` display: flex; @@ -66,17 +67,19 @@ type ServerlessFunctionInputFormData = { [field: string]: string | ServerlessFunctionInputFormData; }; -const TAB_LIST_COMPONENT_ID = 'serverless-function-code-step'; - export const WorkflowEditActionFormServerlessFunction = ({ action, actionOptions, }: WorkflowEditActionFormServerlessFunctionProps) => { - const theme = useTheme(); - const { activeTabId, setActiveTabId } = useTabList(TAB_LIST_COMPONENT_ID); - const { updateOneServerlessFunction } = useUpdateOneServerlessFunction(); - const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion(); const serverlessFunctionId = action.settings.input.serverlessFunctionId; + const theme = useTheme(); + const { activeTabId, setActiveTabId } = useTabList( + WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID, + ); + const { updateOneServerlessFunction, isReady } = + useUpdateOneServerlessFunction(serverlessFunctionId); + const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion(); + const workflowId = useRecoilValue(workflowIdState); const workflow = useWorkflowWithCurrentVersion(workflowId); const { availablePackages } = useGetAvailablePackages({ @@ -112,12 +115,11 @@ export const WorkflowEditActionFormServerlessFunction = ({ const handleSave = useDebouncedCallback(async () => { await updateOneServerlessFunction({ - id: serverlessFunctionId, name: formValues.name, description: formValues.description, code: formValues.code, }); - }, 1_000); + }, 500); const onCodeChange = async (newCode: string) => { if (actionOptions.readonly === true) { @@ -161,7 +163,15 @@ export const WorkflowEditActionFormServerlessFunction = ({ ...action, settings: { ...action.settings, - outputSchema: {}, + outputSchema: { + link: { + isLeaf: true, + icon: 'IconVariable', + tab: 'test', + label: 'Generate Function Input', + }, + _outputSchemaType: 'LINK', + }, input: { ...action.settings.input, serverlessFunctionInput: newMergedInput, @@ -169,7 +179,7 @@ export const WorkflowEditActionFormServerlessFunction = ({ }, }); }, - 1_000, + 500, ); const handleInputChange = async (value: any, path: string[]) => { @@ -254,7 +264,7 @@ export const WorkflowEditActionFormServerlessFunction = ({ !loading && ( @@ -277,7 +287,7 @@ export const WorkflowEditActionFormServerlessFunction = ({ readonly={actionOptions.readonly} /> - Code + Code {!isReady && } `1px solid ${theme.border.color.light}`}; - box-sizing: border-box; - display: flex; - gap: ${({ theme }) => theme.spacing(2)}; - height: ${({ theme }) => theme.spacing(10)}; - .tab-list { - padding-left: 0px; - } - .tab-list > div { - padding: ${({ theme }) => theme.spacing(3) + ' 0'}; - } -`; - const StyledContentContainer = styled.div` flex: 1; width: 100%; @@ -177,15 +162,13 @@ export const SettingsObjectDetailPage = () => { } > - - - + {renderActiveTabContent()} diff --git a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx index ef2fa9a9a..4b8bb6f69 100644 --- a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx +++ b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx @@ -32,7 +32,8 @@ export const SettingsServerlessFunctionDetail = () => { const { enqueueSnackBar } = useSnackBar(); const { activeTabId, setActiveTabId } = useTabList(TAB_LIST_COMPONENT_ID); const [isCodeValid, setIsCodeValid] = useState(true); - const { updateOneServerlessFunction } = useUpdateOneServerlessFunction(); + const { updateOneServerlessFunction } = + useUpdateOneServerlessFunction(serverlessFunctionId); const { publishOneServerlessFunction } = usePublishOneServerlessFunction(); const { formValues, setFormValues, loading } = useServerlessFunctionUpdateFormState(serverlessFunctionId); @@ -45,7 +46,6 @@ export const SettingsServerlessFunctionDetail = () => { const handleSave = useDebouncedCallback(async () => { await updateOneServerlessFunction({ - id: serverlessFunctionId, name: formValues.name, description: formValues.description, code: formValues.code, diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/jobs/build-serverless-function.job.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/jobs/build-serverless-function.job.ts index 1522a3371..317219d8f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/jobs/build-serverless-function.job.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/jobs/build-serverless-function.job.ts @@ -7,7 +7,10 @@ import { Processor } from 'src/engine/core-modules/message-queue/decorators/proc import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service'; -import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; +import { + ServerlessFunctionEntity, + ServerlessFunctionSyncStatus, +} from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { isDefined } from 'src/utils/is-defined'; export type BuildServerlessFunctionBatchEvent = { @@ -42,10 +45,16 @@ export class BuildServerlessFunctionJob { }); if (isDefined(serverlessFunction)) { + await this.serverlessFunctionRepository.update(serverlessFunction.id, { + syncStatus: ServerlessFunctionSyncStatus.NOT_READY, + }); await this.serverlessService.build( serverlessFunction, serverlessFunctionVersion, ); + await this.serverlessFunctionRepository.update(serverlessFunction.id, { + syncStatus: ServerlessFunctionSyncStatus.READY, + }); } } } diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts index 016f1100e..47167b5ca 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts @@ -277,12 +277,6 @@ export class ServerlessFunctionService { serverlessFunctionVersion: 'draft', workspaceId, }); - await this.serverlessFunctionRepository.update( - existingServerlessFunction.id, - { - syncStatus: ServerlessFunctionSyncStatus.READY, - }, - ); return this.serverlessFunctionRepository.findOneBy({ id: existingServerlessFunction.id, diff --git a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-step.workspace-service.ts b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-step.workspace-service.ts index 4efe3301c..af8c41489 100644 --- a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-step.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-step.workspace-service.ts @@ -81,6 +81,15 @@ export class WorkflowVersionStepWorkspaceService { valid: false, settings: { ...BASE_STEP_DEFINITION, + outputSchema: { + link: { + isLeaf: true, + icon: 'IconVariable', + tab: 'test', + label: 'Generate Function Input', + }, + _outputSchemaType: 'LINK', + }, input: { serverlessFunctionId: newServerlessFunction.id, serverlessFunctionVersion: 'draft', diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/types/output-schema.type.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/types/output-schema.type.ts index 6b85078a4..3994425bf 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/types/output-schema.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/types/output-schema.type.ts @@ -15,6 +15,13 @@ export type Node = { value: OutputSchema; }; +type Link = { + isLeaf: true; + tab?: string; + icon?: string; + label?: string; +}; + export type BaseOutputSchema = Record; export type RecordOutputSchema = { @@ -23,4 +30,12 @@ export type RecordOutputSchema = { _outputSchemaType: 'RECORD'; }; -export type OutputSchema = BaseOutputSchema | RecordOutputSchema; +export type LinkOutputSchema = { + link: Link; + _outputSchemaType: 'LINK'; +}; + +export type OutputSchema = + | BaseOutputSchema + | RecordOutputSchema + | LinkOutputSchema; diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index 232b5cd90..595f1abf2 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -250,6 +250,7 @@ export { IconUserCircle, IconUserPlus, IconUsers, + IconVariable, IconVariablePlus, IconVideo, IconWand,