feat: add Relation Field Card plus button in Show Page (#3229)
Closes #3124
This commit is contained in:
@ -1,7 +1,5 @@
|
||||
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 { NewButton } from '@/object-record/record-board/components/NewButton';
|
||||
import { BoardColumnContext } from '@/object-record/record-board/contexts/BoardColumnContext';
|
||||
@ -54,18 +52,10 @@ export const NewOpportunityButton = () => {
|
||||
setIsCreatingCard(false);
|
||||
};
|
||||
|
||||
const { relationPickerSearchFilter } = useRelationPicker();
|
||||
|
||||
// TODO: refactor useFilteredSearchEntityQuery
|
||||
const { findManyRecordsQuery } = useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Company,
|
||||
});
|
||||
const useFindManyQuery = (options: any) =>
|
||||
useQuery(findManyRecordsQuery, options);
|
||||
const { identifiersMapper, searchQuery } = useRelationPicker();
|
||||
const { relationPickerSearchFilter, identifiersMapper, searchQuery } =
|
||||
useRelationPicker();
|
||||
|
||||
const filteredSearchEntityResults = useFilteredSearchEntityQuery({
|
||||
queryHook: useFindManyQuery,
|
||||
filters: [
|
||||
{
|
||||
fieldNames: searchQuery?.computeFilterFields?.('company') ?? [],
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
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 { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||
@ -36,16 +34,9 @@ export const OpportunityPicker = ({
|
||||
|
||||
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 filteredSearchEntityResults = useFilteredSearchEntityQuery({
|
||||
queryHook: useFindManyQuery,
|
||||
filters: [
|
||||
{
|
||||
fieldNames: searchQuery?.computeFilterFields?.('company') ?? [],
|
||||
@ -127,7 +118,7 @@ export const OpportunityPicker = ({
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<RecoilScope>
|
||||
<SingleEntitySelectBase
|
||||
<SingleEntitySelectMenuItems
|
||||
entitiesToSelect={filteredSearchEntityResults.entitiesToSelect}
|
||||
loading={filteredSearchEntityResults.loading}
|
||||
onCancel={onCancel}
|
||||
|
||||
@ -95,7 +95,7 @@ export const usePersistField = () => {
|
||||
where: { id: entityId },
|
||||
updateOneRecordInput: {
|
||||
[`${fieldName}Id`]: valueToPersist?.id ?? null,
|
||||
[`${fieldName}`]: valueToPersist ?? null,
|
||||
[fieldName]: valueToPersist ?? null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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],
|
||||
),
|
||||
[],
|
||||
);
|
||||
@ -6,15 +6,10 @@ import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
// TODO: refactor with scoped state later
|
||||
export const useUpsertRecordFromState = () =>
|
||||
useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
<T extends { id: string }>(entity: T) => {
|
||||
const currentEntity = snapshot
|
||||
.getLoadable(entityFieldsFamilyState(entity.id))
|
||||
.valueOrThrow();
|
||||
|
||||
if (!isDeeplyEqual(currentEntity, entity)) {
|
||||
set(entityFieldsFamilyState(entity.id), entity);
|
||||
}
|
||||
},
|
||||
({ set }) =>
|
||||
<T extends { id: string }>(record: T) =>
|
||||
set(entityFieldsFamilyState(record.id), (previousRecord) =>
|
||||
isDeeplyEqual(previousRecord, record) ? previousRecord : record,
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
|
||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||
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 { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
@ -99,18 +99,16 @@ export const ObjectFilterDropdownEntitySearchSelect = ({
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SingleEntitySelectBase
|
||||
entitiesToSelect={entitiesForSelect.entitiesToSelect}
|
||||
selectedEntity={entitiesForSelect.selectedEntities[0]}
|
||||
loading={entitiesForSelect.loading}
|
||||
onEntitySelected={handleRecordSelected}
|
||||
SelectAllIcon={filterDefinitionUsedInDropdown?.SelectAllIcon}
|
||||
selectAllLabel={filterDefinitionUsedInDropdown?.selectAllLabel}
|
||||
isAllEntitySelected={isAllEntitySelected}
|
||||
isAllEntitySelectShown={isAllEntitySelectShown}
|
||||
onAllEntitySelected={handleAllEntitySelectClick}
|
||||
/>
|
||||
</>
|
||||
<SingleEntitySelectMenuItems
|
||||
entitiesToSelect={entitiesForSelect.entitiesToSelect}
|
||||
selectedEntity={entitiesForSelect.selectedEntities[0]}
|
||||
loading={entitiesForSelect.loading}
|
||||
onEntitySelected={handleRecordSelected}
|
||||
SelectAllIcon={filterDefinitionUsedInDropdown?.SelectAllIcon}
|
||||
selectAllLabel={filterDefinitionUsedInDropdown?.selectAllLabel}
|
||||
isAllEntitySelected={isAllEntitySelected}
|
||||
isAllEntitySelectShown={isAllEntitySelectShown}
|
||||
onAllEntitySelected={handleAllEntitySelectClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
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 { FieldRelationMetadata } from '@/object-record/field/types/FieldMetadata';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { useUpsertRecordFromState } from '@/object-record/hooks/useUpsertRecordFromState';
|
||||
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 { 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';
|
||||
|
||||
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`
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
padding: ${({ theme }) => theme.spacing(0, 1)};
|
||||
`;
|
||||
|
||||
export const RecordRelationFieldCardSection = () => {
|
||||
const { entityId, fieldDefinition } = useContext(FieldContext);
|
||||
const {
|
||||
fieldName,
|
||||
relationFieldMetadataId,
|
||||
relationObjectMetadataNameSingular,
|
||||
relationType,
|
||||
} = fieldDefinition.metadata as FieldRelationMetadata;
|
||||
const record = useRecoilValue(entityFieldsFamilyState(entityId));
|
||||
|
||||
const {
|
||||
labelIdentifierFieldMetadata: relationLabelIdentifierFieldMetadata,
|
||||
@ -40,16 +82,11 @@ export const RecordRelationFieldCardSection = () => {
|
||||
|
||||
const fieldValue = useRecoilValue<
|
||||
({ id: string } & Record<string, any>) | null
|
||||
>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId,
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
}),
|
||||
);
|
||||
>(entityFieldsFamilySelector({ entityId, fieldName }));
|
||||
|
||||
const isToOneObject = relationType === 'TO_ONE_OBJECT';
|
||||
|
||||
const { record: recordFromFieldValue } = useFindOneRecord({
|
||||
const { record: relationRecordFromFieldValue } = useFindOneRecord({
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
objectRecordId: fieldValue?.id,
|
||||
skip: !relationLabelIdentifierFieldMetadata || !isToOneObject,
|
||||
@ -58,9 +95,8 @@ export const RecordRelationFieldCardSection = () => {
|
||||
// ONE_TO_MANY records cannot be retrieved from the field value,
|
||||
// as the record's field is an empty "Connection" object.
|
||||
// TODO: maybe the backend could return an array of related records instead?
|
||||
const { records } = useFindManyRecords({
|
||||
const { records: relationRecordsFromQuery } = useFindManyRecords({
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
limit: 5,
|
||||
filter: {
|
||||
// TODO: this won't work for MANY_TO_MANY relations.
|
||||
[`${relationFieldMetadataItem?.name}Id`]: {
|
||||
@ -74,28 +110,120 @@ export const RecordRelationFieldCardSection = () => {
|
||||
});
|
||||
|
||||
const relationRecords = useMemo(
|
||||
() => (recordFromFieldValue ? [recordFromFieldValue] : records),
|
||||
[recordFromFieldValue, records],
|
||||
() =>
|
||||
relationRecordFromFieldValue
|
||||
? [relationRecordFromFieldValue]
|
||||
: relationRecordsFromQuery,
|
||||
[relationRecordFromFieldValue, relationRecordsFromQuery],
|
||||
);
|
||||
const relationRecordIds = useMemo(
|
||||
() => relationRecords.map(({ id }) => id),
|
||||
[relationRecords],
|
||||
);
|
||||
|
||||
const upsertRecordFromState = useUpsertRecordFromState();
|
||||
|
||||
useEffect(() => {
|
||||
if (!relationRecords.length) return;
|
||||
|
||||
relationRecords.forEach((relationRecord) =>
|
||||
upsertRecordFromState(relationRecord),
|
||||
);
|
||||
}, [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;
|
||||
|
||||
return (
|
||||
<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 && (
|
||||
<Card>
|
||||
{relationRecords.map((relationRecord, index) => (
|
||||
{relationRecords.slice(0, 5).map((relationRecord, index) => (
|
||||
<RecordRelationFieldCardContent
|
||||
key={`${relationRecord.id}${relationLabelIdentifierFieldMetadata?.id}`}
|
||||
divider={index < relationRecords.length - 1}
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useQuery } from '@apollo/client';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||
import { FieldDefinition } from '@/object-record/field/types/FieldDefinition';
|
||||
import { FieldRelationMetadata } from '@/object-record/field/types/FieldMetadata';
|
||||
@ -13,7 +11,7 @@ import { IconForbid } from '@/ui/display/icon';
|
||||
|
||||
export type RelationPickerProps = {
|
||||
recordId?: string;
|
||||
onSubmit: (newUser: EntityForSelect | null) => void;
|
||||
onSubmit: (selectedEntity: EntityForSelect | null) => void;
|
||||
onCancel?: () => void;
|
||||
width?: number;
|
||||
excludeRecordIds?: string[];
|
||||
@ -30,32 +28,24 @@ export const RelationPicker = ({
|
||||
initialSearchFilter,
|
||||
fieldDefinition,
|
||||
}: RelationPickerProps) => {
|
||||
const { relationPickerSearchFilter, setRelationPickerSearchFilter } =
|
||||
useRelationPicker();
|
||||
const {
|
||||
relationPickerSearchFilter,
|
||||
setRelationPickerSearchFilter,
|
||||
identifiersMapper,
|
||||
searchQuery,
|
||||
} = useRelationPicker();
|
||||
|
||||
useEffect(() => {
|
||||
setRelationPickerSearchFilter(initialSearchFilter ?? '');
|
||||
}, [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 } =
|
||||
useObjectNameSingularFromPlural({
|
||||
objectNamePlural:
|
||||
fieldDefinition.metadata.relationObjectMetadataNamePlural,
|
||||
});
|
||||
|
||||
const records = useFilteredSearchEntityQuery({
|
||||
queryHook: useFindManyQuery,
|
||||
const entities = useFilteredSearchEntityQuery({
|
||||
filters: [
|
||||
{
|
||||
fieldNames:
|
||||
@ -76,19 +66,18 @@ export const RelationPicker = ({
|
||||
objectNameSingular: relationObjectNameSingular,
|
||||
});
|
||||
|
||||
const handleEntitySelected = async (selectedUser: any | null | undefined) => {
|
||||
onSubmit(selectedUser ?? null);
|
||||
};
|
||||
const handleEntitySelected = (selectedEntity: any | null | undefined) =>
|
||||
onSubmit(selectedEntity ?? null);
|
||||
|
||||
return (
|
||||
<SingleEntitySelect
|
||||
EmptyIcon={IconForbid}
|
||||
emptyLabel={'No ' + fieldDefinition.label}
|
||||
entitiesToSelect={records.entitiesToSelect}
|
||||
loading={records.loading}
|
||||
entitiesToSelect={entities.entitiesToSelect}
|
||||
loading={entities.loading}
|
||||
onCancel={onCancel}
|
||||
onEntitySelected={handleEntitySelected}
|
||||
selectedEntity={records.selectedEntities[0]}
|
||||
selectedEntity={entities.selectedEntities[0]}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -1,32 +1,16 @@
|
||||
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 {
|
||||
SingleEntitySelectBase,
|
||||
SingleEntitySelectBaseProps,
|
||||
} from './SingleEntitySelectBase';
|
||||
SingleEntitySelectMenuItemsWithSearch,
|
||||
SingleEntitySelectMenuItemsWithSearchProps,
|
||||
} 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 = {
|
||||
disableBackgroundBlur?: boolean;
|
||||
onCreate?: () => void;
|
||||
width?: number;
|
||||
} & Pick<
|
||||
SingleEntitySelectBaseProps,
|
||||
| 'EmptyIcon'
|
||||
| 'emptyLabel'
|
||||
| 'entitiesToSelect'
|
||||
| 'loading'
|
||||
| 'onCancel'
|
||||
| 'onEntitySelected'
|
||||
| 'selectedEntity'
|
||||
>;
|
||||
} & SingleEntitySelectMenuItemsWithSearchProps;
|
||||
|
||||
export const SingleEntitySelect = ({
|
||||
EmptyIcon,
|
||||
@ -42,10 +26,6 @@ export const SingleEntitySelect = ({
|
||||
}: SingleEntitySelectProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { searchFilter, handleSearchFilterChange } = useEntitySelectSearch();
|
||||
|
||||
const showCreateButton = isDefined(onCreate) && searchFilter !== '';
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [containerRef],
|
||||
callback: (event) => {
|
||||
@ -62,13 +42,7 @@ export const SingleEntitySelect = ({
|
||||
width={width}
|
||||
data-select-disable
|
||||
>
|
||||
<DropdownMenuSearchInput
|
||||
value={searchFilter}
|
||||
onChange={handleSearchFilterChange}
|
||||
autoFocus
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<SingleEntitySelectBase
|
||||
<SingleEntitySelectMenuItemsWithSearch
|
||||
{...{
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
@ -78,7 +52,6 @@ export const SingleEntitySelect = ({
|
||||
onCreate,
|
||||
onEntitySelected,
|
||||
selectedEntity,
|
||||
showCreateButton,
|
||||
}}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
|
||||
@ -5,6 +5,8 @@ import { Key } from 'ts-key-enum';
|
||||
import { SelectableMenuItemSelect } from '@/object-record/relation-picker/components/SelectableMenuItemSelect';
|
||||
import { IconPlus } from '@/ui/display/icon';
|
||||
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 { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
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 { 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 { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope';
|
||||
|
||||
export type SingleEntitySelectBaseProps = {
|
||||
export type SingleEntitySelectMenuItemsProps = {
|
||||
EmptyIcon?: IconComponent;
|
||||
emptyLabel?: string;
|
||||
entitiesToSelect: EntityForSelect[];
|
||||
@ -35,7 +35,7 @@ export type SingleEntitySelectBaseProps = {
|
||||
onAllEntitySelected?: () => void;
|
||||
};
|
||||
|
||||
export const SingleEntitySelectBase = ({
|
||||
export const SingleEntitySelectMenuItems = ({
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
entitiesToSelect,
|
||||
@ -50,7 +50,7 @@ export const SingleEntitySelectBase = ({
|
||||
isAllEntitySelected,
|
||||
isAllEntitySelectShown,
|
||||
onAllEntitySelected,
|
||||
}: SingleEntitySelectBaseProps) => {
|
||||
}: SingleEntitySelectMenuItemsProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const entitiesInDropdown = [selectedEntity, ...entitiesToSelect].filter(
|
||||
@ -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,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,11 +1,9 @@
|
||||
import { QueryHookOptions, QueryResult } from '@apollo/client';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { OrderBy } from '@/object-metadata/types/OrderBy';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/components/MultipleEntitySelect';
|
||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||
import { mapPaginatedRecordsToRecords } from '@/object-record/utils/mapPaginatedRecordsToRecords';
|
||||
import { assertNotNull } from '~/utils/assert';
|
||||
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
|
||||
// Filtered entities to select are
|
||||
|
||||
// TODO: replace query hooks by useFindManyRecords
|
||||
export const useFilteredSearchEntityQuery = ({
|
||||
queryHook,
|
||||
orderByField,
|
||||
filters,
|
||||
sortOrder = 'AscNullsLast',
|
||||
@ -28,9 +24,6 @@ export const useFilteredSearchEntityQuery = ({
|
||||
excludeEntityIds = [],
|
||||
objectNameSingular,
|
||||
}: {
|
||||
queryHook: (
|
||||
queryOptions?: QueryHookOptions<any, any>,
|
||||
) => QueryResult<any, any>;
|
||||
orderByField: string;
|
||||
filters: SearchFilter[];
|
||||
sortOrder?: OrderBy;
|
||||
@ -40,22 +33,11 @@ export const useFilteredSearchEntityQuery = ({
|
||||
excludeEntityIds?: string[];
|
||||
objectNameSingular: string;
|
||||
}): EntitiesForMultipleEntitySelect<EntityForSelect> => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { loading: selectedEntitiesLoading, data: selectedEntitiesData } =
|
||||
queryHook({
|
||||
variables: {
|
||||
filter: {
|
||||
id: {
|
||||
in: selectedIds,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
[orderByField]: sortOrder,
|
||||
},
|
||||
} as any,
|
||||
const { loading: selectedRecordsLoading, records: selectedRecords } =
|
||||
useFindManyRecords({
|
||||
objectNameSingular,
|
||||
filter: { id: { in: selectedIds } },
|
||||
orderBy: { [orderByField]: sortOrder },
|
||||
});
|
||||
|
||||
const searchFilter = filters
|
||||
@ -90,74 +72,40 @@ export const useFilteredSearchEntityQuery = ({
|
||||
.filter(isDefined);
|
||||
|
||||
const {
|
||||
loading: filteredSelectedEntitiesLoading,
|
||||
data: filteredSelectedEntitiesData,
|
||||
} = queryHook({
|
||||
variables: {
|
||||
filter: {
|
||||
and: [
|
||||
{
|
||||
and: searchFilter,
|
||||
},
|
||||
{
|
||||
id: {
|
||||
in: selectedIds,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
orderBy: {
|
||||
[orderByField]: sortOrder,
|
||||
},
|
||||
} as any,
|
||||
loading: filteredSelectedRecordsLoading,
|
||||
records: filteredSelectedRecords,
|
||||
} = useFindManyRecords({
|
||||
objectNameSingular,
|
||||
filter: { and: [{ and: searchFilter }, { id: { in: selectedIds } }] },
|
||||
orderBy: { [orderByField]: sortOrder },
|
||||
});
|
||||
|
||||
const { loading: entitiesToSelectLoading, data: entitiesToSelectData } =
|
||||
queryHook({
|
||||
variables: {
|
||||
filter: {
|
||||
and: [
|
||||
{
|
||||
and: searchFilter,
|
||||
},
|
||||
{
|
||||
not: {
|
||||
id: {
|
||||
in: [...selectedIds, ...excludeEntityIds],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
|
||||
orderBy: {
|
||||
[orderByField]: sortOrder,
|
||||
},
|
||||
} as any,
|
||||
const { loading: recordsToSelectLoading, records: recordsToSelect } =
|
||||
useFindManyRecords({
|
||||
objectNameSingular,
|
||||
filter: {
|
||||
and: [
|
||||
{ and: searchFilter },
|
||||
{ not: { id: { in: [...selectedIds, ...excludeEntityIds] } } },
|
||||
],
|
||||
},
|
||||
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
|
||||
orderBy: { [orderByField]: sortOrder },
|
||||
});
|
||||
|
||||
return {
|
||||
selectedEntities: mapPaginatedRecordsToRecords({
|
||||
objectNamePlural: objectMetadataItem.namePlural,
|
||||
pagedRecords: selectedEntitiesData,
|
||||
})
|
||||
selectedEntities: selectedRecords
|
||||
.map(mappingFunction)
|
||||
.filter(assertNotNull),
|
||||
filteredSelectedEntities: mapPaginatedRecordsToRecords({
|
||||
objectNamePlural: objectMetadataItem.namePlural,
|
||||
pagedRecords: filteredSelectedEntitiesData,
|
||||
})
|
||||
filteredSelectedEntities: filteredSelectedRecords
|
||||
.map(mappingFunction)
|
||||
.filter(assertNotNull),
|
||||
entitiesToSelect: mapPaginatedRecordsToRecords({
|
||||
objectNamePlural: objectMetadataItem.namePlural,
|
||||
pagedRecords: entitiesToSelectData,
|
||||
})
|
||||
entitiesToSelect: recordsToSelect
|
||||
.map(mappingFunction)
|
||||
.filter(assertNotNull),
|
||||
loading:
|
||||
entitiesToSelectLoading ||
|
||||
filteredSelectedEntitiesLoading ||
|
||||
selectedEntitiesLoading,
|
||||
recordsToSelectLoading ||
|
||||
filteredSelectedRecordsLoading ||
|
||||
selectedRecordsLoading,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user