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:
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -6,5 +6,6 @@ export type Filter = {
|
||||
type: FilterType;
|
||||
value: string;
|
||||
displayValue: string;
|
||||
displayAvatarUrl?: string;
|
||||
operand: FilterOperand;
|
||||
};
|
||||
|
||||
@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user