Add full name composite field (#9008)

- Add composite field
- Add test
- Fix search variable dropdown scroll
<img width="548" alt="Capture d’écran 2024-12-10 à 16 52 43"
src="https://github.com/user-attachments/assets/c337f0c3-8a70-401a-abd0-7206489ba73e">
This commit is contained in:
Thomas Trompette
2024-12-10 17:29:55 +01:00
committed by GitHub
parent 96d56f8883
commit b6e02b630d
13 changed files with 148 additions and 33 deletions

View File

@ -1,11 +1,16 @@
import { FormBooleanFieldInput } from '@/object-record/record-field/form-types/components/FormBooleanFieldInput';
import { FormFullNameFieldInput } from '@/object-record/record-field/form-types/components/FormFullNameFieldInput';
import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput';
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import {
FieldFullNameValue,
FieldMetadata,
} from '@/object-record/record-field/types/FieldMetadata';
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
@ -55,5 +60,12 @@ export const FormFieldInput = ({
field={field}
VariablePicker={VariablePicker}
/>
) : isFieldFullName(field) ? (
<FormFullNameFieldInput
label={field.label}
defaultValue={defaultValue as FieldFullNameValue | undefined}
onPersist={onPersist}
VariablePicker={VariablePicker}
/>
) : null;
};

View File

@ -0,0 +1,66 @@
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
import { StyledFormCompositeFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormCompositeFieldInputContainer';
import { StyledFormFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputContainer';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/FirstNamePlaceholder';
import { LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/LastNamePlaceholder';
import { FieldFullNameValue } from '@/object-record/record-field/types/FieldMetadata';
import { InputLabel } from '@/ui/input/components/InputLabel';
type FormFullNameFieldInputProps = {
label?: string;
defaultValue: FieldFullNameValue | undefined;
onPersist: (value: FieldFullNameValue) => void;
VariablePicker?: VariablePickerComponent;
readonly?: boolean;
};
export const FormFullNameFieldInput = ({
label,
defaultValue,
onPersist,
readonly,
VariablePicker,
}: FormFullNameFieldInputProps) => {
const handleFirstNameChange = (newText: string) => {
onPersist({
lastName: defaultValue?.lastName ?? '',
firstName: newText,
});
};
const handleLastNameChange = (newText: string) => {
onPersist({
firstName: defaultValue?.firstName ?? '',
lastName: newText,
});
};
return (
<StyledFormFieldInputContainer>
{label ? <InputLabel>{label}</InputLabel> : null}
<StyledFormCompositeFieldInputContainer>
<FormTextFieldInput
label="First Name"
defaultValue={defaultValue?.firstName}
onPersist={handleFirstNameChange}
placeholder={
FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS
}
readonly={readonly}
VariablePicker={VariablePicker}
/>
<FormTextFieldInput
label="Last Name"
defaultValue={defaultValue?.lastName}
onPersist={handleLastNameChange}
placeholder={
LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS
}
readonly={readonly}
VariablePicker={VariablePicker}
/>
</StyledFormCompositeFieldInputContainer>
</StyledFormFieldInputContainer>
);
};

View File

@ -0,0 +1,11 @@
import styled from '@emotion/styled';
export const StyledFormCompositeFieldInputContainer = styled.div`
display: flex;
flex-direction: column;
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(2)};
`;

View File

@ -3,4 +3,5 @@ import styled from '@emotion/styled';
export const StyledFormFieldInputContainer = styled.div`
display: flex;
flex-direction: column;
width: 100%;
`;

View File

@ -0,0 +1,31 @@
import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test';
import { FormFullNameFieldInput } from '../FormFullNameFieldInput';
const meta: Meta<typeof FormFullNameFieldInput> = {
title: 'UI/Data/Field/Form/Input/FormFullNameFieldInput',
component: FormFullNameFieldInput,
args: {},
argTypes: {},
};
export default meta;
type Story = StoryObj<typeof FormFullNameFieldInput>;
export const Default: Story = {
args: {
label: 'Name',
defaultValue: {
firstName: 'John',
lastName: 'Doe',
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('Name');
await canvas.findByText('First Name');
await canvas.findByText('Last Name');
},
};

View File

@ -3,18 +3,14 @@ import { FieldDoubleText } from '@/object-record/record-field/types/FieldDoubleT
import { DoubleTextInput } from '@/ui/field/input/components/DoubleTextInput';
import { FieldInputOverlay } from '@/ui/field/input/components/FieldInputOverlay';
import { FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/FirstNamePlaceholder';
import { LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/LastNamePlaceholder';
import { isDoubleTextFieldEmpty } from '@/object-record/record-field/meta-types/input/utils/isDoubleTextFieldEmpty';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from './DateTimeFieldInput';
const FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS =
'First name';
const LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS =
'Last name';
type FullNameFieldInputProps = {
onClickOutside?: FieldInputClickOutsideEvent;
onEnter?: FieldInputEvent;

View File

@ -0,0 +1,2 @@
export const FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS =
'First name';

View File

@ -0,0 +1,2 @@
export const LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS =
'Last name';

View File

@ -1,4 +1,5 @@
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/StyledDropdownButtonContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { SearchVariablesDropdownFieldItems } from '@/workflow/search-variables/components/SearchVariablesDropdownFieldItems';
@ -26,7 +27,7 @@ const StyledDropdownVariableButtonContainer = styled(
}
`;
const StyledDropdownComponetsContainer = styled.div`
const StyledDropdownComponentsContainer = styled(DropdownMenuItemsContainer)`
background-color: ${({ theme }) => theme.background.transparent.light};
`;
@ -136,9 +137,9 @@ const SearchVariablesDropdown = ({
</StyledDropdownVariableButtonContainer>
}
dropdownComponents={
<StyledDropdownComponetsContainer>
<StyledDropdownComponentsContainer>
{renderSearchVariablesDropdownComponents()}
</StyledDropdownComponetsContainer>
</StyledDropdownComponentsContainer>
}
dropdownPlacement="bottom-end"
dropdownOffset={{ x: 0, y: 4 }}

View File

@ -1,5 +1,4 @@
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import {
BaseOutputSchema,
@ -100,8 +99,14 @@ export const SearchVariablesDropdownFieldItems = ({
: options;
return (
<DropdownMenuItemsContainer>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={goBack}>
<>
<DropdownMenuHeader
StartIcon={IconChevronLeft}
onClick={goBack}
style={{
position: 'fixed',
}}
>
<OverflowingTextWithTooltip text={headerLabel} />
</DropdownMenuHeader>
<HorizontalSeparator
@ -128,6 +133,6 @@ export const SearchVariablesDropdownFieldItems = ({
LeftIcon={value.icon ? getIcon(value.icon) : undefined}
/>
))}
</DropdownMenuItemsContainer>
</>
);
};

View File

@ -1,5 +1,4 @@
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import {
OutputSchema,
@ -112,7 +111,7 @@ export const SearchVariablesDropdownObjectItems = ({
: options;
return (
<DropdownMenuItemsContainer>
<>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={goBack}>
<OverflowingTextWithTooltip text={headerLabel} />
</DropdownMenuHeader>
@ -154,6 +153,6 @@ export const SearchVariablesDropdownObjectItems = ({
LeftIcon={value.icon ? getIcon(value.icon) : undefined}
/>
))}
</DropdownMenuItemsContainer>
</>
);
};

View File

@ -1,5 +1,4 @@
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { StepOutputSchema } from '@/workflow/search-variables/types/StepOutputSchema';
@ -36,7 +35,7 @@ export const SearchVariablesDropdownWorkflowStepItems = ({
);
return (
<DropdownMenuItemsContainer>
<>
<DropdownMenuHeader StartIcon={IconX} onClick={closeDropdown}>
<OverflowingTextWithTooltip text={'Select Step'} />
</DropdownMenuHeader>
@ -74,6 +73,6 @@ export const SearchVariablesDropdownWorkflowStepItems = ({
hasSubMenu={false}
/>
)}
</DropdownMenuItemsContainer>
</>
);
};

View File

@ -1,4 +1,5 @@
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
import { StyledFormCompositeFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormCompositeFieldInputContainer';
import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages';
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
import { useUpdateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useUpdateOneServerlessFunction';
@ -44,17 +45,6 @@ const StyledLabel = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
const StyledInputContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.md};
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(2)};
position: relative;
`;
type WorkflowEditActionFormServerlessFunctionProps = {
action: WorkflowCodeAction;
actionOptions:
@ -223,9 +213,9 @@ export const WorkflowEditActionFormServerlessFunction = ({
return (
<StyledContainer key={pathKey}>
<StyledLabel>{inputKey}</StyledLabel>
<StyledInputContainer>
<StyledFormCompositeFieldInputContainer>
{renderFields(inputValue, currentPath, false)}
</StyledInputContainer>
</StyledFormCompositeFieldInputContainer>
</StyledContainer>
);
} else {