mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-22 03:17:40 +03:00
Fix state mutation serverless action (#8580)
In this PR: - Ensure the `setNestedValue` does a deep copy of the provided object and doesn't mutate it directly. - Store the form's state in a `useState` instead of relying entirely on the backend's state. This makes the form more resilient to slow network connections. - Ensure the input settings are reset when selecting another function. - The Inner component now expects the serverless functions data to be resolved before being mounted, so I split it. Closes https://github.com/twentyhq/twenty/issues/8523
This commit is contained in:
parent
a744515303
commit
c133129eb0
@ -1,6 +1,6 @@
|
|||||||
import { useQuery } from '@apollo/client';
|
|
||||||
import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions';
|
|
||||||
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||||
|
import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions';
|
||||||
|
import { useQuery } from '@apollo/client';
|
||||||
import {
|
import {
|
||||||
GetManyServerlessFunctionsQuery,
|
GetManyServerlessFunctionsQuery,
|
||||||
GetManyServerlessFunctionsQueryVariables,
|
GetManyServerlessFunctionsQueryVariables,
|
||||||
@ -8,13 +8,17 @@ import {
|
|||||||
|
|
||||||
export const useGetManyServerlessFunctions = () => {
|
export const useGetManyServerlessFunctions = () => {
|
||||||
const apolloMetadataClient = useApolloMetadataClient();
|
const apolloMetadataClient = useApolloMetadataClient();
|
||||||
const { data } = useQuery<
|
|
||||||
|
const { data, loading, error } = useQuery<
|
||||||
GetManyServerlessFunctionsQuery,
|
GetManyServerlessFunctionsQuery,
|
||||||
GetManyServerlessFunctionsQueryVariables
|
GetManyServerlessFunctionsQueryVariables
|
||||||
>(FIND_MANY_SERVERLESS_FUNCTIONS, {
|
>(FIND_MANY_SERVERLESS_FUNCTIONS, {
|
||||||
client: apolloMetadataClient ?? undefined,
|
client: apolloMetadataClient ?? undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
serverlessFunctions: data?.findManyServerlessFunctions || [],
|
serverlessFunctions: data?.findManyServerlessFunctions || [],
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,41 +1,6 @@
|
|||||||
import { ReactNode, Fragment } from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { useGetManyServerlessFunctions } from '@/settings/serverless-functions/hooks/useGetManyServerlessFunctions';
|
import { useGetManyServerlessFunctions } from '@/settings/serverless-functions/hooks/useGetManyServerlessFunctions';
|
||||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
import { WorkflowEditActionFormServerlessFunctionInner } from '@/workflow/components/WorkflowEditActionFormServerlessFunctionInner';
|
||||||
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
|
||||||
import VariableTagInput from '@/workflow/search-variables/components/VariableTagInput';
|
|
||||||
import { FunctionInput } from '@/workflow/types/FunctionInput';
|
|
||||||
import { WorkflowCodeAction } from '@/workflow/types/Workflow';
|
import { WorkflowCodeAction } from '@/workflow/types/Workflow';
|
||||||
import { getDefaultFunctionInputFromInputSchema } from '@/workflow/utils/getDefaultFunctionInputFromInputSchema';
|
|
||||||
import { mergeDefaultFunctionInputAndFunctionInput } from '@/workflow/utils/mergeDefaultFunctionInputAndFunctionInput';
|
|
||||||
import { setNestedValue } from '@/workflow/utils/setNestedValue';
|
|
||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import { HorizontalSeparator, IconCode, isDefined } from 'twenty-ui';
|
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: column;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledLabel = styled.div`
|
|
||||||
color: ${({ theme }) => theme.font.color.light};
|
|
||||||
font-size: ${({ theme }) => theme.font.size.md};
|
|
||||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
|
||||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
|
||||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledInputContainer = styled.div`
|
|
||||||
background: ${({ theme }) => theme.background.secondary};
|
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
|
||||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: ${({ theme }) => theme.spacing(2)};
|
|
||||||
padding: ${({ theme }) => theme.spacing(2)};
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
type WorkflowEditActionFormServerlessFunctionProps = {
|
type WorkflowEditActionFormServerlessFunctionProps = {
|
||||||
action: WorkflowCodeAction;
|
action: WorkflowCodeAction;
|
||||||
@ -53,167 +18,17 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
|||||||
action,
|
action,
|
||||||
actionOptions,
|
actionOptions,
|
||||||
}: WorkflowEditActionFormServerlessFunctionProps) => {
|
}: WorkflowEditActionFormServerlessFunctionProps) => {
|
||||||
const theme = useTheme();
|
const { loading: isLoadingServerlessFunctions } =
|
||||||
const { serverlessFunctions } = useGetManyServerlessFunctions();
|
useGetManyServerlessFunctions();
|
||||||
|
|
||||||
const getFunctionInput = (serverlessFunctionId: string) => {
|
if (isLoadingServerlessFunctions) {
|
||||||
if (!serverlessFunctionId) {
|
return null;
|
||||||
return {};
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const serverlessFunction = serverlessFunctions.find(
|
|
||||||
(f) => f.id === serverlessFunctionId,
|
|
||||||
);
|
|
||||||
const inputSchema = serverlessFunction?.latestVersionInputSchema;
|
|
||||||
const defaultFunctionInput =
|
|
||||||
getDefaultFunctionInputFromInputSchema(inputSchema);
|
|
||||||
|
|
||||||
const existingFunctionInput = action.settings.input.serverlessFunctionInput;
|
|
||||||
|
|
||||||
return mergeDefaultFunctionInputAndFunctionInput({
|
|
||||||
defaultFunctionInput,
|
|
||||||
functionInput: existingFunctionInput,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const functionInput = getFunctionInput(
|
|
||||||
action.settings.input.serverlessFunctionId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateFunctionInput = useDebouncedCallback(
|
|
||||||
async (newFunctionInput: object) => {
|
|
||||||
if (actionOptions.readonly === true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
actionOptions.onActionUpdate({
|
|
||||||
...action,
|
|
||||||
settings: {
|
|
||||||
...action.settings,
|
|
||||||
input: {
|
|
||||||
...action.settings.input,
|
|
||||||
serverlessFunctionInput: newFunctionInput,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
1_000,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleInputChange = (value: any, path: string[]) => {
|
|
||||||
updateFunctionInput(setNestedValue(functionInput, path, value));
|
|
||||||
};
|
|
||||||
|
|
||||||
const availableFunctions: Array<SelectOption<string>> = [
|
|
||||||
...serverlessFunctions
|
|
||||||
.filter((serverlessFunction) =>
|
|
||||||
isDefined(serverlessFunction.latestVersion),
|
|
||||||
)
|
|
||||||
.map((serverlessFunction) => ({
|
|
||||||
label: serverlessFunction.name,
|
|
||||||
value: serverlessFunction.id,
|
|
||||||
latestVersionInputSchema: serverlessFunction.latestVersionInputSchema,
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleFunctionChange = (newServerlessFunctionId: string) => {
|
|
||||||
if (actionOptions.readonly === true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverlessFunction = serverlessFunctions.find(
|
|
||||||
(f) => f.id === newServerlessFunctionId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const newProps = {
|
|
||||||
...action,
|
|
||||||
settings: {
|
|
||||||
...action.settings,
|
|
||||||
input: {
|
|
||||||
serverlessFunctionId: newServerlessFunctionId,
|
|
||||||
serverlessFunctionVersion:
|
|
||||||
serverlessFunction?.latestVersion || 'latest',
|
|
||||||
serverlessFunctionInput: getFunctionInput(newServerlessFunctionId),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
actionOptions.onActionUpdate(newProps);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderFields = (
|
|
||||||
functionInput: FunctionInput,
|
|
||||||
path: string[] = [],
|
|
||||||
isRoot = true,
|
|
||||||
): ReactNode[] => {
|
|
||||||
const displaySeparator = (functionInput: FunctionInput) => {
|
|
||||||
const keys = Object.keys(functionInput);
|
|
||||||
if (keys.length > 1) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (keys.length === 1) {
|
|
||||||
const subKeys = Object.keys(functionInput[keys[0]]);
|
|
||||||
return subKeys.length > 0;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
return Object.entries(functionInput).map(([inputKey, inputValue]) => {
|
|
||||||
const currentPath = [...path, inputKey];
|
|
||||||
const pathKey = currentPath.join('.');
|
|
||||||
|
|
||||||
if (inputValue !== null && typeof inputValue === 'object') {
|
|
||||||
if (isRoot) {
|
|
||||||
return (
|
|
||||||
<Fragment key={pathKey}>
|
|
||||||
{displaySeparator(functionInput) && (
|
|
||||||
<HorizontalSeparator noMargin />
|
|
||||||
)}
|
|
||||||
{renderFields(inputValue, currentPath, false)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<StyledContainer key={pathKey}>
|
|
||||||
<StyledLabel>{inputKey}</StyledLabel>
|
|
||||||
<StyledInputContainer>
|
|
||||||
{renderFields(inputValue, currentPath, false)}
|
|
||||||
</StyledInputContainer>
|
|
||||||
</StyledContainer>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<VariableTagInput
|
|
||||||
key={pathKey}
|
|
||||||
inputId={`input-${inputKey}`}
|
|
||||||
label={inputKey}
|
|
||||||
placeholder="Enter value"
|
|
||||||
value={`${inputValue || ''}`}
|
|
||||||
onChange={(value) => handleInputChange(value, currentPath)}
|
|
||||||
readonly={actionOptions.readonly}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkflowEditGenericFormBase
|
<WorkflowEditActionFormServerlessFunctionInner
|
||||||
HeaderIcon={<IconCode color={theme.color.orange} />}
|
action={action}
|
||||||
headerTitle="Code - Serverless Function"
|
actionOptions={actionOptions}
|
||||||
headerType="Code"
|
/>
|
||||||
>
|
|
||||||
<Select
|
|
||||||
dropdownId="select-serverless-function-id"
|
|
||||||
label="Function"
|
|
||||||
fullWidth
|
|
||||||
value={action.settings.input.serverlessFunctionId}
|
|
||||||
options={availableFunctions}
|
|
||||||
emptyOption={{ label: 'None', value: '' }}
|
|
||||||
disabled={actionOptions.readonly}
|
|
||||||
onChange={handleFunctionChange}
|
|
||||||
/>
|
|
||||||
{renderFields(functionInput)}
|
|
||||||
</WorkflowEditGenericFormBase>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,239 @@
|
|||||||
|
import { useGetManyServerlessFunctions } from '@/settings/serverless-functions/hooks/useGetManyServerlessFunctions';
|
||||||
|
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||||
|
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
||||||
|
import VariableTagInput from '@/workflow/search-variables/components/VariableTagInput';
|
||||||
|
import { FunctionInput } from '@/workflow/types/FunctionInput';
|
||||||
|
import { WorkflowCodeAction } from '@/workflow/types/Workflow';
|
||||||
|
import { getDefaultFunctionInputFromInputSchema } from '@/workflow/utils/getDefaultFunctionInputFromInputSchema';
|
||||||
|
import { mergeDefaultFunctionInputAndFunctionInput } from '@/workflow/utils/mergeDefaultFunctionInputAndFunctionInput';
|
||||||
|
import { setNestedValue } from '@/workflow/utils/setNestedValue';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { Fragment, ReactNode, useState } from 'react';
|
||||||
|
import { HorizontalSeparator, IconCode, isDefined } from 'twenty-ui';
|
||||||
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledLabel = styled.div`
|
||||||
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||||
|
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledInputContainer = styled.div`
|
||||||
|
background: ${({ theme }) => theme.background.secondary};
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type WorkflowEditActionFormServerlessFunctionInnerProps = {
|
||||||
|
action: WorkflowCodeAction;
|
||||||
|
actionOptions:
|
||||||
|
| {
|
||||||
|
readonly: true;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
readonly?: false;
|
||||||
|
onActionUpdate: (action: WorkflowCodeAction) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type ServerlessFunctionInputFormData = {
|
||||||
|
[field: string]: string | ServerlessFunctionInputFormData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkflowEditActionFormServerlessFunctionInner = ({
|
||||||
|
action,
|
||||||
|
actionOptions,
|
||||||
|
}: WorkflowEditActionFormServerlessFunctionInnerProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const { serverlessFunctions } = useGetManyServerlessFunctions();
|
||||||
|
|
||||||
|
const getFunctionInput = (serverlessFunctionId: string) => {
|
||||||
|
if (!serverlessFunctionId) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverlessFunction = serverlessFunctions.find(
|
||||||
|
(f) => f.id === serverlessFunctionId,
|
||||||
|
);
|
||||||
|
const inputSchema = serverlessFunction?.latestVersionInputSchema;
|
||||||
|
const defaultFunctionInput =
|
||||||
|
getDefaultFunctionInputFromInputSchema(inputSchema);
|
||||||
|
|
||||||
|
return defaultFunctionInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [selectedFunctionId, setSelectedFunctionId] = useState(
|
||||||
|
action.settings.input.serverlessFunctionId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [functionInput, setFunctionInput] =
|
||||||
|
useState<ServerlessFunctionInputFormData>(
|
||||||
|
mergeDefaultFunctionInputAndFunctionInput({
|
||||||
|
defaultFunctionInput: getFunctionInput(selectedFunctionId),
|
||||||
|
functionInput: action.settings.input.serverlessFunctionInput,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateFunctionInput = useDebouncedCallback(
|
||||||
|
async (newFunctionInput: object) => {
|
||||||
|
if (actionOptions.readonly === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actionOptions.onActionUpdate({
|
||||||
|
...action,
|
||||||
|
settings: {
|
||||||
|
...action.settings,
|
||||||
|
input: {
|
||||||
|
...action.settings.input,
|
||||||
|
serverlessFunctionInput: newFunctionInput,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
1_000,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInputChange = (value: any, path: string[]) => {
|
||||||
|
const updatedFunctionInput = setNestedValue(functionInput, path, value);
|
||||||
|
|
||||||
|
setFunctionInput(updatedFunctionInput);
|
||||||
|
|
||||||
|
updateFunctionInput(updatedFunctionInput);
|
||||||
|
};
|
||||||
|
|
||||||
|
const availableFunctions: Array<SelectOption<string>> = [
|
||||||
|
...serverlessFunctions
|
||||||
|
.filter((serverlessFunction) =>
|
||||||
|
isDefined(serverlessFunction.latestVersion),
|
||||||
|
)
|
||||||
|
.map((serverlessFunction) => ({
|
||||||
|
label: serverlessFunction.name,
|
||||||
|
value: serverlessFunction.id,
|
||||||
|
latestVersionInputSchema: serverlessFunction.latestVersionInputSchema,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleFunctionChange = (newServerlessFunctionId: string) => {
|
||||||
|
if (actionOptions.readonly === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFunctionInput.cancel();
|
||||||
|
|
||||||
|
setSelectedFunctionId(newServerlessFunctionId);
|
||||||
|
|
||||||
|
const serverlessFunction = serverlessFunctions.find(
|
||||||
|
(f) => f.id === newServerlessFunctionId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const newFunctionInput = getFunctionInput(newServerlessFunctionId);
|
||||||
|
|
||||||
|
const newProps = {
|
||||||
|
...action,
|
||||||
|
settings: {
|
||||||
|
...action.settings,
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId: newServerlessFunctionId,
|
||||||
|
serverlessFunctionVersion:
|
||||||
|
serverlessFunction?.latestVersion || 'latest',
|
||||||
|
serverlessFunctionInput: newFunctionInput,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setFunctionInput(newFunctionInput);
|
||||||
|
|
||||||
|
actionOptions.onActionUpdate(newProps);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFields = (
|
||||||
|
functionInput: FunctionInput,
|
||||||
|
path: string[] = [],
|
||||||
|
isRoot = true,
|
||||||
|
): ReactNode[] => {
|
||||||
|
const displaySeparator = (functionInput: FunctionInput) => {
|
||||||
|
const keys = Object.keys(functionInput);
|
||||||
|
if (keys.length > 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (keys.length === 1) {
|
||||||
|
const subKeys = Object.keys(functionInput[keys[0]]);
|
||||||
|
return subKeys.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return Object.entries(functionInput).map(([inputKey, inputValue]) => {
|
||||||
|
const currentPath = [...path, inputKey];
|
||||||
|
const pathKey = currentPath.join('.');
|
||||||
|
|
||||||
|
if (inputValue !== null && typeof inputValue === 'object') {
|
||||||
|
if (isRoot) {
|
||||||
|
return (
|
||||||
|
<Fragment key={pathKey}>
|
||||||
|
{displaySeparator(functionInput) && (
|
||||||
|
<HorizontalSeparator noMargin />
|
||||||
|
)}
|
||||||
|
{renderFields(inputValue, currentPath, false)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<StyledContainer key={pathKey}>
|
||||||
|
<StyledLabel>{inputKey}</StyledLabel>
|
||||||
|
<StyledInputContainer>
|
||||||
|
{renderFields(inputValue, currentPath, false)}
|
||||||
|
</StyledInputContainer>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<VariableTagInput
|
||||||
|
key={pathKey}
|
||||||
|
inputId={`input-${inputKey}`}
|
||||||
|
label={inputKey}
|
||||||
|
placeholder="Enter value"
|
||||||
|
readonly={actionOptions.readonly}
|
||||||
|
value={`${inputValue || ''}`}
|
||||||
|
onChange={(value) => handleInputChange(value, currentPath)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WorkflowEditGenericFormBase
|
||||||
|
HeaderIcon={<IconCode color={theme.color.orange} />}
|
||||||
|
headerTitle="Code - Serverless Function"
|
||||||
|
headerType="Code"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
dropdownId="select-serverless-function-id"
|
||||||
|
label="Function"
|
||||||
|
fullWidth
|
||||||
|
value={selectedFunctionId}
|
||||||
|
options={availableFunctions}
|
||||||
|
emptyOption={{ label: 'None', value: '' }}
|
||||||
|
disabled={actionOptions.readonly}
|
||||||
|
onChange={handleFunctionChange}
|
||||||
|
/>
|
||||||
|
{renderFields(functionInput)}
|
||||||
|
</WorkflowEditGenericFormBase>
|
||||||
|
);
|
||||||
|
};
|
@ -8,4 +8,19 @@ describe('setNestedValue', () => {
|
|||||||
const expectedResult = { a: { b: newValue } };
|
const expectedResult = { a: { b: newValue } };
|
||||||
expect(setNestedValue(obj, path, newValue)).toEqual(expectedResult);
|
expect(setNestedValue(obj, path, newValue)).toEqual(expectedResult);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not mutate the initial object', () => {
|
||||||
|
const expectedObject = { a: { b: 'b' } };
|
||||||
|
|
||||||
|
const initialObject = structuredClone(expectedObject);
|
||||||
|
const path = ['a', 'b'];
|
||||||
|
const newValue = 'bb';
|
||||||
|
|
||||||
|
const updatedObject = setNestedValue(initialObject, path, newValue);
|
||||||
|
|
||||||
|
expect(initialObject).toEqual(expectedObject);
|
||||||
|
|
||||||
|
expect(updatedObject).not.toBe(initialObject);
|
||||||
|
expect(updatedObject.a).not.toBe(initialObject.a);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export const setNestedValue = (obj: any, path: string[], value: any) => {
|
export const setNestedValue = (obj: any, path: string[], value: any) => {
|
||||||
const newObj = { ...obj };
|
const newObj = structuredClone(obj);
|
||||||
path.reduce((o, key, index) => {
|
path.reduce((o, key, index) => {
|
||||||
if (index === path.length - 1) {
|
if (index === path.length - 1) {
|
||||||
o[key] = value;
|
o[key] = value;
|
||||||
|
Loading…
Reference in New Issue
Block a user