Refactor and fixes dropdown bugs (#8807)

Fixes https://github.com/twentyhq/twenty/issues/8788
Fixes https://github.com/twentyhq/twenty/issues/8793
Fixes https://github.com/twentyhq/twenty/issues/8791
Fixes https://github.com/twentyhq/twenty/issues/8890
Fixes https://github.com/twentyhq/twenty/issues/8893

- [x] Also : 

Icon buttons under dropdown are visible without blur : 

![Capture d’écran du 2024-11-29
15-09-53](https://github.com/user-attachments/assets/f563333d-4e43-4ded-acc7-62e116004ed9)

- [x] Also : 

<img width="237" alt="image"
src="https://github.com/user-attachments/assets/e4c70936-beff-4481-89cb-0a32a36e0ee2">

- [x] Also : 

<img width="335" alt="image"
src="https://github.com/user-attachments/assets/5be60395-6baf-49eb-8d40-197add049e20">

- [x] Also : 

<img width="287" alt="image"
src="https://github.com/user-attachments/assets/a317561f-7986-4d70-a1c0-deee4f4e268a">

- Button create new without padding
- Container is expanding

- [x] Also : 

<img width="303" alt="image"
src="https://github.com/user-attachments/assets/09f8a27f-91db-4191-acdc-aaaeedaf6da5">

- [x] Also : 

<img width="133" alt="image"
src="https://github.com/user-attachments/assets/fe17b32e-f7a4-46c4-8040-239eaf8198e8">

Font is cut at bottom ?

- [x] Also : 

<img width="385" alt="image"
src="https://github.com/user-attachments/assets/7bab2092-2936-4112-a2ee-d32d6737e304">

The component should flip and not resize in this situation

- [x] Also : 

<img width="244" alt="image"
src="https://github.com/user-attachments/assets/5384f49a-71f9-4638-a60c-158cc8c83f81">

- [x] Also : 


![image](https://github.com/user-attachments/assets/9cd1f43a-df59-401e-9a41-bdb8e93ebe58)
This commit is contained in:
Lucas Bordeau
2024-12-06 15:27:48 +01:00
committed by GitHub
parent 14b7bcf262
commit a9cb20f317
87 changed files with 1201 additions and 1192 deletions

View File

@ -16,7 +16,6 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
@ -167,43 +166,41 @@ export const ObjectFilterDropdownFilterSelect = ({
setObjectFilterDropdownSearchInput(event.target.value)
}
/>
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<SelectableList
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
selectableItemIdArray={selectableListItemIds}
selectableListId={OBJECT_FILTER_DROPDOWN_ID}
onEnter={handleEnter}
>
<DropdownMenuItemsContainer>
{visibleColumnsFilterDefinitions.map(
(visibleFilterDefinition, index) => (
<SelectableItem
itemId={visibleFilterDefinition.fieldMetadataId}
key={`visible-select-filter-${index}`}
>
<ObjectFilterDropdownFilterSelectMenuItem
filterDefinition={visibleFilterDefinition}
/>
</SelectableItem>
),
)}
{shoudShowSeparator && <DropdownMenuSeparator />}
{hiddenColumnsFilterDefinitions.map(
(hiddenFilterDefinition, index) => (
<SelectableItem
itemId={hiddenFilterDefinition.fieldMetadataId}
key={`hidden-select-filter-${index}`}
>
<ObjectFilterDropdownFilterSelectMenuItem
filterDefinition={hiddenFilterDefinition}
/>
</SelectableItem>
),
)}
</DropdownMenuItemsContainer>
</SelectableList>
{shouldShowAdvancedFilterButton && <AdvancedFilterButton />}
</ScrollWrapper>
<SelectableList
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
selectableItemIdArray={selectableListItemIds}
selectableListId={OBJECT_FILTER_DROPDOWN_ID}
onEnter={handleEnter}
>
<DropdownMenuItemsContainer>
{visibleColumnsFilterDefinitions.map(
(visibleFilterDefinition, index) => (
<SelectableItem
itemId={visibleFilterDefinition.fieldMetadataId}
key={`visible-select-filter-${index}`}
>
<ObjectFilterDropdownFilterSelectMenuItem
filterDefinition={visibleFilterDefinition}
/>
</SelectableItem>
),
)}
{shoudShowSeparator && <DropdownMenuSeparator />}
{hiddenColumnsFilterDefinitions.map(
(hiddenFilterDefinition, index) => (
<SelectableItem
itemId={hiddenFilterDefinition.fieldMetadataId}
key={`hidden-select-filter-${index}`}
>
<ObjectFilterDropdownFilterSelectMenuItem
filterDefinition={hiddenFilterDefinition}
/>
</SelectableItem>
),
)}
</DropdownMenuItemsContainer>
</SelectableList>
{shouldShowAdvancedFilterButton && <AdvancedFilterButton />}
</>
);
};

View File

@ -28,9 +28,11 @@ export const ObjectFilterDropdownOperandDropdown = ({
const theme = useTheme();
const dropdownId = `${filterDropdownId}-operand-dropdown`;
return (
<Dropdown
dropdownId={`${filterDropdownId}-operand-dropdown`}
dropdownId={dropdownId}
clickableComponent={
<StyledDropdownMenuHeader
key={'selected-filter-operand'}

View File

@ -15,7 +15,6 @@ import { useSelectableListStates } from '@/ui/layout/selectable-list/hooks/inter
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { MenuItem, MenuItemMultiSelect } from 'twenty-ui';
import { isDefined } from '~/utils/isDefined';
@ -163,23 +162,21 @@ export const ObjectFilterDropdownOptionSelect = () => {
}
}}
>
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<DropdownMenuItemsContainer hasMaxHeight>
{optionsInDropdown?.map((option) => (
<MenuItemMultiSelect
key={option.id}
selected={option.isSelected}
isKeySelected={option.id === selectedItemId}
onSelectChange={(selected) =>
handleMultipleOptionSelectChange(option, selected)
}
text={option.label}
color={option.color}
className=""
/>
))}
</DropdownMenuItemsContainer>
</ScrollWrapper>
<DropdownMenuItemsContainer hasMaxHeight>
{optionsInDropdown?.map((option) => (
<MenuItemMultiSelect
key={option.id}
selected={option.isSelected}
isKeySelected={option.id === selectedItemId}
onSelectChange={(selected) =>
handleMultipleOptionSelectChange(option, selected)
}
text={option.label}
color={option.color}
className=""
/>
))}
</DropdownMenuItemsContainer>
{showNoResult && <MenuItem text="No result" />}
</SelectableList>
);

View File

@ -11,7 +11,10 @@ import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-sta
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { within } from '@storybook/test';
import { ComponentDecorator } from 'twenty-ui';
import {
ComponentDecorator,
getCanvasElementForDropdownTesting,
} from 'twenty-ui';
import { FieldMetadataType } from '~/generated/graphql';
import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
@ -120,7 +123,7 @@ type Story = StoryObj<typeof TaskGroups>;
export const Default: Story = {
play: async () => {
const canvas = within(document.body);
const canvas = within(getCanvasElementForDropdownTesting());
const filterButton = await canvas.findByText('Filter');
@ -142,7 +145,7 @@ export const Default: Story = {
export const Date: Story = {
play: async () => {
const canvas = within(document.body);
const canvas = within(getCanvasElementForDropdownTesting());
const filterButton = await canvas.findByText('Filter');
@ -156,7 +159,7 @@ export const Date: Story = {
export const Number: Story = {
play: async () => {
const canvas = within(document.body);
const canvas = within(getCanvasElementForDropdownTesting());
const filterButton = await canvas.findByText('Filter');

View File

@ -6,7 +6,6 @@ import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hook
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection';
import { ViewType } from '@/views/types/ViewType';
@ -53,19 +52,17 @@ export const ObjectOptionsDropdownFieldsContent = () => {
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetContent}>
Fields
</DropdownMenuHeader>
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<ViewFieldsVisibilityDropdownSection
title="Visible"
fields={visibleRecordFields}
isDraggable
onDragEnd={handleReorderFields}
onVisibilityChange={handleChangeFieldVisibility}
showSubheader={false}
showDragGrip={true}
/>
</ScrollWrapper>
<ViewFieldsVisibilityDropdownSection
title="Visible"
fields={visibleRecordFields}
isDraggable
onDragEnd={handleReorderFields}
onVisibilityChange={handleChangeFieldVisibility}
showSubheader={false}
showDragGrip={true}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<DropdownMenuItemsContainer withoutScrollWrapper>
<MenuItemNavigate
onClick={() => onContentChange('hiddenFields')}
LeftIcon={IconEyeOff}

View File

@ -18,7 +18,6 @@ import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenu
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection';
import { ViewType } from '@/views/types/ViewType';
@ -71,19 +70,16 @@ export const ObjectOptionsDropdownHiddenFieldsContent = () => {
Hidden Fields
</DropdownMenuHeader>
{hiddenRecordFields.length > 0 && (
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<ViewFieldsVisibilityDropdownSection
title="Hidden"
fields={hiddenRecordFields}
isDraggable={false}
onVisibilityChange={handleChangeFieldVisibility}
showSubheader={false}
showDragGrip={false}
/>
</ScrollWrapper>
<ViewFieldsVisibilityDropdownSection
title="Hidden"
fields={hiddenRecordFields}
isDraggable={false}
onVisibilityChange={handleChangeFieldVisibility}
showSubheader={false}
showDragGrip={false}
/>
)}
<DropdownMenuSeparator />
<UndecoratedLink
to={settingsUrl}
onClick={() => {
@ -91,7 +87,7 @@ export const ObjectOptionsDropdownHiddenFieldsContent = () => {
closeDropdown();
}}
>
<DropdownMenuItemsContainer>
<DropdownMenuItemsContainer withoutScrollWrapper>
<MenuItem LeftIcon={IconSettings} text="Edit Fields" />
</DropdownMenuItemsContainer>
</UndecoratedLink>

View File

@ -15,7 +15,6 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useContext } from 'react';
import { SORT_DIRECTIONS } from '../types/SortDirection';
@ -184,39 +183,37 @@ export const ObjectSortDropdownButton = ({
setObjectSortDropdownSearchInput(event.target.value)
}
/>
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<DropdownMenuItemsContainer>
{visibleColumnsSortDefinitions.map(
(visibleSortDefinition, index) => (
<MenuItem
testId={`visible-select-sort-${index}`}
key={index}
onClick={() => {
setObjectSortDropdownSearchInput('');
handleAddSort(visibleSortDefinition);
}}
LeftIcon={getIcon(visibleSortDefinition.iconName)}
text={visibleSortDefinition.label}
/>
),
)}
{shoudShowSeparator && <DropdownMenuSeparator />}
{hiddenColumnsSortDefinitions.map(
(hiddenSortDefinition, index) => (
<MenuItem
testId={`hidden-select-sort-${index}`}
key={index}
onClick={() => {
setObjectSortDropdownSearchInput('');
handleAddSort(hiddenSortDefinition);
}}
LeftIcon={getIcon(hiddenSortDefinition.iconName)}
text={hiddenSortDefinition.label}
/>
),
)}
</DropdownMenuItemsContainer>
</ScrollWrapper>
<DropdownMenuItemsContainer>
{visibleColumnsSortDefinitions.map(
(visibleSortDefinition, index) => (
<MenuItem
testId={`visible-select-sort-${index}`}
key={index}
onClick={() => {
setObjectSortDropdownSearchInput('');
handleAddSort(visibleSortDefinition);
}}
LeftIcon={getIcon(visibleSortDefinition.iconName)}
text={visibleSortDefinition.label}
/>
),
)}
{shoudShowSeparator && <DropdownMenuSeparator />}
{hiddenColumnsSortDefinitions.map(
(hiddenSortDefinition, index) => (
<MenuItem
testId={`hidden-select-sort-${index}`}
key={index}
onClick={() => {
setObjectSortDropdownSearchInput('');
handleAddSort(hiddenSortDefinition);
}}
LeftIcon={getIcon(hiddenSortDefinition.iconName)}
text={hiddenSortDefinition.label}
/>
),
)}
</DropdownMenuItemsContainer>
</>
}
onClose={handleDropdownButtonClose}

View File

@ -22,7 +22,7 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutsideV2 } from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
@ -81,7 +81,7 @@ export const RecordBoard = () => {
const { resetRecordSelection, setRecordAsSelected } =
useRecordBoardSelection(recordBoardId);
useListenClickOutsideV2({
useListenClickOutside({
excludeClassNames: [
'bottom-bar',
'action-menu-dropdown',

View File

@ -34,6 +34,7 @@ export const RecordBoardColumnDropdownMenu = ({
useListenClickOutside({
refs: [boardColumnMenuRef],
callback: closeMenu,
listenerId: 'record-board-column-dropdown-menu',
});
return (

View File

@ -48,7 +48,10 @@ type FieldInputProps = {
recordFieldInputdId: string;
onSubmit?: FieldInputEvent;
onCancel?: () => void;
onClickOutside?: FieldInputEvent;
onClickOutside?: (
persist: () => void,
event: MouseEvent | TouchEvent,
) => void;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
@ -78,7 +81,10 @@ export const FieldInput = ({
) : isFieldRelationFromManyObjects(fieldDefinition) ? (
<RelationFromManyFieldInput onSubmit={onSubmit} />
) : isFieldPhones(fieldDefinition) ? (
<PhonesFieldInput onCancel={onCancel} />
<PhonesFieldInput
onCancel={onCancel}
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
/>
) : isFieldText(fieldDefinition) ? (
<TextFieldInput
onEnter={onEnter}
@ -88,7 +94,10 @@ export const FieldInput = ({
onShiftTab={onShiftTab}
/>
) : isFieldEmails(fieldDefinition) ? (
<EmailsFieldInput onCancel={onCancel} />
<EmailsFieldInput
onCancel={onCancel}
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
/>
) : isFieldFullName(fieldDefinition) ? (
<FullNameFieldInput
onEnter={onEnter}
@ -122,7 +131,10 @@ export const FieldInput = ({
onShiftTab={onShiftTab}
/>
) : isFieldLinks(fieldDefinition) ? (
<LinksFieldInput onCancel={onCancel} />
<LinksFieldInput
onCancel={onCancel}
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
/>
) : isFieldCurrency(fieldDefinition) ? (
<CurrencyFieldInput
onEnter={onEnter}
@ -158,7 +170,10 @@ export const FieldInput = ({
) : isFieldRichText(fieldDefinition) ? (
<RichTextFieldInput />
) : isFieldArray(fieldDefinition) ? (
<ArrayFieldInput onCancel={onCancel} />
<ArrayFieldInput
onCancel={onCancel}
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
/>
) : (
<></>
)}

View File

@ -108,9 +108,6 @@ export const FormSelectFieldInput = ({
goBackToPreviousHotkeyScope();
};
const [selectWrapperRef, setSelectWrapperRef] =
useState<HTMLDivElement | null>(null);
const [filteredOptions, setFilteredOptions] = useState<SelectOption[]>([]);
const { resetSelectedItem } = useSelectableList(
@ -206,7 +203,6 @@ export const FormSelectFieldInput = ({
<StyledFormFieldInputRowContainer>
<StyledFormFieldInputInputContainer
ref={setSelectWrapperRef}
hasRightElement={isDefined(VariablePicker)}
>
{draftValue.type === 'static' ? (
@ -231,7 +227,6 @@ export const FormSelectFieldInput = ({
selectableItemIdArray={optionIds}
hotkeyScope={hotkeyScope}
onEnter={handleSelectEnter}
selectWrapperRef={selectWrapperRef}
onOptionSelected={handleSubmit}
options={field.metadata.options}
onCancel={onCancel}

View File

@ -4,10 +4,13 @@ import { AddressInput } from '@/ui/field/input/components/AddressInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { FieldInputEvent } from './DateTimeFieldInput';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from './DateTimeFieldInput';
export type AddressFieldInputProps = {
onClickOutside?: FieldInputEvent;
onClickOutside?: FieldInputClickOutsideEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
@ -60,7 +63,7 @@ export const AddressFieldInput = ({
event: MouseEvent | TouchEvent,
newAddress: FieldAddressDraftValue,
) => {
onClickOutside?.(() => persistField(convertToAddress(newAddress)));
onClickOutside?.(() => persistField(convertToAddress(newAddress)), event);
};
const handleChange = (newAddress: FieldAddressDraftValue) => {

View File

@ -6,9 +6,13 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
type ArrayFieldInputProps = {
onCancel?: () => void;
onClickOutside?: (event: MouseEvent | TouchEvent) => void;
};
export const ArrayFieldInput = ({ onCancel }: ArrayFieldInputProps) => {
export const ArrayFieldInput = ({
onCancel,
onClickOutside,
}: ArrayFieldInputProps) => {
const { persistArrayField, hotkeyScope, fieldValue } = useArrayField();
const arrayItems = useMemo<Array<string>>(
@ -23,6 +27,7 @@ export const ArrayFieldInput = ({ onCancel }: ArrayFieldInputProps) => {
items={arrayItems}
onPersist={persistArrayField}
onCancel={onCancel}
onClickOutside={onClickOutside}
placeholder="Enter value"
fieldMetadataType={FieldMetadataType.Array}
renderItem={({ value, index, handleEdit, handleDelete }) => (

View File

@ -8,10 +8,13 @@ import { FieldInputOverlay } from '../../../../../ui/field/input/components/Fiel
import { useCurrencyField } from '../../hooks/useCurrencyField';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { FieldInputEvent } from './DateTimeFieldInput';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from './DateTimeFieldInput';
type CurrencyFieldInputProps = {
onClickOutside?: FieldInputEvent;
onClickOutside?: FieldInputClickOutsideEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
@ -79,7 +82,7 @@ export const CurrencyFieldInput = ({
amountText: newValue,
currencyCode,
});
});
}, event);
};
const handleTab = (newValue: string) => {

View File

@ -5,11 +5,12 @@ import { DateInput } from '@/ui/field/input/components/DateInput';
import { isDefined } from '~/utils/isDefined';
import { usePersistField } from '../../../hooks/usePersistField';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
type FieldInputEvent = (persist: () => void) => void;
type DateFieldInputProps = {
onClickOutside?: FieldInputEvent;
onClickOutside?: FieldInputClickOutsideEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onClear?: FieldInputEvent;
@ -50,10 +51,10 @@ export const DateFieldInput = ({
};
const handleClickOutside = (
_event: MouseEvent | TouchEvent,
event: MouseEvent | TouchEvent,
newDate: Nullable<Date>,
) => {
onClickOutside?.(() => persistDate(newDate));
onClickOutside?.(() => persistDate(newDate), event);
};
const handleChange = (newDate: Nullable<Date>) => {

View File

@ -6,9 +6,13 @@ import { usePersistField } from '../../../hooks/usePersistField';
import { useDateTimeField } from '../../hooks/useDateTimeField';
export type FieldInputEvent = (persist: () => void) => void;
export type FieldInputClickOutsideEvent = (
persist: () => void,
event: MouseEvent | TouchEvent,
) => void;
export type DateTimeFieldInputProps = {
onClickOutside?: FieldInputEvent;
onClickOutside?: FieldInputClickOutsideEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onClear?: FieldInputEvent;
@ -45,10 +49,10 @@ export const DateTimeFieldInput = ({
};
const handleClickOutside = (
_event: MouseEvent | TouchEvent,
event: MouseEvent | TouchEvent,
newDate: Nullable<Date>,
) => {
onClickOutside?.(() => persistDate(newDate));
onClickOutside?.(() => persistDate(newDate), event);
};
const handleChange = (newDate: Nullable<Date>) => {

View File

@ -8,9 +8,13 @@ import { MultiItemFieldInput } from './MultiItemFieldInput';
type EmailsFieldInputProps = {
onCancel?: () => void;
onClickOutside?: (event: MouseEvent | TouchEvent) => void;
};
export const EmailsFieldInput = ({ onCancel }: EmailsFieldInputProps) => {
export const EmailsFieldInput = ({
onCancel,
onClickOutside,
}: EmailsFieldInputProps) => {
const { persistEmailsField, hotkeyScope, fieldValue } = useEmailsField();
const emails = useMemo<string[]>(
@ -45,6 +49,7 @@ export const EmailsFieldInput = ({ onCancel }: EmailsFieldInputProps) => {
items={emails}
onPersist={handlePersistEmails}
onCancel={onCancel}
onClickOutside={onClickOutside}
placeholder="Email"
fieldMetadataType={FieldMetadataType.Emails}
validateInput={validateInput}

View File

@ -4,7 +4,10 @@ import { DoubleTextInput } from '@/ui/field/input/components/DoubleTextInput';
import { FieldInputOverlay } from '@/ui/field/input/components/FieldInputOverlay';
import { isDoubleTextFieldEmpty } from '@/object-record/record-field/meta-types/input/utils/isDoubleTextFieldEmpty';
import { FieldInputEvent } from './DateTimeFieldInput';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from './DateTimeFieldInput';
const FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS =
'First name';
@ -13,7 +16,7 @@ const LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS =
'Last name';
type FullNameFieldInputProps = {
onClickOutside?: FieldInputEvent;
onClickOutside?: FieldInputClickOutsideEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
@ -57,8 +60,9 @@ export const FullNameFieldInput = ({
event: MouseEvent | TouchEvent,
newDoubleText: FieldDoubleText,
) => {
onClickOutside?.(() =>
persistFullNameField(convertToFullName(newDoubleText)),
onClickOutside?.(
() => persistFullNameField(convertToFullName(newDoubleText)),
event,
);
};

View File

@ -8,9 +8,13 @@ import { MultiItemFieldInput } from './MultiItemFieldInput';
type LinksFieldInputProps = {
onCancel?: () => void;
onClickOutside?: (event: MouseEvent | TouchEvent) => void;
};
export const LinksFieldInput = ({ onCancel }: LinksFieldInputProps) => {
export const LinksFieldInput = ({
onCancel,
onClickOutside,
}: LinksFieldInputProps) => {
const { persistLinksField, hotkeyScope, fieldValue } = useLinksField();
const links = useMemo<{ url: string; label: string }[]>(
@ -49,6 +53,7 @@ export const LinksFieldInput = ({ onCancel }: LinksFieldInputProps) => {
items={links}
onPersist={handlePersistLinks}
onCancel={onCancel}
onClickOutside={onClickOutside}
placeholder="URL"
fieldMetadataType={FieldMetadataType.Links}
validateInput={(input) => ({

View File

@ -41,6 +41,7 @@ type MultiItemFieldInputProps<T> = {
newItemLabel?: string;
fieldMetadataType: FieldMetadataType;
renderInput?: DropdownMenuInputProps['renderInput'];
onClickOutside?: (event: MouseEvent | TouchEvent) => void;
};
// Todo: the API of this component does not look healthy: we have renderInput, renderItem, formatInput, ...
@ -57,24 +58,19 @@ export const MultiItemFieldInput = <T,>({
newItemLabel,
fieldMetadataType,
renderInput,
onClickOutside,
}: MultiItemFieldInputProps<T>) => {
const containerRef = useRef<HTMLDivElement>(null);
const handleDropdownClose = () => {
onCancel?.();
};
const handleDropdownCloseOutside = (event: MouseEvent | TouchEvent) => {
event.stopImmediatePropagation();
if (inputValue?.trim().length > 0) {
handleSubmitInput();
} else {
onCancel?.();
}
};
useListenClickOutside({
refs: [containerRef],
callback: handleDropdownCloseOutside,
callback: (event) => {
onClickOutside?.(event);
},
listenerId: hotkeyScope,
});
useScopedHotkeys(Key.Escape, handleDropdownClose, hotkeyScope);

View File

@ -1,7 +1,7 @@
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItemWithOptionDropdown } from '@/ui/navigation/menu-item/components/MenuItemWithOptionDropdown';
import { useState } from 'react';
import React, { useState } from 'react';
import {
IconBookmark,
IconBookmarkPlus,
@ -37,23 +37,29 @@ export const MultiItemFieldMenuItem = <T,>({
const handleMouseEnter = () => setIsHovered(true);
const handleMouseLeave = () => {
setIsHovered(false);
if (isDropdownOpen) {
closeDropdown();
}
};
const handleDeleteClick = () => {
const handleDeleteClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
event.preventDefault();
closeDropdown();
setIsHovered(false);
onDelete?.();
};
const handleSetAsPrimaryClick = () => {
const handleSetAsPrimaryClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
event.preventDefault();
closeDropdown();
onSetAsPrimary?.();
};
const handleEditClick = () => {
const handleEditClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
event.preventDefault();
closeDropdown();
onEdit?.();
};

View File

@ -88,6 +88,7 @@ export const MultiSelectFieldInput = ({
}
resetSelectedItem();
},
listenerId: 'MultiSelectFieldInput',
});
const optionIds = filteredOptionsInDropDown.map((option) => option.value);

View File

@ -2,11 +2,12 @@ import { TextInput } from '@/ui/field/input/components/TextInput';
import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay';
import { useNumberField } from '../../hooks/useNumberField';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
export type FieldInputEvent = (persist: () => void) => void;
export type NumberFieldInputProps = {
onClickOutside?: FieldInputEvent;
onClickOutside?: FieldInputClickOutsideEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
@ -40,7 +41,7 @@ export const NumberFieldInput = ({
event: MouseEvent | TouchEvent,
newText: string,
) => {
onClickOutside?.(() => persistNumberField(newText));
onClickOutside?.(() => persistNumberField(newText), event);
};
const handleTab = (newText: string) => {

View File

@ -49,10 +49,14 @@ const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)`
type PhonesFieldInputProps = {
onCancel?: () => void;
onClickOutside?: (event: MouseEvent | TouchEvent) => void;
};
export const PhonesFieldInput = ({ onCancel }: PhonesFieldInputProps) => {
const { persistPhonesField, hotkeyScope, fieldDefinition, fieldValue } =
export const PhonesFieldInput = ({
onCancel,
onClickOutside,
}: PhonesFieldInputProps) => {
const { persistPhonesField, hotkeyScope, fieldValue, fieldDefinition } =
usePhonesField();
const phones = createPhonesFromFieldValue(fieldValue);
@ -83,6 +87,7 @@ export const PhonesFieldInput = ({ onCancel }: PhonesFieldInputProps) => {
<MultiItemFieldInput
items={phones}
onPersist={handlePersistPhones}
onClickOutside={onClickOutside}
onCancel={onCancel}
placeholder="Phone"
fieldMetadataType={FieldMetadataType.Phones}

View File

@ -3,10 +3,13 @@ import { TextAreaInput } from '@/ui/field/input/components/TextAreaInput';
import { useJsonField } from '../../hooks/useJsonField';
import { FieldInputEvent } from './DateTimeFieldInput';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from './DateTimeFieldInput';
type RawJsonFieldInputProps = {
onClickOutside?: FieldInputEvent;
onClickOutside?: FieldInputClickOutsideEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
@ -37,10 +40,10 @@ export const RawJsonFieldInput = ({
};
const handleClickOutside = (
_event: MouseEvent | TouchEvent,
event: MouseEvent | TouchEvent,
newText: string,
) => {
onClickOutside?.(() => persistJsonField(newText));
onClickOutside?.(() => persistJsonField(newText), event);
};
const handleTab = (newText: string) => {

View File

@ -21,8 +21,6 @@ export const SelectFieldInput = ({
}: SelectFieldInputProps) => {
const { persistField, fieldDefinition, fieldValue, hotkeyScope } =
useSelectField();
const [selectWrapperRef, setSelectWrapperRef] =
useState<HTMLDivElement | null>(null);
const [filteredOptions, setFilteredOptions] = useState<SelectOption[]>([]);
@ -62,7 +60,7 @@ export const SelectFieldInput = ({
];
return (
<div ref={setSelectWrapperRef}>
<div>
<SelectInput
selectableListId={SINGLE_RECORD_SELECT_BASE_LIST}
selectableItemIdArray={optionIds}
@ -76,7 +74,6 @@ export const SelectFieldInput = ({
resetSelectedItem();
}
}}
selectWrapperRef={selectWrapperRef}
onOptionSelected={handleSubmit}
options={fieldDefinition.metadata.options}
onCancel={onCancel}

View File

@ -5,10 +5,13 @@ import { usePersistField } from '../../../hooks/usePersistField';
import { useTextField } from '../../hooks/useTextField';
import { turnIntoUndefinedIfWhitespacesOnly } from '~/utils/string/turnIntoUndefinedIfWhitespacesOnly';
import { FieldInputEvent } from './DateTimeFieldInput';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from './DateTimeFieldInput';
export type TextFieldInputProps = {
onClickOutside?: FieldInputEvent;
onClickOutside?: FieldInputClickOutsideEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
@ -38,7 +41,7 @@ export const TextFieldInput = ({
event: MouseEvent | TouchEvent,
newText: string,
) => {
onClickOutside?.(() => persistField(newText.trim()));
onClickOutside?.(() => persistField(newText.trim()), event);
};
const handleTab = (newText: string) => {

View File

@ -1,12 +1,5 @@
import { Decorator, Meta, StoryObj } from '@storybook/react';
import {
expect,
fireEvent,
fn,
userEvent,
waitFor,
within,
} from '@storybook/test';
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
import { useEffect } from 'react';
import { useSetRecoilState } from 'recoil';
@ -26,6 +19,7 @@ import {
import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider';
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
import { getCanvasElementForDropdownTesting } from 'twenty-ui';
import {
RelationToOneFieldInput,
RelationToOneFieldInputProps,
@ -136,8 +130,8 @@ export const Default: Story = {
export const Submit: Story = {
decorators: [ComponentWithRecoilScopeDecorator],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
play: async () => {
const canvas = within(getCanvasElementForDropdownTesting());
expect(submitJestFn).toHaveBeenCalledTimes(0);
@ -154,16 +148,16 @@ export const Submit: Story = {
export const Cancel: Story = {
decorators: [ComponentWithRecoilScopeDecorator],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
play: async () => {
const canvas = within(getCanvasElementForDropdownTesting());
expect(cancelJestFn).toHaveBeenCalledTimes(0);
await canvas.findByText('John Wick', undefined, { timeout: 3000 });
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
fireEvent.click(emptyDiv);
await waitFor(() => {
userEvent.click(emptyDiv);
expect(cancelJestFn).toHaveBeenCalledTimes(1);
});
},

View File

@ -28,10 +28,10 @@ export const useRegisterInputEvents = <T>({
useListenClickOutside({
refs: [inputRef, copyRef].filter(isDefined),
callback: (event) => {
event.stopImmediatePropagation();
onClickOutside?.(event, inputValue);
},
enabled: isDefined(onClickOutside),
listenerId: hotkeyScope,
});
useScopedHotkeys(

View File

@ -14,7 +14,11 @@ import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types
import { useInlineCell } from '../hooks/useInlineCell';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
import { useRecoilCallback } from 'recoil';
import { RecordInlineCellContainer } from './RecordInlineCellContainer';
import {
RecordInlineCellContext,
@ -65,10 +69,30 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
closeInlineCell();
};
const handleClickOutside: FieldInputEvent = (persistField) => {
persistField();
closeInlineCell();
};
const handleClickOutside: FieldInputClickOutsideEvent = useRecoilCallback(
({ snapshot }) =>
(persistField, event) => {
const recordFieldDropdownId = getDropdownFocusIdForRecordField(
recordId,
fieldDefinition.fieldMetadataId,
'inline-cell',
);
const activeDropdownFocusId = snapshot
.getLoadable(activeDropdownFocusIdState)
.getValue();
if (recordFieldDropdownId !== activeDropdownFocusId) {
return;
}
event.stopImmediatePropagation();
persistField();
closeInlineCell();
},
[closeInlineCell, fieldDefinition.fieldMetadataId, recordId],
);
const { getIcon } = useIcons();

View File

@ -18,11 +18,10 @@ const StyledInlineCellInput = styled.div`
display: flex;
min-height: 32px;
min-width: 240px;
width: inherit;
width: 240px;
z-index: 1000;
z-index: 30;
`;
type RecordInlineCellEditModeProps = {

View File

@ -7,6 +7,9 @@ import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { isDefined } from '~/utils/isDefined';
import { useInitDraftValueV2 } from '@/object-record/record-field/hooks/useInitDraftValueV2';
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { isInlineCellInEditModeScopedState } from '../states/isInlineCellInEditModeScopedState';
import { InlineCellHotkeyScope } from '../types/InlineCellHotkeyScope';
@ -21,6 +24,11 @@ export const useInlineCell = () => {
isInlineCellInEditModeScopedState(recoilScopeId),
);
const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious();
const { goBackToPreviousDropdownFocusId } =
useGoBackToPreviousDropdownFocusId();
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
@ -32,6 +40,8 @@ export const useInlineCell = () => {
setIsInlineCellInEditMode(false);
goBackToPreviousHotkeyScope();
goBackToPreviousDropdownFocusId();
};
const openInlineCell = (customEditHotkeyScopeForField?: HotkeyScope) => {
@ -46,6 +56,14 @@ export const useInlineCell = () => {
} else {
setHotkeyScopeAndMemorizePreviousScope(InlineCellHotkeyScope.InlineCell);
}
setActiveDropdownFocusIdAndMemorizePrevious(
getDropdownFocusIdForRecordField(
recordId,
fieldDefinition.fieldMetadataId,
'inline-cell',
),
);
};
return {

View File

@ -2,6 +2,7 @@ import { useRecoilCallback } from 'recoil';
import { currentTableCellInEditModePositionComponentState } from '@/object-record/record-table/states/currentTableCellInEditModePositionComponentState';
import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState';
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
@ -16,6 +17,9 @@ export const useCloseCurrentTableCellInEditMode = (recordTableId?: string) => {
recordTableId,
);
const { goBackToPreviousDropdownFocusId } =
useGoBackToPreviousDropdownFocusId();
return useRecoilCallback(
({ set, snapshot }) => {
return async () => {
@ -28,8 +32,14 @@ export const useCloseCurrentTableCellInEditMode = (recordTableId?: string) => {
isTableCellInEditModeFamilyState(currentTableCellInEditModePosition),
false,
);
goBackToPreviousDropdownFocusId();
};
},
[currentTableCellInEditModePositionState, isTableCellInEditModeFamilyState],
[
currentTableCellInEditModePositionState,
isTableCellInEditModeFamilyState,
goBackToPreviousDropdownFocusId,
],
);
};

View File

@ -5,7 +5,7 @@ import { useLeaveTableFocus } from '@/object-record/record-table/hooks/internal/
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutsideV2 } from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
type RecordTableBodyUnselectEffectProps = {
tableBodyRef: React.RefObject<HTMLDivElement>;
@ -32,7 +32,7 @@ export const RecordTableBodyUnselectEffect = ({
TableHotkeyScope.Table,
);
useListenClickOutsideV2({
useListenClickOutside({
excludeClassNames: [
'bottom-bar',
'action-menu-dropdown',

View File

@ -4,19 +4,13 @@ import { FieldInput } from '@/object-record/record-field/components/FieldInput';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
import { useSetRecoilState } from 'recoil';
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
import { useRecoilCallback } from 'recoil';
export const RecordTableCellFieldInput = () => {
const { getClickOutsideListenerIsActivatedState } =
useClickOustideListenerStates(RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID);
const setClickOutsideListenerIsActivated = useSetRecoilState(
getClickOutsideListenerIsActivatedState,
);
const { onUpsertRecord, onMoveFocus, onCloseTableCell } =
useContext(RecordTableContext);
@ -48,17 +42,35 @@ export const RecordTableCellFieldInput = () => {
onCloseTableCell();
};
const handleClickOutside: FieldInputEvent = (persistField) => {
setClickOutsideListenerIsActivated(false);
const handleClickOutside = useRecoilCallback(
({ snapshot }) =>
(persistField: () => void, event: MouseEvent | TouchEvent) => {
const dropdownFocusId = getDropdownFocusIdForRecordField(
recordId,
fieldDefinition.fieldMetadataId,
'table-cell',
);
onUpsertRecord({
persistField,
recordId,
fieldName: fieldDefinition.metadata.fieldName,
});
const activeDropdownFocusId = snapshot
.getLoadable(activeDropdownFocusIdState)
.getValue();
onCloseTableCell();
};
if (activeDropdownFocusId !== dropdownFocusId) {
return;
}
event.stopImmediatePropagation();
onUpsertRecord({
persistField,
recordId,
fieldName: fieldDefinition.metadata.fieldName,
});
onCloseTableCell();
},
[fieldDefinition, onCloseTableCell, onUpsertRecord, recordId],
);
const handleEscape: FieldInputEvent = (persistField) => {
onUpsertRecord({

View File

@ -10,13 +10,11 @@ import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEm
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
import { useToggleEditOnlyInput } from '@/object-record/record-field/hooks/useToggleEditOnlyInput';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { useCloseCurrentTableCellInEditMode } from '@/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode';
import { RecordTableCellButton } from '@/object-record/record-table/record-table-cell/components/RecordTableCellButton';
import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell';
import { isSoftFocusUsingMouseState } from '@/object-record/record-table/states/isSoftFocusUsingMouseState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isDefined } from '~/utils/isDefined';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
@ -34,7 +32,6 @@ export const RecordTableCellSoftFocusMode = ({
nonEditModeContent,
}: RecordTableCellSoftFocusModeProps) => {
const { columnIndex } = useContext(RecordTableCellContext);
const closeCurrentTableCell = useCloseCurrentTableCellInEditMode();
const isFieldReadOnly = useIsFieldValueReadOnly();
@ -135,13 +132,6 @@ export const RecordTableCellSoftFocusMode = ({
*/
};
useListenClickOutside({
refs: [scrollRef],
callback: () => {
closeCurrentTableCell();
},
});
const isFirstColumn = columnIndex === 0;
const customButtonIcon = useGetButtonIcon();
const buttonIcon = isFirstColumn

View File

@ -22,6 +22,8 @@ import { isDefined } from '~/utils/isDefined';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
import { useContext } from 'react';
import { useNavigate } from 'react-router-dom';
@ -69,6 +71,9 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
const navigate = useNavigate();
const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious();
const openTableCell = useRecoilCallback(
({ snapshot, set }) =>
({
@ -142,6 +147,14 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
DEFAULT_CELL_SCOPE.customScopes,
);
}
setActiveDropdownFocusIdAndMemorizePrevious(
getDropdownFocusIdForRecordField(
recordId,
fieldDefinition.fieldMetadataId,
'table-cell',
),
);
},
[
getClickOutsideListenerIsActivatedState,
@ -156,6 +169,7 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
setViewableRecordNameSingular,
openRightDrawer,
setHotkeyScope,
setActiveDropdownFocusIdAndMemorizePrevious,
],
);

View File

@ -15,7 +15,6 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { onToggleColumnFilterComponentState } from '@/object-record/record-table/states/onToggleColumnFilterComponentState';
import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useTableColumns } from '../../hooks/useTableColumns';
import { ColumnDefinition } from '../../types/ColumnDefinition';
@ -92,45 +91,43 @@ export const RecordTableColumnHeadDropdownMenu = ({
const canHide = column.isLabelIdentifier !== true;
return (
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<DropdownMenuItemsContainer>
{isFilterable && (
<MenuItem
LeftIcon={IconFilter}
onClick={handleFilterClick}
text="Filter"
/>
)}
{isSortable && (
<MenuItem
LeftIcon={IconSortDescending}
onClick={handleSortClick}
text="Sort"
/>
)}
{showSeparator && <DropdownMenuSeparator />}
{canMoveLeft && (
<MenuItem
LeftIcon={IconArrowLeft}
onClick={handleColumnMoveLeft}
text="Move left"
/>
)}
{canMoveRight && (
<MenuItem
LeftIcon={IconArrowRight}
onClick={handleColumnMoveRight}
text="Move right"
/>
)}
{canHide && (
<MenuItem
LeftIcon={IconEyeOff}
onClick={handleColumnVisibility}
text="Hide"
/>
)}
</DropdownMenuItemsContainer>
</ScrollWrapper>
<DropdownMenuItemsContainer>
{isFilterable && (
<MenuItem
LeftIcon={IconFilter}
onClick={handleFilterClick}
text="Filter"
/>
)}
{isSortable && (
<MenuItem
LeftIcon={IconSortDescending}
onClick={handleSortClick}
text="Sort"
/>
)}
{showSeparator && <DropdownMenuSeparator />}
{canMoveLeft && (
<MenuItem
LeftIcon={IconArrowLeft}
onClick={handleColumnMoveLeft}
text="Move left"
/>
)}
{canMoveRight && (
<MenuItem
LeftIcon={IconArrowRight}
onClick={handleColumnMoveRight}
text="Move right"
/>
)}
{canHide && (
<MenuItem
LeftIcon={IconEyeOff}
onClick={handleColumnVisibility}
text="Hide"
/>
)}
</DropdownMenuItemsContainer>
);
};

View File

@ -13,7 +13,6 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordTableHeaderPlusButtonContent = () => {
@ -42,20 +41,18 @@ export const RecordTableHeaderPlusButtonContent = () => {
return (
<>
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<DropdownMenuItemsContainer>
{hiddenTableColumns.map((column) => (
<MenuItem
key={column.fieldMetadataId}
onClick={() => handleAddColumn(column)}
LeftIcon={getIcon(column.iconName)}
text={column.label}
/>
))}
</DropdownMenuItemsContainer>
</ScrollWrapper>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
{hiddenTableColumns.map((column) => (
<MenuItem
key={column.fieldMetadataId}
onClick={() => handleAddColumn(column)}
LeftIcon={getIcon(column.iconName)}
text={column.label}
/>
))}
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer withoutScrollWrapper>
<UndecoratedLink
fullWidth
to={`/settings/objects/${getObjectSlug(objectMetadataItem)}`}

View File

@ -179,7 +179,11 @@ export const MultiRecordSelect = ({
{objectRecordsIdsMultiSelect?.length > 0 && (
<DropdownMenuSeparator />
)}
{isDefined(onCreate) && <div>{createNewButton}</div>}
{isDefined(onCreate) && (
<DropdownMenuItemsContainer withoutScrollWrapper>
{createNewButton}
</DropdownMenuItemsContainer>
)}
</>
)}
</DropdownMenu>

View File

@ -3,6 +3,7 @@ import { useEffect } from 'react';
import { MULTI_OBJECT_RECORD_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordClickOutsideListenerId';
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
export const MultipleObjectRecordOnClickOutsideEffect = ({
containerRef,
@ -11,10 +12,6 @@ export const MultipleObjectRecordOnClickOutsideEffect = ({
containerRef: React.RefObject<HTMLDivElement>;
onClickOutside: () => void;
}) => {
const { useListenClickOutside } = useClickOutsideListener(
MULTI_OBJECT_RECORD_CLICK_OUTSIDE_LISTENER_ID,
);
const { toggleClickOutsideListener: toggleRightDrawerClickOustideListener } =
useClickOutsideListener(RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID);
@ -35,6 +32,7 @@ export const MultipleObjectRecordOnClickOutsideEffect = ({
onClickOutside();
},
listenerId: MULTI_OBJECT_RECORD_CLICK_OUTSIDE_LISTENER_ID,
});
return <></>;

View File

@ -41,6 +41,7 @@ export const SingleRecordSelect = ({
onCancel();
}
},
listenerId: 'single-record-select',
});
return (

View File

@ -59,27 +59,6 @@ export const SingleRecordSelectMenuItemsWithSearch = ({
/>
);
const results = (
<SingleRecordSelectMenuItems
recordsToSelect={records.recordsToSelect}
loading={records.loading}
selectedRecord={
records.recordsToSelect.length === 1
? records.recordsToSelect[0]
: undefined
}
shouldSelectEmptyOption={selectedRecordIds?.length === 0}
hotkeyScope={recordPickerInstanceId}
isFiltered={!!recordPickerSearchFilter}
{...{
EmptyIcon,
emptyLabel,
onCancel,
onRecordSelected,
}}
/>
);
return (
<>
{dropdownPlacement?.includes('end') && (
@ -88,7 +67,26 @@ export const SingleRecordSelectMenuItemsWithSearch = ({
{createNewButton}
</DropdownMenuItemsContainer>
{records.recordsToSelect.length > 0 && <DropdownMenuSeparator />}
{records.recordsToSelect.length > 0 && results}
{records.recordsToSelect.length > 0 && (
<SingleRecordSelectMenuItems
recordsToSelect={records.recordsToSelect}
loading={records.loading}
selectedRecord={
records.recordsToSelect.length === 1
? records.recordsToSelect[0]
: undefined
}
shouldSelectEmptyOption={selectedRecordIds?.length === 0}
hotkeyScope={recordPickerInstanceId}
isFiltered={!!recordPickerSearchFilter}
{...{
EmptyIcon,
emptyLabel,
onCancel,
onRecordSelected,
}}
/>
)}
<DropdownMenuSeparator />
</>
)}
@ -97,7 +95,26 @@ export const SingleRecordSelectMenuItemsWithSearch = ({
isUndefinedOrNull(dropdownPlacement)) && (
<>
<DropdownMenuSeparator />
{records.recordsToSelect.length > 0 && results}
{records.recordsToSelect.length > 0 && (
<SingleRecordSelectMenuItems
recordsToSelect={records.recordsToSelect}
loading={records.loading}
selectedRecord={
records.recordsToSelect.length === 1
? records.recordsToSelect[0]
: undefined
}
shouldSelectEmptyOption={selectedRecordIds?.length === 0}
hotkeyScope={recordPickerInstanceId}
isFiltered={!!recordPickerSearchFilter}
{...{
EmptyIcon,
emptyLabel,
onCancel,
onRecordSelected,
}}
/>
)}
{records.recordsToSelect.length > 0 && isDefined(onCreate) && (
<DropdownMenuSeparator />
)}

View File

@ -0,0 +1,7 @@
export const getDropdownFocusIdForRecordField = (
recordId: string,
fieldMetadataId: string,
componentType: 'table-cell' | 'inline-cell',
) => {
return `dropdown-${componentType}-record-${recordId}-field-${fieldMetadataId}`;
};