diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 386ab99f2..db66ddacb 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -1,5 +1,5 @@ -import { gql } from '@apollo/client'; import * as Apollo from '@apollo/client'; +import { gql } from '@apollo/client'; export type Maybe = T | null; export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; @@ -1160,6 +1160,7 @@ export type UpdateObjectPayload = { labelSingular?: InputMaybe; namePlural?: InputMaybe; nameSingular?: InputMaybe; + shortcut?: InputMaybe; shouldSyncLabelAndName?: InputMaybe; }; @@ -1477,6 +1478,7 @@ export type Object = { labelSingular: Scalars['String']; namePlural: Scalars['String']; nameSingular: Scalars['String']; + shortcut?: Maybe; shouldSyncLabelAndName: Scalars['Boolean']; updatedAt: Scalars['DateTime']; }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx index b08fe9e75..3f41ab6ad 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx @@ -31,7 +31,7 @@ const StyledContainer = styled.div` const StyledColumnContainer = styled.div` display: flex; - & > *:not(:first-child) { + & > *:not(:first-of-type) { border-left: 1px solid ${({ theme }) => theme.border.color.light}; } `; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx index f989b4432..b28453238 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx @@ -18,7 +18,7 @@ const StyledHeaderContainer = styled.div` top: 0; } - & > *:not(:first-child) { + & > *:not(:first-of-type) { border-left: 1px solid ${({ theme }) => theme.border.color.light}; } `; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx index f0cbf686d..d2ce24f8c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx @@ -10,6 +10,7 @@ import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldM import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect'; import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/relation-picker/hooks/useAddNewRecordAndOpenRightDrawer'; import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; type RelationFromManyFieldInputProps = { onSubmit?: FieldInputEvent; @@ -50,6 +51,8 @@ export const RelationFromManyFieldInput = ({ recordId, }); + const { dropdownPlacement } = useDropdown(relationPickerScopeId); + return ( <> @@ -58,6 +61,7 @@ export const RelationFromManyFieldInput = ({ onSubmit={handleSubmit} onChange={updateRelation} onCreate={createNewRecordAndOpenRightDrawer} + dropdownPlacement={dropdownPlacement} /> diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx index 0281cf306..59bcb9e82 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx @@ -82,7 +82,8 @@ export const RecordDetailRelationSection = ({ const dropdownId = `record-field-card-relation-picker-${fieldDefinition.label}-${recordId}`; - const { closeDropdown, isDropdownOpen } = useDropdown(dropdownId); + const { closeDropdown, isDropdownOpen, dropdownPlacement } = + useDropdown(dropdownId); const { setRelationPickerSearchFilter } = useRelationPicker({ relationPickerScopeId: dropdownId, @@ -183,7 +184,7 @@ export const RecordDetailRelationSection = ({ ) : ( <> @@ -212,6 +214,7 @@ export const RecordDetailRelationSection = ({ onCreate={createNewRecordAndOpenRightDrawer} onChange={updateRelation} onSubmit={closeDropdown} + dropdownPlacement={dropdownPlacement} /> )} diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx index fedb01ae8..cc69eae40 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx @@ -5,6 +5,7 @@ import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/r import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates'; import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext'; import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton'; +import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; @@ -18,11 +19,12 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import styled from '@emotion/styled'; +import { Placement } from '@floating-ui/react'; import { useCallback, useEffect, useRef } from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { Key } from 'ts-key-enum'; import { IconPlus, isDefined } from 'twenty-ui'; -import { useDebouncedCallback } from 'use-debounce'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const StyledSelectableItem = styled(SelectableItem)` height: 100%; @@ -33,10 +35,12 @@ export const MultiRecordSelect = ({ onChange, onSubmit, onCreate, + dropdownPlacement, }: { onChange?: (changedRecordForSelectId: string) => void; onSubmit?: () => void; onCreate?: ((searchInput?: string) => void) | (() => void); + dropdownPlacement?: Placement | null; }) => { const containerRef = useRef(null); const setHotkeyScope = useSetHotkeyScope(); @@ -55,6 +59,7 @@ export const MultiRecordSelect = ({ const recordMultiSelectIsLoading = useRecoilValue( recordMultiSelectIsLoadingState, ); + const objectRecordsIdsMultiSelect = useRecoilValue( objectRecordsIdsMultiSelectState, ); @@ -67,9 +72,6 @@ export const MultiRecordSelect = ({ const relationPickerSearchFilter = useRecoilValue( relationPickerSearchFilterState, ); - const debouncedSetSearchFilter = useDebouncedCallback(setSearchFilter, 100, { - leading: true, - }); useEffect(() => { setHotkeyScope(relationPickerScopedId); @@ -86,16 +88,53 @@ export const MultiRecordSelect = ({ [onSubmit, goBackToPreviousHotkeyScope, resetSelectedItem], ); - const debouncedOnCreate = useDebouncedCallback( - () => onCreate?.(relationPickerSearchFilter), - 500, - ); - const handleFilterChange = useCallback( (event: React.ChangeEvent) => { - debouncedSetSearchFilter(event.currentTarget.value); + setSearchFilter(event.currentTarget.value); }, - [debouncedSetSearchFilter], + [setSearchFilter], + ); + + const results = ( + + { + onChange?.(selectedId); + resetSelectedItem(); + }} + > + {objectRecordsIdsMultiSelect?.map((recordId) => { + return ( + { + onChange?.(recordId); + resetSelectedItem(); + }} + /> + ); + })} + + {objectRecordsIdsMultiSelect?.length === 0 && + !recordMultiSelectIsLoading && } + + ); + + const createNewButton = isDefined(onCreate) && ( + <> + + + onCreate?.(relationPickerSearchFilter)} + LeftIcon={IconPlus} + text="Add New" + /> + + ); return ( @@ -107,55 +146,30 @@ export const MultiRecordSelect = ({ }} /> + {dropdownPlacement?.includes('end') && ( + <> + {createNewButton} + {results} + {recordMultiSelectIsLoading && !relationPickerSearchFilter && ( + + )} + + + )} - - - {recordMultiSelectIsLoading ? ( - - ) : ( - <> - { - onChange?.(selectedId); - resetSelectedItem(); - }} - > - {objectRecordsIdsMultiSelect?.map((recordId) => { - return ( - { - onChange?.(recordId); - resetSelectedItem(); - }} - /> - ); - })} - - {objectRecordsIdsMultiSelect?.length === 0 && ( - - )} - - )} - - {isDefined(onCreate) && ( + {(dropdownPlacement?.includes('start') || + isUndefinedOrNull(dropdownPlacement)) && ( <> - - - + {recordMultiSelectIsLoading && !relationPickerSearchFilter && ( + + )} + {results} + {createNewButton} )} diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItems.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItems.tsx index a7f029e12..e1748c5b4 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItems.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItems.tsx @@ -36,6 +36,7 @@ export type SingleEntitySelectMenuItemsProps = { isAllEntitySelectShown?: boolean; onAllEntitySelected?: () => void; hotkeyScope?: string; + isFiltered: boolean; }; export const SingleEntitySelectMenuItems = ({ @@ -54,6 +55,7 @@ export const SingleEntitySelectMenuItems = ({ isAllEntitySelectShown, onAllEntitySelected, hotkeyScope = RelationPickerHotkeyScope.RelationPicker, + isFiltered, }: SingleEntitySelectMenuItemsProps) => { const containerRef = useRef(null); @@ -139,9 +141,11 @@ export const SingleEntitySelectMenuItems = ({ }} > - {loading ? ( + {loading && !isFiltered ? ( - ) : entitiesInDropdown.length === 0 && !isAllEntitySelectShown ? ( + ) : entitiesInDropdown.length === 0 && + !isAllEntitySelectShown && + !loading ? ( <> {entitiesToSelect.length > 0 && } diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx index 40c6fe337..83c628668 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx @@ -6,7 +6,9 @@ import { useEntitySelectSearch } from '@/object-record/relation-picker/hooks/use import { useRelationPickerEntitiesOptions } from '@/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; +import { Placement } from '@floating-ui/react'; import { isDefined } from '~/utils/isDefined'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export type SingleEntitySelectMenuItemsWithSearchProps = { excludedRelationRecordIds?: string[]; @@ -14,6 +16,7 @@ export type SingleEntitySelectMenuItemsWithSearchProps = { relationObjectNameSingular: string; relationPickerScopeId?: string; selectedRelationRecordIds: string[]; + dropdownPlacement?: Placement | null; } & Pick< SingleEntitySelectMenuItemsProps, | 'EmptyIcon' @@ -34,6 +37,7 @@ export const SingleEntitySelectMenuItemsWithSearch = ({ relationPickerScopeId = 'relation-picker', selectedEntity, selectedRelationRecordIds, + dropdownPlacement, }: SingleEntitySelectMenuItemsWithSearchProps) => { const { handleSearchFilterChange } = useEntitySelectSearch({ relationPickerScopeId, @@ -62,29 +66,45 @@ export const SingleEntitySelectMenuItemsWithSearch = ({ }; } + const results = ( + + ); + return ( <> + {dropdownPlacement?.includes('end') && ( + <> + {results} + + + )} - - + {(dropdownPlacement?.includes('start') || + isUndefinedOrNull(dropdownPlacement)) && ( + <> + + {results} + + )} ); }; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx index 98da24f05..c6d5205e3 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx @@ -6,7 +6,7 @@ import { Placement, useFloating, } from '@floating-ui/react'; -import { MouseEvent, useRef } from 'react'; +import { MouseEvent, useEffect, useRef } from 'react'; import { Keys } from 'react-hotkeys-hook'; import { Key } from 'ts-key-enum'; @@ -64,8 +64,13 @@ export const Dropdown = ({ }: DropdownProps) => { const containerRef = useRef(null); - const { isDropdownOpen, toggleDropdown, closeDropdown, dropdownWidth } = - useDropdown(dropdownId); + const { + isDropdownOpen, + toggleDropdown, + closeDropdown, + dropdownWidth, + setDropdownPlacement, + } = useDropdown(dropdownId); const offsetMiddlewares = []; @@ -77,13 +82,17 @@ export const Dropdown = ({ offsetMiddlewares.push(offset({ mainAxis: dropdownOffset.y })); } - const { refs, floatingStyles } = useFloating({ + const { refs, floatingStyles, placement } = useFloating({ placement: dropdownPlacement, middleware: [flip(), ...offsetMiddlewares], whileElementsMounted: autoUpdate, strategy: dropdownStrategy, }); + useEffect(() => { + setDropdownPlacement(placement); + }, [placement, setDropdownPlacement]); + const handleHotkeyTriggered = () => { toggleDropdown(); }; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx index 86f43bbcd..2db2ca5df 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx @@ -3,7 +3,6 @@ import styled from '@emotion/styled'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; const StyledDropdownMenuItemsExternalContainer = styled.div<{ - hasMinHeight?: boolean; hasMaxHeight?: boolean; }>` --padding: ${({ theme }) => theme.spacing(1)}; @@ -13,7 +12,6 @@ const StyledDropdownMenuItemsExternalContainer = styled.div<{ flex-direction: column; gap: 2px; - min-height: ${({ hasMinHeight }) => (hasMinHeight ? '150px' : '100%')}; max-height: ${({ hasMaxHeight }) => (hasMaxHeight ? '188px' : 'none')}; overflow-y: auto; @@ -38,18 +36,13 @@ const StyledDropdownMenuItemsInternalContainer = styled.div` export const DropdownMenuItemsContainer = ({ children, - hasMinHeight, hasMaxHeight, }: { children: React.ReactNode; - hasMinHeight?: boolean; hasMaxHeight?: boolean; }) => { return ( - + {hasMaxHeight ? ( diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/internal/useDropdownStates.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/internal/useDropdownStates.ts index c976f2bd0..feb437745 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/internal/useDropdownStates.ts +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/internal/useDropdownStates.ts @@ -1,5 +1,6 @@ import { DropdownScopeInternalContext } from '@/ui/layout/dropdown/scopes/scope-internal-context/DropdownScopeInternalContext'; import { dropdownHotkeyComponentState } from '@/ui/layout/dropdown/states/dropdownHotkeyComponentState'; +import { dropdownPlacementComponentState } from '@/ui/layout/dropdown/states/dropdownPlacementComponentState'; import { dropdownWidthComponentState } from '@/ui/layout/dropdown/states/dropdownWidthComponentState'; import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; @@ -19,6 +20,10 @@ export const useDropdownStates = ({ return { scopeId, + dropdownPlacementState: extractComponentState( + dropdownPlacementComponentState, + scopeId, + ), dropdownHotkeyScopeState: extractComponentState( dropdownHotkeyComponentState, scopeId, diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts index 129d93f1d..24225beab 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts @@ -12,6 +12,7 @@ export const useDropdown = (dropdownId?: string) => { dropdownHotkeyScopeState, dropdownWidthState, isDropdownOpenState, + dropdownPlacementState, } = useDropdownStates({ dropdownScopeId: getScopeIdOrUndefinedFromComponentId(dropdownId), }); @@ -25,6 +26,10 @@ export const useDropdown = (dropdownId?: string) => { const [dropdownWidth, setDropdownWidth] = useRecoilState(dropdownWidthState); + const [dropdownPlacement, setDropdownPlacement] = useRecoilState( + dropdownPlacementState, + ); + const [isDropdownOpen, setIsDropdownOpen] = useRecoilState(isDropdownOpenState); @@ -59,5 +64,7 @@ export const useDropdown = (dropdownId?: string) => { openDropdown, dropdownWidth, setDropdownWidth, + dropdownPlacement, + setDropdownPlacement, }; }; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/states/dropdownPlacementComponentState.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/states/dropdownPlacementComponentState.ts new file mode 100644 index 000000000..e8e4b9df0 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/states/dropdownPlacementComponentState.ts @@ -0,0 +1,9 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +import { Placement } from '@floating-ui/react'; + +export const dropdownPlacementComponentState = + createComponentState({ + key: 'dropdownPlacementComponentState', + defaultValue: null, + });