feat: add Relation Field Card plus button in Show Page (#3229)

Closes #3124
This commit is contained in:
Thaïs
2024-01-09 06:29:01 -03:00
committed by GitHub
parent dc94d26997
commit ed06cc0310
12 changed files with 306 additions and 209 deletions

View File

@ -1,7 +1,5 @@
import { useCallback, useContext, useState } from 'react'; import { useCallback, useContext, useState } from 'react';
import { useQuery } from '@apollo/client';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { NewButton } from '@/object-record/record-board/components/NewButton'; import { NewButton } from '@/object-record/record-board/components/NewButton';
import { BoardColumnContext } from '@/object-record/record-board/contexts/BoardColumnContext'; import { BoardColumnContext } from '@/object-record/record-board/contexts/BoardColumnContext';
@ -54,18 +52,10 @@ export const NewOpportunityButton = () => {
setIsCreatingCard(false); setIsCreatingCard(false);
}; };
const { relationPickerSearchFilter } = useRelationPicker(); const { relationPickerSearchFilter, identifiersMapper, searchQuery } =
useRelationPicker();
// TODO: refactor useFilteredSearchEntityQuery
const { findManyRecordsQuery } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Company,
});
const useFindManyQuery = (options: any) =>
useQuery(findManyRecordsQuery, options);
const { identifiersMapper, searchQuery } = useRelationPicker();
const filteredSearchEntityResults = useFilteredSearchEntityQuery({ const filteredSearchEntityResults = useFilteredSearchEntityQuery({
queryHook: useFindManyQuery,
filters: [ filters: [
{ {
fieldNames: searchQuery?.computeFilterFields?.('company') ?? [], fieldNames: searchQuery?.computeFilterFields?.('company') ?? [],

View File

@ -1,10 +1,8 @@
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from '@apollo/client';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { SingleEntitySelectBase } from '@/object-record/relation-picker/components/SingleEntitySelectBase'; import { SingleEntitySelectMenuItems } from '@/object-record/relation-picker/components/SingleEntitySelectMenuItems';
import { useEntitySelectSearch } from '@/object-record/relation-picker/hooks/useEntitySelectSearch'; import { useEntitySelectSearch } from '@/object-record/relation-picker/hooks/useEntitySelectSearch';
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker'; import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
@ -36,16 +34,9 @@ export const OpportunityPicker = ({
const { searchFilter, handleSearchFilterChange } = useEntitySelectSearch(); const { searchFilter, handleSearchFilterChange } = useEntitySelectSearch();
// TODO: refactor useFilteredSearchEntityQuery
const { findManyRecordsQuery: findManyCompanies } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Company,
});
const useFindManyQuery = (options: any) =>
useQuery(findManyCompanies, options);
const { identifiersMapper, searchQuery } = useRelationPicker(); const { identifiersMapper, searchQuery } = useRelationPicker();
const filteredSearchEntityResults = useFilteredSearchEntityQuery({ const filteredSearchEntityResults = useFilteredSearchEntityQuery({
queryHook: useFindManyQuery,
filters: [ filters: [
{ {
fieldNames: searchQuery?.computeFilterFields?.('company') ?? [], fieldNames: searchQuery?.computeFilterFields?.('company') ?? [],
@ -127,7 +118,7 @@ export const OpportunityPicker = ({
/> />
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<RecoilScope> <RecoilScope>
<SingleEntitySelectBase <SingleEntitySelectMenuItems
entitiesToSelect={filteredSearchEntityResults.entitiesToSelect} entitiesToSelect={filteredSearchEntityResults.entitiesToSelect}
loading={filteredSearchEntityResults.loading} loading={filteredSearchEntityResults.loading}
onCancel={onCancel} onCancel={onCancel}

View File

@ -95,7 +95,7 @@ export const usePersistField = () => {
where: { id: entityId }, where: { id: entityId },
updateOneRecordInput: { updateOneRecordInput: {
[`${fieldName}Id`]: valueToPersist?.id ?? null, [`${fieldName}Id`]: valueToPersist?.id ?? null,
[`${fieldName}`]: valueToPersist ?? null, [fieldName]: valueToPersist ?? null,
}, },
}, },
}); });

View File

@ -0,0 +1,24 @@
import { useRecoilCallback } from 'recoil';
import { entityFieldsFamilySelector } from '@/object-record/field/states/selectors/entityFieldsFamilySelector';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useUpsertRecordFieldFromState = () =>
useRecoilCallback(
({ set }) =>
<T extends { id: string }, F extends keyof T>({
record,
fieldName,
}: {
record: T;
fieldName: F extends string ? F : never;
}) =>
set(
entityFieldsFamilySelector({ entityId: record.id, fieldName }),
(previousField) =>
isDeeplyEqual(previousField, record[fieldName])
? previousField
: record[fieldName],
),
[],
);

View File

@ -6,15 +6,10 @@ import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
// TODO: refactor with scoped state later // TODO: refactor with scoped state later
export const useUpsertRecordFromState = () => export const useUpsertRecordFromState = () =>
useRecoilCallback( useRecoilCallback(
({ set, snapshot }) => ({ set }) =>
<T extends { id: string }>(entity: T) => { <T extends { id: string }>(record: T) =>
const currentEntity = snapshot set(entityFieldsFamilyState(record.id), (previousRecord) =>
.getLoadable(entityFieldsFamilyState(entity.id)) isDeeplyEqual(previousRecord, record) ? previousRecord : record,
.valueOrThrow(); ),
if (!isDeeplyEqual(currentEntity, entity)) {
set(entityFieldsFamilyState(entity.id), entity);
}
},
[], [],
); );

View File

@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/components/MultipleEntitySelect'; import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/components/MultipleEntitySelect';
import { SingleEntitySelectBase } from '@/object-record/relation-picker/components/SingleEntitySelectBase'; import { SingleEntitySelectMenuItems } from '@/object-record/relation-picker/components/SingleEntitySelectMenuItems';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
@ -99,18 +99,16 @@ export const ObjectFilterDropdownEntitySearchSelect = ({
]); ]);
return ( return (
<> <SingleEntitySelectMenuItems
<SingleEntitySelectBase entitiesToSelect={entitiesForSelect.entitiesToSelect}
entitiesToSelect={entitiesForSelect.entitiesToSelect} selectedEntity={entitiesForSelect.selectedEntities[0]}
selectedEntity={entitiesForSelect.selectedEntities[0]} loading={entitiesForSelect.loading}
loading={entitiesForSelect.loading} onEntitySelected={handleRecordSelected}
onEntitySelected={handleRecordSelected} SelectAllIcon={filterDefinitionUsedInDropdown?.SelectAllIcon}
SelectAllIcon={filterDefinitionUsedInDropdown?.SelectAllIcon} selectAllLabel={filterDefinitionUsedInDropdown?.selectAllLabel}
selectAllLabel={filterDefinitionUsedInDropdown?.selectAllLabel} isAllEntitySelected={isAllEntitySelected}
isAllEntitySelected={isAllEntitySelected} isAllEntitySelectShown={isAllEntitySelectShown}
isAllEntitySelectShown={isAllEntitySelectShown} onAllEntitySelected={handleAllEntitySelectClick}
onAllEntitySelected={handleAllEntitySelectClick} />
/>
</>
); );
}; };

View File

@ -1,31 +1,73 @@
import { useContext, useEffect, useMemo } from 'react'; import { useCallback, useContext, useEffect, useMemo } from 'react';
import { css } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { FieldContext } from '@/object-record/field/contexts/FieldContext'; import { FieldContext } from '@/object-record/field/contexts/FieldContext';
import { usePersistField } from '@/object-record/field/hooks/usePersistField';
import { entityFieldsFamilyState } from '@/object-record/field/states/entityFieldsFamilyState';
import { entityFieldsFamilySelector } from '@/object-record/field/states/selectors/entityFieldsFamilySelector'; import { entityFieldsFamilySelector } from '@/object-record/field/states/selectors/entityFieldsFamilySelector';
import { FieldRelationMetadata } from '@/object-record/field/types/FieldMetadata'; import { FieldRelationMetadata } from '@/object-record/field/types/FieldMetadata';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useUpsertRecordFromState } from '@/object-record/hooks/useUpsertRecordFromState'; import { useUpsertRecordFromState } from '@/object-record/hooks/useUpsertRecordFromState';
import { RecordRelationFieldCardContent } from '@/object-record/record-relation-card/components/RecordRelationFieldCardContent'; import { RecordRelationFieldCardContent } from '@/object-record/record-relation-card/components/RecordRelationFieldCardContent';
import { SingleEntitySelectMenuItemsWithSearch } from '@/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch';
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
import { IconForbid, IconPlus } from '@/ui/display/icon';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { Card } from '@/ui/layout/card/components/Card'; import { Card } from '@/ui/layout/card/components/Card';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { Section } from '@/ui/layout/section/components/Section'; import { Section } from '@/ui/layout/section/components/Section';
const StyledAddDropdown = styled(Dropdown)`
margin-left: auto;
`;
const StyledHeader = styled.header<{ isDropdownOpen?: boolean }>`
align-items: center;
display: flex;
margin-bottom: ${({ theme }) => theme.spacing(2)};
${({ isDropdownOpen, theme }) =>
isDropdownOpen
? ''
: css`
.displayOnHover {
opacity: 0;
pointer-events: none;
transition: opacity ${theme.animation.duration.instant}s ease;
}
`}
&:hover {
.displayOnHover {
opacity: 1;
pointer-events: auto;
}
}
`;
const StyledTitle = styled.div` const StyledTitle = styled.div`
font-weight: ${({ theme }) => theme.font.weight.medium}; font-weight: ${({ theme }) => theme.font.weight.medium};
margin-bottom: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(0, 1)}; padding: ${({ theme }) => theme.spacing(0, 1)};
`; `;
export const RecordRelationFieldCardSection = () => { export const RecordRelationFieldCardSection = () => {
const { entityId, fieldDefinition } = useContext(FieldContext); const { entityId, fieldDefinition } = useContext(FieldContext);
const { const {
fieldName,
relationFieldMetadataId, relationFieldMetadataId,
relationObjectMetadataNameSingular, relationObjectMetadataNameSingular,
relationType, relationType,
} = fieldDefinition.metadata as FieldRelationMetadata; } = fieldDefinition.metadata as FieldRelationMetadata;
const record = useRecoilValue(entityFieldsFamilyState(entityId));
const { const {
labelIdentifierFieldMetadata: relationLabelIdentifierFieldMetadata, labelIdentifierFieldMetadata: relationLabelIdentifierFieldMetadata,
@ -40,16 +82,11 @@ export const RecordRelationFieldCardSection = () => {
const fieldValue = useRecoilValue< const fieldValue = useRecoilValue<
({ id: string } & Record<string, any>) | null ({ id: string } & Record<string, any>) | null
>( >(entityFieldsFamilySelector({ entityId, fieldName }));
entityFieldsFamilySelector({
entityId,
fieldName: fieldDefinition.metadata.fieldName,
}),
);
const isToOneObject = relationType === 'TO_ONE_OBJECT'; const isToOneObject = relationType === 'TO_ONE_OBJECT';
const { record: recordFromFieldValue } = useFindOneRecord({ const { record: relationRecordFromFieldValue } = useFindOneRecord({
objectNameSingular: relationObjectMetadataNameSingular, objectNameSingular: relationObjectMetadataNameSingular,
objectRecordId: fieldValue?.id, objectRecordId: fieldValue?.id,
skip: !relationLabelIdentifierFieldMetadata || !isToOneObject, skip: !relationLabelIdentifierFieldMetadata || !isToOneObject,
@ -58,9 +95,8 @@ export const RecordRelationFieldCardSection = () => {
// ONE_TO_MANY records cannot be retrieved from the field value, // ONE_TO_MANY records cannot be retrieved from the field value,
// as the record's field is an empty "Connection" object. // as the record's field is an empty "Connection" object.
// TODO: maybe the backend could return an array of related records instead? // TODO: maybe the backend could return an array of related records instead?
const { records } = useFindManyRecords({ const { records: relationRecordsFromQuery } = useFindManyRecords({
objectNameSingular: relationObjectMetadataNameSingular, objectNameSingular: relationObjectMetadataNameSingular,
limit: 5,
filter: { filter: {
// TODO: this won't work for MANY_TO_MANY relations. // TODO: this won't work for MANY_TO_MANY relations.
[`${relationFieldMetadataItem?.name}Id`]: { [`${relationFieldMetadataItem?.name}Id`]: {
@ -74,28 +110,120 @@ export const RecordRelationFieldCardSection = () => {
}); });
const relationRecords = useMemo( const relationRecords = useMemo(
() => (recordFromFieldValue ? [recordFromFieldValue] : records), () =>
[recordFromFieldValue, records], relationRecordFromFieldValue
? [relationRecordFromFieldValue]
: relationRecordsFromQuery,
[relationRecordFromFieldValue, relationRecordsFromQuery],
);
const relationRecordIds = useMemo(
() => relationRecords.map(({ id }) => id),
[relationRecords],
); );
const upsertRecordFromState = useUpsertRecordFromState(); const upsertRecordFromState = useUpsertRecordFromState();
useEffect(() => { useEffect(() => {
if (!relationRecords.length) return;
relationRecords.forEach((relationRecord) => relationRecords.forEach((relationRecord) =>
upsertRecordFromState(relationRecord), upsertRecordFromState(relationRecord),
); );
}, [relationRecords, upsertRecordFromState]); }, [relationRecords, upsertRecordFromState]);
const dropdownScopeId = `record-field-card-relation-picker-${fieldDefinition.label}`;
const { closeDropdown, isDropdownOpen } = useDropdown(dropdownScopeId);
const {
identifiersMapper,
relationPickerSearchFilter,
searchQuery,
setRelationPickerSearchFilter,
} = useRelationPicker();
const entities = useFilteredSearchEntityQuery({
filters: [
{
fieldNames:
searchQuery?.computeFilterFields?.(
relationObjectMetadataNameSingular,
) ?? [],
filter: relationPickerSearchFilter,
},
],
orderByField: 'createdAt',
mappingFunction: (recordToMap: any) =>
identifiersMapper?.(recordToMap, relationObjectMetadataNameSingular),
selectedIds: relationRecordIds,
excludeEntityIds: relationRecordIds,
objectNameSingular: relationObjectMetadataNameSingular,
});
const handleCloseRelationPickerDropdown = useCallback(() => {
setRelationPickerSearchFilter('');
}, [setRelationPickerSearchFilter]);
const persistField = usePersistField();
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular: relationObjectMetadataNameSingular,
});
const handleRelationPickerEntitySelected = (
selectedRelationEntity?: EntityForSelect,
) => {
closeDropdown();
if (!selectedRelationEntity?.id) return;
if (isToOneObject) {
persistField(selectedRelationEntity.record);
return;
}
if (!relationFieldMetadataItem?.name) return;
updateOneRecord({
idToUpdate: selectedRelationEntity.id,
updateOneRecordInput: {
[`${relationFieldMetadataItem.name}Id`]: entityId,
[relationFieldMetadataItem.name]: record,
},
});
};
if (!relationLabelIdentifierFieldMetadata) return null; if (!relationLabelIdentifierFieldMetadata) return null;
return ( return (
<Section> <Section>
<StyledTitle>{fieldDefinition.label}</StyledTitle> <StyledHeader isDropdownOpen={isDropdownOpen}>
<StyledTitle>{fieldDefinition.label}</StyledTitle>
<DropdownScope dropdownScopeId={dropdownScopeId}>
<StyledAddDropdown
dropdownPlacement="right-start"
onClose={handleCloseRelationPickerDropdown}
clickableComponent={
<LightIconButton
className="displayOnHover"
Icon={IconPlus}
accent="tertiary"
/>
}
dropdownComponents={
<SingleEntitySelectMenuItemsWithSearch
EmptyIcon={IconForbid}
entitiesToSelect={entities.entitiesToSelect}
loading={entities.loading}
onEntitySelected={handleRelationPickerEntitySelected}
/>
}
dropdownHotkeyScope={{
scope: dropdownScopeId,
}}
/>
</DropdownScope>
</StyledHeader>
{!!relationRecords.length && ( {!!relationRecords.length && (
<Card> <Card>
{relationRecords.map((relationRecord, index) => ( {relationRecords.slice(0, 5).map((relationRecord, index) => (
<RecordRelationFieldCardContent <RecordRelationFieldCardContent
key={`${relationRecord.id}${relationLabelIdentifierFieldMetadata?.id}`} key={`${relationRecord.id}${relationLabelIdentifierFieldMetadata?.id}`}
divider={index < relationRecords.length - 1} divider={index < relationRecords.length - 1}

View File

@ -1,7 +1,5 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useQuery } from '@apollo/client';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { FieldDefinition } from '@/object-record/field/types/FieldDefinition'; import { FieldDefinition } from '@/object-record/field/types/FieldDefinition';
import { FieldRelationMetadata } from '@/object-record/field/types/FieldMetadata'; import { FieldRelationMetadata } from '@/object-record/field/types/FieldMetadata';
@ -13,7 +11,7 @@ import { IconForbid } from '@/ui/display/icon';
export type RelationPickerProps = { export type RelationPickerProps = {
recordId?: string; recordId?: string;
onSubmit: (newUser: EntityForSelect | null) => void; onSubmit: (selectedEntity: EntityForSelect | null) => void;
onCancel?: () => void; onCancel?: () => void;
width?: number; width?: number;
excludeRecordIds?: string[]; excludeRecordIds?: string[];
@ -30,32 +28,24 @@ export const RelationPicker = ({
initialSearchFilter, initialSearchFilter,
fieldDefinition, fieldDefinition,
}: RelationPickerProps) => { }: RelationPickerProps) => {
const { relationPickerSearchFilter, setRelationPickerSearchFilter } = const {
useRelationPicker(); relationPickerSearchFilter,
setRelationPickerSearchFilter,
identifiersMapper,
searchQuery,
} = useRelationPicker();
useEffect(() => { useEffect(() => {
setRelationPickerSearchFilter(initialSearchFilter ?? ''); setRelationPickerSearchFilter(initialSearchFilter ?? '');
}, [initialSearchFilter, setRelationPickerSearchFilter]); }, [initialSearchFilter, setRelationPickerSearchFilter]);
// TODO: refactor useFilteredSearchEntityQuery
const { findManyRecordsQuery } = useObjectMetadataItem({
objectNameSingular:
fieldDefinition.metadata.relationObjectMetadataNameSingular,
});
const useFindManyQuery = (options: any) =>
useQuery(findManyRecordsQuery, options);
const { identifiersMapper, searchQuery } = useRelationPicker();
const { objectNameSingular: relationObjectNameSingular } = const { objectNameSingular: relationObjectNameSingular } =
useObjectNameSingularFromPlural({ useObjectNameSingularFromPlural({
objectNamePlural: objectNamePlural:
fieldDefinition.metadata.relationObjectMetadataNamePlural, fieldDefinition.metadata.relationObjectMetadataNamePlural,
}); });
const records = useFilteredSearchEntityQuery({ const entities = useFilteredSearchEntityQuery({
queryHook: useFindManyQuery,
filters: [ filters: [
{ {
fieldNames: fieldNames:
@ -76,19 +66,18 @@ export const RelationPicker = ({
objectNameSingular: relationObjectNameSingular, objectNameSingular: relationObjectNameSingular,
}); });
const handleEntitySelected = async (selectedUser: any | null | undefined) => { const handleEntitySelected = (selectedEntity: any | null | undefined) =>
onSubmit(selectedUser ?? null); onSubmit(selectedEntity ?? null);
};
return ( return (
<SingleEntitySelect <SingleEntitySelect
EmptyIcon={IconForbid} EmptyIcon={IconForbid}
emptyLabel={'No ' + fieldDefinition.label} emptyLabel={'No ' + fieldDefinition.label}
entitiesToSelect={records.entitiesToSelect} entitiesToSelect={entities.entitiesToSelect}
loading={records.loading} loading={entities.loading}
onCancel={onCancel} onCancel={onCancel}
onEntitySelected={handleEntitySelected} onEntitySelected={handleEntitySelected}
selectedEntity={records.selectedEntities[0]} selectedEntity={entities.selectedEntities[0]}
width={width} width={width}
/> />
); );

View File

@ -1,32 +1,16 @@
import { useRef } from 'react'; import { useRef } from 'react';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isDefined } from '~/utils/isDefined';
import { useEntitySelectSearch } from '../hooks/useEntitySelectSearch';
import { import {
SingleEntitySelectBase, SingleEntitySelectMenuItemsWithSearch,
SingleEntitySelectBaseProps, SingleEntitySelectMenuItemsWithSearchProps,
} from './SingleEntitySelectBase'; } from '@/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
export type SingleEntitySelectProps = { export type SingleEntitySelectProps = {
disableBackgroundBlur?: boolean; disableBackgroundBlur?: boolean;
onCreate?: () => void;
width?: number; width?: number;
} & Pick< } & SingleEntitySelectMenuItemsWithSearchProps;
SingleEntitySelectBaseProps,
| 'EmptyIcon'
| 'emptyLabel'
| 'entitiesToSelect'
| 'loading'
| 'onCancel'
| 'onEntitySelected'
| 'selectedEntity'
>;
export const SingleEntitySelect = ({ export const SingleEntitySelect = ({
EmptyIcon, EmptyIcon,
@ -42,10 +26,6 @@ export const SingleEntitySelect = ({
}: SingleEntitySelectProps) => { }: SingleEntitySelectProps) => {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const { searchFilter, handleSearchFilterChange } = useEntitySelectSearch();
const showCreateButton = isDefined(onCreate) && searchFilter !== '';
useListenClickOutside({ useListenClickOutside({
refs: [containerRef], refs: [containerRef],
callback: (event) => { callback: (event) => {
@ -62,13 +42,7 @@ export const SingleEntitySelect = ({
width={width} width={width}
data-select-disable data-select-disable
> >
<DropdownMenuSearchInput <SingleEntitySelectMenuItemsWithSearch
value={searchFilter}
onChange={handleSearchFilterChange}
autoFocus
/>
<DropdownMenuSeparator />
<SingleEntitySelectBase
{...{ {...{
EmptyIcon, EmptyIcon,
emptyLabel, emptyLabel,
@ -78,7 +52,6 @@ export const SingleEntitySelect = ({
onCreate, onCreate,
onEntitySelected, onEntitySelected,
selectedEntity, selectedEntity,
showCreateButton,
}} }}
/> />
</DropdownMenu> </DropdownMenu>

View File

@ -5,6 +5,8 @@ import { Key } from 'ts-key-enum';
import { SelectableMenuItemSelect } from '@/object-record/relation-picker/components/SelectableMenuItemSelect'; import { SelectableMenuItemSelect } from '@/object-record/relation-picker/components/SelectableMenuItemSelect';
import { IconPlus } from '@/ui/display/icon'; import { IconPlus } from '@/ui/display/icon';
import { IconComponent } from '@/ui/display/icon/types/IconComponent'; import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton';
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
@ -13,12 +15,10 @@ import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSel
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { assertNotNull } from '~/utils/assert'; import { assertNotNull } from '~/utils/assert';
import { CreateNewButton } from '../../../ui/input/relation-picker/components/CreateNewButton';
import { DropdownMenuSkeletonItem } from '../../../ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
import { EntityForSelect } from '../types/EntityForSelect'; import { EntityForSelect } from '../types/EntityForSelect';
import { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope'; import { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope';
export type SingleEntitySelectBaseProps = { export type SingleEntitySelectMenuItemsProps = {
EmptyIcon?: IconComponent; EmptyIcon?: IconComponent;
emptyLabel?: string; emptyLabel?: string;
entitiesToSelect: EntityForSelect[]; entitiesToSelect: EntityForSelect[];
@ -35,7 +35,7 @@ export type SingleEntitySelectBaseProps = {
onAllEntitySelected?: () => void; onAllEntitySelected?: () => void;
}; };
export const SingleEntitySelectBase = ({ export const SingleEntitySelectMenuItems = ({
EmptyIcon, EmptyIcon,
emptyLabel, emptyLabel,
entitiesToSelect, entitiesToSelect,
@ -50,7 +50,7 @@ export const SingleEntitySelectBase = ({
isAllEntitySelected, isAllEntitySelected,
isAllEntitySelectShown, isAllEntitySelectShown,
onAllEntitySelected, onAllEntitySelected,
}: SingleEntitySelectBaseProps) => { }: SingleEntitySelectMenuItemsProps) => {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const entitiesInDropdown = [selectedEntity, ...entitiesToSelect].filter( const entitiesInDropdown = [selectedEntity, ...entitiesToSelect].filter(

View File

@ -0,0 +1,61 @@
import {
SingleEntitySelectMenuItems,
SingleEntitySelectMenuItemsProps,
} from '@/object-record/relation-picker/components/SingleEntitySelectMenuItems';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { isDefined } from '~/utils/isDefined';
import { useEntitySelectSearch } from '../hooks/useEntitySelectSearch';
export type SingleEntitySelectMenuItemsWithSearchProps = {
onCreate?: () => void;
} & Pick<
SingleEntitySelectMenuItemsProps,
| 'EmptyIcon'
| 'emptyLabel'
| 'entitiesToSelect'
| 'loading'
| 'onCancel'
| 'onEntitySelected'
| 'selectedEntity'
>;
export const SingleEntitySelectMenuItemsWithSearch = ({
EmptyIcon,
emptyLabel,
entitiesToSelect,
loading,
onCancel,
onCreate,
onEntitySelected,
selectedEntity,
}: SingleEntitySelectMenuItemsWithSearchProps) => {
const { searchFilter, handleSearchFilterChange } = useEntitySelectSearch();
const showCreateButton = isDefined(onCreate) && searchFilter !== '';
return (
<>
<DropdownMenuSearchInput
value={searchFilter}
onChange={handleSearchFilterChange}
autoFocus
/>
<DropdownMenuSeparator />
<SingleEntitySelectMenuItems
{...{
EmptyIcon,
emptyLabel,
entitiesToSelect,
loading,
onCancel,
onCreate,
onEntitySelected,
selectedEntity,
showCreateButton,
}}
/>
</>
);
};

View File

@ -1,11 +1,9 @@
import { QueryHookOptions, QueryResult } from '@apollo/client';
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { OrderBy } from '@/object-metadata/types/OrderBy'; import { OrderBy } from '@/object-metadata/types/OrderBy';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/components/MultipleEntitySelect'; import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/components/MultipleEntitySelect';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { mapPaginatedRecordsToRecords } from '@/object-record/utils/mapPaginatedRecordsToRecords';
import { assertNotNull } from '~/utils/assert'; import { assertNotNull } from '~/utils/assert';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
@ -16,9 +14,7 @@ export const DEFAULT_SEARCH_REQUEST_LIMIT = 60;
// TODO: use this for all search queries, because we need selectedEntities and entitiesToSelect each time we want to search // TODO: use this for all search queries, because we need selectedEntities and entitiesToSelect each time we want to search
// Filtered entities to select are // Filtered entities to select are
// TODO: replace query hooks by useFindManyRecords
export const useFilteredSearchEntityQuery = ({ export const useFilteredSearchEntityQuery = ({
queryHook,
orderByField, orderByField,
filters, filters,
sortOrder = 'AscNullsLast', sortOrder = 'AscNullsLast',
@ -28,9 +24,6 @@ export const useFilteredSearchEntityQuery = ({
excludeEntityIds = [], excludeEntityIds = [],
objectNameSingular, objectNameSingular,
}: { }: {
queryHook: (
queryOptions?: QueryHookOptions<any, any>,
) => QueryResult<any, any>;
orderByField: string; orderByField: string;
filters: SearchFilter[]; filters: SearchFilter[];
sortOrder?: OrderBy; sortOrder?: OrderBy;
@ -40,22 +33,11 @@ export const useFilteredSearchEntityQuery = ({
excludeEntityIds?: string[]; excludeEntityIds?: string[];
objectNameSingular: string; objectNameSingular: string;
}): EntitiesForMultipleEntitySelect<EntityForSelect> => { }): EntitiesForMultipleEntitySelect<EntityForSelect> => {
const { objectMetadataItem } = useObjectMetadataItem({ const { loading: selectedRecordsLoading, records: selectedRecords } =
objectNameSingular, useFindManyRecords({
}); objectNameSingular,
filter: { id: { in: selectedIds } },
const { loading: selectedEntitiesLoading, data: selectedEntitiesData } = orderBy: { [orderByField]: sortOrder },
queryHook({
variables: {
filter: {
id: {
in: selectedIds,
},
},
orderBy: {
[orderByField]: sortOrder,
},
} as any,
}); });
const searchFilter = filters const searchFilter = filters
@ -90,74 +72,40 @@ export const useFilteredSearchEntityQuery = ({
.filter(isDefined); .filter(isDefined);
const { const {
loading: filteredSelectedEntitiesLoading, loading: filteredSelectedRecordsLoading,
data: filteredSelectedEntitiesData, records: filteredSelectedRecords,
} = queryHook({ } = useFindManyRecords({
variables: { objectNameSingular,
filter: { filter: { and: [{ and: searchFilter }, { id: { in: selectedIds } }] },
and: [ orderBy: { [orderByField]: sortOrder },
{
and: searchFilter,
},
{
id: {
in: selectedIds,
},
},
],
},
orderBy: {
[orderByField]: sortOrder,
},
} as any,
}); });
const { loading: entitiesToSelectLoading, data: entitiesToSelectData } = const { loading: recordsToSelectLoading, records: recordsToSelect } =
queryHook({ useFindManyRecords({
variables: { objectNameSingular,
filter: { filter: {
and: [ and: [
{ { and: searchFilter },
and: searchFilter, { not: { id: { in: [...selectedIds, ...excludeEntityIds] } } },
}, ],
{ },
not: { limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
id: { orderBy: { [orderByField]: sortOrder },
in: [...selectedIds, ...excludeEntityIds],
},
},
},
],
},
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
orderBy: {
[orderByField]: sortOrder,
},
} as any,
}); });
return { return {
selectedEntities: mapPaginatedRecordsToRecords({ selectedEntities: selectedRecords
objectNamePlural: objectMetadataItem.namePlural,
pagedRecords: selectedEntitiesData,
})
.map(mappingFunction) .map(mappingFunction)
.filter(assertNotNull), .filter(assertNotNull),
filteredSelectedEntities: mapPaginatedRecordsToRecords({ filteredSelectedEntities: filteredSelectedRecords
objectNamePlural: objectMetadataItem.namePlural,
pagedRecords: filteredSelectedEntitiesData,
})
.map(mappingFunction) .map(mappingFunction)
.filter(assertNotNull), .filter(assertNotNull),
entitiesToSelect: mapPaginatedRecordsToRecords({ entitiesToSelect: recordsToSelect
objectNamePlural: objectMetadataItem.namePlural,
pagedRecords: entitiesToSelectData,
})
.map(mappingFunction) .map(mappingFunction)
.filter(assertNotNull), .filter(assertNotNull),
loading: loading:
entitiesToSelectLoading || recordsToSelectLoading ||
filteredSelectedEntitiesLoading || filteredSelectedRecordsLoading ||
selectedEntitiesLoading, selectedRecordsLoading,
}; };
}; };