refactor: apply relation optimistic effects on record update (#3556)
* refactor: apply relation optimistic effects on record update Related to #3509 * refactor: remove need to pass relation id field to create and update mutations * fix: fix tests * fix: fix SingleEntitySelect glitch * fix: fix usePersistField tests * fix: fix wrong import after rebase * fix: fix several tests * fix: fix test types
This commit is contained in:
@ -5,7 +5,6 @@ import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldM
|
||||
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
|
||||
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 } from '@/ui/display/icon';
|
||||
|
||||
export type RelationPickerProps = {
|
||||
@ -27,33 +26,15 @@ export const RelationPicker = ({
|
||||
initialSearchFilter,
|
||||
fieldDefinition,
|
||||
}: RelationPickerProps) => {
|
||||
const {
|
||||
relationPickerSearchFilter,
|
||||
setRelationPickerSearchFilter,
|
||||
searchQuery,
|
||||
} = useRelationPicker({ relationPickerScopeId: 'relation-picker' });
|
||||
const relationPickerScopeId = 'relation-picker';
|
||||
const { setRelationPickerSearchFilter } = useRelationPicker({
|
||||
relationPickerScopeId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setRelationPickerSearchFilter(initialSearchFilter ?? '');
|
||||
}, [initialSearchFilter, setRelationPickerSearchFilter]);
|
||||
|
||||
const entities = useFilteredSearchEntityQuery({
|
||||
filters: [
|
||||
{
|
||||
fieldNames:
|
||||
searchQuery?.computeFilterFields?.(
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
) ?? [],
|
||||
filter: relationPickerSearchFilter,
|
||||
},
|
||||
],
|
||||
orderByField: 'createdAt',
|
||||
selectedIds: recordId ? [recordId] : [],
|
||||
excludeEntityIds: excludeRecordIds,
|
||||
objectNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const handleEntitySelected = (
|
||||
selectedEntity: EntityForSelect | null | undefined,
|
||||
) => onSubmit(selectedEntity ?? null);
|
||||
@ -62,12 +43,15 @@ export const RelationPicker = ({
|
||||
<SingleEntitySelect
|
||||
EmptyIcon={IconForbid}
|
||||
emptyLabel={'No ' + fieldDefinition.label}
|
||||
entitiesToSelect={entities.entitiesToSelect}
|
||||
loading={entities.loading}
|
||||
onCancel={onCancel}
|
||||
onEntitySelected={handleEntitySelected}
|
||||
selectedEntity={entities.selectedEntities[0]}
|
||||
width={width}
|
||||
relationObjectNameSingular={
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular
|
||||
}
|
||||
relationPickerScopeId={relationPickerScopeId}
|
||||
selectedRelationRecordIds={recordId ? [recordId] : []}
|
||||
excludedRelationRecordIds={excludeRecordIds}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -13,15 +13,17 @@ export type SingleEntitySelectProps = {
|
||||
} & SingleEntitySelectMenuItemsWithSearchProps;
|
||||
|
||||
export const SingleEntitySelect = ({
|
||||
EmptyIcon,
|
||||
disableBackgroundBlur = false,
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
entitiesToSelect,
|
||||
loading,
|
||||
excludedRelationRecordIds,
|
||||
onCancel,
|
||||
onCreate,
|
||||
onEntitySelected,
|
||||
relationObjectNameSingular,
|
||||
relationPickerScopeId,
|
||||
selectedEntity,
|
||||
selectedRelationRecordIds,
|
||||
width = 200,
|
||||
}: SingleEntitySelectProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@ -52,12 +54,14 @@ export const SingleEntitySelect = ({
|
||||
{...{
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
entitiesToSelect,
|
||||
loading,
|
||||
excludedRelationRecordIds,
|
||||
onCancel,
|
||||
onCreate,
|
||||
onEntitySelected,
|
||||
relationObjectNameSingular,
|
||||
relationPickerScopeId,
|
||||
selectedEntity,
|
||||
selectedRelationRecordIds,
|
||||
}}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
|
||||
@ -86,58 +86,54 @@ export const SingleEntitySelectMenuItems = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{loading ? (
|
||||
<DropdownMenuSkeletonItem />
|
||||
) : entitiesInDropdown.length === 0 && !isAllEntitySelectShown ? (
|
||||
<MenuItem text="No result" />
|
||||
) : (
|
||||
<>
|
||||
{isAllEntitySelectShown &&
|
||||
selectAllLabel &&
|
||||
onAllEntitySelected && (
|
||||
<MenuItemSelect
|
||||
key="select-all"
|
||||
onClick={() => onAllEntitySelected()}
|
||||
LeftIcon={SelectAllIcon}
|
||||
text={selectAllLabel}
|
||||
selected={!!isAllEntitySelected}
|
||||
/>
|
||||
)}
|
||||
{emptyLabel && (
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{loading ? (
|
||||
<DropdownMenuSkeletonItem />
|
||||
) : entitiesInDropdown.length === 0 && !isAllEntitySelectShown ? (
|
||||
<MenuItem text="No result" />
|
||||
) : (
|
||||
<>
|
||||
{isAllEntitySelectShown &&
|
||||
selectAllLabel &&
|
||||
onAllEntitySelected && (
|
||||
<MenuItemSelect
|
||||
key="select-none"
|
||||
onClick={() => onEntitySelected()}
|
||||
LeftIcon={EmptyIcon}
|
||||
text={emptyLabel}
|
||||
selected={!selectedEntity}
|
||||
key="select-all"
|
||||
onClick={() => onAllEntitySelected()}
|
||||
LeftIcon={SelectAllIcon}
|
||||
text={selectAllLabel}
|
||||
selected={!!isAllEntitySelected}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{entitiesInDropdown?.map((entity) => (
|
||||
<SelectableMenuItemSelect
|
||||
key={entity.id}
|
||||
entity={entity}
|
||||
onEntitySelected={onEntitySelected}
|
||||
selectedEntity={selectedEntity}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
{showCreateButton && !loading && (
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{entitiesToSelect.length > 0 && <DropdownMenuSeparator />}
|
||||
<CreateNewButton
|
||||
onClick={onCreate}
|
||||
LeftIcon={IconPlus}
|
||||
text="Add New"
|
||||
{emptyLabel && (
|
||||
<MenuItemSelect
|
||||
key="select-none"
|
||||
onClick={() => onEntitySelected()}
|
||||
LeftIcon={EmptyIcon}
|
||||
text={emptyLabel}
|
||||
selected={!selectedEntity}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{entitiesInDropdown?.map((entity) => (
|
||||
<SelectableMenuItemSelect
|
||||
key={entity.id}
|
||||
entity={entity}
|
||||
onEntitySelected={onEntitySelected}
|
||||
selectedEntity={selectedEntity}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
))}
|
||||
{showCreateButton && !loading && (
|
||||
<>
|
||||
{entitiesToSelect.length > 0 && <DropdownMenuSeparator />}
|
||||
<CreateNewButton
|
||||
onClick={onCreate}
|
||||
LeftIcon={IconPlus}
|
||||
text="Add New"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
</SelectableList>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect';
|
||||
import {
|
||||
SingleEntitySelectMenuItems,
|
||||
SingleEntitySelectMenuItemsProps,
|
||||
} from '@/object-record/relation-picker/components/SingleEntitySelectMenuItems';
|
||||
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
@ -9,13 +11,15 @@ import { isDefined } from '~/utils/isDefined';
|
||||
import { useEntitySelectSearch } from '../hooks/useEntitySelectSearch';
|
||||
|
||||
export type SingleEntitySelectMenuItemsWithSearchProps = {
|
||||
excludedRelationRecordIds?: string[];
|
||||
onCreate?: () => void;
|
||||
relationObjectNameSingular: string;
|
||||
relationPickerScopeId?: string;
|
||||
selectedRelationRecordIds: string[];
|
||||
} & Pick<
|
||||
SingleEntitySelectMenuItemsProps,
|
||||
| 'EmptyIcon'
|
||||
| 'emptyLabel'
|
||||
| 'entitiesToSelect'
|
||||
| 'loading'
|
||||
| 'onCancel'
|
||||
| 'onEntitySelected'
|
||||
| 'selectedEntity'
|
||||
@ -24,19 +28,41 @@ export type SingleEntitySelectMenuItemsWithSearchProps = {
|
||||
export const SingleEntitySelectMenuItemsWithSearch = ({
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
entitiesToSelect,
|
||||
loading,
|
||||
excludedRelationRecordIds,
|
||||
onCancel,
|
||||
onCreate,
|
||||
onEntitySelected,
|
||||
relationObjectNameSingular,
|
||||
relationPickerScopeId = 'relation-picker',
|
||||
selectedEntity,
|
||||
selectedRelationRecordIds,
|
||||
}: SingleEntitySelectMenuItemsWithSearchProps) => {
|
||||
const { searchFilter, handleSearchFilterChange } = useEntitySelectSearch();
|
||||
const { searchFilter, searchQuery, handleSearchFilterChange } =
|
||||
useEntitySelectSearch({
|
||||
relationPickerScopeId,
|
||||
});
|
||||
|
||||
const showCreateButton = isDefined(onCreate) && searchFilter !== '';
|
||||
|
||||
const entities = useFilteredSearchEntityQuery({
|
||||
filters: [
|
||||
{
|
||||
fieldNames:
|
||||
searchQuery?.computeFilterFields?.(relationObjectNameSingular) ?? [],
|
||||
filter: searchFilter,
|
||||
},
|
||||
],
|
||||
orderByField: 'createdAt',
|
||||
selectedIds: selectedRelationRecordIds,
|
||||
excludeEntityIds: excludedRelationRecordIds,
|
||||
objectNameSingular: relationObjectNameSingular,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<ObjectMetadataItemsRelationPickerEffect
|
||||
relationPickerScopeId={relationPickerScopeId}
|
||||
/>
|
||||
<DropdownMenuSearchInput
|
||||
value={searchFilter}
|
||||
onChange={handleSearchFilterChange}
|
||||
@ -44,15 +70,15 @@ export const SingleEntitySelectMenuItemsWithSearch = ({
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<SingleEntitySelectMenuItems
|
||||
entitiesToSelect={entities.entitiesToSelect}
|
||||
loading={entities.loading}
|
||||
selectedEntity={selectedEntity ?? entities.selectedEntities[0]}
|
||||
{...{
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
entitiesToSelect,
|
||||
loading,
|
||||
onCancel,
|
||||
onCreate,
|
||||
onEntitySelected,
|
||||
selectedEntity,
|
||||
showCreateButton,
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { expect, userEvent, within } from '@storybook/test';
|
||||
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { IconUserCircle } from '@/ui/display/icon';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { RelationPickerDecorator } from '~/testing/decorators/RelationPickerDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { mockedPeopleData } from '~/testing/mock-data/people';
|
||||
import { sleep } from '~/testing/sleep';
|
||||
|
||||
@ -26,7 +30,13 @@ const meta: Meta<typeof SingleEntitySelect> = {
|
||||
ComponentDecorator,
|
||||
ComponentWithRecoilScopeDecorator,
|
||||
RelationPickerDecorator,
|
||||
ObjectMetadataItemsDecorator,
|
||||
SnackBarDecorator,
|
||||
],
|
||||
args: {
|
||||
relationObjectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
||||
selectedRelationRecordIds: [],
|
||||
},
|
||||
argTypes: {
|
||||
selectedEntity: {
|
||||
options: entities.map(({ name }) => name),
|
||||
@ -36,37 +46,8 @@ const meta: Meta<typeof SingleEntitySelect> = {
|
||||
),
|
||||
},
|
||||
},
|
||||
render: ({
|
||||
EmptyIcon,
|
||||
disableBackgroundBlur = false,
|
||||
emptyLabel,
|
||||
loading,
|
||||
onCancel,
|
||||
onCreate,
|
||||
onEntitySelected,
|
||||
selectedEntity,
|
||||
width,
|
||||
}) => {
|
||||
const filteredEntities = entities.filter(
|
||||
(entity) => entity.id !== selectedEntity?.id,
|
||||
);
|
||||
|
||||
return (
|
||||
<SingleEntitySelect
|
||||
{...{
|
||||
EmptyIcon,
|
||||
disableBackgroundBlur,
|
||||
emptyLabel,
|
||||
loading,
|
||||
onCancel,
|
||||
onCreate,
|
||||
onEntitySelected,
|
||||
selectedEntity,
|
||||
width,
|
||||
}}
|
||||
entitiesToSelect={filteredEntities}
|
||||
/>
|
||||
);
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
};
|
||||
|
||||
@ -89,7 +70,7 @@ export const WithEmptyOption: Story = {
|
||||
export const WithSearchFilter: Story = {
|
||||
play: async ({ canvasElement, step }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const searchInput = canvas.getByRole('textbox');
|
||||
const searchInput = await canvas.findByRole('textbox');
|
||||
|
||||
await step('Enter search text', async () => {
|
||||
await sleep(50);
|
||||
|
||||
@ -2,12 +2,17 @@ import debounce from 'lodash.debounce';
|
||||
|
||||
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
||||
|
||||
export const useEntitySelectSearch = () => {
|
||||
export const useEntitySelectSearch = ({
|
||||
relationPickerScopeId,
|
||||
}: {
|
||||
relationPickerScopeId?: string;
|
||||
} = {}) => {
|
||||
const {
|
||||
setRelationPickerPreselectedId,
|
||||
relationPickerSearchFilter,
|
||||
searchQuery,
|
||||
setRelationPickerPreselectedId,
|
||||
setRelationPickerSearchFilter,
|
||||
} = useRelationPicker();
|
||||
} = useRelationPicker({ relationPickerScopeId });
|
||||
|
||||
const debouncedSetSearchFilter = debounce(
|
||||
setRelationPickerSearchFilter,
|
||||
@ -26,6 +31,7 @@ export const useEntitySelectSearch = () => {
|
||||
|
||||
return {
|
||||
searchFilter: relationPickerSearchFilter,
|
||||
searchQuery,
|
||||
handleSearchFilterChange,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user