Add base form action without logic (#10811)

<img width="1298" alt="Capture d’écran 2025-03-12 à 15 32 27"
src="https://github.com/user-attachments/assets/8a3140e5-e165-445e-a718-748aa76b525c"
/>
This commit is contained in:
Thomas Trompette
2025-03-12 18:05:31 +01:00
committed by GitHub
parent a4ef820f13
commit f4a362b53a
26 changed files with 385 additions and 132 deletions

View File

@ -560,7 +560,6 @@ export enum FeatureFlagKey {
IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled', IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled',
IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled', IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled',
IsApprovedAccessDomainsEnabled = 'IsApprovedAccessDomainsEnabled', IsApprovedAccessDomainsEnabled = 'IsApprovedAccessDomainsEnabled',
IsCommandMenuV2Enabled = 'IsCommandMenuV2Enabled',
IsCopilotEnabled = 'IsCopilotEnabled', IsCopilotEnabled = 'IsCopilotEnabled',
IsCustomDomainEnabled = 'IsCustomDomainEnabled', IsCustomDomainEnabled = 'IsCustomDomainEnabled',
IsEventObjectEnabled = 'IsEventObjectEnabled', IsEventObjectEnabled = 'IsEventObjectEnabled',
@ -570,7 +569,8 @@ export enum FeatureFlagKey {
IsPostgreSQLIntegrationEnabled = 'IsPostgreSQLIntegrationEnabled', IsPostgreSQLIntegrationEnabled = 'IsPostgreSQLIntegrationEnabled',
IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled', IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled',
IsUniqueIndexesEnabled = 'IsUniqueIndexesEnabled', IsUniqueIndexesEnabled = 'IsUniqueIndexesEnabled',
IsWorkflowEnabled = 'IsWorkflowEnabled' IsWorkflowEnabled = 'IsWorkflowEnabled',
IsWorkflowFormActionEnabled = 'IsWorkflowFormActionEnabled'
} }
export type Field = { export type Field = {

View File

@ -491,7 +491,6 @@ export enum FeatureFlagKey {
IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled', IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled',
IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled', IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled',
IsApprovedAccessDomainsEnabled = 'IsApprovedAccessDomainsEnabled', IsApprovedAccessDomainsEnabled = 'IsApprovedAccessDomainsEnabled',
IsCommandMenuV2Enabled = 'IsCommandMenuV2Enabled',
IsCopilotEnabled = 'IsCopilotEnabled', IsCopilotEnabled = 'IsCopilotEnabled',
IsCustomDomainEnabled = 'IsCustomDomainEnabled', IsCustomDomainEnabled = 'IsCustomDomainEnabled',
IsEventObjectEnabled = 'IsEventObjectEnabled', IsEventObjectEnabled = 'IsEventObjectEnabled',
@ -501,7 +500,8 @@ export enum FeatureFlagKey {
IsPostgreSQLIntegrationEnabled = 'IsPostgreSQLIntegrationEnabled', IsPostgreSQLIntegrationEnabled = 'IsPostgreSQLIntegrationEnabled',
IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled', IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled',
IsUniqueIndexesEnabled = 'IsUniqueIndexesEnabled', IsUniqueIndexesEnabled = 'IsUniqueIndexesEnabled',
IsWorkflowEnabled = 'IsWorkflowEnabled' IsWorkflowEnabled = 'IsWorkflowEnabled',
IsWorkflowFormActionEnabled = 'IsWorkflowFormActionEnabled'
} }
export type Field = { export type Field = {
@ -1658,6 +1658,8 @@ export type ServerlessFunctionExecutionResult = {
duration: Scalars['Float']; duration: Scalars['Float'];
/** Execution error in JSON format */ /** Execution error in JSON format */
error?: Maybe<Scalars['JSON']>; error?: Maybe<Scalars['JSON']>;
/** Execution Logs */
logs: Scalars['String'];
/** Execution status */ /** Execution status */
status: ServerlessFunctionExecutionStatus; status: ServerlessFunctionExecutionStatus;
}; };
@ -2220,11 +2222,6 @@ export type GetTimelineThreadsFromPersonIdQueryVariables = Exact<{
export type GetTimelineThreadsFromPersonIdQuery = { __typename?: 'Query', getTimelineThreadsFromPersonId: { __typename?: 'TimelineThreadsWithTotal', totalNumberOfThreads: number, timelineThreads: Array<{ __typename?: 'TimelineThread', id: any, read: boolean, visibility: MessageChannelVisibility, lastMessageReceivedAt: string, lastMessageBody: string, subject: string, numberOfMessagesInThread: number, participantCount: number, firstParticipant: { __typename?: 'TimelineThreadParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }, lastTwoParticipants: Array<{ __typename?: 'TimelineThreadParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }> } }; export type GetTimelineThreadsFromPersonIdQuery = { __typename?: 'Query', getTimelineThreadsFromPersonId: { __typename?: 'TimelineThreadsWithTotal', totalNumberOfThreads: number, timelineThreads: Array<{ __typename?: 'TimelineThread', id: any, read: boolean, visibility: MessageChannelVisibility, lastMessageReceivedAt: string, lastMessageBody: string, subject: string, numberOfMessagesInThread: number, participantCount: number, firstParticipant: { __typename?: 'TimelineThreadParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }, lastTwoParticipants: Array<{ __typename?: 'TimelineThreadParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }> } };
export type EmptyQueryVariables = Exact<{ [key: string]: never; }>;
export type EmptyQuery = { __typename: 'Query' };
export type TrackMutationVariables = Exact<{ export type TrackMutationVariables = Exact<{
action: Scalars['String']; action: Scalars['String'];
payload: Scalars['JSON']; payload: Scalars['JSON'];
@ -3065,38 +3062,6 @@ export function useGetTimelineThreadsFromPersonIdLazyQuery(baseOptions?: Apollo.
export type GetTimelineThreadsFromPersonIdQueryHookResult = ReturnType<typeof useGetTimelineThreadsFromPersonIdQuery>; export type GetTimelineThreadsFromPersonIdQueryHookResult = ReturnType<typeof useGetTimelineThreadsFromPersonIdQuery>;
export type GetTimelineThreadsFromPersonIdLazyQueryHookResult = ReturnType<typeof useGetTimelineThreadsFromPersonIdLazyQuery>; export type GetTimelineThreadsFromPersonIdLazyQueryHookResult = ReturnType<typeof useGetTimelineThreadsFromPersonIdLazyQuery>;
export type GetTimelineThreadsFromPersonIdQueryResult = Apollo.QueryResult<GetTimelineThreadsFromPersonIdQuery, GetTimelineThreadsFromPersonIdQueryVariables>; export type GetTimelineThreadsFromPersonIdQueryResult = Apollo.QueryResult<GetTimelineThreadsFromPersonIdQuery, GetTimelineThreadsFromPersonIdQueryVariables>;
export const EmptyDocument = gql`
query Empty {
__typename
}
`;
/**
* __useEmptyQuery__
*
* To run a query within a React component, call `useEmptyQuery` and pass it any options that fit your needs.
* When your component renders, `useEmptyQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useEmptyQuery({
* variables: {
* },
* });
*/
export function useEmptyQuery(baseOptions?: Apollo.QueryHookOptions<EmptyQuery, EmptyQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<EmptyQuery, EmptyQueryVariables>(EmptyDocument, options);
}
export function useEmptyLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<EmptyQuery, EmptyQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<EmptyQuery, EmptyQueryVariables>(EmptyDocument, options);
}
export type EmptyQueryHookResult = ReturnType<typeof useEmptyQuery>;
export type EmptyLazyQueryHookResult = ReturnType<typeof useEmptyLazyQuery>;
export type EmptyQueryResult = Apollo.QueryResult<EmptyQuery, EmptyQueryVariables>;
export const TrackDocument = gql` export const TrackDocument = gql`
mutation Track($action: String!, $payload: JSON!) { mutation Track($action: String!, $payload: JSON!) {
track(action: $action, payload: $payload) { track(action: $action, payload: $payload) {

View File

@ -26,27 +26,12 @@ import { RecordFiltersComponentInstanceContext } from '@/object-record/record-fi
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext'; import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { HttpResponse, graphql } from 'msw'; import { HttpResponse, graphql } from 'msw';
import { IconDotsVertical } from 'twenty-ui'; import { IconDotsVertical } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter'; import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter';
import { CommandMenu } from '../CommandMenu'; import { CommandMenu } from '../CommandMenu';
const openTimeout = 50; const openTimeout = 50;
// Mock workspace with feature flag enabled
const mockWorkspaceWithFeatureFlag = {
...mockCurrentWorkspace,
featureFlags: [
...(mockCurrentWorkspace.featureFlags || []),
{
id: 'mock-id',
key: FeatureFlagKey.IsCommandMenuV2Enabled,
value: true,
workspaceId: mockCurrentWorkspace.id,
},
],
};
const ContextStoreDecorator: Decorator = (Story) => { const ContextStoreDecorator: Decorator = (Story) => {
return ( return (
<RecordFilterGroupsComponentInstanceContext.Provider <RecordFilterGroupsComponentInstanceContext.Provider
@ -92,7 +77,7 @@ const meta: Meta<typeof CommandMenu> = {
commandMenuNavigationStackState, commandMenuNavigationStackState,
); );
setCurrentWorkspace(mockWorkspaceWithFeatureFlag); setCurrentWorkspace(mockCurrentWorkspace);
setCurrentWorkspaceMember(mockedWorkspaceMemberData); setCurrentWorkspaceMember(mockedWorkspaceMemberData);
setIsCommandMenuOpened(true); setIsCommandMenuOpened(true);
setCommandMenuNavigationStack([ setCommandMenuNavigationStack([

View File

@ -4,7 +4,9 @@ import { RightDrawerWorkflowSelectStepTitle } from '@/workflow/workflow-steps/co
import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep'; import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep';
import { OTHER_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/OtherActions'; import { OTHER_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/OtherActions';
import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions'; import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { MenuItemCommand, useIcons } from 'twenty-ui'; import { MenuItemCommand, useIcons } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const CommandMenuWorkflowSelectActionContent = ({ export const CommandMenuWorkflowSelectActionContent = ({
workflow, workflow,
@ -15,6 +17,9 @@ export const CommandMenuWorkflowSelectActionContent = ({
const { createStep } = useCreateStep({ const { createStep } = useCreateStep({
workflow, workflow,
}); });
const isWorkflowFormActionEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsWorkflowFormActionEnabled,
);
return ( return (
<RightDrawerStepListContainer> <RightDrawerStepListContainer>
@ -32,7 +37,9 @@ export const CommandMenuWorkflowSelectActionContent = ({
<RightDrawerWorkflowSelectStepTitle> <RightDrawerWorkflowSelectStepTitle>
Other Other
</RightDrawerWorkflowSelectStepTitle> </RightDrawerWorkflowSelectStepTitle>
{OTHER_ACTIONS.map((action) => ( {OTHER_ACTIONS.filter(
(action) => isWorkflowFormActionEnabled || action.type !== 'FORM',
).map((action) => (
<MenuItemCommand <MenuItemCommand
key={action.type} key={action.type}
LeftIcon={getIcon(action.icon)} LeftIcon={getIcon(action.icon)}

View File

@ -10,6 +10,8 @@ import {
workflowDeleteRecordActionSettingsSchema, workflowDeleteRecordActionSettingsSchema,
workflowFindRecordsActionSchema, workflowFindRecordsActionSchema,
workflowFindRecordsActionSettingsSchema, workflowFindRecordsActionSettingsSchema,
workflowFormActionSchema,
workflowFormActionSettingsSchema,
workflowManualTriggerSchema, workflowManualTriggerSchema,
workflowRunContextSchema, workflowRunContextSchema,
workflowRunOutputSchema, workflowRunOutputSchema,
@ -42,6 +44,9 @@ export type WorkflowDeleteRecordActionSettings = z.infer<
export type WorkflowFindRecordsActionSettings = z.infer< export type WorkflowFindRecordsActionSettings = z.infer<
typeof workflowFindRecordsActionSettingsSchema typeof workflowFindRecordsActionSettingsSchema
>; >;
export type WorkflowFormActionSettings = z.infer<
typeof workflowFormActionSettingsSchema
>;
export type WorkflowCodeAction = z.infer<typeof workflowCodeActionSchema>; export type WorkflowCodeAction = z.infer<typeof workflowCodeActionSchema>;
export type WorkflowSendEmailAction = z.infer< export type WorkflowSendEmailAction = z.infer<
@ -59,6 +64,7 @@ export type WorkflowDeleteRecordAction = z.infer<
export type WorkflowFindRecordsAction = z.infer< export type WorkflowFindRecordsAction = z.infer<
typeof workflowFindRecordsActionSchema typeof workflowFindRecordsActionSchema
>; >;
export type WorkflowFormAction = z.infer<typeof workflowFormActionSchema>;
export type WorkflowAction = z.infer<typeof workflowActionSchema>; export type WorkflowAction = z.infer<typeof workflowActionSchema>;
export type WorkflowActionType = WorkflowAction['type']; export type WorkflowActionType = WorkflowAction['type'];

View File

@ -1,3 +1,4 @@
import { FieldMetadataType } from 'twenty-shared';
import { z } from 'zod'; import { z } from 'zod';
// Base schemas // Base schemas
@ -81,6 +82,19 @@ export const workflowFindRecordsActionSettingsSchema =
}), }),
}); });
export const workflowFormActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.array(
z.object({
label: z.string(),
name: z.string(),
type: z.nativeEnum(FieldMetadataType),
placeholder: z.string().optional(),
settings: z.record(z.any()),
}),
),
});
// Action schemas // Action schemas
export const workflowCodeActionSchema = baseWorkflowActionSchema.extend({ export const workflowCodeActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('CODE'), type: z.literal('CODE'),
@ -118,6 +132,11 @@ export const workflowFindRecordsActionSchema = baseWorkflowActionSchema.extend({
settings: workflowFindRecordsActionSettingsSchema, settings: workflowFindRecordsActionSettingsSchema,
}); });
export const workflowFormActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('FORM'),
settings: workflowFormActionSettingsSchema,
});
// Combined action schema // Combined action schema
export const workflowActionSchema = z.discriminatedUnion('type', [ export const workflowActionSchema = z.discriminatedUnion('type', [
workflowCodeActionSchema, workflowCodeActionSchema,
@ -126,6 +145,7 @@ export const workflowActionSchema = z.discriminatedUnion('type', [
workflowUpdateRecordActionSchema, workflowUpdateRecordActionSchema,
workflowDeleteRecordActionSchema, workflowDeleteRecordActionSchema,
workflowFindRecordsActionSchema, workflowFindRecordsActionSchema,
workflowFormActionSchema,
]); ]);
// Trigger schemas // Trigger schemas

View File

@ -1,11 +1,12 @@
import { WorkflowAction, WorkflowTrigger } from '@/workflow/types/Workflow'; import { WorkflowAction, WorkflowTrigger } from '@/workflow/types/Workflow';
import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; 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 { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
import { WorkflowEditActionFormDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormDeleteRecord'; import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
import { WorkflowEditActionFormFindRecords } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormFindRecords'; import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFindRecords';
import { WorkflowEditActionFormSendEmail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormSendEmail'; import { WorkflowEditActionForm } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionForm';
import { WorkflowEditActionFormUpdateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormUpdateRecord'; import { WorkflowEditActionSendEmail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail';
import { WorkflowEditActionUpdateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord';
import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm'; 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';
@ -13,19 +14,19 @@ 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';
const WorkflowEditActionFormServerlessFunction = lazy(() => const WorkflowEditActionServerlessFunction = lazy(() =>
import( import(
'@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormServerlessFunction' '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionServerlessFunction'
).then((module) => ({ ).then((module) => ({
default: module.WorkflowEditActionFormServerlessFunction, default: module.WorkflowEditActionServerlessFunction,
})), })),
); );
const WorkflowReadonlyActionFormServerlessFunction = lazy(() => const WorkflowReadonlyActionServerlessFunction = lazy(() =>
import( import(
'@/workflow/workflow-steps/workflow-actions/components/WorkflowReadonlyActionFormServerlessFunction' '@/workflow/workflow-steps/workflow-actions/components/WorkflowReadonlyActionServerlessFunction'
).then((module) => ({ ).then((module) => ({
default: module.WorkflowReadonlyActionFormServerlessFunction, default: module.WorkflowReadonlyActionServerlessFunction,
})), })),
); );
@ -106,12 +107,12 @@ export const WorkflowStepDetail = ({
return ( return (
<Suspense fallback={<RightDrawerSkeletonLoader />}> <Suspense fallback={<RightDrawerSkeletonLoader />}>
{props.readonly ? ( {props.readonly ? (
<WorkflowReadonlyActionFormServerlessFunction <WorkflowReadonlyActionServerlessFunction
key={stepId} key={stepId}
action={stepDefinition.definition} action={stepDefinition.definition}
/> />
) : ( ) : (
<WorkflowEditActionFormServerlessFunction <WorkflowEditActionServerlessFunction
key={stepId} key={stepId}
action={stepDefinition.definition} action={stepDefinition.definition}
actionOptions={props} actionOptions={props}
@ -122,7 +123,7 @@ export const WorkflowStepDetail = ({
} }
case 'SEND_EMAIL': { case 'SEND_EMAIL': {
return ( return (
<WorkflowEditActionFormSendEmail <WorkflowEditActionSendEmail
key={stepId} key={stepId}
action={stepDefinition.definition} action={stepDefinition.definition}
actionOptions={props} actionOptions={props}
@ -131,7 +132,7 @@ export const WorkflowStepDetail = ({
} }
case 'CREATE_RECORD': { case 'CREATE_RECORD': {
return ( return (
<WorkflowEditActionFormCreateRecord <WorkflowEditActionCreateRecord
key={stepId} key={stepId}
action={stepDefinition.definition} action={stepDefinition.definition}
actionOptions={props} actionOptions={props}
@ -141,7 +142,7 @@ export const WorkflowStepDetail = ({
case 'UPDATE_RECORD': { case 'UPDATE_RECORD': {
return ( return (
<WorkflowEditActionFormUpdateRecord <WorkflowEditActionUpdateRecord
key={stepId} key={stepId}
action={stepDefinition.definition} action={stepDefinition.definition}
actionOptions={props} actionOptions={props}
@ -151,7 +152,7 @@ export const WorkflowStepDetail = ({
case 'DELETE_RECORD': { case 'DELETE_RECORD': {
return ( return (
<WorkflowEditActionFormDeleteRecord <WorkflowEditActionDeleteRecord
key={stepId} key={stepId}
action={stepDefinition.definition} action={stepDefinition.definition}
actionOptions={props} actionOptions={props}
@ -161,7 +162,17 @@ export const WorkflowStepDetail = ({
case 'FIND_RECORDS': { case 'FIND_RECORDS': {
return ( return (
<WorkflowEditActionFormFindRecords <WorkflowEditActionFindRecords
key={stepId}
action={stepDefinition.definition}
actionOptions={props}
/>
);
}
case 'FORM': {
return (
<WorkflowEditActionForm
key={stepId} key={stepId}
action={stepDefinition.definition} action={stepDefinition.definition}
actionOptions={props} actionOptions={props}

View File

@ -17,7 +17,7 @@ import { JsonValue } from 'type-fest';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import { FieldMetadataType } from '~/generated/graphql'; import { FieldMetadataType } from '~/generated/graphql';
type WorkflowEditActionFormCreateRecordProps = { type WorkflowEditActionCreateRecordProps = {
action: WorkflowCreateRecordAction; action: WorkflowCreateRecordAction;
actionOptions: actionOptions:
| { | {
@ -53,10 +53,10 @@ const sortByViewFieldPosition = (
return 0; return 0;
}; };
export const WorkflowEditActionFormCreateRecord = ({ export const WorkflowEditActionCreateRecord = ({
action, action,
actionOptions, actionOptions,
}: WorkflowEditActionFormCreateRecordProps) => { }: WorkflowEditActionCreateRecordProps) => {
const theme = useTheme(); const theme = useTheme();
const { getIcon } = useIcons(); const { getIcon } = useIcons();

View File

@ -13,7 +13,7 @@ import { HorizontalSeparator, useIcons } from 'twenty-ui';
import { JsonValue } from 'type-fest'; import { JsonValue } from 'type-fest';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
type WorkflowEditActionFormDeleteRecordProps = { type WorkflowEditActionDeleteRecordProps = {
action: WorkflowDeleteRecordAction; action: WorkflowDeleteRecordAction;
actionOptions: actionOptions:
| { | {
@ -30,10 +30,10 @@ type DeleteRecordFormData = {
objectRecordId: string; objectRecordId: string;
}; };
export const WorkflowEditActionFormDeleteRecord = ({ export const WorkflowEditActionDeleteRecord = ({
action, action,
actionOptions, actionOptions,
}: WorkflowEditActionFormDeleteRecordProps) => { }: WorkflowEditActionDeleteRecordProps) => {
const theme = useTheme(); const theme = useTheme();
const { getIcon } = useIcons(); const { getIcon } = useIcons();

View File

@ -12,7 +12,7 @@ import { isDefined } from 'twenty-shared';
import { HorizontalSeparator, useIcons } from 'twenty-ui'; import { HorizontalSeparator, useIcons } from 'twenty-ui';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
type WorkflowEditActionFormFindRecordsProps = { type WorkflowEditActionFindRecordsProps = {
action: WorkflowFindRecordsAction; action: WorkflowFindRecordsAction;
actionOptions: actionOptions:
| { | {
@ -29,10 +29,10 @@ type FindRecordsFormData = {
limit?: number; limit?: number;
}; };
export const WorkflowEditActionFormFindRecords = ({ export const WorkflowEditActionFindRecords = ({
action, action,
actionOptions, actionOptions,
}: WorkflowEditActionFormFindRecordsProps) => { }: WorkflowEditActionFindRecordsProps) => {
const theme = useTheme(); const theme = useTheme();
const { getIcon } = useIcons(); const { getIcon } = useIcons();

View File

@ -0,0 +1,123 @@
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 { InputLabel } from '@/ui/input/components/InputLabel';
import { WorkflowFormAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared';
import { IconChevronDown, IconPlus, useIcons } from 'twenty-ui';
type WorkflowEditActionFormProps = {
action: WorkflowFormAction;
actionOptions:
| {
readonly: true;
}
| {
readonly?: false;
onActionUpdate: (action: WorkflowFormAction) => void;
};
};
const StyledContainer = styled.div`
align-items: center;
background: transparent;
border: none;
display: flex;
font-family: inherit;
padding-inline: ${({ theme }) => theme.spacing(2)};
width: 100%;
cursor: pointer;
&:hover,
&[data-open='true'] {
background-color: ${({ theme }) => theme.background.transparent.lighter};
}
`;
const StyledPlaceholder = styled.div`
color: ${({ theme }) => theme.font.color.light};
font-weight: ${({ theme }) => theme.font.weight.medium};
width: 100%;
`;
const StyledAddFieldContainer = styled.div`
display: flex;
color: ${({ theme }) => theme.font.color.secondary};
font-weight: ${({ theme }) => theme.font.weight.medium};
justify-content: center;
width: 100%;
gap: ${({ theme }) => theme.spacing(0.5)};
`;
export const WorkflowEditActionForm = ({
action,
actionOptions,
}: WorkflowEditActionFormProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const { t } = useLingui();
const headerTitle = isDefined(action.name) ? action.name : `Form`;
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={actionOptions.readonly}
/>
<WorkflowStepBody>
{action.settings.input.map((field) => (
<FormFieldInputContainer key={field.name}>
{field.label ? <InputLabel>{field.label}</InputLabel> : null}
<FormFieldInputRowContainer>
<FormFieldInputInputContainer hasRightElement={false}>
<StyledContainer onClick={() => {}}>
<StyledPlaceholder>{field.placeholder}</StyledPlaceholder>
<IconChevronDown
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
</StyledContainer>
</FormFieldInputInputContainer>
</FormFieldInputRowContainer>
</FormFieldInputContainer>
))}
{!actionOptions.readonly && (
<FormFieldInputContainer>
<FormFieldInputRowContainer>
<FormFieldInputInputContainer hasRightElement={false}>
<StyledContainer onClick={() => {}}>
<StyledAddFieldContainer>
<IconPlus size={theme.icon.size.sm} />
{t`Add Field`}
</StyledAddFieldContainer>
</StyledContainer>
</FormFieldInputInputContainer>
</FormFieldInputRowContainer>
</FormFieldInputContainer>
)}
</WorkflowStepBody>
</>
);
};

View File

@ -27,7 +27,7 @@ import { JsonValue } from 'type-fest';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { useNavigateSettings } from '~/hooks/useNavigateSettings';
type WorkflowEditActionFormSendEmailProps = { type WorkflowEditActionSendEmailProps = {
action: WorkflowSendEmailAction; action: WorkflowSendEmailAction;
actionOptions: actionOptions:
| { | {
@ -46,10 +46,10 @@ type SendEmailFormData = {
body: string; body: string;
}; };
export const WorkflowEditActionFormSendEmail = ({ export const WorkflowEditActionSendEmail = ({
action, action,
actionOptions, actionOptions,
}: WorkflowEditActionFormSendEmailProps) => { }: WorkflowEditActionSendEmailProps) => {
const theme = useTheme(); const theme = useTheme();
const { getIcon } = useIcons(); const { getIcon } = useIcons();
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);

View File

@ -22,7 +22,7 @@ import { TabList } from '@/ui/layout/tab/components/TabList';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
import { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState'; import { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowEditActionFormServerlessFunctionFields } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormServerlessFunctionFields'; import { WorkflowEditActionServerlessFunctionFields } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionServerlessFunctionFields';
import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/constants/WorkflowServerlessFunctionTabListComponentId'; import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/constants/WorkflowServerlessFunctionTabListComponentId';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon'; import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/utils/getWrongExportedFunctionMarkers'; import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/utils/getWrongExportedFunctionMarkers';
@ -54,7 +54,7 @@ const StyledTabList = styled(TabList)`
padding-left: ${({ theme }) => theme.spacing(2)}; padding-left: ${({ theme }) => theme.spacing(2)};
`; `;
type WorkflowEditActionFormServerlessFunctionProps = { type WorkflowEditActionServerlessFunctionProps = {
action: WorkflowCodeAction; action: WorkflowCodeAction;
actionOptions: actionOptions:
| { | {
@ -70,10 +70,10 @@ type ServerlessFunctionInputFormData = {
[field: string]: string | ServerlessFunctionInputFormData; [field: string]: string | ServerlessFunctionInputFormData;
}; };
export const WorkflowEditActionFormServerlessFunction = ({ export const WorkflowEditActionServerlessFunction = ({
action, action,
actionOptions, actionOptions,
}: WorkflowEditActionFormServerlessFunctionProps) => { }: WorkflowEditActionServerlessFunctionProps) => {
const theme = useTheme(); const theme = useTheme();
const { getIcon } = useIcons(); const { getIcon } = useIcons();
const serverlessFunctionId = action.settings.input.serverlessFunctionId; const serverlessFunctionId = action.settings.input.serverlessFunctionId;
@ -303,7 +303,7 @@ export const WorkflowEditActionFormServerlessFunction = ({
<WorkflowStepBody> <WorkflowStepBody>
{activeTabId === 'code' && ( {activeTabId === 'code' && (
<> <>
<WorkflowEditActionFormServerlessFunctionFields <WorkflowEditActionServerlessFunctionFields
functionInput={functionInput} functionInput={functionInput}
VariablePicker={WorkflowVariablePicker} VariablePicker={WorkflowVariablePicker}
onInputChange={handleInputChange} onInputChange={handleInputChange}
@ -327,7 +327,7 @@ export const WorkflowEditActionFormServerlessFunction = ({
)} )}
{activeTabId === 'test' && ( {activeTabId === 'test' && (
<> <>
<WorkflowEditActionFormServerlessFunctionFields <WorkflowEditActionServerlessFunctionFields
functionInput={serverlessFunctionTestData.input} functionInput={serverlessFunctionTestData.input}
onInputChange={handleTestInputChange} onInputChange={handleTestInputChange}
readonly={actionOptions.readonly} readonly={actionOptions.readonly}

View File

@ -11,7 +11,7 @@ const StyledContainer = styled.div`
flex-direction: column; flex-direction: column;
`; `;
type WorkflowEditActionFormServerlessFunctionFieldsProps = { type WorkflowEditActionServerlessFunctionFieldsProps = {
functionInput: FunctionInput; functionInput: FunctionInput;
path?: string[]; path?: string[];
readonly?: boolean; readonly?: boolean;
@ -19,13 +19,13 @@ type WorkflowEditActionFormServerlessFunctionFieldsProps = {
VariablePicker?: VariablePickerComponent; VariablePicker?: VariablePickerComponent;
}; };
export const WorkflowEditActionFormServerlessFunctionFields = ({ export const WorkflowEditActionServerlessFunctionFields = ({
functionInput, functionInput,
path = [], path = [],
readonly, readonly,
onInputChange, onInputChange,
VariablePicker, VariablePicker,
}: WorkflowEditActionFormServerlessFunctionFieldsProps) => { }: WorkflowEditActionServerlessFunctionFieldsProps) => {
return ( return (
<> <>
{Object.entries(functionInput).map(([inputKey, inputValue]) => { {Object.entries(functionInput).map(([inputKey, inputValue]) => {
@ -37,7 +37,7 @@ export const WorkflowEditActionFormServerlessFunctionFields = ({
<StyledContainer key={pathKey}> <StyledContainer key={pathKey}>
<InputLabel>{inputKey}</InputLabel> <InputLabel>{inputKey}</InputLabel>
<FormNestedFieldInputContainer> <FormNestedFieldInputContainer>
<WorkflowEditActionFormServerlessFunctionFields <WorkflowEditActionServerlessFunctionFields
functionInput={inputValue} functionInput={inputValue}
path={currentPath} path={currentPath}
readonly={readonly} readonly={readonly}

View File

@ -18,7 +18,7 @@ import { JsonValue } from 'type-fest';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
type WorkflowEditActionFormUpdateRecordProps = { type WorkflowEditActionUpdateRecordProps = {
action: WorkflowUpdateRecordAction; action: WorkflowUpdateRecordAction;
actionOptions: actionOptions:
| { | {
@ -55,10 +55,10 @@ const AVAILABLE_FIELD_METADATA_TYPES = [
FieldMetadataType.UUID, FieldMetadataType.UUID,
]; ];
export const WorkflowEditActionFormUpdateRecord = ({ export const WorkflowEditActionUpdateRecord = ({
action, action,
actionOptions, actionOptions,
}: WorkflowEditActionFormUpdateRecordProps) => { }: WorkflowEditActionUpdateRecordProps) => {
const theme = useTheme(); const theme = useTheme();
const { getIcon } = useIcons(); const { getIcon } = useIcons();

View File

@ -5,7 +5,7 @@ import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/Workflo
import { INDEX_FILE_PATH } from '@/serverless-functions/constants/IndexFilePath'; import { INDEX_FILE_PATH } from '@/serverless-functions/constants/IndexFilePath';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowEditActionFormServerlessFunctionFields } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormServerlessFunctionFields'; import { WorkflowEditActionServerlessFunctionFields } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionServerlessFunctionFields';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon'; import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/utils/getWrongExportedFunctionMarkers'; import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/utils/getWrongExportedFunctionMarkers';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
@ -27,13 +27,13 @@ const StyledCodeEditorContainer = styled.div`
flex-direction: column; flex-direction: column;
`; `;
type WorkflowReadonlyActionFormServerlessFunctionProps = { type WorkflowReadonlyActionServerlessFunctionProps = {
action: WorkflowCodeAction; action: WorkflowCodeAction;
}; };
export const WorkflowReadonlyActionFormServerlessFunction = ({ export const WorkflowReadonlyActionServerlessFunction = ({
action, action,
}: WorkflowReadonlyActionFormServerlessFunctionProps) => { }: WorkflowReadonlyActionServerlessFunctionProps) => {
const theme = useTheme(); const theme = useTheme();
const { getIcon } = useIcons(); const { getIcon } = useIcons();
const serverlessFunctionId = action.settings.input.serverlessFunctionId; const serverlessFunctionId = action.settings.input.serverlessFunctionId;
@ -81,7 +81,7 @@ export const WorkflowReadonlyActionFormServerlessFunction = ({
disabled disabled
/> />
<WorkflowStepBody> <WorkflowStepBody>
<WorkflowEditActionFormServerlessFunctionFields <WorkflowEditActionServerlessFunctionFields
functionInput={action.settings.input.serverlessFunctionInput} functionInput={action.settings.input.serverlessFunctionInput}
readonly readonly
/> />

View File

@ -10,11 +10,11 @@ import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorato
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator'; import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow'; import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow';
import { WorkflowEditActionFormCreateRecord } from '../WorkflowEditActionFormCreateRecord'; import { WorkflowEditActionCreateRecord } from '../WorkflowEditActionCreateRecord';
const meta: Meta<typeof WorkflowEditActionFormCreateRecord> = { const meta: Meta<typeof WorkflowEditActionCreateRecord> = {
title: 'Modules/Workflow/WorkflowEditActionFormCreateRecord', title: 'Modules/Workflow/WorkflowEditActionCreateRecord',
component: WorkflowEditActionFormCreateRecord, component: WorkflowEditActionCreateRecord,
parameters: { parameters: {
msw: graphqlMocks, msw: graphqlMocks,
}, },
@ -54,7 +54,7 @@ const meta: Meta<typeof WorkflowEditActionFormCreateRecord> = {
export default meta; export default meta;
type Story = StoryObj<typeof WorkflowEditActionFormCreateRecord>; type Story = StoryObj<typeof WorkflowEditActionCreateRecord>;
export const Default: Story = { export const Default: Story = {
args: { args: {

View File

@ -11,7 +11,7 @@ import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { allMockPersonRecords } from '~/testing/mock-data/people'; import { allMockPersonRecords } from '~/testing/mock-data/people';
import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow'; import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow';
import { WorkflowEditActionFormDeleteRecord } from '../WorkflowEditActionFormDeleteRecord'; import { WorkflowEditActionDeleteRecord } from '../WorkflowEditActionDeleteRecord';
const DEFAULT_ACTION = { const DEFAULT_ACTION = {
id: getWorkflowNodeIdMock(), id: getWorkflowNodeIdMock(),
@ -35,9 +35,9 @@ const DEFAULT_ACTION = {
}, },
} satisfies WorkflowDeleteRecordAction; } satisfies WorkflowDeleteRecordAction;
const meta: Meta<typeof WorkflowEditActionFormDeleteRecord> = { const meta: Meta<typeof WorkflowEditActionDeleteRecord> = {
title: 'Modules/Workflow/WorkflowEditActionFormDeleteRecord', title: 'Modules/Workflow/WorkflowEditActionDeleteRecord',
component: WorkflowEditActionFormDeleteRecord, component: WorkflowEditActionDeleteRecord,
parameters: { parameters: {
msw: graphqlMocks, msw: graphqlMocks,
}, },
@ -58,7 +58,7 @@ const meta: Meta<typeof WorkflowEditActionFormDeleteRecord> = {
export default meta; export default meta;
type Story = StoryObj<typeof WorkflowEditActionFormDeleteRecord>; type Story = StoryObj<typeof WorkflowEditActionDeleteRecord>;
export const Default: Story = { export const Default: Story = {
args: { args: {

View File

@ -1,5 +1,5 @@
import { WorkflowFindRecordsAction } from '@/workflow/types/Workflow'; import { WorkflowFindRecordsAction } from '@/workflow/types/Workflow';
import { WorkflowEditActionFormFindRecords } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormFindRecords'; import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFindRecords';
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { expect, fn, userEvent, within } from '@storybook/test'; import { expect, fn, userEvent, within } from '@storybook/test';
import { ComponentDecorator, RouterDecorator } from 'twenty-ui'; import { ComponentDecorator, RouterDecorator } from 'twenty-ui';
@ -33,9 +33,9 @@ const DEFAULT_ACTION = {
}, },
} satisfies WorkflowFindRecordsAction; } satisfies WorkflowFindRecordsAction;
const meta: Meta<typeof WorkflowEditActionFormFindRecords> = { const meta: Meta<typeof WorkflowEditActionFindRecords> = {
title: 'Modules/Workflow/WorkflowEditActionFormFindRecords', title: 'Modules/Workflow/WorkflowEditActionFindRecords',
component: WorkflowEditActionFormFindRecords, component: WorkflowEditActionFindRecords,
parameters: { parameters: {
msw: graphqlMocks, msw: graphqlMocks,
}, },
@ -55,7 +55,7 @@ const meta: Meta<typeof WorkflowEditActionFormFindRecords> = {
export default meta; export default meta;
type Story = StoryObj<typeof WorkflowEditActionFormFindRecords>; type Story = StoryObj<typeof WorkflowEditActionFindRecords>;
export const Default: Story = { export const Default: Story = {
args: { args: {

View File

@ -0,0 +1,99 @@
import { WorkflowFormAction } from '@/workflow/types/Workflow';
import { WorkflowEditActionForm } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionForm';
import { Meta, StoryObj } from '@storybook/react';
import { expect, fn, within } from '@storybook/test';
import { FieldMetadataType } from 'twenty-shared';
import { ComponentDecorator, RouterDecorator } from 'twenty-ui';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow';
const DEFAULT_ACTION = {
id: getWorkflowNodeIdMock(),
name: 'Form',
type: 'FORM',
valid: false,
settings: {
input: [
{
name: 'company',
type: FieldMetadataType.TEXT,
label: 'Company',
placeholder: 'Select a company',
settings: {},
},
{
name: 'number',
type: FieldMetadataType.NUMBER,
label: 'Number',
placeholder: '1000',
settings: {},
},
],
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
} satisfies WorkflowFormAction;
const meta: Meta<typeof WorkflowEditActionForm> = {
title: 'Modules/Workflow/WorkflowEditActionForm',
component: WorkflowEditActionForm,
parameters: {
msw: graphqlMocks,
},
args: {
action: DEFAULT_ACTION,
},
decorators: [
WorkflowStepActionDrawerDecorator,
ComponentDecorator,
RouterDecorator,
I18nFrontDecorator,
],
};
export default meta;
type Story = StoryObj<typeof WorkflowEditActionForm>;
export const Default: Story = {
args: {
actionOptions: {
onActionUpdate: fn(),
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('Company');
await canvas.findByText('Add Field');
},
};
export const DisabledWithEmptyValues: Story = {
args: {
actionOptions: {
readonly: true,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const titleInput = await canvas.findByDisplayValue('Form');
expect(titleInput).toBeDisabled();
await canvas.findByText('Company');
const addFieldButton = canvas.queryByText('Add Field');
expect(addFieldButton).not.toBeInTheDocument();
},
};

View File

@ -11,7 +11,7 @@ import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { allMockPersonRecords } from '~/testing/mock-data/people'; import { allMockPersonRecords } from '~/testing/mock-data/people';
import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow'; import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow';
import { WorkflowEditActionFormUpdateRecord } from '../WorkflowEditActionFormUpdateRecord'; import { WorkflowEditActionUpdateRecord } from '../WorkflowEditActionUpdateRecord';
const DEFAULT_ACTION = { const DEFAULT_ACTION = {
id: getWorkflowNodeIdMock(), id: getWorkflowNodeIdMock(),
@ -48,9 +48,9 @@ const DEFAULT_ACTION = {
valid: false, valid: false,
} satisfies WorkflowUpdateRecordAction; } satisfies WorkflowUpdateRecordAction;
const meta: Meta<typeof WorkflowEditActionFormUpdateRecord> = { const meta: Meta<typeof WorkflowEditActionUpdateRecord> = {
title: 'Modules/Workflow/WorkflowEditActionFormUpdateRecord', title: 'Modules/Workflow/WorkflowEditActionUpdateRecord',
component: WorkflowEditActionFormUpdateRecord, component: WorkflowEditActionUpdateRecord,
parameters: { parameters: {
msw: graphqlMocks, msw: graphqlMocks,
}, },
@ -71,7 +71,7 @@ const meta: Meta<typeof WorkflowEditActionFormUpdateRecord> = {
export default meta; export default meta;
type Story = StoryObj<typeof WorkflowEditActionFormUpdateRecord>; type Story = StoryObj<typeof WorkflowEditActionUpdateRecord>;
export const Default: Story = { export const Default: Story = {
args: { args: {

View File

@ -15,4 +15,9 @@ export const OTHER_ACTIONS: Array<{
type: 'CODE', type: 'CODE',
icon: 'IconCode', icon: 'IconCode',
}, },
{
label: 'Form',
type: 'FORM',
icon: 'IconForms',
},
]; ];

View File

@ -20,9 +20,10 @@ export const RECORD_ACTIONS: Array<{
type: 'DELETE_RECORD', type: 'DELETE_RECORD',
icon: 'IconTrash', icon: 'IconTrash',
}, },
{ // TODO: Add search records action
label: 'Search Records', // {
type: 'FIND_RECORDS', // label: 'Search Records',
icon: 'IconSearch', // type: 'FIND_RECORDS',
}, // icon: 'IconSearch',
// },
]; ];

View File

@ -70,6 +70,11 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId, workspaceId: workspaceId,
value: false, value: false,
}, },
{
key: FeatureFlagKey.IsWorkflowFormActionEnabled,
workspaceId: workspaceId,
value: true,
},
]) ])
.execute(); .execute();
}; };

View File

@ -13,4 +13,5 @@ export enum FeatureFlagKey {
IsApprovedAccessDomainsEnabled = 'IS_APPROVED_ACCESS_DOMAINS_ENABLED', IsApprovedAccessDomainsEnabled = 'IS_APPROVED_ACCESS_DOMAINS_ENABLED',
IsNewRelationEnabled = 'IS_NEW_RELATION_ENABLED', IsNewRelationEnabled = 'IS_NEW_RELATION_ENABLED',
IsPermissionsEnabled = 'IS_PERMISSIONS_ENABLED', IsPermissionsEnabled = 'IS_PERMISSIONS_ENABLED',
IsWorkflowFormActionEnabled = 'IS_WORKFLOW_FORM_ACTION_ENABLED',
} }

View File

@ -1,7 +1,7 @@
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 { FieldMetadataType, isDefined } from 'twenty-shared';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
@ -495,6 +495,31 @@ export class WorkflowVersionStepWorkspaceService {
}, },
}; };
} }
case WorkflowActionType.FORM: {
return {
id: newStepId,
name: 'Form',
type: WorkflowActionType.FORM,
valid: false,
settings: {
...BASE_STEP_DEFINITION,
input: [
{
label: 'Company',
name: 'company',
placeholder: 'Select a company',
type: FieldMetadataType.TEXT,
},
{
label: 'Number',
name: 'number',
placeholder: '1000',
type: FieldMetadataType.NUMBER,
},
],
},
};
}
default: default:
throw new WorkflowVersionStepException( throw new WorkflowVersionStepException(
`WorkflowActionType '${type}' unknown`, `WorkflowActionType '${type}' unknown`,