feat: persist table columns on change (#1697)

* feat: persist table columns on change

Closes #1580

* fix: fix drag-and-select on Table Options dropdown toggle
This commit is contained in:
Thaïs
2023-09-21 22:15:57 +02:00
committed by GitHub
parent 189bf4a627
commit ab0cdbf960
15 changed files with 149 additions and 158 deletions

View File

@ -10,7 +10,7 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis
import { useDropdownButton } from '../hooks/useDropdownButton';
import { useInternalHotkeyScopeManagement } from '../hooks/useInternalHotkeyScopeManagement';
import { DropdownCloseEffect } from './DropdownCloseEffect';
import { DropdownToggleEffect } from './DropdownToggleEffect';
type OwnProps = {
buttonComponents?: JSX.Element | JSX.Element[];
@ -24,6 +24,7 @@ type OwnProps = {
dropdownPlacement?: Placement;
onClickOutside?: () => void;
onClose?: () => void;
onOpen?: () => void;
};
export const DropdownButton = ({
@ -35,6 +36,7 @@ export const DropdownButton = ({
dropdownPlacement = 'bottom-end',
onClickOutside,
onClose,
onOpen,
}: OwnProps) => {
const containerRef = useRef<HTMLDivElement>(null);
@ -93,9 +95,10 @@ export const DropdownButton = ({
{dropdownComponents}
</div>
)}
<DropdownCloseEffect
<DropdownToggleEffect
dropdownId={dropdownId}
onDropdownClose={() => onClose?.()}
onDropdownClose={onClose}
onDropdownOpen={onOpen}
/>
</div>
);

View File

@ -2,20 +2,24 @@ import { useEffect } from 'react';
import { useDropdownButton } from '../hooks/useDropdownButton';
export const DropdownCloseEffect = ({
export const DropdownToggleEffect = ({
dropdownId,
onDropdownClose,
onDropdownOpen,
}: {
dropdownId: string;
onDropdownClose: () => void;
onDropdownClose?: () => void;
onDropdownOpen?: () => void;
}) => {
const { isDropdownButtonOpen } = useDropdownButton({ dropdownId });
useEffect(() => {
if (!isDropdownButtonOpen) {
onDropdownClose();
if (isDropdownButtonOpen) {
onDropdownOpen?.();
} else {
onDropdownClose?.();
}
}, [isDropdownButtonOpen, onDropdownClose]);
}, [isDropdownButtonOpen, onDropdownClose, onDropdownOpen]);
return null;
};

View File

@ -1,6 +1,6 @@
import { useRef } from 'react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { useRecoilValue } from 'recoil';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
@ -90,9 +90,7 @@ type OwnProps = {
export const EntityTable = ({ updateEntityMutation }: OwnProps) => {
const tableBodyRef = useRef<HTMLDivElement>(null);
const [isDraggingAndSelecting, setIsDraggingAndSelecting] = useRecoilState(
isDraggingAndSelectingState,
);
const isDraggingAndSelecting = useRecoilValue(isDraggingAndSelectingState);
const setRowSelectedState = useSetRowSelectedState();
const resetTableRowSelection = useResetTableRowSelection();
@ -105,7 +103,6 @@ export const EntityTable = ({ updateEntityMutation }: OwnProps) => {
refs: [tableBodyRef],
callback: () => {
leaveTableFocus();
setIsDraggingAndSelecting(true);
},
});

View File

@ -7,9 +7,9 @@ import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-co
import { IconPlus } from '@/ui/icon';
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { useTableColumns } from '../hooks/useTableColumns';
import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext';
import { resizeFieldOffsetState } from '../states/resizeFieldOffsetState';
import { hiddenTableColumnsScopedSelector } from '../states/selectors/hiddenTableColumnsScopedSelector';
@ -76,7 +76,7 @@ export const EntityTableHeader = () => {
const [resizeFieldOffset, setResizeFieldOffset] = useRecoilState(
resizeFieldOffsetState,
);
const [tableColumns, setTableColumns] = useRecoilScopedState(
const tableColumns = useRecoilScopedValue(
tableColumnsScopedState,
TableRecoilScopeContext,
);
@ -99,6 +99,8 @@ export const EntityTableHeader = () => {
const [resizedFieldKey, setResizedFieldKey] = useState<string | null>(null);
const [isColumnMenuOpen, setIsColumnMenuOpen] = useState(false);
const { handleColumnsChange } = useTableColumns();
const handleResizeHandlerStart = useCallback((positionX: number) => {
setInitialPointerPositionX(positionX);
}, []);
@ -113,7 +115,7 @@ export const EntityTableHeader = () => {
const handleResizeHandlerEnd = useRecoilCallback(
({ snapshot, set }) =>
() => {
async () => {
if (!resizedFieldKey) return;
const nextWidth = Math.round(
@ -131,14 +133,14 @@ export const EntityTableHeader = () => {
: column,
);
setTableColumns(nextColumns);
await handleColumnsChange(nextColumns);
}
set(resizeFieldOffsetState, 0);
setInitialPointerPositionX(null);
setResizedFieldKey(null);
},
[resizedFieldKey, tableColumnsByKey, tableColumns, setTableColumns],
[resizedFieldKey, tableColumnsByKey, tableColumns, handleColumnsChange],
);
useTrackPointer({

View File

@ -0,0 +1,11 @@
import { createContext } from 'react';
import type { ViewFieldMetadata } from '@/ui/editable-field/types/ViewField';
import type { ColumnDefinition } from '../types/ColumnDefinition';
export const TableContext = createContext<{
onColumnsChange?: (
columns: ColumnDefinition<ViewFieldMetadata>[],
) => void | Promise<void>;
}>({});

View File

@ -1,15 +1,28 @@
import { useCallback } from 'react';
import { useCallback, useContext } from 'react';
import { useSetRecoilState } from 'recoil';
import type { ViewFieldMetadata } from '@/ui/editable-field/types/ViewField';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState';
import { TableContext } from '../contexts/TableContext';
import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext';
import { savedTableColumnsFamilyState } from '../states/savedTableColumnsFamilyState';
import { tableColumnsByKeyScopedSelector } from '../states/selectors/tableColumnsByKeyScopedSelector';
import { tableColumnsScopedState } from '../states/tableColumnsScopedState';
import type { ColumnDefinition } from '../types/ColumnDefinition';
export const useTableColumns = () => {
const { onColumnsChange } = useContext(TableContext);
const currentViewId = useRecoilScopedValue(
currentViewIdScopedState,
TableRecoilScopeContext,
);
const setSavedTableColumns = useSetRecoilState(
savedTableColumnsFamilyState(currentViewId),
);
const [tableColumns, setTableColumns] = useRecoilScopedState(
tableColumnsScopedState,
TableRecoilScopeContext,
@ -19,21 +32,30 @@ export const useTableColumns = () => {
TableRecoilScopeContext,
);
const handleColumnReorder = useCallback(
(columns: ColumnDefinition<ViewFieldMetadata>[]) => {
const updatedColumnOrder = columns
.map((column, index) => {
return { ...column, index };
})
.sort((columnA, columnB) => columnA.index - columnB.index);
const handleColumnsChange = useCallback(
async (columns: ColumnDefinition<ViewFieldMetadata>[]) => {
await onColumnsChange?.(columns);
setTableColumns(updatedColumnOrder);
setSavedTableColumns(columns);
setTableColumns(columns);
},
[setTableColumns],
[onColumnsChange, setSavedTableColumns, setTableColumns],
);
const handleColumnReorder = useCallback(
async (columns: ColumnDefinition<ViewFieldMetadata>[]) => {
const updatedColumns = columns.map((column, index) => ({
...column,
index,
}));
await handleColumnsChange(updatedColumns);
},
[handleColumnsChange],
);
const handleColumnVisibilityChange = useCallback(
(column: ColumnDefinition<ViewFieldMetadata>) => {
async (column: ColumnDefinition<ViewFieldMetadata>) => {
const nextColumns = tableColumnsByKey[column.key]
? tableColumns.map((previousColumn) =>
previousColumn.key === column.key
@ -44,13 +66,13 @@ export const useTableColumns = () => {
(columnA, columnB) => columnA.index - columnB.index,
);
setTableColumns(nextColumns);
await handleColumnsChange(nextColumns);
},
[tableColumnsByKey, tableColumns, setTableColumns],
[tableColumnsByKey, tableColumns, handleColumnsChange],
);
const handleColumnMove = useCallback(
(direction: string, column: ColumnDefinition<ViewFieldMetadata>) => {
async (direction: string, column: ColumnDefinition<ViewFieldMetadata>) => {
const currentColumnArrayIndex = tableColumns.findIndex(
(tableColumn) => tableColumn.key === column.key,
);
@ -73,10 +95,10 @@ export const useTableColumns = () => {
index: targetColumn.index,
};
setTableColumns(newTableColumns);
await handleColumnsChange(newTableColumns);
}
},
[tableColumns, setTableColumns],
[tableColumns, handleColumnsChange],
);
const handleColumnLeftMove = useCallback(
@ -98,5 +120,6 @@ export const useTableColumns = () => {
handleColumnLeftMove,
handleColumnRightMove,
handleColumnReorder,
handleColumnsChange,
};
};

View File

@ -1,10 +1,11 @@
import { useResetRecoilState } from 'recoil';
import { useRecoilState, useResetRecoilState } from 'recoil';
import { DropdownButton } from '@/ui/dropdown/components/DropdownButton';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { viewEditModeState } from '@/ui/view-bar/states/viewEditModeState';
import { TableOptionsDropdownId } from '../../constants/TableOptionsDropdownId';
import { isDraggingAndSelectingState } from '../../states/isDraggingAndSelectingState';
import { TableOptionsDropdownButton } from './TableOptionsDropdownButton';
import { TableOptionsDropdownContent } from './TableOptionsDropdownContent';
@ -18,6 +19,14 @@ export const TableOptionsDropdown = ({
}: TableOptionsDropdownProps) => {
const resetViewEditMode = useResetRecoilState(viewEditModeState);
const [, setIsDraggingAndSelecting] = useRecoilState(
isDraggingAndSelectingState,
);
const handleClose = () => setIsDraggingAndSelecting(true);
const handleOpen = () => setIsDraggingAndSelecting(false);
return (
<DropdownButton
buttonComponents={<TableOptionsDropdownButton />}
@ -25,6 +34,8 @@ export const TableOptionsDropdown = ({
dropdownId={TableOptionsDropdownId}
dropdownComponents={<TableOptionsDropdownContent />}
onClickOutside={resetViewEditMode}
onClose={handleClose}
onOpen={handleOpen}
/>
);
};

View File

@ -1,27 +1,16 @@
import { useRecoilState } from 'recoil';
import { StyledHeaderDropdownButton } from '@/ui/dropdown/components/StyledHeaderDropdownButton';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
import { TableOptionsDropdownId } from '@/ui/table/constants/TableOptionsDropdownId';
import { isDraggingAndSelectingState } from '@/ui/table/states/isDraggingAndSelectingState';
export const TableOptionsDropdownButton = () => {
const [, setIsDraggingAndSelecting] = useRecoilState(
isDraggingAndSelectingState,
);
const { isDropdownButtonOpen, toggleDropdownButton } = useDropdownButton({
dropdownId: TableOptionsDropdownId,
});
const toggleDropdown = () => {
setIsDraggingAndSelecting(false);
toggleDropdownButton();
};
return (
<StyledHeaderDropdownButton
isUnfolded={isDropdownButtonOpen}
onClick={toggleDropdown}
onClick={toggleDropdownButton}
>
Options
</StyledHeaderDropdownButton>

View File

@ -1,23 +0,0 @@
import { selectorFamily } from 'recoil';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { savedTableColumnsFamilyState } from '../savedTableColumnsFamilyState';
import { tableColumnsScopedState } from '../tableColumnsScopedState';
export const canPersistTableColumnsScopedFamilySelector = selectorFamily({
key: 'canPersistTableColumnsScopedFamilySelector',
get:
({
recoilScopeId,
viewId,
}: {
recoilScopeId: string;
viewId: string | undefined;
}) =>
({ get }) =>
!isDeeplyEqual(
get(savedTableColumnsFamilyState(viewId)),
get(tableColumnsScopedState(recoilScopeId)),
),
});

View File

@ -1,21 +1,16 @@
import { useContext } from 'react';
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import { useRecoilCallback } from 'recoil';
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId';
import { ViewBar } from '@/ui/view-bar/components/ViewBar';
import { ViewBarContext } from '@/ui/view-bar/contexts/ViewBarContext';
import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState';
import { TableOptionsDropdownId } from '../../constants/TableOptionsDropdownId';
import { TableOptionsDropdown } from '../../options/components/TableOptionsDropdown';
import { isDraggingAndSelectingState } from '../../states/isDraggingAndSelectingState';
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
import { savedTableColumnsFamilyState } from '../../states/savedTableColumnsFamilyState';
import { canPersistTableColumnsScopedFamilySelector } from '../../states/selectors/canPersistTableColumnsScopedFamilySelector';
import { tableColumnsScopedState } from '../../states/tableColumnsScopedState';
import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope';
@ -24,29 +19,6 @@ export const TableHeader = () => {
useContext(ViewBarContext);
const tableRecoilScopeId = useRecoilScopeId(TableRecoilScopeContext);
const currentViewId = useRecoilScopedValue(
currentViewIdScopedState,
TableRecoilScopeContext,
);
const canPersistTableColumns = useRecoilValue(
canPersistTableColumnsScopedFamilySelector({
recoilScopeId: tableRecoilScopeId,
viewId: currentViewId,
}),
);
const [tableColumns, setTableColumns] = useRecoilScopedState(
tableColumnsScopedState,
TableRecoilScopeContext,
);
const [savedTableColumns, setSavedTableColumns] = useRecoilState(
savedTableColumnsFamilyState(currentViewId),
);
const [, setIsDraggingAndSelecting] = useRecoilState(
isDraggingAndSelectingState,
);
const handleViewBarReset = () => setTableColumns(savedTableColumns);
const handleViewSelect = useRecoilCallback(
({ set, snapshot }) =>
async (viewId: string) => {
@ -58,23 +30,12 @@ export const TableHeader = () => {
[tableRecoilScopeId],
);
const handleCurrentViewSubmit = async () => {
if (canPersistTableColumns) {
setSavedTableColumns(tableColumns);
setIsDraggingAndSelecting(true);
}
await onCurrentViewSubmit?.();
};
return (
<RecoilScope CustomRecoilScopeContext={DropdownRecoilScopeContext}>
<ViewBarContext.Provider
value={{
...viewBarContextProps,
canPersistViewFields: canPersistTableColumns,
onCurrentViewSubmit: handleCurrentViewSubmit,
onViewBarReset: handleViewBarReset,
onCurrentViewSubmit,
onViewSelect: handleViewSelect,
}}
>

View File

@ -1,9 +1,8 @@
import { type ReactNode, useContext } from 'react';
import styled from '@emotion/styled';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useRecoilValue } from 'recoil';
import { IconArrowDown, IconArrowUp } from '@/ui/icon/index';
import { isDraggingAndSelectingState } from '@/ui/table/states/isDraggingAndSelectingState';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId';
@ -116,10 +115,6 @@ export const ViewBarDetails = ({
ViewBarRecoilScopeContext,
);
const [, setIsDraggingAndSelecting] = useRecoilState(
isDraggingAndSelectingState,
);
const savedFilters = useRecoilValue(
savedFiltersFamilySelector(currentViewId),
);
@ -172,7 +167,6 @@ export const ViewBarDetails = ({
const handleCancelClick = () => {
onViewBarReset?.();
setIsDraggingAndSelecting(true);
setFilters(savedFilters);
setSorts(savedSorts);
};
@ -239,7 +233,7 @@ export const ViewBarDetails = ({
data-testid="cancel-button"
onClick={handleCancelClick}
>
Cancel
Reset
</StyledCancelButton>
)}
{rightComponent}