Execute workflow form action (#11099)
- create a form filler component - send the response on submit - put back a field name. We need it for the step output - validate a form is well set before activation TODO: - we need to refresh to see the form submitted. We need to discuss about a strategy - the response is not saved in the step settings. We need a new endpoint to update workflow run step https://github.com/user-attachments/assets/0f34a6cd-ed8c-4d9a-a1d4-51455cc83443
This commit is contained in:
@ -318,6 +318,17 @@ export type CreateOneFieldMetadataInput = {
|
|||||||
field: CreateFieldInput;
|
field: CreateFieldInput;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CreateRoleInput = {
|
||||||
|
canDestroyAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
canReadAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
canSoftDeleteAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
canUpdateAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
canUpdateAllSettings?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
description?: InputMaybe<Scalars['String']>;
|
||||||
|
icon?: InputMaybe<Scalars['String']>;
|
||||||
|
label: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
export type CreateServerlessFunctionInput = {
|
export type CreateServerlessFunctionInput = {
|
||||||
description?: InputMaybe<Scalars['String']>;
|
description?: InputMaybe<Scalars['String']>;
|
||||||
name: Scalars['String'];
|
name: Scalars['String'];
|
||||||
@ -497,6 +508,7 @@ export enum FeatureFlagKey {
|
|||||||
IsJsonFilterEnabled = 'IsJsonFilterEnabled',
|
IsJsonFilterEnabled = 'IsJsonFilterEnabled',
|
||||||
IsNewRelationEnabled = 'IsNewRelationEnabled',
|
IsNewRelationEnabled = 'IsNewRelationEnabled',
|
||||||
IsPermissionsEnabled = 'IsPermissionsEnabled',
|
IsPermissionsEnabled = 'IsPermissionsEnabled',
|
||||||
|
IsPermissionsV2Enabled = 'IsPermissionsV2Enabled',
|
||||||
IsPostgreSQLIntegrationEnabled = 'IsPostgreSQLIntegrationEnabled',
|
IsPostgreSQLIntegrationEnabled = 'IsPostgreSQLIntegrationEnabled',
|
||||||
IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled',
|
IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled',
|
||||||
IsUniqueIndexesEnabled = 'IsUniqueIndexesEnabled',
|
IsUniqueIndexesEnabled = 'IsUniqueIndexesEnabled',
|
||||||
@ -810,6 +822,7 @@ export type Mutation = {
|
|||||||
createOneAppToken: AppToken;
|
createOneAppToken: AppToken;
|
||||||
createOneField: Field;
|
createOneField: Field;
|
||||||
createOneObject: Object;
|
createOneObject: Object;
|
||||||
|
createOneRole: Role;
|
||||||
createOneServerlessFunction: ServerlessFunction;
|
createOneServerlessFunction: ServerlessFunction;
|
||||||
createSAMLIdentityProvider: SetupSsoOutput;
|
createSAMLIdentityProvider: SetupSsoOutput;
|
||||||
createWorkflowVersionStep: WorkflowAction;
|
createWorkflowVersionStep: WorkflowAction;
|
||||||
@ -850,6 +863,7 @@ export type Mutation = {
|
|||||||
updateLabPublicFeatureFlag: FeatureFlag;
|
updateLabPublicFeatureFlag: FeatureFlag;
|
||||||
updateOneField: Field;
|
updateOneField: Field;
|
||||||
updateOneObject: Object;
|
updateOneObject: Object;
|
||||||
|
updateOneRole: Role;
|
||||||
updateOneServerlessFunction: ServerlessFunction;
|
updateOneServerlessFunction: ServerlessFunction;
|
||||||
updatePasswordViaResetToken: InvalidatePassword;
|
updatePasswordViaResetToken: InvalidatePassword;
|
||||||
updateWorkflowVersionStep: WorkflowAction;
|
updateWorkflowVersionStep: WorkflowAction;
|
||||||
@ -915,6 +929,11 @@ export type MutationCreateOneFieldArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationCreateOneRoleArgs = {
|
||||||
|
createRoleInput: CreateRoleInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationCreateOneServerlessFunctionArgs = {
|
export type MutationCreateOneServerlessFunctionArgs = {
|
||||||
input: CreateServerlessFunctionInput;
|
input: CreateServerlessFunctionInput;
|
||||||
};
|
};
|
||||||
@ -1088,6 +1107,11 @@ export type MutationUpdateOneObjectArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationUpdateOneRoleArgs = {
|
||||||
|
updateRoleInput: UpdateRoleInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationUpdateOneServerlessFunctionArgs = {
|
export type MutationUpdateOneServerlessFunctionArgs = {
|
||||||
input: UpdateServerlessFunctionInput;
|
input: UpdateServerlessFunctionInput;
|
||||||
};
|
};
|
||||||
@ -1904,6 +1928,23 @@ export type UpdateOneObjectInput = {
|
|||||||
update: UpdateObjectPayload;
|
update: UpdateObjectPayload;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateRoleInput = {
|
||||||
|
/** The id of the role to update */
|
||||||
|
id: Scalars['UUID'];
|
||||||
|
update: UpdateRolePayload;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateRolePayload = {
|
||||||
|
canDestroyAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
canReadAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
canSoftDeleteAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
canUpdateAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
canUpdateAllSettings?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
description?: InputMaybe<Scalars['String']>;
|
||||||
|
icon?: InputMaybe<Scalars['String']>;
|
||||||
|
label?: InputMaybe<Scalars['String']>;
|
||||||
|
};
|
||||||
|
|
||||||
export type UpdateServerlessFunctionInput = {
|
export type UpdateServerlessFunctionInput = {
|
||||||
code: Scalars['JSON'];
|
code: Scalars['JSON'];
|
||||||
description?: InputMaybe<Scalars['String']>;
|
description?: InputMaybe<Scalars['String']>;
|
||||||
@ -2628,6 +2669,13 @@ export type UpdateWorkflowVersionStepMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type UpdateWorkflowVersionStepMutation = { __typename?: 'Mutation', updateWorkflowVersionStep: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean } };
|
export type UpdateWorkflowVersionStepMutation = { __typename?: 'Mutation', updateWorkflowVersionStep: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean } };
|
||||||
|
|
||||||
|
export type SubmitFormStepMutationVariables = Exact<{
|
||||||
|
input: SubmitFormStepInput;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type SubmitFormStepMutation = { __typename?: 'Mutation', submitFormStep: boolean };
|
||||||
|
|
||||||
export type DeleteWorkspaceInvitationMutationVariables = Exact<{
|
export type DeleteWorkspaceInvitationMutationVariables = Exact<{
|
||||||
appTokenId: Scalars['String'];
|
appTokenId: Scalars['String'];
|
||||||
}>;
|
}>;
|
||||||
@ -5254,6 +5302,37 @@ export function useUpdateWorkflowVersionStepMutation(baseOptions?: Apollo.Mutati
|
|||||||
export type UpdateWorkflowVersionStepMutationHookResult = ReturnType<typeof useUpdateWorkflowVersionStepMutation>;
|
export type UpdateWorkflowVersionStepMutationHookResult = ReturnType<typeof useUpdateWorkflowVersionStepMutation>;
|
||||||
export type UpdateWorkflowVersionStepMutationResult = Apollo.MutationResult<UpdateWorkflowVersionStepMutation>;
|
export type UpdateWorkflowVersionStepMutationResult = Apollo.MutationResult<UpdateWorkflowVersionStepMutation>;
|
||||||
export type UpdateWorkflowVersionStepMutationOptions = Apollo.BaseMutationOptions<UpdateWorkflowVersionStepMutation, UpdateWorkflowVersionStepMutationVariables>;
|
export type UpdateWorkflowVersionStepMutationOptions = Apollo.BaseMutationOptions<UpdateWorkflowVersionStepMutation, UpdateWorkflowVersionStepMutationVariables>;
|
||||||
|
export const SubmitFormStepDocument = gql`
|
||||||
|
mutation SubmitFormStep($input: SubmitFormStepInput!) {
|
||||||
|
submitFormStep(input: $input)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type SubmitFormStepMutationFn = Apollo.MutationFunction<SubmitFormStepMutation, SubmitFormStepMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useSubmitFormStepMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useSubmitFormStepMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useSubmitFormStepMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [submitFormStepMutation, { data, loading, error }] = useSubmitFormStepMutation({
|
||||||
|
* variables: {
|
||||||
|
* input: // value for 'input'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useSubmitFormStepMutation(baseOptions?: Apollo.MutationHookOptions<SubmitFormStepMutation, SubmitFormStepMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<SubmitFormStepMutation, SubmitFormStepMutationVariables>(SubmitFormStepDocument, options);
|
||||||
|
}
|
||||||
|
export type SubmitFormStepMutationHookResult = ReturnType<typeof useSubmitFormStepMutation>;
|
||||||
|
export type SubmitFormStepMutationResult = Apollo.MutationResult<SubmitFormStepMutation>;
|
||||||
|
export type SubmitFormStepMutationOptions = Apollo.BaseMutationOptions<SubmitFormStepMutation, SubmitFormStepMutationVariables>;
|
||||||
export const DeleteWorkspaceInvitationDocument = gql`
|
export const DeleteWorkspaceInvitationDocument = gql`
|
||||||
mutation DeleteWorkspaceInvitation($appTokenId: String!) {
|
mutation DeleteWorkspaceInvitation($appTokenId: String!) {
|
||||||
deleteWorkspaceInvitation(appTokenId: $appTokenId)
|
deleteWorkspaceInvitation(appTokenId: $appTokenId)
|
||||||
|
|||||||
@ -25,6 +25,12 @@ const StyledTabList = styled(TabList)`
|
|||||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
type TabId = WorkflowRunTabIdType;
|
type TabId = WorkflowRunTabIdType;
|
||||||
|
|
||||||
export const CommandMenuWorkflowRunViewStep = () => {
|
export const CommandMenuWorkflowRunViewStep = () => {
|
||||||
@ -73,35 +79,43 @@ export const CommandMenuWorkflowRunViewStep = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkflowStepContextProvider
|
<WorkflowStepContextProvider
|
||||||
value={{ workflowVersionId: workflowRun.workflowVersionId }}
|
value={{
|
||||||
|
workflowVersionId: workflowRun.workflowVersionId,
|
||||||
|
workflowRunId: workflowRun.id,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<StyledTabList
|
<StyledContainer>
|
||||||
tabs={tabs}
|
<StyledTabList
|
||||||
behaveAsLinks={false}
|
tabs={tabs}
|
||||||
componentInstanceId={WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID}
|
behaveAsLinks={false}
|
||||||
/>
|
componentInstanceId={
|
||||||
|
WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID
|
||||||
{activeTabId === WorkflowRunTabId.NODE ? (
|
}
|
||||||
<WorkflowRunStepNodeDetail
|
|
||||||
stepId={workflowSelectedNode}
|
|
||||||
trigger={flow.trigger}
|
|
||||||
steps={flow.steps}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
|
||||||
|
|
||||||
{activeTabId === WorkflowRunTabId.INPUT ? (
|
{activeTabId === WorkflowRunTabId.NODE ? (
|
||||||
<WorkflowRunStepInputDetail
|
<WorkflowRunStepNodeDetail
|
||||||
key={workflowSelectedNode}
|
stepId={workflowSelectedNode}
|
||||||
stepId={workflowSelectedNode}
|
trigger={flow.trigger}
|
||||||
/>
|
steps={flow.steps}
|
||||||
) : null}
|
stepExecutionStatus={stepExecutionStatus}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{activeTabId === WorkflowRunTabId.OUTPUT ? (
|
{activeTabId === WorkflowRunTabId.INPUT ? (
|
||||||
<WorkflowRunStepOutputDetail
|
<WorkflowRunStepInputDetail
|
||||||
key={workflowSelectedNode}
|
key={workflowSelectedNode}
|
||||||
stepId={workflowSelectedNode}
|
stepId={workflowSelectedNode}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{activeTabId === WorkflowRunTabId.OUTPUT ? (
|
||||||
|
<WorkflowRunStepOutputDetail
|
||||||
|
key={workflowSelectedNode}
|
||||||
|
stepId={workflowSelectedNode}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</StyledContainer>
|
||||||
</WorkflowStepContextProvider>
|
</WorkflowStepContextProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -43,7 +43,7 @@ import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUu
|
|||||||
import { JsonValue } from 'type-fest';
|
import { JsonValue } from 'type-fest';
|
||||||
|
|
||||||
type FormFieldInputProps = {
|
type FormFieldInputProps = {
|
||||||
field: FieldDefinition<FieldMetadata>;
|
field: Pick<FieldDefinition<FieldMetadata>, 'label' | 'metadata' | 'type'>;
|
||||||
defaultValue: JsonValue;
|
defaultValue: JsonValue;
|
||||||
onChange: (value: JsonValue) => void;
|
onChange: (value: JsonValue) => void;
|
||||||
VariablePicker?: VariablePickerComponent;
|
VariablePicker?: VariablePickerComponent;
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { createRequiredContext } from '~/utils/createRequiredContext';
|
|||||||
|
|
||||||
type WorkflowStepContextType = {
|
type WorkflowStepContextType = {
|
||||||
workflowVersionId: string;
|
workflowVersionId: string;
|
||||||
|
workflowRunId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const [WorkflowStepContextProvider, useWorkflowStepContextOrThrow] =
|
export const [WorkflowStepContextProvider, useWorkflowStepContextOrThrow] =
|
||||||
|
|||||||
@ -86,7 +86,8 @@ export const workflowFormActionSettingsSchema =
|
|||||||
baseWorkflowActionSettingsSchema.extend({
|
baseWorkflowActionSettingsSchema.extend({
|
||||||
input: z.array(
|
input: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
label: z.string(),
|
label: z.string(),
|
||||||
type: z.union([
|
type: z.union([
|
||||||
z.literal(FieldMetadataType.TEXT),
|
z.literal(FieldMetadataType.TEXT),
|
||||||
@ -222,6 +223,7 @@ export const workflowTriggerSchema = z.discriminatedUnion('type', [
|
|||||||
const workflowExecutorOutputSchema = z.object({
|
const workflowExecutorOutputSchema = z.object({
|
||||||
result: z.any().optional(),
|
result: z.any().optional(),
|
||||||
error: z.string().optional(),
|
error: z.string().optional(),
|
||||||
|
pendingEvent: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const workflowRunOutputStepsOutputSchema = z.record(
|
export const workflowRunOutputStepsOutputSchema = z.record(
|
||||||
|
|||||||
@ -83,7 +83,7 @@ export const generateWorkflowRunDiagram = ({
|
|||||||
let runStatus: WorkflowDiagramRunStatus;
|
let runStatus: WorkflowDiagramRunStatus;
|
||||||
if (skippedExecution) {
|
if (skippedExecution) {
|
||||||
runStatus = 'not-executed';
|
runStatus = 'not-executed';
|
||||||
} else if (!isDefined(runResult)) {
|
} else if (!isDefined(runResult) || isDefined(runResult.pendingEvent)) {
|
||||||
runStatus = 'running';
|
runStatus = 'running';
|
||||||
} else {
|
} else {
|
||||||
if (isDefined(runResult.error)) {
|
if (isDefined(runResult.error)) {
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
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 { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
import { WorkflowActionServerlessFunction } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionServerlessFunction';
|
import { WorkflowActionServerlessFunction } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionServerlessFunction';
|
||||||
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
|
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
|
||||||
import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
|
import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
|
||||||
import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFindRecords';
|
import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFindRecords';
|
||||||
import { WorkflowEditActionSendEmail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail';
|
import { WorkflowEditActionSendEmail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail';
|
||||||
import { WorkflowEditActionUpdateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord';
|
import { WorkflowEditActionUpdateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord';
|
||||||
|
import { WorkflowEditActionFormFiller } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFiller';
|
||||||
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';
|
||||||
@ -17,12 +19,14 @@ type WorkflowRunStepNodeDetailProps = {
|
|||||||
stepId: string;
|
stepId: string;
|
||||||
trigger: WorkflowTrigger | null;
|
trigger: WorkflowTrigger | null;
|
||||||
steps: Array<WorkflowAction> | null;
|
steps: Array<WorkflowAction> | null;
|
||||||
|
stepExecutionStatus?: WorkflowDiagramRunStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorkflowRunStepNodeDetail = ({
|
export const WorkflowRunStepNodeDetail = ({
|
||||||
stepId,
|
stepId,
|
||||||
trigger,
|
trigger,
|
||||||
steps,
|
steps,
|
||||||
|
stepExecutionStatus,
|
||||||
}: WorkflowRunStepNodeDetailProps) => {
|
}: WorkflowRunStepNodeDetailProps) => {
|
||||||
const stepDefinition = getStepDefinitionOrThrow({
|
const stepDefinition = getStepDefinitionOrThrow({
|
||||||
stepId,
|
stepId,
|
||||||
@ -161,8 +165,17 @@ export const WorkflowRunStepNodeDetail = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'FORM': {
|
case 'FORM': {
|
||||||
// TODO: Implement form filler
|
return (
|
||||||
return null;
|
<WorkflowEditActionFormFiller
|
||||||
|
key={stepId}
|
||||||
|
action={stepDefinition.definition}
|
||||||
|
actionOptions={{
|
||||||
|
readonly: stepExecutionStatus !== 'running',
|
||||||
|
// TODO: Implement update worklfow run flow step
|
||||||
|
onActionUpdate: () => {},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,10 @@ export const getWorkflowRunStepExecutionStatus = ({
|
|||||||
return 'failure';
|
return 'failure';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isDefined(stepOutput?.pendingEvent)) {
|
||||||
|
return 'running';
|
||||||
|
}
|
||||||
|
|
||||||
if (isDefined(stepOutput?.result)) {
|
if (isDefined(stepOutput?.result)) {
|
||||||
return 'success';
|
return 'success';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { WorkflowFormAction } from '@/workflow/types/Workflow';
|
|||||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||||
import { WorkflowEditActionFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFieldSettings';
|
import { WorkflowEditActionFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFieldSettings';
|
||||||
|
import { WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
|
||||||
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
|
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
|
||||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
@ -13,13 +14,7 @@ import styled from '@emotion/styled';
|
|||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { FieldMetadataType, isDefined } from 'twenty-shared';
|
import { FieldMetadataType, isDefined } from 'twenty-shared';
|
||||||
import {
|
import { IconChevronDown, IconPlus, IconTrash, useIcons } from 'twenty-ui';
|
||||||
IconChevronDown,
|
|
||||||
IconChevronUp,
|
|
||||||
IconPlus,
|
|
||||||
IconTrash,
|
|
||||||
useIcons,
|
|
||||||
} from 'twenty-ui';
|
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
@ -36,14 +31,6 @@ export type WorkflowEditActionFormBuilderProps = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkflowFormActionField = {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
type: FieldMetadataType.TEXT | FieldMetadataType.NUMBER;
|
|
||||||
placeholder?: string;
|
|
||||||
settings?: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type FormData = WorkflowFormActionField[];
|
type FormData = WorkflowFormActionField[];
|
||||||
|
|
||||||
const StyledRowContainer = styled.div`
|
const StyledRowContainer = styled.div`
|
||||||
@ -193,12 +180,7 @@ export const WorkflowEditActionFormBuilder = ({
|
|||||||
>
|
>
|
||||||
<StyledFieldContainer>
|
<StyledFieldContainer>
|
||||||
<StyledPlaceholder>{field.placeholder}</StyledPlaceholder>
|
<StyledPlaceholder>{field.placeholder}</StyledPlaceholder>
|
||||||
{isFieldSelected(field.id) ? (
|
{!isFieldSelected(field.id) && (
|
||||||
<IconChevronUp
|
|
||||||
size={theme.icon.size.md}
|
|
||||||
color={theme.font.color.tertiary}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<IconChevronDown
|
<IconChevronDown
|
||||||
size={theme.icon.size.md}
|
size={theme.icon.size.md}
|
||||||
color={theme.font.color.tertiary}
|
color={theme.font.color.tertiary}
|
||||||
@ -207,16 +189,12 @@ export const WorkflowEditActionFormBuilder = ({
|
|||||||
</StyledFieldContainer>
|
</StyledFieldContainer>
|
||||||
</FormFieldInputInputContainer>
|
</FormFieldInputInputContainer>
|
||||||
</FormFieldInputRowContainer>
|
</FormFieldInputRowContainer>
|
||||||
{isFieldSelected(field.id) && (
|
{!actionOptions.readonly && isFieldSelected(field.id) && (
|
||||||
<StyledIconButtonContainer>
|
<StyledIconButtonContainer>
|
||||||
<IconTrash
|
<IconTrash
|
||||||
size={theme.icon.size.md}
|
size={theme.icon.size.md}
|
||||||
color={theme.font.color.secondary}
|
color={theme.font.color.secondary}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (actionOptions.readonly === true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedFormData = formData.filter(
|
const updatedFormData = formData.filter(
|
||||||
(currentField) => currentField.id !== field.id,
|
(currentField) => currentField.id !== field.id,
|
||||||
);
|
);
|
||||||
@ -253,12 +231,12 @@ export const WorkflowEditActionFormBuilder = ({
|
|||||||
<FormFieldInputInputContainer
|
<FormFieldInputInputContainer
|
||||||
hasRightElement={false}
|
hasRightElement={false}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const { label, placeholder } = getDefaultFormFieldSettings(
|
const { label, placeholder, name } =
|
||||||
FieldMetadataType.TEXT,
|
getDefaultFormFieldSettings(FieldMetadataType.TEXT);
|
||||||
);
|
|
||||||
|
|
||||||
const newField: WorkflowFormActionField = {
|
const newField: WorkflowFormActionField = {
|
||||||
id: v4(),
|
id: v4(),
|
||||||
|
name,
|
||||||
type: FieldMetadataType.TEXT,
|
type: FieldMetadataType.TEXT,
|
||||||
label,
|
label,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
|
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
|
||||||
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
|
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
|
||||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||||
import { WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder';
|
|
||||||
import { WorkflowFormFieldSettingsByType } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormFieldSettingsByType';
|
import { WorkflowFormFieldSettingsByType } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormFieldSettingsByType';
|
||||||
|
import { WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
|
||||||
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
|
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
|
import camelCase from 'lodash.camelcase';
|
||||||
import { FieldMetadataType } from 'twenty-shared';
|
import { FieldMetadataType } from 'twenty-shared';
|
||||||
import {
|
import {
|
||||||
IconSettingsAutomation,
|
IconSettingsAutomation,
|
||||||
@ -66,10 +67,18 @@ export const WorkflowEditActionFormFieldSettings = ({
|
|||||||
}: WorkflowEditActionFormFieldSettingsProps) => {
|
}: WorkflowEditActionFormFieldSettingsProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const onSubFieldUpdate = (fieldName: string, value: any) => {
|
const onSubFieldUpdate = (fieldName: string, value: any) => {
|
||||||
onChange({
|
if (fieldName === 'label') {
|
||||||
...field,
|
onChange({
|
||||||
[fieldName]: value,
|
...field,
|
||||||
});
|
name: camelCase(value),
|
||||||
|
label: value,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onChange({
|
||||||
|
...field,
|
||||||
|
[fieldName]: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -0,0 +1,161 @@
|
|||||||
|
import { CmdEnterActionButton } from '@/action-menu/components/CmdEnterActionButton';
|
||||||
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||||
|
import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput';
|
||||||
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
|
||||||
|
import { useWorkflowStepContextOrThrow } from '@/workflow/states/context/WorkflowStepContext';
|
||||||
|
import { WorkflowFormAction } from '@/workflow/types/Workflow';
|
||||||
|
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||||
|
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||||
|
import { useSubmitFormStep } from '@/workflow/workflow-steps/workflow-actions/form-action/hooks/useSubmitFormStep';
|
||||||
|
import { WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
|
||||||
|
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
import { useIcons } from 'twenty-ui';
|
||||||
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
|
export type WorkflowEditActionFormFillerProps = {
|
||||||
|
action: WorkflowFormAction;
|
||||||
|
actionOptions:
|
||||||
|
| {
|
||||||
|
readonly: true;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
readonly?: false;
|
||||||
|
onActionUpdate: (action: WorkflowFormAction) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormData = WorkflowFormActionField[];
|
||||||
|
|
||||||
|
export const WorkflowEditActionFormFiller = ({
|
||||||
|
action,
|
||||||
|
actionOptions,
|
||||||
|
}: WorkflowEditActionFormFillerProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { getIcon } = useIcons();
|
||||||
|
const { submitFormStep } = useSubmitFormStep();
|
||||||
|
const [formData, setFormData] = useState<FormData>(action.settings.input);
|
||||||
|
const { workflowRunId } = useWorkflowStepContextOrThrow();
|
||||||
|
const { closeCommandMenu } = useCommandMenu();
|
||||||
|
|
||||||
|
if (!isDefined(workflowRunId)) {
|
||||||
|
throw new Error('Form filler action must be used in a workflow run');
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerTitle = isDefined(action.name) ? action.name : `Form`;
|
||||||
|
const headerIcon = getActionIcon(action.type);
|
||||||
|
|
||||||
|
const onFieldUpdate = ({
|
||||||
|
fieldId,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
fieldId: string;
|
||||||
|
value: any;
|
||||||
|
}) => {
|
||||||
|
if (actionOptions.readonly === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedFormData = formData.map((field) =>
|
||||||
|
field.id === fieldId ? { ...field, value } : field,
|
||||||
|
);
|
||||||
|
|
||||||
|
setFormData(updatedFormData);
|
||||||
|
|
||||||
|
saveAction(updatedFormData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveAction = useDebouncedCallback(async (updatedFormData: FormData) => {
|
||||||
|
if (actionOptions.readonly === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actionOptions.onActionUpdate({
|
||||||
|
...action,
|
||||||
|
settings: {
|
||||||
|
...action.settings,
|
||||||
|
input: updatedFormData,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, 1_000);
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
const response = formData.reduce(
|
||||||
|
(acc, field) => {
|
||||||
|
acc[field.name] = field.value;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, any>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await submitFormStep({
|
||||||
|
stepId: action.id,
|
||||||
|
workflowRunId,
|
||||||
|
response,
|
||||||
|
});
|
||||||
|
|
||||||
|
closeCommandMenu();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
saveAction.flush();
|
||||||
|
};
|
||||||
|
}, [saveAction]);
|
||||||
|
|
||||||
|
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
|
||||||
|
/>
|
||||||
|
<WorkflowStepBody>
|
||||||
|
{formData.map((field) => (
|
||||||
|
<FormFieldInput
|
||||||
|
key={field.id}
|
||||||
|
field={{
|
||||||
|
label: field.label,
|
||||||
|
type: field.type,
|
||||||
|
metadata: {} as FieldMetadata,
|
||||||
|
}}
|
||||||
|
onChange={(value) => {
|
||||||
|
onFieldUpdate({
|
||||||
|
fieldId: field.id,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
defaultValue={field.value ?? ''}
|
||||||
|
readonly={actionOptions.readonly}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</WorkflowStepBody>
|
||||||
|
{!actionOptions.readonly && (
|
||||||
|
<RightDrawerFooter
|
||||||
|
actions={[
|
||||||
|
<CmdEnterActionButton
|
||||||
|
title="Submit"
|
||||||
|
onClick={onSubmit}
|
||||||
|
disabled={actionOptions.readonly}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder';
|
import { WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
|
||||||
import { assertUnreachable, FieldMetadataType } from 'twenty-shared';
|
import { assertUnreachable, FieldMetadataType } from 'twenty-shared';
|
||||||
import { WorkflowFormFieldSettingsNumber } from './WorkflowFormFieldSettingsNumber';
|
import { WorkflowFormFieldSettingsNumber } from './WorkflowFormFieldSettingsNumber';
|
||||||
import { WorkflowFormFieldSettingsText } from './WorkflowFormFieldSettingsText';
|
import { WorkflowFormFieldSettingsText } from './WorkflowFormFieldSettingsText';
|
||||||
|
|||||||
@ -18,6 +18,7 @@ const DEFAULT_ACTION = {
|
|||||||
input: [
|
input: [
|
||||||
{
|
{
|
||||||
id: 'ed00b897-519f-44cd-8201-a6502a3a9dc8',
|
id: 'ed00b897-519f-44cd-8201-a6502a3a9dc8',
|
||||||
|
name: 'company',
|
||||||
type: FieldMetadataType.TEXT,
|
type: FieldMetadataType.TEXT,
|
||||||
label: 'Company',
|
label: 'Company',
|
||||||
placeholder: 'Select a company',
|
placeholder: 'Select a company',
|
||||||
@ -25,6 +26,7 @@ const DEFAULT_ACTION = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ed00b897-519f-44cd-8201-a6502a3a9dc9',
|
id: 'ed00b897-519f-44cd-8201-a6502a3a9dc9',
|
||||||
|
name: 'number',
|
||||||
type: FieldMetadataType.NUMBER,
|
type: FieldMetadataType.NUMBER,
|
||||||
label: 'Number',
|
label: 'Number',
|
||||||
placeholder: '1000',
|
placeholder: '1000',
|
||||||
|
|||||||
@ -29,6 +29,7 @@ const mockAction: WorkflowFormAction = {
|
|||||||
input: [
|
input: [
|
||||||
{
|
{
|
||||||
id: 'field-1',
|
id: 'field-1',
|
||||||
|
name: 'text',
|
||||||
label: 'Text Field',
|
label: 'Text Field',
|
||||||
type: FieldMetadataType.TEXT,
|
type: FieldMetadataType.TEXT,
|
||||||
placeholder: 'Enter text',
|
placeholder: 'Enter text',
|
||||||
@ -67,6 +68,7 @@ export const NumberFieldSettings: Story = {
|
|||||||
args: {
|
args: {
|
||||||
field: {
|
field: {
|
||||||
id: 'field-2',
|
id: 'field-2',
|
||||||
|
name: 'number',
|
||||||
label: 'Number Field',
|
label: 'Number Field',
|
||||||
type: FieldMetadataType.NUMBER,
|
type: FieldMetadataType.NUMBER,
|
||||||
placeholder: 'Enter number',
|
placeholder: 'Enter number',
|
||||||
|
|||||||
@ -0,0 +1,103 @@
|
|||||||
|
import { WorkflowFormAction } from '@/workflow/types/Workflow';
|
||||||
|
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 { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||||
|
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
||||||
|
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||||
|
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
|
||||||
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
|
import { WorkflowEditActionFormFiller } from '../WorkflowEditActionFormFiller';
|
||||||
|
|
||||||
|
const meta: Meta<typeof WorkflowEditActionFormFiller> = {
|
||||||
|
title: 'Modules/Workflow/Actions/Form/WorkflowEditActionFormFiller',
|
||||||
|
component: WorkflowEditActionFormFiller,
|
||||||
|
parameters: {
|
||||||
|
msw: graphqlMocks,
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
WorkflowStepActionDrawerDecorator,
|
||||||
|
ComponentDecorator,
|
||||||
|
I18nFrontDecorator,
|
||||||
|
WorkflowStepDecorator,
|
||||||
|
RouterDecorator,
|
||||||
|
ObjectMetadataItemsDecorator,
|
||||||
|
WorkspaceDecorator,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof WorkflowEditActionFormFiller>;
|
||||||
|
|
||||||
|
const mockAction: WorkflowFormAction = {
|
||||||
|
id: 'form-action-1',
|
||||||
|
type: 'FORM',
|
||||||
|
name: 'Test Form',
|
||||||
|
valid: true,
|
||||||
|
settings: {
|
||||||
|
input: [
|
||||||
|
{
|
||||||
|
id: 'field-1',
|
||||||
|
name: 'text',
|
||||||
|
label: 'Text Field',
|
||||||
|
type: FieldMetadataType.TEXT,
|
||||||
|
placeholder: 'Enter text',
|
||||||
|
settings: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'field-2',
|
||||||
|
name: 'number',
|
||||||
|
label: 'Number Field',
|
||||||
|
type: FieldMetadataType.NUMBER,
|
||||||
|
placeholder: 'Enter number',
|
||||||
|
settings: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputSchema: {},
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: false },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
action: mockAction,
|
||||||
|
actionOptions: {
|
||||||
|
onActionUpdate: fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const textField = await canvas.findByText('Text Field');
|
||||||
|
expect(textField).toBeVisible();
|
||||||
|
|
||||||
|
const numberField = await canvas.findByText('Number Field');
|
||||||
|
expect(numberField).toBeVisible();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReadonlyMode: Story = {
|
||||||
|
args: {
|
||||||
|
action: mockAction,
|
||||||
|
actionOptions: {
|
||||||
|
readonly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const textField = await canvas.findByText('Text Field');
|
||||||
|
expect(textField).toBeVisible();
|
||||||
|
|
||||||
|
const numberInput = await canvas.findByPlaceholderText('Number Field');
|
||||||
|
expect(numberInput).toBeDisabled();
|
||||||
|
|
||||||
|
const submitButton = await canvas.queryByText('Submit');
|
||||||
|
expect(submitButton).not.toBeInTheDocument();
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const SUBMIT_FORM_STEP = gql`
|
||||||
|
mutation SubmitFormStep($input: SubmitFormStepInput!) {
|
||||||
|
submitFormStep(input: $input)
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { useFindOneRecordQuery } from '@/object-record/hooks/useFindOneRecordQuery';
|
||||||
|
import { SUBMIT_FORM_STEP } from '@/workflow/workflow-steps/workflow-actions/form-action/graphql/mutations/submitFormStep';
|
||||||
|
import { useApolloClient, useMutation } from '@apollo/client';
|
||||||
|
import {
|
||||||
|
SubmitFormStepInput,
|
||||||
|
SubmitFormStepMutation,
|
||||||
|
SubmitFormStepMutationVariables,
|
||||||
|
} from '~/generated/graphql';
|
||||||
|
|
||||||
|
export const useSubmitFormStep = () => {
|
||||||
|
const apolloClient = useApolloClient();
|
||||||
|
const [mutate] = useMutation<
|
||||||
|
SubmitFormStepMutation,
|
||||||
|
SubmitFormStepMutationVariables
|
||||||
|
>(SUBMIT_FORM_STEP, {
|
||||||
|
client: apolloClient,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { findOneRecordQuery: findOneWorkflowRunQuery } = useFindOneRecordQuery(
|
||||||
|
{
|
||||||
|
objectNameSingular: CoreObjectNameSingular.WorkflowRun,
|
||||||
|
recordGqlFields: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
status: true,
|
||||||
|
output: true,
|
||||||
|
context: true,
|
||||||
|
startedAt: true,
|
||||||
|
endedAt: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const submitFormStep = async (input: SubmitFormStepInput) => {
|
||||||
|
const result = await mutate({
|
||||||
|
variables: { input },
|
||||||
|
awaitRefetchQueries: true,
|
||||||
|
refetchQueries: [
|
||||||
|
{
|
||||||
|
query: findOneWorkflowRunQuery,
|
||||||
|
variables: {
|
||||||
|
objectRecordId: input.workflowRunId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const isSuccess = result?.data?.submitFormStep;
|
||||||
|
|
||||||
|
return isSuccess;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { submitFormStep };
|
||||||
|
};
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { FieldMetadataType } from 'twenty-shared';
|
||||||
|
import { JsonValue } from 'type-fest';
|
||||||
|
|
||||||
|
export type WorkflowFormActionField = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
type: FieldMetadataType.TEXT | FieldMetadataType.NUMBER;
|
||||||
|
placeholder?: string;
|
||||||
|
settings?: Record<string, unknown>;
|
||||||
|
value?: JsonValue;
|
||||||
|
};
|
||||||
@ -1,19 +1,26 @@
|
|||||||
import { FieldMetadataType } from 'twenty-shared';
|
import { FieldMetadataType } from 'twenty-shared';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
export const getDefaultFormFieldSettings = (type: FieldMetadataType) => {
|
export const getDefaultFormFieldSettings = (type: FieldMetadataType) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case FieldMetadataType.TEXT:
|
case FieldMetadataType.TEXT:
|
||||||
return {
|
return {
|
||||||
|
id: v4(),
|
||||||
|
name: 'text',
|
||||||
label: 'Text',
|
label: 'Text',
|
||||||
placeholder: 'Enter your text',
|
placeholder: 'Enter your text',
|
||||||
};
|
};
|
||||||
case FieldMetadataType.NUMBER:
|
case FieldMetadataType.NUMBER:
|
||||||
return {
|
return {
|
||||||
|
id: v4(),
|
||||||
|
name: 'number',
|
||||||
label: 'Number',
|
label: 'Number',
|
||||||
placeholder: '1000',
|
placeholder: '1000',
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
|
id: v4(),
|
||||||
|
name: '',
|
||||||
label: type.charAt(0).toUpperCase() + type.slice(1),
|
label: type.charAt(0).toUpperCase() + type.slice(1),
|
||||||
placeholder: 'Enter your value',
|
placeholder: 'Enter your value',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -41,7 +41,10 @@ export const WorkflowStepDecorator: Decorator = (Story) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkflowStepContextProvider
|
<WorkflowStepContextProvider
|
||||||
value={{ workflowVersionId: workflowVersion.id }}
|
value={{
|
||||||
|
workflowVersionId: workflowVersion.id,
|
||||||
|
workflowRunId: '123',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{ready && <Story />}
|
{ready && <Story />}
|
||||||
</WorkflowStepContextProvider>
|
</WorkflowStepContextProvider>
|
||||||
|
|||||||
@ -7,16 +7,19 @@ describe('generateFakeFormResponse', () => {
|
|||||||
const schema = [
|
const schema = [
|
||||||
{
|
{
|
||||||
id: '96939213-49ac-4dee-949d-56e6c7be98e6',
|
id: '96939213-49ac-4dee-949d-56e6c7be98e6',
|
||||||
|
name: 'name',
|
||||||
type: FieldMetadataType.TEXT,
|
type: FieldMetadataType.TEXT,
|
||||||
label: 'Name',
|
label: 'Name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '96939213-49ac-4dee-949d-56e6c7be98e7',
|
id: '96939213-49ac-4dee-949d-56e6c7be98e7',
|
||||||
|
name: 'age',
|
||||||
type: FieldMetadataType.NUMBER,
|
type: FieldMetadataType.NUMBER,
|
||||||
label: 'Age',
|
label: 'Age',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '96939213-49ac-4dee-949d-56e6c7be98e8',
|
id: '96939213-49ac-4dee-949d-56e6c7be98e8',
|
||||||
|
name: 'email',
|
||||||
type: FieldMetadataType.EMAILS,
|
type: FieldMetadataType.EMAILS,
|
||||||
label: 'Email',
|
label: 'Email',
|
||||||
},
|
},
|
||||||
@ -25,7 +28,7 @@ describe('generateFakeFormResponse', () => {
|
|||||||
const result = generateFakeFormResponse(schema);
|
const result = generateFakeFormResponse(schema);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
'96939213-49ac-4dee-949d-56e6c7be98e8': {
|
email: {
|
||||||
isLeaf: false,
|
isLeaf: false,
|
||||||
label: 'Email',
|
label: 'Email',
|
||||||
value: {
|
value: {
|
||||||
@ -44,14 +47,14 @@ describe('generateFakeFormResponse', () => {
|
|||||||
},
|
},
|
||||||
icon: undefined,
|
icon: undefined,
|
||||||
},
|
},
|
||||||
'96939213-49ac-4dee-949d-56e6c7be98e6': {
|
name: {
|
||||||
isLeaf: true,
|
isLeaf: true,
|
||||||
label: 'Name',
|
label: 'Name',
|
||||||
type: FieldMetadataType.TEXT,
|
type: FieldMetadataType.TEXT,
|
||||||
value: 'My text',
|
value: 'My text',
|
||||||
icon: undefined,
|
icon: undefined,
|
||||||
},
|
},
|
||||||
'96939213-49ac-4dee-949d-56e6c7be98e7': {
|
age: {
|
||||||
isLeaf: true,
|
isLeaf: true,
|
||||||
label: 'Age',
|
label: 'Age',
|
||||||
type: FieldMetadataType.NUMBER,
|
type: FieldMetadataType.NUMBER,
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export const generateFakeFormResponse = (
|
|||||||
formMetadata: FormFieldMetadata[],
|
formMetadata: FormFieldMetadata[],
|
||||||
): Record<string, Leaf | Node> => {
|
): Record<string, Leaf | Node> => {
|
||||||
return formMetadata.reduce((acc, formFieldMetadata) => {
|
return formMetadata.reduce((acc, formFieldMetadata) => {
|
||||||
acc[formFieldMetadata.id] = generateFakeField({
|
acc[formFieldMetadata.name] = generateFakeField({
|
||||||
type: formFieldMetadata.type,
|
type: formFieldMetadata.type,
|
||||||
label: formFieldMetadata.label,
|
label: formFieldMetadata.label,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -506,12 +506,14 @@ export class WorkflowVersionStepWorkspaceService {
|
|||||||
input: [
|
input: [
|
||||||
{
|
{
|
||||||
id: v4(),
|
id: v4(),
|
||||||
|
name: 'company',
|
||||||
label: 'Company',
|
label: 'Company',
|
||||||
placeholder: 'Select a company',
|
placeholder: 'Select a company',
|
||||||
type: FieldMetadataType.TEXT,
|
type: FieldMetadataType.TEXT,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: v4(),
|
id: v4(),
|
||||||
|
name: 'number',
|
||||||
label: 'Number',
|
label: 'Number',
|
||||||
placeholder: '1000',
|
placeholder: '1000',
|
||||||
type: FieldMetadataType.NUMBER,
|
type: FieldMetadataType.NUMBER,
|
||||||
|
|||||||
@ -4,8 +4,10 @@ import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-execut
|
|||||||
|
|
||||||
export type FormFieldMetadata = {
|
export type FormFieldMetadata = {
|
||||||
id: string;
|
id: string;
|
||||||
|
name: string;
|
||||||
label: string;
|
label: string;
|
||||||
type: FieldMetadataType;
|
type: FieldMetadataType;
|
||||||
|
value?: any;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
settings?: Record<string, any>;
|
settings?: Record<string, any>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,8 +1,15 @@
|
|||||||
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
WorkflowVersionStatus,
|
WorkflowVersionStatus,
|
||||||
WorkflowVersionWorkspaceEntity,
|
WorkflowVersionWorkspaceEntity,
|
||||||
} from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
} from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||||
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
||||||
|
import { WorkflowFormActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type';
|
||||||
|
import {
|
||||||
|
WorkflowAction,
|
||||||
|
WorkflowActionType,
|
||||||
|
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||||
import {
|
import {
|
||||||
WorkflowTriggerException,
|
WorkflowTriggerException,
|
||||||
WorkflowTriggerExceptionCode,
|
WorkflowTriggerExceptionCode,
|
||||||
@ -58,6 +65,10 @@ function assertVersionIsValid(workflowVersion: WorkflowVersionWorkspaceEntity) {
|
|||||||
workflowVersion.trigger.type,
|
workflowVersion.trigger.type,
|
||||||
workflowVersion.trigger.settings,
|
workflowVersion.trigger.settings,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
workflowVersion.steps.forEach((step) => {
|
||||||
|
assertStepIsValid(step);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertTriggerSettingsAreValid(
|
function assertTriggerSettingsAreValid(
|
||||||
@ -188,3 +199,46 @@ function assertDatabaseEventTriggerSettingsAreValid(settings: any) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertStepIsValid(step: WorkflowAction) {
|
||||||
|
switch (step.type) {
|
||||||
|
case WorkflowActionType.FORM:
|
||||||
|
assertFormStepIsValid(step.settings);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertFormStepIsValid(settings: WorkflowFormActionSettings) {
|
||||||
|
if (!settings.input) {
|
||||||
|
throw new WorkflowTriggerException(
|
||||||
|
'No input provided in form step',
|
||||||
|
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check all fields have unique and defined names
|
||||||
|
const fieldNames = settings.input.map((fieldMetadata) => fieldMetadata.name);
|
||||||
|
const uniqueFieldNames = new Set(fieldNames);
|
||||||
|
|
||||||
|
if (fieldNames.length !== uniqueFieldNames.size) {
|
||||||
|
throw new WorkflowTriggerException(
|
||||||
|
'Form action fields must have unique names',
|
||||||
|
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check all fields have defined labels and types
|
||||||
|
settings.input.forEach((fieldMetadata) => {
|
||||||
|
if (
|
||||||
|
!isNonEmptyString(fieldMetadata.label) ||
|
||||||
|
!isNonEmptyString(fieldMetadata.type)
|
||||||
|
) {
|
||||||
|
throw new WorkflowTriggerException(
|
||||||
|
'Form action fields must have a defined label and type',
|
||||||
|
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user