Fix form record picker field (#11817)
- enrich response so the record is available in the step output. Today this is available in the schema but only the id is set - make the full record picker clickable instead of the arrow only <img width="467" alt="Capture d’écran 2025-04-30 à 16 08 04" src="https://github.com/user-attachments/assets/db74b9a6-7f1d-4e54-bf06-9be3d67ee398" />
This commit is contained in:
@ -30,7 +30,7 @@ type FormSingleRecordFieldChipProps = {
|
|||||||
};
|
};
|
||||||
selectedRecord?: ObjectRecord;
|
selectedRecord?: ObjectRecord;
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
onRemove: () => void;
|
onRemove: (event?: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -11,19 +11,36 @@ import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-r
|
|||||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
|
||||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||||
|
import { css, useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { isDefined, isValidUuid } from 'twenty-shared/utils';
|
import { isDefined, isValidUuid } from 'twenty-shared/utils';
|
||||||
import { IconChevronDown, IconForbid } from 'twenty-ui/display';
|
import { IconChevronDown, IconForbid } from 'twenty-ui/display';
|
||||||
import { LightIconButton } from 'twenty-ui/input';
|
|
||||||
|
|
||||||
const StyledFormSelectContainer = styled(FormFieldInputInnerContainer)`
|
const StyledFormSelectContainer = styled(FormFieldInputInnerContainer)<{
|
||||||
justify-content: space-between;
|
readonly?: boolean;
|
||||||
|
}>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
height: 32px;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||||
|
|
||||||
|
${({ readonly, theme }) =>
|
||||||
|
!readonly &&
|
||||||
|
css`
|
||||||
|
&:hover,
|
||||||
|
&[data-open='true'] {
|
||||||
|
background-color: ${theme.background.transparent.light};
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIconButton = styled.div`
|
||||||
|
display: flex;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type RecordId = string;
|
export type RecordId = string;
|
||||||
@ -58,6 +75,7 @@ export const FormSingleRecordPicker = ({
|
|||||||
testId,
|
testId,
|
||||||
VariablePicker,
|
VariablePicker,
|
||||||
}: FormSingleRecordPickerProps) => {
|
}: FormSingleRecordPickerProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
const draftValue: FormSingleRecordPickerValue = isStandaloneVariableString(
|
const draftValue: FormSingleRecordPickerValue = isStandaloneVariableString(
|
||||||
defaultValue,
|
defaultValue,
|
||||||
)
|
)
|
||||||
@ -103,12 +121,11 @@ export const FormSingleRecordPicker = ({
|
|||||||
|
|
||||||
const handleVariableTagInsert = (variable: string) => {
|
const handleVariableTagInsert = (variable: string) => {
|
||||||
onChange?.(variable);
|
onChange?.(variable);
|
||||||
closeDropdown();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUnlinkVariable = () => {
|
const handleUnlinkVariable = (event?: React.MouseEvent<HTMLDivElement>) => {
|
||||||
closeDropdown();
|
// Prevents the dropdown to open when clicking on the chip
|
||||||
|
event?.stopPropagation();
|
||||||
onChange('');
|
onChange('');
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -130,47 +147,57 @@ export const FormSingleRecordPicker = ({
|
|||||||
<FormFieldInputContainer testId={testId}>
|
<FormFieldInputContainer testId={testId}>
|
||||||
{label ? <InputLabel>{label}</InputLabel> : null}
|
{label ? <InputLabel>{label}</InputLabel> : null}
|
||||||
<FormFieldInputRowContainer>
|
<FormFieldInputRowContainer>
|
||||||
<StyledFormSelectContainer
|
{disabled ? (
|
||||||
hasRightElement={isDefined(VariablePicker) && !disabled}
|
<StyledFormSelectContainer hasRightElement={false} readonly>
|
||||||
preventSetHotkeyScope={true}
|
<FormSingleRecordFieldChip
|
||||||
>
|
draftValue={draftValue}
|
||||||
<FormSingleRecordFieldChip
|
selectedRecord={selectedRecord}
|
||||||
draftValue={draftValue}
|
objectNameSingular={objectNameSingular}
|
||||||
selectedRecord={selectedRecord}
|
onRemove={handleUnlinkVariable}
|
||||||
objectNameSingular={objectNameSingular}
|
disabled={disabled}
|
||||||
onRemove={handleUnlinkVariable}
|
/>
|
||||||
disabled={disabled}
|
</StyledFormSelectContainer>
|
||||||
/>
|
) : (
|
||||||
{!disabled && (
|
<Dropdown
|
||||||
<DropdownScope dropdownScopeId={dropdownId}>
|
dropdownId={dropdownId}
|
||||||
<Dropdown
|
dropdownPlacement="bottom-start"
|
||||||
dropdownId={dropdownId}
|
clickableComponentWidth={'100%'}
|
||||||
dropdownPlacement="left-start"
|
onClose={handleCloseRelationPickerDropdown}
|
||||||
onClose={handleCloseRelationPickerDropdown}
|
onOpen={handleOpenDropdown}
|
||||||
onOpen={handleOpenDropdown}
|
clickableComponent={
|
||||||
clickableComponent={
|
<StyledFormSelectContainer
|
||||||
<LightIconButton
|
hasRightElement={isDefined(VariablePicker) && !disabled}
|
||||||
className="displayOnHover"
|
preventSetHotkeyScope={true}
|
||||||
Icon={IconChevronDown}
|
>
|
||||||
accent="tertiary"
|
<FormSingleRecordFieldChip
|
||||||
|
draftValue={draftValue}
|
||||||
|
selectedRecord={selectedRecord}
|
||||||
|
objectNameSingular={objectNameSingular}
|
||||||
|
onRemove={handleUnlinkVariable}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<StyledIconButton>
|
||||||
|
<IconChevronDown
|
||||||
|
size={theme.icon.size.md}
|
||||||
|
color={theme.font.color.light}
|
||||||
/>
|
/>
|
||||||
}
|
</StyledIconButton>
|
||||||
dropdownComponents={
|
</StyledFormSelectContainer>
|
||||||
<SingleRecordPicker
|
}
|
||||||
componentInstanceId={dropdownId}
|
dropdownComponents={
|
||||||
EmptyIcon={IconForbid}
|
<SingleRecordPicker
|
||||||
emptyLabel={'No ' + objectNameSingular}
|
componentInstanceId={dropdownId}
|
||||||
onCancel={() => closeDropdown()}
|
EmptyIcon={IconForbid}
|
||||||
onRecordSelected={handleRecordSelected}
|
emptyLabel={'No ' + objectNameSingular}
|
||||||
objectNameSingular={objectNameSingular}
|
onCancel={() => closeDropdown()}
|
||||||
recordPickerInstanceId={dropdownId}
|
onRecordSelected={handleRecordSelected}
|
||||||
/>
|
objectNameSingular={objectNameSingular}
|
||||||
}
|
recordPickerInstanceId={dropdownId}
|
||||||
dropdownHotkeyScope={{ scope: dropdownId }}
|
|
||||||
/>
|
/>
|
||||||
</DropdownScope>
|
}
|
||||||
)}
|
dropdownHotkeyScope={{ scope: dropdownId }}
|
||||||
</StyledFormSelectContainer>
|
/>
|
||||||
|
)}
|
||||||
{isDefined(VariablePicker) && !disabled && (
|
{isDefined(VariablePicker) && !disabled && (
|
||||||
<VariablePicker
|
<VariablePicker
|
||||||
inputId={variablesDropdownId}
|
inputId={variablesDropdownId}
|
||||||
|
|||||||
@ -0,0 +1,98 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { expect, fn, userEvent, within } from '@storybook/test';
|
||||||
|
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
|
||||||
|
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||||
|
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||||
|
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
|
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||||
|
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
|
||||||
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
|
import { MOCKED_STEP_ID } from '~/testing/mock-data/workflow';
|
||||||
|
import { FormSingleRecordPicker } from '../FormSingleRecordPicker';
|
||||||
|
|
||||||
|
const meta: Meta<typeof FormSingleRecordPicker> = {
|
||||||
|
title: 'UI/Data/Field/Form/Input/FormSingleRecordPicker',
|
||||||
|
component: FormSingleRecordPicker,
|
||||||
|
parameters: {
|
||||||
|
msw: graphqlMocks,
|
||||||
|
},
|
||||||
|
args: {},
|
||||||
|
argTypes: {},
|
||||||
|
decorators: [
|
||||||
|
I18nFrontDecorator,
|
||||||
|
ObjectMetadataItemsDecorator,
|
||||||
|
ComponentDecorator,
|
||||||
|
WorkspaceDecorator,
|
||||||
|
SnackBarDecorator,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof FormSingleRecordPicker>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Company',
|
||||||
|
defaultValue: '123e4567-e89b-12d3-a456-426614174000',
|
||||||
|
objectNameSingular: 'company',
|
||||||
|
onChange: fn(),
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
await canvas.findByText('Company');
|
||||||
|
const dropdown = await canvas.findByRole('button');
|
||||||
|
expect(dropdown).toBeVisible();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithVariables: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Company',
|
||||||
|
defaultValue: `{{${MOCKED_STEP_ID}.company.id}}`,
|
||||||
|
objectNameSingular: 'company',
|
||||||
|
onChange: fn(),
|
||||||
|
VariablePicker: () => <div>VariablePicker</div>,
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
WorkflowStepDecorator,
|
||||||
|
ComponentDecorator,
|
||||||
|
ObjectMetadataItemsDecorator,
|
||||||
|
SnackBarDecorator,
|
||||||
|
RouterDecorator,
|
||||||
|
],
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
await canvas.findByText('Company');
|
||||||
|
const variablePicker = await canvas.findByText('VariablePicker');
|
||||||
|
expect(variablePicker).toBeVisible();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Disabled: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Company',
|
||||||
|
defaultValue: '123e4567-e89b-12d3-a456-426614174000',
|
||||||
|
objectNameSingular: 'company',
|
||||||
|
onChange: fn(),
|
||||||
|
disabled: true,
|
||||||
|
VariablePicker: () => <div>VariablePicker</div>,
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement, args }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
await canvas.findByText('Company');
|
||||||
|
const dropdown = canvas.queryByRole('button');
|
||||||
|
expect(dropdown).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
// Variable picker should not be visible when disabled
|
||||||
|
const variablePicker = canvas.queryByText('VariablePicker');
|
||||||
|
expect(variablePicker).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
// Clicking should not trigger onChange
|
||||||
|
await userEvent.click(dropdown);
|
||||||
|
expect(args.onChange).not.toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -25,19 +25,24 @@ import { isDefined } from 'twenty-shared/utils';
|
|||||||
import { useIsMobile } from 'twenty-ui/utilities';
|
import { useIsMobile } from 'twenty-ui/utilities';
|
||||||
import { useDropdown } from '../hooks/useDropdown';
|
import { useDropdown } from '../hooks/useDropdown';
|
||||||
|
|
||||||
|
type Width = `${string}px` | `${number}%` | 'auto' | number;
|
||||||
const StyledDropdownFallbackAnchor = styled.div`
|
const StyledDropdownFallbackAnchor = styled.div`
|
||||||
left: 0;
|
left: 0;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledClickableComponent = styled.div`
|
const StyledClickableComponent = styled.div<{
|
||||||
|
width?: Width;
|
||||||
|
}>`
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
width: ${({ width }) => width ?? 'auto'};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type DropdownProps = {
|
export type DropdownProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
clickableComponent?: ReactNode;
|
clickableComponent?: ReactNode;
|
||||||
|
clickableComponentWidth?: Width;
|
||||||
dropdownComponents: ReactNode;
|
dropdownComponents: ReactNode;
|
||||||
hotkey?: {
|
hotkey?: {
|
||||||
key: Keys;
|
key: Keys;
|
||||||
@ -46,7 +51,7 @@ export type DropdownProps = {
|
|||||||
dropdownHotkeyScope: HotkeyScope;
|
dropdownHotkeyScope: HotkeyScope;
|
||||||
dropdownId: string;
|
dropdownId: string;
|
||||||
dropdownPlacement?: Placement;
|
dropdownPlacement?: Placement;
|
||||||
dropdownWidth?: `${string}px` | `${number}%` | 'auto' | number;
|
dropdownWidth?: Width;
|
||||||
dropdownOffset?: DropdownOffset;
|
dropdownOffset?: DropdownOffset;
|
||||||
dropdownStrategy?: 'fixed' | 'absolute';
|
dropdownStrategy?: 'fixed' | 'absolute';
|
||||||
onClickOutside?: () => void;
|
onClickOutside?: () => void;
|
||||||
@ -70,6 +75,7 @@ export const Dropdown = ({
|
|||||||
onClose,
|
onClose,
|
||||||
onOpen,
|
onOpen,
|
||||||
avoidPortal,
|
avoidPortal,
|
||||||
|
clickableComponentWidth = 'auto',
|
||||||
}: DropdownProps) => {
|
}: DropdownProps) => {
|
||||||
const { isDropdownOpen, toggleDropdown } = useDropdown(dropdownId);
|
const { isDropdownOpen, toggleDropdown } = useDropdown(dropdownId);
|
||||||
|
|
||||||
@ -159,6 +165,7 @@ export const Dropdown = ({
|
|||||||
aria-expanded={isDropdownOpen}
|
aria-expanded={isDropdownOpen}
|
||||||
aria-haspopup={true}
|
aria-haspopup={true}
|
||||||
role="button"
|
role="button"
|
||||||
|
width={clickableComponentWidth}
|
||||||
>
|
>
|
||||||
{clickableComponent}
|
{clickableComponent}
|
||||||
</StyledClickableComponent>
|
</StyledClickableComponent>
|
||||||
|
|||||||
@ -10,4 +10,5 @@ export enum WorkflowVersionStepExceptionCode {
|
|||||||
NOT_FOUND = 'NOT_FOUND',
|
NOT_FOUND = 'NOT_FOUND',
|
||||||
UNDEFINED = 'UNDEFINED',
|
UNDEFINED = 'UNDEFINED',
|
||||||
FAILURE = 'FAILURE',
|
FAILURE = 'FAILURE',
|
||||||
|
INVALID = 'INVALID',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined, isValidUuid } from 'twenty-shared/utils';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
@ -25,6 +25,7 @@ import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-execut
|
|||||||
import {
|
import {
|
||||||
WorkflowAction,
|
WorkflowAction,
|
||||||
WorkflowActionType,
|
WorkflowActionType,
|
||||||
|
WorkflowFormAction,
|
||||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||||
import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
|
import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
|
||||||
import { WorkflowRunnerWorkspaceService } from 'src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service';
|
import { WorkflowRunnerWorkspaceService } from 'src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service';
|
||||||
@ -287,16 +288,28 @@ export class WorkflowVersionStepWorkspaceService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (step.type !== WorkflowActionType.FORM) {
|
||||||
|
throw new WorkflowVersionStepException(
|
||||||
|
'Step is not a form',
|
||||||
|
WorkflowVersionStepExceptionCode.INVALID,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const enrichedResponse = await this.enrichFormStepResponse({
|
||||||
|
step,
|
||||||
|
response,
|
||||||
|
});
|
||||||
|
|
||||||
const newStepOutput: StepOutput = {
|
const newStepOutput: StepOutput = {
|
||||||
id: stepId,
|
id: stepId,
|
||||||
output: {
|
output: {
|
||||||
result: response,
|
result: enrichedResponse,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedContext = {
|
const updatedContext = {
|
||||||
...workflowRun.context,
|
...workflowRun.context,
|
||||||
[stepId]: response,
|
[stepId]: enrichedResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.workflowRunWorkspaceService.saveWorkflowRunState({
|
await this.workflowRunWorkspaceService.saveWorkflowRunState({
|
||||||
@ -547,4 +560,49 @@ export class WorkflowVersionStepWorkspaceService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async enrichFormStepResponse({
|
||||||
|
step,
|
||||||
|
response,
|
||||||
|
}: {
|
||||||
|
step: WorkflowFormAction;
|
||||||
|
response: object;
|
||||||
|
}) {
|
||||||
|
const responseKeys = Object.keys(response);
|
||||||
|
|
||||||
|
const enrichedResponses = await Promise.all(
|
||||||
|
responseKeys.map(async (key) => {
|
||||||
|
if (!isDefined(response[key])) {
|
||||||
|
return { key, value: response[key] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = step.settings.input.find((field) => field.name === key);
|
||||||
|
|
||||||
|
if (
|
||||||
|
field?.type === 'RECORD' &&
|
||||||
|
field?.settings?.objectName &&
|
||||||
|
isDefined(response[key].id) &&
|
||||||
|
isValidUuid(response[key].id)
|
||||||
|
) {
|
||||||
|
const repository = await this.twentyORMManager.getRepository(
|
||||||
|
field.settings.objectName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const record = await repository.findOne({
|
||||||
|
where: { id: response[key].id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { key, value: record };
|
||||||
|
} else {
|
||||||
|
return { key, value: response[key] };
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return enrichedResponses.reduce((acc, { key, value }) => {
|
||||||
|
acc[key] = value;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { isDefined } from 'class-validator';
|
||||||
|
import { isValidUuid } from 'twenty-shared/utils';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { WorkflowExecutor } from 'src/modules/workflow/workflow-executor/interfaces/workflow-executor.interface';
|
import { WorkflowExecutor } from 'src/modules/workflow/workflow-executor/interfaces/workflow-executor.interface';
|
||||||
@ -59,6 +61,17 @@ export class DeleteRecordWorkflowAction implements WorkflowExecutor {
|
|||||||
context,
|
context,
|
||||||
) as WorkflowDeleteRecordActionInput;
|
) as WorkflowDeleteRecordActionInput;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isDefined(workflowActionInput.objectRecordId) ||
|
||||||
|
!isValidUuid(workflowActionInput.objectRecordId) ||
|
||||||
|
!isDefined(workflowActionInput.objectName)
|
||||||
|
) {
|
||||||
|
throw new RecordCRUDActionException(
|
||||||
|
'Failed to update: Object record ID and name are required',
|
||||||
|
RecordCRUDActionExceptionCode.INVALID_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const repository = await this.twentyORMManager.getRepository(
|
const repository = await this.twentyORMManager.getRepository(
|
||||||
workflowActionInput.objectName,
|
workflowActionInput.objectName,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import deepEqual from 'deep-equal';
|
import deepEqual from 'deep-equal';
|
||||||
|
import { isDefined, isValidUuid } from 'twenty-shared/utils';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { WorkflowExecutor } from 'src/modules/workflow/workflow-executor/interfaces/workflow-executor.interface';
|
import { WorkflowExecutor } from 'src/modules/workflow/workflow-executor/interfaces/workflow-executor.interface';
|
||||||
@ -67,6 +68,17 @@ export class UpdateRecordWorkflowAction implements WorkflowExecutor {
|
|||||||
context,
|
context,
|
||||||
) as WorkflowUpdateRecordActionInput;
|
) as WorkflowUpdateRecordActionInput;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isDefined(workflowActionInput.objectRecordId) ||
|
||||||
|
!isValidUuid(workflowActionInput.objectRecordId) ||
|
||||||
|
!isDefined(workflowActionInput.objectName)
|
||||||
|
) {
|
||||||
|
throw new RecordCRUDActionException(
|
||||||
|
'Failed to update: Object record ID and name are required',
|
||||||
|
RecordCRUDActionExceptionCode.INVALID_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const repository = await this.twentyORMManager.getRepository(
|
const repository = await this.twentyORMManager.getRepository(
|
||||||
workflowActionInput.objectName,
|
workflowActionInput.objectName,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user