Set error on number field and prevent form from being submitted (#11637)
<img width="943" alt="Capture d’écran 2025-04-17 à 18 14 46" src="https://github.com/user-attachments/assets/1208075e-937f-4224-886c-be578264d448" />
This commit is contained in:
@ -23,7 +23,7 @@ export const CmdEnterActionButton = ({
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
title={title}
|
title={title}
|
||||||
variant="primary"
|
variant={disabled ? 'secondary' : 'primary'}
|
||||||
accent="blue"
|
accent="blue"
|
||||||
size="medium"
|
size="medium"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
|||||||
@ -52,6 +52,8 @@ type FormFieldInputProps = {
|
|||||||
VariablePicker?: VariablePickerComponent;
|
VariablePicker?: VariablePickerComponent;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
error?: string;
|
||||||
|
onError?: (error: string | undefined) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FormFieldInput = ({
|
export const FormFieldInput = ({
|
||||||
@ -61,6 +63,8 @@ export const FormFieldInput = ({
|
|||||||
VariablePicker,
|
VariablePicker,
|
||||||
readonly,
|
readonly,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
error,
|
||||||
|
onError,
|
||||||
}: FormFieldInputProps) => {
|
}: FormFieldInputProps) => {
|
||||||
return isFieldNumber(field) ? (
|
return isFieldNumber(field) ? (
|
||||||
<FormNumberFieldInput
|
<FormNumberFieldInput
|
||||||
@ -70,6 +74,8 @@ export const FormFieldInput = ({
|
|||||||
VariablePicker={VariablePicker}
|
VariablePicker={VariablePicker}
|
||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
error={error}
|
||||||
|
onError={onError}
|
||||||
/>
|
/>
|
||||||
) : isFieldBoolean(field) ? (
|
) : isFieldBoolean(field) ? (
|
||||||
<FormBooleanFieldInput
|
<FormBooleanFieldInput
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { InputHint } from '@/ui/input/components/InputHint';
|
|||||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||||
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import isEmpty from 'lodash.isempty';
|
||||||
import { useId, useState } from 'react';
|
import { useId, useState } from 'react';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import {
|
import {
|
||||||
@ -22,7 +23,6 @@ const StyledInput = styled(TextInput)`
|
|||||||
|
|
||||||
type FormNumberFieldInputProps = {
|
type FormNumberFieldInputProps = {
|
||||||
label?: string;
|
label?: string;
|
||||||
error?: string;
|
|
||||||
defaultValue: number | string | undefined;
|
defaultValue: number | string | undefined;
|
||||||
onChange: (value: number | null | string) => void;
|
onChange: (value: number | null | string) => void;
|
||||||
onBlur?: () => void;
|
onBlur?: () => void;
|
||||||
@ -30,20 +30,26 @@ type FormNumberFieldInputProps = {
|
|||||||
hint?: string;
|
hint?: string;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
error?: string;
|
||||||
|
onError?: (error: string | undefined) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FormNumberFieldInput = ({
|
export const FormNumberFieldInput = ({
|
||||||
label,
|
label,
|
||||||
error,
|
|
||||||
placeholder,
|
placeholder,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
onChange,
|
onChange,
|
||||||
|
onError,
|
||||||
onBlur,
|
onBlur,
|
||||||
VariablePicker,
|
VariablePicker,
|
||||||
hint,
|
hint,
|
||||||
readonly,
|
readonly,
|
||||||
|
error: errorFromProps,
|
||||||
}: FormNumberFieldInputProps) => {
|
}: FormNumberFieldInputProps) => {
|
||||||
const inputId = useId();
|
const inputId = useId();
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
const [draftValue, setDraftValue] = useState<
|
const [draftValue, setDraftValue] = useState<
|
||||||
| {
|
| {
|
||||||
@ -68,9 +74,14 @@ export const FormNumberFieldInput = ({
|
|||||||
|
|
||||||
const persistNumber = (newValue: string) => {
|
const persistNumber = (newValue: string) => {
|
||||||
if (!canBeCastAsNumberOrNull(newValue)) {
|
if (!canBeCastAsNumberOrNull(newValue)) {
|
||||||
|
setErrorMessage('Invalid number');
|
||||||
|
onError?.('Invalid number');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setErrorMessage(undefined);
|
||||||
|
onError?.(undefined);
|
||||||
|
|
||||||
const castedValue = castAsNumberOrNull(newValue);
|
const castedValue = castAsNumberOrNull(newValue);
|
||||||
|
|
||||||
onChange(castedValue);
|
onChange(castedValue);
|
||||||
@ -115,7 +126,11 @@ export const FormNumberFieldInput = ({
|
|||||||
{draftValue.type === 'static' ? (
|
{draftValue.type === 'static' ? (
|
||||||
<StyledInput
|
<StyledInput
|
||||||
inputId={inputId}
|
inputId={inputId}
|
||||||
placeholder={placeholder ?? 'Enter a number'}
|
placeholder={
|
||||||
|
isDefined(placeholder) && !isEmpty(placeholder)
|
||||||
|
? placeholder
|
||||||
|
: 'Enter a number'
|
||||||
|
}
|
||||||
value={draftValue.value}
|
value={draftValue.value}
|
||||||
copyButton={false}
|
copyButton={false}
|
||||||
hotkeyScope="record-create"
|
hotkeyScope="record-create"
|
||||||
@ -139,7 +154,7 @@ export const FormNumberFieldInput = ({
|
|||||||
</FormFieldInputRowContainer>
|
</FormFieldInputRowContainer>
|
||||||
|
|
||||||
{hint ? <InputHint>{hint}</InputHint> : null}
|
{hint ? <InputHint>{hint}</InputHint> : null}
|
||||||
<InputErrorHelper>{error}</InputErrorHelper>
|
<InputErrorHelper>{errorMessage ?? errorFromProps}</InputErrorHelper>
|
||||||
</FormFieldInputContainer>
|
</FormFieldInputContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -70,3 +70,16 @@ export const Disabled: Story = {
|
|||||||
expect(variablePicker).not.toBeInTheDocument();
|
expect(variablePicker).not.toBeInTheDocument();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const WithError: Story = {
|
||||||
|
args: {
|
||||||
|
error: 'Invalid number',
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const error = await canvas.findByText('Invalid number');
|
||||||
|
|
||||||
|
expect(error).toBeVisible();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@ -16,8 +16,8 @@ import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/
|
|||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
|
||||||
import { useIcons } from 'twenty-ui/display';
|
import { useIcons } from 'twenty-ui/display';
|
||||||
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
export type WorkflowEditActionFormFillerProps = {
|
export type WorkflowEditActionFormFillerProps = {
|
||||||
action: WorkflowFormAction;
|
action: WorkflowFormAction;
|
||||||
@ -39,6 +39,8 @@ export const WorkflowEditActionFormFiller = ({
|
|||||||
const { workflowRunId } = useWorkflowStepContextOrThrow();
|
const { workflowRunId } = useWorkflowStepContextOrThrow();
|
||||||
const { closeCommandMenu } = useCommandMenu();
|
const { closeCommandMenu } = useCommandMenu();
|
||||||
const { updateWorkflowRunStep } = useUpdateWorkflowRunStep();
|
const { updateWorkflowRunStep } = useUpdateWorkflowRunStep();
|
||||||
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
|
const canSubmit = !actionOptions.readonly && !isDefined(error);
|
||||||
|
|
||||||
if (!isDefined(workflowRunId)) {
|
if (!isDefined(workflowRunId)) {
|
||||||
throw new Error('Form filler action must be used in a workflow run');
|
throw new Error('Form filler action must be used in a workflow run');
|
||||||
@ -164,6 +166,9 @@ export const WorkflowEditActionFormFiller = ({
|
|||||||
field.placeholder ??
|
field.placeholder ??
|
||||||
getDefaultFormFieldSettings(field.type).placeholder
|
getDefaultFormFieldSettings(field.type).placeholder
|
||||||
}
|
}
|
||||||
|
onError={(error) => {
|
||||||
|
setError(error);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -174,7 +179,7 @@ export const WorkflowEditActionFormFiller = ({
|
|||||||
<CmdEnterActionButton
|
<CmdEnterActionButton
|
||||||
title="Submit"
|
title="Submit"
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
disabled={actionOptions.readonly}
|
disabled={!canSubmit}
|
||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { WorkflowFormAction } from '@/workflow/types/Workflow';
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import { expect, within } from '@storybook/test';
|
import { expect, within } from '@storybook/test';
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
|
||||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||||
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
||||||
@ -9,7 +10,6 @@ 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 { WorkflowEditActionFormFiller } from '../WorkflowEditActionFormFiller';
|
import { WorkflowEditActionFormFiller } from '../WorkflowEditActionFormFiller';
|
||||||
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
|
|
||||||
|
|
||||||
const meta: Meta<typeof WorkflowEditActionFormFiller> = {
|
const meta: Meta<typeof WorkflowEditActionFormFiller> = {
|
||||||
title: 'Modules/Workflow/Actions/Form/WorkflowEditActionFormFiller',
|
title: 'Modules/Workflow/Actions/Form/WorkflowEditActionFormFiller',
|
||||||
|
|||||||
Reference in New Issue
Block a user