Fix activity target bug (#3390)

This commit is contained in:
Charles Bochet
2024-01-11 20:18:54 +01:00
committed by GitHub
parent 2c8c22a979
commit 99247fb689
10 changed files with 157 additions and 211 deletions

View File

@ -18,6 +18,7 @@ export const ActivityTargetChips = ({
<StyledContainer> <StyledContainer>
{activityTargetObjectRecords?.map((activityTargetObjectRecord) => ( {activityTargetObjectRecords?.map((activityTargetObjectRecord) => (
<RecordChip <RecordChip
key={activityTargetObjectRecord.targetObjectRecord.id}
record={activityTargetObjectRecord.targetObjectRecord} record={activityTargetObjectRecord.targetObjectRecord}
objectNameSingular={ objectNameSingular={
activityTargetObjectRecord.targetObjectMetadataItem.nameSingular activityTargetObjectRecord.targetObjectMetadataItem.nameSingular

View File

@ -46,9 +46,7 @@ export const ActivityTargetsInlineCell = ({
activityTargetObjectRecords={activityTargetObjectRecords} activityTargetObjectRecords={activityTargetObjectRecords}
/> />
} }
isDisplayModeContentEmpty={ isDisplayModeContentEmpty={activityTargetObjectRecords.length === 0}
activity?.activityTargets?.edges?.length === 0
}
/> />
</RecoilScope> </RecoilScope>
); );

View File

@ -2,8 +2,8 @@ import { useEffect, useState } from 'react';
import { ObjectFilterDropdownId } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; import { ObjectFilterDropdownId } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
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 { SingleEntitySelectMenuItems } from '@/object-record/relation-picker/components/SingleEntitySelectMenuItems'; import { SingleEntitySelectMenuItems } from '@/object-record/relation-picker/components/SingleEntitySelectMenuItems';
import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect';
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';

View File

@ -1,126 +0,0 @@
import { useRef } from 'react';
import { isNonEmptyString } from '@sniptt/guards';
import debounce from 'lodash.debounce';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { Avatar } from '@/users/components/Avatar';
import { EntityForSelect } from '../types/EntityForSelect';
export type EntitiesForMultipleEntitySelect<
CustomEntityForSelect extends EntityForSelect,
> = {
selectedEntities: CustomEntityForSelect[];
filteredSelectedEntities: CustomEntityForSelect[];
entitiesToSelect: CustomEntityForSelect[];
loading: boolean;
};
export const MultipleEntitySelect = <
CustomEntityForSelect extends EntityForSelect,
>({
entities,
onChange,
onSubmit,
onSearchFilterChange,
searchFilter,
value,
}: {
entities: EntitiesForMultipleEntitySelect<CustomEntityForSelect>;
searchFilter: string;
onSearchFilterChange: (newSearchFilter: string) => void;
onChange: (value: Record<string, boolean>) => void;
onCancel?: () => void;
onSubmit?: () => void;
value: Record<string, boolean>;
}) => {
const debouncedSetSearchFilter = debounce(onSearchFilterChange, 100, {
leading: true,
});
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
debouncedSetSearchFilter(event.currentTarget.value);
onSearchFilterChange(event.currentTarget.value);
};
let entitiesInDropdown = [
...(entities.filteredSelectedEntities ?? []),
...(entities.entitiesToSelect ?? []),
];
entitiesInDropdown = entitiesInDropdown.filter((entity) =>
isNonEmptyString(entity.name),
);
const containerRef = useRef<HTMLDivElement>(null);
useListenClickOutside({
refs: [containerRef],
callback: (event) => {
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
onSubmit?.();
},
});
const selectableItemIds = entitiesInDropdown.map((entity) => entity.id);
return (
<DropdownMenu ref={containerRef} data-select-disable>
<DropdownMenuSearchInput
value={searchFilter}
onChange={handleFilterChange}
autoFocus
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
<SelectableList
selectableListId="multiple-entity-select-list"
selectableItemIdArray={selectableItemIds}
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
onEnter={(_itemId) => {
if (_itemId in value === false || value[_itemId] === false) {
onChange({ ...value, [_itemId]: true });
} else {
onChange({ ...value, [_itemId]: false });
}
}}
>
{entitiesInDropdown?.map((entity) => (
<SelectableItem itemId={entity.id} key={entity.id}>
<MenuItemMultiSelectAvatar
key={entity.id}
selected={value[entity.id]}
onSelectChange={(newCheckedValue) =>
onChange({ ...value, [entity.id]: newCheckedValue })
}
avatar={
<Avatar
avatarUrl={entity.avatarUrl}
colorId={entity.id}
placeholder={entity.name}
size="md"
type={entity.avatarType ?? 'rounded'}
/>
}
text={entity.name}
/>
</SelectableItem>
))}
</SelectableList>
{entitiesInDropdown?.length === 0 && <MenuItem text="No result" />}
</DropdownMenuItemsContainer>
</DropdownMenu>
);
};

View File

@ -0,0 +1,22 @@
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
export const MultipleObjectRecordOnClickOutsideEffect = ({
containerRef,
onClickOutside,
}: {
containerRef: React.RefObject<HTMLDivElement>;
onClickOutside: () => void;
}) => {
useListenClickOutside({
refs: [containerRef],
callback: (event) => {
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
onClickOutside();
},
});
return <></>;
};

View File

@ -2,8 +2,10 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
import debounce from 'lodash.debounce'; import debounce from 'lodash.debounce';
import { v4 } from 'uuid';
import { MultipleObjectRecordOnClickOutsideEffect } from '@/object-record/relation-picker/components/MultipleObjectRecordOnClickOutsideEffect';
import { MultipleObjectRecordSelectItem } from '@/object-record/relation-picker/components/MultipleObjectRecordSelectItem';
import { MultiObjectRecordSelectSelectableListId } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
import { import {
ObjectRecordForSelect, ObjectRecordForSelect,
SelectedObjectRecordId, SelectedObjectRecordId,
@ -17,9 +19,6 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { Avatar } from '@/users/components/Avatar';
export const StyledSelectableItem = styled(SelectableItem)` export const StyledSelectableItem = styled(SelectableItem)`
height: 100%; height: 100%;
@ -116,61 +115,61 @@ export const MultipleObjectRecordSelect = ({
[filteredSelectedObjectRecords, objectRecordsToSelect], [filteredSelectedObjectRecords, objectRecordsToSelect],
); );
useListenClickOutside({
refs: [containerRef],
callback: (event) => {
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
onSubmit?.(internalSelectedRecords);
},
});
const selectableItemIds = entitiesInDropdown.map( const selectableItemIds = entitiesInDropdown.map(
(entity) => entity.record.id, (entity) => entity.record.id,
); );
return ( return (
<DropdownMenu ref={containerRef} data-select-disable> <>
<DropdownMenuSearchInput <MultipleObjectRecordOnClickOutsideEffect
value={searchFilter} containerRef={containerRef}
onChange={handleFilterChange} onClickOutside={() => {
autoFocus onSubmit?.(internalSelectedRecords);
}}
/> />
<DropdownMenuSeparator /> <DropdownMenu ref={containerRef} data-select-disable>
<DropdownMenuItemsContainer hasMaxHeight> <DropdownMenuSearchInput
{loading ? ( value={searchFilter}
<MenuItem text="Loading..." /> onChange={handleFilterChange}
) : ( autoFocus
<> />
<SelectableList <DropdownMenuSeparator />
selectableListId="multiple-entity-select-list" <DropdownMenuItemsContainer hasMaxHeight>
selectableItemIdArray={selectableItemIds} {loading ? (
hotkeyScope={RelationPickerHotkeyScope.RelationPicker} <MenuItem text="Loading..." />
onEnter={(recordId) => { ) : (
const recordIsSelected = internalSelectedRecords?.some( <>
(selectedRecord) => selectedRecord.record.id === recordId, <SelectableList
); selectableListId={MultiObjectRecordSelectSelectableListId}
selectableItemIdArray={selectableItemIds}
const correspondingRecordForSelect = entitiesInDropdown?.find( hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
(entity) => entity.record.id === recordId, onEnter={(recordId) => {
); const recordIsSelected = internalSelectedRecords?.some(
(selectedRecord) => selectedRecord.record.id === recordId,
if (correspondingRecordForSelect) {
handleSelectChange(
correspondingRecordForSelect,
!recordIsSelected,
); );
}
}} const correspondingRecordForSelect = entitiesInDropdown?.find(
> (entity) => entity.record.id === recordId,
{entitiesInDropdown?.map((objectRecordForSelect) => ( );
<StyledSelectableItem
itemId={objectRecordForSelect.record.id} if (correspondingRecordForSelect) {
key={objectRecordForSelect.record.id + v4()} handleSelectChange(
> correspondingRecordForSelect,
<MenuItemMultiSelectAvatar !recordIsSelected,
);
}
}}
>
{entitiesInDropdown?.map((objectRecordForSelect) => (
<MultipleObjectRecordSelectItem
key={objectRecordForSelect.record.id}
objectRecordForSelect={objectRecordForSelect}
onSelectedChange={(newSelectedValue) =>
handleSelectChange(
objectRecordForSelect,
newSelectedValue,
)
}
selected={internalSelectedRecords?.some( selected={internalSelectedRecords?.some(
(selectedRecord) => { (selectedRecord) => {
return ( return (
@ -179,34 +178,16 @@ export const MultipleObjectRecordSelect = ({
); );
}, },
)} )}
onSelectChange={(newCheckedValue) =>
handleSelectChange(objectRecordForSelect, newCheckedValue)
}
avatar={
<Avatar
avatarUrl={
objectRecordForSelect.recordIdentifier.avatarUrl
}
colorId={objectRecordForSelect.record.id}
placeholder={
objectRecordForSelect.recordIdentifier.name
}
size="md"
type={
objectRecordForSelect.recordIdentifier.avatarType ??
'rounded'
}
/>
}
text={objectRecordForSelect.recordIdentifier.name}
/> />
</StyledSelectableItem> ))}
))} </SelectableList>
</SelectableList> {entitiesInDropdown?.length === 0 && (
{entitiesInDropdown?.length === 0 && <MenuItem text="No result" />} <MenuItem text="No result" />
</> )}
)} </>
</DropdownMenuItemsContainer> )}
</DropdownMenu> </DropdownMenuItemsContainer>
</DropdownMenu>
</>
); );
}; };

View File

@ -0,0 +1,58 @@
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { v4 } from 'uuid';
import { MultiObjectRecordSelectSelectableListId } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar';
import { Avatar } from '@/users/components/Avatar';
export const StyledSelectableItem = styled(SelectableItem)`
height: 100%;
width: 100%;
`;
export const MultipleObjectRecordSelectItem = ({
objectRecordForSelect,
onSelectedChange,
selected,
}: {
objectRecordForSelect: ObjectRecordForSelect;
onSelectedChange?: (selected: boolean) => void;
selected: boolean;
}) => {
const { isSelectedItemIdSelector } = useSelectableList(
MultiObjectRecordSelectSelectableListId,
);
const isSelectedByKeyboard = useRecoilValue(
isSelectedItemIdSelector(objectRecordForSelect.record.id),
);
return (
<StyledSelectableItem
itemId={objectRecordForSelect.record.id}
key={objectRecordForSelect.record.id + v4()}
>
<MenuItemMultiSelectAvatar
selected={selected}
onSelectChange={onSelectedChange}
isKeySelected={isSelectedByKeyboard}
avatar={
<Avatar
avatarUrl={objectRecordForSelect.recordIdentifier.avatarUrl}
colorId={objectRecordForSelect.record.id}
placeholder={objectRecordForSelect.recordIdentifier.name}
size="md"
type={
objectRecordForSelect.recordIdentifier.avatarType ?? 'rounded'
}
/>
}
text={objectRecordForSelect.recordIdentifier.name}
/>
</StyledSelectableItem>
);
};

View File

@ -0,0 +1,2 @@
export const MultiObjectRecordSelectSelectableListId =
'multi-object-record-select-selectable-list';

View File

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

View File

@ -2,7 +2,7 @@ import { isNonEmptyString } from '@sniptt/guards';
import { OrderBy } from '@/object-metadata/types/OrderBy'; import { OrderBy } from '@/object-metadata/types/OrderBy';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/components/MultipleEntitySelect'; import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { assertNotNull } from '~/utils/assert'; import { assertNotNull } from '~/utils/assert';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';