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:
@ -3043,6 +3043,7 @@ export enum ViewFilterOperand {
|
||||
GreaterThan = 'GreaterThan',
|
||||
Is = 'Is',
|
||||
IsNot = 'IsNot',
|
||||
IsNotNull = 'IsNotNull',
|
||||
LessThan = 'LessThan'
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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) => (
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -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'
|
||||
)}
|
||||
|
||||
@ -8,4 +8,6 @@ export type FilterDefinition = {
|
||||
Icon: IconComponent;
|
||||
type: FilterType;
|
||||
entitySelectComponent?: JSX.Element;
|
||||
selectAllLabel?: string;
|
||||
SelectAllIcon?: IconComponent;
|
||||
};
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@ -823,6 +823,7 @@ enum ViewFilterOperand {
|
||||
LessThan
|
||||
Is
|
||||
IsNot
|
||||
IsNotNull
|
||||
}
|
||||
|
||||
model ViewFilter {
|
||||
|
||||
Reference in New Issue
Block a user