Disable the fields of all CRUD workflow actions on readonly mode (#9939)

Fixes
https://discord.com/channels/1130383047699738754/1333822806504247467

In this PR:

- Make the workflow step title input readonly when the visualizer is in
readonly mode
- Make all the fields of the Update Record and Delete Record readonly
when the visualizer is in readonly mode
- Create stories for the Create Record, Updated Record and Delete Record
actions; I'm checking for the default mode and several variants of the
disabled mode
- Set up mocks for the workflows and use them in msw handlers

Follow up:

- We use `readonly` and `disabled` alternatively; these are two
different states when talking about a HTML `<input />` element. I think
we should settle on a single word.
- Refactor the `<WorkflowSingleRecordPicker />` component to behave as
other selects

| Current component | Should look like |
|--------|--------|
| ![CleanShot 2025-01-30 at 17 30
29@2x](https://github.com/user-attachments/assets/104f2e7f-d758-4121-987a-f62f2e138df2)
| ![CleanShot 2025-01-30 at 17 30
49@2x](https://github.com/user-attachments/assets/e74b318e-a41a-40b9-9db8-bcc8015a1d67)
|
This commit is contained in:
Baptiste Devessier
2025-01-31 12:31:57 +01:00
committed by GitHub
parent 4e32fd1c98
commit d946cdcba4
16 changed files with 2227 additions and 186 deletions

View File

@ -27,6 +27,7 @@ type FormMultiSelectFieldInputProps = {
VariablePicker?: VariablePickerComponent;
readonly?: boolean;
placeholder?: string;
testId?: string;
};
const StyledDisplayModeReadonlyContainer = styled.div`
@ -68,6 +69,7 @@ export const FormMultiSelectFieldInput = ({
VariablePicker,
readonly,
placeholder,
testId,
}: FormMultiSelectFieldInputProps) => {
const inputId = useId();
const theme = useTheme();
@ -176,7 +178,7 @@ export const FormMultiSelectFieldInput = ({
const placeholderText = placeholder ?? label;
return (
<FormFieldInputContainer>
<FormFieldInputContainer data-testid={testId}>
{label ? <InputLabel>{label}</InputLabel> : null}
<FormFieldInputRowContainer>

View File

@ -49,12 +49,14 @@ export const WorkflowStepHeader = ({
iconColor,
initialTitle,
headerType,
disabled,
}: {
onTitleChange: (newTitle: string) => void;
Icon: IconComponent;
iconColor: string;
initialTitle: string;
headerType: string;
disabled?: boolean;
}) => {
const theme = useTheme();
const [title, setTitle] = useState(initialTitle);
@ -67,17 +69,16 @@ export const WorkflowStepHeader = ({
return (
<StyledHeader>
<StyledHeaderIconContainer>
{
<Icon
color={iconColor}
stroke={theme.icon.stroke.sm}
size={theme.icon.size.lg}
/>
}
<Icon
color={iconColor}
stroke={theme.icon.stroke.sm}
size={theme.icon.size.lg}
/>
</StyledHeaderIconContainer>
<StyledHeaderInfo>
<StyledHeaderTitle>
<TextInput
disabled={disabled}
value={title}
copyButton={false}
hotkeyScope="workflow-step-title"

View File

@ -0,0 +1,89 @@
import { Meta, StoryObj } from '@storybook/react';
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
import { ComponentDecorator, IconPlus, THEME_LIGHT } from 'twenty-ui';
import { WorkflowStepHeader } from '../WorkflowStepHeader';
const meta: Meta<typeof WorkflowStepHeader> = {
title: 'Modules/Workflow/WorkflowStepHeader',
component: WorkflowStepHeader,
args: {
onTitleChange: fn(),
},
argTypes: {},
decorators: [ComponentDecorator],
};
export default meta;
type Story = StoryObj<typeof WorkflowStepHeader>;
export const Default: Story = {
args: {
headerType: 'Action',
iconColor: THEME_LIGHT.font.color.tertiary,
initialTitle: 'Create Record',
Icon: IconPlus,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(await canvas.findByDisplayValue('Create Record')).toBeVisible();
expect(await canvas.findByText('Action')).toBeVisible();
},
};
export const EditableTitle: Story = {
args: {
headerType: 'Action',
iconColor: THEME_LIGHT.font.color.tertiary,
initialTitle: 'Create Record',
Icon: IconPlus,
onTitleChange: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const titleInput = await canvas.findByDisplayValue('Create Record');
const NEW_TITLE = 'New Title';
await userEvent.clear(titleInput);
await waitFor(() => {
expect(args.onTitleChange).toHaveBeenCalledWith('');
});
await userEvent.type(titleInput, NEW_TITLE);
await waitFor(() => {
expect(args.onTitleChange).toHaveBeenCalledWith(NEW_TITLE);
});
expect(args.onTitleChange).toHaveBeenCalledTimes(2);
expect(titleInput).toHaveValue(NEW_TITLE);
},
};
export const Disabled: Story = {
args: {
headerType: 'Action',
iconColor: THEME_LIGHT.font.color.tertiary,
initialTitle: 'Create Record',
Icon: IconPlus,
disabled: true,
onTitleChange: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const titleInput = await canvas.findByDisplayValue('Create Record');
expect(titleInput).toBeDisabled();
const NEW_TITLE = 'New Title';
await userEvent.type(titleInput, NEW_TITLE);
expect(args.onTitleChange).not.toHaveBeenCalled();
expect(titleInput).toHaveValue('Create Record');
},
};

View File

@ -177,6 +177,7 @@ export const WorkflowEditActionFormCreateRecord = ({
iconColor={theme.font.color.tertiary}
initialTitle={headerTitle}
headerType="Action"
disabled={isFormDisabled}
/>
<WorkflowStepBody>
<Select

View File

@ -125,6 +125,7 @@ export const WorkflowEditActionFormDeleteRecord = ({
iconColor={theme.font.color.tertiary}
initialTitle={headerTitle}
headerType="Action"
disabled={isFormDisabled}
/>
<WorkflowStepBody>
<Select
@ -157,6 +158,8 @@ export const WorkflowEditActionFormDeleteRecord = ({
}
objectNameSingular={formData.objectName}
defaultValue={formData.objectRecordId}
testId="workflow-edit-action-record-delete-object-record-id"
disabled={isFormDisabled}
/>
</WorkflowStepBody>
</>

View File

@ -177,6 +177,7 @@ export const WorkflowEditActionFormUpdateRecord = ({
iconColor={theme.font.color.tertiary}
initialTitle={headerTitle}
headerType="Action"
disabled={isFormDisabled}
/>
<WorkflowStepBody>
@ -205,15 +206,18 @@ export const WorkflowEditActionFormUpdateRecord = ({
<HorizontalSeparator noMargin />
<WorkflowSingleRecordPicker
testId="workflow-edit-action-record-update-object-record-id"
label="Record"
onChange={(objectRecordId) =>
handleFieldChange('objectRecordId', objectRecordId)
}
objectNameSingular={formData.objectName}
defaultValue={formData.objectRecordId}
disabled={isFormDisabled}
/>
<FormMultiSelectFieldInput
testId="workflow-edit-action-record-update-fields-to-update"
label="Fields to update"
defaultValue={formData.fieldsToUpdate}
options={inlineFieldDefinitions.map((field) => ({
@ -226,6 +230,7 @@ export const WorkflowEditActionFormUpdateRecord = ({
handleFieldChange('fieldsToUpdate', fieldsToUpdate)
}
placeholder="Select fields to update"
readonly={isFormDisabled}
/>
<HorizontalSeparator noMargin />

View File

@ -32,6 +32,7 @@ type WorkflowSingleRecordFieldChipProps = {
selectedRecord?: ObjectRecord;
objectNameSingular: string;
onRemove: () => void;
disabled?: boolean;
};
export const WorkflowSingleRecordFieldChip = ({
@ -39,6 +40,7 @@ export const WorkflowSingleRecordFieldChip = ({
selectedRecord,
objectNameSingular,
onRemove,
disabled,
}: WorkflowSingleRecordFieldChipProps) => {
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
@ -50,7 +52,7 @@ export const WorkflowSingleRecordFieldChip = ({
return (
<VariableChipStandalone
rawVariableName={objectMetadataItem.labelSingular}
onRemove={onRemove}
onRemove={disabled ? undefined : onRemove}
/>
);
}

View File

@ -7,6 +7,7 @@ import {
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { SingleRecordSelect } from '@/object-record/relation-picker/components/SingleRecordSelect';
import { useRecordPicker } from '@/object-record/relation-picker/hooks/useRecordPicker';
@ -24,18 +25,7 @@ import styled from '@emotion/styled';
import { useCallback } from 'react';
import { isValidUuid } from '~/utils/isValidUuid';
const StyledFormSelectContainer = styled.div`
background-color: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
border-bottom-left-radius: ${({ theme }) => theme.border.radius.sm};
border-right: none;
border-bottom-right-radius: none;
border-top-right-radius: none;
box-sizing: border-box;
display: flex;
overflow: 'hidden';
width: 100%;
const StyledFormSelectContainer = styled(FormFieldInputInputContainer)`
justify-content: space-between;
align-items: center;
padding-right: ${({ theme }) => theme.spacing(1)};
@ -76,6 +66,8 @@ export type WorkflowSingleRecordPickerProps = {
defaultValue: RecordId | Variable;
onChange: (value: RecordId | Variable) => void;
objectNameSingular: string;
disabled?: boolean;
testId?: string;
};
export const WorkflowSingleRecordPicker = ({
@ -83,6 +75,8 @@ export const WorkflowSingleRecordPicker = ({
defaultValue,
objectNameSingular,
onChange,
disabled,
testId,
}: WorkflowSingleRecordPickerProps) => {
const draftValue: WorkflowSingleRecordPickerValue =
isStandaloneVariableString(defaultValue)
@ -137,60 +131,65 @@ export const WorkflowSingleRecordPicker = ({
};
return (
<FormFieldInputContainer>
<FormFieldInputContainer data-testid={testId}>
{label ? <InputLabel>{label}</InputLabel> : null}
<FormFieldInputRowContainer>
<StyledFormSelectContainer>
<StyledFormSelectContainer hasRightElement={!disabled}>
<WorkflowSingleRecordFieldChip
draftValue={draftValue}
selectedRecord={selectedRecord}
objectNameSingular={objectNameSingular}
onRemove={handleUnlinkVariable}
disabled={disabled}
/>
<DropdownScope dropdownScopeId={dropdownId}>
<Dropdown
dropdownId={dropdownId}
dropdownPlacement="left-start"
onClose={handleCloseRelationPickerDropdown}
clickableComponent={
<LightIconButton
className="displayOnHover"
Icon={IconChevronDown}
accent="tertiary"
/>
}
dropdownComponents={
<RecordPickerComponentInstanceContext.Provider
value={{ instanceId: dropdownId }}
>
<SingleRecordSelect
EmptyIcon={IconForbid}
emptyLabel={'No ' + objectNameSingular}
onCancel={() => closeDropdown()}
onRecordSelected={handleRecordSelected}
objectNameSingular={objectNameSingular}
recordPickerInstanceId={dropdownId}
selectedRecordIds={
draftValue?.value &&
!isStandaloneVariableString(draftValue.value)
? [draftValue.value]
: []
}
{!disabled && (
<DropdownScope dropdownScopeId={dropdownId}>
<Dropdown
dropdownId={dropdownId}
dropdownPlacement="left-start"
onClose={handleCloseRelationPickerDropdown}
clickableComponent={
<LightIconButton
className="displayOnHover"
Icon={IconChevronDown}
accent="tertiary"
/>
</RecordPickerComponentInstanceContext.Provider>
}
dropdownHotkeyScope={{ scope: dropdownId }}
/>
</DropdownScope>
}
dropdownComponents={
<RecordPickerComponentInstanceContext.Provider
value={{ instanceId: dropdownId }}
>
<SingleRecordSelect
EmptyIcon={IconForbid}
emptyLabel={'No ' + objectNameSingular}
onCancel={() => closeDropdown()}
onRecordSelected={handleRecordSelected}
objectNameSingular={objectNameSingular}
recordPickerInstanceId={dropdownId}
selectedRecordIds={
draftValue?.value &&
!isStandaloneVariableString(draftValue.value)
? [draftValue.value]
: []
}
/>
</RecordPickerComponentInstanceContext.Provider>
}
dropdownHotkeyScope={{ scope: dropdownId }}
/>
</DropdownScope>
)}
</StyledFormSelectContainer>
<StyledSearchVariablesDropdownContainer>
<WorkflowVariablesDropdown
inputId={variablesDropdownId}
onVariableSelect={handleVariableTagInsert}
disabled={false}
objectNameSingularToSelect={objectNameSingular}
/>
</StyledSearchVariablesDropdownContainer>
{!disabled && (
<StyledSearchVariablesDropdownContainer>
<WorkflowVariablesDropdown
inputId={variablesDropdownId}
onVariableSelect={handleVariableTagInsert}
objectNameSingularToSelect={objectNameSingular}
/>
</StyledSearchVariablesDropdownContainer>
)}
</FormFieldInputRowContainer>
</FormFieldInputContainer>
);

View File

@ -0,0 +1,84 @@
import { expect } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import { fn, userEvent, within } from '@storybook/test';
import { ComponentDecorator } 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 { graphqlMocks } from '~/testing/graphqlMocks';
import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow';
import { WorkflowEditActionFormCreateRecord } from '../WorkflowEditActionFormCreateRecord';
const meta: Meta<typeof WorkflowEditActionFormCreateRecord> = {
title: 'Modules/Workflow/WorkflowEditActionFormCreateRecord',
component: WorkflowEditActionFormCreateRecord,
parameters: {
msw: graphqlMocks,
},
args: {
action: {
id: getWorkflowNodeIdMock(),
name: 'Create Record',
type: 'CREATE_RECORD',
valid: false,
settings: {
input: {
objectName: 'person',
objectRecord: {},
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
},
},
decorators: [
WorkflowStepActionDrawerDecorator,
WorkflowStepDecorator,
ComponentDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,
],
};
export default meta;
type Story = StoryObj<typeof WorkflowEditActionFormCreateRecord>;
export const Default: Story = {
args: {
actionOptions: {
onActionUpdate: fn(),
},
},
};
export const Disabled: Story = {
args: {
actionOptions: {
readonly: true,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const titleInput = await canvas.findByDisplayValue('Create Record');
expect(titleInput).toBeDisabled();
const objectSelectCurrentValue = await canvas.findByText('People');
await userEvent.click(objectSelectCurrentValue);
const searchInputInSelectDropdown = canvas.queryByPlaceholderText('Search');
expect(searchInputInSelectDropdown).not.toBeInTheDocument();
},
};

View File

@ -0,0 +1,201 @@
import { WorkflowDeleteRecordAction } from '@/workflow/types/Workflow';
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 { graphqlMocks } from '~/testing/graphqlMocks';
import { getPeopleMock } from '~/testing/mock-data/people';
import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow';
import { WorkflowEditActionFormDeleteRecord } from '../WorkflowEditActionFormDeleteRecord';
const DEFAULT_ACTION = {
id: getWorkflowNodeIdMock(),
name: 'Delete Record',
type: 'DELETE_RECORD',
valid: false,
settings: {
input: {
objectName: 'person',
objectRecordId: '',
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
} satisfies WorkflowDeleteRecordAction;
const meta: Meta<typeof WorkflowEditActionFormDeleteRecord> = {
title: 'Modules/Workflow/WorkflowEditActionFormDeleteRecord',
component: WorkflowEditActionFormDeleteRecord,
parameters: {
msw: graphqlMocks,
},
args: {
action: DEFAULT_ACTION,
},
decorators: [
WorkflowStepActionDrawerDecorator,
WorkflowStepDecorator,
ComponentDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,
RouterDecorator,
],
};
export default meta;
type Story = StoryObj<typeof WorkflowEditActionFormDeleteRecord>;
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('Delete Record');
expect(titleInput).toBeDisabled();
const objectSelectCurrentValue = await canvas.findByText('People');
await userEvent.click(objectSelectCurrentValue);
{
const searchInputInSelectDropdown =
canvas.queryByPlaceholderText('Search');
expect(searchInputInSelectDropdown).not.toBeInTheDocument();
}
const openRecordSelectButton = within(
await canvas.findByTestId(
'workflow-edit-action-record-delete-object-record-id',
),
).queryByRole('button');
expect(openRecordSelectButton).not.toBeInTheDocument();
},
};
const peopleMock = getPeopleMock()[0];
export const DisabledWithDefaultStaticValues: Story = {
args: {
action: {
...DEFAULT_ACTION,
settings: {
...DEFAULT_ACTION.settings,
input: {
...DEFAULT_ACTION.settings.input,
objectRecordId: peopleMock.id,
},
},
},
actionOptions: {
readonly: true,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const titleInput = await canvas.findByDisplayValue('Delete Record');
expect(titleInput).toBeDisabled();
const objectSelectCurrentValue = await canvas.findByText('People');
await userEvent.click(objectSelectCurrentValue);
{
const searchInputInSelectDropdown =
canvas.queryByPlaceholderText('Search');
expect(searchInputInSelectDropdown).not.toBeInTheDocument();
}
const openRecordSelectButton = within(
await canvas.findByTestId(
'workflow-edit-action-record-delete-object-record-id',
),
).queryByRole('button');
expect(openRecordSelectButton).not.toBeInTheDocument();
const selectedRecordToDelete = await canvas.findByText(
`${peopleMock.name.firstName} ${peopleMock.name.lastName}`,
);
expect(selectedRecordToDelete).toBeVisible();
},
};
export const DisabledWithDefaultVariableValues: Story = {
args: {
action: {
...DEFAULT_ACTION,
settings: {
...DEFAULT_ACTION.settings,
input: {
...DEFAULT_ACTION.settings.input,
objectRecordId: '{{trigger.recordId}}',
},
},
},
actionOptions: {
readonly: true,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const titleInput = await canvas.findByDisplayValue('Delete Record');
expect(titleInput).toBeDisabled();
const objectSelectCurrentValue = await canvas.findByText('People');
await userEvent.click(objectSelectCurrentValue);
{
const searchInputInSelectDropdown =
canvas.queryByPlaceholderText('Search');
expect(searchInputInSelectDropdown).not.toBeInTheDocument();
}
const openRecordSelectButton = within(
await canvas.findByTestId(
'workflow-edit-action-record-delete-object-record-id',
),
).queryByRole('button');
expect(openRecordSelectButton).not.toBeInTheDocument();
const recordVariableToDelete = await within(
canvas.getByTestId('workflow-edit-action-record-delete-object-record-id'),
).findByText('Person');
expect(recordVariableToDelete).toBeVisible();
},
};

View File

@ -0,0 +1,252 @@
import { WorkflowUpdateRecordAction } from '@/workflow/types/Workflow';
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 { graphqlMocks } from '~/testing/graphqlMocks';
import { getPeopleMock } from '~/testing/mock-data/people';
import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow';
import { WorkflowEditActionFormUpdateRecord } from '../WorkflowEditActionFormUpdateRecord';
const DEFAULT_ACTION = {
id: getWorkflowNodeIdMock(),
name: 'Update Record',
type: 'UPDATE_RECORD',
settings: {
input: {
objectName: 'person',
objectRecordId: '',
objectRecord: {},
fieldsToUpdate: [
'updatedAt',
'averageEstimatedNumberOfAtomsInTheUniverse',
'comments',
'createdAt',
'deletedAt',
'name',
'participants',
'percentageOfCompletion',
'score',
'shortNotes',
],
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
valid: false,
} satisfies WorkflowUpdateRecordAction;
const meta: Meta<typeof WorkflowEditActionFormUpdateRecord> = {
title: 'Modules/Workflow/WorkflowEditActionFormUpdateRecord',
component: WorkflowEditActionFormUpdateRecord,
parameters: {
msw: graphqlMocks,
},
args: {
action: DEFAULT_ACTION,
},
decorators: [
WorkflowStepActionDrawerDecorator,
WorkflowStepDecorator,
ComponentDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,
RouterDecorator,
],
};
export default meta;
type Story = StoryObj<typeof WorkflowEditActionFormUpdateRecord>;
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('Update Record');
expect(titleInput).toBeDisabled();
const objectSelectCurrentValue = await canvas.findByText('People');
await userEvent.click(objectSelectCurrentValue);
{
const searchInputInSelectDropdown =
canvas.queryByPlaceholderText('Search');
expect(searchInputInSelectDropdown).not.toBeInTheDocument();
}
const openRecordSelectButton = within(
await canvas.findByTestId(
'workflow-edit-action-record-update-object-record-id',
),
).queryByRole('button');
expect(openRecordSelectButton).not.toBeInTheDocument();
const firstSelectedUpdatableField = await within(
await canvas.findByTestId(
'workflow-edit-action-record-update-fields-to-update',
),
).findByText('Creation date');
await userEvent.click(firstSelectedUpdatableField);
{
const searchInputInSelectDropdown =
canvas.queryByPlaceholderText('Search');
expect(searchInputInSelectDropdown).not.toBeInTheDocument();
}
},
};
const peopleMock = getPeopleMock()[0];
export const DisabledWithDefaultStaticValues: Story = {
args: {
action: {
...DEFAULT_ACTION,
settings: {
...DEFAULT_ACTION.settings,
input: {
...DEFAULT_ACTION.settings.input,
objectRecordId: peopleMock.id,
},
},
},
actionOptions: {
readonly: true,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const titleInput = await canvas.findByDisplayValue('Update Record');
expect(titleInput).toBeDisabled();
const objectSelectCurrentValue = await canvas.findByText('People');
await userEvent.click(objectSelectCurrentValue);
{
const searchInputInSelectDropdown =
canvas.queryByPlaceholderText('Search');
expect(searchInputInSelectDropdown).not.toBeInTheDocument();
}
const selectedRecord = await canvas.findByText(
`${peopleMock.name.firstName} ${peopleMock.name.lastName}`,
);
expect(selectedRecord).toBeVisible();
const openRecordSelectButton = within(
await canvas.findByTestId(
'workflow-edit-action-record-update-object-record-id',
),
).queryByRole('button');
expect(openRecordSelectButton).not.toBeInTheDocument();
const firstSelectedUpdatableField = await within(
await canvas.findByTestId(
'workflow-edit-action-record-update-fields-to-update',
),
).findByText('Creation date');
await userEvent.click(firstSelectedUpdatableField);
{
const searchInputInSelectDropdown =
canvas.queryByPlaceholderText('Search');
expect(searchInputInSelectDropdown).not.toBeInTheDocument();
}
},
};
export const DisabledWithDefaultVariableValues: Story = {
args: {
action: {
...DEFAULT_ACTION,
settings: {
...DEFAULT_ACTION.settings,
input: {
...DEFAULT_ACTION.settings.input,
objectRecordId: '{{trigger.recordId}}',
},
},
},
actionOptions: {
readonly: true,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const titleInput = await canvas.findByDisplayValue('Update Record');
expect(titleInput).toBeDisabled();
const objectSelectCurrentValue = await canvas.findByText('People');
await userEvent.click(objectSelectCurrentValue);
{
const searchInputInSelectDropdown =
canvas.queryByPlaceholderText('Search');
expect(searchInputInSelectDropdown).not.toBeInTheDocument();
}
const openRecordSelectButton = within(
await canvas.findByTestId(
'workflow-edit-action-record-update-object-record-id',
),
).queryByRole('button');
expect(openRecordSelectButton).not.toBeInTheDocument();
const firstSelectedUpdatableField = await within(
await canvas.findByTestId(
'workflow-edit-action-record-update-fields-to-update',
),
).findByText('Creation date');
await userEvent.click(firstSelectedUpdatableField);
{
const searchInputInSelectDropdown =
canvas.queryByPlaceholderText('Search');
expect(searchInputInSelectDropdown).not.toBeInTheDocument();
}
},
};

View File

@ -0,0 +1,13 @@
import styled from '@emotion/styled';
import { Decorator } from '@storybook/react';
const StyledWrapper = styled.div`
display: flex;
flex-direction: column;
`;
export const WorkflowStepActionDrawerDecorator: Decorator = (Story) => (
<StyledWrapper>
<Story />
</StyledWrapper>
);

View File

@ -0,0 +1,21 @@
import { workflowIdState } from '@/workflow/states/workflowIdState';
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
import { Decorator } from '@storybook/react';
import { useEffect } from 'react';
import { useSetRecoilState } from 'recoil';
import {
getWorkflowMock,
getWorkflowNodeIdMock,
} from '~/testing/mock-data/workflow';
export const WorkflowStepDecorator: Decorator = (Story) => {
const setWorkflowId = useSetRecoilState(workflowIdState);
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
useEffect(() => {
setWorkflowId(getWorkflowMock().id);
setWorkflowSelectedNode(getWorkflowNodeIdMock());
}, [setWorkflowId, setWorkflowSelectedNode]);
return <Story />;
};

View File

@ -1,5 +1,5 @@
import { getOperationName } from '@apollo/client/utilities';
import { graphql, http, HttpResponse } from 'msw';
import { graphql, GraphQLQuery, http, HttpResponse } from 'msw';
import { TRACK } from '@/analytics/graphql/queries/track';
import { GET_CLIENT_CONFIG } from '@/client-config/graphql/queries/getClientConfig';
@ -23,6 +23,11 @@ import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
import { GET_PUBLIC_WORKSPACE_DATA_BY_SUBDOMAIN } from '@/auth/graphql/queries/getPublicWorkspaceDataBySubdomain';
import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/mock-metadata-query-result';
import { mockedTasks } from '~/testing/mock-data/tasks';
import {
getWorkflowMock,
getWorkflowVersionsMock,
workflowQueryResult,
} from '~/testing/mock-data/workflow';
import { mockedRemoteServers } from './mock-data/remote-servers';
import { mockedViewFieldsData } from './mock-data/view-fields';
@ -638,135 +643,32 @@ export const graphqlMocks = {
},
});
}),
graphql.query<GraphQLQuery, { objectRecordId: string }>(
'FindOnePerson',
({ variables: { objectRecordId } }) => {
return HttpResponse.json({
data: {
person: peopleMock.find((person) => person.id === objectRecordId),
},
});
},
),
graphql.query('FindManyWorkflows', () => {
return HttpResponse.json({
data: {
workflows: {
__typename: 'WorkflowConnection',
totalCount: 1,
pageInfo: {
__typename: 'PageInfo',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'eyJpZCI6IjIwMGMxNTA4LWYxMDItNGJiOS1hZjMyLWVkYTU1MjM5YWU2MSJ9',
endCursor:
'eyJpZCI6IjIwMGMxNTA4LWYxMDItNGJiOS1hZjMyLWVkYTU1MjM5YWU2MSJ9',
},
edges: [
{
__typename: 'WorkflowEdge',
cursor:
'eyJpZCI6IjIwMGMxNTA4LWYxMDItNGJiOS1hZjMyLWVkYTU1MjM5YWU2MSJ9',
node: {
__typename: 'Workflow',
id: '200c1508-f102-4bb9-af32-eda55239ae61',
},
},
],
},
},
data: workflowQueryResult,
});
}),
graphql.query('FindOneWorkflow', () => {
return HttpResponse.json({
data: {
workflow: {
__typename: 'Workflow',
id: '200c1508-f102-4bb9-af32-eda55239ae61',
name: '1231 qqerrt',
statuses: null,
lastPublishedVersionId: '',
deletedAt: null,
updatedAt: '2024-09-19T10:10:04.505Z',
position: 0,
createdAt: '2024-09-19T10:10:04.505Z',
favorites: {
__typename: 'FavoriteConnection',
edges: [],
},
eventListeners: {
__typename: 'WorkflowEventListenerConnection',
edges: [],
},
runs: {
__typename: 'WorkflowRunConnection',
edges: [],
},
versions: {
__typename: 'WorkflowVersionConnection',
edges: [
{
__typename: 'WorkflowVersionEdge',
node: {
__typename: 'WorkflowVersion',
updatedAt: '2024-09-19T10:13:12.075Z',
steps: null,
createdAt: '2024-09-19T10:10:04.725Z',
status: 'DRAFT',
name: 'v1',
id: 'f618843a-26be-4a54-a60f-f4ce88a594f0',
trigger: {
type: 'DATABASE_EVENT',
settings: {
eventName: 'note.created',
},
},
deletedAt: null,
workflowId: '200c1508-f102-4bb9-af32-eda55239ae61',
},
},
],
},
},
workflow: getWorkflowMock(),
},
});
}),
graphql.query('FindManyWorkflowVersions', () => {
return HttpResponse.json({
data: {
workflowVersions: {
__typename: 'WorkflowVersionConnection',
totalCount: 1,
pageInfo: {
__typename: 'PageInfo',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTE5VDEwOjEwOjA0LjcyNVoiLCJpZCI6ImY2MTg4NDNhLTI2YmUtNGE1NC1hNjBmLWY0Y2U4OGE1OTRmMCJ9',
endCursor:
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTE5VDEwOjEwOjA0LjcyNVoiLCJpZCI6ImY2MTg4NDNhLTI2YmUtNGE1NC1hNjBmLWY0Y2U4OGE1OTRmMCJ9',
},
edges: [
{
__typename: 'WorkflowVersionEdge',
cursor:
'eyJjcmVhdGVkQXQiOiIyMDI0LTA5LTE5VDEwOjEwOjA0LjcyNVoiLCJpZCI6ImY2MTg4NDNhLTI2YmUtNGE1NC1hNjBmLWY0Y2U4OGE1OTRmMCJ9',
node: {
__typename: 'WorkflowVersion',
updatedAt: '2024-09-19T10:13:12.075Z',
steps: null,
createdAt: '2024-09-19T10:10:04.725Z',
status: 'DRAFT',
name: 'v1',
id: 'f618843a-26be-4a54-a60f-f4ce88a594f0',
trigger: {
type: 'DATABASE_EVENT',
settings: {
eventName: 'note.created',
},
},
deletedAt: null,
workflowId: '200c1508-f102-4bb9-af32-eda55239ae61',
workflow: {
__typename: 'Workflow',
id: '200c1508-f102-4bb9-af32-eda55239ae61',
name: '1231 qqerrt',
},
},
},
],
},
workflowVersions: getWorkflowVersionsMock(),
},
});
}),

View File

@ -26,7 +26,7 @@ export const mockedEmptyPersonData = {
__typename: 'Person',
};
export const peopleQueryResult: { people: RecordGqlConnection } = {
export const peopleQueryResult = {
people: {
__typename: 'PersonConnection',
totalCount: 16,
@ -58,8 +58,8 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
email: 'asd.com',
name: {
__typename: 'FullName',
firstName: 'Test ',
lastName: 'tTest',
firstName: 'Test',
lastName: 'Test',
},
noteTargets: {
__typename: 'NoteTargetConnection',
@ -1719,4 +1719,4 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
},
],
},
};
} satisfies { people: RecordGqlConnection };

File diff suppressed because it is too large Load Diff