Add tab hotkey on table page (#457)
* wip * wip * - Added scopes on useHotkeys - Use new EditableCellV2 - Implemented Recoil Scoped State with specific context - Implemented soft focus position - Factorized open/close editable cell - Removed editable relation old components - Broke down entity table into multiple components - Added Recoil Scope by CellContext - Added Recoil Scope by RowContext * First working version * Use a new EditableCellSoftFocusMode * Fixed initialize soft focus * Fixed enter mode * Added TODO * Fix * Fixes * Fix tests * Fix lint * Fixes --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -6,7 +6,7 @@ import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import {
|
||||
FilterConfigType,
|
||||
@ -16,12 +16,13 @@ import {
|
||||
SelectedSortType,
|
||||
SortType,
|
||||
} from '@/filters-and-sorts/interfaces/sorts/interface';
|
||||
import { contextMenuPositionState } from '@/ui/tables/states/contextMenuPositionState';
|
||||
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
||||
import { RowContext } from '@/ui/tables/states/RowContext';
|
||||
|
||||
import { useResetTableRowSelection } from '../../tables/hooks/useResetTableRowSelection';
|
||||
import { currentRowSelectionState } from '../../tables/states/rowSelectionState';
|
||||
|
||||
import { TableHeader } from './table-header/TableHeader';
|
||||
import { EntityTableRow } from './EntityTableRow';
|
||||
|
||||
type OwnProps<TData extends { id: string }, SortField> = {
|
||||
data: Array<TData>;
|
||||
@ -100,11 +101,6 @@ const StyledTableScrollableContainer = styled.div`
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const StyledRow = styled.tr<{ selected: boolean }>`
|
||||
background: ${(props) =>
|
||||
props.selected ? props.theme.background.secondary : 'none'};
|
||||
`;
|
||||
|
||||
export function EntityTable<TData extends { id: string }, SortField>({
|
||||
data,
|
||||
columns,
|
||||
@ -118,13 +114,6 @@ export function EntityTable<TData extends { id: string }, SortField>({
|
||||
const [currentRowSelection, setCurrentRowSelection] = useRecoilState(
|
||||
currentRowSelectionState,
|
||||
);
|
||||
const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
|
||||
|
||||
const resetTableRowSelection = useResetTableRowSelection();
|
||||
|
||||
React.useEffect(() => {
|
||||
resetTableRowSelection();
|
||||
}, [resetTableRowSelection]);
|
||||
|
||||
const table = useReactTable<TData>({
|
||||
data,
|
||||
@ -138,16 +127,6 @@ export function EntityTable<TData extends { id: string }, SortField>({
|
||||
getRowId: (row) => row.id,
|
||||
});
|
||||
|
||||
function handleContextMenu(event: React.MouseEvent, id: string) {
|
||||
event.preventDefault();
|
||||
setCurrentRowSelection((prev) => ({ ...prev, [id]: true }));
|
||||
|
||||
setContextMenuPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledTableWithHeader>
|
||||
<TableHeader
|
||||
@ -186,33 +165,9 @@ export function EntityTable<TData extends { id: string }, SortField>({
|
||||
</thead>
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map((row, index) => (
|
||||
<StyledRow
|
||||
key={row.id}
|
||||
data-testid={`row-id-${row.index}`}
|
||||
selected={!!currentRowSelection[row.id]}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
<td
|
||||
key={cell.id + row.original.id}
|
||||
onContextMenu={(event) =>
|
||||
handleContextMenu(event, row.original.id)
|
||||
}
|
||||
style={{
|
||||
width: cell.column.getSize(),
|
||||
minWidth: cell.column.getSize(),
|
||||
maxWidth: cell.column.getSize(),
|
||||
}}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
<td></td>
|
||||
</StyledRow>
|
||||
<RecoilScope SpecificContext={RowContext} key={row.id}>
|
||||
<EntityTableRow row={row} index={index} />
|
||||
</RecoilScope>
|
||||
))}
|
||||
</tbody>
|
||||
</StyledTable>
|
||||
|
||||
56
front/src/modules/ui/components/table/EntityTableCell.tsx
Normal file
56
front/src/modules/ui/components/table/EntityTableCell.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { useEffect } from 'react';
|
||||
import { flexRender } from '@tanstack/react-table';
|
||||
import { Cell, Row } from '@tanstack/table-core';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { CellContext } from '@/ui/tables/states/CellContext';
|
||||
import { contextMenuPositionState } from '@/ui/tables/states/contextMenuPositionState';
|
||||
import { currentColumnNumberScopedState } from '@/ui/tables/states/currentColumnNumberScopedState';
|
||||
import { currentRowSelectionState } from '@/ui/tables/states/rowSelectionState';
|
||||
|
||||
export function EntityTableCell<TData extends { id: string }>({
|
||||
row,
|
||||
cell,
|
||||
cellIndex,
|
||||
}: {
|
||||
row: Row<TData>;
|
||||
cell: Cell<TData, unknown>;
|
||||
cellIndex: number;
|
||||
}) {
|
||||
const [, setCurrentRowSelection] = useRecoilState(currentRowSelectionState);
|
||||
|
||||
const [, setCurrentColumnNumber] = useRecoilScopedState(
|
||||
currentColumnNumberScopedState,
|
||||
CellContext,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentColumnNumber(cellIndex);
|
||||
}, [cellIndex, setCurrentColumnNumber]);
|
||||
|
||||
const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
|
||||
|
||||
function handleContextMenu(event: React.MouseEvent, id: string) {
|
||||
event.preventDefault();
|
||||
setCurrentRowSelection((prev) => ({ ...prev, [id]: true }));
|
||||
|
||||
setContextMenuPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<td
|
||||
onContextMenu={(event) => handleContextMenu(event, row.original.id)}
|
||||
style={{
|
||||
width: cell.column.getSize(),
|
||||
minWidth: cell.column.getSize(),
|
||||
maxWidth: cell.column.getSize(),
|
||||
}}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
63
front/src/modules/ui/components/table/EntityTableRow.tsx
Normal file
63
front/src/modules/ui/components/table/EntityTableRow.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { useEffect } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Row } from '@tanstack/table-core';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
||||
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { CellContext } from '@/ui/tables/states/CellContext';
|
||||
import { currentRowNumberScopedState } from '@/ui/tables/states/currentRowNumberScopedState';
|
||||
import { RowContext } from '@/ui/tables/states/RowContext';
|
||||
import { currentRowSelectionState } from '@/ui/tables/states/rowSelectionState';
|
||||
|
||||
import { EntityTableCell } from './EntityTableCell';
|
||||
|
||||
const StyledRow = styled.tr<{ selected: boolean }>`
|
||||
background: ${(props) =>
|
||||
props.selected ? props.theme.background.secondary : 'none'};
|
||||
`;
|
||||
|
||||
export function EntityTableRow<TData extends { id: string }>({
|
||||
row,
|
||||
index,
|
||||
}: {
|
||||
row: Row<TData>;
|
||||
index: number;
|
||||
}) {
|
||||
const [currentRowSelection] = useRecoilState(currentRowSelectionState);
|
||||
|
||||
const [, setCurrentRowNumber] = useRecoilScopedState(
|
||||
currentRowNumberScopedState,
|
||||
RowContext,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentRowNumber(index);
|
||||
}, [index, setCurrentRowNumber]);
|
||||
|
||||
return (
|
||||
<StyledRow
|
||||
key={row.id}
|
||||
data-testid={`row-id-${row.index}`}
|
||||
selected={!!currentRowSelection[row.id]}
|
||||
>
|
||||
{row.getVisibleCells().map((cell, cellIndex) => {
|
||||
return (
|
||||
<RecoilScope
|
||||
SpecificContext={CellContext}
|
||||
key={cell.id + row.original.id}
|
||||
>
|
||||
<RecoilScope>
|
||||
<EntityTableCell<TData>
|
||||
row={row}
|
||||
cell={cell}
|
||||
cellIndex={cellIndex}
|
||||
/>
|
||||
</RecoilScope>
|
||||
</RecoilScope>
|
||||
);
|
||||
})}
|
||||
<td></td>
|
||||
</StyledRow>
|
||||
);
|
||||
}
|
||||
19
front/src/modules/ui/components/table/HooksEntityTable.tsx
Normal file
19
front/src/modules/ui/components/table/HooksEntityTable.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { useInitializeEntityTable } from '@/ui/tables/hooks/useInitializeEntityTable';
|
||||
import { useMapKeyboardToSoftFocus } from '@/ui/tables/hooks/useMapKeyboardToSoftFocus';
|
||||
|
||||
export function HooksEntityTable({
|
||||
numberOfColumns,
|
||||
numberOfRows,
|
||||
}: {
|
||||
numberOfColumns: number;
|
||||
numberOfRows: number;
|
||||
}) {
|
||||
useMapKeyboardToSoftFocus();
|
||||
|
||||
useInitializeEntityTable({
|
||||
numberOfColumns,
|
||||
numberOfRows,
|
||||
});
|
||||
|
||||
return <></>;
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
import { ReactNode, useRef } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
|
||||
import { IconChevronDown } from '@/ui/icons/index';
|
||||
import { overlayBackground, textInputStyle } from '@/ui/themes/effects';
|
||||
|
||||
@ -159,11 +161,17 @@ function DropdownButton({
|
||||
setIsUnfolded,
|
||||
resetState,
|
||||
}: OwnProps) {
|
||||
const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
|
||||
captureHotkeyTypeInFocusState,
|
||||
);
|
||||
|
||||
const onButtonClick = () => {
|
||||
setIsUnfolded && setIsUnfolded(!isUnfolded);
|
||||
setCaptureHotkeyTypeInFocus(!isUnfolded);
|
||||
};
|
||||
|
||||
const onOutsideClick = () => {
|
||||
setCaptureHotkeyTypeInFocus(false);
|
||||
setIsUnfolded && setIsUnfolded(false);
|
||||
resetState && resetState();
|
||||
};
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import React from 'react';
|
||||
import { ApolloProvider } from '@apollo/client';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { userEvent, within } from '@storybook/testing-library';
|
||||
|
||||
import { IconList } from '@/ui/icons/index';
|
||||
import { FullHeightStorybookLayout } from '~/testing/FullHeightStorybookLayout';
|
||||
import { mockedClient } from '~/testing/mockedClient';
|
||||
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||
|
||||
import { availableFilters } from '../../../../../../pages/companies/companies-filters';
|
||||
import { availableSorts } from '../../../../../../pages/companies/companies-sorts';
|
||||
@ -20,32 +18,24 @@ export default meta;
|
||||
type Story = StoryObj<typeof TableHeader>;
|
||||
|
||||
export const Empty: Story = {
|
||||
render: () => (
|
||||
<ApolloProvider client={mockedClient}>
|
||||
<FullHeightStorybookLayout>
|
||||
<TableHeader
|
||||
viewName="ViewName"
|
||||
viewIcon={<IconList />}
|
||||
availableSorts={availableSorts}
|
||||
availableFilters={availableFilters}
|
||||
/>
|
||||
</FullHeightStorybookLayout>
|
||||
</ApolloProvider>
|
||||
render: getRenderWrapperForComponent(
|
||||
<TableHeader
|
||||
viewName="ViewName"
|
||||
viewIcon={<IconList />}
|
||||
availableSorts={availableSorts}
|
||||
availableFilters={availableFilters}
|
||||
/>,
|
||||
),
|
||||
};
|
||||
|
||||
export const WithSortsAndFilters: Story = {
|
||||
render: () => (
|
||||
<ApolloProvider client={mockedClient}>
|
||||
<FullHeightStorybookLayout>
|
||||
<TableHeader
|
||||
viewName="ViewName"
|
||||
viewIcon={<IconList />}
|
||||
availableSorts={availableSorts}
|
||||
availableFilters={availableFilters}
|
||||
/>
|
||||
</FullHeightStorybookLayout>
|
||||
</ApolloProvider>
|
||||
render: getRenderWrapperForComponent(
|
||||
<TableHeader
|
||||
viewName="ViewName"
|
||||
viewIcon={<IconList />}
|
||||
availableSorts={availableSorts}
|
||||
availableFilters={availableFilters}
|
||||
/>,
|
||||
),
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
Reference in New Issue
Block a user