Feat: Add "All assignees" in Task team member dropdown (#1763)

* implemented all select option FilterDropdownEntitySearchSelect and enabled it for tasks page filter

* created new filter operand IsNotNull for make a select all qraphql query, added internal state for tracking isAllEntitySelected

* used filterCurrentlyEdited to track if isAllEntitySelected is selected

* fixed filter button icon SelectAll Icon
This commit is contained in:
Ayush Agrawal
2023-10-03 20:25:31 +05:30
committed by GitHub
parent aea088df16
commit 77997674e5
11 changed files with 209 additions and 94 deletions

View File

@ -3043,6 +3043,7 @@ export enum ViewFilterOperand {
GreaterThan = 'GreaterThan',
Is = 'Is',
IsNot = 'IsNot',
IsNotNull = 'IsNotNull',
LessThan = 'LessThan'
}

View File

@ -1,6 +1,8 @@
import * as React from 'react';
import { useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react';
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { Avatar, AvatarType } from '@/users/components/Avatar';
import { isNonEmptyString } from '~/utils/isNonEmptyString';
@ -13,6 +15,7 @@ type OwnProps = {
pictureUrl?: string;
avatarType?: AvatarType;
variant?: EntityChipVariant;
LeftIcon?: IconComponent;
};
export enum EntityChipVariant {
@ -27,9 +30,12 @@ export const EntityChip = ({
pictureUrl,
avatarType = 'rounded',
variant = EntityChipVariant.Regular,
LeftIcon,
}: OwnProps) => {
const navigate = useNavigate();
const theme = useTheme();
const handleLinkClick = (event: React.MouseEvent<HTMLDivElement>) => {
if (linkToEntity) {
event.preventDefault();
@ -50,13 +56,17 @@ export const EntityChip = ({
: ChipVariant.Transparent
}
leftComponent={
<Avatar
avatarUrl={pictureUrl}
colorId={entityId}
placeholder={name}
size="sm"
type={avatarType}
/>
LeftIcon ? (
<LeftIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
) : (
<Avatar
avatarUrl={pictureUrl}
colorId={entityId}
placeholder={name}
size="sm"
type={avatarType}
/>
)
}
/>
</div>

View File

@ -33,6 +33,11 @@ export type SingleEntitySelectBaseProps<
selectedEntity?: CustomEntityForSelect;
onCreate?: () => void;
showCreateButton?: boolean;
SelectAllIcon?: IconComponent;
selectAllLabel?: string;
isAllEntitySelected?: boolean;
isAllEntitySelectShown?: boolean;
onAllEntitySelected?: () => void;
};
export const SingleEntitySelectBase = <
@ -47,6 +52,11 @@ export const SingleEntitySelectBase = <
selectedEntity,
onCreate,
showCreateButton,
SelectAllIcon,
selectAllLabel,
isAllEntitySelected,
isAllEntitySelectShown,
onAllEntitySelected,
}: SingleEntitySelectBaseProps<CustomEntityForSelect>) => {
const containerRef = useRef<HTMLDivElement>(null);
@ -94,6 +104,15 @@ export const SingleEntitySelectBase = <
return (
<>
<StyledDropdownMenuItemsContainer ref={containerRef} hasMaxHeight>
{isAllEntitySelectShown && selectAllLabel && onAllEntitySelected && (
<MenuItemSelect
onClick={() => onAllEntitySelected()}
LeftIcon={SelectAllIcon}
text={selectAllLabel}
hovered={preselectedOptionId === EmptyButtonId}
selected={!!isAllEntitySelected}
/>
)}
{emptyLabel && (
<MenuItemSelect
onClick={() => onEntitySelected()}
@ -105,7 +124,7 @@ export const SingleEntitySelectBase = <
)}
{loading ? (
<DropdownMenuSkeletonItem />
) : entitiesInDropdown.length === 0 ? (
) : entitiesInDropdown.length === 0 && !isAllEntitySelectShown ? (
<MenuItem text="No result" />
) : (
entitiesInDropdown?.map((entity) => (

View File

@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { EntitiesForMultipleEntitySelect } from '@/ui/input/relation-picker/components/MultipleEntitySelect';
import { SingleEntitySelectBase } from '@/ui/input/relation-picker/components/SingleEntitySelectBase';
@ -12,6 +12,8 @@ import { filterDropdownSelectedEntityIdScopedState } from '@/ui/view-bar/states/
import { selectedOperandInDropdownScopedState } from '@/ui/view-bar/states/selectedOperandInDropdownScopedState';
import { useViewBarContext } from '../hooks/useViewBarContext';
import { filterDropdownSearchInputScopedState } from '../states/filterDropdownSearchInputScopedState';
import { FilterOperand } from '../types/FilterOperand';
export const FilterDropdownEntitySearchSelect = ({
entitiesForSelect,
@ -20,6 +22,8 @@ export const FilterDropdownEntitySearchSelect = ({
}) => {
const { ViewBarRecoilScopeContext } = useViewBarContext();
const [isAllEntitySelected, setIsAllEntitySelected] = useState(false);
const [filterDropdownSelectedEntityId, setFilterDropdownSelectedEntityId] =
useRecoilScopedState(
filterDropdownSelectedEntityIdScopedState,
@ -52,6 +56,10 @@ export const FilterDropdownEntitySearchSelect = ({
return;
}
if (isAllEntitySelected) {
setIsAllEntitySelected(false);
}
const clickedOnAlreadySelectedEntity =
selectedEntity.id === filterDropdownSelectedEntityId;
@ -72,11 +80,54 @@ export const FilterDropdownEntitySearchSelect = ({
}
};
const [filterDropdownSearchInput] = useRecoilScopedState(
filterDropdownSearchInputScopedState,
ViewBarRecoilScopeContext,
);
const isAllEntitySelectShown =
!!filterDefinitionUsedInDropdown?.selectAllLabel &&
!!filterDefinitionUsedInDropdown?.SelectAllIcon &&
(isAllEntitySelected ||
filterDefinitionUsedInDropdown?.selectAllLabel
.toLocaleLowerCase()
.includes(filterDropdownSearchInput.toLocaleLowerCase()));
const handleAllEntitySelectClick = () => {
if (
!filterDefinitionUsedInDropdown ||
!selectedOperandInDropdown ||
!filterDefinitionUsedInDropdown.selectAllLabel
) {
return;
}
if (isAllEntitySelected) {
setIsAllEntitySelected(false);
removeFilter(filterDefinitionUsedInDropdown.key);
} else {
setIsAllEntitySelected(true);
setFilterDropdownSelectedEntityId(null);
upsertFilter({
displayValue: filterDefinitionUsedInDropdown.selectAllLabel,
key: filterDefinitionUsedInDropdown.key,
operand: FilterOperand.IsNotNull,
type: filterDefinitionUsedInDropdown.type,
value: '',
});
}
};
useEffect(() => {
if (!filterCurrentlyEdited) {
setFilterDropdownSelectedEntityId(null);
} else {
setFilterDropdownSelectedEntityId(filterCurrentlyEdited.value);
setIsAllEntitySelected(
filterCurrentlyEdited.operand === FilterOperand.IsNotNull,
);
}
}, [
filterCurrentlyEdited,
@ -91,6 +142,11 @@ export const FilterDropdownEntitySearchSelect = ({
selectedEntity={entitiesForSelect.selectedEntities[0]}
loading={entitiesForSelect.loading}
onEntitySelected={handleUserSelected}
SelectAllIcon={filterDefinitionUsedInDropdown?.SelectAllIcon}
selectAllLabel={filterDefinitionUsedInDropdown?.selectAllLabel}
isAllEntitySelected={isAllEntitySelected}
isAllEntitySelectShown={isAllEntitySelectShown}
onAllEntitySelected={handleAllEntitySelectClick}
/>
</>
);

View File

@ -1,16 +1,19 @@
import { EntityChip } from '@/ui/chip/components/EntityChip';
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { Filter } from '../types/Filter';
type OwnProps = {
filter: Filter;
Icon?: IconComponent;
};
export const GenericEntityFilterChip = ({ filter }: OwnProps) => (
export const GenericEntityFilterChip = ({ filter, Icon }: OwnProps) => (
<EntityChip
entityId={filter.value}
name={filter.displayValue}
avatarType="rounded"
pictureUrl={filter.displayAvatarUrl}
LeftIcon={Icon}
/>
);

View File

@ -17,6 +17,7 @@ import { useViewBarContext } from '../hooks/useViewBarContext';
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
import { filtersScopedState } from '../states/filtersScopedState';
import { isFilterDropdownUnfoldedScopedState } from '../states/isFilterDropdownUnfoldedScopedState';
import { FilterOperand } from '../types/FilterOperand';
import { getOperandsForFilterType } from '../utils/getOperandsForFilterType';
import { FilterDropdownEntitySearchInput } from './FilterDropdownEntitySearchInput';
@ -100,7 +101,14 @@ export const SingleEntityFilterDropdownButton = ({
onClick={() => handleIsUnfoldedChange(!isFilterDropdownUnfolded)}
>
{filters[0] ? (
<GenericEntityFilterChip filter={filters[0]} />
<GenericEntityFilterChip
filter={filters[0]}
Icon={
filters[0].operand === FilterOperand.IsNotNull
? availableFilter.SelectAllIcon
: undefined
}
/>
) : (
'Filter'
)}

View File

@ -8,4 +8,6 @@ export type FilterDefinition = {
Icon: IconComponent;
type: FilterType;
entitySelectComponent?: JSX.Element;
selectAllLabel?: string;
SelectAllIcon?: IconComponent;
};

View File

@ -14,6 +14,8 @@ export const getOperandLabel = (operand: FilterOperand | null | undefined) => {
return 'Is';
case FilterOperand.IsNot:
return 'Is not';
case FilterOperand.IsNotNull:
return 'Is not null';
default:
return '';
}
@ -29,6 +31,8 @@ export const getOperandLabelShort = (
case FilterOperand.IsNot:
case FilterOperand.DoesNotContain:
return ': Not';
case FilterOperand.IsNotNull:
return ': NotNull';
case FilterOperand.GreaterThan:
return '\u00A0> ';
case FilterOperand.LessThan:

View File

@ -4,88 +4,97 @@ import { Filter } from '../types/Filter';
import { FilterOperand } from '../types/FilterOperand';
export const turnFilterIntoWhereClause = (filter: Filter) => {
switch (filter.type) {
case 'text':
switch (filter.operand) {
case FilterOperand.Contains:
return {
[filter.key]: {
contains: filter.value,
mode: QueryMode.Insensitive,
},
};
case FilterOperand.DoesNotContain:
return {
[filter.key]: {
not: {
contains: filter.value,
mode: QueryMode.Insensitive,
},
},
};
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.type} filter`,
);
}
case 'number':
switch (filter.operand) {
case FilterOperand.GreaterThan:
return {
[filter.key]: {
gte: parseFloat(filter.value),
},
};
case FilterOperand.LessThan:
return {
[filter.key]: {
lte: parseFloat(filter.value),
},
};
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.type} filter`,
);
}
case 'date':
switch (filter.operand) {
case FilterOperand.GreaterThan:
return {
[filter.key]: {
gte: filter.value,
},
};
case FilterOperand.LessThan:
return {
[filter.key]: {
lte: filter.value,
},
};
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.type} filter`,
);
}
case 'entity':
switch (filter.operand) {
case FilterOperand.Is:
return {
[filter.key]: {
equals: filter.value,
},
};
case FilterOperand.IsNot:
return {
[filter.key]: {
not: { equals: filter.value },
},
};
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.type} filter`,
);
}
switch (filter.operand) {
case FilterOperand.IsNotNull:
return {
[filter.key]: {
not: null,
},
};
default:
throw new Error('Unknown filter type');
switch (filter.type) {
case 'text':
switch (filter.operand) {
case FilterOperand.Contains:
return {
[filter.key]: {
contains: filter.value,
mode: QueryMode.Insensitive,
},
};
case FilterOperand.DoesNotContain:
return {
[filter.key]: {
not: {
contains: filter.value,
mode: QueryMode.Insensitive,
},
},
};
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.type} filter`,
);
}
case 'number':
switch (filter.operand) {
case FilterOperand.GreaterThan:
return {
[filter.key]: {
gte: parseFloat(filter.value),
},
};
case FilterOperand.LessThan:
return {
[filter.key]: {
lte: parseFloat(filter.value),
},
};
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.type} filter`,
);
}
case 'date':
switch (filter.operand) {
case FilterOperand.GreaterThan:
return {
[filter.key]: {
gte: filter.value,
},
};
case FilterOperand.LessThan:
return {
[filter.key]: {
lte: filter.value,
},
};
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.type} filter`,
);
}
case 'entity':
switch (filter.operand) {
case FilterOperand.Is:
return {
[filter.key]: {
equals: filter.value,
},
};
case FilterOperand.IsNot:
return {
[filter.key]: {
not: { equals: filter.value },
},
};
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.type} filter`,
);
}
default:
throw new Error('Unknown filter type');
}
}
};

View File

@ -1,5 +1,5 @@
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
import { IconUser } from '@/ui/icon';
import { IconUser, IconUserCircle } from '@/ui/icon';
import { FilterDefinitionByEntity } from '@/ui/view-bar/types/FilterDefinitionByEntity';
import { FilterDropdownUserSearchSelect } from '@/users/components/FilterDropdownUserSearchSelect';
import { Activity } from '~/generated/graphql';
@ -13,5 +13,7 @@ export const tasksFilters: FilterDefinitionByEntity<Activity>[] = [
entitySelectComponent: (
<FilterDropdownUserSearchSelect context={TasksRecoilScopeContext} />
),
selectAllLabel: 'All assignees',
SelectAllIcon: IconUserCircle,
},
];

View File

@ -823,6 +823,7 @@ enum ViewFilterOperand {
LessThan
Is
IsNot
IsNotNull
}
model ViewFilter {