9024 workflow test serverless function follow up (#9066)

-  Fix Tablist style
- Fix dropdown style (wrong grey background)
- Update dropdown variable when no outputSchema is available 



https://github.com/user-attachments/assets/56698fe8-8dd3-404a-b2b2-f1eca6f5fa28
This commit is contained in:
martmull
2024-12-17 10:35:38 +01:00
committed by GitHub
parent 0692bba710
commit 5dfcc413cf
25 changed files with 218 additions and 89 deletions

View File

@ -10,14 +10,22 @@ describe('getFunctionOutputSchema', () => {
e: [1, 2, 3], e: [1, 2, 3],
}; };
const expectedOutputSchema = { const expectedOutputSchema = {
a: { isLeaf: true, type: 'unknown', value: null }, a: { isLeaf: true, type: 'unknown', value: null, icon: 'IconVariable' },
b: { isLeaf: true, type: 'string', value: 'b' }, b: { isLeaf: true, type: 'string', value: 'b', icon: 'IconVariable' },
c: { c: {
isLeaf: false, 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); expect(getFunctionOutputSchema(testResult)).toEqual(expectedOutputSchema);
}); });

View File

@ -32,6 +32,7 @@ export const getFunctionOutputSchema = (testResult: object) => {
if (isObject(value) && !Array.isArray(value)) { if (isObject(value) && !Array.isArray(value)) {
acc[key] = { acc[key] = {
isLeaf: false, isLeaf: false,
icon: 'IconVariable',
value: getFunctionOutputSchema(value), value: getFunctionOutputSchema(value),
}; };
} else { } else {
@ -39,6 +40,7 @@ export const getFunctionOutputSchema = (testResult: object) => {
isLeaf: true, isLeaf: true,
value, value,
type: getValueType(value), type: getValueType(value),
icon: 'IconVariable',
}; };
} }
return acc; return acc;

View File

