Refator/sorts dropdown (#1568)

* WIP

* Fixed lint

* Ok for sorts

* Fixed on dropdown toggle

* Fix lint
This commit is contained in:
Lucas Bordeau
2023-09-14 01:38:11 +02:00
committed by GitHub
parent a392a81994
commit 8627416d60
55 changed files with 339 additions and 309 deletions

View File

@ -2,11 +2,11 @@ import { LightButton } from '@/ui/button/components/LightButton';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
import { IconPlus } from '@/ui/icon';
import { FilterDropdownKey } from '../types/FilterDropdownKey';
import { FilterDropdownId } from '../constants/FilterDropdownId';
export function AddFilterFromDropdownButton() {
const { toggleDropdownButton } = useDropdownButton({
key: FilterDropdownKey,
dropdownId: FilterDropdownId,
});
function handleClick() {

View File

@ -1,5 +1,6 @@
import { Context } from 'react';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
@ -9,12 +10,12 @@ import { SingleEntityFilterDropdownButton } from './SingleEntityFilterDropdownBu
type FilterDropdownButtonProps = {
context: Context<string | null>;
hotkeyScope: string;
hotkeyScope: HotkeyScope;
};
export function FilterDropdownButton({
context,
hotkeyScope,
context,
}: FilterDropdownButtonProps) {
const [availableFilters] = useRecoilScopedState(
availableFiltersScopedState,

View File

@ -1,11 +1,11 @@
import { StyledHeaderDropdownButton } from '@/ui/dropdown/components/StyledHeaderDropdownButton';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
import { FilterDropdownKey } from '../types/FilterDropdownKey';
import { FilterDropdownId } from '../constants/FilterDropdownId';
export function MultipleFiltersButton() {
const { isDropdownButtonOpen, toggleDropdownButton } = useDropdownButton({
key: FilterDropdownKey,
dropdownId: FilterDropdownId,
});
function handleClick() {

View File

@ -1,24 +1,27 @@
import { Context, useCallback } from 'react';
import { Context, useCallback, useEffect } from 'react';
import { DropdownButton } from '@/ui/dropdown/components/DropdownButton';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { FilterDropdownId } from '../constants/FilterDropdownId';
import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState';
import { filterDropdownSearchInputScopedState } from '../states/filterDropdownSearchInputScopedState';
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '../states/isFilterDropdownOperandSelectUnfoldedScopedState';
import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState';
import { FilterDropdownKey } from '../types/FilterDropdownKey';
import { MultipleFiltersButton } from './MultipleFiltersButton';
import { MultipleFiltersDropdownContent } from './MultipleFiltersDropdownContent';
type MultipleFiltersDropdownButtonProps = {
context: Context<string | null>;
hotkeyScope: string;
hotkeyScope: HotkeyScope;
};
export function MultipleFiltersDropdownButton({
context,
hotkeyScope,
}: MultipleFiltersDropdownButtonProps) {
const [, setIsFilterDropdownOperandSelectUnfolded] = useRecoilScopedState(
isFilterDropdownOperandSelectUnfoldedScopedState,
@ -40,6 +43,10 @@ export function MultipleFiltersDropdownButton({
context,
);
const { isDropdownButtonOpen } = useDropdownButton({
dropdownId: FilterDropdownId,
});
const resetState = useCallback(() => {
setIsFilterDropdownOperandSelectUnfolded(false);
setFilterDefinitionUsedInDropdown(null);
@ -51,14 +58,19 @@ export function MultipleFiltersDropdownButton({
setFilterDropdownSearchInput,
setIsFilterDropdownOperandSelectUnfolded,
]);
useEffect(() => {
if (!isDropdownButtonOpen) {
resetState();
}
}, [isDropdownButtonOpen, resetState]);
return (
<DropdownButton
dropdownKey={FilterDropdownKey}
dropdownId={FilterDropdownId}
buttonComponents={<MultipleFiltersButton />}
dropdownComponents={<MultipleFiltersDropdownContent context={context} />}
onDropdownToggle={() => {
resetState();
}}
dropdownHotkeyScope={hotkeyScope}
/>
);
}

View File

@ -39,6 +39,8 @@ export function MultipleFiltersDropdownContent({
context,
);
console.log('filterDefinitionUsedInDropdown', filterDefinitionUsedInDropdown);
return (
<StyledDropdownMenu>
<>

View File

@ -6,6 +6,7 @@ import styled from '@emotion/styled';
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
import { IconChevronDown } from '@/ui/icon';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/view-bar/states/filterDefinitionUsedInDropdownScopedState';
import { filterDropdownSearchInputScopedState } from '@/ui/view-bar/states/filterDropdownSearchInputScopedState';
@ -33,7 +34,7 @@ export function SingleEntityFilterDropdownButton({
hotkeyScope,
}: {
context: Context<string | null>;
hotkeyScope: string;
hotkeyScope: HotkeyScope;
}) {
const theme = useTheme();
@ -80,10 +81,10 @@ export function SingleEntityFilterDropdownButton({
function handleIsUnfoldedChange(newIsUnfolded: boolean) {
if (newIsUnfolded) {
setHotkeyScope(hotkeyScope);
setHotkeyScope(hotkeyScope.scope, hotkeyScope.customScopes);
setIsFilterDropdownUnfolded(true);
} else {
setHotkeyScope(hotkeyScope);
setHotkeyScope(hotkeyScope.scope, hotkeyScope.customScopes);
setIsFilterDropdownUnfolded(false);
setFilterDropdownSearchInput('');
}

View File

@ -1,120 +1,136 @@
import { Context, useCallback, useState } from 'react';
import { produce } from 'immer';
import { LightButton } from '@/ui/button/components/LightButton';
import { DropdownButton } from '@/ui/dropdown/components/DropdownButton';
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
import { IconChevronDown } from '@/ui/icon';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { SortDropdownId } from '../constants/SortDropdownId';
import { availableSortsScopedState } from '../states/availableSortsScopedState';
import { sortsScopedState } from '../states/sortsScopedState';
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
import { SelectedSortType, SortType } from '../types/interface';
import { SortDefinition } from '../types/SortDefinition';
import { SORT_DIRECTIONS, SortDirection } from '../types/SortDirection';
import DropdownButton from './DropdownButton';
export type SortDropdownButtonProps<SortField> = {
availableSorts: SortType<SortField>[];
hotkeyScope: FiltersHotkeyScope;
export type SortDropdownButtonProps = {
context: Context<string | null>;
hotkeyScope: HotkeyScope;
isPrimaryButton?: boolean;
};
const options: Array<SelectedSortType<any>['order']> = ['asc', 'desc'];
export function SortDropdownButton<SortField>({
context,
availableSorts,
export function SortDropdownButton({
hotkeyScope,
}: SortDropdownButtonProps<SortField>) {
const [isUnfolded, setIsUnfolded] = useState(false);
const [isOptionUnfolded, setIsOptionUnfolded] = useState(false);
context,
}: SortDropdownButtonProps) {
const [isSortDirectionMenuUnfolded, setIsSortDirectionMenuUnfolded] =
useState(false);
const [selectedSortDirection, setSelectedSortDirection] =
useState<SelectedSortType<SortField>['order']>('asc');
const [sorts, setSorts] = useRecoilScopedState<SelectedSortType<SortField>[]>(
sortsScopedState,
context,
);
const isSortSelected = sorts.length > 0;
const onSortItemSelect = useCallback(
(sort: SortType<SortField>) => {
const newSort = { ...sort, order: selectedSortDirection };
const sortIndex = sorts.findIndex((sort) => sort.key === newSort.key);
const newSorts = [...sorts];
if (sortIndex !== -1) {
newSorts[sortIndex] = newSort;
} else {
newSorts.push(newSort);
}
setSorts(newSorts);
},
[selectedSortDirection, setSorts, sorts],
);
useState<SortDirection>('asc');
const resetState = useCallback(() => {
setIsOptionUnfolded(false);
setIsSortDirectionMenuUnfolded(false);
setSelectedSortDirection('asc');
}, []);
function handleIsUnfoldedChange(newIsUnfolded: boolean) {
setIsUnfolded(newIsUnfolded);
if (!newIsUnfolded) resetState();
const [availableSorts] = useRecoilScopedState(
availableSortsScopedState,
context,
);
const [sorts, setSorts] = useRecoilScopedState(sortsScopedState, context);
const isSortSelected = sorts.length > 0;
const { toggleDropdownButton } = useDropdownButton({
dropdownId: SortDropdownId,
});
function handleButtonClick() {
toggleDropdownButton();
resetState();
}
function handleAddSort(sort: SortType<SortField>) {
setIsUnfolded(false);
onSortItemSelect(sort);
function handleAddSort(selectedSortDefinition: SortDefinition) {
toggleDropdownButton();
setSorts(
produce(sorts, (existingSortsDraft) => {
const foundExistingSortIndex = existingSortsDraft.findIndex(
(existingSort) => existingSort.key === selectedSortDefinition.key,
);
if (foundExistingSortIndex !== -1) {
existingSortsDraft[foundExistingSortIndex].direction =
selectedSortDirection;
} else {
existingSortsDraft.push({
key: selectedSortDefinition.key,
direction: selectedSortDirection,
definition: selectedSortDefinition,
});
}
}),
);
}
return (
<DropdownButton
label="Sort"
isActive={isSortSelected}
isUnfolded={isUnfolded}
onIsUnfoldedChange={handleIsUnfoldedChange}
hotkeyScope={hotkeyScope}
>
{isOptionUnfolded ? (
<StyledDropdownMenuItemsContainer>
{options.map((option, index) => (
<MenuItem
key={index}
onClick={() => {
setSelectedSortDirection(option);
setIsOptionUnfolded(false);
}}
text={option === 'asc' ? 'Ascending' : 'Descending'}
/>
))}
</StyledDropdownMenuItemsContainer>
) : (
<>
<DropdownMenuHeader
EndIcon={IconChevronDown}
onClick={() => setIsOptionUnfolded(true)}
>
{selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'}
</DropdownMenuHeader>
<StyledDropdownMenuSeparator />
<StyledDropdownMenuItemsContainer>
{availableSorts.map((sort, index) => (
<MenuItem
testId={`select-sort-${index}`}
key={index}
onClick={() => handleAddSort(sort)}
LeftIcon={sort.Icon}
text={sort.label}
/>
))}
</StyledDropdownMenuItemsContainer>
</>
)}
</DropdownButton>
dropdownId={SortDropdownId}
dropdownHotkeyScope={hotkeyScope}
buttonComponents={
<LightButton
title="Sort"
active={isSortSelected}
onClick={handleButtonClick}
/>
}
dropdownComponents={
<StyledDropdownMenu>
{isSortDirectionMenuUnfolded ? (
<StyledDropdownMenuItemsContainer>
{SORT_DIRECTIONS.map((sortOrder, index) => (
<MenuItem
key={index}
onClick={() => {
setSelectedSortDirection(sortOrder);
setIsSortDirectionMenuUnfolded(false);
}}
text={sortOrder === 'asc' ? 'Ascending' : 'Descending'}
/>
))}
</StyledDropdownMenuItemsContainer>
) : (
<>
<DropdownMenuHeader
EndIcon={IconChevronDown}
onClick={() => setIsSortDirectionMenuUnfolded(true)}
>
{selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'}
</DropdownMenuHeader>
<StyledDropdownMenuSeparator />
<StyledDropdownMenuItemsContainer>
{availableSorts.map((availableSort, index) => (
<MenuItem
testId={`select-sort-${index}`}
key={index}
onClick={() => handleAddSort(availableSort)}
LeftIcon={availableSort.Icon}
text={availableSort.label}
/>
))}
</StyledDropdownMenuItemsContainer>
</>
)}
</StyledDropdownMenu>
}
></DropdownButton>
);
}

View File

@ -7,10 +7,7 @@ import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
import { ViewsHotkeyScope } from '../types/ViewsHotkeyScope';
import { FilterDropdownButton } from './FilterDropdownButton';
import {
SortDropdownButton,
type SortDropdownButtonProps,
} from './SortDropdownButton';
import { SortDropdownButton } from './SortDropdownButton';
import {
UpdateViewButtonGroup,
type UpdateViewButtonGroupProps,
@ -21,7 +18,7 @@ import {
type ViewsDropdownButtonProps,
} from './ViewsDropdownButton';
export type ViewBarProps<SortField> = ComponentProps<'div'> & {
export type ViewBarProps = ComponentProps<'div'> & {
optionsDropdownButton: ReactNode;
optionsDropdownKey: string;
scopeContext: Context<string | null>;
@ -29,12 +26,10 @@ export type ViewBarProps<SortField> = ComponentProps<'div'> & {
ViewsDropdownButtonProps,
'defaultViewName' | 'onViewsChange' | 'onViewSelect'
> &
Pick<SortDropdownButtonProps<SortField>, 'availableSorts'> &
Pick<ViewBarDetailsProps, 'canPersistViewFields' | 'onReset'> &
Pick<UpdateViewButtonGroupProps, 'onViewSubmit'>;
export const ViewBar = <SortField,>({
availableSorts,
export const ViewBar = ({
canPersistViewFields,
defaultViewName,
onReset,
@ -45,9 +40,9 @@ export const ViewBar = <SortField,>({
optionsDropdownKey,
scopeContext,
...props
}: ViewBarProps<SortField>) => {
}: ViewBarProps) => {
const { openDropdownButton: openOptionsDropdownButton } = useDropdownButton({
key: optionsDropdownKey,
dropdownId: optionsDropdownKey,
});
return (
@ -67,13 +62,12 @@ export const ViewBar = <SortField,>({
rightComponent={
<>
<FilterDropdownButton
hotkeyScope={{ scope: FiltersHotkeyScope.FilterDropdownButton }}
context={scopeContext}
hotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
/>
<SortDropdownButton<SortField>
<SortDropdownButton
context={scopeContext}
availableSorts={availableSorts}
hotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
hotkeyScope={{ scope: FiltersHotkeyScope.FilterDropdownButton }}
isPrimaryButton
/>
{optionsDropdownButton}

View File

@ -15,7 +15,6 @@ import { isViewBarExpandedScopedState } from '../states/isViewBarExpandedScopedS
import { canPersistFiltersScopedFamilySelector } from '../states/selectors/canPersistFiltersScopedFamilySelector';
import { canPersistSortsScopedFamilySelector } from '../states/selectors/canPersistSortsScopedFamilySelector';
import { sortsScopedState } from '../states/sortsScopedState';
import { SelectedSortType } from '../types/interface';
import { getOperandLabelShort } from '../utils/getOperandLabel';
import { AddFilterFromDropdownButton } from './AddFilterFromDetailsButton';
@ -97,7 +96,7 @@ const StyledAddFilterContainer = styled.div`
z-index: 5;
`;
function ViewBarDetails<SortField>({
function ViewBarDetails({
canPersistViewFields,
context,
hasFilterButton = false,
@ -120,10 +119,8 @@ function ViewBarDetails<SortField>({
canPersistFiltersScopedFamilySelector([recoilScopeId, currentViewId]),
);
const [sorts, setSorts] = useRecoilScopedState<SelectedSortType<SortField>[]>(
sortsScopedState,
context,
);
const [sorts, setSorts] = useRecoilScopedState(sortsScopedState, context);
const canPersistSorts = useRecoilValue(
canPersistSortsScopedFamilySelector([recoilScopeId, currentViewId]),
);
@ -177,9 +174,9 @@ function ViewBarDetails<SortField>({
<SortOrFilterChip
key={sort.key}
testId={sort.key}
labelValue={sort.label}
labelValue={sort.definition.label}
Icon={
sort.order === 'desc'
sort.direction === 'desc'
? IconArrowNarrowDown
: IconArrowNarrowUp
}

View File

@ -0,0 +1 @@
export const FilterDropdownId = 'filter';

View File

@ -0,0 +1 @@
export const SortDropdownId = 'sort-dropdown';

View File

@ -1,17 +0,0 @@
import { SortOrder as Order_By } from '~/generated/graphql';
import { SelectedSortType } from './types/interface';
export const reduceSortsToOrderBy = <OrderByTemplate>(
sorts: SelectedSortType<OrderByTemplate>[],
): OrderByTemplate[] =>
sorts
.map((sort) => {
const order = sort.order === 'asc' ? Order_By.Asc : Order_By.Desc;
return (
sort.orderByTemplate?.(order) || [
{ [sort.key]: order } as OrderByTemplate,
]
);
})
.flat();

View File

@ -0,0 +1,8 @@
import { atomFamily } from 'recoil';
import { SortDefinition } from '../types/SortDefinition';
export const availableSortsScopedState = atomFamily<SortDefinition[], string>({
key: 'availableSortsScopedState',
default: [],
});

View File

@ -1,11 +1,8 @@
import { atomFamily } from 'recoil';
import type { SelectedSortType } from '../types/interface';
import { Sort } from '../types/Sort';
export const savedSortsFamilyState = atomFamily<
SelectedSortType<any>[],
string | undefined
>({
export const savedSortsFamilyState = atomFamily<Sort[], string | undefined>({
key: 'savedSortsFamilyState',
default: [],
});

View File

@ -1,6 +1,6 @@
import { selectorFamily } from 'recoil';
import type { SelectedSortType } from '../../types/interface';
import { Sort } from '../../types/Sort';
import { savedSortsFamilyState } from '../savedSortsFamilyState';
export const savedSortsByKeyFamilySelector = selectorFamily({
@ -8,7 +8,8 @@ export const savedSortsByKeyFamilySelector = selectorFamily({
get:
(viewId: string | undefined) =>
({ get }) =>
get(savedSortsFamilyState(viewId)).reduce<
Record<string, SelectedSortType<any>>
>((result, sort) => ({ ...result, [sort.key]: sort }), {}),
get(savedSortsFamilyState(viewId)).reduce<Record<string, Sort>>(
(result, sort) => ({ ...result, [sort.key]: sort }),
{},
),
});

View File

@ -2,7 +2,7 @@ import { selectorFamily } from 'recoil';
import { SortOrder } from '~/generated/graphql';
import { reduceSortsToOrderBy } from '../../helpers';
import { reduceSortsToOrderBy } from '../../utils/helpers';
import { sortsScopedState } from '../sortsScopedState';
export const sortsOrderByScopedSelector = selectorFamily({

View File

@ -1,8 +1,8 @@
import { atomFamily } from 'recoil';
import type { SelectedSortType } from '../types/interface';
import { Sort } from '../types/Sort';
export const sortsScopedState = atomFamily<SelectedSortType<any>[], string>({
export const sortsScopedState = atomFamily<Sort[], string>({
key: 'sortsScopedState',
default: [],
});

View File

@ -1 +0,0 @@
export const FilterDropdownKey = 'filter';

View File

@ -0,0 +1,8 @@
import { SortDefinition } from './SortDefinition';
import { SortDirection } from './SortDirection';
export type Sort = {
key: string;
direction: SortDirection;
definition: SortDefinition;
};

View File

@ -0,0 +1,10 @@
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { SortDirection } from './SortDirection';
export type SortDefinition = {
key: string;
label: string;
Icon?: IconComponent;
getOrderByTemplate?: (direction: SortDirection) => any[];
};

View File

@ -0,0 +1,3 @@
export const SORT_DIRECTIONS = ['asc', 'desc'] as const;
export type SortDirection = (typeof SORT_DIRECTIONS)[number];

View File

@ -0,0 +1,16 @@
import { SortOrder as Order_By } from '~/generated/graphql';
import { Sort } from '../types/Sort';
export const reduceSortsToOrderBy = (sorts: Sort[]): any[] =>
sorts
.map((sort) => {
const direction = sort.direction === 'asc' ? Order_By.Asc : Order_By.Desc;
if (sort.definition.getOrderByTemplate) {
return sort.definition.getOrderByTemplate(direction);
} else {
return [{ [sort.definition.key]: direction }];
}
})
.flat();