Skip to content

Commit 9898ca3

Browse files
gitstart-app[bot]gitstart-twentylucasbordeau
authored
TWNTY-6135 - Improve Data Importer Select Matching (#6338)
### Description: - we move all logic about the unmatchedOptions to a new component called UnmatchColumn, because as it will be a full line in the table, it was better to update where the component will be rendered - In the latest changes to keep the columns when we change the step to step 3 and go back to step 2, we added a fallback state initialComputedColumnsState that saves the columns and only reverts the updates when we go back to step 1 or close by clicking the X button ### Refs: #6135 ``` It was necessary to add references and floating styles to the generic component to fix the bug when the last option was open and the dropdown was being hidden in the next row of the spreadsheet table. We fixed the same problem that occurs in the companies table as well ``` we used this approach mentioned on this documentation to be able to use the hook without calling it on each component, we are calling only once, on the shared component <https://floating-ui.com/docs/useFloating#elements>\ before: ![](https://assets-service.gitstart.com/25493/2c994e0f-6548-4a9e-8b22-2c6eccb73b2e.png) now: ![](https://assets-service.gitstart.com/25493/f56fd516-7e95-4616-b1ed-c9ea5195a8ae.png)### Demo: <https://jam.dev/c/e0e0b921-7551-4a94-ac1c-8a50c53fdb0c> Fixes #6135 NOTES: the enter key are not working on main branch too --------- Co-authored-by: gitstart-twenty <[email protected]> Co-authored-by: Lucas Bordeau <[email protected]>
1 parent eab202f commit 9898ca3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1209
-657
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,15 @@
1-
import styled from '@emotion/styled';
2-
import { useRef, useState } from 'react';
3-
import { useRecoilValue } from 'recoil';
4-
import { Key } from 'ts-key-enum';
5-
61
import { useClearField } from '@/object-record/record-field/hooks/useClearField';
72
import { useSelectField } from '@/object-record/record-field/meta-types/hooks/useSelectField';
83
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
94
import { SINGLE_ENTITY_SELECT_BASE_LIST } from '@/object-record/relation-picker/constants/SingleEntitySelectBaseList';
10-
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
11-
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
12-
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
13-
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
5+
import { SelectOption } from '@/spreadsheet-import/types';
6+
import { SelectInput } from '@/ui/input/components/SelectInput';
147
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
15-
import { useSelectableListStates } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListStates';
168
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
17-
import { MenuItemSelectTag } from '@/ui/navigation/menu-item/components/MenuItemSelectTag';
189
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
19-
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
20-
import { isDefined } from '~/utils/isDefined';
21-
22-
const StyledRelationPickerContainer = styled.div`
23-
left: -1px;
24-
position: absolute;
25-
top: -1px;
26-
`;
10+
import { useState } from 'react';
11+
import { Key } from 'ts-key-enum';
12+
import { isDefined } from 'twenty-ui';
2713

2814
type SelectFieldInputProps = {
2915
onSubmit?: FieldInputEvent;
@@ -36,55 +22,30 @@ export const SelectFieldInput = ({
3622
}: SelectFieldInputProps) => {
3723
const { persistField, fieldDefinition, fieldValue, hotkeyScope } =
3824
useSelectField();
39-
const { selectedItemIdState } = useSelectableListStates({
40-
selectableListScopeId: SINGLE_ENTITY_SELECT_BASE_LIST,
41-
});
25+
const [selectWrapperRef, setSelectWrapperRef] =
26+
useState<HTMLDivElement | null>(null);
27+
28+
const [filteredOptions, setFilteredOptions] = useState<SelectOption[]>([]);
29+
4230
const { handleResetSelectedPosition } = useSelectableList(
4331
SINGLE_ENTITY_SELECT_BASE_LIST,
4432
);
4533
const clearField = useClearField();
4634

47-
const selectedItemId = useRecoilValue(selectedItemIdState);
48-
const [searchFilter, setSearchFilter] = useState('');
49-
const containerRef = useRef<HTMLDivElement>(null);
50-
5135
const selectedOption = fieldDefinition.metadata.options.find(
5236
(option) => option.value === fieldValue,
5337
);
54-
55-
const optionsToSelect =
56-
fieldDefinition.metadata.options.filter((option) => {
57-
return (
58-
option.value !== fieldValue &&
59-
option.label.toLowerCase().includes(searchFilter.toLowerCase())
60-
);
61-
}) || [];
62-
63-
const optionsInDropDown = selectedOption
64-
? [selectedOption, ...optionsToSelect]
65-
: optionsToSelect;
66-
6738
// handlers
6839
const handleClearField = () => {
6940
clearField();
7041
onCancel?.();
7142
};
7243

73-
useListenClickOutside({
74-
refs: [containerRef],
75-
callback: (event) => {
76-
event.stopImmediatePropagation();
44+
const handleSubmit = (option: SelectOption) => {
45+
onSubmit?.(() => persistField(option?.value));
7746

78-
const weAreNotInAnHTMLInput = !(
79-
event.target instanceof HTMLInputElement &&
80-
event.target.tagName === 'INPUT'
81-
);
82-
if (weAreNotInAnHTMLInput && isDefined(onCancel)) {
83-
onCancel();
84-
handleResetSelectedPosition();
85-
}
86-
},
87-
});
47+
handleResetSelectedPosition();
48+
};
8849

8950
useScopedHotkeys(
9051
Key.Escape,
@@ -96,81 +57,40 @@ export const SelectFieldInput = ({
9657
[onCancel, handleResetSelectedPosition],
9758
);
9859

99-
useScopedHotkeys(
100-
Key.Enter,
101-
() => {
102-
const selectedOption = optionsInDropDown.find((option) =>
103-
option.label.toLowerCase().includes(searchFilter.toLowerCase()),
104-
);
105-
106-
if (isDefined(selectedOption)) {
107-
onSubmit?.(() => persistField(selectedOption.value));
108-
}
109-
handleResetSelectedPosition();
110-
},
111-
hotkeyScope,
112-
);
113-
11460
const optionIds = [
11561
`No ${fieldDefinition.label}`,
116-
...optionsInDropDown.map((option) => option.value),
62+
...filteredOptions.map((option) => option.value),
11763
];
11864

11965
return (
120-
<SelectableList
121-
selectableListId={SINGLE_ENTITY_SELECT_BASE_LIST}
122-
selectableItemIdArray={optionIds}
123-
hotkeyScope={hotkeyScope}
124-
onEnter={(itemId) => {
125-
const option = optionsInDropDown.find(
126-
(option) => option.value === itemId,
127-
);
128-
if (isDefined(option)) {
129-
onSubmit?.(() => persistField(option.value));
130-
handleResetSelectedPosition();
131-
}
132-
}}
133-
>
134-
<StyledRelationPickerContainer ref={containerRef}>
135-
<DropdownMenu data-select-disable>
136-
<DropdownMenuSearchInput
137-
value={searchFilter}
138-
onChange={(event) => setSearchFilter(event.currentTarget.value)}
139-
autoFocus
140-
/>
141-
<DropdownMenuSeparator />
142-
143-
<DropdownMenuItemsContainer hasMaxHeight>
144-
{fieldDefinition.metadata.isNullable ?? (
145-
<MenuItemSelectTag
146-
key={`No ${fieldDefinition.label}`}
147-
selected={false}
148-
text={`No ${fieldDefinition.label}`}
149-
color="transparent"
150-
variant="outline"
151-
onClick={handleClearField}
152-
isKeySelected={selectedItemId === `No ${fieldDefinition.label}`}
153-
/>
154-
)}
155-
156-
{optionsInDropDown.map((option) => {
157-
return (
158-
<MenuItemSelectTag
159-
key={option.value}
160-
selected={option.value === fieldValue}
161-
text={option.label}
162-
color={option.color}
163-
onClick={() => {
164-
onSubmit?.(() => persistField(option.value));
165-
handleResetSelectedPosition();
166-
}}
167-
isKeySelected={selectedItemId === option.value}
168-
/>
169-
);
170-
})}
171-
</DropdownMenuItemsContainer>
172-
</DropdownMenu>
173-
</StyledRelationPickerContainer>
174-
</SelectableList>
66+
<div ref={setSelectWrapperRef}>
67+
<SelectableList
68+
selectableListId={SINGLE_ENTITY_SELECT_BASE_LIST}
69+
selectableItemIdArray={optionIds}
70+
hotkeyScope={hotkeyScope}
71+
onEnter={(itemId) => {
72+
const option = filteredOptions.find(
73+
(option) => option.value === itemId,
74+
);
75+
if (isDefined(option)) {
76+
onSubmit?.(() => persistField(option.value));
77+
handleResetSelectedPosition();
78+
}
79+
}}
80+
>
81+
<SelectInput
82+
parentRef={selectWrapperRef}
83+
onOptionSelected={handleSubmit}
84+
options={fieldDefinition.metadata.options}
85+
onCancel={onCancel}
86+
defaultOption={selectedOption}
87+
onFilterChange={setFilteredOptions}
88+
onClear={
89+
fieldDefinition.metadata.isNullable ? handleClearField : undefined
90+
}
91+
clearLabel={fieldDefinition.label}
92+
/>
93+
</SelectableList>
94+
</div>
17595
);
17696
};

packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts

+32
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,38 @@ export const useBuildAvailableFieldsForImport = () => {
123123
),
124124
});
125125
});
126+
} else if (fieldMetadataItem.type === FieldMetadataType.Select) {
127+
availableFieldsForImport.push({
128+
icon: getIcon(fieldMetadataItem.icon),
129+
label: fieldMetadataItem.label,
130+
key: fieldMetadataItem.name,
131+
fieldType: {
132+
type: 'select',
133+
options:
134+
fieldMetadataItem.options?.map((option) => ({
135+
label: option.label,
136+
value: option.value,
137+
color: option.color,
138+
})) || [],
139+
},
140+
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
141+
fieldMetadataItem.type,
142+
fieldMetadataItem.label + ' (ID)',
143+
),
144+
});
145+
} else if (fieldMetadataItem.type === FieldMetadataType.Boolean) {
146+
availableFieldsForImport.push({
147+
icon: getIcon(fieldMetadataItem.icon),
148+
label: fieldMetadataItem.label,
149+
key: fieldMetadataItem.name,
150+
fieldType: {
151+
type: 'checkbox',
152+
},
153+
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
154+
fieldMetadataItem.type,
155+
fieldMetadataItem.label,
156+
),
157+
});
126158
} else {
127159
availableFieldsForImport.push({
128160
icon: getIcon(fieldMetadataItem.icon),
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { FieldValidationDefinition } from '@/spreadsheet-import/types';
1+
import {
2+
FieldValidationDefinition,
3+
SpreadsheetImportFieldType,
4+
} from '@/spreadsheet-import/types';
25
import { IconComponent } from 'twenty-ui';
36

47
export type AvailableFieldForImport = {
58
icon: IconComponent;
69
label: string;
710
key: string;
8-
fieldType: {
9-
type: 'input' | 'checkbox';
10-
};
11+
fieldType: SpreadsheetImportFieldType;
1112
fieldValidationDefinitions?: FieldValidationDefinition[];
1213
};

packages/twenty-front/src/modules/spreadsheet-import/components/Heading.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const StyledTitle = styled.span`
2020
`;
2121

2222
const StyledDescription = styled.span`
23-
color: ${({ theme }) => theme.font.color.primary};
23+
color: ${({ theme }) => theme.font.color.secondary};
2424
font-size: ${({ theme }) => theme.font.size.sm};
2525
font-weight: ${({ theme }) => theme.font.weight.regular};
2626
margin-top: ${({ theme }) => theme.spacing(3)};

packages/twenty-front/src/modules/spreadsheet-import/components/ModalWrapper.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const StyledModal = styled(Modal)`
1010
height: 61%;
1111
min-height: 600px;
1212
min-width: 800px;
13+
padding: 0;
1314
position: relative;
1415
width: 63%;
1516
@media (max-width: ${MOBILE_VIEWPORT}px) {
@@ -42,7 +43,7 @@ export const ModalWrapper = ({
4243
return (
4344
<>
4445
{isOpen && (
45-
<StyledModal size="large" onClose={onClose} isClosable={true}>
46+
<StyledModal size="large">
4647
<StyledRtlLtr dir={rtl ? 'rtl' : 'ltr'}>
4748
<ModalCloseButton onClose={onClose} />
4849
{children}

packages/twenty-front/src/modules/spreadsheet-import/components/Providers.tsx renamed to packages/twenty-front/src/modules/spreadsheet-import/components/ReactSpreadsheetImportContextProvider.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
55

66
export const RsiContext = createContext({} as any);
77

8-
type ProvidersProps<T extends string> = {
8+
type ReactSpreadsheetImportContextProviderProps<T extends string> = {
99
children: React.ReactNode;
1010
values: SpreadsheetImportDialogOptions<T>;
1111
};
1212

13-
export const Providers = <T extends string>({
13+
export const ReactSpreadsheetImportContextProvider = <T extends string>({
1414
children,
1515
values,
16-
}: ProvidersProps<T>) => {
16+
}: ReactSpreadsheetImportContextProviderProps<T>) => {
1717
if (isUndefinedOrNull(values.fields)) {
1818
throw new Error('Fields must be provided to spreadsheet-import');
1919
}

packages/twenty-front/src/modules/spreadsheet-import/components/StepNavigationButton.tsx

+19-16
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import { Modal } from '@/ui/layout/modal/components/Modal';
77
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
88

99
const StyledFooter = styled(Modal.Footer)`
10-
gap: ${({ theme }) => theme.spacing(2)};
10+
gap: ${({ theme }) => theme.spacing(2.5)};
1111
justify-content: space-between;
12+
padding: ${({ theme }) => theme.spacing(6)} ${({ theme }) => theme.spacing(8)};
1213
`;
1314

1415
type StepNavigationButtonProps = {
@@ -23,21 +24,23 @@ export const StepNavigationButton = ({
2324
title,
2425
isLoading,
2526
onBack,
26-
}: StepNavigationButtonProps) => (
27-
<StyledFooter>
28-
{!isUndefinedOrNull(onBack) && (
27+
}: StepNavigationButtonProps) => {
28+
return (
29+
<StyledFooter>
30+
{!isUndefinedOrNull(onBack) && (
31+
<MainButton
32+
Icon={isLoading ? CircularProgressBar : undefined}
33+
title="Back"
34+
onClick={!isLoading ? onBack : undefined}
35+
variant="secondary"
36+
/>
37+
)}
2938
<MainButton
3039
Icon={isLoading ? CircularProgressBar : undefined}
31-
title="Back"
32-
onClick={!isLoading ? onBack : undefined}
33-
variant="secondary"
40+
title={title}
41+
onClick={!isLoading ? onClick : undefined}
42+
variant="primary"
3443
/>
35-
)}
36-
<MainButton
37-
Icon={isLoading ? CircularProgressBar : undefined}
38-
title={title}
39-
onClick={!isLoading ? onClick : undefined}
40-
variant="primary"
41-
/>
42-
</StyledFooter>
43-
);
44+
</StyledFooter>
45+
);
46+
};

packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { RecoilRoot, useRecoilState } from 'recoil';
33

44
import { useOpenSpreadsheetImportDialog } from '@/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog';
55
import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spreadsheetImportDialogState';
6-
import { StepType } from '@/spreadsheet-import/steps/components/UploadFlow';
6+
import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType';
77
import {
88
ImportedRow,
99
SpreadsheetImportDialogOptions,
@@ -38,7 +38,7 @@ export const mockedSpreadsheetOptions: SpreadsheetImportDialogOptions<Spreadshee
3838
autoMapHeaders: true,
3939
autoMapDistance: 1,
4040
initialStepState: {
41-
type: StepType.upload,
41+
type: SpreadsheetImportStepType.upload,
4242
},
4343
dateFormat: 'MM/DD/YY',
4444
parseRaw: true,

0 commit comments

Comments
 (0)