Create form field number (#8634)

- Refactor VariableTagInput to have a reusable low-level TipTap editor
- Create three primitive form fields:
  - Text
  - Number
  - Boolean

## Notes

- We should automatically recognize the placeholder to use for every
FormFieldInput, as it's done for FieldInputs.

## Design decisions

Our main challenge was for variables and inputs to be able to
communicate between each other. We chose an API that adds some
duplication but remains simple and doesn't rely on "hacks" to work.
Common styles are centralized.

## Demo

"Workflow" mode with variables:

![CleanShot 2024-11-26 at 10 43
25@2x](https://github.com/user-attachments/assets/cc17098a-ca27-4f97-b86a-bf88593e53db)

FormFieldInput mode, without variables:

![CleanShot 2024-11-26 at 10 44
26@2x](https://github.com/user-attachments/assets/fec07c36-5944-4a1d-a863-516fd77c8f55)

Behavior difference between fields that can contain variables and static
content, and inputs that can have either a variable value or a static
value:

![CleanShot 2024-11-26 at 10 47
13@2x](https://github.com/user-attachments/assets/1e562cd8-c362-46d0-b438-481215159da9)
This commit is contained in:
Baptiste Devessier
2024-11-28 18:03:24 +01:00
committed by GitHub
parent 3573d89c3c
commit d73dc1a728
32 changed files with 951 additions and 332 deletions

View File

@ -2,6 +2,7 @@ import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilte
import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput';
import { Select, SelectOption } from '@/ui/input/components/Select';
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
import { WorkflowVariablePicker } from '@/workflow/components/WorkflowVariablePicker';
import { WorkflowRecordCreateAction } from '@/workflow/types/Workflow';
import { useTheme } from '@emotion/react';
import { useEffect, useState } from 'react';
@ -11,6 +12,7 @@ import {
isDefined,
useIcons,
} from 'twenty-ui';
import { JsonValue } from 'type-fest';
import { useDebouncedCallback } from 'use-debounce';
import { FieldMetadataType } from '~/generated/graphql';
@ -55,7 +57,7 @@ export const WorkflowEditActionFormRecordCreate = ({
const handleFieldChange = (
fieldName: keyof SendEmailFormData,
updatedValue: string,
updatedValue: JsonValue,
) => {
const newFormData: SendEmailFormData = {
...formData,
@ -163,17 +165,21 @@ export const WorkflowEditActionFormRecordCreate = ({
<HorizontalSeparator noMargin />
{editableFields.map((field) => (
<FormFieldInput
key={field.id}
recordFieldInputdId={field.id}
label={field.label}
value={formData[field.name] as string}
onChange={(value) => {
handleFieldChange(field.name, value);
}}
/>
))}
{editableFields.map((field) => {
const currentValue = formData[field.name] as JsonValue;
return (
<FormFieldInput
key={field.id}
defaultValue={currentValue}
field={field}
onPersist={(value) => {
handleFieldChange(field.name, value);
}}
VariablePicker={WorkflowVariablePicker}
/>
);
})}
</WorkflowEditGenericFormBase>
);
};

View File

@ -2,10 +2,11 @@ import { GMAIL_SEND_SCOPE } from '@/accounts/constants/GmailSendScope';
import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
import { Select, SelectOption } from '@/ui/input/components/Select';
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
import { VariableTagInput } from '@/workflow/search-variables/components/VariableTagInput';
import { WorkflowVariablePicker } from '@/workflow/components/WorkflowVariablePicker';
import { workflowIdState } from '@/workflow/states/workflowIdState';
import { WorkflowSendEmailAction } from '@/workflow/types/Workflow';
import { useTheme } from '@emotion/react';
@ -214,16 +215,16 @@ export const WorkflowEditActionFormSendEmail = ({
name="email"
control={form.control}
render={({ field }) => (
<VariableTagInput
inputId="email-input"
<FormTextFieldInput
label="Email"
placeholder="Enter receiver email"
value={field.value}
onChange={(email) => {
field.onChange(email);
readonly={actionOptions.readonly}
defaultValue={field.value}
onPersist={(value) => {
field.onChange(value);
handleSave();
}}
readonly={actionOptions.readonly}
VariablePicker={WorkflowVariablePicker}
/>
)}
/>
@ -231,16 +232,16 @@ export const WorkflowEditActionFormSendEmail = ({
name="subject"
control={form.control}
render={({ field }) => (
<VariableTagInput
inputId="email-subject-input"
<FormTextFieldInput
label="Subject"
placeholder="Enter email subject"
value={field.value}
onChange={(email) => {
field.onChange(email);
readonly={actionOptions.readonly}
defaultValue={field.value}
onPersist={(value) => {
field.onChange(value);
handleSave();
}}
readonly={actionOptions.readonly}
VariablePicker={WorkflowVariablePicker}
/>
)}
/>
@ -248,17 +249,16 @@ export const WorkflowEditActionFormSendEmail = ({
name="body"
control={form.control}
render={({ field }) => (
<VariableTagInput
inputId="email-body-input"
<FormTextFieldInput
label="Body"
placeholder="Enter email body"
value={field.value}
onChange={(email) => {
field.onChange(email);
readonly={actionOptions.readonly}
defaultValue={field.value}
onPersist={(value) => {
field.onChange(value);
handleSave();
}}
multiline
readonly={actionOptions.readonly}
VariablePicker={WorkflowVariablePicker}
/>
)}
/>

View File

@ -1,7 +1,8 @@
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
import { useGetManyServerlessFunctions } from '@/settings/serverless-functions/hooks/useGetManyServerlessFunctions';
import { Select, SelectOption } from '@/ui/input/components/Select';
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
import VariableTagInput from '@/workflow/search-variables/components/VariableTagInput';
import { WorkflowVariablePicker } from '@/workflow/components/WorkflowVariablePicker';
import { FunctionInput } from '@/workflow/types/FunctionInput';
import { WorkflowCodeAction } from '@/workflow/types/Workflow';
import { getDefaultFunctionInputFromInputSchema } from '@/workflow/utils/getDefaultFunctionInputFromInputSchema';
@ -203,14 +204,16 @@ export const WorkflowEditActionFormServerlessFunctionInner = ({
);
} else {
return (
<VariableTagInput
<FormTextFieldInput
key={pathKey}
inputId={`input-${inputKey}`}
label={inputKey}
placeholder="Enter value"
defaultValue={inputValue ? String(inputValue) : ''}
readonly={actionOptions.readonly}
value={`${inputValue || ''}`}
onChange={(value) => handleInputChange(value, currentPath)}
onPersist={(value) => {
handleInputChange(value, currentPath);
}}
VariablePicker={WorkflowVariablePicker}
/>
);
}

View File

@ -0,0 +1,57 @@
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import SearchVariablesDropdown from '@/workflow/search-variables/components/SearchVariablesDropdown';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
export const StyledSearchVariablesDropdownContainer = styled.div<{
multiline?: boolean;
readonly?: boolean;
}>`
align-items: center;
display: flex;
justify-content: center;
${({ theme, readonly }) =>
!readonly &&
css`
:hover {
background-color: ${theme.background.transparent.light};
}
`}
${({ theme, multiline }) =>
multiline
? css`
border-radius: ${theme.border.radius.sm};
padding: ${theme.spacing(0.5)} ${theme.spacing(0)};
position: absolute;
right: ${theme.spacing(0)};
top: ${theme.spacing(0)};
`
: css`
background-color: ${theme.background.transparent.lighter};
border-top-right-radius: ${theme.border.radius.sm};
border-bottom-right-radius: ${theme.border.radius.sm};
border: 1px solid ${theme.border.color.medium};
`}
`;
export const WorkflowVariablePicker: VariablePickerComponent = ({
inputId,
disabled,
multiline,
onVariableSelect,
}) => {
return (
<StyledSearchVariablesDropdownContainer
multiline={multiline}
readonly={disabled}
>
<SearchVariablesDropdown
inputId={inputId}
onVariableSelect={onVariableSelect}
disabled={disabled}
/>
</StyledSearchVariablesDropdownContainer>
);
};