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:
Thaïs
2024-01-29 08:00:00 -03:00
committed by GitHub
parent d66d8c9907
commit a58b4cf437
43 changed files with 970 additions and 1109 deletions

View File

@ -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}
/>
);
};

View File

@ -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>

View File

@ -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>
);

View File

@ -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,
}}
/>

View File

@ -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);

View File

@ -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,
};
};