Skip to content

Commit

Permalink
#25 Table wide undo history
Browse files Browse the repository at this point in the history
  • Loading branch information
Polleps committed Jul 12, 2023
1 parent dc4a69c commit cc31ecb
Show file tree
Hide file tree
Showing 14 changed files with 468 additions and 114 deletions.
21 changes: 19 additions & 2 deletions data-browser/src/components/TableEditor/TableEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import React, { useCallback, useEffect, useId, useRef, useState } from 'react';
import React, {
useCallback,
useEffect,
useId,
useMemo,
useRef,
useState,
} from 'react';
import styled from 'styled-components';
import { FixedSizeList, ListOnScrollProps } from 'react-window';
import Autosizer from 'react-virtualized-auto-sizer';
Expand Down Expand Up @@ -36,6 +43,7 @@ interface FancyTableProps<T> {
rowHeight?: number;
columnToKey: (column: T) => string;
lablledBy: string;
onUndoCommand?: () => void;
onClearRow?: (index: number) => void;
onClearCells?: (cells: CellIndex<T>[]) => void;
onCopyCommand?: (cells: CellIndex<T>[]) => Promise<CopyValue[][]>;
Expand Down Expand Up @@ -77,6 +85,7 @@ function FancyTableInner<T>({
onClearCells,
onClearRow,
onCopyCommand,
onUndoCommand,
onPasteCommand,
onColumnReorder,
HeadingComponent,
Expand All @@ -100,12 +109,20 @@ function FancyTableInner<T>({
const triggerCopyCommand = useCopyCommand(columns, onCopyCommand);
const triggerPasteCommand = usePasteCommand(columns, onPasteCommand);

const tableCommands = useMemo(
() => ({
copy: triggerCopyCommand,
undo: onUndoCommand,
}),
[triggerCopyCommand, onUndoCommand],
);

const handleKeyDown = useTableEditorKeyboardNavigation(
columns.length,
itemCount,
tableRef,
headerRef,
triggerCopyCommand,
tableCommands,
);

useClearCommands(columns, onClearRow, onClearCells);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,21 @@ export enum KeyboardInteraction {
MoveMultiSelectCornerDown,
MoveMultiSelectCornerLeft,
MoveMultiSelectCornerRight,
Undo,
}

export interface HandlerContext {
export type TableCommands = {
copy?: () => void;
undo?: () => void;
};

export type HandlerContext = {
tableContext: TableEditorContext;
event: React.KeyboardEvent;
tableRef: React.RefObject<HTMLDivElement>;
translateCursor: (row: number, column: number) => void;
columnCount: number;
triggerCopyCommand: () => void;
}
} & TableCommands;

export interface KeyboardHandler {
id: KeyboardInteraction;
Expand Down Expand Up @@ -140,7 +145,7 @@ const editPreviousCell: KeyboardHandler = {
},
};

const copy: KeyboardHandler = {
const copyCommand: KeyboardHandler = {
id: KeyboardInteraction.Copy,
keys: new Set(['c']),
mod: true,
Expand All @@ -149,9 +154,20 @@ const copy: KeyboardHandler = {
tableContext.selectedColumn !== undefined &&
tableContext.selectedRow !== undefined,

handler: ({ event, triggerCopyCommand }) => {
handler: ({ event, copy }) => {
event.preventDefault();
triggerCopyCommand();
copy?.();
},
};

const undoCommand: KeyboardHandler = {
id: KeyboardInteraction.Undo,
keys: new Set(['z']),
mod: true,
cursorMode: new Set([CursorMode.Visual, CursorMode.MultiSelect]),
condition: () => document.activeElement?.tagName !== 'INPUT',
handler: ({ undo }) => {
undo?.();
},
};

Expand Down Expand Up @@ -321,7 +337,8 @@ export const tableKeyboardHandlers = [
editNextRow,
editNextCell,
editPreviousCell,
copy,
copyCommand,
undoCommand,
deleteCell,
deleteRow,
moveCursorUp,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
import {
HandlerContext,
KeyboardHandler,
TableCommands,
tableKeyboardHandlers,
} from '../helpers/keyboardHandlers';
import { useTableEditorContext } from '../TableEditorContext';
Expand Down Expand Up @@ -31,7 +32,7 @@ export function useTableEditorKeyboardNavigation(
rowCount: number,
tableRef: React.RefObject<HTMLDivElement>,
headerRef: React.RefObject<HTMLDivElement>,
triggerCopyCommand: () => void,
commands: TableCommands,
) {
const tableContext = useTableEditorContext();
const {
Expand Down Expand Up @@ -78,7 +79,7 @@ export function useTableEditorKeyboardNavigation(
event: e,
tableRef,
columnCount,
triggerCopyCommand,
...commands,
translateCursor,
};

Expand Down Expand Up @@ -107,7 +108,7 @@ export function useTableEditorKeyboardNavigation(
multiSelectCornerRow,
multiSelectCornerColumn,
tableContext,
triggerCopyCommand,
commands,
hasControlLock,
],
);
Expand Down
33 changes: 26 additions & 7 deletions data-browser/src/views/TablePage/TableCell.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { JSONValue, Property, Resource, urls, useValue } from '@tomic/react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import { Cell } from '../../components/TableEditor';
import { CellAlign } from '../../components/TableEditor/Cell';
import {
Expand All @@ -12,6 +18,8 @@ import {
dataTypeCellMap,
} from './dataTypeMaps';
import { StringCell } from './EditorCells/StringCell';
import { TablePageContext } from './tablePageContext';
import { createValueChangedHistoryItem } from './helpers/useTableHistory';

interface TableCell {
columnIndex: number;
Expand Down Expand Up @@ -45,7 +53,10 @@ export function TableCell({
invalidateTable,
}: TableCell): JSX.Element {
const [markForInvalidate, setMarkForInvalidate] = useState(false);
const { addItemsToHistoryStack } = useContext(TablePageContext);

const [value, setValue] = useValue(resource, property.subject, valueOpts);

const [createdAt, setCreatedAt] = useValue(
resource,
urls.properties.commit.createdAt,
Expand All @@ -65,16 +76,22 @@ export function TableCell({
const onChange = useCallback(
async (v: JSONValue) => {
if (!createdAt) {
addItemsToHistoryStack(
createValueChangedHistoryItem(resource, property.subject),
);
await setValue(v);
await setCreatedAt(Date.now());
setMarkForInvalidate(true);

return;
}

addItemsToHistoryStack(
createValueChangedHistoryItem(resource, property.subject),
);
await setValue(v);
},
[setValue, setCreatedAt, createdAt],
[setValue, setCreatedAt, createdAt, value, resource, property],
);

const handleEnterEditModeWithCharacter = useCallback(
Expand Down Expand Up @@ -106,11 +123,13 @@ export function TableCell({
resource={resource}
/>
) : (
<Editor.Display
value={value}
onChange={onChange}
property={property.subject}
/>
<React.Fragment key={`${value}`}>
<Editor.Display
value={value}
onChange={onChange}
property={property.subject}
/>
</React.Fragment>
)}
</Cell>
);
Expand Down
81 changes: 22 additions & 59 deletions data-browser/src/views/TablePage/TablePage.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,26 @@
import { Collection, Property, useStore } from '@tomic/react';
import { Property, useStore } from '@tomic/react';
import React, { useCallback, useId, useMemo } from 'react';
import { ContainerFull } from '../../components/Containers';
import { EditableTitle } from '../../components/EditableTitle';
import { CellIndex, CopyValue, FancyTable } from '../../components/TableEditor';
import { FancyTable } from '../../components/TableEditor';
import type { ResourcePageProps } from '../ResourcePage';
import { TableHeading } from './TableHeading';
import { useTableColumns } from './useTableColumns';
import { TableNewRow, TableRow } from './TableRow';
import { useTableData } from './useTableData';
import { getValuesFromSubject } from './helpers/clipboard';
import { NewColumnButton } from './NewColumnButton';
import { TablePageContext, TablePageContextType } from './tablePageContext';
import { useHandlePaste } from './helpers/useHandlePaste';
import { useHandleColumnResize } from './helpers/useHandleColumnResize';
import {
createResourceDeletedHistoryItem,
useTableHistory,
} from './helpers/useTableHistory';
import { useHandleClearCells } from './helpers/useHandleClearCells';
import { useHandleCopyCommand } from './helpers/useHandleCopyCommand';

const columnToKey = (column: Property) => column.subject;

const transformToPropertiesPerSubject = async (
cells: CellIndex<Property>[],
collection: Collection,
): Promise<Record<string, Property[]>> => {
const result: Record<string, Property[]> = {};

for (const [rowIndex, property] of cells) {
const subject = await collection.getMemberWithIndex(rowIndex);

result[subject] = [...(result[subject] ?? []), property];
}

return result;
};

export function TablePage({ resource }: ResourcePageProps): JSX.Element {
const store = useStore();
const titleId = useId();
Expand All @@ -46,11 +36,15 @@ export function TablePage({ resource }: ResourcePageProps): JSX.Element {

const { columns, reorderColumns } = useTableColumns(tableClass);

const { undoLastItem, addItemsToHistoryStack } =
useTableHistory(invalidateCollection);

const handlePaste = useHandlePaste(
resource,
collection,
tableClass,
invalidateCollection,
addItemsToHistoryStack,
collectionVersion,
);

Expand All @@ -59,8 +53,9 @@ export function TablePage({ resource }: ResourcePageProps): JSX.Element {
tableClassResource: tableClass,
sorting,
setSortBy,
addItemsToHistoryStack,
}),
[tableClass, setSortBy, sorting],
[tableClass, setSortBy, sorting, addItemsToHistoryStack],
);

const handleDeleteRow = useCallback(
Expand All @@ -72,54 +67,21 @@ export function TablePage({ resource }: ResourcePageProps): JSX.Element {
}

const rowResource = store.getResourceLoading(row);
addItemsToHistoryStack(createResourceDeletedHistoryItem(rowResource));

await rowResource.destroy(store);

invalidateCollection();
},
[collection, store, invalidateCollection],
);

const handleClearCells = useCallback(
async (cells: CellIndex<Property>[]) => {
const resourcePropMap = await transformToPropertiesPerSubject(
cells,
collection,
);

const removePropvals = async ([subject, props]: [string, Property[]]) => {
const res = await store.getResourceAsync(subject);

await Promise.all(
props.map(prop => res.set(prop.subject, undefined, store, false)),
);

await res.save(store);
};

await Promise.all(
Array.from(Object.entries(resourcePropMap)).map(removePropvals),
);

invalidateCollection();
},
[store, collection, invalidateCollection],
const handleClearCells = useHandleClearCells(
collection,
addItemsToHistoryStack,
);

const handleCopyCommand = useCallback(
async (cells: CellIndex<Property>[]): Promise<CopyValue[][]> => {
const propertiesPerSubject = await transformToPropertiesPerSubject(
cells,
collection,
);

const unresolvedValues = Array.from(
Object.entries(propertiesPerSubject),
).map(([subject, props]) => getValuesFromSubject(subject, props, store));

return Promise.all(unresolvedValues);
},

[collection, collectionVersion, store],
);
const handleCopyCommand = useHandleCopyCommand(collection, collectionVersion);

const [columnSizes, handleColumnResize] = useHandleColumnResize(resource);

Expand Down Expand Up @@ -162,6 +124,7 @@ export function TablePage({ resource }: ResourcePageProps): JSX.Element {
onClearCells={handleClearCells}
onCopyCommand={handleCopyCommand}
onPasteCommand={handlePaste}
onUndoCommand={undoLastItem}
onColumnReorder={reorderColumns}
HeadingComponent={TableHeading}
NewColumnButtonComponent={NewColumnButton}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Collection, Property } from '@tomic/react';
import { CellIndex } from '../../../components/TableEditor';

export const transformToPropertiesPerSubject = async (
cells: CellIndex<Property>[],
collection: Collection,
): Promise<Record<string, Property[]>> => {
const result: Record<string, Property[]> = {};

for (const [rowIndex, property] of cells) {
const subject = await collection.getMemberWithIndex(rowIndex);

result[subject] = [...(result[subject] ?? []), property];
}

return result;
};
Loading

0 comments on commit cc31ecb

Please sign in to comment.