Feat/improve editable cell (#959)

* Removed isSomeInputInEditMode

* Removed console.log

* Added a first version of generic cell text

* Removed metadata from entity table  V1

* Fix

* Fix

* Fix
This commit is contained in:
Lucas Bordeau
2023-07-27 07:53:57 +02:00
committed by GitHub
parent 13f415a859
commit 011d9e840f
27 changed files with 705 additions and 92 deletions

View File

@ -0,0 +1,36 @@
import { useRecoilValue } from 'recoil';
import { isNavbarSwitchingSizeState } from '@/ui/layout/states/isNavbarSwitchingSizeState';
import { isFetchingEntityTableDataState } from '../states/isFetchingEntityTableDataState';
import { RowIdContext } from '../states/RowIdContext';
import { RowIndexContext } from '../states/RowIndexContext';
import { tableRowIdsState } from '../states/tableRowIdsState';
import { EntityTableRow } from './EntityTableRowV2';
export function EntityTableBody() {
const rowIds = useRecoilValue(tableRowIdsState);
const isNavbarSwitchingSize = useRecoilValue(isNavbarSwitchingSizeState);
const isFetchingEntityTableData = useRecoilValue(
isFetchingEntityTableDataState,
);
if (isFetchingEntityTableData || isNavbarSwitchingSize) {
return null;
}
return (
<tbody>
{rowIds.map((rowId, index) => (
<RowIdContext.Provider value={rowId} key={rowId}>
<RowIndexContext.Provider value={index}>
<EntityTableRow rowId={rowId} />
</RowIndexContext.Provider>
</RowIdContext.Provider>
))}
</tbody>
);
}

View File

@ -0,0 +1,50 @@
import { useContext } from 'react';
import { useSetRecoilState } from 'recoil';
import { GenericEditableCell } from '@/people/table/components/GenericEditableCell';
import { RecoilScope } from '../../recoil-scope/components/RecoilScope';
import { useCurrentRowSelected } from '../hooks/useCurrentRowSelected';
import { ColumnIndexContext } from '../states/ColumnIndexContext';
import { contextMenuPositionState } from '../states/contextMenuPositionState';
import { EntityFieldMetadataContext } from '../states/EntityFieldMetadataContext';
export function EntityTableCell({ cellIndex }: { cellIndex: number }) {
const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
const { setCurrentRowSelected } = useCurrentRowSelected();
function handleContextMenu(event: React.MouseEvent) {
event.preventDefault();
setCurrentRowSelected(true);
setContextMenuPosition({
x: event.clientX,
y: event.clientY,
});
}
const entityFieldMetadata = useContext(EntityFieldMetadataContext);
if (!entityFieldMetadata) {
return null;
}
return (
<RecoilScope>
<ColumnIndexContext.Provider value={cellIndex}>
<td
onContextMenu={(event) => handleContextMenu(event)}
style={{
width: entityFieldMetadata.columnSize,
minWidth: entityFieldMetadata.columnSize,
maxWidth: entityFieldMetadata.columnSize,
}}
>
<GenericEditableCell entityFieldMetadata={entityFieldMetadata} />
</td>
</ColumnIndexContext.Provider>
</RecoilScope>
);
}

View File

@ -0,0 +1,42 @@
import { useRecoilValue } from 'recoil';
import { entityFieldMetadataArrayState } from '../states/entityFieldMetadataArrayState';
import { ColumnHead } from './ColumnHead';
import { SelectAllCheckbox } from './SelectAllCheckbox';
export function EntityTableHeader() {
const fieldMetadataArray = useRecoilValue(entityFieldMetadataArrayState);
return (
<thead>
<tr>
<th
style={{
width: 30,
minWidth: 30,
maxWidth: 30,
}}
>
<SelectAllCheckbox />
</th>
{fieldMetadataArray.map((fieldMetadata) => (
<th
key={fieldMetadata.fieldName.toString()}
style={{
width: fieldMetadata.columnSize,
minWidth: fieldMetadata.columnSize,
maxWidth: fieldMetadata.columnSize,
}}
>
<ColumnHead
viewName={fieldMetadata.label}
viewIcon={fieldMetadata.icon}
/>
</th>
))}
<th></th>
</tr>
</thead>
);
}

View File

@ -0,0 +1,38 @@
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { entityFieldMetadataArrayState } from '../states/entityFieldMetadataArrayState';
import { EntityFieldMetadataContext } from '../states/EntityFieldMetadataContext';
import { CheckboxCell } from './CheckboxCell';
import { EntityTableCell } from './EntityTableCellV2';
const StyledRow = styled.tr<{ selected: boolean }>`
background: ${(props) =>
props.selected ? props.theme.background.secondary : 'none'};
`;
export function EntityTableRow({ rowId }: { rowId: string }) {
const entityFieldMetadataArray = useRecoilValue(
entityFieldMetadataArrayState,
);
return (
<StyledRow data-testid={`row-id-${rowId}`} selected={false}>
<td>
<CheckboxCell />
</td>
{entityFieldMetadataArray.map((entityFieldMetadata, columnIndex) => {
return (
<EntityFieldMetadataContext.Provider
value={entityFieldMetadata}
key={entityFieldMetadata.fieldName}
>
<EntityTableCell cellIndex={columnIndex} />
</EntityFieldMetadataContext.Provider>
);
})}
<td></td>
</StyledRow>
);
}

View File

@ -0,0 +1,136 @@
import * as React from 'react';
import styled from '@emotion/styled';
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
import { useListenClickOutside } from '@/ui/hooks/useListenClickOutside';
import { useLeaveTableFocus } from '../hooks/useLeaveTableFocus';
import { useMapKeyboardToSoftFocus } from '../hooks/useMapKeyboardToSoftFocus';
import { EntityUpdateFieldHookContext } from '../states/EntityUpdateFieldHookContext';
import { TableHeader } from '../table-header/components/TableHeader';
import { EntityUpdateFieldHook } from '../types/CellUpdateFieldHook';
import { EntityTableBody } from './EntityTableBodyV2';
import { EntityTableHeader } from './EntityTableHeaderV2';
const StyledTable = styled.table`
border-collapse: collapse;
border-radius: ${({ theme }) => theme.border.radius.sm};
border-spacing: 0;
margin-left: ${({ theme }) => theme.table.horizontalCellMargin};
margin-right: ${({ theme }) => theme.table.horizontalCellMargin};
table-layout: fixed;
width: calc(100% - ${({ theme }) => theme.table.horizontalCellMargin} * 2);
th {
border: 1px solid ${({ theme }) => theme.border.color.light};
border-collapse: collapse;
color: ${({ theme }) => theme.font.color.tertiary};
padding: 0;
text-align: left;
:last-child {
border-right-color: transparent;
}
:first-of-type {
border-left-color: transparent;
border-right-color: transparent;
}
:last-of-type {
min-width: 0;
width: 100%;
}
}
td {
border: 1px solid ${({ theme }) => theme.border.color.light};
border-collapse: collapse;
color: ${({ theme }) => theme.font.color.primary};
padding: 0;
text-align: left;
:last-child {
border-right-color: transparent;
}
:first-of-type {
border-left-color: transparent;
border-right-color: transparent;
}
:last-of-type {
min-width: 0;
width: 100%;
}
}
`;
const StyledTableWithHeader = styled.div`
display: flex;
flex: 1;
flex-direction: column;
width: 100%;
`;
const StyledTableContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
overflow: auto;
`;
const StyledTableWrapper = styled.div`
flex: 1;
overflow: auto;
`;
type OwnProps<SortField> = {
viewName: string;
viewIcon?: React.ReactNode;
availableSorts?: Array<SortType<SortField>>;
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onRowSelectionChange?: (rowSelection: string[]) => void;
useUpdateField: EntityUpdateFieldHook;
};
export function EntityTable<SortField>({
viewName,
viewIcon,
availableSorts,
onSortsUpdate,
useUpdateField,
}: OwnProps<SortField>) {
const tableBodyRef = React.useRef<HTMLDivElement>(null);
useMapKeyboardToSoftFocus();
const leaveTableFocus = useLeaveTableFocus();
useListenClickOutside({
refs: [tableBodyRef],
callback: () => {
leaveTableFocus();
},
});
return (
<EntityUpdateFieldHookContext.Provider value={useUpdateField}>
<StyledTableWithHeader>
<StyledTableContainer ref={tableBodyRef}>
<TableHeader
viewName={viewName}
viewIcon={viewIcon}
availableSorts={availableSorts}
onSortsUpdate={onSortsUpdate}
/>
<StyledTableWrapper>
<StyledTable>
<EntityTableHeader />
<EntityTableBody />
</StyledTable>
</StyledTableWrapper>
</StyledTableContainer>
</StyledTableWithHeader>
</EntityUpdateFieldHookContext.Provider>
);
}

View File

@ -1,12 +1,10 @@
import { useContext } from 'react';
import { useRecoilCallback } from 'recoil';
import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
import { useCloseCurrentCellInEditMode } from '../../hooks/useClearCellInEditMode';
import { CellHotkeyScopeContext } from '../../states/CellHotkeyScopeContext';
import { isSomeInputInEditModeState } from '../../states/isSomeInputInEditModeState';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
import { useCurrentCellEditMode } from './useCurrentCellEditMode';
@ -29,33 +27,18 @@ export function useEditableCell() {
setHotkeyScope(TableHotkeyScope.TableSoftFocus);
}
const openEditableCell = useRecoilCallback(
({ snapshot, set }) =>
() => {
const isSomeInputInEditMode = snapshot
.getLoadable(isSomeInputInEditModeState)
.valueOrThrow();
function openEditableCell() {
setCurrentCellInEditMode();
if (!isSomeInputInEditMode) {
set(isSomeInputInEditModeState, true);
setCurrentCellInEditMode();
if (customCellHotkeyScope) {
setHotkeyScope(
customCellHotkeyScope.scope,
customCellHotkeyScope.customScopes,
);
} else {
setHotkeyScope(
DEFAULT_CELL_SCOPE.scope,
DEFAULT_CELL_SCOPE.customScopes,
);
}
}
},
[setCurrentCellInEditMode, setHotkeyScope, customCellHotkeyScope],
);
if (customCellHotkeyScope) {
setHotkeyScope(
customCellHotkeyScope.scope,
customCellHotkeyScope.customScopes,
);
} else {
setHotkeyScope(DEFAULT_CELL_SCOPE.scope, DEFAULT_CELL_SCOPE.customScopes);
}
}
return {
closeEditableCell,

View File

@ -0,0 +1,7 @@
import { useContext } from 'react';
import { EntityUpdateFieldHookContext } from '../states/EntityUpdateFieldHookContext';
export function useEntityUpdateFieldHook() {
return useContext(EntityUpdateFieldHookContext);
}

View File

@ -2,7 +2,6 @@ import { useRecoilCallback } from 'recoil';
import { currentCellInEditModePositionState } from '../states/currentCellInEditModePositionState';
import { isCellInEditModeFamilyState } from '../states/isCellInEditModeFamilyState';
import { isSomeInputInEditModeState } from '../states/isSomeInputInEditModeState';
export function useCloseCurrentCellInEditMode() {
return useRecoilCallback(({ set, snapshot }) => {
@ -12,11 +11,6 @@ export function useCloseCurrentCellInEditMode() {
);
set(isCellInEditModeFamilyState(currentCellInEditModePosition), false);
// TODO: find a way to remove this
await new Promise((resolve) => setTimeout(resolve, 20));
set(isSomeInputInEditModeState, false);
};
}, []);
}

View File

@ -4,7 +4,6 @@ import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
import { currentHotkeyScopeState } from '@/ui/hotkey/states/internal/currentHotkeyScopeState';
import { isSoftFocusActiveState } from '../states/isSoftFocusActiveState';
import { isSomeInputInEditModeState } from '../states/isSomeInputInEditModeState';
import { TableHotkeyScope } from '../types/TableHotkeyScope';
import { useCloseCurrentCellInEditMode } from './useClearCellInEditMode';
@ -23,19 +22,11 @@ export function useLeaveTableFocus() {
.getLoadable(isSoftFocusActiveState)
.valueOrThrow();
const isSomeInputInEditMode = snapshot
.getLoadable(isSomeInputInEditModeState)
.valueOrThrow();
const currentHotkeyScope = snapshot
.getLoadable(currentHotkeyScopeState)
.valueOrThrow();
if (isSomeInputInEditMode) {
return;
}
if (!isSoftFocusActive && !isSomeInputInEditMode) {
if (!isSoftFocusActive) {
return;
}

View File

@ -1,10 +1,8 @@
import { useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
import { isSomeInputInEditModeState } from '../states/isSomeInputInEditModeState';
import { TableHotkeyScope } from '../types/TableHotkeyScope';
import { useDisableSoftFocus } from './useDisableSoftFocus';
@ -16,50 +14,40 @@ export function useMapKeyboardToSoftFocus() {
const disableSoftFocus = useDisableSoftFocus();
const setHotkeyScope = useSetHotkeyScope();
const [isSomeInputInEditMode] = useRecoilState(isSomeInputInEditModeState);
useScopedHotkeys(
[Key.ArrowUp, `${Key.Shift}+${Key.Enter}`],
() => {
if (!isSomeInputInEditMode) {
moveUp();
}
moveUp();
},
TableHotkeyScope.TableSoftFocus,
[moveUp, isSomeInputInEditMode],
[moveUp],
);
useScopedHotkeys(
Key.ArrowDown,
() => {
if (!isSomeInputInEditMode) {
moveDown();
}
moveDown();
},
TableHotkeyScope.TableSoftFocus,
[moveDown, isSomeInputInEditMode],
[moveDown],
);
useScopedHotkeys(
[Key.ArrowLeft, `${Key.Shift}+${Key.Tab}`],
() => {
if (!isSomeInputInEditMode) {
moveLeft();
}
moveLeft();
},
TableHotkeyScope.TableSoftFocus,
[moveLeft, isSomeInputInEditMode],
[moveLeft],
);
useScopedHotkeys(
[Key.ArrowRight, Key.Tab],
() => {
if (!isSomeInputInEditMode) {
moveRight();
}
moveRight();
},
TableHotkeyScope.TableSoftFocus,
[moveRight, isSomeInputInEditMode],
[moveRight],
);
useScopedHotkeys(

View File

@ -0,0 +1,6 @@
import { createContext } from 'react';
import { EntityFieldMetadata } from '../types/EntityFieldMetadata';
export const EntityFieldMetadataContext =
createContext<EntityFieldMetadata | null>(null);

View File

@ -0,0 +1,6 @@
import { createContext } from 'react';
import { EntityUpdateFieldHook } from '../types/CellUpdateFieldHook';
export const EntityUpdateFieldHookContext =
createContext<EntityUpdateFieldHook | null>(null);

View File

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

View File

@ -1,6 +0,0 @@
import { atom } from 'recoil';
export const isSomeInputInEditModeState = atom<boolean>({
key: 'isSomeInputInEditModeState',
default: false,
});

View File

@ -0,0 +1,9 @@
import { atomFamily } from 'recoil';
export const tableEntitiesFamilyState = atomFamily<
Record<string, unknown> | null,
string
>({
key: 'tableEntitiesFamilyState',
default: null,
});

View File

@ -0,0 +1,18 @@
import { selectorFamily } from 'recoil';
import { tableEntitiesFamilyState } from './tableEntitiesFamilyState';
export const tableEntityFieldFamilySelector = selectorFamily({
key: 'tableEntityFieldFamilySelector',
get:
<T>({ fieldName, entityId }: { fieldName: string; entityId: string }) =>
({ get }) =>
get(tableEntitiesFamilyState(entityId))?.[fieldName] as T,
set:
<T>({ fieldName, entityId }: { fieldName: string; entityId: string }) =>
({ set }, newValue: T) =>
set(tableEntitiesFamilyState(entityId), (prevState) => ({
...prevState,
[fieldName]: newValue,
})),
});

View File

@ -0,0 +1,5 @@
export type EntityUpdateFieldHook = () => <T>(
entityId: string,
fieldName: string,
value: T,
) => void | Promise<void>;

View File

@ -0,0 +1,16 @@
export type EntityFieldType =
| 'text'
| 'number'
| 'date'
| 'select'
| 'checkbox'
| 'icon';
export type EntityFieldMetadata = {
fieldName: string;
label: string;
type: EntityFieldType;
icon: JSX.Element;
columnSize: number;
filterIcon?: JSX.Element;
};