@ -8,9 +8,15 @@ import {
UpdateOneServerlessFunctionMutationVariables, UpdateOneServerlessFunctionMutationVariables,
UpdateServerlessFunctionInput, UpdateServerlessFunctionInput,
} from '~/generated-metadata/graphql'; } 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 apolloMetadataClient = useApolloMetadataClient();
const [isReady, setIsReady] = useState(false);
const [mutate] = useMutation< const [mutate] = useMutation<
UpdateOneServerlessFunctionMutation, UpdateOneServerlessFunctionMutation,
UpdateOneServerlessFunctionMutationVariables UpdateOneServerlessFunctionMutationVariables
@ -19,18 +25,48 @@ export const useUpdateOneServerlessFunction = () => {
}); });
const updateOneServerlessFunction = async ( const updateOneServerlessFunction = async (
input: UpdateServerlessFunctionInput, input: Omit<UpdateServerlessFunctionInput, 'id'>,
) => { ) => {
return await mutate({ const result = await mutate({
variables: { variables: {
input, input: { ...input, id: serverlessFunctionId },
}, },
awaitRefetchQueries: true, awaitRefetchQueries: true,
refetchQueries: [ refetchQueries: [
getOperationName(FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE) ?? '', 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 };
}; };

View File

@ -4,6 +4,7 @@ import styled from '@emotion/styled';
import { ReactElement } from 'react'; import { ReactElement } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { IconComponent, Pill } from 'twenty-ui'; import { IconComponent, Pill } from 'twenty-ui';
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
type TabProps = { type TabProps = {
id: string; id: string;
@ -93,7 +94,7 @@ export const Tab = ({
<StyledHover> <StyledHover>
{logo && <StyledLogo src={logo} alt={`${title} logo`} />} {logo && <StyledLogo src={logo} alt={`${title} logo`} />}
{Icon && <Icon size={theme.icon.size.md} />} {Icon && <Icon size={theme.icon.size.md} />}
{title} <EllipsisDisplay>{title}</EllipsisDisplay>
{pill && typeof pill === 'string' ? <Pill label={pill} /> : pill} {pill && typeof pill === 'string' ? <Pill label={pill} /> : pill}
</StyledHover> </StyledHover>
</StyledTab> </StyledTab>

View File

@ -37,6 +37,10 @@ const StyledTabsContainer = styled.div`
height: 40px; height: 40px;
user-select: none; user-select: none;
margin-bottom: -1px; margin-bottom: -1px;
overflow-y: scroll;
::-webkit-scrollbar {
display: none;
}
`; `;
const StyledContainer = styled.div` const StyledContainer = styled.div`
@ -52,10 +56,10 @@ export const TabList = ({
}: TabListProps) => { }: TabListProps) => {
const visibleTabs = tabs.filter((tab) => !tab.hide); const visibleTabs = tabs.filter((tab) => !tab.hide);
const initialActiveTabId = visibleTabs[0]?.id || '';
const { activeTabId, setActiveTabId } = useTabList(tabListInstanceId); const { activeTabId, setActiveTabId } = useTabList(tabListInstanceId);
const initialActiveTabId = activeTabId || visibleTabs[0]?.id || '';
useEffect(() => { useEffect(() => {
setActiveTabId(initialActiveTabId); setActiveTabId(initialActiveTabId);
}, [initialActiveTabId, setActiveTabId]); }, [initialActiveTabId, setActiveTabId]);

View File

@ -13,11 +13,11 @@ const wrapper = ({ children }: { children: React.ReactNode }) => (
); );
describe('useTriggerNodeSelection', () => { describe('useTriggerNodeSelection', () => {
const mockUpdateNode = jest.fn(); const mockSetNodes = jest.fn();
beforeEach(() => { beforeEach(() => {
(useReactFlow as jest.Mock).mockReturnValue({ (useReactFlow as jest.Mock).mockReturnValue({
updateNode: mockUpdateNode, setNodes: mockSetNodes,
}); });
}); });
@ -51,7 +51,6 @@ describe('useTriggerNodeSelection', () => {
result.current.setWorkflowDiagramTriggerNodeSelection(mockNodeId); result.current.setWorkflowDiagramTriggerNodeSelection(mockNodeId);
}); });
expect(mockUpdateNode).toHaveBeenCalledWith(mockNodeId, { selected: true });
expect(result.current.workflowDiagramTriggerNodeSelection).toBeUndefined(); expect(result.current.workflowDiagramTriggerNodeSelection).toBeUndefined();
}); });
@ -61,6 +60,6 @@ describe('useTriggerNodeSelection', () => {
}); });
// Ensure updateNode is not called when state is undefined // Ensure updateNode is not called when state is undefined
expect(mockUpdateNode).not.toHaveBeenCalled(); expect(mockSetNodes).not.toHaveBeenCalled();
}); });
}); });

View File

@ -1,7 +1,6 @@
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { workflowCreateStepFromParentStepIdState } from '@/workflow/states/workflowCreateStepFromParentStepIdState'; import { workflowCreateStepFromParentStepIdState } from '@/workflow/states/workflowCreateStepFromParentStepIdState';
import { workflowDiagramTriggerNodeSelectionState } from '@/workflow/states/workflowDiagramTriggerNodeSelectionState';
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState'; import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
import { import {
WorkflowStepType, WorkflowStepType,
@ -25,10 +24,6 @@ export const useCreateStep = ({
workflowCreateStepFromParentStepIdState, workflowCreateStepFromParentStepIdState,
); );
const setWorkflowDiagramTriggerNodeSelection = useSetRecoilState(
workflowDiagramTriggerNodeSelectionState,
);
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion(); const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
const createStep = async (newStepType: WorkflowStepType) => { const createStep = async (newStepType: WorkflowStepType) => {
@ -51,15 +46,6 @@ export const useCreateStep = ({
setWorkflowSelectedNode(createdStep.id); setWorkflowSelectedNode(createdStep.id);
openRightDrawer(RightDrawerPages.WorkflowStepEdit); 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 { return {

View File

@ -21,9 +21,12 @@ export const useTriggerNodeSelection = () => {
return; return;
} }
reactflow.updateNode(workflowDiagramTriggerNodeSelection, { reactflow.setNodes((nodes) =>
selected: true, nodes.map((node) => ({
}); ...node,
selected: workflowDiagramTriggerNodeSelection === node.id,
})),
);
setWorkflowDiagramTriggerNodeSelection(undefined); setWorkflowDiagramTriggerNodeSelection(undefined);
}, [ }, [

View File

@ -28,7 +28,8 @@ const StyledDropdownVariableButtonContainer = styled(
`; `;
const StyledDropdownComponentsContainer = styled(DropdownMenuItemsContainer)` const StyledDropdownComponentsContainer = styled(DropdownMenuItemsContainer)`
background-color: ${({ theme }) => theme.background.transparent.light}; display: flex;
flex-direction: column;
`; `;
const SearchVariablesDropdown = ({ const SearchVariablesDropdown = ({

View File

@ -2,6 +2,7 @@ import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenu
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { import {
BaseOutputSchema, BaseOutputSchema,
LinkOutputSchema,
OutputSchema, OutputSchema,
StepOutputSchema, StepOutputSchema,
} from '@/workflow/search-variables/types/StepOutputSchema'; } from '@/workflow/search-variables/types/StepOutputSchema';
@ -13,10 +14,17 @@ import { useState } from 'react';
import { import {
HorizontalSeparator, HorizontalSeparator,
IconChevronLeft, IconChevronLeft,
isDefined,
MenuItemSelect, MenuItemSelect,
OverflowingTextWithTooltip, OverflowingTextWithTooltip,
useIcons, useIcons,
} from 'twenty-ui'; } 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 = { type SearchVariablesDropdownFieldItemsProps = {
step: StepOutputSchema; step: StepOutputSchema;
@ -33,6 +41,13 @@ export const SearchVariablesDropdownFieldItems = ({
const [currentPath, setCurrentPath] = useState<string[]>([]); const [currentPath, setCurrentPath] = useState<string[]>([]);
const [searchInputValue, setSearchInputValue] = useState(''); const [searchInputValue, setSearchInputValue] = useState('');
const { getIcon } = useIcons(); const { getIcon } = useIcons();
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
const { setActiveTabId } = useTabList(
WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID,
);
const setWorkflowDiagramTriggerNodeSelection = useSetRecoilState(
workflowDiagramTriggerNodeSelectionState,
);
const getCurrentSubStep = (): OutputSchema => { const getCurrentSubStep = (): OutputSchema => {
let currentSubStep = step.outputSchema; let currentSubStep = step.outputSchema;
@ -51,7 +66,9 @@ export const SearchVariablesDropdownFieldItems = ({
const getDisplayedSubStepFields = () => { const getDisplayedSubStepFields = () => {
const currentSubStep = getCurrentSubStep(); const currentSubStep = getCurrentSubStep();
if (isRecordOutputSchema(currentSubStep)) { if (isLinkOutputSchema(currentSubStep)) {
return { link: currentSubStep.link };
} else if (isRecordOutputSchema(currentSubStep)) {
return currentSubStep.fields; return currentSubStep.fields;
} else if (isBaseOutputSchema(currentSubStep)) { } else if (isBaseOutputSchema(currentSubStep)) {
return currentSubStep; return currentSubStep;
@ -60,6 +77,7 @@ export const SearchVariablesDropdownFieldItems = ({
const handleSelectField = (key: string) => { const handleSelectField = (key: string) => {
const currentSubStep = getCurrentSubStep(); const currentSubStep = getCurrentSubStep();
const handleSelectBaseOutputSchema = ( const handleSelectBaseOutputSchema = (
baseOutputSchema: BaseOutputSchema, 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); handleSelectBaseOutputSchema(currentSubStep.fields);
} else if (isBaseOutputSchema(currentSubStep)) { } else if (isBaseOutputSchema(currentSubStep)) {
handleSelectBaseOutputSchema(currentSubStep); handleSelectBaseOutputSchema(currentSubStep);

View File

@ -60,6 +60,10 @@ export const SearchVariablesDropdownObjectItems = ({
const getDisplayedSubStepObject = () => { const getDisplayedSubStepObject = () => {
const currentSubStep = getCurrentSubStep(); const currentSubStep = getCurrentSubStep();
if (!isRecordOutputSchema(currentSubStep)) {
return;
}
return currentSubStep.object; return currentSubStep.object;
}; };

View File

@ -10,6 +10,7 @@ import {
MenuItem, MenuItem,
MenuItemSelect, MenuItemSelect,
OverflowingTextWithTooltip, OverflowingTextWithTooltip,
useIcons,
} from 'twenty-ui'; } from 'twenty-ui';
type SearchVariablesDropdownWorkflowStepItemsProps = { type SearchVariablesDropdownWorkflowStepItemsProps = {
@ -24,6 +25,7 @@ export const SearchVariablesDropdownWorkflowStepItems = ({
onSelect, onSelect,
}: SearchVariablesDropdownWorkflowStepItemsProps) => { }: SearchVariablesDropdownWorkflowStepItemsProps) => {
const theme = useTheme(); const theme = useTheme();
const { getIcon } = useIcons();
const [searchInputValue, setSearchInputValue] = useState(''); const [searchInputValue, setSearchInputValue] = useState('');
const { closeDropdown } = useDropdown(dropdownId); const { closeDropdown } = useDropdown(dropdownId);
@ -60,7 +62,7 @@ export const SearchVariablesDropdownWorkflowStepItems = ({
hovered={false} hovered={false}
onClick={() => onSelect(item.id)} onClick={() => onSelect(item.id)}
text={item.name} text={item.name}
LeftIcon={undefined} LeftIcon={item.icon ? getIcon(item.icon) : undefined}
hasSubMenu hasSubMenu
/> />
)) ))

View File

@ -82,6 +82,7 @@ export const useAvailableVariablesInWorkflowStep = ({
id: previousStep.id, id: previousStep.id,
name: previousStep.name, name: previousStep.name,
outputSchema: filteredOutputSchema, outputSchema: filteredOutputSchema,
...(previousStep.type === 'CODE' ? { icon: 'IconCode' } : {}),
}); });
} }
}); });

View File

@ -1,6 +1,6 @@
import { InputSchemaPropertyType } from '@/workflow/types/InputSchema'; import { InputSchemaPropertyType } from '@/workflow/types/InputSchema';
export type Leaf = { type Leaf = {
isLeaf: true; isLeaf: true;
type?: InputSchemaPropertyType; type?: InputSchemaPropertyType;
icon?: string; icon?: string;
@ -8,13 +8,20 @@ export type Leaf = {
value: any; value: any;
}; };
export type Node = { type Node = {
isLeaf: false; isLeaf: false;
icon?: string; icon?: string;
label?: string; label?: string;
value: OutputSchema; value: OutputSchema;
}; };
type Link = {
isLeaf: true;
tab?: string;
icon?: string;
label?: string;
};
export type BaseOutputSchema = Record<string, Leaf | Node>; export type BaseOutputSchema = Record<string, Leaf | Node>;
export type RecordOutputSchema = { export type RecordOutputSchema = {
@ -23,10 +30,19 @@ export type RecordOutputSchema = {
_outputSchemaType: 'RECORD'; _outputSchemaType: 'RECORD';
}; };
export type OutputSchema = BaseOutputSchema | RecordOutputSchema; export type LinkOutputSchema = {
link: Link;
_outputSchemaType: 'LINK';
};
export type OutputSchema =
| BaseOutputSchema
| RecordOutputSchema
| LinkOutputSchema;
export type StepOutputSchema = { export type StepOutputSchema = {
id: string; id: string;
name: string; name: string;
icon?: string;
outputSchema: OutputSchema; outputSchema: OutputSchema;
}; };

View File

@ -6,6 +6,7 @@ import {
import { isBaseOutputSchema } from '@/workflow/search-variables/utils/isBaseOutputSchema'; import { isBaseOutputSchema } from '@/workflow/search-variables/utils/isBaseOutputSchema';
import { isRecordOutputSchema } from '@/workflow/search-variables/utils/isRecordOutputSchema'; import { isRecordOutputSchema } from '@/workflow/search-variables/utils/isRecordOutputSchema';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
import { isLinkOutputSchema } from '@/workflow/search-variables/utils/isLinkOutputSchema';
const isValidRecordOutputSchema = ( const isValidRecordOutputSchema = (
outputSchema: RecordOutputSchema, outputSchema: RecordOutputSchema,
@ -105,7 +106,9 @@ export const filterOutputSchema = (
return outputSchema; return outputSchema;
} }
if (isRecordOutputSchema(outputSchema)) { if (isLinkOutputSchema(outputSchema)) {
return outputSchema;
} else if (isRecordOutputSchema(outputSchema)) {
return filterRecordOutputSchema(outputSchema, objectNameSingularToSelect); return filterRecordOutputSchema(outputSchema, objectNameSingularToSelect);
} else if (isBaseOutputSchema(outputSchema)) { } else if (isBaseOutputSchema(outputSchema)) {
return filterBaseOutputSchema(outputSchema, objectNameSingularToSelect); return filterBaseOutputSchema(outputSchema, objectNameSingularToSelect);

View File

@ -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';
};

View File

@ -15,7 +15,7 @@ import { editor } from 'monaco-editor';
import { AutoTypings } from 'monaco-editor-auto-typings'; import { AutoTypings } from 'monaco-editor-auto-typings';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil'; 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 { useDebouncedCallback } from 'use-debounce';
import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody'; import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
import { TabList } from '@/ui/layout/tab/components/TabList'; 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 { getFunctionInputFromSourceCode } from '@/serverless-functions/utils/getFunctionInputFromSourceCode';
import { mergeDefaultFunctionInputAndFunctionInput } from '@/serverless-functions/utils/mergeDefaultFunctionInputAndFunctionInput'; import { mergeDefaultFunctionInputAndFunctionInput } from '@/serverless-functions/utils/mergeDefaultFunctionInputAndFunctionInput';
import { WorkflowEditActionFormServerlessFunctionFields } from '@/workflow/workflow-actions/components/WorkflowEditActionFormServerlessFunctionFields'; 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` const StyledContainer = styled.div`
display: flex; display: flex;
@ -66,17 +67,19 @@ type ServerlessFunctionInputFormData = {
[field: string]: string | ServerlessFunctionInputFormData; [field: string]: string | ServerlessFunctionInputFormData;
}; };
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 { activeTabId, setActiveTabId } = useTabList(TAB_LIST_COMPONENT_ID);
const { updateOneServerlessFunction } = useUpdateOneServerlessFunction();
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
const serverlessFunctionId = action.settings.input.serverlessFunctionId; 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 workflowId = useRecoilValue(workflowIdState);
const workflow = useWorkflowWithCurrentVersion(workflowId); const workflow = useWorkflowWithCurrentVersion(workflowId);
const { availablePackages } = useGetAvailablePackages({ const { availablePackages } = useGetAvailablePackages({
@ -112,12 +115,11 @@ export const WorkflowEditActionFormServerlessFunction = ({
const handleSave = useDebouncedCallback(async () => { const handleSave = useDebouncedCallback(async () => {
await updateOneServerlessFunction({ await updateOneServerlessFunction({
id: serverlessFunctionId,
name: formValues.name, name: formValues.name,
description: formValues.description, description: formValues.description,
code: formValues.code, code: formValues.code,
}); });
}, 1_000); }, 500);
const onCodeChange = async (newCode: string) => { const onCodeChange = async (newCode: string) => {
if (actionOptions.readonly === true) { if (actionOptions.readonly === true) {
@ -161,7 +163,15 @@ export const WorkflowEditActionFormServerlessFunction = ({
...action, ...action,
settings: { settings: {
...action.settings, ...action.settings,
outputSchema: {}, outputSchema: {
link: {
isLeaf: true,
icon: 'IconVariable',
tab: 'test',
label: 'Generate Function Input',
},
_outputSchemaType: 'LINK',
},
input: { input: {
...action.settings.input, ...action.settings.input,
serverlessFunctionInput: newMergedInput, serverlessFunctionInput: newMergedInput,
@ -169,7 +179,7 @@ export const WorkflowEditActionFormServerlessFunction = ({
}, },
}); });
}, },
1_000, 500,
); );
const handleInputChange = async (value: any, path: string[]) => { const handleInputChange = async (value: any, path: string[]) => {
@ -254,7 +264,7 @@ export const WorkflowEditActionFormServerlessFunction = ({
!loading && ( !loading && (
<StyledContainer> <StyledContainer>
<StyledTabList <StyledTabList
tabListInstanceId={TAB_LIST_COMPONENT_ID} tabListInstanceId={WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID}
tabs={tabs} tabs={tabs}
behaveAsLinks={false} behaveAsLinks={false}
/> />
@ -277,7 +287,7 @@ export const WorkflowEditActionFormServerlessFunction = ({
readonly={actionOptions.readonly} readonly={actionOptions.readonly}
/> />
<StyledCodeEditorContainer> <StyledCodeEditorContainer>
<InputLabel>Code</InputLabel> <InputLabel>Code {!isReady && <span></span>}</InputLabel>
<CodeEditor <CodeEditor
height={343} height={343}
value={formValues.code?.[INDEX_FILE_PATH]} value={formValues.code?.[INDEX_FILE_PATH]}

View File

@ -0,0 +1,2 @@
export const WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID =
'workflow-serverless-function-tab-list-component-id';

View File

@ -33,21 +33,6 @@ import {
import { SETTINGS_OBJECT_DETAIL_TABS } from '~/pages/settings/data-model/constants/SettingsObjectDetailTabs'; import { SETTINGS_OBJECT_DETAIL_TABS } from '~/pages/settings/data-model/constants/SettingsObjectDetailTabs';
import { updatedObjectSlugState } from '~/pages/settings/data-model/states/updatedObjectSlugState'; import { updatedObjectSlugState } from '~/pages/settings/data-model/states/updatedObjectSlugState';
const StyledTabListContainer = styled.div`
align-items: center;
border-bottom: ${({ theme }) => `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` const StyledContentContainer = styled.div`
flex: 1; flex: 1;
width: 100%; width: 100%;
@ -177,15 +162,13 @@ export const SettingsObjectDetailPage = () => {
} }
> >
<SettingsPageContainer> <SettingsPageContainer>
<StyledTabListContainer> <TabList
<TabList tabListInstanceId={
tabListInstanceId={ SETTINGS_OBJECT_DETAIL_TABS.COMPONENT_INSTANCE_ID
SETTINGS_OBJECT_DETAIL_TABS.COMPONENT_INSTANCE_ID }
} tabs={tabs}
tabs={tabs} className="tab-list"
className="tab-list" />
/>
</StyledTabListContainer>
<StyledContentContainer> <StyledContentContainer>
{renderActiveTabContent()} {renderActiveTabContent()}
</StyledContentContainer> </StyledContentContainer>

View File

@ -32,7 +32,8 @@ export const SettingsServerlessFunctionDetail = () => {
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const { activeTabId, setActiveTabId } = useTabList(TAB_LIST_COMPONENT_ID); const { activeTabId, setActiveTabId } = useTabList(TAB_LIST_COMPONENT_ID);
const [isCodeValid, setIsCodeValid] = useState(true); const [isCodeValid, setIsCodeValid] = useState(true);
const { updateOneServerlessFunction } = useUpdateOneServerlessFunction(); const { updateOneServerlessFunction } =
useUpdateOneServerlessFunction(serverlessFunctionId);
const { publishOneServerlessFunction } = usePublishOneServerlessFunction(); const { publishOneServerlessFunction } = usePublishOneServerlessFunction();
const { formValues, setFormValues, loading } = const { formValues, setFormValues, loading } =
useServerlessFunctionUpdateFormState(serverlessFunctionId); useServerlessFunctionUpdateFormState(serverlessFunctionId);
@ -45,7 +46,6 @@ export const SettingsServerlessFunctionDetail = () => {
const handleSave = useDebouncedCallback(async () => { const handleSave = useDebouncedCallback(async () => {
await updateOneServerlessFunction({ await updateOneServerlessFunction({
id: serverlessFunctionId,
name: formValues.name, name: formValues.name,
description: formValues.description, description: formValues.description,
code: formValues.code, code: formValues.code,

View File

@ -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 { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service'; 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'; import { isDefined } from 'src/utils/is-defined';
export type BuildServerlessFunctionBatchEvent = { export type BuildServerlessFunctionBatchEvent = {
@ -42,10 +45,16 @@ export class BuildServerlessFunctionJob {
}); });
if (isDefined(serverlessFunction)) { if (isDefined(serverlessFunction)) {
await this.serverlessFunctionRepository.update(serverlessFunction.id, {
syncStatus: ServerlessFunctionSyncStatus.NOT_READY,
});
await this.serverlessService.build( await this.serverlessService.build(
serverlessFunction, serverlessFunction,
serverlessFunctionVersion, serverlessFunctionVersion,
); );
await this.serverlessFunctionRepository.update(serverlessFunction.id, {
syncStatus: ServerlessFunctionSyncStatus.READY,
});
} }
} }
} }

View File

@ -277,12 +277,6 @@ export class ServerlessFunctionService {
serverlessFunctionVersion: 'draft', serverlessFunctionVersion: 'draft',
workspaceId, workspaceId,
}); });
await this.serverlessFunctionRepository.update(
existingServerlessFunction.id,
{
syncStatus: ServerlessFunctionSyncStatus.READY,
},
);
return this.serverlessFunctionRepository.findOneBy({ return this.serverlessFunctionRepository.findOneBy({
id: existingServerlessFunction.id, id: existingServerlessFunction.id,

View File

@ -81,6 +81,15 @@ export class WorkflowVersionStepWorkspaceService {
valid: false, valid: false,
settings: { settings: {
...BASE_STEP_DEFINITION, ...BASE_STEP_DEFINITION,
outputSchema: {
link: {
isLeaf: true,
icon: 'IconVariable',
tab: 'test',
label: 'Generate Function Input',
},
_outputSchemaType: 'LINK',
},
input: { input: {
serverlessFunctionId: newServerlessFunction.id, serverlessFunctionId: newServerlessFunction.id,
serverlessFunctionVersion: 'draft', serverlessFunctionVersion: 'draft',

View File

@ -15,6 +15,13 @@ export type Node = {
value: OutputSchema; value: OutputSchema;
}; };
type Link = {
isLeaf: true;
tab?: string;
icon?: string;
label?: string;
};
export type BaseOutputSchema = Record<string, Leaf | Node>; export type BaseOutputSchema = Record<string, Leaf | Node>;
export type RecordOutputSchema = { export type RecordOutputSchema = {
@ -23,4 +30,12 @@ export type RecordOutputSchema = {
_outputSchemaType: 'RECORD'; _outputSchemaType: 'RECORD';
}; };
export type OutputSchema = BaseOutputSchema | RecordOutputSchema; export type LinkOutputSchema = {
link: Link;
_outputSchemaType: 'LINK';
};
export type OutputSchema =
| BaseOutputSchema
| RecordOutputSchema
| LinkOutputSchema;

View File

@ -250,6 +250,7 @@ export {
IconUserCircle, IconUserCircle,
IconUserPlus, IconUserPlus,
IconUsers, IconUsers,
IconVariable,
IconVariablePlus, IconVariablePlus,
IconVideo, IconVideo,
IconWand, IconWand,