Create base search record action (#10135)
Create Search Record action without filters neither sorting <img width="234" alt="Capture d’écran 2025-02-11 à 18 19 25" src="https://github.com/user-attachments/assets/f18caaa3-476a-436f-9f93-1ad506b24da2" /> <img width="262" alt="Capture d’écran 2025-02-11 à 18 19 38" src="https://github.com/user-attachments/assets/25fcbfcf-57fb-476f-aba9-119be7c3e067" /> <img width="236" alt="Capture d’écran 2025-02-11 à 18 19 53" src="https://github.com/user-attachments/assets/1eb2be25-0727-4797-868c-afdc05040e6a" />
This commit is contained in:
@ -55,6 +55,13 @@ export type WorkflowDeleteRecordActionSettings = BaseWorkflowActionSettings & {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WorkflowFindRecordsActionSettings = BaseWorkflowActionSettings & {
|
||||||
|
input: {
|
||||||
|
objectName: string;
|
||||||
|
limit?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
type BaseWorkflowAction = {
|
type BaseWorkflowAction = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -86,12 +93,18 @@ export type WorkflowDeleteRecordAction = BaseWorkflowAction & {
|
|||||||
settings: WorkflowDeleteRecordActionSettings;
|
settings: WorkflowDeleteRecordActionSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WorkflowFindRecordsAction = BaseWorkflowAction & {
|
||||||
|
type: 'FIND_RECORDS';
|
||||||
|
settings: WorkflowFindRecordsActionSettings;
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkflowAction =
|
export type WorkflowAction =
|
||||||
| WorkflowCodeAction
|
| WorkflowCodeAction
|
||||||
| WorkflowSendEmailAction
|
| WorkflowSendEmailAction
|
||||||
| WorkflowCreateRecordAction
|
| WorkflowCreateRecordAction
|
||||||
| WorkflowUpdateRecordAction
|
| WorkflowUpdateRecordAction
|
||||||
| WorkflowDeleteRecordAction;
|
| WorkflowDeleteRecordAction
|
||||||
|
| WorkflowFindRecordsAction;
|
||||||
|
|
||||||
export type WorkflowActionType = WorkflowAction['type'];
|
export type WorkflowActionType = WorkflowAction['type'];
|
||||||
|
|
||||||
|
|||||||
@ -7,11 +7,12 @@ import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
|||||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||||
import { WorkflowEditActionFormCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormCreateRecord';
|
import { WorkflowEditActionFormCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormCreateRecord';
|
||||||
import { WorkflowEditActionFormDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormDeleteRecord';
|
import { WorkflowEditActionFormDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormDeleteRecord';
|
||||||
|
import { WorkflowEditActionFormFindRecords } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormFindRecords';
|
||||||
import { WorkflowEditActionFormSendEmail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormSendEmail';
|
import { WorkflowEditActionFormSendEmail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormSendEmail';
|
||||||
import { WorkflowEditActionFormUpdateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormUpdateRecord';
|
import { WorkflowEditActionFormUpdateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormUpdateRecord';
|
||||||
|
import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm';
|
||||||
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm';
|
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm';
|
||||||
import { WorkflowEditTriggerManualForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManualForm';
|
import { WorkflowEditTriggerManualForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManualForm';
|
||||||
import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm';
|
|
||||||
import { Suspense, lazy } from 'react';
|
import { Suspense, lazy } from 'react';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
import { RightDrawerSkeletonLoader } from '~/loading/components/RightDrawerSkeletonLoader';
|
import { RightDrawerSkeletonLoader } from '~/loading/components/RightDrawerSkeletonLoader';
|
||||||
@ -138,6 +139,16 @@ export const WorkflowStepDetail = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'FIND_RECORDS': {
|
||||||
|
return (
|
||||||
|
<WorkflowEditActionFormFindRecords
|
||||||
|
key={stepId}
|
||||||
|
action={stepDefinition.definition}
|
||||||
|
actionOptions={props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -0,0 +1,147 @@
|
|||||||
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
|
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||||
|
import { WorkflowFindRecordsAction } from '@/workflow/types/Workflow';
|
||||||
|
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput';
|
||||||
|
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||||
|
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
import { HorizontalSeparator, useIcons } from 'twenty-ui';
|
||||||
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
|
type WorkflowEditActionFormFindRecordsProps = {
|
||||||
|
action: WorkflowFindRecordsAction;
|
||||||
|
actionOptions:
|
||||||
|
| {
|
||||||
|
readonly: true;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
readonly?: false;
|
||||||
|
onActionUpdate: (action: WorkflowFindRecordsAction) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type FindRecordsFormData = {
|
||||||
|
objectName: string;
|
||||||
|
limit?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkflowEditActionFormFindRecords = ({
|
||||||
|
action,
|
||||||
|
actionOptions,
|
||||||
|
}: WorkflowEditActionFormFindRecordsProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
|
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
||||||
|
|
||||||
|
const availableMetadata: Array<SelectOption<string>> =
|
||||||
|
activeObjectMetadataItems.map((item) => ({
|
||||||
|
Icon: getIcon(item.icon),
|
||||||
|
label: item.labelPlural,
|
||||||
|
value: item.nameSingular,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<FindRecordsFormData>({
|
||||||
|
objectName: action.settings.input.objectName,
|
||||||
|
limit: action.settings.input.limit,
|
||||||
|
});
|
||||||
|
const isFormDisabled = actionOptions.readonly;
|
||||||
|
|
||||||
|
const selectedObjectMetadataItemNameSingular = formData.objectName;
|
||||||
|
|
||||||
|
const selectedObjectMetadataItem = activeObjectMetadataItems.find(
|
||||||
|
(item) => item.nameSingular === selectedObjectMetadataItemNameSingular,
|
||||||
|
);
|
||||||
|
if (!isDefined(selectedObjectMetadataItem)) {
|
||||||
|
throw new Error('Should have found the metadata item');
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveAction = useDebouncedCallback(
|
||||||
|
async (formData: FindRecordsFormData) => {
|
||||||
|
if (actionOptions.readonly === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { objectName: updatedObjectName, limit: updatedLimit } = formData;
|
||||||
|
|
||||||
|
actionOptions.onActionUpdate({
|
||||||
|
...action,
|
||||||
|
settings: {
|
||||||
|
...action.settings,
|
||||||
|
input: {
|
||||||
|
objectName: updatedObjectName,
|
||||||
|
limit: updatedLimit ?? 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
1_000,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
saveAction.flush();
|
||||||
|
};
|
||||||
|
}, [saveAction]);
|
||||||
|
|
||||||
|
const headerTitle = isDefined(action.name) ? action.name : `Search Records`;
|
||||||
|
const headerIcon = getActionIcon(action.type);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<WorkflowStepHeader
|
||||||
|
onTitleChange={(newName: string) => {
|
||||||
|
if (actionOptions.readonly === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actionOptions.onActionUpdate({
|
||||||
|
...action,
|
||||||
|
name: newName,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
Icon={getIcon(headerIcon)}
|
||||||
|
iconColor={theme.font.color.tertiary}
|
||||||
|
initialTitle={headerTitle}
|
||||||
|
headerType="Action"
|
||||||
|
disabled={isFormDisabled}
|
||||||
|
/>
|
||||||
|
<WorkflowStepBody>
|
||||||
|
<Select
|
||||||
|
dropdownId="workflow-edit-action-record-find-records-object-name"
|
||||||
|
label="Object"
|
||||||
|
fullWidth
|
||||||
|
disabled={isFormDisabled}
|
||||||
|
value={formData.objectName}
|
||||||
|
emptyOption={{ label: 'Select an option', value: '' }}
|
||||||
|
options={availableMetadata}
|
||||||
|
onChange={(objectName) => {
|
||||||
|
const newFormData: FindRecordsFormData = {
|
||||||
|
objectName,
|
||||||
|
limit: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
setFormData(newFormData);
|
||||||
|
|
||||||
|
saveAction(newFormData);
|
||||||
|
}}
|
||||||
|
withSearchInput
|
||||||
|
/>
|
||||||
|
|
||||||
|
<HorizontalSeparator noMargin />
|
||||||
|
|
||||||
|
<FormNumberFieldInput
|
||||||
|
label="Limit"
|
||||||
|
defaultValue={formData.limit}
|
||||||
|
placeholder="Enter limit"
|
||||||
|
onPersist={() => {}}
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</WorkflowStepBody>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
import { WorkflowFindRecordsAction } from '@/workflow/types/Workflow';
|
||||||
|
import { WorkflowEditActionFormFindRecords } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormFindRecords';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { expect, fn, userEvent, within } from '@storybook/test';
|
||||||
|
import { ComponentDecorator, RouterDecorator } from 'twenty-ui';
|
||||||
|
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||||
|
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
|
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
||||||
|
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||||
|
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
|
||||||
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
|
import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow';
|
||||||
|
|
||||||
|
const DEFAULT_ACTION = {
|
||||||
|
id: getWorkflowNodeIdMock(),
|
||||||
|
name: 'Search Records',
|
||||||
|
type: 'FIND_RECORDS',
|
||||||
|
valid: false,
|
||||||
|
settings: {
|
||||||
|
input: {
|
||||||
|
objectName: 'person',
|
||||||
|
limit: 1,
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: {
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
continueOnFailure: {
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies WorkflowFindRecordsAction;
|
||||||
|
|
||||||
|
const meta: Meta<typeof WorkflowEditActionFormFindRecords> = {
|
||||||
|
title: 'Modules/Workflow/WorkflowEditActionFormFindRecords',
|
||||||
|
component: WorkflowEditActionFormFindRecords,
|
||||||
|
parameters: {
|
||||||
|
msw: graphqlMocks,
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
action: DEFAULT_ACTION,
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
WorkflowStepActionDrawerDecorator,
|
||||||
|
WorkflowStepDecorator,
|
||||||
|
ComponentDecorator,
|
||||||
|
ObjectMetadataItemsDecorator,
|
||||||
|
SnackBarDecorator,
|
||||||
|
RouterDecorator,
|
||||||
|
WorkspaceDecorator,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof WorkflowEditActionFormFindRecords>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
actionOptions: {
|
||||||
|
onActionUpdate: fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DisabledWithEmptyValues: Story = {
|
||||||
|
args: {
|
||||||
|
actionOptions: {
|
||||||
|
readonly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const titleInput = await canvas.findByDisplayValue('Search Records');
|
||||||
|
|
||||||
|
expect(titleInput).toBeDisabled();
|
||||||
|
|
||||||
|
const objectSelectCurrentValue = await canvas.findByText('People');
|
||||||
|
|
||||||
|
await userEvent.click(objectSelectCurrentValue);
|
||||||
|
|
||||||
|
{
|
||||||
|
const searchInputInSelectDropdown =
|
||||||
|
canvas.queryByPlaceholderText('Search');
|
||||||
|
|
||||||
|
expect(searchInputInSelectDropdown).not.toBeInTheDocument();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -20,4 +20,9 @@ export const RECORD_ACTIONS: Array<{
|
|||||||
type: 'DELETE_RECORD',
|
type: 'DELETE_RECORD',
|
||||||
icon: 'IconTrash',
|
icon: 'IconTrash',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Search Records',
|
||||||
|
type: 'FIND_RECORDS',
|
||||||
|
icon: 'IconSearch',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -6,6 +6,7 @@ export const getActionIcon = (actionType: string) => {
|
|||||||
case 'CREATE_RECORD':
|
case 'CREATE_RECORD':
|
||||||
case 'UPDATE_RECORD':
|
case 'UPDATE_RECORD':
|
||||||
case 'DELETE_RECORD':
|
case 'DELETE_RECORD':
|
||||||
|
case 'FIND_RECORDS':
|
||||||
return RECORD_ACTIONS.find((item) => item.type === actionType)?.icon;
|
return RECORD_ACTIONS.find((item) => item.type === actionType)?.icon;
|
||||||
default:
|
default:
|
||||||
return OTHER_ACTIONS.find((item) => item.type === actionType)?.icon;
|
return OTHER_ACTIONS.find((item) => item.type === actionType)?.icon;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { isDefined } from 'twenty-shared';
|
|
||||||
|
|
||||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema';
|
import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema';
|
||||||
@ -186,6 +186,26 @@ export class WorkflowVersionStepWorkspaceService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case WorkflowActionType.FIND_RECORDS: {
|
||||||
|
const activeObjectMetadataItem =
|
||||||
|
await this.objectMetadataRepository.findOne({
|
||||||
|
where: { workspaceId, isActive: true, isSystem: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: newStepId,
|
||||||
|
name: 'Search Records',
|
||||||
|
type: WorkflowActionType.FIND_RECORDS,
|
||||||
|
valid: false,
|
||||||
|
settings: {
|
||||||
|
...BASE_STEP_DEFINITION,
|
||||||
|
input: {
|
||||||
|
objectName: activeObjectMetadataItem?.nameSingular || '',
|
||||||
|
limit: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new WorkflowVersionStepException(
|
throw new WorkflowVersionStepException(
|
||||||
`WorkflowActionType '${type}' unknown`,
|
`WorkflowActionType '${type}' unknown`,
|
||||||
|
|||||||
Reference in New Issue
Block a user