Feat/filter activity inbox (#1032)

* Move files

* Add filtering for tasks inbox

* Add filter dropdown for single entity

* Minor

* Fill empty button

* Refine logic for filter dropdown

* remove log

* Fix unwanted change

* Set current user as default filter

* Add avatar on filter

* Improve initialization of assignee filter

* Add story for Tasks page

* Add more stories

* Add sotry with no tasks

* Improve dates

* Enh tests

---------

Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
Emilien Chauvet
2023-08-02 21:36:16 +02:00
committed by GitHub
parent 2128d44212
commit 4252a0a2c3
28 changed files with 601 additions and 189 deletions

View File

@ -1,25 +1,12 @@
import { Context, useCallback, useState } from 'react';
import { Context } from 'react';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/filter-n-sort/states/filterDefinitionUsedInDropdownScopedState';
import { filterDropdownSearchInputScopedState } from '@/ui/filter-n-sort/states/filterDropdownSearchInputScopedState';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '@/ui/filter-n-sort/states/isFilterDropdownOperandSelectUnfoldedScopedState';
import { selectedOperandInDropdownScopedState } from '@/ui/filter-n-sort/states/selectedOperandInDropdownScopedState';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
import DropdownButton from './DropdownButton';
import { FilterDropdownDateSearchInput } from './FilterDropdownDateSearchInput';
import { FilterDropdownEntitySearchInput } from './FilterDropdownEntitySearchInput';
import { FilterDropdownEntitySelect } from './FilterDropdownEntitySelect';
import { FilterDropdownFilterSelect } from './FilterDropdownFilterSelect';
import { FilterDropdownNumberSearchInput } from './FilterDropdownNumberSearchInput';
import { FilterDropdownOperandButton } from './FilterDropdownOperandButton';
import { FilterDropdownOperandSelect } from './FilterDropdownOperandSelect';
import { FilterDropdownTextSearchInput } from './FilterDropdownTextSearchInput';
import { MultipleFiltersDropdownButton } from './MultipleFiltersDropdownButton';
import { SingleEntityFilterDropdownButton } from './SingleEntityFilterDropdownButton';
export function FilterDropdownButton({
context,
@ -28,93 +15,20 @@ export function FilterDropdownButton({
context: Context<string | null>;
HotkeyScope: FiltersHotkeyScope;
}) {
const [isUnfolded, setIsUnfolded] = useState(false);
const [
isFilterDropdownOperandSelectUnfolded,
setIsFilterDropdownOperandSelectUnfolded,
] = useRecoilScopedState(
isFilterDropdownOperandSelectUnfoldedScopedState,
const [availableFilters] = useRecoilScopedState(
availableFiltersScopedState,
context,
);
const [filterDefinitionUsedInDropdown, setFilterDefinitionUsedInDropdown] =
useRecoilScopedState(filterDefinitionUsedInDropdownScopedState, context);
const [, setFilterDropdownSearchInput] = useRecoilScopedState(
filterDropdownSearchInputScopedState,
context,
);
const [filters] = useRecoilScopedState(filtersScopedState, context);
const [selectedOperandInDropdown, setSelectedOperandInDropdown] =
useRecoilScopedState(selectedOperandInDropdownScopedState, context);
const resetState = useCallback(() => {
setIsFilterDropdownOperandSelectUnfolded(false);
setFilterDefinitionUsedInDropdown(null);
setSelectedOperandInDropdown(null);
setFilterDropdownSearchInput('');
}, [
setFilterDefinitionUsedInDropdown,
setSelectedOperandInDropdown,
setFilterDropdownSearchInput,
setIsFilterDropdownOperandSelectUnfolded,
]);
const isFilterSelected = (filters?.length ?? 0) > 0;
const setHotkeyScope = useSetHotkeyScope();
function handleIsUnfoldedChange(newIsUnfolded: boolean) {
if (newIsUnfolded) {
setHotkeyScope(HotkeyScope);
setIsUnfolded(true);
} else {
if (filterDefinitionUsedInDropdown?.type === 'entity') {
setHotkeyScope(HotkeyScope);
}
setIsUnfolded(false);
resetState();
}
}
return (
<DropdownButton
label="Filter"
isActive={isFilterSelected}
isUnfolded={isUnfolded}
onIsUnfoldedChange={handleIsUnfoldedChange}
return availableFilters.length === 1 &&
availableFilters[0].type === 'entity' ? (
<SingleEntityFilterDropdownButton
context={context}
HotkeyScope={HotkeyScope}
>
{!filterDefinitionUsedInDropdown ? (
<FilterDropdownFilterSelect context={context} />
) : isFilterDropdownOperandSelectUnfolded ? (
<FilterDropdownOperandSelect context={context} />
) : (
selectedOperandInDropdown && (
<>
<FilterDropdownOperandButton context={context} />
<DropdownMenuSeparator />
{filterDefinitionUsedInDropdown.type === 'text' && (
<FilterDropdownTextSearchInput context={context} />
)}
{filterDefinitionUsedInDropdown.type === 'number' && (
<FilterDropdownNumberSearchInput context={context} />
)}
{filterDefinitionUsedInDropdown.type === 'date' && (
<FilterDropdownDateSearchInput context={context} />
)}
{filterDefinitionUsedInDropdown.type === 'entity' && (
<FilterDropdownEntitySearchInput context={context} />
)}
{filterDefinitionUsedInDropdown.type === 'entity' && (
<FilterDropdownEntitySelect context={context} />
)}
</>
)
)}
</DropdownButton>
/>
) : (
<MultipleFiltersDropdownButton
context={context}
HotkeyScope={HotkeyScope}
/>
);
}

View File

@ -0,0 +1,18 @@
import { EntityChip } from '@/ui/chip/components/EntityChip';
import { Filter } from '../types/Filter';
type OwnProps = {
filter: Filter;
};
export function GenericEntityFilterChip({ filter }: OwnProps) {
return (
<EntityChip
entityId={filter.value}
name={filter.displayValue}
avatarType="rounded"
pictureUrl={filter.displayAvatarUrl}
/>
);
}

View File

@ -0,0 +1,120 @@
import { Context, useCallback, useState } from 'react';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/filter-n-sort/states/filterDefinitionUsedInDropdownScopedState';
import { filterDropdownSearchInputScopedState } from '@/ui/filter-n-sort/states/filterDropdownSearchInputScopedState';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '@/ui/filter-n-sort/states/isFilterDropdownOperandSelectUnfoldedScopedState';
import { selectedOperandInDropdownScopedState } from '@/ui/filter-n-sort/states/selectedOperandInDropdownScopedState';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
import DropdownButton from './DropdownButton';
import { FilterDropdownDateSearchInput } from './FilterDropdownDateSearchInput';
import { FilterDropdownEntitySearchInput } from './FilterDropdownEntitySearchInput';
import { FilterDropdownEntitySelect } from './FilterDropdownEntitySelect';
import { FilterDropdownFilterSelect } from './FilterDropdownFilterSelect';
import { FilterDropdownNumberSearchInput } from './FilterDropdownNumberSearchInput';
import { FilterDropdownOperandButton } from './FilterDropdownOperandButton';
import { FilterDropdownOperandSelect } from './FilterDropdownOperandSelect';
import { FilterDropdownTextSearchInput } from './FilterDropdownTextSearchInput';
export function MultipleFiltersDropdownButton({
context,
HotkeyScope,
}: {
context: Context<string | null>;
HotkeyScope: FiltersHotkeyScope;
}) {
const [isUnfolded, setIsUnfolded] = useState(false);
const [
isFilterDropdownOperandSelectUnfolded,
setIsFilterDropdownOperandSelectUnfolded,
] = useRecoilScopedState(
isFilterDropdownOperandSelectUnfoldedScopedState,
context,
);
const [filterDefinitionUsedInDropdown, setFilterDefinitionUsedInDropdown] =
useRecoilScopedState(filterDefinitionUsedInDropdownScopedState, context);
const [, setFilterDropdownSearchInput] = useRecoilScopedState(
filterDropdownSearchInputScopedState,
context,
);
const [filters] = useRecoilScopedState(filtersScopedState, context);
const [selectedOperandInDropdown, setSelectedOperandInDropdown] =
useRecoilScopedState(selectedOperandInDropdownScopedState, context);
const resetState = useCallback(() => {
setIsFilterDropdownOperandSelectUnfolded(false);
setFilterDefinitionUsedInDropdown(null);
setSelectedOperandInDropdown(null);
setFilterDropdownSearchInput('');
}, [
setFilterDefinitionUsedInDropdown,
setSelectedOperandInDropdown,
setFilterDropdownSearchInput,
setIsFilterDropdownOperandSelectUnfolded,
]);
const isFilterSelected = (filters?.length ?? 0) > 0;
const setHotkeyScope = useSetHotkeyScope();
function handleIsUnfoldedChange(newIsUnfolded: boolean) {
if (newIsUnfolded) {
setHotkeyScope(HotkeyScope);
setIsUnfolded(true);
} else {
if (filterDefinitionUsedInDropdown?.type === 'entity') {
setHotkeyScope(HotkeyScope);
}
setIsUnfolded(false);
resetState();
}
}
return (
<DropdownButton
label="Filter"
isActive={isFilterSelected}
isUnfolded={isUnfolded}
onIsUnfoldedChange={handleIsUnfoldedChange}
HotkeyScope={HotkeyScope}
>
{!filterDefinitionUsedInDropdown ? (
<FilterDropdownFilterSelect context={context} />
) : isFilterDropdownOperandSelectUnfolded ? (
<FilterDropdownOperandSelect context={context} />
) : (
selectedOperandInDropdown && (
<>
<FilterDropdownOperandButton context={context} />
<DropdownMenuSeparator />
{filterDefinitionUsedInDropdown.type === 'text' && (
<FilterDropdownTextSearchInput context={context} />
)}
{filterDefinitionUsedInDropdown.type === 'number' && (
<FilterDropdownNumberSearchInput context={context} />
)}
{filterDefinitionUsedInDropdown.type === 'date' && (
<FilterDropdownDateSearchInput context={context} />
)}
{filterDefinitionUsedInDropdown.type === 'entity' && (
<FilterDropdownEntitySearchInput context={context} />
)}
{filterDefinitionUsedInDropdown.type === 'entity' && (
<FilterDropdownEntitySelect context={context} />
)}
</>
)
)}
</DropdownButton>
);
}

View File

@ -0,0 +1,131 @@
import { Context, useState } from 'react';
import React from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconChevronDown } from '@tabler/icons-react';
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/filter-n-sort/states/filterDefinitionUsedInDropdownScopedState';
import { filterDropdownSearchInputScopedState } from '@/ui/filter-n-sort/states/filterDropdownSearchInputScopedState';
import { selectedOperandInDropdownScopedState } from '@/ui/filter-n-sort/states/selectedOperandInDropdownScopedState';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
import { filtersScopedState } from '../states/filtersScopedState';
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
import { getOperandsForFilterType } from '../utils/getOperandsForFilterType';
import { DropdownMenuContainer } from './DropdownMenuContainer';
import { FilterDropdownEntitySearchInput } from './FilterDropdownEntitySearchInput';
import { FilterDropdownEntitySelect } from './FilterDropdownEntitySelect';
import { GenericEntityFilterChip } from './GenericEntityFilterChip';
const StyledDropdownButtonContainer = styled.div`
display: flex;
flex-direction: column;
position: relative;
z-index: 1;
`;
type StyledDropdownButtonProps = {
isUnfolded: boolean;
};
const StyledDropdownButton = styled.div<StyledDropdownButtonProps>`
align-items: center;
background: ${({ theme }) => theme.background.primary};
border-radius: ${({ theme }) => theme.border.radius.sm};
cursor: pointer;
display: flex;
filter: ${(props) => (props.isUnfolded ? 'brightness(0.95)' : 'none')};
padding: ${({ theme }) => theme.spacing(1)};
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
&:hover {
filter: brightness(0.95);
}
user-select: none;
`;
export function SingleEntityFilterDropdownButton({
context,
HotkeyScope,
}: {
context: Context<string | null>;
HotkeyScope: FiltersHotkeyScope;
}) {
const theme = useTheme();
const [availableFilters] = useRecoilScopedState(
availableFiltersScopedState,
context,
);
const availableFilter = availableFilters[0];
const [isUnfolded, setIsUnfolded] = useState(false);
const [filters] = useRecoilScopedState(filtersScopedState, context);
const [, setFilterDefinitionUsedInDropdown] = useRecoilScopedState(
filterDefinitionUsedInDropdownScopedState,
context,
);
const [, setFilterDropdownSearchInput] = useRecoilScopedState(
filterDropdownSearchInputScopedState,
context,
);
const [, setSelectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
context,
);
React.useEffect(() => {
setFilterDefinitionUsedInDropdown(availableFilter);
const defaultOperand = getOperandsForFilterType(availableFilter?.type)[0];
setSelectedOperandInDropdown(defaultOperand);
}, [
availableFilter,
setFilterDefinitionUsedInDropdown,
setSelectedOperandInDropdown,
]);
const setHotkeyScope = useSetHotkeyScope();
function handleIsUnfoldedChange(newIsUnfolded: boolean) {
if (newIsUnfolded) {
setHotkeyScope(HotkeyScope);
setIsUnfolded(true);
} else {
setHotkeyScope(HotkeyScope);
setIsUnfolded(false);
setFilterDropdownSearchInput('');
}
}
return (
<StyledDropdownButtonContainer>
<StyledDropdownButton
isUnfolded={isUnfolded}
onClick={() => handleIsUnfoldedChange(!isUnfolded)}
>
{filters[0] ? (
<GenericEntityFilterChip filter={filters[0]} />
) : (
'Filter'
)}
<IconChevronDown size={theme.icon.size.md} />
</StyledDropdownButton>
{isUnfolded && (
<DropdownMenuContainer onClose={() => handleIsUnfoldedChange(false)}>
<FilterDropdownEntitySearchInput context={context} />
<FilterDropdownEntitySelect context={context} />
</DropdownMenuContainer>
)}
</StyledDropdownButtonContainer>
);
}

View File

@ -6,5 +6,6 @@ export type Filter = {
type: FilterType;
value: string;
displayValue: string;
displayAvatarUrl?: string;
operand: FilterOperand;
};

View File

@ -4,7 +4,7 @@ import { v4 as uuidV4 } from 'uuid';
import { Avatar } from '@/users/components/Avatar';
import {
beautifyExactDate,
beautifyExactDateTime,
beautifyPastDateRelativeToNow,
} from '~/utils/date-utils';
@ -71,7 +71,7 @@ export function ShowPageSummaryCard({
}: OwnProps) {
const beautifiedCreatedAt =
date !== '' ? beautifyPastDateRelativeToNow(date) : '';
const exactCreatedAt = date !== '' ? beautifyExactDate(date) : '';
const exactCreatedAt = date !== '' ? beautifyExactDateTime(date) : '';
const dateElementId = `date-id-${uuidV4()}`;
return (