feat: persist view filters and sorts on Update View button click (#1290)

* feat: add viewFilters table

Closes #1121

* feat: add Update View button + Create View dropdown

Closes #1124, #1289

* feat: add View Filter resolvers

* feat: persist view filters and sorts on Update View button click

Closes #1123

* refactor: code review

- Rename recoil selectors
- Rename filters `field` property to `key`
This commit is contained in:
Thaïs
2023-08-23 18:20:43 +02:00
committed by GitHub
parent 76246ec880
commit 74ab0142c7
54 changed files with 1331 additions and 277 deletions

View File

@ -5,7 +5,7 @@ import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
import { SortType } from '@/ui/filter-n-sort/types/interface';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
@ -97,8 +97,8 @@ type OwnProps<SortField> = {
viewIcon?: React.ReactNode;
availableSorts?: Array<SortType<SortField>>;
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onViewsChange?: (views: TableView[]) => void;
onViewSubmit?: () => void;
onImport?: () => void;
updateEntityMutation: any;
};
@ -107,8 +107,8 @@ export function EntityTable<SortField>({
viewName,
availableSorts,
onColumnsChange,
onSortsUpdate,
onViewsChange,
onViewSubmit,
onImport,
updateEntityMutation,
}: OwnProps<SortField>) {
@ -136,8 +136,8 @@ export function EntityTable<SortField>({
viewName={viewName}
availableSorts={availableSorts}
onColumnsChange={onColumnsChange}
onSortsUpdate={onSortsUpdate}
onViewsChange={onViewsChange}
onViewSubmit={onViewSubmit}
onImport={onImport}
/>
<StyledTableWrapper>

View File

@ -0,0 +1,123 @@
import { useCallback, useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { Button, ButtonSize } from '@/ui/button/components/Button';
import { ButtonGroup } from '@/ui/button/components/ButtonGroup';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuContainer } from '@/ui/filter-n-sort/components/DropdownMenuContainer';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { savedFiltersScopedState } from '@/ui/filter-n-sort/states/savedFiltersScopedState';
import { savedSortsScopedState } from '@/ui/filter-n-sort/states/savedSortsScopedState';
import { sortsScopedState } from '@/ui/filter-n-sort/states/sortsScopedState';
import { IconChevronDown, IconPlus } from '@/ui/icon';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
import {
currentTableViewIdState,
tableViewEditModeState,
} from '../../states/tableViewsState';
const StyledContainer = styled.div`
display: inline-flex;
margin-right: ${({ theme }) => theme.spacing(2)};
position: relative;
`;
const StyledDropdownMenuContainer = styled(DropdownMenuContainer)`
z-index: 1;
`;
type TableUpdateViewButtonGroupProps = {
onViewSubmit?: () => void;
HotkeyScope: string;
};
export const TableUpdateViewButtonGroup = ({
onViewSubmit,
HotkeyScope,
}: TableUpdateViewButtonGroupProps) => {
const theme = useTheme();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const tableScopeId = useContextScopeId(TableRecoilScopeContext);
const currentViewId = useRecoilScopedValue(
currentTableViewIdState,
TableRecoilScopeContext,
);
const setViewEditMode = useSetRecoilState(tableViewEditModeState);
const handleArrowDownButtonClick = useCallback(() => {
setIsDropdownOpen((previousIsOpen) => !previousIsOpen);
}, []);
const handleCreateViewButtonClick = useCallback(() => {
setViewEditMode({ mode: 'create', viewId: undefined });
setIsDropdownOpen(false);
}, [setViewEditMode]);
const handleDropdownClose = useCallback(() => {
setIsDropdownOpen(false);
}, []);
const handleViewSubmit = useRecoilCallback(
({ set, snapshot }) =>
async () => {
await Promise.resolve(onViewSubmit?.());
const selectedFilters = await snapshot.getPromise(
filtersScopedState(tableScopeId),
);
set(savedFiltersScopedState(currentViewId), selectedFilters);
const selectedSorts = await snapshot.getPromise(
sortsScopedState(tableScopeId),
);
set(savedSortsScopedState(currentViewId), selectedSorts);
},
[currentViewId, onViewSubmit],
);
useScopedHotkeys(
[Key.Enter, Key.Escape],
handleDropdownClose,
HotkeyScope,
[],
);
return (
<StyledContainer>
<ButtonGroup size={ButtonSize.Small}>
<Button
title="Update view"
disabled={!currentViewId}
onClick={handleViewSubmit}
/>
<Button
size={ButtonSize.Small}
icon={<IconChevronDown />}
onClick={handleArrowDownButtonClick}
/>
</ButtonGroup>
{isDropdownOpen && (
<StyledDropdownMenuContainer onClose={handleDropdownClose}>
<DropdownMenuItemsContainer>
<DropdownMenuItem onClick={handleCreateViewButtonClick}>
<IconPlus size={theme.icon.size.md} />
Create view
</DropdownMenuItem>
</DropdownMenuItemsContainer>
</StyledDropdownMenuContainer>
)}
</StyledContainer>
);
};

View File

@ -1,13 +1,17 @@
import { type MouseEvent, useCallback, useEffect, useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useSetRecoilState } from 'recoil';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { IconButton } from '@/ui/button/components/IconButton';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
import DropdownButton from '@/ui/filter-n-sort/components/DropdownButton';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { savedFiltersScopedState } from '@/ui/filter-n-sort/states/savedFiltersScopedState';
import { savedSortsScopedState } from '@/ui/filter-n-sort/states/savedSortsScopedState';
import { sortsScopedState } from '@/ui/filter-n-sort/states/sortsScopedState';
import {
IconChevronDown,
IconList,
@ -23,6 +27,7 @@ import {
tableViewsState,
} from '@/ui/table/states/tableViewsState';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
@ -59,6 +64,8 @@ export const TableViewsDropdownButton = ({
const theme = useTheme();
const [isUnfolded, setIsUnfolded] = useState(false);
const tableScopeId = useContextScopeId(TableRecoilScopeContext);
const currentView = useRecoilScopedValue(
currentTableViewState,
TableRecoilScopeContext,
@ -78,11 +85,21 @@ export const TableViewsDropdownButton = ({
setHotkeyScopeAndMemorizePreviousScope,
} = usePreviousHotkeyScope();
const handleViewSelect = useCallback(
(viewId?: string) => {
setCurrentViewId(viewId);
setIsUnfolded(false);
},
const handleViewSelect = useRecoilCallback(
({ set, snapshot }) =>
async (viewId?: string) => {
const savedFilters = await snapshot.getPromise(
savedFiltersScopedState(viewId),
);
const savedSorts = await snapshot.getPromise(
savedSortsScopedState(viewId),
);
set(filtersScopedState(tableScopeId), savedFilters);
set(sortsScopedState(tableScopeId), savedSorts);
setCurrentViewId(viewId);
setIsUnfolded(false);
},
[setCurrentViewId],
);

View File

@ -7,13 +7,14 @@ import type {
import { FilterDropdownButton } from '@/ui/filter-n-sort/components/FilterDropdownButton';
import SortAndFilterBar from '@/ui/filter-n-sort/components/SortAndFilterBar';
import { SortDropdownButton } from '@/ui/filter-n-sort/components/SortDropdownButton';
import { sortScopedState } from '@/ui/filter-n-sort/states/sortScopedState';
import { sortsScopedState } from '@/ui/filter-n-sort/states/sortsScopedState';
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
import { TableOptionsDropdownButton } from '@/ui/table/options/components/TableOptionsDropdownButton';
import { TopBar } from '@/ui/top-bar/TopBar';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { TableUpdateViewButtonGroup } from '../../options/components/TableUpdateViewButtonGroup';
import { TableViewsDropdownButton } from '../../options/components/TableViewsDropdownButton';
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
import type { TableView } from '../../states/tableViewsState';
@ -24,8 +25,8 @@ type OwnProps<SortField> = {
viewName: string;
availableSorts?: Array<SortType<SortField>>;
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onViewsChange?: (views: TableView[]) => void;
onViewSubmit?: () => void;
onImport?: () => void;
};
@ -33,30 +34,29 @@ export function TableHeader<SortField>({
viewName,
availableSorts,
onColumnsChange,
onSortsUpdate,
onViewsChange,
onViewSubmit,
onImport,
}: OwnProps<SortField>) {
const [sorts, setSorts] = useRecoilScopedState<SelectedSortType<SortField>[]>(
sortScopedState,
sortsScopedState,
TableRecoilScopeContext,
);
const handleSortsUpdate = onSortsUpdate ?? setSorts;
const sortSelect = useCallback(
(newSort: SelectedSortType<SortField>) => {
const newSorts = updateSortOrFilterByKey(sorts, newSort);
handleSortsUpdate(newSorts);
setSorts(newSorts);
},
[handleSortsUpdate, sorts],
[setSorts, sorts],
);
const sortUnselect = useCallback(
(sortKey: string) => {
const newSorts = sorts.filter((sort) => sort.key !== sortKey);
handleSortsUpdate(newSorts);
setSorts(newSorts);
},
[handleSortsUpdate, sorts],
[setSorts, sorts],
);
return (
@ -65,7 +65,7 @@ export function TableHeader<SortField>({
<TableViewsDropdownButton
defaultViewName={viewName}
onViewsChange={onViewsChange}
HotkeyScope={TableViewsHotkeyScope.Dropdown}
HotkeyScope={TableViewsHotkeyScope.ListDropdown}
/>
}
displayBottomBorder={false}
@ -97,10 +97,14 @@ export function TableHeader<SortField>({
context={TableRecoilScopeContext}
sorts={sorts}
onRemoveSort={sortUnselect}
onCancelClick={() => {
handleSortsUpdate([]);
}}
onCancelClick={() => setSorts([])}
hasFilterButton
rightComponent={
<TableUpdateViewButtonGroup
onViewSubmit={onViewSubmit}
HotkeyScope={TableViewsHotkeyScope.CreateDropdown}
/>
}
/>
}
/>

View File

@ -1,3 +1,4 @@
export enum TableViewsHotkeyScope {
Dropdown = 'table-views-dropdown',
ListDropdown = 'table-views-list-dropdown',
CreateDropdown = 'table-views-create-dropdown',
}