Upgrade relation picker (#8795)

- Rename all parts using the name "relation" to "record" when component
is only selecting record
- Remove the use of scope states in folder
- Rename entities to records

This PR prepares the use of the record picker in workflows
This commit is contained in:
Thomas Trompette
2024-11-28 18:08:39 +01:00
committed by GitHub
parent d73dc1a728
commit 83223eeae3
62 changed files with 585 additions and 687 deletions

View File

@ -9,10 +9,10 @@ import {
import { useObjectRecordMultiSelectScopedStates } from '@/activities/hooks/useObjectRecordMultiSelectScopedStates';
import { objectRecordMultiSelectComponentFamilyState } from '@/object-record/record-field/states/objectRecordMultiSelectComponentFamilyState';
import { objectRecordMultiSelectMatchesFilterRecordsIdsComponentState } from '@/object-record/record-field/states/objectRecordMultiSelectMatchesFilterRecordsIdsComponentState';
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
import { ObjectRecordForSelect } from '@/object-record/types/ObjectRecordForSelect';
import { SelectedObjectRecordId } from '@/object-record/types/SelectedObjectRecordId';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const ActivityTargetInlineCellEditModeMultiRecordsEffect = ({
@ -20,13 +20,13 @@ export const ActivityTargetInlineCellEditModeMultiRecordsEffect = ({
}: {
selectedObjectRecordIds: SelectedObjectRecordId[];
}) => {
const scopeId = useAvailableScopeIdOrThrow(
RelationPickerScopeInternalContext,
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordPickerComponentInstanceContext,
);
const {
objectRecordsIdsMultiSelectState,
objectRecordMultiSelectCheckedRecordsIdsState,
} = useObjectRecordMultiSelectScopedStates(scopeId);
} = useObjectRecordMultiSelectScopedStates(instanceId);
const [objectRecordsIdsMultiSelect, setObjectRecordsIdsMultiSelect] =
useRecoilState(objectRecordsIdsMultiSelectState);
@ -41,7 +41,7 @@ export const ActivityTargetInlineCellEditModeMultiRecordsEffect = ({
const currentRecord = snapshot
.getLoadable(
objectRecordMultiSelectComponentFamilyState({
scopeId: scopeId,
scopeId: instanceId,
familyKey: newRecord.record.id,
}),
)
@ -66,7 +66,7 @@ export const ActivityTargetInlineCellEditModeMultiRecordsEffect = ({
) {
set(
objectRecordMultiSelectComponentFamilyState({
scopeId: scopeId,
scopeId: instanceId,
familyKey: newRecordWithSelected.record.id,
}),
newRecordWithSelected,
@ -74,12 +74,12 @@ export const ActivityTargetInlineCellEditModeMultiRecordsEffect = ({
}
}
},
[objectRecordMultiSelectCheckedRecordsIdsState, scopeId],
[objectRecordMultiSelectCheckedRecordsIdsState, instanceId],
);
const matchesSearchFilterObjectRecords = useRecoilValue(
objectRecordMultiSelectMatchesFilterRecordsIdsComponentState({
scopeId,
scopeId: instanceId,
}),
);

View File

@ -1,35 +1,29 @@
import { useEffect } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useSetRecoilState } from 'recoil';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { objectRecordMultiSelectMatchesFilterRecordsIdsComponentState } from '@/object-record/record-field/states/objectRecordMultiSelectMatchesFilterRecordsIdsComponentState';
import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates';
import { useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray } from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray';
import { useMultiObjectSearch } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
import { recordPickerSearchFilterComponentState } from '@/object-record/relation-picker/states/recordPickerSearchFilterComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect =
() => {
const scopeId = useAvailableScopeIdOrThrow(
RelationPickerScopeInternalContext,
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordPickerComponentInstanceContext,
);
const setRecordMultiSelectMatchesFilterRecords = useSetRecoilState(
objectRecordMultiSelectMatchesFilterRecordsIdsComponentState({
scopeId,
scopeId: instanceId,
}),
);
const relationPickerScopedId = useAvailableScopeIdOrThrow(
RelationPickerScopeInternalContext,
);
const { relationPickerSearchFilterState } = useRelationPickerScopedStates({
relationPickerScopedId,
});
const relationPickerSearchFilter = useRecoilValue(
relationPickerSearchFilterState,
const recordPickerSearchFilter = useRecoilComponentValueV2(
recordPickerSearchFilterComponentState,
instanceId,
);
const { matchesSearchFilterObjectRecordsQueryResult } =
@ -38,7 +32,7 @@ export const ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect =
CoreObjectNameSingular.Task,
CoreObjectNameSingular.Note,
],
searchFilterValue: relationPickerSearchFilter,
searchFilterValue: recordPickerSearchFilter,
limit: 10,
});

View File

@ -2,8 +2,8 @@ import { useObjectRecordMultiSelectScopedStates } from '@/activities/hooks/useOb
import { MultipleObjectRecordOnClickOutsideEffect } from '@/object-record/relation-picker/components/MultipleObjectRecordOnClickOutsideEffect';
import { MultipleObjectRecordSelectItem } from '@/object-record/relation-picker/components/MultipleObjectRecordSelectItem';
import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates';
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
import { recordPickerSearchFilterComponentState } from '@/object-record/relation-picker/states/recordPickerSearchFilterComponentState';
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';
@ -16,11 +16,13 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
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 { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import styled from '@emotion/styled';
import { Placement } from '@floating-ui/react';
import { useCallback, useEffect, useRef } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { IconPlus, isDefined } from 'twenty-ui';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@ -45,12 +47,12 @@ export const MultiRecordSelect = ({
const setHotkeyScope = useSetHotkeyScope();
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
const relationPickerScopedId = useAvailableScopeIdOrThrow(
RelationPickerScopeInternalContext,
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordPickerComponentInstanceContext,
);
const { objectRecordsIdsMultiSelectState, recordMultiSelectIsLoadingState } =
useObjectRecordMultiSelectScopedStates(relationPickerScopedId);
useObjectRecordMultiSelectScopedStates(instanceId);
const { resetSelectedItem } = useSelectableList(
MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID,
@ -63,18 +65,18 @@ export const MultiRecordSelect = ({
objectRecordsIdsMultiSelectState,
);
const { relationPickerSearchFilterState } = useRelationPickerScopedStates({
relationPickerScopedId,
});
const setSearchFilter = useSetRecoilState(relationPickerSearchFilterState);
const relationPickerSearchFilter = useRecoilValue(
relationPickerSearchFilterState,
const setSearchFilter = useSetRecoilComponentStateV2(
recordPickerSearchFilterComponentState,
instanceId,
);
const recordPickerSearchFilter = useRecoilComponentValueV2(
recordPickerSearchFilterComponentState,
instanceId,
);
useEffect(() => {
setHotkeyScope(relationPickerScopedId);
}, [setHotkeyScope, relationPickerScopedId]);
setHotkeyScope(instanceId);
}, [setHotkeyScope, instanceId]);
useScopedHotkeys(
Key.Escape,
@ -83,7 +85,7 @@ export const MultiRecordSelect = ({
goBackToPreviousHotkeyScope();
resetSelectedItem();
},
relationPickerScopedId,
instanceId,
[onSubmit, goBackToPreviousHotkeyScope, resetSelectedItem],
);
@ -99,7 +101,7 @@ export const MultiRecordSelect = ({
<SelectableList
selectableListId={MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID}
selectableItemIdArray={objectRecordsIdsMultiSelect}
hotkeyScope={relationPickerScopedId}
hotkeyScope={instanceId}
onEnter={(selectedId) => {
onChange?.(selectedId);
resetSelectedItem();
@ -123,7 +125,7 @@ export const MultiRecordSelect = ({
const createNewButton = isDefined(onCreate) && (
<CreateNewButton
onClick={() => onCreate?.(relationPickerSearchFilter)}
onClick={() => onCreate?.(recordPickerSearchFilter)}
LeftIcon={IconPlus}
text="Add New"
/>
@ -147,7 +149,7 @@ export const MultiRecordSelect = ({
)}
<DropdownMenuSeparator />
{objectRecordsIdsMultiSelect?.length > 0 && results}
{recordMultiSelectIsLoading && !relationPickerSearchFilter && (
{recordMultiSelectIsLoading && !recordPickerSearchFilter && (
<>
<DropdownMenuSkeletonItem />
<DropdownMenuSeparator />
@ -159,7 +161,7 @@ export const MultiRecordSelect = ({
</>
)}
<DropdownMenuSearchInput
value={relationPickerSearchFilter}
value={recordPickerSearchFilter}
onChange={handleFilterChange}
autoFocus
/>
@ -167,7 +169,7 @@ export const MultiRecordSelect = ({
isUndefinedOrNull(dropdownPlacement)) && (
<>
<DropdownMenuSeparator />
{recordMultiSelectIsLoading && !relationPickerSearchFilter && (
{recordMultiSelectIsLoading && !recordPickerSearchFilter && (
<>
<DropdownMenuSkeletonItem />
<DropdownMenuSeparator />

View File

@ -4,10 +4,10 @@ import { Avatar, MenuItemMultiSelectAvatar } from 'twenty-ui';
import { useObjectRecordMultiSelectScopedStates } from '@/activities/hooks/useObjectRecordMultiSelectScopedStates';
import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { isDefined } from '~/utils/isDefined';
export const StyledSelectableItem = styled(SelectableItem)`
@ -29,14 +29,14 @@ export const MultipleObjectRecordSelectItem = ({
const isSelectedByKeyboard = useRecoilValue(
isSelectedItemIdSelector(objectRecordId),
);
const scopeId = useAvailableScopeIdOrThrow(
RelationPickerScopeInternalContext,
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordPickerComponentInstanceContext,
);
const {
objectRecordMultiSelectFamilyState,
objectRecordMultiSelectCheckedRecordsIdsState,
} = useObjectRecordMultiSelectScopedStates(scopeId);
} = useObjectRecordMultiSelectScopedStates(instanceId);
const record = useRecoilValue(
objectRecordMultiSelectFamilyState(objectRecordId),

View File

@ -6,16 +6,16 @@ import { FieldContext } from '@/object-record/record-field/contexts/FieldContext
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { SearchPickerInitialValueEffect } from '@/object-record/relation-picker/components/SearchPickerInitialValueEffect';
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
import { SingleRecordSelect } from '@/object-record/relation-picker/components/SingleRecordSelect';
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/relation-picker/hooks/useAddNewRecordAndOpenRightDrawer';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { RecordForSelect } from '@/object-record/relation-picker/types/RecordForSelect';
export type RelationPickerProps = {
selectedRecordId?: string;
onSubmit: (selectedEntity: EntityForSelect | null) => void;
onSubmit: (selectedRecord: RecordForSelect | null) => void;
onCancel?: () => void;
width?: number;
excludeRecordIds?: string[];
excludedRecordIds?: string[];
initialSearchFilter?: string | null;
fieldDefinition: FieldDefinition<FieldRelationMetadata>;
};
@ -24,16 +24,16 @@ export const RelationPicker = ({
selectedRecordId,
onSubmit,
onCancel,
excludeRecordIds,
excludedRecordIds,
width,
initialSearchFilter,
fieldDefinition,
}: RelationPickerProps) => {
const relationPickerScopeId = 'relation-picker';
const recordPickerInstanceId = 'relation-picker';
const handleEntitySelected = (
selectedEntity: EntityForSelect | null | undefined,
) => onSubmit(selectedEntity ?? null);
const handleRecordSelected = (
selectedRecord: RecordForSelect | null | undefined,
) => onSubmit(selectedRecord ?? null);
const { objectMetadataItem: relationObjectMetadataItem } =
useObjectMetadataItem({
@ -60,21 +60,21 @@ export const RelationPicker = ({
<>
<SearchPickerInitialValueEffect
initialValueForSearchFilter={initialSearchFilter}
relationPickerScopeId={relationPickerScopeId}
recordPickerInstanceId={recordPickerInstanceId}
/>
<SingleEntitySelect
<SingleRecordSelect
EmptyIcon={IconForbid}
emptyLabel={'No ' + fieldDefinition.label}
onCancel={onCancel}
onCreate={createNewRecordAndOpenRightDrawer}
onEntitySelected={handleEntitySelected}
onRecordSelected={handleRecordSelected}
width={width}
relationObjectNameSingular={
objectNameSingular={
fieldDefinition.metadata.relationObjectMetadataNameSingular
}
relationPickerScopeId={relationPickerScopeId}
selectedRelationRecordIds={selectedRecordId ? [selectedRecordId] : []}
excludedRelationRecordIds={excludeRecordIds}
recordPickerInstanceId={recordPickerInstanceId}
selectedRecordIds={selectedRecordId ? [selectedRecordId] : []}
excludedRecordIds={excludedRecordIds}
/>
</>
);

View File

@ -1,25 +1,22 @@
import { getRelationPickerScopedStates } from '@/object-record/relation-picker/utils/getRelationPickerScopedStates';
import { recordPickerSearchFilterComponentState } from '@/object-record/relation-picker/states/recordPickerSearchFilterComponentState';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useEffect } from 'react';
import { useSetRecoilState } from 'recoil';
export const SearchPickerInitialValueEffect = ({
initialValueForSearchFilter,
relationPickerScopeId,
recordPickerInstanceId,
}: {
initialValueForSearchFilter?: string | null;
relationPickerScopeId: string;
recordPickerInstanceId: string;
}) => {
const { relationPickerSearchFilterState } = getRelationPickerScopedStates({
relationPickerScopeId: relationPickerScopeId,
});
const setRelationPickerSearchFilter = useSetRecoilState(
relationPickerSearchFilterState,
const setRecordPickerSearchFilter = useSetRecoilComponentStateV2(
recordPickerSearchFilterComponentState,
recordPickerInstanceId,
);
useEffect(() => {
setRelationPickerSearchFilter(initialValueForSearchFilter ?? '');
}, [initialValueForSearchFilter, setRelationPickerSearchFilter]);
setRecordPickerSearchFilter(initialValueForSearchFilter ?? '');
}, [initialValueForSearchFilter, setRecordPickerSearchFilter]);
return <></>;
};

View File

@ -2,14 +2,15 @@ import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { Avatar, MenuItemSelectAvatar } from 'twenty-ui';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { SINGLE_RECORD_SELECT_BASE_LIST } from '@/object-record/relation-picker/constants/SingleRecordSelectBaseList';
import { RecordForSelect } from '@/object-record/relation-picker/types/RecordForSelect';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
type SelectableMenuItemSelectProps = {
entity: EntityForSelect;
onEntitySelected: (entitySelected?: EntityForSelect) => void;
selectedEntity?: EntityForSelect;
record: RecordForSelect;
onRecordSelected: (recordSelected?: RecordForSelect) => void;
selectedRecord?: RecordForSelect;
};
const StyledSelectableItem = styled(SelectableItem)`
@ -17,31 +18,31 @@ const StyledSelectableItem = styled(SelectableItem)`
`;
export const SelectableMenuItemSelect = ({
entity,
onEntitySelected,
selectedEntity,
record,
onRecordSelected,
selectedRecord,
}: SelectableMenuItemSelectProps) => {
const { isSelectedItemIdSelector } = useSelectableList(
'single-entity-select-base-list',
SINGLE_RECORD_SELECT_BASE_LIST,
);
const isSelectedItemId = useRecoilValue(isSelectedItemIdSelector(entity.id));
const isSelectedItemId = useRecoilValue(isSelectedItemIdSelector(record.id));
return (
<StyledSelectableItem itemId={entity.id} key={entity.id}>
<StyledSelectableItem itemId={record.id} key={record.id}>
<MenuItemSelectAvatar
key={entity.id}
key={record.id}
testId="menu-item"
onClick={() => onEntitySelected(entity)}
text={entity.name}
selected={selectedEntity?.id === entity.id}
onClick={() => onRecordSelected(record)}
text={record.name}
selected={selectedRecord?.id === record.id}
hovered={isSelectedItemId}
avatar={
<Avatar
avatarUrl={entity.avatarUrl}
placeholderColorSeed={entity.id}
placeholder={entity.name}
avatarUrl={record.avatarUrl}
placeholderColorSeed={record.id}
placeholder={record.name}
size="md"
type={entity.avatarType ?? 'rounded'}
type={record.avatarType ?? 'rounded'}
/>
}
/>

View File

@ -1,114 +0,0 @@
import {
SingleEntitySelectMenuItems,
SingleEntitySelectMenuItemsProps,
} from '@/object-record/relation-picker/components/SingleEntitySelectMenuItems';
import { useEntitySelectSearch } from '@/object-record/relation-picker/hooks/useEntitySelectSearch';
import { useRelationPickerEntitiesOptions } from '@/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions';
import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { Placement } from '@floating-ui/react';
import { IconPlus } from 'twenty-ui';
import { isDefined } from '~/utils/isDefined';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export type SingleEntitySelectMenuItemsWithSearchProps = {
excludedRelationRecordIds?: string[];
onCreate?: ((searchInput?: string) => void) | (() => void);
relationObjectNameSingular: string;
relationPickerScopeId?: string;
selectedRelationRecordIds: string[];
dropdownPlacement?: Placement | null;
} & Pick<
SingleEntitySelectMenuItemsProps,
| 'EmptyIcon'
| 'emptyLabel'
| 'onCancel'
| 'onEntitySelected'
| 'selectedEntity'
>;
export const SingleEntitySelectMenuItemsWithSearch = ({
EmptyIcon,
emptyLabel,
excludedRelationRecordIds,
onCancel,
onCreate,
onEntitySelected,
relationObjectNameSingular,
relationPickerScopeId = 'relation-picker',
selectedRelationRecordIds,
dropdownPlacement,
}: SingleEntitySelectMenuItemsWithSearchProps) => {
const { handleSearchFilterChange } = useEntitySelectSearch({
relationPickerScopeId,
});
const { entities, relationPickerSearchFilter } =
useRelationPickerEntitiesOptions({
relationObjectNameSingular,
selectedRelationRecordIds,
excludedRelationRecordIds,
});
const createNewButton = isDefined(onCreate) && (
<CreateNewButton
onClick={() => onCreate?.(relationPickerSearchFilter)}
LeftIcon={IconPlus}
text="Add New"
/>
);
const results = (
<SingleEntitySelectMenuItems
entitiesToSelect={entities.entitiesToSelect}
loading={entities.loading}
selectedEntity={
entities.selectedEntities.length === 1
? entities.selectedEntities[0]
: undefined
}
shouldSelectEmptyOption={selectedRelationRecordIds?.length === 0}
hotkeyScope={relationPickerScopeId}
isFiltered={!!relationPickerSearchFilter}
{...{
EmptyIcon,
emptyLabel,
onCancel,
onEntitySelected,
}}
/>
);
return (
<>
{dropdownPlacement?.includes('end') && (
<>
<DropdownMenuItemsContainer>
{createNewButton}
</DropdownMenuItemsContainer>
{entities.entitiesToSelect.length > 0 && <DropdownMenuSeparator />}
{entities.entitiesToSelect.length > 0 && results}
<DropdownMenuSeparator />
</>
)}
<DropdownMenuSearchInput onChange={handleSearchFilterChange} autoFocus />
{(dropdownPlacement?.includes('start') ||
isUndefinedOrNull(dropdownPlacement)) && (
<>
<DropdownMenuSeparator />
{entities.entitiesToSelect.length > 0 && results}
{entities.entitiesToSelect.length > 0 && isDefined(onCreate) && (
<DropdownMenuSeparator />
)}
{isDefined(onCreate) && (
<DropdownMenuItemsContainer>
{createNewButton}
</DropdownMenuItemsContainer>
)}
</>
)}
</>
);
};

View File

@ -1,31 +1,31 @@
import { useRef } from 'react';
import {
SingleEntitySelectMenuItemsWithSearch,
SingleEntitySelectMenuItemsWithSearchProps,
} from '@/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch';
SingleRecordSelectMenuItemsWithSearch,
SingleRecordSelectMenuItemsWithSearchProps,
} from '@/object-record/relation-picker/components/SingleRecordSelectMenuItemsWithSearch';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isDefined } from '~/utils/isDefined';
export type SingleEntitySelectProps = {
export type SingleRecordSelectProps = {
disableBackgroundBlur?: boolean;
width?: number;
} & SingleEntitySelectMenuItemsWithSearchProps;
} & SingleRecordSelectMenuItemsWithSearchProps;
export const SingleEntitySelect = ({
export const SingleRecordSelect = ({
disableBackgroundBlur = false,
EmptyIcon,
emptyLabel,
excludedRelationRecordIds,
excludedRecordIds,
onCancel,
onCreate,
onEntitySelected,
relationObjectNameSingular,
relationPickerScopeId,
selectedRelationRecordIds,
onRecordSelected,
objectNameSingular,
recordPickerInstanceId,
selectedRecordIds,
width = 200,
}: SingleEntitySelectProps) => {
}: SingleRecordSelectProps) => {
const containerRef = useRef<HTMLDivElement>(null);
useListenClickOutside({
@ -50,17 +50,17 @@ export const SingleEntitySelect = ({
width={width}
data-select-disable
>
<SingleEntitySelectMenuItemsWithSearch
<SingleRecordSelectMenuItemsWithSearch
{...{
EmptyIcon,
emptyLabel,
excludedRelationRecordIds,
excludedRecordIds,
onCancel,
onCreate,
onEntitySelected,
relationObjectNameSingular,
relationPickerScopeId,
selectedRelationRecordIds,
onRecordSelected,
objectNameSingular,
recordPickerInstanceId,
selectedRecordIds,
}}
/>
</DropdownMenu>

View File

@ -5,7 +5,7 @@ import { Key } from 'ts-key-enum';
import { IconComponent, MenuItemSelect } from 'twenty-ui';
import { SelectableMenuItemSelect } from '@/object-record/relation-picker/components/SelectableMenuItemSelect';
import { SINGLE_ENTITY_SELECT_BASE_LIST } from '@/object-record/relation-picker/constants/SingleEntitySelectBaseList';
import { SINGLE_RECORD_SELECT_BASE_LIST } from '@/object-record/relation-picker/constants/SingleRecordSelectBaseList';
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
@ -13,44 +13,44 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { isDefined } from '~/utils/isDefined';
import { EntityForSelect } from '../types/EntityForSelect';
import { RecordForSelect } from '../types/RecordForSelect';
import { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope';
export type SingleEntitySelectMenuItemsProps = {
export type SingleRecordSelectMenuItemsProps = {
EmptyIcon?: IconComponent;
emptyLabel?: string;
entitiesToSelect: EntityForSelect[];
recordsToSelect: RecordForSelect[];
loading?: boolean;
onCancel?: () => void;
onEntitySelected: (entity?: EntityForSelect) => void;
selectedEntity?: EntityForSelect;
onRecordSelected: (entity?: RecordForSelect) => void;
selectedRecord?: RecordForSelect;
SelectAllIcon?: IconComponent;
selectAllLabel?: string;
isAllEntitySelected?: boolean;
isAllEntitySelectShown?: boolean;
onAllEntitySelected?: () => void;
isAllRecordsSelected?: boolean;
isAllRecordsSelectShown?: boolean;
onAllRecordsSelected?: () => void;
hotkeyScope?: string;
isFiltered: boolean;
shouldSelectEmptyOption?: boolean;
};
export const SingleEntitySelectMenuItems = ({
export const SingleRecordSelectMenuItems = ({
EmptyIcon,
emptyLabel,
entitiesToSelect,
recordsToSelect,
loading,
onCancel,
onEntitySelected,
selectedEntity,
onRecordSelected,
selectedRecord,
SelectAllIcon,
selectAllLabel,
isAllEntitySelected,
isAllEntitySelectShown,
onAllEntitySelected,
isAllRecordsSelected,
isAllRecordsSelectShown,
onAllRecordsSelected,
hotkeyScope = RelationPickerHotkeyScope.RelationPicker,
isFiltered,
shouldSelectEmptyOption,
}: SingleEntitySelectMenuItemsProps) => {
}: SingleRecordSelectMenuItemsProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const selectNone = emptyLabel
@ -61,7 +61,7 @@ export const SingleEntitySelectMenuItems = ({
}
: null;
const selectAll = isAllEntitySelectShown
const selectAll = isAllRecordsSelectShown
? {
__typename: '',
id: 'select-all',
@ -69,18 +69,18 @@ export const SingleEntitySelectMenuItems = ({
}
: null;
const entitiesInDropdown = [
const recordsInDropdown = [
selectAll,
selectNone,
selectedEntity,
...entitiesToSelect,
selectedRecord,
...recordsToSelect,
].filter(
(entity): entity is EntityForSelect =>
(entity): entity is RecordForSelect =>
isDefined(entity) && isNonEmptyString(entity.name),
);
const { isSelectedItemIdSelector, resetSelectedItem } = useSelectableList(
SINGLE_ENTITY_SELECT_BASE_LIST,
SINGLE_RECORD_SELECT_BASE_LIST,
);
const isSelectedSelectNoneButton = useRecoilValue(
@ -101,38 +101,38 @@ export const SingleEntitySelectMenuItems = ({
[onCancel, resetSelectedItem],
);
const selectableItemIds = entitiesInDropdown.map((entity) => entity.id);
const selectableItemIds = recordsInDropdown.map((entity) => entity.id);
return (
<div ref={containerRef}>
<SelectableList
selectableListId={SINGLE_ENTITY_SELECT_BASE_LIST}
selectableListId={SINGLE_RECORD_SELECT_BASE_LIST}
selectableItemIdArray={selectableItemIds}
hotkeyScope={hotkeyScope}
onEnter={(itemId) => {
const entityIndex = entitiesInDropdown.findIndex(
(entity) => entity.id === itemId,
const recordIndex = recordsInDropdown.findIndex(
(record) => record.id === itemId,
);
onEntitySelected(entitiesInDropdown[entityIndex]);
onRecordSelected(recordsInDropdown[recordIndex]);
resetSelectedItem();
}}
>
<DropdownMenuItemsContainer hasMaxHeight>
{loading && !isFiltered ? (
<DropdownMenuSkeletonItem />
) : entitiesInDropdown.length === 0 &&
!isAllEntitySelectShown &&
) : recordsInDropdown.length === 0 &&
!isAllRecordsSelectShown &&
!loading ? (
<></>
) : (
entitiesInDropdown?.map((entity) => {
switch (entity.id) {
recordsInDropdown?.map((record) => {
switch (record.id) {
case 'select-none': {
return (
emptyLabel && (
<MenuItemSelect
key={entity.id}
onClick={() => onEntitySelected()}
key={record.id}
onClick={() => onRecordSelected()}
LeftIcon={EmptyIcon}
text={emptyLabel}
selected={shouldSelectEmptyOption === true}
@ -143,15 +143,15 @@ export const SingleEntitySelectMenuItems = ({
}
case 'select-all': {
return (
isAllEntitySelectShown &&
isAllRecordsSelectShown &&
selectAllLabel &&
onAllEntitySelected && (
onAllRecordsSelected && (
<MenuItemSelect
key={entity.id}
onClick={() => onAllEntitySelected()}
key={record.id}
onClick={() => onAllRecordsSelected()}
LeftIcon={SelectAllIcon}
text={selectAllLabel}
selected={!!isAllEntitySelected}
selected={!!isAllRecordsSelected}
hovered={isSelectedSelectAllButton}
/>
)
@ -160,10 +160,10 @@ export const SingleEntitySelectMenuItems = ({
default: {
return (
<SelectableMenuItemSelect
key={entity.id}
entity={entity}
onEntitySelected={onEntitySelected}
selectedEntity={selectedEntity}
key={record.id}
record={record}
onRecordSelected={onRecordSelected}
selectedRecord={selectedRecord}
/>
);
}

View File

@ -0,0 +1,113 @@
import {
SingleRecordSelectMenuItems,
SingleRecordSelectMenuItemsProps,
} from '@/object-record/relation-picker/components/SingleRecordSelectMenuItems';
import { useRecordPickerRecordsOptions } from '@/object-record/relation-picker/hooks/useRecordPickerRecordsOptions';
import { useRecordSelectSearch } from '@/object-record/relation-picker/hooks/useRecordSelectSearch';
import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { Placement } from '@floating-ui/react';
import { IconPlus } from 'twenty-ui';
import { isDefined } from '~/utils/isDefined';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export type SingleRecordSelectMenuItemsWithSearchProps = {
excludedRecordIds?: string[];
onCreate?: ((searchInput?: string) => void) | (() => void);
objectNameSingular: string;
recordPickerInstanceId?: string;
selectedRecordIds: string[];
dropdownPlacement?: Placement | null;
} & Pick<
SingleRecordSelectMenuItemsProps,
| 'EmptyIcon'
| 'emptyLabel'
| 'onCancel'
| 'onRecordSelected'
| 'selectedRecord'
>;
export const SingleRecordSelectMenuItemsWithSearch = ({
EmptyIcon,
emptyLabel,
excludedRecordIds,
onCancel,
onCreate,
onRecordSelected,
objectNameSingular,
recordPickerInstanceId = 'record-picker',
selectedRecordIds,
dropdownPlacement,
}: SingleRecordSelectMenuItemsWithSearchProps) => {
const { handleSearchFilterChange } = useRecordSelectSearch({
recordPickerInstanceId,
});
const { records, recordPickerSearchFilter } = useRecordPickerRecordsOptions({
objectNameSingular,
selectedRecordIds,
excludedRecordIds,
});
const createNewButton = isDefined(onCreate) && (
<CreateNewButton
onClick={() => onCreate?.(recordPickerSearchFilter)}
LeftIcon={IconPlus}
text="Add New"
/>
);
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') && (
<>
<DropdownMenuItemsContainer>
{createNewButton}
</DropdownMenuItemsContainer>
{records.recordsToSelect.length > 0 && <DropdownMenuSeparator />}
{records.recordsToSelect.length > 0 && results}
<DropdownMenuSeparator />
</>
)}
<DropdownMenuSearchInput onChange={handleSearchFilterChange} autoFocus />
{(dropdownPlacement?.includes('start') ||
isUndefinedOrNull(dropdownPlacement)) && (
<>
<DropdownMenuSeparator />
{records.recordsToSelect.length > 0 && results}
{records.recordsToSelect.length > 0 && isDefined(onCreate) && (
<DropdownMenuSeparator />
)}
{isDefined(onCreate) && (
<DropdownMenuItemsContainer>
{createNewButton}
</DropdownMenuItemsContainer>
)}
</>
)}
</>
);
};

View File

@ -5,18 +5,18 @@ import { ComponentDecorator, IconUserCircle } from 'twenty-ui';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { RelationPickerDecorator } from '~/testing/decorators/RelationPickerDecorator';
import { RecordPickerDecorator } from '~/testing/decorators/RecordPickerDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { getPeopleMock } from '~/testing/mock-data/people';
import { sleep } from '~/utils/sleep';
import { EntityForSelect } from '../../types/EntityForSelect';
import { SingleEntitySelect } from '../SingleEntitySelect';
import { RecordForSelect } from '../../types/RecordForSelect';
import { SingleRecordSelect } from '../SingleRecordSelect';
const peopleMock = getPeopleMock();
const entities = peopleMock.map<EntityForSelect>((person) => ({
const records = peopleMock.map<RecordForSelect>((person) => ({
id: person.id,
name: person.name.firstName + ' ' + person.name.lastName,
avatarUrl: 'https://picsum.photos/200',
@ -24,24 +24,24 @@ const entities = peopleMock.map<EntityForSelect>((person) => ({
record: { ...person, __typename: 'Person' },
}));
const meta: Meta<typeof SingleEntitySelect> = {
title: 'UI/Input/RelationPicker/SingleEntitySelect',
component: SingleEntitySelect,
const meta: Meta<typeof SingleRecordSelect> = {
title: 'UI/Input/RelationPicker/SingleRecordSelect',
component: SingleRecordSelect,
decorators: [
ComponentDecorator,
ComponentWithRecoilScopeDecorator,
RelationPickerDecorator,
RecordPickerDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,
],
args: {
relationObjectNameSingular: CoreObjectNameSingular.WorkspaceMember,
selectedRelationRecordIds: [],
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
selectedRecordIds: [],
},
argTypes: {
selectedEntity: {
options: entities.map(({ name }) => name),
mapping: entities.reduce(
selectedRecord: {
options: records.map(({ name }) => name),
mapping: records.reduce(
(result, entity) => ({ ...result, [entity.name]: entity }),
{},
),
@ -53,12 +53,12 @@ const meta: Meta<typeof SingleEntitySelect> = {
};
export default meta;
type Story = StoryObj<typeof SingleEntitySelect>;
type Story = StoryObj<typeof SingleRecordSelect>;
export const Default: Story = {};
export const WithSelectedEntity: Story = {
args: { selectedEntity: entities[2] },
export const WithSelectedRecord: Story = {
args: { selectedRecord: records[2] },
};
export const WithEmptyOption: Story = {

View File

@ -1 +0,0 @@
export const SINGLE_ENTITY_SELECT_BASE_LIST = 'single-entity-select-base-list';

View File

@ -0,0 +1 @@
export const SINGLE_RECORD_SELECT_BASE_LIST = 'single-record-select-base-list';

View File

@ -1,46 +0,0 @@
import { ChangeEvent } from 'react';
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates';
import { useEntitySelectSearch } from '@/object-record/relation-picker/hooks/useEntitySelectSearch';
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
const scopeId = 'scopeId';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RelationPickerScopeInternalContext.Provider value={{ scopeId }}>
<RecoilRoot>{children}</RecoilRoot>
</RelationPickerScopeInternalContext.Provider>
);
describe('useEntitySelectSearch', () => {
it('should update searchFilter after change event', async () => {
const { result } = renderHook(
() => {
const entitySelectSearchHook = useEntitySelectSearch({
relationPickerScopeId: 'relation-picker',
});
const relationPickerScopedStatesHook = useRelationPickerScopedStates({
relationPickerScopedId: 'relation-picker',
});
const internallyStoredFilter = useRecoilValue(
relationPickerScopedStatesHook.relationPickerSearchFilterState,
);
return { entitySelectSearchHook, internallyStoredFilter };
},
{
wrapper: Wrapper,
},
);
const filter = 'value';
act(() => {
result.current.entitySelectSearchHook.handleSearchFilterChange({
currentTarget: { value: filter },
} as ChangeEvent<HTMLInputElement>);
});
expect(result.current.internallyStoredFilter).toBe(filter);
});
});

View File

@ -3,13 +3,13 @@ import { RecoilRoot } from 'recoil';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useLimitPerMetadataItem } from '@/object-record/relation-picker/hooks/useLimitPerMetadataItem';
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
const scopeId = 'scopeId';
const instanceId = 'instanceId';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RelationPickerScopeInternalContext.Provider value={{ scopeId }}>
<RecordPickerComponentInstanceContext.Provider value={{ instanceId }}>
<RecoilRoot>{children}</RecoilRoot>
</RelationPickerScopeInternalContext.Provider>
</RecordPickerComponentInstanceContext.Provider>
);
describe('useLimitPerMetadataItem', () => {

View File

@ -3,14 +3,14 @@ import { RecoilRoot, useSetRecoilState } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray } from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray';
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
const scopeId = 'scopeId';
const instanceId = 'instanceId';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RelationPickerScopeInternalContext.Provider value={{ scopeId }}>
<RecordPickerComponentInstanceContext.Provider value={{ instanceId }}>
<RecoilRoot>{children}</RecoilRoot>
</RelationPickerScopeInternalContext.Provider>
</RecordPickerComponentInstanceContext.Provider>
);
const opportunityId = 'cb702502-4b1d-488e-9461-df3fb096ebf6';

View File

@ -3,14 +3,14 @@ import { RecoilRoot, useSetRecoilState } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap } from '@/object-record/relation-picker/hooks/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap';
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
const scopeId = 'scopeId';
const instanceId = 'instanceId';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RelationPickerScopeInternalContext.Provider value={{ scopeId }}>
<RecordPickerComponentInstanceContext.Provider value={{ instanceId }}>
<RecoilRoot>{children}</RecoilRoot>
</RelationPickerScopeInternalContext.Provider>
</RecordPickerComponentInstanceContext.Provider>
);
const opportunityId = 'cb702502-4b1d-488e-9461-df3fb096ebf6';

View File

@ -0,0 +1,45 @@
import { act, renderHook } from '@testing-library/react';
import { ChangeEvent } from 'react';
import { RecoilRoot } from 'recoil';
import { useRecordSelectSearch } from '@/object-record/relation-picker/hooks/useRecordSelectSearch';
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
import { recordPickerSearchFilterComponentState } from '@/object-record/relation-picker/states/recordPickerSearchFilterComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
const instanceId = 'instanceId';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RecordPickerComponentInstanceContext.Provider value={{ instanceId }}>
<RecoilRoot>{children}</RecoilRoot>
</RecordPickerComponentInstanceContext.Provider>
);
describe('useRecordSelectSearch', () => {
it('should update searchFilter after change event', async () => {
const { result } = renderHook(
() => {
const recordSelectSearchHook = useRecordSelectSearch({
recordPickerInstanceId: instanceId,
});
const internallyStoredFilter = useRecoilComponentValueV2(
recordPickerSearchFilterComponentState,
instanceId,
);
return { recordSelectSearchHook, internallyStoredFilter };
},
{
wrapper: Wrapper,
},
);
const filter = 'value';
act(() => {
result.current.recordSelectSearchHook.handleSearchFilterChange({
currentTarget: { value: filter },
} as ChangeEvent<HTMLInputElement>);
});
expect(result.current.internallyStoredFilter).toBe(filter);
});
});

View File

@ -1,29 +0,0 @@
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { getRelationPickerScopedStates } from '@/object-record/relation-picker/utils/getRelationPickerScopedStates';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
export const useRelationPickerScopedStates = (args?: {
relationPickerScopedId?: string;
}) => {
const { relationPickerScopedId } = args ?? {};
const scopeId = useAvailableComponentInstanceIdOrThrow(
RecordTableComponentInstanceContext,
relationPickerScopedId,
);
const {
relationPickerSearchFilterState,
relationPickerPreselectedIdState,
searchQueryState,
} = getRelationPickerScopedStates({
relationPickerScopeId: scopeId,
});
return {
scopeId,
relationPickerSearchFilterState,
relationPickerPreselectedIdState,
searchQueryState,
};
};

View File

@ -0,0 +1,24 @@
import { recordPickerPreselectedIdComponentState } from '@/object-record/relation-picker/states/recordPickerPreselectedIdComponentState';
import { recordPickerSearchFilterComponentState } from '@/object-record/relation-picker/states/recordPickerSearchFilterComponentState';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
export const useRecordPicker = ({
recordPickerInstanceId,
}: {
recordPickerInstanceId?: string;
}) => {
const setRecordPickerSearchFilter = useSetRecoilComponentStateV2(
recordPickerSearchFilterComponentState,
recordPickerInstanceId,
);
const setRecordPickerPreselectedId = useSetRecoilComponentStateV2(
recordPickerPreselectedIdComponentState,
recordPickerInstanceId,
);
return {
setRecordPickerSearchFilter,
setRecordPickerPreselectedId,
};
};

View File

@ -0,0 +1,26 @@
import { recordPickerSearchFilterComponentState } from '@/object-record/relation-picker/states/recordPickerSearchFilterComponentState';
import { useFilteredSearchRecordQuery } from '@/search/hooks/useFilteredSearchRecordQuery';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const useRecordPickerRecordsOptions = ({
objectNameSingular,
selectedRecordIds = [],
excludedRecordIds = [],
}: {
objectNameSingular: string;
selectedRecordIds?: string[];
excludedRecordIds?: string[];
}) => {
const recordPickerSearchFilter = useRecoilComponentValueV2(
recordPickerSearchFilterComponentState,
);
const records = useFilteredSearchRecordQuery({
searchFilter: recordPickerSearchFilter,
selectedIds: selectedRecordIds,
excludedRecordIds: excludedRecordIds,
objectNameSingular,
});
return { records, recordPickerSearchFilter };
};

View File

@ -1,17 +1,17 @@
import { useDebouncedCallback } from 'use-debounce';
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
import { useRecordPicker } from '@/object-record/relation-picker/hooks/useRecordPicker';
export const useEntitySelectSearch = ({
relationPickerScopeId,
export const useRecordSelectSearch = ({
recordPickerInstanceId,
}: {
relationPickerScopeId?: string;
recordPickerInstanceId?: string;
} = {}) => {
const { setRelationPickerSearchFilter, setRelationPickerPreselectedId } =
useRelationPicker({ relationPickerScopeId });
const { setRecordPickerSearchFilter, setRecordPickerPreselectedId } =
useRecordPicker({ recordPickerInstanceId });
const debouncedSetSearchFilter = useDebouncedCallback(
setRelationPickerSearchFilter,
setRecordPickerSearchFilter,
100,
{
leading: true,
@ -20,14 +20,14 @@ export const useEntitySelectSearch = ({
const resetSearchFilter = () => {
debouncedSetSearchFilter('');
setRelationPickerPreselectedId('');
setRecordPickerPreselectedId('');
};
const handleSearchFilterChange = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
debouncedSetSearchFilter(event.currentTarget.value);
setRelationPickerPreselectedId('');
setRecordPickerPreselectedId('');
};
return {

View File

@ -1,34 +0,0 @@
import { useSetRecoilState } from 'recoil';
import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates';
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
type useRelationPickeProps = {
relationPickerScopeId?: string;
};
export const useRelationPicker = (props?: useRelationPickeProps) => {
const scopeId = useAvailableScopeIdOrThrow(
RelationPickerScopeInternalContext,
props?.relationPickerScopeId,
);
const { relationPickerSearchFilterState, relationPickerPreselectedIdState } =
useRelationPickerScopedStates({
relationPickerScopedId: scopeId,
});
const setRelationPickerSearchFilter = useSetRecoilState(
relationPickerSearchFilterState,
);
const setRelationPickerPreselectedId = useSetRecoilState(
relationPickerPreselectedIdState,
);
return {
setRelationPickerSearchFilter,
setRelationPickerPreselectedId,
};
};

View File

@ -1,36 +0,0 @@
import { useRecoilValue } from 'recoil';
import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates';
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
export const useRelationPickerEntitiesOptions = ({
relationObjectNameSingular,
selectedRelationRecordIds = [],
excludedRelationRecordIds = [],
}: {
relationObjectNameSingular: string;
selectedRelationRecordIds?: string[];
excludedRelationRecordIds?: string[];
}) => {
const scopeId = useAvailableScopeIdOrThrow(
RelationPickerScopeInternalContext,
);
const { relationPickerSearchFilterState } = useRelationPickerScopedStates({
relationPickerScopedId: scopeId,
});
const relationPickerSearchFilter = useRecoilValue(
relationPickerSearchFilterState,
);
const entities = useFilteredSearchEntityQuery({
searchFilter: relationPickerSearchFilter,
selectedIds: selectedRelationRecordIds,
excludeRecordIds: excludedRelationRecordIds,
objectNameSingular: relationObjectNameSingular,
});
return { entities, relationPickerSearchFilter };
};

View File

@ -1,21 +0,0 @@
import { ReactNode } from 'react';
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
type RelationPickerScopeProps = {
children: ReactNode;
relationPickerScopeId: string;
};
export const RelationPickerScope = ({
children,
relationPickerScopeId,
}: RelationPickerScopeProps) => {
return (
<RelationPickerScopeInternalContext.Provider
value={{ scopeId: relationPickerScopeId }}
>
{children}
</RelationPickerScopeInternalContext.Provider>
);
};

View File

@ -1,7 +0,0 @@
import { createScopeInternalContext } from '@/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext';
import { RecoilComponentStateKey } from '@/ui/utilities/state/component-state/types/RecoilComponentStateKey';
type RelationPickerScopeInternalContextProps = RecoilComponentStateKey;
export const RelationPickerScopeInternalContext =
createScopeInternalContext<RelationPickerScopeInternalContextProps>();

View File

@ -0,0 +1,4 @@
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
export const RecordPickerComponentInstanceContext =
createComponentInstanceContext();

View File

@ -1,5 +0,0 @@
import { createContext } from 'react';
export const RelationPickerRecoilScopeContext = createContext<string | null>(
'relation-picker-context',
);

View File

@ -0,0 +1,10 @@
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const recordPickerPreselectedIdComponentState = createComponentStateV2<
string | undefined
>({
key: 'recordPickerPreselectedIdComponentState',
defaultValue: undefined,
componentInstanceContext: RecordPickerComponentInstanceContext,
});

View File

@ -0,0 +1,9 @@
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const recordPickerSearchFilterComponentState =
createComponentStateV2<string>({
key: 'recordPickerSearchFilterComponentState',
defaultValue: '',
componentInstanceContext: RecordPickerComponentInstanceContext,
});

View File

@ -1,8 +0,0 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const relationPickerPreselectedIdScopedState = createComponentState<
string | undefined
>({
key: 'relationPickerPreselectedIdScopedState',
defaultValue: undefined,
});

View File

@ -1,7 +0,0 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const relationPickerSearchFilterScopedState =
createComponentState<string>({
key: 'relationPickerSearchFilterScopedState',
defaultValue: '',
});

View File

@ -0,0 +1,10 @@
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
import { SearchQuery } from '@/object-record/relation-picker/types/SearchQuery';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const searchQueryComponentState =
createComponentStateV2<SearchQuery | null>({
key: 'searchQueryComponentState',
defaultValue: null,
componentInstanceContext: RecordPickerComponentInstanceContext,
});

View File

@ -1,7 +0,0 @@
import { SearchQuery } from '@/object-record/relation-picker/types/SearchQuery';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const searchQueryScopedState = createComponentState<SearchQuery | null>({
key: 'searchQueryScopedState',
defaultValue: null,
});

View File

@ -1,10 +0,0 @@
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
export type EntitiesForMultipleEntitySelect<
CustomEntityForSelect extends EntityForSelect,
> = {
selectedEntities: CustomEntityForSelect[];
filteredSelectedEntities: CustomEntityForSelect[];
entitiesToSelect: CustomEntityForSelect[];
loading: boolean;
};

View File

@ -1,6 +1,6 @@
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
export type EntityForSelect = ObjectRecordIdentifier & {
export type RecordForSelect = ObjectRecordIdentifier & {
record: ObjectRecord;
};

View File

@ -0,0 +1,10 @@
import { RecordForSelect } from '@/object-record/relation-picker/types/RecordForSelect';
export type RecordsForMultipleRecordSelect<
CustomRecordForSelect extends RecordForSelect,
> = {
selectedRecords: CustomRecordForSelect[];
filteredSelectedRecords: CustomRecordForSelect[];
recordsToSelect: CustomRecordForSelect[];
loading: boolean;
};

View File

@ -1,31 +0,0 @@
import { relationPickerPreselectedIdScopedState } from '@/object-record/relation-picker/states/relationPickerPreselectedIdScopedState';
import { relationPickerSearchFilterScopedState } from '@/object-record/relation-picker/states/relationPickerSearchFilterScopedState';
import { searchQueryScopedState } from '@/object-record/relation-picker/states/searchQueryScopedState';
import { getScopedStateDeprecated } from '@/ui/utilities/recoil-scope/utils/getScopedStateDeprecated';
export const getRelationPickerScopedStates = ({
relationPickerScopeId,
}: {
relationPickerScopeId: string;
}) => {
const searchQueryState = getScopedStateDeprecated(
searchQueryScopedState,
relationPickerScopeId,
);
const relationPickerPreselectedIdState = getScopedStateDeprecated(
relationPickerPreselectedIdScopedState,
relationPickerScopeId,
);
const relationPickerSearchFilterState = getScopedStateDeprecated(
relationPickerSearchFilterScopedState,
relationPickerScopeId,
);
return {
relationPickerSearchFilterState,
relationPickerPreselectedIdState,
searchQueryState,
};
};