From 6c67004e2406c0953e1f6692a381d7bf80ea0ec3 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Wed, 12 Jul 2023 20:26:26 -0400 Subject: [PATCH 01/85] Begin scaffolding new Selection data structure --- mathesar_ui/.eslintrc.cjs | 1 + mathesar_ui/src/components/sheet/cellIds.ts | 24 ++ .../components/sheet/selection/IdSequence.ts | 50 ++++ .../src/components/sheet/selection/Plane.ts | 72 ++++++ .../components/sheet/selection/Selection.ts | 243 ++++++++++++++++++ .../components/sheet/tests/cellIds.test.ts | 27 ++ .../src/utils/__tests__/iterUtils.test.ts | 21 ++ mathesar_ui/src/utils/iterUtils.ts | 14 + 8 files changed, 452 insertions(+) create mode 100644 mathesar_ui/src/components/sheet/cellIds.ts create mode 100644 mathesar_ui/src/components/sheet/selection/IdSequence.ts create mode 100644 mathesar_ui/src/components/sheet/selection/Plane.ts create mode 100644 mathesar_ui/src/components/sheet/selection/Selection.ts create mode 100644 mathesar_ui/src/components/sheet/tests/cellIds.test.ts create mode 100644 mathesar_ui/src/utils/__tests__/iterUtils.test.ts create mode 100644 mathesar_ui/src/utils/iterUtils.ts diff --git a/mathesar_ui/.eslintrc.cjs b/mathesar_ui/.eslintrc.cjs index 5487252656..7de5aaf976 100644 --- a/mathesar_ui/.eslintrc.cjs +++ b/mathesar_ui/.eslintrc.cjs @@ -21,6 +21,7 @@ module.exports = { rules: { 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], 'no-console': ['warn', { allow: ['error'] }], + 'generator-star-spacing': 'off', '@typescript-eslint/explicit-member-accessibility': 'off', '@typescript-eslint/ban-ts-comment': [ 'error', diff --git a/mathesar_ui/src/components/sheet/cellIds.ts b/mathesar_ui/src/components/sheet/cellIds.ts new file mode 100644 index 0000000000..40c8297dea --- /dev/null +++ b/mathesar_ui/src/components/sheet/cellIds.ts @@ -0,0 +1,24 @@ +const CELL_ID_DELIMITER = '-'; + +/** + * We can serialize a cell id this way only because we're confident that the + * rowId will never contain the delimiter. Some columnIds _do_ contain + * delimiters (e.g. in the Data Explorer), but that's okay because we can still + * separate the values based on the first delimiter. + */ +export function makeCellId(rowId: string, columnId: string): string { + return `${rowId}${CELL_ID_DELIMITER}${columnId}`; +} + +export function parseCellId(cellId: string): { + rowId: string; + columnId: string; +} { + const delimiterIndex = cellId.indexOf(CELL_ID_DELIMITER); + if (delimiterIndex === -1) { + throw new Error(`Unable to parse cell id without a delimiter: ${cellId}.`); + } + const rowId = cellId.slice(0, delimiterIndex); + const columnId = cellId.slice(delimiterIndex + 1); + return { rowId, columnId }; +} diff --git a/mathesar_ui/src/components/sheet/selection/IdSequence.ts b/mathesar_ui/src/components/sheet/selection/IdSequence.ts new file mode 100644 index 0000000000..85eb73ebe6 --- /dev/null +++ b/mathesar_ui/src/components/sheet/selection/IdSequence.ts @@ -0,0 +1,50 @@ +import { ImmutableMap } from '@mathesar/component-library'; + +export default class IdSequence { + private readonly values: Id[]; + + /** Maps the id value to its index */ + private readonly indexLookup: ImmutableMap; + + /** + * @throws Error if duplicate values are provided + */ + constructor(values: Id[]) { + this.values = values; + this.indexLookup = new ImmutableMap( + values.map((value, index) => [value, index]), + ); + if (new Set(values).size !== values.length) { + throw new Error('Duplicate values are not allowed within an IdSequence.'); + } + } + + get length(): number { + return this.values.length; + } + + /** + * Return an iterator of all values between the two provided values, + * inclusive. Iteration occurs in the order stored. The two provided values + * may be present in any order. + * + * @throws an Error if either value is not present in the sequence + */ + range(a: Id, b: Id): Iterable { + const aIndex = this.indexLookup.get(a); + const bIndex = this.indexLookup.get(b); + + if (aIndex === undefined || bIndex === undefined) { + throw new Error('Id value not found within sequence.'); + } + + const startIndex = Math.min(aIndex, bIndex); + const endIndex = Math.max(aIndex, bIndex); + + return this.values.slice(startIndex, endIndex + 1); + } + + [Symbol.iterator](): Iterator { + return this.values[Symbol.iterator](); + } +} diff --git a/mathesar_ui/src/components/sheet/selection/Plane.ts b/mathesar_ui/src/components/sheet/selection/Plane.ts new file mode 100644 index 0000000000..cec6abe732 --- /dev/null +++ b/mathesar_ui/src/components/sheet/selection/Plane.ts @@ -0,0 +1,72 @@ +import { map } from 'iter-tools'; + +import { cartesianProduct } from '@mathesar/utils/iterUtils'; +import type IdSequence from './IdSequence'; +import { makeCellId } from '../cellIds'; + +function makeCells( + rowIds: Iterable, + columnIds: Iterable, +): Iterable { + return map( + ([rowId, columnId]) => makeCellId(rowId, columnId), + cartesianProduct(rowIds, columnIds), + ); +} + +export default class Plane { + readonly rowIds: IdSequence; + + readonly columnIds: IdSequence; + + readonly placeholderRowId: string | undefined; + + constructor( + rowIds: IdSequence, + columnIds: IdSequence, + placeholderRowId: string | undefined, + ) { + this.rowIds = rowIds; + this.columnIds = columnIds; + this.placeholderRowId = placeholderRowId; + } + + /** + * @returns an iterable of all the data cells in the plane. This does not + * include header cells or placeholder cells. + */ + allDataCells(): Iterable { + return makeCells(this.rowIds, this.columnIds); + } + + /** + * @returns an iterable of all the data cells in the plane that are in or + * between the two given rows. This does not include header cells. + * + * @throws Error if the id of the placeholder row is specified because the + * placeholder row is not a data row. + */ + dataCellsInRowRange(rowIdA: string, rowIdB: string): Iterable { + return makeCells(this.rowIds.range(rowIdA, rowIdB), this.columnIds); + } + + /** + * @returns an iterable of all the data cells in the plane that are in or + * between the two given columns. This does not include header cells or + * placeholder cells. + */ + dataCellsInColumnRange( + columnIdA: string, + columnIdB: string, + ): Iterable { + return makeCells(this.rowIds, this.columnIds.range(columnIdA, columnIdB)); + } + + get hasResultRows(): boolean { + return this.rowIds.length > 0; + } + + get hasPlaceholder(): boolean { + return this.placeholderRowId !== undefined; + } +} diff --git a/mathesar_ui/src/components/sheet/selection/Selection.ts b/mathesar_ui/src/components/sheet/selection/Selection.ts new file mode 100644 index 0000000000..44f70d27c4 --- /dev/null +++ b/mathesar_ui/src/components/sheet/selection/Selection.ts @@ -0,0 +1,243 @@ +import { first } from 'iter-tools'; + +import { ImmutableSet } from '@mathesar/component-library'; +import type Plane from './Plane'; +import { parseCellId } from '../cellIds'; + +export enum Direction { + Up = 'up', + Down = 'down', + Left = 'left', + Right = 'right', +} + +interface Basis { + /** + * - `'dataCells'` means that the selection contains data cells. This is by + * far the most common type of selection basis. + * + * - `'emptyColumns'` is used when the sheet has no rows. In this case we + * still want to allow the user to select columns, so we use this basis. + * + * - `'placeholderCell'` is used when the user is selecting a cell in the + * placeholder row. This is a special case because we don't want to allow + * the user to select multiple cells in the placeholder row, and we also + * don't want to allow selections that include cells in data rows _and_ the + * placeholder row. + */ + readonly type: 'dataCells' | 'emptyColumns' | 'placeholderCell'; + readonly activeCellId: string | undefined; + readonly cellIds: ImmutableSet; + readonly rowIds: ImmutableSet; + readonly columnIds: ImmutableSet; +} + +function basisFromDataCells( + cellIds: Iterable, + activeCellId?: string, +): Basis { + const parsedCells = [...cellIds].map(parseCellId); + return { + type: 'dataCells', + activeCellId: activeCellId ?? first(cellIds), + cellIds: new ImmutableSet(cellIds), + columnIds: new ImmutableSet(parsedCells.map((cellId) => cellId.columnId)), + rowIds: new ImmutableSet(parsedCells.map((cellId) => cellId.rowId)), + }; +} + +function basisFromEmptyColumns(columnIds: Iterable): Basis { + return { + type: 'emptyColumns', + activeCellId: undefined, + cellIds: new ImmutableSet(), + columnIds: new ImmutableSet(columnIds), + rowIds: new ImmutableSet(), + }; +} + +function basisFromZeroEmptyColumns(): Basis { + return basisFromEmptyColumns([]); +} + +function basisFromPlaceholderCell(activeCellId: string): Basis { + return { + type: 'placeholderCell', + activeCellId, + cellIds: new ImmutableSet([activeCellId]), + columnIds: new ImmutableSet([parseCellId(activeCellId).columnId]), + rowIds: new ImmutableSet(), + }; +} + +export default class Selection { + private readonly plane: Plane; + + private readonly basis: Basis; + + constructor(plane: Plane, basis: Basis) { + this.plane = plane; + this.basis = basis; + } + + get activeCellId() { + return this.basis.activeCellId; + } + + get cellIds() { + return this.basis.cellIds; + } + + get rowIds() { + return this.basis.rowIds; + } + + get columnIds() { + return this.basis.columnIds; + } + + private withBasis(basis: Basis): Selection { + return new Selection(this.plane, basis); + } + + /** + * @returns a new selection with all cells selected. The active cell will be the + * cell in the first row and first column. + */ + ofAllDataCells(): Selection { + if (!this.plane.hasResultRows) { + return this.withBasis(basisFromZeroEmptyColumns()); + } + return this.withBasis(basisFromDataCells(this.plane.allDataCells())); + } + + /** + * @returns a new selection with the cell in the first row and first column + * selected. + */ + ofFirstDataCell(): Selection { + const firstCellId = first(this.plane.allDataCells()); + if (firstCellId === undefined) { + return this.withBasis(basisFromZeroEmptyColumns()); + } + return this.withBasis(basisFromDataCells([firstCellId])); + } + + /** + * @returns a new selection with all rows selected between (and including) the + * provided rows. + * + * If either of the provided rows are placeholder rows, then the last data row + * will be used in their place. This ensures that the selection is made only + * of data rows, and will never include the placeholder row, even if a user + * drags to select it. + */ + ofRowRange(rowIdA: string, rowIdB: string): Selection { + throw new Error('Not implemented'); + } + + /** + * @returns a new selection of all data cells in all columns between the + * provided columnIds, inclusive. + */ + ofColumnRange(columnIdA: string, columnIdB: string): Selection { + if (!this.plane.hasResultRows) { + throw new Error('Not implemented'); + } + throw new Error('Not implemented'); + } + + /** + * @returns a new selection formed by the rectangle between the provided + * cells, inclusive. + * + * If either of the provided cells are in the placeholder row, then the cell + * in the last data row will be used in its place. This ensures that the + * selection is made only of data cells, and will never include cells in the + * placeholder row, even if a user drags to select a cell in it. + */ + ofCellRange(cellIdA: string, cellIdB: string): Selection { + throw new Error('Not implemented'); + } + + /** + * @returns a new selection formed from one cell within the placeholder row. + * Note that we do not support selections of multiple cells in the placeholder + * row. + */ + atPlaceholderCell(cellId: string): Selection { + throw new Error('Not implemented'); + } + + /** + * @returns a new selection that fits within the provided plane. This is + * useful when a column is deleted, reordered, or inserted. + */ + forNewPlane(plane: Plane): Selection { + throw new Error('Not implemented'); + } + + /** + * @returns a new selection formed by the rectangle between the currently + * active cell and provided cell, inclusive. + * + * This operation is designed to mimic the behavior of Google Sheets when + * shift-clicking a specific cell, or when dragging to create a new selection. + * A new selection is created that contains all cells in a rectangle bounded + * by the active cell (also the first cell selected when dragging) and the + * provided cell. + */ + drawnToCell(cellId: string): Selection { + throw new Error('Not implemented'); + } + + /** + * @returns a new selection formed by the cells in all the rows between the + * active cell and the provided row, inclusive. + */ + drawnToRow(rowId: string): Selection { + throw new Error('Not implemented'); + } + + drawnToColumn(columnId: string): Selection { + throw new Error('Not implemented'); + } + + /** + * @returns a new selection that mimics the behavior of arrow keys in + * spreadsheets. If the active cell can be moved in the provided direction, + * then a new selection is created with only that one cell selected. + */ + collapsedAndMoved(direction: Direction): Selection { + throw new Error('Not implemented'); + } + + /** + * @returns a new selection with the active cell moved within the selection, + * left to right, top to bottom. + * + * This is to handle the `Tab` and `Shift+Tab` keys. + */ + withActiveCellAdvanced(direction: 'forward' | 'back' = 'forward'): Selection { + throw new Error('Not implemented'); + } + + /** + * @returns a new selection that is grown or shrunk to mimic the behavior of + * Google Sheets when manipulating selections via keyboard shortcuts like + * `Shift+Down`. The selection is deformed in the direction of the provided + * argument. The selection is _shrunk_ if doing so will keep the active cell + * within the selection. Otherwise, the selection is _grown_. + * + * Note that other spreadsheet applications have slightly different behavior + * for Shift + arrow keys. For example, LibreOffice Calc maintains state for + * the origin of the selection separate from the active cell. The two cells + * may be different if the user presses `Tab` after making a selection. In + * this case, the selection will be resized with respect to the origin, not + * the active cell. We chose to mimic Google Sheets behavior here because it + * is simpler. + */ + resized(direction: Direction): Selection { + throw new Error('Not implemented'); + } +} diff --git a/mathesar_ui/src/components/sheet/tests/cellIds.test.ts b/mathesar_ui/src/components/sheet/tests/cellIds.test.ts new file mode 100644 index 0000000000..a4c8702b66 --- /dev/null +++ b/mathesar_ui/src/components/sheet/tests/cellIds.test.ts @@ -0,0 +1,27 @@ +import { parseCellId } from '../cellIds'; + +test.each( + // prettier-ignore + [ + // cellId , rowId , columnId + ['a-b' , 'a' , 'b' ], + ['a-b-c' , 'a' , 'b-c' ], + ['a--b' , 'a' , '-b' ], + [' a - b ' , ' a ' , ' b ' ], + ['-' , '' , '' ], + [' - ' , ' ' , ' ' ], + ['--' , '' , '-' ], + ['-a-b' , '' , 'a-b' ], + ['a-' , 'a' , '' ], + ], +)('parseCellId success %#', (cellId, rowId, columnId) => { + const result = parseCellId(cellId); + expect(result.rowId).toBe(rowId); + expect(result.columnId).toBe(columnId); +}); + +test.each([[''], ['foobar']])('parseCellId failure %#', (cellId) => { + expect(() => { + parseCellId(cellId); + }).toThrow(); +}); diff --git a/mathesar_ui/src/utils/__tests__/iterUtils.test.ts b/mathesar_ui/src/utils/__tests__/iterUtils.test.ts new file mode 100644 index 0000000000..a87d5dc5ef --- /dev/null +++ b/mathesar_ui/src/utils/__tests__/iterUtils.test.ts @@ -0,0 +1,21 @@ +import { cartesianProduct } from '../iterUtils'; + +test.each([ + [[], [], []], + [[], ['a', 'b'], []], + [['a', 'b'], [], []], + [ + ['a', 'b'], + ['x', 'y', 'z'], + [ + ['a', 'x'], + ['a', 'y'], + ['a', 'z'], + ['b', 'x'], + ['b', 'y'], + ['b', 'z'], + ], + ], +])('cartesianProduct', (a, b, result) => { + expect([...cartesianProduct(a, b)]).toStrictEqual(result); +}); diff --git a/mathesar_ui/src/utils/iterUtils.ts b/mathesar_ui/src/utils/iterUtils.ts new file mode 100644 index 0000000000..6a8b915719 --- /dev/null +++ b/mathesar_ui/src/utils/iterUtils.ts @@ -0,0 +1,14 @@ +export function cartesianProduct( + a: Iterable, + b: Iterable, +): Iterable<[T, U]> { + return { + *[Symbol.iterator]() { + for (const x of a) { + for (const y of b) { + yield [x, y]; + } + } + }, + }; +} From be561de892212d04c87f3415d1c81375a49e1b61 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Tue, 18 Jul 2023 12:35:46 -0400 Subject: [PATCH 02/85] Upgrade iter-tools package --- mathesar_ui/package-lock.json | 6 +++--- mathesar_ui/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mathesar_ui/package-lock.json b/mathesar_ui/package-lock.json index bd4d9d4ed3..6e78aa6ad4 100644 --- a/mathesar_ui/package-lock.json +++ b/mathesar_ui/package-lock.json @@ -18511,9 +18511,9 @@ } }, "iter-tools": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/iter-tools/-/iter-tools-7.4.0.tgz", - "integrity": "sha512-twcHD87GOpbnfFmOtEQMcZuJzBCViNaYx4//70KkkjFcQTkKcUfcuni5G5dfO4Uyb80KIsWnVGZ4vvkpmYCNQQ==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/iter-tools/-/iter-tools-7.5.3.tgz", + "integrity": "sha512-iEcHpgM9cn6tsI5MewqxyEega9KPbIDytQTEnu6c0MtlQQhQFofssYuRqxCarZgUdzliepRZPwwwflE4wAIjaA==", "requires": { "@babel/runtime": "^7.12.1" } diff --git a/mathesar_ui/package.json b/mathesar_ui/package.json index b092a7c033..9a25e205b9 100644 --- a/mathesar_ui/package.json +++ b/mathesar_ui/package.json @@ -63,7 +63,7 @@ "dayjs": "^1.11.5", "fast-diff": "^1.2.0", "flatpickr": "^4.6.13", - "iter-tools": "^7.4.0", + "iter-tools": "^7.5.3", "js-cookie": "^3.0.1", "papaparse": "^5.4.1", "perfect-scrollbar": "^1.5.5" From 29aedab6ae6e605adc8a4c4630442cb7b571a790 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Tue, 18 Jul 2023 15:15:31 -0400 Subject: [PATCH 03/85] Fill in more logic within Selection scaffolding --- .../components/sheet/selection/Direction.ts | 50 +++++++ .../components/sheet/selection/IdSequence.ts | 82 +++++++++- .../src/components/sheet/selection/Plane.ts | 141 +++++++++++++++++- .../components/sheet/selection/Selection.ts | 122 ++++++++++----- .../selection/__tests__/IdSequence.test.ts | 44 ++++++ .../sheet/selection/__tests__/Plane.test.ts | 58 +++++++ 6 files changed, 456 insertions(+), 41 deletions(-) create mode 100644 mathesar_ui/src/components/sheet/selection/Direction.ts create mode 100644 mathesar_ui/src/components/sheet/selection/__tests__/IdSequence.test.ts create mode 100644 mathesar_ui/src/components/sheet/selection/__tests__/Plane.test.ts diff --git a/mathesar_ui/src/components/sheet/selection/Direction.ts b/mathesar_ui/src/components/sheet/selection/Direction.ts new file mode 100644 index 0000000000..2693dd9000 --- /dev/null +++ b/mathesar_ui/src/components/sheet/selection/Direction.ts @@ -0,0 +1,50 @@ +export enum Direction { + Up = 'up', + Down = 'down', + Left = 'left', + Right = 'right', +} + +export function getDirection(event: KeyboardEvent): Direction | undefined { + const { key } = event; + const shift = event.shiftKey; + switch (true) { + case shift && key === 'Tab': + return Direction.Left; + case shift: + return undefined; + case key === 'ArrowUp': + return Direction.Up; + case key === 'ArrowDown': + return Direction.Down; + case key === 'ArrowLeft': + return Direction.Left; + case key === 'ArrowRight': + case key === 'Tab': + return Direction.Right; + default: + return undefined; + } +} + +export function getColumnOffset(direction: Direction): number { + switch (direction) { + case Direction.Left: + return -1; + case Direction.Right: + return 1; + default: + return 0; + } +} + +export function getRowOffset(direction: Direction): number { + switch (direction) { + case Direction.Up: + return -1; + case Direction.Down: + return 1; + default: + return 0; + } +} diff --git a/mathesar_ui/src/components/sheet/selection/IdSequence.ts b/mathesar_ui/src/components/sheet/selection/IdSequence.ts index 85eb73ebe6..ea0396f621 100644 --- a/mathesar_ui/src/components/sheet/selection/IdSequence.ts +++ b/mathesar_ui/src/components/sheet/selection/IdSequence.ts @@ -1,4 +1,5 @@ import { ImmutableMap } from '@mathesar/component-library'; +import { filter, findBest, firstHighest, firstLowest } from 'iter-tools'; export default class IdSequence { private readonly values: Id[]; @@ -19,6 +20,10 @@ export default class IdSequence { } } + private getIndex(value: Id): number | undefined { + return this.indexLookup.get(value); + } + get length(): number { return this.values.length; } @@ -31,8 +36,8 @@ export default class IdSequence { * @throws an Error if either value is not present in the sequence */ range(a: Id, b: Id): Iterable { - const aIndex = this.indexLookup.get(a); - const bIndex = this.indexLookup.get(b); + const aIndex = this.getIndex(a); + const bIndex = this.getIndex(b); if (aIndex === undefined || bIndex === undefined) { throw new Error('Id value not found within sequence.'); @@ -44,6 +49,79 @@ export default class IdSequence { return this.values.slice(startIndex, endIndex + 1); } + get first(): Id | undefined { + return this.values[0]; + } + + get last(): Id | undefined { + return this.values[this.values.length - 1]; + } + + has(value: Id): boolean { + return this.getIndex(value) !== undefined; + } + + /** + * This method is used for `min` and `max`, but could potentially be used for + * other things too. + * + * @param comparator Corresponds to the [iter-tools comparators][1] + * + * [1]: + * https://github.com/iter-tools/iter-tools/blob/d7.5/API.md#compare-values-and-return-true-or-false + */ + best( + values: Iterable, + comparator: (best: number, v: number) => boolean, + ): Id | undefined { + const validValues = filter((v) => this.has(v), values); + return findBest(comparator, (v) => this.getIndex(v) ?? 0, validValues); + } + + min(values: Iterable): Id | undefined { + return this.best(values, firstLowest); + } + + max(values: Iterable): Id | undefined { + return this.best(values, firstHighest); + } + + /** + * @returns the value positioned relative to the given value by the given + * offset. If the offset is 0, the given value will be returned. If the offset + * is positive, the returned value will be that many positions _after_ the + * given value. If no such value is present, then `undefined` will be + * returned. + */ + offset(value: Id, offset: number): Id | undefined { + if (offset === 0) { + return this.has(value) ? value : undefined; + } + const index = this.getIndex(value); + if (index === undefined) { + return undefined; + } + return this.values[index + offset]; + } + + /** + * This is similar to `offset`, but accepts an iterable of values and treats + * them as a unified block to be collapsed into one value. When `offset` is + * positive, the returned value will be that many positions _after_ the last + * value in the block. If no such value is present, then `undefined` will be + * returned. If offset is zero, then `undefined` will be returned. + */ + collapsedOffset(values: Iterable, offset: number): Id | undefined { + if (offset === 0) { + return undefined; + } + const outerValue = offset > 0 ? this.max(values) : this.min(values); + if (outerValue === undefined) { + return undefined; + } + return this.offset(outerValue, offset); + } + [Symbol.iterator](): Iterator { return this.values[Symbol.iterator](); } diff --git a/mathesar_ui/src/components/sheet/selection/Plane.ts b/mathesar_ui/src/components/sheet/selection/Plane.ts index cec6abe732..7b70dd6cb4 100644 --- a/mathesar_ui/src/components/sheet/selection/Plane.ts +++ b/mathesar_ui/src/components/sheet/selection/Plane.ts @@ -1,8 +1,9 @@ import { map } from 'iter-tools'; import { cartesianProduct } from '@mathesar/utils/iterUtils'; +import { makeCellId, parseCellId } from '../cellIds'; +import { Direction, getColumnOffset, getRowOffset } from './Direction'; import type IdSequence from './IdSequence'; -import { makeCellId } from '../cellIds'; function makeCells( rowIds: Iterable, @@ -14,6 +15,35 @@ function makeCells( ); } +/** + * This describes the different kinds of cells that can be adjacent to a given + * cell in a particular direction. + */ +export type AdjacentCell = + | { + type: 'dataCell'; + cellId: string; + } + | { + type: 'placeholderCell'; + cellId: string; + } + | { + type: 'none'; + }; + +function noAdjacentCell(): AdjacentCell { + return { type: 'none' }; +} + +function adjacentDataCell(cellId: string): AdjacentCell { + return { type: 'dataCell', cellId }; +} + +function adjacentPlaceholderCell(cellId: string): AdjacentCell { + return { type: 'placeholderCell', cellId }; +} + export default class Plane { readonly rowIds: IdSequence; @@ -31,6 +61,19 @@ export default class Plane { this.placeholderRowId = placeholderRowId; } + /** + * @returns the row id that should be used to represent the given row id in + * the plane. If the given row id is the placeholder row id, then the last + * row id in the plane will be returned. Otherwise, the given row id will be + * returned. + */ + private normalizeFlexibleRowId(rowId: string): string | undefined { + if (rowId === this.placeholderRowId) { + return this.rowIds.last; + } + return rowId; + } + /** * @returns an iterable of all the data cells in the plane. This does not * include header cells or placeholder cells. @@ -43,11 +86,24 @@ export default class Plane { * @returns an iterable of all the data cells in the plane that are in or * between the two given rows. This does not include header cells. * - * @throws Error if the id of the placeholder row is specified because the - * placeholder row is not a data row. + * If either of the provided rows are placeholder rows, then the last data row + * will be used in their place. This ensures that the selection is made only + * of data rows, and will never include the placeholder row, even if a user + * drags to select it. */ - dataCellsInRowRange(rowIdA: string, rowIdB: string): Iterable { - return makeCells(this.rowIds.range(rowIdA, rowIdB), this.columnIds); + dataCellsInFlexibleRowRange( + rowIdA: string, + rowIdB: string, + ): Iterable { + const a = this.normalizeFlexibleRowId(rowIdA); + if (a === undefined) { + return []; + } + const b = this.normalizeFlexibleRowId(rowIdB); + if (b === undefined) { + return []; + } + return makeCells(this.rowIds.range(a, b), this.columnIds); } /** @@ -62,6 +118,81 @@ export default class Plane { return makeCells(this.rowIds, this.columnIds.range(columnIdA, columnIdB)); } + /** + * @returns an iterable of all the data cells in the plane that are in or + * between the two given cell. This does not include header cells. + * + * If either of the provided cells are placeholder cells, then cells in the + * last row and last column will be used in their place. This ensures that the + * selection is made only of data cells, and will never include the + * placeholder cell, even if a user drags to select it. + */ + dataCellsInFlexibleCellRange( + cellIdA: string, + cellIdB: string, + ): Iterable { + const cellA = parseCellId(cellIdA); + const cellB = parseCellId(cellIdB); + const rowIdA = this.normalizeFlexibleRowId(cellA.rowId); + if (rowIdA === undefined) { + return []; + } + const rowIdB = this.normalizeFlexibleRowId(cellB.rowId); + if (rowIdB === undefined) { + return []; + } + const rowIds = this.rowIds.range(rowIdA, rowIdB); + const columnIds = this.columnIds.range(cellA.columnId, cellB.columnId); + return makeCells(rowIds, columnIds); + } + + getAdjacentCell(cellId: string, direction: Direction): AdjacentCell { + const cell = parseCellId(cellId); + + const columnOffset = getColumnOffset(direction); + const newColumnId = this.columnIds.offset(cell.columnId, columnOffset); + if (newColumnId === undefined) { + return noAdjacentCell(); + } + + if (cell.rowId === this.placeholderRowId) { + if (direction === Direction.Up) { + const lastRowId = this.rowIds.last; + if (lastRowId === undefined) { + // Can't go up from the placeholder row if there are no data rows + return noAdjacentCell(); + } + // Move up from the placeholder row into the last data row + return adjacentDataCell(makeCellId(lastRowId, newColumnId)); + } + if (direction === Direction.Down) { + // Can't go down from the placeholder row + return noAdjacentCell(); + } + // Move laterally within the placeholder row + return adjacentPlaceholderCell(makeCellId(cell.rowId, newColumnId)); + } + + const rowOffset = getRowOffset(direction); + const newRowId = this.rowIds.offset(cell.rowId, rowOffset); + if (newRowId === undefined) { + if (direction === Direction.Down) { + if (this.placeholderRowId === undefined) { + // Can't go down from the last data row if there is no placeholder row + return noAdjacentCell(); + } + const newCellId = makeCellId(this.placeholderRowId, newColumnId); + // Move down from the last data row into the placeholder row + return adjacentPlaceholderCell(newCellId); + } + // Can't move up from the first data row + return noAdjacentCell(); + } + + // Normal movement from one data cell to another data cell + return adjacentDataCell(makeCellId(newRowId, newColumnId)); + } + get hasResultRows(): boolean { return this.rowIds.length > 0; } diff --git a/mathesar_ui/src/components/sheet/selection/Selection.ts b/mathesar_ui/src/components/sheet/selection/Selection.ts index 44f70d27c4..c98e5570dc 100644 --- a/mathesar_ui/src/components/sheet/selection/Selection.ts +++ b/mathesar_ui/src/components/sheet/selection/Selection.ts @@ -1,31 +1,28 @@ import { first } from 'iter-tools'; import { ImmutableSet } from '@mathesar/component-library'; -import type Plane from './Plane'; +import { assertExhaustive } from '@mathesar/utils/typeUtils'; import { parseCellId } from '../cellIds'; +import { Direction, getColumnOffset } from './Direction'; +import type Plane from './Plane'; -export enum Direction { - Up = 'up', - Down = 'down', - Left = 'left', - Right = 'right', -} +/** + * - `'dataCells'` means that the selection contains data cells. This is by + * far the most common type of selection basis. + * + * - `'emptyColumns'` is used when the sheet has no rows. In this case we + * still want to allow the user to select columns, so we use this basis. + * + * - `'placeholderCell'` is used when the user is selecting a cell in the + * placeholder row. This is a special case because we don't want to allow + * the user to select multiple cells in the placeholder row, and we also + * don't want to allow selections that include cells in data rows _and_ the + * placeholder row. + */ +type BasisType = 'dataCells' | 'emptyColumns' | 'placeholderCell'; interface Basis { - /** - * - `'dataCells'` means that the selection contains data cells. This is by - * far the most common type of selection basis. - * - * - `'emptyColumns'` is used when the sheet has no rows. In this case we - * still want to allow the user to select columns, so we use this basis. - * - * - `'placeholderCell'` is used when the user is selecting a cell in the - * placeholder row. This is a special case because we don't want to allow - * the user to select multiple cells in the placeholder row, and we also - * don't want to allow selections that include cells in data rows _and_ the - * placeholder row. - */ - readonly type: 'dataCells' | 'emptyColumns' | 'placeholderCell'; + readonly type: BasisType; readonly activeCellId: string | undefined; readonly cellIds: ImmutableSet; readonly rowIds: ImmutableSet; @@ -101,8 +98,8 @@ export default class Selection { } /** - * @returns a new selection with all cells selected. The active cell will be the - * cell in the first row and first column. + * @returns a new selection with all cells selected. The active cell will be + * the cell in the first row and first column. */ ofAllDataCells(): Selection { if (!this.plane.hasResultRows) { @@ -133,7 +130,11 @@ export default class Selection { * drags to select it. */ ofRowRange(rowIdA: string, rowIdB: string): Selection { - throw new Error('Not implemented'); + return this.withBasis( + basisFromDataCells( + this.plane.dataCellsInFlexibleRowRange(rowIdA, rowIdB), + ), + ); } /** @@ -141,10 +142,12 @@ export default class Selection { * provided columnIds, inclusive. */ ofColumnRange(columnIdA: string, columnIdB: string): Selection { - if (!this.plane.hasResultRows) { - throw new Error('Not implemented'); - } - throw new Error('Not implemented'); + const newBasis = this.plane.hasResultRows + ? basisFromDataCells( + this.plane.dataCellsInColumnRange(columnIdA, columnIdB), + ) + : basisFromEmptyColumns(this.plane.columnIds.range(columnIdA, columnIdB)); + return this.withBasis(newBasis); } /** @@ -157,7 +160,11 @@ export default class Selection { * placeholder row, even if a user drags to select a cell in it. */ ofCellRange(cellIdA: string, cellIdB: string): Selection { - throw new Error('Not implemented'); + return this.withBasis( + basisFromDataCells( + this.plane.dataCellsInFlexibleCellRange(cellIdA, cellIdB), + ), + ); } /** @@ -166,7 +173,14 @@ export default class Selection { * row. */ atPlaceholderCell(cellId: string): Selection { - throw new Error('Not implemented'); + return this.withBasis(basisFromPlaceholderCell(cellId)); + } + + /** + * @returns a new selection formed from one cell within the data rows. + */ + atDataCell(cellId: string): Selection { + return this.withBasis(basisFromDataCells([cellId], cellId)); } /** @@ -188,7 +202,7 @@ export default class Selection { * provided cell. */ drawnToCell(cellId: string): Selection { - throw new Error('Not implemented'); + return this.ofCellRange(this.activeCellId ?? cellId, cellId); } /** @@ -196,11 +210,19 @@ export default class Selection { * active cell and the provided row, inclusive. */ drawnToRow(rowId: string): Selection { - throw new Error('Not implemented'); + const activeRowId = this.activeCellId + ? parseCellId(this.activeCellId).rowId + : rowId; + return this.ofRowRange(activeRowId, rowId); } drawnToColumn(columnId: string): Selection { - throw new Error('Not implemented'); + // TODO improve handling for empty columns + + const activeColumnId = this.activeCellId + ? parseCellId(this.activeCellId).columnId + : columnId; + return this.ofColumnRange(activeColumnId, columnId); } /** @@ -209,7 +231,39 @@ export default class Selection { * then a new selection is created with only that one cell selected. */ collapsedAndMoved(direction: Direction): Selection { - throw new Error('Not implemented'); + if (this.basis.type === 'emptyColumns') { + const offset = getColumnOffset(direction); + const newActiveColumnId = this.plane.columnIds.collapsedOffset( + this.basis.columnIds, + offset, + ); + if (newActiveColumnId === undefined) { + // If we couldn't shift in the direction, then do nothing + return this; + } + return this.withBasis(basisFromEmptyColumns([newActiveColumnId])); + } + + if (this.activeCellId === undefined) { + // If no cells are selected, then select the first data cell + return this.ofFirstDataCell(); + } + + const adjacent = this.plane.getAdjacentCell(this.activeCellId, direction); + + if (adjacent.type === 'none') { + // If we can't move anywhere, then do nothing + return this; + } + if (adjacent.type === 'dataCell') { + // Move to an adjacent data cell + return this.atDataCell(adjacent.cellId); + } + if (adjacent.type === 'placeholderCell') { + // Move to an adjacent placeholder cell + return this.atPlaceholderCell(adjacent.cellId); + } + return assertExhaustive(adjacent); } /** diff --git a/mathesar_ui/src/components/sheet/selection/__tests__/IdSequence.test.ts b/mathesar_ui/src/components/sheet/selection/__tests__/IdSequence.test.ts new file mode 100644 index 0000000000..14da6d0a24 --- /dev/null +++ b/mathesar_ui/src/components/sheet/selection/__tests__/IdSequence.test.ts @@ -0,0 +1,44 @@ +import IdSequence from '../IdSequence'; + +test('IdSequence', () => { + const s = new IdSequence(['h', 'i', 'j', 'k', 'l']); + expect(s.length).toBe(5); + expect(s.first).toBe('h'); + expect(s.last).toBe('l'); + + expect([...s]).toEqual(['h', 'i', 'j', 'k', 'l']); + + expect(s.min(['i', 'j', 'k'])).toBe('i'); + expect(s.min(['k', 'j', 'i'])).toBe('i'); + expect(s.min(['i', 'NOPE'])).toBe('i'); + expect(s.min(['NOPE'])).toBe(undefined); + expect(s.min([])).toBe(undefined); + + expect(s.max(['i', 'j', 'k'])).toBe('k'); + expect(s.max(['k', 'j', 'i'])).toBe('k'); + expect(s.max(['i', 'NOPE'])).toBe('i'); + expect(s.max(['NOPE'])).toBe(undefined); + expect(s.max([])).toBe(undefined); + + expect([...s.range('i', 'k')]).toEqual(['i', 'j', 'k']); + expect([...s.range('k', 'i')]).toEqual(['i', 'j', 'k']); + expect([...s.range('i', 'i')]).toEqual(['i']); + expect(() => s.range('i', 'NOPE')).toThrow(); + + expect(s.offset('i', 0)).toBe('i'); + expect(s.offset('i', 1)).toBe('j'); + expect(s.offset('i', 2)).toBe('k'); + expect(s.offset('i', -1)).toBe('h'); + expect(s.offset('i', -2)).toBe(undefined); + expect(s.offset('NOPE', 0)).toBe(undefined); + expect(s.offset('NOPE', 1)).toBe(undefined); + + expect(s.collapsedOffset(['i', 'k'], 0)).toBe(undefined); + expect(s.collapsedOffset(['i', 'k'], 1)).toBe('l'); + expect(s.collapsedOffset(['i', 'k'], 2)).toBe(undefined); + expect(s.collapsedOffset(['i', 'k'], -1)).toBe('h'); + expect(s.collapsedOffset(['i', 'k'], -2)).toBe(undefined); + expect(s.collapsedOffset(['i', 'NOPE'], 0)).toBe(undefined); + expect(s.collapsedOffset(['i', 'NOPE'], 1)).toBe('j'); + expect(s.collapsedOffset([], 0)).toBe(undefined); +}); diff --git a/mathesar_ui/src/components/sheet/selection/__tests__/Plane.test.ts b/mathesar_ui/src/components/sheet/selection/__tests__/Plane.test.ts new file mode 100644 index 0000000000..6e70548ed2 --- /dev/null +++ b/mathesar_ui/src/components/sheet/selection/__tests__/Plane.test.ts @@ -0,0 +1,58 @@ +import IdSequence from '../IdSequence'; +import Plane from '../Plane'; +import { Direction } from '../Direction'; + +test('Plane with placeholder row', () => { + const p = new Plane( + new IdSequence(['r1', 'r2', 'r3', 'r4']), + new IdSequence(['c1', 'c2', 'c3', 'c4']), + 'PL', + ); + + expect([...p.allDataCells()]).toEqual([ + 'r1-c1', + 'r1-c2', + 'r1-c3', + 'r1-c4', + 'r2-c1', + 'r2-c2', + 'r2-c3', + 'r2-c4', + 'r3-c1', + 'r3-c2', + 'r3-c3', + 'r3-c4', + 'r4-c1', + 'r4-c2', + 'r4-c3', + 'r4-c4', + ]); + + expect([...p.dataCellsInFlexibleRowRange('r1', 'r2')].length).toBe(8); + expect([...p.dataCellsInFlexibleRowRange('r1', 'r3')].length).toBe(12); + expect([...p.dataCellsInFlexibleRowRange('r3', 'r4')].length).toBe(8); + expect([...p.dataCellsInFlexibleRowRange('r3', 'PL')].length).toBe(8); + expect([...p.dataCellsInFlexibleRowRange('PL', 'PL')]).toEqual([ + 'r4-c1', + 'r4-c2', + 'r4-c3', + 'r4-c4', + ]); + + expect([...p.dataCellsInColumnRange('c1', 'c2')].length).toBe(8); + expect([...p.dataCellsInColumnRange('c1', 'c3')].length).toBe(12); + + expect(p.getAdjacentCell('r4-c4', Direction.Up)).toEqual({ + type: 'dataCell', + cellId: 'r3-c4', + }); + expect(p.getAdjacentCell('r4-c4', Direction.Right)).toEqual({ type: 'none' }); + expect(p.getAdjacentCell('r4-c4', Direction.Down)).toEqual({ + type: 'placeholderCell', + cellId: 'PL-c4', + }); + expect(p.getAdjacentCell('r4-c4', Direction.Left)).toEqual({ + type: 'dataCell', + cellId: 'r4-c3', + }); +}); From 8b5ccdcb7a54b59b144c8b99602d987b42200886 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Tue, 18 Jul 2023 15:17:46 -0400 Subject: [PATCH 04/85] Improve some naming --- mathesar_ui/src/components/sheet/selection/Plane.ts | 10 +++++----- .../sheet/selection/{IdSequence.ts => Series.ts} | 4 ++-- .../components/sheet/selection/__tests__/Plane.test.ts | 6 +++--- .../__tests__/{IdSequence.test.ts => Series.test.ts} | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) rename mathesar_ui/src/components/sheet/selection/{IdSequence.ts => Series.ts} (98%) rename mathesar_ui/src/components/sheet/selection/__tests__/{IdSequence.test.ts => Series.test.ts} (92%) diff --git a/mathesar_ui/src/components/sheet/selection/Plane.ts b/mathesar_ui/src/components/sheet/selection/Plane.ts index 7b70dd6cb4..20ed0fccc5 100644 --- a/mathesar_ui/src/components/sheet/selection/Plane.ts +++ b/mathesar_ui/src/components/sheet/selection/Plane.ts @@ -3,7 +3,7 @@ import { map } from 'iter-tools'; import { cartesianProduct } from '@mathesar/utils/iterUtils'; import { makeCellId, parseCellId } from '../cellIds'; import { Direction, getColumnOffset, getRowOffset } from './Direction'; -import type IdSequence from './IdSequence'; +import type Series from './Series'; function makeCells( rowIds: Iterable, @@ -45,15 +45,15 @@ function adjacentPlaceholderCell(cellId: string): AdjacentCell { } export default class Plane { - readonly rowIds: IdSequence; + readonly rowIds: Series; - readonly columnIds: IdSequence; + readonly columnIds: Series; readonly placeholderRowId: string | undefined; constructor( - rowIds: IdSequence, - columnIds: IdSequence, + rowIds: Series, + columnIds: Series, placeholderRowId: string | undefined, ) { this.rowIds = rowIds; diff --git a/mathesar_ui/src/components/sheet/selection/IdSequence.ts b/mathesar_ui/src/components/sheet/selection/Series.ts similarity index 98% rename from mathesar_ui/src/components/sheet/selection/IdSequence.ts rename to mathesar_ui/src/components/sheet/selection/Series.ts index ea0396f621..7b8cd45dcc 100644 --- a/mathesar_ui/src/components/sheet/selection/IdSequence.ts +++ b/mathesar_ui/src/components/sheet/selection/Series.ts @@ -1,7 +1,7 @@ import { ImmutableMap } from '@mathesar/component-library'; import { filter, findBest, firstHighest, firstLowest } from 'iter-tools'; -export default class IdSequence { +export default class Series { private readonly values: Id[]; /** Maps the id value to its index */ @@ -16,7 +16,7 @@ export default class IdSequence { values.map((value, index) => [value, index]), ); if (new Set(values).size !== values.length) { - throw new Error('Duplicate values are not allowed within an IdSequence.'); + throw new Error('Duplicate values are not allowed within a Series.'); } } diff --git a/mathesar_ui/src/components/sheet/selection/__tests__/Plane.test.ts b/mathesar_ui/src/components/sheet/selection/__tests__/Plane.test.ts index 6e70548ed2..dd6b89500c 100644 --- a/mathesar_ui/src/components/sheet/selection/__tests__/Plane.test.ts +++ b/mathesar_ui/src/components/sheet/selection/__tests__/Plane.test.ts @@ -1,11 +1,11 @@ -import IdSequence from '../IdSequence'; +import Series from '../Series'; import Plane from '../Plane'; import { Direction } from '../Direction'; test('Plane with placeholder row', () => { const p = new Plane( - new IdSequence(['r1', 'r2', 'r3', 'r4']), - new IdSequence(['c1', 'c2', 'c3', 'c4']), + new Series(['r1', 'r2', 'r3', 'r4']), + new Series(['c1', 'c2', 'c3', 'c4']), 'PL', ); diff --git a/mathesar_ui/src/components/sheet/selection/__tests__/IdSequence.test.ts b/mathesar_ui/src/components/sheet/selection/__tests__/Series.test.ts similarity index 92% rename from mathesar_ui/src/components/sheet/selection/__tests__/IdSequence.test.ts rename to mathesar_ui/src/components/sheet/selection/__tests__/Series.test.ts index 14da6d0a24..8109932d9f 100644 --- a/mathesar_ui/src/components/sheet/selection/__tests__/IdSequence.test.ts +++ b/mathesar_ui/src/components/sheet/selection/__tests__/Series.test.ts @@ -1,7 +1,7 @@ -import IdSequence from '../IdSequence'; +import Series from '../Series'; -test('IdSequence', () => { - const s = new IdSequence(['h', 'i', 'j', 'k', 'l']); +test('Series', () => { + const s = new Series(['h', 'i', 'j', 'k', 'l']); expect(s.length).toBe(5); expect(s.first).toBe('h'); expect(s.last).toBe('l'); From 66f4e637cc1759af7cf08475ea3389513acfab0e Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Tue, 18 Jul 2023 16:34:44 -0400 Subject: [PATCH 05/85] Rename classes to mark old code as deprecated --- ...etSelection.ts => LegacySheetSelection.ts} | 5 ++- .../components/sheet/SheetClipboardHandler.ts | 6 ++-- mathesar_ui/src/components/sheet/index.ts | 4 +-- .../{Selection.ts => SheetSelection.ts} | 36 ++++++++++--------- .../src/stores/table-data/tabularData.ts | 13 ++++--- .../src/systems/data-explorer/QueryRunner.ts | 6 ++-- .../src/systems/table-view/Body.svelte | 2 +- .../src/systems/table-view/StatusPane.svelte | 2 +- .../src/systems/table-view/TableView.svelte | 9 +++-- .../systems/table-view/header/Header.svelte | 2 +- .../src/systems/table-view/row/Row.svelte | 9 +++-- .../table-inspector/TableInspector.svelte | 2 +- .../table-inspector/column/ColumnMode.svelte | 2 +- .../ExtractColumnsModal.svelte | 6 +++- .../table-inspector/record/RecordMode.svelte | 2 +- 15 files changed, 64 insertions(+), 42 deletions(-) rename mathesar_ui/src/components/sheet/{SheetSelection.ts => LegacySheetSelection.ts} (99%) rename mathesar_ui/src/components/sheet/selection/{Selection.ts => SheetSelection.ts} (90%) diff --git a/mathesar_ui/src/components/sheet/SheetSelection.ts b/mathesar_ui/src/components/sheet/LegacySheetSelection.ts similarity index 99% rename from mathesar_ui/src/components/sheet/SheetSelection.ts rename to mathesar_ui/src/components/sheet/LegacySheetSelection.ts index 4c88ef8c50..0aeeeb31c6 100644 --- a/mathesar_ui/src/components/sheet/SheetSelection.ts +++ b/mathesar_ui/src/components/sheet/LegacySheetSelection.ts @@ -196,7 +196,10 @@ export function getSelectedRowIndex(selectedCell: string): number { return Number(selectedCell.split(ROW_COLUMN_SEPARATOR)[0]); } -export default class SheetSelection< +/** + * @deprecated + */ +export default class LegacySheetSelection< Row extends SelectionRow, Column extends SelectionColumn, > { diff --git a/mathesar_ui/src/components/sheet/SheetClipboardHandler.ts b/mathesar_ui/src/components/sheet/SheetClipboardHandler.ts index c8e7a8b3d7..2af5549205 100644 --- a/mathesar_ui/src/components/sheet/SheetClipboardHandler.ts +++ b/mathesar_ui/src/components/sheet/SheetClipboardHandler.ts @@ -2,9 +2,9 @@ import * as Papa from 'papaparse'; import { get } from 'svelte/store'; import { ImmutableSet, type MakeToast } from '@mathesar-component-library'; -import SheetSelection, { +import LegacySheetSelection, { isCellSelected, -} from '@mathesar/components/sheet/SheetSelection'; +} from '@mathesar/components/sheet/LegacySheetSelection'; import type { ClipboardHandler } from '@mathesar/stores/clipboard'; import type { ProcessedColumn, @@ -76,7 +76,7 @@ interface SheetClipboardHandlerDeps< Row extends QueryRow | RecordRow, Column extends ProcessedQueryOutputColumn | ProcessedColumn, > { - selection: SheetSelection; + selection: LegacySheetSelection; toast: MakeToast; getRows(): Row[]; getColumnsMap(): ReadableMapLike; diff --git a/mathesar_ui/src/components/sheet/index.ts b/mathesar_ui/src/components/sheet/index.ts index cc67f1275b..0448df86aa 100644 --- a/mathesar_ui/src/components/sheet/index.ts +++ b/mathesar_ui/src/components/sheet/index.ts @@ -5,7 +5,7 @@ export { default as SheetPositionableCell } from './SheetPositionableCell.svelte export { default as SheetCellResizer } from './SheetCellResizer.svelte'; export { default as SheetVirtualRows } from './SheetVirtualRows.svelte'; export { default as SheetRow } from './SheetRow.svelte'; -export { default as SheetSelection } from './SheetSelection'; +export { default as LegacySheetSelection } from './LegacySheetSelection'; export { isColumnSelected, isRowSelected, @@ -14,4 +14,4 @@ export { isCellActive, scrollBasedOnActiveCell, scrollBasedOnSelection, -} from './SheetSelection'; +} from './LegacySheetSelection'; diff --git a/mathesar_ui/src/components/sheet/selection/Selection.ts b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts similarity index 90% rename from mathesar_ui/src/components/sheet/selection/Selection.ts rename to mathesar_ui/src/components/sheet/selection/SheetSelection.ts index c98e5570dc..ba8c8b1b8b 100644 --- a/mathesar_ui/src/components/sheet/selection/Selection.ts +++ b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts @@ -67,7 +67,7 @@ function basisFromPlaceholderCell(activeCellId: string): Basis { }; } -export default class Selection { +export default class SheetSelection { private readonly plane: Plane; private readonly basis: Basis; @@ -93,15 +93,15 @@ export default class Selection { return this.basis.columnIds; } - private withBasis(basis: Basis): Selection { - return new Selection(this.plane, basis); + private withBasis(basis: Basis): SheetSelection { + return new SheetSelection(this.plane, basis); } /** * @returns a new selection with all cells selected. The active cell will be * the cell in the first row and first column. */ - ofAllDataCells(): Selection { + ofAllDataCells(): SheetSelection { if (!this.plane.hasResultRows) { return this.withBasis(basisFromZeroEmptyColumns()); } @@ -112,7 +112,7 @@ export default class Selection { * @returns a new selection with the cell in the first row and first column * selected. */ - ofFirstDataCell(): Selection { + ofFirstDataCell(): SheetSelection { const firstCellId = first(this.plane.allDataCells()); if (firstCellId === undefined) { return this.withBasis(basisFromZeroEmptyColumns()); @@ -129,7 +129,7 @@ export default class Selection { * of data rows, and will never include the placeholder row, even if a user * drags to select it. */ - ofRowRange(rowIdA: string, rowIdB: string): Selection { + ofRowRange(rowIdA: string, rowIdB: string): SheetSelection { return this.withBasis( basisFromDataCells( this.plane.dataCellsInFlexibleRowRange(rowIdA, rowIdB), @@ -141,7 +141,7 @@ export default class Selection { * @returns a new selection of all data cells in all columns between the * provided columnIds, inclusive. */ - ofColumnRange(columnIdA: string, columnIdB: string): Selection { + ofColumnRange(columnIdA: string, columnIdB: string): SheetSelection { const newBasis = this.plane.hasResultRows ? basisFromDataCells( this.plane.dataCellsInColumnRange(columnIdA, columnIdB), @@ -159,7 +159,7 @@ export default class Selection { * selection is made only of data cells, and will never include cells in the * placeholder row, even if a user drags to select a cell in it. */ - ofCellRange(cellIdA: string, cellIdB: string): Selection { + ofCellRange(cellIdA: string, cellIdB: string): SheetSelection { return this.withBasis( basisFromDataCells( this.plane.dataCellsInFlexibleCellRange(cellIdA, cellIdB), @@ -172,14 +172,14 @@ export default class Selection { * Note that we do not support selections of multiple cells in the placeholder * row. */ - atPlaceholderCell(cellId: string): Selection { + atPlaceholderCell(cellId: string): SheetSelection { return this.withBasis(basisFromPlaceholderCell(cellId)); } /** * @returns a new selection formed from one cell within the data rows. */ - atDataCell(cellId: string): Selection { + atDataCell(cellId: string): SheetSelection { return this.withBasis(basisFromDataCells([cellId], cellId)); } @@ -187,7 +187,7 @@ export default class Selection { * @returns a new selection that fits within the provided plane. This is * useful when a column is deleted, reordered, or inserted. */ - forNewPlane(plane: Plane): Selection { + forNewPlane(plane: Plane): SheetSelection { throw new Error('Not implemented'); } @@ -201,7 +201,7 @@ export default class Selection { * by the active cell (also the first cell selected when dragging) and the * provided cell. */ - drawnToCell(cellId: string): Selection { + drawnToCell(cellId: string): SheetSelection { return this.ofCellRange(this.activeCellId ?? cellId, cellId); } @@ -209,14 +209,14 @@ export default class Selection { * @returns a new selection formed by the cells in all the rows between the * active cell and the provided row, inclusive. */ - drawnToRow(rowId: string): Selection { + drawnToRow(rowId: string): SheetSelection { const activeRowId = this.activeCellId ? parseCellId(this.activeCellId).rowId : rowId; return this.ofRowRange(activeRowId, rowId); } - drawnToColumn(columnId: string): Selection { + drawnToColumn(columnId: string): SheetSelection { // TODO improve handling for empty columns const activeColumnId = this.activeCellId @@ -230,7 +230,7 @@ export default class Selection { * spreadsheets. If the active cell can be moved in the provided direction, * then a new selection is created with only that one cell selected. */ - collapsedAndMoved(direction: Direction): Selection { + collapsedAndMoved(direction: Direction): SheetSelection { if (this.basis.type === 'emptyColumns') { const offset = getColumnOffset(direction); const newActiveColumnId = this.plane.columnIds.collapsedOffset( @@ -272,7 +272,9 @@ export default class Selection { * * This is to handle the `Tab` and `Shift+Tab` keys. */ - withActiveCellAdvanced(direction: 'forward' | 'back' = 'forward'): Selection { + withActiveCellAdvanced( + direction: 'forward' | 'back' = 'forward', + ): SheetSelection { throw new Error('Not implemented'); } @@ -291,7 +293,7 @@ export default class Selection { * the active cell. We chose to mimic Google Sheets behavior here because it * is simpler. */ - resized(direction: Direction): Selection { + resized(direction: Direction): SheetSelection { throw new Error('Not implemented'); } } diff --git a/mathesar_ui/src/stores/table-data/tabularData.ts b/mathesar_ui/src/stores/table-data/tabularData.ts index a29e0c9836..543e977ea3 100644 --- a/mathesar_ui/src/stores/table-data/tabularData.ts +++ b/mathesar_ui/src/stores/table-data/tabularData.ts @@ -11,7 +11,7 @@ import type { TableEntry } from '@mathesar/api/types/tables'; import type { AbstractTypesMap } from '@mathesar/stores/abstract-types/types'; import { States } from '@mathesar/api/utils/requestUtils'; import type { Column } from '@mathesar/api/types/tables/columns'; -import { SheetSelection } from '@mathesar/components/sheet'; +import { LegacySheetSelection } from '@mathesar/components/sheet'; import { getColumnOrder } from '@mathesar/utils/tables'; import { Meta } from './meta'; import { ColumnsDataStore } from './columns'; @@ -44,7 +44,10 @@ export interface TabularDataProps { >[0]['hasEnhancedPrimaryKeyCell']; } -export type TabularDataSelection = SheetSelection; +export type TabularDataSelection = LegacySheetSelection< + RecordRow, + ProcessedColumn +>; export class TabularData { id: DBObjectEntry['id']; @@ -63,7 +66,7 @@ export class TabularData { isLoading: Readable; - selection: TabularDataSelection; + legacySelection: TabularDataSelection; table: TableEntry; @@ -109,7 +112,7 @@ export class TabularData { this.table = props.table; - this.selection = new SheetSelection({ + this.legacySelection = new LegacySheetSelection({ getColumns: () => [...get(this.processedColumns).values()], getColumnOrder: () => getColumnOrder([...get(this.processedColumns).values()], this.table), @@ -218,7 +221,7 @@ export class TabularData { this.recordsData.destroy(); this.constraintsDataStore.destroy(); this.columnsDataStore.destroy(); - this.selection.destroy(); + this.legacySelection.destroy(); } } diff --git a/mathesar_ui/src/systems/data-explorer/QueryRunner.ts b/mathesar_ui/src/systems/data-explorer/QueryRunner.ts index bf3985c226..2b7ee402e2 100644 --- a/mathesar_ui/src/systems/data-explorer/QueryRunner.ts +++ b/mathesar_ui/src/systems/data-explorer/QueryRunner.ts @@ -14,7 +14,7 @@ import type { QueryColumnMetaData, } from '@mathesar/api/types/queries'; import { runQuery } from '@mathesar/stores/queries'; -import { SheetSelection } from '@mathesar/components/sheet'; +import { LegacySheetSelection } from '@mathesar/components/sheet'; import type { AbstractTypesMap } from '@mathesar/stores/abstract-types/types'; import type QueryModel from './QueryModel'; import QueryInspector from './QueryInspector'; @@ -42,7 +42,7 @@ export interface QueryRowsData { rows: QueryRow[]; } -export type QuerySheetSelection = SheetSelection< +export type QuerySheetSelection = LegacySheetSelection< QueryRow, ProcessedQueryOutputColumn >; @@ -81,7 +81,7 @@ export default class QueryRunner< this.query = writable(query); this.speculateProcessedColumns(); void this.run(); - this.selection = new SheetSelection({ + this.selection = new LegacySheetSelection({ getColumns: () => [...get(this.processedColumns).values()], getColumnOrder: () => [...get(this.processedColumns).values()].map((column) => column.id), diff --git a/mathesar_ui/src/systems/table-view/Body.svelte b/mathesar_ui/src/systems/table-view/Body.svelte index bed4c366a6..f917c5dc71 100644 --- a/mathesar_ui/src/systems/table-view/Body.svelte +++ b/mathesar_ui/src/systems/table-view/Body.svelte @@ -31,7 +31,7 @@ export let usesVirtualList = false; - $: ({ id, display, columnsDataStore, selection } = $tabularData); + $: ({ id, display, columnsDataStore } = $tabularData); $: ({ displayableRecords } = display); $: ({ pkColumn } = columnsDataStore); diff --git a/mathesar_ui/src/systems/table-view/StatusPane.svelte b/mathesar_ui/src/systems/table-view/StatusPane.svelte index 36a0672e1c..459e02c56c 100644 --- a/mathesar_ui/src/systems/table-view/StatusPane.svelte +++ b/mathesar_ui/src/systems/table-view/StatusPane.svelte @@ -32,7 +32,7 @@ isLoading, columnsDataStore, constraintsDataStore, - selection, + legacySelection: selection, } = $tabularData); $: ({ pagination } = meta); $: ({ size: pageSize, leftBound, rightBound } = $pagination); diff --git a/mathesar_ui/src/systems/table-view/TableView.svelte b/mathesar_ui/src/systems/table-view/TableView.svelte index 3cb518ae72..8e7e129cff 100644 --- a/mathesar_ui/src/systems/table-view/TableView.svelte +++ b/mathesar_ui/src/systems/table-view/TableView.svelte @@ -40,8 +40,13 @@ $: usesVirtualList = context === 'page'; $: allowsDdlOperations = context === 'page' && canExecuteDDL; $: sheetHasBorder = context === 'widget'; - $: ({ processedColumns, display, isLoading, selection, recordsData } = - $tabularData); + $: ({ + processedColumns, + display, + isLoading, + legacySelection: selection, + recordsData, + } = $tabularData); $: clipboardHandler = new SheetClipboardHandler({ selection, toast, diff --git a/mathesar_ui/src/systems/table-view/header/Header.svelte b/mathesar_ui/src/systems/table-view/header/Header.svelte index e70a5574fd..3b71564078 100644 --- a/mathesar_ui/src/systems/table-view/header/Header.svelte +++ b/mathesar_ui/src/systems/table-view/header/Header.svelte @@ -28,7 +28,7 @@ $: columnOrder = columnOrder ?? []; $: columnOrderString = columnOrder.map(String); - $: ({ selection, processedColumns } = $tabularData); + $: ({ legacySelection: selection, processedColumns } = $tabularData); $: ({ selectedCells, columnsSelectedWhenTheTableIsEmpty, diff --git a/mathesar_ui/src/systems/table-view/row/Row.svelte b/mathesar_ui/src/systems/table-view/row/Row.svelte index eb717f1ba6..967c17b677 100644 --- a/mathesar_ui/src/systems/table-view/row/Row.svelte +++ b/mathesar_ui/src/systems/table-view/row/Row.svelte @@ -29,8 +29,13 @@ const tabularData = getTabularDataStoreFromContext(); - $: ({ recordsData, columnsDataStore, meta, processedColumns, selection } = - $tabularData); + $: ({ + recordsData, + columnsDataStore, + meta, + processedColumns, + legacySelection: selection, + } = $tabularData); $: ({ rowStatus, rowCreationStatus, diff --git a/mathesar_ui/src/systems/table-view/table-inspector/TableInspector.svelte b/mathesar_ui/src/systems/table-view/table-inspector/TableInspector.svelte index 967d289a26..d628a0d124 100644 --- a/mathesar_ui/src/systems/table-view/table-inspector/TableInspector.svelte +++ b/mathesar_ui/src/systems/table-view/table-inspector/TableInspector.svelte @@ -29,7 +29,7 @@ let activeTab: TabItem; const tabularData = getTabularDataStoreFromContext(); - $: ({ selection } = $tabularData); + $: ({ legacySelection: selection } = $tabularData); $: ({ selectedCells } = selection); $: { diff --git a/mathesar_ui/src/systems/table-view/table-inspector/column/ColumnMode.svelte b/mathesar_ui/src/systems/table-view/table-inspector/column/ColumnMode.svelte index cf92780c1b..fe1e01f610 100644 --- a/mathesar_ui/src/systems/table-view/table-inspector/column/ColumnMode.svelte +++ b/mathesar_ui/src/systems/table-view/table-inspector/column/ColumnMode.svelte @@ -21,7 +21,7 @@ $: database = $currentDatabase; $: schema = $currentSchema; - $: ({ processedColumns, selection } = $tabularData); + $: ({ processedColumns, legacySelection: selection } = $tabularData); $: ({ selectedCells, columnsSelectedWhenTheTableIsEmpty } = selection); $: selectedColumns = (() => { const ids = selection.getSelectedUniqueColumnsId( diff --git a/mathesar_ui/src/systems/table-view/table-inspector/column/column-extraction/ExtractColumnsModal.svelte b/mathesar_ui/src/systems/table-view/table-inspector/column/column-extraction/ExtractColumnsModal.svelte index cb1d622145..ed5d468f15 100644 --- a/mathesar_ui/src/systems/table-view/table-inspector/column/column-extraction/ExtractColumnsModal.svelte +++ b/mathesar_ui/src/systems/table-view/table-inspector/column/column-extraction/ExtractColumnsModal.svelte @@ -48,7 +48,11 @@ export let controller: ExtractColumnsModalController; - $: ({ processedColumns, constraintsDataStore, selection } = $tabularData); + $: ({ + processedColumns, + constraintsDataStore, + legacySelection: selection, + } = $tabularData); $: ({ constraints } = $constraintsDataStore); $: availableProcessedColumns = [...$processedColumns.values()]; $: ({ targetType, columns, isOpen } = controller); diff --git a/mathesar_ui/src/systems/table-view/table-inspector/record/RecordMode.svelte b/mathesar_ui/src/systems/table-view/table-inspector/record/RecordMode.svelte index f85100b121..878701fec1 100644 --- a/mathesar_ui/src/systems/table-view/table-inspector/record/RecordMode.svelte +++ b/mathesar_ui/src/systems/table-view/table-inspector/record/RecordMode.svelte @@ -14,7 +14,7 @@ $: database = $currentDatabase; $: schema = $currentSchema; - $: ({ selection, recordsData } = $tabularData); + $: ({ legacySelection: selection, recordsData } = $tabularData); $: ({ selectedCells } = selection); $: selectedRowIndices = $selectedCells .valuesArray() From 8157a8bc27cac20951ecca92edd795444a06911c Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Wed, 19 Jul 2023 13:53:22 -0400 Subject: [PATCH 06/85] Begin integrating SheetSelection into TabularData --- .../src/components/sheet/selection/Plane.ts | 8 +-- .../src/components/sheet/selection/Series.ts | 5 +- .../sheet/selection/SheetSelection.ts | 37 +++++++++----- mathesar_ui/src/stores/table-data/records.ts | 12 +++++ .../src/stores/table-data/tabularData.ts | 49 ++++++++++++++++--- 5 files changed, 86 insertions(+), 25 deletions(-) diff --git a/mathesar_ui/src/components/sheet/selection/Plane.ts b/mathesar_ui/src/components/sheet/selection/Plane.ts index 20ed0fccc5..7a3185852e 100644 --- a/mathesar_ui/src/components/sheet/selection/Plane.ts +++ b/mathesar_ui/src/components/sheet/selection/Plane.ts @@ -3,7 +3,7 @@ import { map } from 'iter-tools'; import { cartesianProduct } from '@mathesar/utils/iterUtils'; import { makeCellId, parseCellId } from '../cellIds'; import { Direction, getColumnOffset, getRowOffset } from './Direction'; -import type Series from './Series'; +import Series from './Series'; function makeCells( rowIds: Iterable, @@ -52,9 +52,9 @@ export default class Plane { readonly placeholderRowId: string | undefined; constructor( - rowIds: Series, - columnIds: Series, - placeholderRowId: string | undefined, + rowIds: Series = new Series(), + columnIds: Series = new Series(), + placeholderRowId?: string, ) { this.rowIds = rowIds; this.columnIds = columnIds; diff --git a/mathesar_ui/src/components/sheet/selection/Series.ts b/mathesar_ui/src/components/sheet/selection/Series.ts index 7b8cd45dcc..7034fd06b7 100644 --- a/mathesar_ui/src/components/sheet/selection/Series.ts +++ b/mathesar_ui/src/components/sheet/selection/Series.ts @@ -1,6 +1,7 @@ -import { ImmutableMap } from '@mathesar/component-library'; import { filter, findBest, firstHighest, firstLowest } from 'iter-tools'; +import { ImmutableMap } from '@mathesar/component-library'; + export default class Series { private readonly values: Id[]; @@ -10,7 +11,7 @@ export default class Series { /** * @throws Error if duplicate values are provided */ - constructor(values: Id[]) { + constructor(values: Id[] = []) { this.values = values; this.indexLookup = new ImmutableMap( values.map((value, index) => [value, index]), diff --git a/mathesar_ui/src/components/sheet/selection/SheetSelection.ts b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts index ba8c8b1b8b..a16948d9e0 100644 --- a/mathesar_ui/src/components/sheet/selection/SheetSelection.ts +++ b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts @@ -4,22 +4,26 @@ import { ImmutableSet } from '@mathesar/component-library'; import { assertExhaustive } from '@mathesar/utils/typeUtils'; import { parseCellId } from '../cellIds'; import { Direction, getColumnOffset } from './Direction'; -import type Plane from './Plane'; +import Plane from './Plane'; /** - * - `'dataCells'` means that the selection contains data cells. This is by - * far the most common type of selection basis. + * - `'dataCells'` means that the selection contains data cells. This is by far + * the most common type of selection basis. * - * - `'emptyColumns'` is used when the sheet has no rows. In this case we - * still want to allow the user to select columns, so we use this basis. + * - `'emptyColumns'` is used when the sheet has no rows. In this case we still + * want to allow the user to select columns, so we use this basis. * * - `'placeholderCell'` is used when the user is selecting a cell in the - * placeholder row. This is a special case because we don't want to allow - * the user to select multiple cells in the placeholder row, and we also - * don't want to allow selections that include cells in data rows _and_ the + * placeholder row. This is a special case because we don't want to allow the + * user to select multiple cells in the placeholder row, and we also don't + * want to allow selections that include cells in data rows _and_ the * placeholder row. + * + * - `'empty'` is used when no cells are selected. We try to avoid this state, + * but we also allow for it because it makes it easier to construct selection + * instances if we don't already have the full plane data. */ -type BasisType = 'dataCells' | 'emptyColumns' | 'placeholderCell'; +type BasisType = 'dataCells' | 'emptyColumns' | 'placeholderCell' | 'empty'; interface Basis { readonly type: BasisType; @@ -67,12 +71,22 @@ function basisFromPlaceholderCell(activeCellId: string): Basis { }; } +function emptyBasis(): Basis { + return { + type: 'empty', + activeCellId: undefined, + cellIds: new ImmutableSet(), + columnIds: new ImmutableSet(), + rowIds: new ImmutableSet(), + }; +} + export default class SheetSelection { private readonly plane: Plane; private readonly basis: Basis; - constructor(plane: Plane, basis: Basis) { + constructor(plane: Plane = new Plane(), basis: Basis = emptyBasis()) { this.plane = plane; this.basis = basis; } @@ -187,7 +201,8 @@ export default class SheetSelection { * @returns a new selection that fits within the provided plane. This is * useful when a column is deleted, reordered, or inserted. */ - forNewPlane(plane: Plane): SheetSelection { + forNewPlane(newPlane: Plane): SheetSelection { + // TODO_NEXT throw new Error('Not implemented'); } diff --git a/mathesar_ui/src/stores/table-data/records.ts b/mathesar_ui/src/stores/table-data/records.ts index 4719c1ac81..870770ff9e 100644 --- a/mathesar_ui/src/stores/table-data/records.ts +++ b/mathesar_ui/src/stores/table-data/records.ts @@ -29,6 +29,7 @@ import { patchAPI, postAPI, } from '@mathesar/api/utils/requestUtils'; +import Series from '@mathesar/components/sheet/selection/Series'; import type Pagination from '@mathesar/utils/Pagination'; import { getErrorMessage } from '@mathesar/utils/errors'; import { pluralize } from '@mathesar/utils/languageUtils'; @@ -293,6 +294,8 @@ export class RecordsData { error: Writable; + selectableRowIds: Readable>; + private promise: CancellablePromise | undefined; // @ts-ignore: https://github.com/centerofci/mathesar/issues/1055 @@ -339,6 +342,15 @@ export class RecordsData { this.url = `/api/db/v0/tables/${this.parentId}/records/`; void this.fetch(); + this.selectableRowIds = derived( + [this.savedRecords, this.newRecords], + ([savedRecords, newRecords]) => + new Series([ + ...savedRecords.map((r) => r.identifier), + ...newRecords.map((r) => r.identifier), + ]), + ); + // TODO: Create base class to abstract subscriptions and unsubscriptions this.requestParamsUnsubscriber = this.meta.recordsRequestParamsData.subscribe(() => { diff --git a/mathesar_ui/src/stores/table-data/tabularData.ts b/mathesar_ui/src/stores/table-data/tabularData.ts index 543e977ea3..086affd1e3 100644 --- a/mathesar_ui/src/stores/table-data/tabularData.ts +++ b/mathesar_ui/src/stores/table-data/tabularData.ts @@ -1,30 +1,34 @@ import { getContext, setContext } from 'svelte'; import { derived, - writable, get, + writable, type Readable, type Writable, } from 'svelte/store'; + import type { DBObjectEntry } from '@mathesar/AppTypes'; import type { TableEntry } from '@mathesar/api/types/tables'; -import type { AbstractTypesMap } from '@mathesar/stores/abstract-types/types'; -import { States } from '@mathesar/api/utils/requestUtils'; import type { Column } from '@mathesar/api/types/tables/columns'; +import { States } from '@mathesar/api/utils/requestUtils'; import { LegacySheetSelection } from '@mathesar/components/sheet'; -import { getColumnOrder } from '@mathesar/utils/tables'; -import { Meta } from './meta'; +import Plane from '@mathesar/components/sheet/selection/Plane'; +import Series from '@mathesar/components/sheet/selection/Series'; +import SheetSelection from '@mathesar/components/sheet/selection/SheetSelection'; +import type { AbstractTypesMap } from '@mathesar/stores/abstract-types/types'; +import { getColumnOrder, orderProcessedColumns } from '@mathesar/utils/tables'; import { ColumnsDataStore } from './columns'; -import type { RecordRow, TableRecordsData } from './records'; -import { RecordsData } from './records'; -import { Display } from './display'; import type { ConstraintsData } from './constraints'; import { ConstraintsDataStore } from './constraints'; +import { Display } from './display'; +import { Meta } from './meta'; import type { ProcessedColumn, ProcessedColumnsStore, } from './processedColumns'; import { processColumn } from './processedColumns'; +import type { RecordRow, TableRecordsData } from './records'; +import { RecordsData } from './records'; export interface TabularDataProps { id: DBObjectEntry['id']; @@ -56,8 +60,11 @@ export class TabularData { columnsDataStore: ColumnsDataStore; + /** TODO eliminate `processedColumns` in favor of `orderedProcessedColumns` */ processedColumns: ProcessedColumnsStore; + orderedProcessedColumns: ProcessedColumnsStore; + constraintsDataStore: ConstraintsDataStore; recordsData: RecordsData; @@ -68,8 +75,12 @@ export class TabularData { legacySelection: TabularDataSelection; + selection: Writable; + table: TableEntry; + cleanupFunctions: (() => void)[] = []; + constructor(props: TabularDataProps) { const contextualFilters = props.contextualFilters ?? new Map(); @@ -112,6 +123,27 @@ export class TabularData { this.table = props.table; + this.orderedProcessedColumns = derived(this.processedColumns, (p) => + orderProcessedColumns(p, this.table), + ); + + const plane = derived( + [this.recordsData.selectableRowIds, this.orderedProcessedColumns], + ([selectableRowIds, orderedProcessedColumns]) => { + const columnIds = new Series( + [...orderedProcessedColumns.values()].map((c) => String(c.id)), + ); + return new Plane(selectableRowIds, columnIds); + }, + ); + + // TODO add id of placeholder row to selection + this.selection = writable(new SheetSelection()); + + this.cleanupFunctions.push( + plane.subscribe((p) => this.selection.update((s) => s.forNewPlane(p))), + ); + this.legacySelection = new LegacySheetSelection({ getColumns: () => [...get(this.processedColumns).values()], getColumnOrder: () => @@ -222,6 +254,7 @@ export class TabularData { this.constraintsDataStore.destroy(); this.columnsDataStore.destroy(); this.legacySelection.destroy(); + this.cleanupFunctions.forEach((f) => f()); } } From 640eb9b08ba1636005cad928652c13a41de4731e Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Fri, 25 Aug 2023 09:42:40 -0400 Subject: [PATCH 07/85] Some readability improvements --- .../src/components/sheet/selection/Plane.ts | 12 +++++++ .../src/components/sheet/selection/Series.ts | 36 ++++++++++--------- .../sheet/selection/SheetSelection.ts | 17 +++++++++ 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/mathesar_ui/src/components/sheet/selection/Plane.ts b/mathesar_ui/src/components/sheet/selection/Plane.ts index 7a3185852e..2fd4b9df48 100644 --- a/mathesar_ui/src/components/sheet/selection/Plane.ts +++ b/mathesar_ui/src/components/sheet/selection/Plane.ts @@ -44,6 +44,18 @@ function adjacentPlaceholderCell(cellId: string): AdjacentCell { return { type: 'placeholderCell', cellId }; } +/** + * A Plane is like a coordinate system for a sheet. We can query it to find the + * ids of cells within certain bounding rectangles. + * + * The Plane can also have a "placeholder row", which is a row at the bottom of + * the sheet that provides a visual cue to the user that they can add more rows. + * It never contains any data, but we allow the user to move the active cell + * into the placeholder row in order to easily add more rows. + * + * The term "Flexible" is used in methods to indicate that it will gracefully + * handle ids of cells within the placeholder row. + */ export default class Plane { readonly rowIds: Series; diff --git a/mathesar_ui/src/components/sheet/selection/Series.ts b/mathesar_ui/src/components/sheet/selection/Series.ts index 7034fd06b7..cbfd31d16d 100644 --- a/mathesar_ui/src/components/sheet/selection/Series.ts +++ b/mathesar_ui/src/components/sheet/selection/Series.ts @@ -2,16 +2,20 @@ import { filter, findBest, firstHighest, firstLowest } from 'iter-tools'; import { ImmutableMap } from '@mathesar/component-library'; -export default class Series { - private readonly values: Id[]; +/** + * A Series is an immutable ordered collection of values with methods that + * provide efficient access to _ranges_ of those values. + */ +export default class Series { + private readonly values: Value[]; /** Maps the id value to its index */ - private readonly indexLookup: ImmutableMap; + private readonly indexLookup: ImmutableMap; /** * @throws Error if duplicate values are provided */ - constructor(values: Id[] = []) { + constructor(values: Value[] = []) { this.values = values; this.indexLookup = new ImmutableMap( values.map((value, index) => [value, index]), @@ -21,7 +25,7 @@ export default class Series { } } - private getIndex(value: Id): number | undefined { + private getIndex(value: Value): number | undefined { return this.indexLookup.get(value); } @@ -36,7 +40,7 @@ export default class Series { * * @throws an Error if either value is not present in the sequence */ - range(a: Id, b: Id): Iterable { + range(a: Value, b: Value): Iterable { const aIndex = this.getIndex(a); const bIndex = this.getIndex(b); @@ -50,15 +54,15 @@ export default class Series { return this.values.slice(startIndex, endIndex + 1); } - get first(): Id | undefined { + get first(): Value | undefined { return this.values[0]; } - get last(): Id | undefined { + get last(): Value | undefined { return this.values[this.values.length - 1]; } - has(value: Id): boolean { + has(value: Value): boolean { return this.getIndex(value) !== undefined; } @@ -72,18 +76,18 @@ export default class Series { * https://github.com/iter-tools/iter-tools/blob/d7.5/API.md#compare-values-and-return-true-or-false */ best( - values: Iterable, + values: Iterable, comparator: (best: number, v: number) => boolean, - ): Id | undefined { + ): Value | undefined { const validValues = filter((v) => this.has(v), values); return findBest(comparator, (v) => this.getIndex(v) ?? 0, validValues); } - min(values: Iterable): Id | undefined { + min(values: Iterable): Value | undefined { return this.best(values, firstLowest); } - max(values: Iterable): Id | undefined { + max(values: Iterable): Value | undefined { return this.best(values, firstHighest); } @@ -94,7 +98,7 @@ export default class Series { * given value. If no such value is present, then `undefined` will be * returned. */ - offset(value: Id, offset: number): Id | undefined { + offset(value: Value, offset: number): Value | undefined { if (offset === 0) { return this.has(value) ? value : undefined; } @@ -112,7 +116,7 @@ export default class Series { * value in the block. If no such value is present, then `undefined` will be * returned. If offset is zero, then `undefined` will be returned. */ - collapsedOffset(values: Iterable, offset: number): Id | undefined { + collapsedOffset(values: Iterable, offset: number): Value | undefined { if (offset === 0) { return undefined; } @@ -123,7 +127,7 @@ export default class Series { return this.offset(outerValue, offset); } - [Symbol.iterator](): Iterator { + [Symbol.iterator](): Iterator { return this.values[Symbol.iterator](); } } diff --git a/mathesar_ui/src/components/sheet/selection/SheetSelection.ts b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts index a16948d9e0..d9b7bcd84f 100644 --- a/mathesar_ui/src/components/sheet/selection/SheetSelection.ts +++ b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts @@ -25,6 +25,14 @@ import Plane from './Plane'; */ type BasisType = 'dataCells' | 'emptyColumns' | 'placeholderCell' | 'empty'; +/** + * This type stores data about "which cells are selected", with some redundancy + * for efficient and consistent lookup across different kinds of selections. + * + * Due to the redundant nature of some properties on this type, you should be + * sure to only instantiate Basis using the utility functions below. This will + * ensure that the data is always valid. + */ interface Basis { readonly type: BasisType; readonly activeCellId: string | undefined; @@ -81,6 +89,15 @@ function emptyBasis(): Basis { }; } +/** + * This is an immutable data structure which fully represents the state of a + * selection selection of cells along with the Plane in which they were + * selected. + * + * We store the Plane here so to make it possible to provide methods on + * `SheetSelection` instances which return new mutations of the selection that + * are still valid within the Plane. + */ export default class SheetSelection { private readonly plane: Plane; From de875e41130da4d667e0615a04a4d18a852f20f4 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Fri, 25 Aug 2023 10:48:46 -0400 Subject: [PATCH 08/85] Add `intersect` method to ImmutableSet --- .../component-library/common/utils/ImmutableSet.ts | 9 +++++++++ .../common/utils/__tests__/ImmutableSet.test.ts | 12 ++++++++++++ 2 files changed, 21 insertions(+) diff --git a/mathesar_ui/src/component-library/common/utils/ImmutableSet.ts b/mathesar_ui/src/component-library/common/utils/ImmutableSet.ts index 609c156ecd..d442984ad0 100644 --- a/mathesar_ui/src/component-library/common/utils/ImmutableSet.ts +++ b/mathesar_ui/src/component-library/common/utils/ImmutableSet.ts @@ -32,6 +32,15 @@ export default class ImmutableSet { return this.getNewInstance(set); } + /** + * @returns a new ImmutableSet that contains only the items that are present + * in both this set and the other set. The order of the items in the returned + * set is taken from this set (not the supplied set). + */ + intersect(other: { has: (v: T) => boolean }): this { + return this.getNewInstance([...this].filter((v) => other.has(v))); + } + without(itemOrItems: T | T[]): this { const items = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems]; const set = new Set(this.set); diff --git a/mathesar_ui/src/component-library/common/utils/__tests__/ImmutableSet.test.ts b/mathesar_ui/src/component-library/common/utils/__tests__/ImmutableSet.test.ts index 24938eb957..3d20add0d6 100644 --- a/mathesar_ui/src/component-library/common/utils/__tests__/ImmutableSet.test.ts +++ b/mathesar_ui/src/component-library/common/utils/__tests__/ImmutableSet.test.ts @@ -20,3 +20,15 @@ test('union', () => { expect(a.union(empty).valuesArray()).toEqual([2, 7, 13, 19, 5]); expect(empty.union(a).valuesArray()).toEqual([2, 7, 13, 19, 5]); }); + +test('intersect', () => { + const a = new ImmutableSet([2, 7, 13, 19, 5]); + const b = new ImmutableSet([23, 13, 3, 2]); + const empty = new ImmutableSet(); + expect(a.intersect(a).valuesArray()).toEqual([2, 7, 13, 19, 5]); + expect(a.intersect(b).valuesArray()).toEqual([2, 13]); + expect(b.intersect(a).valuesArray()).toEqual([13, 2]); + expect(a.intersect(empty).valuesArray()).toEqual([]); + expect(empty.intersect(a).valuesArray()).toEqual([]); + expect(empty.intersect(empty).valuesArray()).toEqual([]); +}); From c64e12b87a9f6d81b19770e0c0290fc235309b0f Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Fri, 25 Aug 2023 21:57:14 -0400 Subject: [PATCH 09/85] Refactor Series tests to use cases --- .../sheet/selection/__tests__/Series.test.ts | 116 +++++++++++------- 1 file changed, 75 insertions(+), 41 deletions(-) diff --git a/mathesar_ui/src/components/sheet/selection/__tests__/Series.test.ts b/mathesar_ui/src/components/sheet/selection/__tests__/Series.test.ts index 8109932d9f..0d485b64a7 100644 --- a/mathesar_ui/src/components/sheet/selection/__tests__/Series.test.ts +++ b/mathesar_ui/src/components/sheet/selection/__tests__/Series.test.ts @@ -1,44 +1,78 @@ import Series from '../Series'; -test('Series', () => { - const s = new Series(['h', 'i', 'j', 'k', 'l']); - expect(s.length).toBe(5); - expect(s.first).toBe('h'); - expect(s.last).toBe('l'); - - expect([...s]).toEqual(['h', 'i', 'j', 'k', 'l']); - - expect(s.min(['i', 'j', 'k'])).toBe('i'); - expect(s.min(['k', 'j', 'i'])).toBe('i'); - expect(s.min(['i', 'NOPE'])).toBe('i'); - expect(s.min(['NOPE'])).toBe(undefined); - expect(s.min([])).toBe(undefined); - - expect(s.max(['i', 'j', 'k'])).toBe('k'); - expect(s.max(['k', 'j', 'i'])).toBe('k'); - expect(s.max(['i', 'NOPE'])).toBe('i'); - expect(s.max(['NOPE'])).toBe(undefined); - expect(s.max([])).toBe(undefined); - - expect([...s.range('i', 'k')]).toEqual(['i', 'j', 'k']); - expect([...s.range('k', 'i')]).toEqual(['i', 'j', 'k']); - expect([...s.range('i', 'i')]).toEqual(['i']); - expect(() => s.range('i', 'NOPE')).toThrow(); - - expect(s.offset('i', 0)).toBe('i'); - expect(s.offset('i', 1)).toBe('j'); - expect(s.offset('i', 2)).toBe('k'); - expect(s.offset('i', -1)).toBe('h'); - expect(s.offset('i', -2)).toBe(undefined); - expect(s.offset('NOPE', 0)).toBe(undefined); - expect(s.offset('NOPE', 1)).toBe(undefined); - - expect(s.collapsedOffset(['i', 'k'], 0)).toBe(undefined); - expect(s.collapsedOffset(['i', 'k'], 1)).toBe('l'); - expect(s.collapsedOffset(['i', 'k'], 2)).toBe(undefined); - expect(s.collapsedOffset(['i', 'k'], -1)).toBe('h'); - expect(s.collapsedOffset(['i', 'k'], -2)).toBe(undefined); - expect(s.collapsedOffset(['i', 'NOPE'], 0)).toBe(undefined); - expect(s.collapsedOffset(['i', 'NOPE'], 1)).toBe('j'); - expect(s.collapsedOffset([], 0)).toBe(undefined); +describe('Series', () => { + const h = 'h'; + const i = 'i'; + const j = 'j'; + const k = 'k'; + const l = 'l'; + const all = [h, i, j, k, l]; + const s = new Series(all); + + test('basics', () => { + expect(s.length).toBe(5); + expect(s.first).toBe(h); + expect(s.last).toBe(l); + expect([...s]).toEqual(all); + }); + + test.each([ + [[i, j, k], i], + [[k, j, i], i], + [[i, 'NOPE'], i], + [['NOPE'], undefined], + [[], undefined], + ])('min %#', (input, expected) => { + expect(s.min(input)).toBe(expected); + }); + + test.each([ + [[i, j, k], k], + [[k, j, i], k], + [[i, 'NOPE'], i], + [['NOPE'], undefined], + [[], undefined], + ])('max %#', (input, expected) => { + expect(s.max(input)).toBe(expected); + }); + + test.each([ + [i, k, [i, j, k]], + [k, i, [i, j, k]], + [i, i, [i]], + ])('range %#', (a, b, expected) => { + expect([...s.range(a, b)]).toEqual(expected); + }); + + test.each([ + [i, 'NOPE'], + ['NOPE', i], + ])('range failures %#', (a, b) => { + expect(() => s.range(a, b)).toThrow(); + }); + + test.each([ + [i, 0, i], + [i, 1, j], + [i, 2, k], + [i, -1, h], + [i, -2, undefined], + ['NOPE', 0, undefined], + ['NOPE', 1, undefined], + ])('offset %#', (value, offset, expected) => { + expect(s.offset(value, offset)).toBe(expected); + }); + + test.each([ + [[i, k], 0, undefined], + [[i, k], 1, l], + [[i, k], 2, undefined], + [[i, k], -1, h], + [[i, k], -2, undefined], + [[i, 'NOPE'], 0, undefined], + [[i, 'NOPE'], 1, j], + [[], 0, undefined], + ])('collapsedOffset %#', (values, offset, expected) => { + expect(s.collapsedOffset(values, offset)).toBe(expected); + }); }); From be749bad54be4996f0a4055d4db818b7f5fed7f1 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Fri, 25 Aug 2023 21:57:48 -0400 Subject: [PATCH 10/85] Tiny readability improvement --- mathesar_ui/src/components/sheet/selection/Series.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mathesar_ui/src/components/sheet/selection/Series.ts b/mathesar_ui/src/components/sheet/selection/Series.ts index cbfd31d16d..b0e897aab3 100644 --- a/mathesar_ui/src/components/sheet/selection/Series.ts +++ b/mathesar_ui/src/components/sheet/selection/Series.ts @@ -1,10 +1,9 @@ import { filter, findBest, firstHighest, firstLowest } from 'iter-tools'; - -import { ImmutableMap } from '@mathesar/component-library'; +import { ImmutableMap } from '@mathesar-component-library'; /** - * A Series is an immutable ordered collection of values with methods that - * provide efficient access to _ranges_ of those values. + * A Series is an immutable ordered collection of unique values with methods + * that provide efficient access to _ranges_ of those values. */ export default class Series { private readonly values: Value[]; From 0476730ddbfaca8bd3548b926ce8c17d0ce1fbba Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Fri, 25 Aug 2023 21:58:20 -0400 Subject: [PATCH 11/85] Implement SheetSelection.forNewPlane --- .../src/components/sheet/selection/Plane.ts | 30 ++++++++- .../sheet/selection/SheetSelection.ts | 62 ++++++++++++++++++- 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/mathesar_ui/src/components/sheet/selection/Plane.ts b/mathesar_ui/src/components/sheet/selection/Plane.ts index 2fd4b9df48..3f0d87780f 100644 --- a/mathesar_ui/src/components/sheet/selection/Plane.ts +++ b/mathesar_ui/src/components/sheet/selection/Plane.ts @@ -145,16 +145,40 @@ export default class Plane { ): Iterable { const cellA = parseCellId(cellIdA); const cellB = parseCellId(cellIdB); - const rowIdA = this.normalizeFlexibleRowId(cellA.rowId); + return this.dataCellsInFlexibleRowColumnRange( + cellA.rowId, + cellB.rowId, + cellA.columnId, + cellB.columnId, + ); + } + + /** + * @returns an iterable of all the data cells in the plane that are within the + * rectangle bounded by the given rows and columns. This does not include + * header cells. + * + * If either of the provided rowIds are placeholder cells, then cells in the + * last row and last column will be used in their place. This ensures that the + * selection is made only of data cells, and will never include the + * placeholder cell, even if a user drags to select it. + */ + dataCellsInFlexibleRowColumnRange( + flexibleRowIdA: string, + flexibleRowIdB: string, + columnIdA: string, + columnIdB: string, + ): Iterable { + const rowIdA = this.normalizeFlexibleRowId(flexibleRowIdA); if (rowIdA === undefined) { return []; } - const rowIdB = this.normalizeFlexibleRowId(cellB.rowId); + const rowIdB = this.normalizeFlexibleRowId(flexibleRowIdB); if (rowIdB === undefined) { return []; } const rowIds = this.rowIds.range(rowIdA, rowIdB); - const columnIds = this.columnIds.range(cellA.columnId, cellB.columnId); + const columnIds = this.columnIds.range(columnIdA, columnIdB); return makeCells(rowIds, columnIds); } diff --git a/mathesar_ui/src/components/sheet/selection/SheetSelection.ts b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts index d9b7bcd84f..9be7602e84 100644 --- a/mathesar_ui/src/components/sheet/selection/SheetSelection.ts +++ b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts @@ -219,8 +219,66 @@ export default class SheetSelection { * useful when a column is deleted, reordered, or inserted. */ forNewPlane(newPlane: Plane): SheetSelection { - // TODO_NEXT - throw new Error('Not implemented'); + if (this.basis.type === 'dataCells') { + if (!newPlane.hasResultRows) { + return new SheetSelection(newPlane, basisFromZeroEmptyColumns()); + } + const minColumnId = newPlane.columnIds.min(this.basis.columnIds); + const maxColumnId = newPlane.columnIds.max(this.basis.columnIds); + const minRowId = newPlane.rowIds.min(this.basis.rowIds); + const maxRowId = newPlane.rowIds.max(this.basis.rowIds); + if ( + minColumnId === undefined || + maxColumnId === undefined || + minRowId === undefined || + maxRowId === undefined + ) { + return new SheetSelection(newPlane); + } + const cellIds = newPlane.dataCellsInFlexibleRowColumnRange( + minRowId, + maxRowId, + minColumnId, + maxColumnId, + ); + return new SheetSelection(newPlane, basisFromDataCells(cellIds)); + } + + if (this.basis.type === 'emptyColumns') { + if (newPlane.hasResultRows) { + return new SheetSelection(newPlane); + } + const minColumnId = newPlane.columnIds.min(this.basis.columnIds); + const maxColumnId = newPlane.columnIds.max(this.basis.columnIds); + if (minColumnId === undefined || maxColumnId === undefined) { + return new SheetSelection(newPlane, basisFromZeroEmptyColumns()); + } + const columnIds = newPlane.columnIds.range(minColumnId, maxColumnId); + return new SheetSelection(newPlane, basisFromEmptyColumns(columnIds)); + } + + if (this.basis.type === 'placeholderCell') { + const columnId = first(this.basis.columnIds); + if (columnId === undefined) { + return new SheetSelection(newPlane); + } + const newPlaneHasSelectedCell = + newPlane.columnIds.has(columnId) && + newPlane.placeholderRowId === this.plane.placeholderRowId; + if (newPlaneHasSelectedCell) { + // If we can retain the selected placeholder cell, then do so. + return new SheetSelection(newPlane, basisFromPlaceholderCell(columnId)); + } + // Otherwise, return an empty selection + return new SheetSelection(newPlane); + } + + if (this.basis.type === 'empty') { + // If the selection is empty, we keep it empty. + return new SheetSelection(newPlane); + } + + return assertExhaustive(this.basis.type); } /** From 09cd2198ede775e1d79dad7f05057675e4df9f1e Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Sun, 27 Aug 2023 21:15:45 -0400 Subject: [PATCH 12/85] Begin incorporating new Selection into table sheet --- .../sheet/selection/SheetSelection.ts | 42 ++++++----- .../src/systems/table-view/row/Row.svelte | 41 +++++------ .../src/systems/table-view/row/RowCell.svelte | 72 ++++++++----------- 3 files changed, 68 insertions(+), 87 deletions(-) diff --git a/mathesar_ui/src/components/sheet/selection/SheetSelection.ts b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts index 9be7602e84..08f5b67a61 100644 --- a/mathesar_ui/src/components/sheet/selection/SheetSelection.ts +++ b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts @@ -55,6 +55,10 @@ function basisFromDataCells( }; } +function basisFromOneDataCell(cellId: string): Basis { + return basisFromDataCells([cellId], cellId); +} + function basisFromEmptyColumns(columnIds: Iterable): Basis { return { type: 'emptyColumns', @@ -106,6 +110,8 @@ export default class SheetSelection { constructor(plane: Plane = new Plane(), basis: Basis = emptyBasis()) { this.plane = plane; this.basis = basis; + // TODO validate that basis is valid within plane. For example, remove + // selected cells from the basis that do not occur within the plane. } get activeCellId() { @@ -199,19 +205,17 @@ export default class SheetSelection { } /** - * @returns a new selection formed from one cell within the placeholder row. - * Note that we do not support selections of multiple cells in the placeholder - * row. + * @returns a new selection formed from one cell within the data rows or the + * placeholder row. */ - atPlaceholderCell(cellId: string): SheetSelection { - return this.withBasis(basisFromPlaceholderCell(cellId)); - } - - /** - * @returns a new selection formed from one cell within the data rows. - */ - atDataCell(cellId: string): SheetSelection { - return this.withBasis(basisFromDataCells([cellId], cellId)); + ofOneCell(cellId: string): SheetSelection { + const { rowId } = parseCellId(cellId); + const { placeholderRowId } = this.plane; + const makeBasis = + rowId === placeholderRowId + ? basisFromPlaceholderCell + : basisFromOneDataCell; + return this.withBasis(makeBasis(cellId)); } /** @@ -233,6 +237,10 @@ export default class SheetSelection { minRowId === undefined || maxRowId === undefined ) { + // TODO: in some cases maybe we can be smarter here. Instead of + // returning an empty selection, we could try to return a selection of + // the same dimensions that is placed as close as possible to the + // original selection. return new SheetSelection(newPlane); } const cellIds = newPlane.dataCellsInFlexibleRowColumnRange( @@ -345,13 +353,9 @@ export default class SheetSelection { // If we can't move anywhere, then do nothing return this; } - if (adjacent.type === 'dataCell') { - // Move to an adjacent data cell - return this.atDataCell(adjacent.cellId); - } - if (adjacent.type === 'placeholderCell') { - // Move to an adjacent placeholder cell - return this.atPlaceholderCell(adjacent.cellId); + if (adjacent.type === 'dataCell' || adjacent.type === 'placeholderCell') { + // Move to an adjacent data cell or adjacent placeholder cell + return this.ofOneCell(adjacent.cellId); } return assertExhaustive(adjacent); } diff --git a/mathesar_ui/src/systems/table-view/row/Row.svelte b/mathesar_ui/src/systems/table-view/row/Row.svelte index 967c17b677..07a1faab61 100644 --- a/mathesar_ui/src/systems/table-view/row/Row.svelte +++ b/mathesar_ui/src/systems/table-view/row/Row.svelte @@ -11,11 +11,7 @@ ID_ROW_CONTROL_COLUMN, type Row, } from '@mathesar/stores/table-data'; - import { - SheetRow, - SheetCell, - isRowSelected, - } from '@mathesar/components/sheet'; + import { SheetRow, SheetCell } from '@mathesar/components/sheet'; import { rowHeightPx } from '@mathesar/geometry'; import { ContextMenu } from '@mathesar/component-library'; import NewRecordMessage from './NewRecordMessage.svelte'; @@ -29,13 +25,8 @@ const tabularData = getTabularDataStoreFromContext(); - $: ({ - recordsData, - columnsDataStore, - meta, - processedColumns, - legacySelection: selection, - } = $tabularData); + $: ({ recordsData, columnsDataStore, meta, processedColumns, selection } = + $tabularData); $: ({ rowStatus, rowCreationStatus, @@ -50,29 +41,31 @@ $: creationStatus = $rowCreationStatus.get(rowKey)?.state; $: status = $rowStatus.get(rowKey); $: wholeRowState = status?.wholeRowState; - $: ({ selectedCells } = selection); - $: isSelected = rowHasRecord(row) && isRowSelected($selectedCells, row); + $: isSelected = $selection.rowIds.has(row.identifier); $: hasWholeRowErrors = wholeRowState === 'failure'; /** Including whole row errors and individual cell errors */ $: hasAnyErrors = !!status?.errorsFromWholeRowAndCells?.length; function checkAndCreateEmptyRow() { - if (isPlaceholderRow(row)) { - void recordsData.addEmptyRecord(); - selection.selectAndActivateFirstDataEntryCellInLastRow(); - } + // // TODO_3037 + // if (isPlaceholderRow(row)) { + // void recordsData.addEmptyRecord(); + // selection.selectAndActivateFirstDataEntryCellInLastRow(); + // } } const handleRowMouseDown = () => { - if (rowHasRecord(row) && !isPlaceholderRow(row)) { - selection.onRowSelectionStart(row); - } + // // TODO_3037 + // if (rowHasRecord(row) && !isPlaceholderRow(row)) { + // selection.onRowSelectionStart(row); + // } }; const handleRowMouseEnter = () => { - if (rowHasRecord(row) && !isPlaceholderRow(row)) { - selection.onMouseEnterRowHeaderWhileSelection(row); - } + // // TODO_3037 + // if (rowHasRecord(row) && !isPlaceholderRow(row)) { + // selection.onMouseEnterRowHeaderWhileSelection(row); + // } }; diff --git a/mathesar_ui/src/systems/table-view/row/RowCell.svelte b/mathesar_ui/src/systems/table-view/row/RowCell.svelte index d91fb0657a..0b9aa43f15 100644 --- a/mathesar_ui/src/systems/table-view/row/RowCell.svelte +++ b/mathesar_ui/src/systems/table-view/row/RowCell.svelte @@ -1,26 +1,27 @@ - - {#key id} {#if usesVirtualList} + import { first } from 'iter-tools'; + + import type { TableEntry } from '@mathesar/api/types/tables'; + import { ContextMenu } from '@mathesar/component-library'; import { - getTabularDataStoreFromContext, - ID_ADD_NEW_COLUMN, - ID_ROW_CONTROL_COLUMN, - } from '@mathesar/stores/table-data'; - import { - SheetHeader, SheetCell, SheetCellResizer, - isColumnSelected, + SheetHeader, } from '@mathesar/components/sheet'; import type { ProcessedColumn } from '@mathesar/stores/table-data'; + import { + getTabularDataStoreFromContext, + ID_ADD_NEW_COLUMN, + ID_ROW_CONTROL_COLUMN, + } from '@mathesar/stores/table-data'; import { saveColumnOrder } from '@mathesar/stores/tables'; - import type { TableEntry } from '@mathesar/api/types/tables'; - import { ContextMenu } from '@mathesar/component-library'; - import HeaderCell from './header-cell/HeaderCell.svelte'; - import NewColumnCell from './new-column-cell/NewColumnCell.svelte'; import { Draggable, Droppable } from './drag-and-drop'; import ColumnHeaderContextMenu from './header-cell/ColumnHeaderContextMenu.svelte'; + import HeaderCell from './header-cell/HeaderCell.svelte'; + import NewColumnCell from './new-column-cell/NewColumnCell.svelte'; const tabularData = getTabularDataStoreFromContext(); @@ -27,17 +28,7 @@ $: columnOrder = columnOrder ?? []; $: columnOrderString = columnOrder.map(String); - - $: ({ legacySelection: selection, processedColumns } = $tabularData); - $: ({ - selectedCells, - columnsSelectedWhenTheTableIsEmpty, - selectionInProgress, - } = selection); - $: selectedColumnIds = selection.getSelectedUniqueColumnsId( - $selectedCells, - $columnsSelectedWhenTheTableIsEmpty, - ); + $: ({ selection, processedColumns } = $tabularData); let locationOfFirstDraggedColumn: number | undefined = undefined; let selectedColumnIdsOrdered: string[] = []; @@ -55,7 +46,7 @@ columnOrderString = columnOrderString; // Remove selected column IDs and keep their order for (const id of columnOrderString) { - if (selectedColumnIds.map(String).includes(id)) { + if ($selection.columnIds.has(id)) { selectedColumnIdsOrdered.push(id); if (!locationOfFirstDraggedColumn) { locationOfFirstDraggedColumn = columnOrderString.indexOf(id); @@ -70,9 +61,8 @@ // Early exit if a column is dropped in the same place. // Should only be done for single column if non-continuous selection is allowed. if ( - selectedColumnIds.length > 0 && columnDroppedOn && - selectedColumnIds[0] === columnDroppedOn.id + first($selection.columnIds) === String(columnDroppedOn.id) ) { // Reset drag information locationOfFirstDraggedColumn = undefined; @@ -122,6 +112,7 @@ {#each [...$processedColumns] as [columnId, processedColumn] (columnId)} + {@const isSelected = $selection.columnIds.has(String(columnId))}
@@ -129,32 +120,27 @@ on:dragstart={() => dragColumn()} column={processedColumn} {selection} - selectionInProgress={$selectionInProgress} > dropColumn(processedColumn)} on:dragover={(e) => e.preventDefault()} {locationOfFirstDraggedColumn} columnLocation={columnOrderString.indexOf(columnId.toString())} - isSelected={isColumnSelected( - $selectedCells, - $columnsSelectedWhenTheTableIsEmpty, - processedColumn, - )} + {isSelected} > - selection.onColumnSelectionStart(processedColumn)} - on:mouseenter={() => - selection.onMouseEnterColumnHeaderWhileSelection( - processedColumn, - )} + {isSelected} + on:mousedown={() => { + // // TODO_3037 + // selection.onColumnSelectionStart(processedColumn) + }} + on:mouseenter={() => { + // // TODO_3037 + // selection.onMouseEnterColumnHeaderWhileSelection( + // processedColumn, + // ) + }} /> diff --git a/mathesar_ui/src/systems/table-view/header/drag-and-drop/Draggable.svelte b/mathesar_ui/src/systems/table-view/header/drag-and-drop/Draggable.svelte index 2dcfe0a5bd..1347bd2f5a 100644 --- a/mathesar_ui/src/systems/table-view/header/drag-and-drop/Draggable.svelte +++ b/mathesar_ui/src/systems/table-view/header/drag-and-drop/Draggable.svelte @@ -1,17 +1,19 @@
Date: Fri, 1 Sep 2023 12:53:56 -0400 Subject: [PATCH 14/85] Fix clipboard code to work with new selection code --- .../components/sheet/SheetClipboardHandler.ts | 116 +++++++----------- mathesar_ui/src/stores/table-data/records.ts | 22 ++-- .../src/stores/table-data/tabularData.ts | 12 +- .../src/systems/data-explorer/QueryRunner.ts | 15 ++- .../exploration-inspector/CellTab.svelte | 1 + .../data-explorer/result-pane/Results.svelte | 20 +-- .../src/systems/table-view/TableView.svelte | 86 ++++++------- .../src/systems/table-view/row/Row.svelte | 21 ++-- .../src/systems/table-view/row/RowCell.svelte | 3 +- mathesar_ui/src/utils/collectionUtils.ts | 3 + 10 files changed, 148 insertions(+), 151 deletions(-) create mode 100644 mathesar_ui/src/utils/collectionUtils.ts diff --git a/mathesar_ui/src/components/sheet/SheetClipboardHandler.ts b/mathesar_ui/src/components/sheet/SheetClipboardHandler.ts index 39728674d7..4d55f5aad3 100644 --- a/mathesar_ui/src/components/sheet/SheetClipboardHandler.ts +++ b/mathesar_ui/src/components/sheet/SheetClipboardHandler.ts @@ -1,19 +1,9 @@ import * as Papa from 'papaparse'; -import { get } from 'svelte/store'; -import { ImmutableSet, type MakeToast } from '@mathesar-component-library'; -import LegacySheetSelection, { - isCellSelected, -} from '@mathesar/components/sheet/LegacySheetSelection'; +import type { ImmutableSet } from '@mathesar-component-library'; import type { AbstractTypeCategoryIdentifier } from '@mathesar/stores/abstract-types/types'; import type { ClipboardHandler } from '@mathesar/stores/clipboard'; -import type { - ProcessedColumn, - RecordRow, - RecordSummariesForSheet, -} from '@mathesar/stores/table-data'; -import type { QueryRow } from '@mathesar/systems/data-explorer/QueryRunner'; -import type { ProcessedQueryOutputColumn } from '@mathesar/systems/data-explorer/utils'; +import type { RecordSummariesForSheet } from '@mathesar/stores/table-data'; import type { ReadableMapLike } from '@mathesar/typeUtils'; import { labeledCount } from '@mathesar/utils/languageUtils'; @@ -21,25 +11,34 @@ const MIME_PLAIN_TEXT = 'text/plain'; const MIME_MATHESAR_SHEET_CLIPBOARD = 'application/x-vnd.mathesar-sheet-clipboard'; -/** Keys are row ids, values are records */ -type IndexedRecords = Map>; +/** + * A column which allows the cells in it to be copied. + */ +interface CopyableColumn { + abstractType: { identifier: AbstractTypeCategoryIdentifier }; + formatCellValue: ( + cellValue: unknown, + recordSummaries?: RecordSummariesForSheet, + ) => string | null | undefined; +} -function getRawCellValue< - Column extends ProcessedQueryOutputColumn | ProcessedColumn, ->( - indexedRecords: IndexedRecords, - rowId: number, - columnId: Column['id'], -): unknown { - return indexedRecords.get(rowId)?.[String(columnId)]; +/** + * This is the stuff we need to know from the sheet in order to copy the content + * of cells to the clipboard. + */ +interface CopyingContext { + /** Keys are row ids, values are records */ + rowsMap: Map>; + columnsMap: ReadableMapLike; + recordSummaries: RecordSummariesForSheet; + selectedRowIds: ImmutableSet; + selectedColumnIds: ImmutableSet; } -function getFormattedCellValue< - Column extends ProcessedQueryOutputColumn | ProcessedColumn, ->( +function getFormattedCellValue( rawCellValue: unknown, - columnsMap: ReadableMapLike, - columnId: Column['id'], + columnsMap: CopyingContext['columnsMap'], + columnId: string, recordSummaries: RecordSummariesForSheet, ): string { if (rawCellValue === undefined || rawCellValue === null) { @@ -76,70 +75,37 @@ export interface StructuredCell { formatted: string; } -interface SheetClipboardHandlerDeps< - Row extends QueryRow | RecordRow, - Column extends ProcessedQueryOutputColumn | ProcessedColumn, -> { - selection: LegacySheetSelection; - toast: MakeToast; - getRows(): Row[]; - getColumnsMap(): ReadableMapLike; - getRecordSummaries(): RecordSummariesForSheet; +interface Dependencies { + getCopyingContext(): CopyingContext; + showToastInfo(msg: string): void; } -export class SheetClipboardHandler< - Row extends QueryRow | RecordRow, - Column extends ProcessedQueryOutputColumn | ProcessedColumn, -> implements ClipboardHandler -{ - private readonly deps: SheetClipboardHandlerDeps; +export class SheetClipboardHandler implements ClipboardHandler { + private readonly deps: Dependencies; - constructor(deps: SheetClipboardHandlerDeps) { + constructor(deps: Dependencies) { this.deps = deps; } - private getColumnIds(cells: ImmutableSet) { - return this.deps.selection.getSelectedUniqueColumnsId( - cells, - // We don't care about the columns selected when the table is empty, - // because we only care about cells selected that have content. - new ImmutableSet(), - ); - } - - private getRowIds(cells: ImmutableSet) { - return this.deps.selection.getSelectedUniqueRowsId(cells); - } - private getCopyContent(): { structured: string; tsv: string } { - const cells = get(this.deps.selection.selectedCells); - const indexedRecords = new Map( - this.deps.getRows().map((r) => [r.rowIndex, r.record]), - ); - const columns = this.deps.getColumnsMap(); - const recordSummaries = this.deps.getRecordSummaries(); - + const context = this.deps.getCopyingContext(); const tsvRows: string[][] = []; const structuredRows: StructuredCell[][] = []; - for (const rowId of this.getRowIds(cells)) { + for (const rowId of context.selectedRowIds) { const tsvRow: string[] = []; const structuredRow: StructuredCell[] = []; - for (const columnId of this.getColumnIds(cells)) { - const column = columns.get(columnId); - if (!isCellSelected(cells, { rowIndex: rowId }, { id: columnId })) { - // Ignore cells that are not selected. - continue; - } + for (const columnId of context.selectedColumnIds) { + const column = context.columnsMap.get(columnId); if (!column) { // Ignore cells with no associated column. This should never happen. continue; } - const rawCellValue = getRawCellValue(indexedRecords, rowId, columnId); + const rawCellValue = context.rowsMap.get(rowId)?.[columnId]; const formattedCellValue = getFormattedCellValue( rawCellValue, - columns, + context.columnsMap, columnId, - recordSummaries, + context.recordSummaries, ); const type = column.abstractType.identifier; structuredRow.push({ @@ -152,7 +118,9 @@ export class SheetClipboardHandler< tsvRows.push(tsvRow); structuredRows.push(structuredRow); } - this.deps.toast.info(`Copied ${labeledCount(cells.size, 'cells')}.`); + const cellCount = + context.selectedRowIds.size * context.selectedColumnIds.size; + this.deps.showToastInfo(`Copied ${labeledCount(cellCount, 'cells')}.`); return { structured: JSON.stringify(structuredRows), tsv: serializeTsv(tsvRows), diff --git a/mathesar_ui/src/stores/table-data/records.ts b/mathesar_ui/src/stores/table-data/records.ts index d2fcd34183..da90576fd9 100644 --- a/mathesar_ui/src/stores/table-data/records.ts +++ b/mathesar_ui/src/stores/table-data/records.ts @@ -26,11 +26,10 @@ import { States, deleteAPI, getAPI, + getQueryStringFromParams, patchAPI, postAPI, - getQueryStringFromParams, } from '@mathesar/api/utils/requestUtils'; -import Series from '@mathesar/components/sheet/selection/Series'; import type Pagination from '@mathesar/utils/Pagination'; import { getErrorMessage } from '@mathesar/utils/errors'; import { pluralize } from '@mathesar/utils/languageUtils'; @@ -175,7 +174,6 @@ export function filterRecordRows(rows: Row[]): RecordRow[] { export function rowHasSavedRecord(row: Row): row is RecordRow { return rowHasRecord(row) && Object.entries(row.record).length > 0; } - export interface TableRecordsData { state: States; error?: string; @@ -184,6 +182,10 @@ export interface TableRecordsData { grouping?: RecordGrouping; } +export function getRowSelectionId(row: Row): string { + return row.identifier; +} + export function getRowKey(row: Row, primaryKeyColumnId?: Column['id']): string { if (rowHasRecord(row) && primaryKeyColumnId !== undefined) { const primaryKeyCellValue = row.record[primaryKeyColumnId]; @@ -300,7 +302,8 @@ export class RecordsData { error: Writable; - selectableRowIds: Readable>; + /** Keys are row ids, values are records */ + selectableRowsMap: Readable>>; private promise: CancellablePromise | undefined; @@ -357,13 +360,12 @@ export class RecordsData { this.url = `/api/db/v0/tables/${this.tableId}/records/`; void this.fetch(); - this.selectableRowIds = derived( + this.selectableRowsMap = derived( [this.savedRecords, this.newRecords], - ([savedRecords, newRecords]) => - new Series([ - ...savedRecords.map((r) => r.identifier), - ...newRecords.map((r) => r.identifier), - ]), + ([savedRecords, newRecords]) => { + const records = [...savedRecords, ...newRecords]; + return new Map(records.map((r) => [getRowSelectionId(r), r.record])); + }, ); // TODO: Create base class to abstract subscriptions and unsubscriptions diff --git a/mathesar_ui/src/stores/table-data/tabularData.ts b/mathesar_ui/src/stores/table-data/tabularData.ts index 5029d92376..46d8453809 100644 --- a/mathesar_ui/src/stores/table-data/tabularData.ts +++ b/mathesar_ui/src/stores/table-data/tabularData.ts @@ -143,12 +143,12 @@ export class TabularData { ); const plane = derived( - [this.recordsData.selectableRowIds, this.orderedProcessedColumns], - ([selectableRowIds, orderedProcessedColumns]) => { - const columnIds = new Series( - [...orderedProcessedColumns.values()].map((c) => String(c.id)), - ); - return new Plane(selectableRowIds, columnIds); + [this.recordsData.selectableRowsMap, this.orderedProcessedColumns], + ([selectableRowsMap, orderedProcessedColumns]) => { + const rowIds = new Series([...selectableRowsMap.keys()]); + const columns = [...orderedProcessedColumns.values()]; + const columnIds = new Series(columns.map((c) => String(c.id))); + return new Plane(rowIds, columnIds); }, ); diff --git a/mathesar_ui/src/systems/data-explorer/QueryRunner.ts b/mathesar_ui/src/systems/data-explorer/QueryRunner.ts index 1a678575b8..ae6c285652 100644 --- a/mathesar_ui/src/systems/data-explorer/QueryRunner.ts +++ b/mathesar_ui/src/systems/data-explorer/QueryRunner.ts @@ -1,5 +1,5 @@ -import { get, writable } from 'svelte/store'; -import type { Writable } from 'svelte/store'; +import { derived, get, writable } from 'svelte/store'; +import type { Readable, Writable } from 'svelte/store'; import type { RequestStatus } from '@mathesar/api/utils/requestUtils'; import { ApiMultiError } from '@mathesar/api/utils/errors'; import { ImmutableMap, CancellablePromise } from '@mathesar-component-library'; @@ -31,6 +31,10 @@ export interface QueryRow { rowIndex: number; } +function getRowSelectionId(row: QueryRow): string { + return String(row.rowIndex); +} + export interface QueryRowsData { totalCount: number; rows: QueryRow[]; @@ -63,6 +67,9 @@ export default class QueryRunner { new ImmutableMap(), ); + /** Keys are row ids, values are records */ + selectableRowsMap: Readable>>; + selection: QuerySheetSelection; inspector: QueryInspector; @@ -100,6 +107,10 @@ export default class QueryRunner { this.shareConsumer = shareConsumer; this.speculateProcessedColumns(); void this.run(); + this.selectableRowsMap = derived( + this.rowsData, + ({ rows }) => new Map(rows.map((r) => [getRowSelectionId(r), r.record])), + ); this.selection = new LegacySheetSelection({ getColumns: () => [...get(this.processedColumns).values()], getColumnOrder: () => diff --git a/mathesar_ui/src/systems/data-explorer/exploration-inspector/CellTab.svelte b/mathesar_ui/src/systems/data-explorer/exploration-inspector/CellTab.svelte index a9490a9ceb..e6fccb7554 100644 --- a/mathesar_ui/src/systems/data-explorer/exploration-inspector/CellTab.svelte +++ b/mathesar_ui/src/systems/data-explorer/exploration-inspector/CellTab.svelte @@ -10,6 +10,7 @@ const cell = $activeCell; if (cell) { const rows = queryHandler.getRows(); + // TODO_3037 change rowIndex to use getRowSelectionId if (rows[cell.rowIndex]) { return rows[cell.rowIndex].record[cell.columnId]; } diff --git a/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte b/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte index 9703ad41a9..da32a65f4d 100644 --- a/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte +++ b/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte @@ -1,16 +1,16 @@
diff --git a/mathesar_ui/src/systems/table-view/row/Row.svelte b/mathesar_ui/src/systems/table-view/row/Row.svelte index 07a1faab61..a07cbddbc7 100644 --- a/mathesar_ui/src/systems/table-view/row/Row.svelte +++ b/mathesar_ui/src/systems/table-view/row/Row.svelte @@ -1,24 +1,25 @@
diff --git a/mathesar_ui/src/systems/table-view/table-inspector/column/ColumnMode.svelte b/mathesar_ui/src/systems/table-view/table-inspector/column/ColumnMode.svelte index fe1e01f610..7be1b9cd62 100644 --- a/mathesar_ui/src/systems/table-view/table-inspector/column/ColumnMode.svelte +++ b/mathesar_ui/src/systems/table-view/table-inspector/column/ColumnMode.svelte @@ -21,18 +21,17 @@ $: database = $currentDatabase; $: schema = $currentSchema; - $: ({ processedColumns, legacySelection: selection } = $tabularData); - $: ({ selectedCells, columnsSelectedWhenTheTableIsEmpty } = selection); + $: ({ processedColumns, selection } = $tabularData); + // TODO_3037 verify that table inspector shows selected columns $: selectedColumns = (() => { - const ids = selection.getSelectedUniqueColumnsId( - $selectedCells, - $columnsSelectedWhenTheTableIsEmpty, - ); + const ids = $selection.columnIds; const columns = []; for (const id of ids) { - const c = $processedColumns.get(id); - if (c !== undefined) { - columns.push(c); + // TODO_3037 add code comments explaining why this is necessary + const parsedId = parseInt(id, 10); + const column = $processedColumns.get(parsedId); + if (column !== undefined) { + columns.push(column); } } return columns; diff --git a/mathesar_ui/src/systems/table-view/table-inspector/record/RecordMode.svelte b/mathesar_ui/src/systems/table-view/table-inspector/record/RecordMode.svelte index 878701fec1..c342c3eefa 100644 --- a/mathesar_ui/src/systems/table-view/table-inspector/record/RecordMode.svelte +++ b/mathesar_ui/src/systems/table-view/table-inspector/record/RecordMode.svelte @@ -1,6 +1,5 @@
- {#if uniquelySelectedRowIndices.length} - {#if uniquelySelectedRowIndices.length > 1} + {#if selectedRowCount > 0} + {#if selectedRowCount > 1} - {labeledCount(uniquelySelectedRowIndices, 'records')} selected + {labeledCount(selectedRowCount, 'records')} selected {/if}
{:else} - Select one or more cells to view associated record properties. + + Select one or more cells to view associated record properties. + {/if}
diff --git a/mathesar_ui/src/systems/table-view/table-inspector/record/RowActions.svelte b/mathesar_ui/src/systems/table-view/table-inspector/record/RowActions.svelte index 66f343f999..53435f4981 100644 --- a/mathesar_ui/src/systems/table-view/table-inspector/record/RowActions.svelte +++ b/mathesar_ui/src/systems/table-view/table-inspector/record/RowActions.svelte @@ -12,7 +12,6 @@ import type { ColumnsDataStore, RecordsData, - TabularDataSelection, } from '@mathesar/stores/table-data'; import { getPkValueInRecord } from '@mathesar/stores/table-data/records'; import { toast } from '@mathesar/stores/toast'; @@ -20,7 +19,6 @@ export let selectedRowIndices: number[]; export let recordsData: RecordsData; - export let selection: TabularDataSelection; export let columnsDataStore: ColumnsDataStore; export let canEditTableRecords: boolean; @@ -38,7 +36,8 @@ toast.success({ title: `${confirmationTitle} deleted successfully!`, }); - selection.resetSelection(); + // // TODO_3037 verify that selection behaves okay after deleting records + // selection.resetSelection(); }, }); } From dd7f24232bd9d5b3f10b30f7a85e859a6992d182 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Mon, 11 Sep 2023 13:28:10 -0400 Subject: [PATCH 16/85] Modify util fn to handle non-rectangular selection --- .../sheet/selection/SheetSelection.ts | 63 ++++++++++++++----- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/mathesar_ui/src/components/sheet/selection/SheetSelection.ts b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts index b2da58135f..3cf039710e 100644 --- a/mathesar_ui/src/components/sheet/selection/SheetSelection.ts +++ b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts @@ -1,4 +1,4 @@ -import { first } from 'iter-tools'; +import { execPipe, filter, first, map } from 'iter-tools'; import { ImmutableSet } from '@mathesar/component-library'; import { assertExhaustive } from '@mathesar/utils/typeUtils'; @@ -98,30 +98,61 @@ function getFullySelectedColumnIds( basis: Basis, ): ImmutableSet { if (basis.type === 'dataCells') { - // The logic here is: if all rows are selected, then every selected column - // FULLY selected. Otherwise, no columns are fully selected. - // - // THIS LOGIC ASSUMES THAT ALL SELECTIONS ARE RECTANGULAR. The application - // enforces this assumption at various levels, but NOT within the Basis data - // structure. That is, it's theoretically possible to have basis data which - // represents a non-rectangular selection, but such data should be - // considered a bug. The logic within this function leverages this - // assumption for the purpose of improving performance. If we know that the - // selection is rectangular, then we can avoid iterating over all cells in - // each selected column to see if the column is fully selected. - return basis.rowIds.size < plane.rowIds.length - ? new ImmutableSet() - : basis.columnIds; + // The logic within this branch is somewhat complex because: + // - We want to suppor non-rectangular selections. + // - For performance, we want to avoid iterating over all the selected + // cells. + + const selctedRowCount = basis.rowIds.size; + const availableRowCount = plane.rowIds.length; + if (selctedRowCount < availableRowCount) { + // Performance heuristic. If the number of selected rows is less than the + // total number of rows, we can assume that no column exist in which all + // rows are selected. + return new ImmutableSet(); + } + + const selectedColumnCount = basis.columnIds.size; + const selectedCellCount = basis.cellIds.size; + const avgCellsSelectedPerColumn = selectedCellCount / selectedColumnCount; + if (avgCellsSelectedPerColumn === availableRowCount) { + // Performance heuristic. We know that no column can have more cells + // selected than the number of rows. Thus, if the average number of cells + // selected per column is equal to the number of rows, then we know that + // all selected columns are fully selected. + return basis.columnIds; + } + + // This is the worst-case scenario, performance-wise, which is why we try to + // return early before hitting this branch. This case will only happen when + // we have a mix of fully selected columns and partially selected columns. + // This case should be rare because most selections are rectangular. + const countSelectedCellsPerColumn = new Map(); + for (const cellId of basis.cellIds) { + const { columnId } = parseCellId(cellId); + const count = countSelectedCellsPerColumn.get(columnId) ?? 0; + countSelectedCellsPerColumn.set(columnId, count + 1); + } + const fullySelectedColumnIds = execPipe( + countSelectedCellsPerColumn, + filter(([, count]) => count === availableRowCount), + map(([id]) => id), + ); + return new ImmutableSet(fullySelectedColumnIds); } + if (basis.type === 'emptyColumns') { return basis.columnIds; } + if (basis.type === 'placeholderCell') { return new ImmutableSet(); } + if (basis.type === 'empty') { return new ImmutableSet(); } + return assertExhaustive(basis.type); } @@ -139,7 +170,7 @@ export default class SheetSelection { private readonly basis: Basis; - /** Ids of columns in which _all_ cells are selected */ + /** Ids of columns in which _all_ data cells are selected */ fullySelectedColumnIds: ImmutableSet; constructor(plane: Plane = new Plane(), basis: Basis = emptyBasis()) { From 70927ff2380a8376755be85347667f8edc48335f Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Mon, 11 Sep 2023 13:49:18 -0400 Subject: [PATCH 17/85] Refactor ExtractColumnsModal to use new selection --- mathesar_ui/src/components/sheet/cellIds.ts | 14 +++++ .../src/components/sheet/selection/Plane.ts | 15 +----- .../sheet/selection/SheetSelection.ts | 21 +++++++- mathesar_ui/src/stores/table-data/index.ts | 1 - .../src/stores/table-data/tabularData.ts | 54 ++----------------- .../ExtractColumnsModal.svelte | 12 ++--- 6 files changed, 44 insertions(+), 73 deletions(-) diff --git a/mathesar_ui/src/components/sheet/cellIds.ts b/mathesar_ui/src/components/sheet/cellIds.ts index 40c8297dea..fd6a263637 100644 --- a/mathesar_ui/src/components/sheet/cellIds.ts +++ b/mathesar_ui/src/components/sheet/cellIds.ts @@ -1,3 +1,7 @@ +import { map } from 'iter-tools'; + +import { cartesianProduct } from '@mathesar/utils/iterUtils'; + const CELL_ID_DELIMITER = '-'; /** @@ -22,3 +26,13 @@ export function parseCellId(cellId: string): { const columnId = cellId.slice(delimiterIndex + 1); return { rowId, columnId }; } + +export function makeCells( + rowIds: Iterable, + columnIds: Iterable, +): Iterable { + return map( + ([rowId, columnId]) => makeCellId(rowId, columnId), + cartesianProduct(rowIds, columnIds), + ); +} diff --git a/mathesar_ui/src/components/sheet/selection/Plane.ts b/mathesar_ui/src/components/sheet/selection/Plane.ts index 3f0d87780f..0c57de5c5b 100644 --- a/mathesar_ui/src/components/sheet/selection/Plane.ts +++ b/mathesar_ui/src/components/sheet/selection/Plane.ts @@ -1,20 +1,7 @@ -import { map } from 'iter-tools'; - -import { cartesianProduct } from '@mathesar/utils/iterUtils'; -import { makeCellId, parseCellId } from '../cellIds'; +import { makeCellId, makeCells, parseCellId } from '../cellIds'; import { Direction, getColumnOffset, getRowOffset } from './Direction'; import Series from './Series'; -function makeCells( - rowIds: Iterable, - columnIds: Iterable, -): Iterable { - return map( - ([rowId, columnId]) => makeCellId(rowId, columnId), - cartesianProduct(rowIds, columnIds), - ); -} - /** * This describes the different kinds of cells that can be adjacent to a given * cell in a particular direction. diff --git a/mathesar_ui/src/components/sheet/selection/SheetSelection.ts b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts index 3cf039710e..53318a7af3 100644 --- a/mathesar_ui/src/components/sheet/selection/SheetSelection.ts +++ b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts @@ -2,7 +2,7 @@ import { execPipe, filter, first, map } from 'iter-tools'; import { ImmutableSet } from '@mathesar/component-library'; import { assertExhaustive } from '@mathesar/utils/typeUtils'; -import { parseCellId } from '../cellIds'; +import { makeCells, parseCellId } from '../cellIds'; import { Direction, getColumnOffset } from './Direction'; import Plane from './Plane'; @@ -260,6 +260,17 @@ export default class SheetSelection { return this.withBasis(newBasis); } + /** + * @returns a new selection of all data cells in the intersection of the + * provided rows and columns. + */ + ofRowColumnIntersection( + rowIds: Iterable, + columnIds: Iterable, + ): SheetSelection { + return this.withBasis(basisFromDataCells(makeCells(rowIds, columnIds))); + } + /** * @returns a new selection formed by the rectangle between the provided * cells, inclusive. @@ -291,6 +302,14 @@ export default class SheetSelection { return this.withBasis(makeBasis(cellId)); } + ofOneRow(rowId: string): SheetSelection { + return this.ofRowRange(rowId, rowId); + } + + ofOneColumn(columnId: string): SheetSelection { + return this.ofColumnRange(columnId, columnId); + } + /** * @returns a new selection that fits within the provided plane. This is * useful when a column is deleted, reordered, or inserted. diff --git a/mathesar_ui/src/stores/table-data/index.ts b/mathesar_ui/src/stores/table-data/index.ts index 279ba7d931..2d68b467ac 100644 --- a/mathesar_ui/src/stores/table-data/index.ts +++ b/mathesar_ui/src/stores/table-data/index.ts @@ -42,7 +42,6 @@ export { getTabularDataStoreFromContext, TabularData, type TabularDataProps, - type TabularDataSelection, } from './tabularData'; export { type ProcessedColumn, diff --git a/mathesar_ui/src/stores/table-data/tabularData.ts b/mathesar_ui/src/stores/table-data/tabularData.ts index 820db37e0e..0e81eb4775 100644 --- a/mathesar_ui/src/stores/table-data/tabularData.ts +++ b/mathesar_ui/src/stores/table-data/tabularData.ts @@ -1,34 +1,24 @@ import { getContext, setContext } from 'svelte'; -import { - derived, - get, - writable, - type Readable, - type Writable, -} from 'svelte/store'; +import { derived, writable, type Readable, type Writable } from 'svelte/store'; import type { DBObjectEntry } from '@mathesar/AppTypes'; import type { TableEntry } from '@mathesar/api/types/tables'; import type { Column } from '@mathesar/api/types/tables/columns'; import { States } from '@mathesar/api/utils/requestUtils'; -import { LegacySheetSelection } from '@mathesar/components/sheet'; import Plane from '@mathesar/components/sheet/selection/Plane'; import Series from '@mathesar/components/sheet/selection/Series'; import SheetSelection from '@mathesar/components/sheet/selection/SheetSelection'; import type { AbstractTypesMap } from '@mathesar/stores/abstract-types/types'; -import { getColumnOrder, orderProcessedColumns } from '@mathesar/utils/tables'; import type { ShareConsumer } from '@mathesar/utils/shares'; +import { orderProcessedColumns } from '@mathesar/utils/tables'; import { ColumnsDataStore } from './columns'; import type { ConstraintsData } from './constraints'; import { ConstraintsDataStore } from './constraints'; import { Display } from './display'; import { Meta } from './meta'; -import type { - ProcessedColumn, - ProcessedColumnsStore, -} from './processedColumns'; +import type { ProcessedColumnsStore } from './processedColumns'; import { processColumn } from './processedColumns'; -import type { RecordRow, TableRecordsData } from './records'; +import type { TableRecordsData } from './records'; import { RecordsData } from './records'; export interface TabularDataProps { @@ -50,11 +40,6 @@ export interface TabularDataProps { >[0]['hasEnhancedPrimaryKeyCell']; } -export type TabularDataSelection = LegacySheetSelection< - RecordRow, - ProcessedColumn ->; - export class TabularData { id: DBObjectEntry['id']; @@ -75,13 +60,6 @@ export class TabularData { isLoading: Readable; - /** - * TODO_3037 - * - * @deprecated - */ - legacySelection: TabularDataSelection; - selection: Writable; table: TableEntry; @@ -159,29 +137,6 @@ export class TabularData { plane.subscribe((p) => this.selection.update((s) => s.forNewPlane(p))), ); - this.legacySelection = new LegacySheetSelection({ - getColumns: () => [...get(this.processedColumns).values()], - getColumnOrder: () => - getColumnOrder([...get(this.processedColumns).values()], this.table), - getRows: () => this.recordsData.getRecordRows(), - getMaxSelectionRowIndex: () => { - const totalCount = get(this.recordsData.totalCount) ?? 0; - const savedRecords = get(this.recordsData.savedRecords); - const newRecords = get(this.recordsData.newRecords); - const pagination = get(this.meta.pagination); - const { offset } = pagination; - const pageSize = pagination.size; - /** - * We are not subtracting 1 from the below maxRowIndex calculation - * inorder to account for the add-new-record placeholder row - */ - return ( - Math.min(pageSize, totalCount - offset, savedRecords.length) + - newRecords.length - ); - }, - }); - this.isLoading = derived( [ this.columnsDataStore.fetchStatus, @@ -268,7 +223,6 @@ export class TabularData { this.recordsData.destroy(); this.constraintsDataStore.destroy(); this.columnsDataStore.destroy(); - this.legacySelection.destroy(); this.cleanupFunctions.forEach((f) => f()); } } diff --git a/mathesar_ui/src/systems/table-view/table-inspector/column/column-extraction/ExtractColumnsModal.svelte b/mathesar_ui/src/systems/table-view/table-inspector/column/column-extraction/ExtractColumnsModal.svelte index ed5d468f15..f1fb68cfa3 100644 --- a/mathesar_ui/src/systems/table-view/table-inspector/column/column-extraction/ExtractColumnsModal.svelte +++ b/mathesar_ui/src/systems/table-view/table-inspector/column/column-extraction/ExtractColumnsModal.svelte @@ -48,11 +48,7 @@ export let controller: ExtractColumnsModalController; - $: ({ - processedColumns, - constraintsDataStore, - legacySelection: selection, - } = $tabularData); + $: ({ processedColumns, constraintsDataStore, selection } = $tabularData); $: ({ constraints } = $constraintsDataStore); $: availableProcessedColumns = [...$processedColumns.values()]; $: ({ targetType, columns, isOpen } = controller); @@ -110,7 +106,9 @@ // unmounting this component. return; } - selection.intersectSelectedRowsWithGivenColumns(_columns); + // TODO_3037 test to verify that selected columns are updated + const columnIds = _columns.map((c) => String(c.id)); + selection.update((s) => s.ofRowColumnIntersection(s.rowIds, columnIds)); } $: handleColumnsChange($columns); @@ -193,7 +191,7 @@ // will need to modify this logic when we position the new column where // the old columns were. const newFkColumn = allColumns.slice(-1)[0]; - selection.toggleColumnSelection(newFkColumn); + selection.update((s) => s.ofOneColumn(String(newFkColumn.id))); await tick(); scrollBasedOnSelection(); } From 9bc1e5ed2a05b69dadc81567a2350cb7a978389e Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Mon, 11 Sep 2023 15:03:38 -0400 Subject: [PATCH 18/85] Deprecate selection within Data Explorer --- .../src/stores/table-data/tabularData.ts | 2 +- .../src/systems/data-explorer/QueryRunner.ts | 78 +++++++++++++------ .../exploration-inspector/CellTab.svelte | 2 +- .../column-tab/ColumnTab.svelte | 6 +- .../data-explorer/result-pane/Results.svelte | 2 +- 5 files changed, 64 insertions(+), 26 deletions(-) diff --git a/mathesar_ui/src/stores/table-data/tabularData.ts b/mathesar_ui/src/stores/table-data/tabularData.ts index 0e81eb4775..a16572f80f 100644 --- a/mathesar_ui/src/stores/table-data/tabularData.ts +++ b/mathesar_ui/src/stores/table-data/tabularData.ts @@ -64,7 +64,7 @@ export class TabularData { table: TableEntry; - cleanupFunctions: (() => void)[] = []; + private cleanupFunctions: (() => void)[] = []; shareConsumer?: ShareConsumer; diff --git a/mathesar_ui/src/systems/data-explorer/QueryRunner.ts b/mathesar_ui/src/systems/data-explorer/QueryRunner.ts index ae6c285652..e2089bee6d 100644 --- a/mathesar_ui/src/systems/data-explorer/QueryRunner.ts +++ b/mathesar_ui/src/systems/data-explorer/QueryRunner.ts @@ -1,29 +1,33 @@ -import { derived, get, writable } from 'svelte/store'; import type { Readable, Writable } from 'svelte/store'; -import type { RequestStatus } from '@mathesar/api/utils/requestUtils'; -import { ApiMultiError } from '@mathesar/api/utils/errors'; -import { ImmutableMap, CancellablePromise } from '@mathesar-component-library'; -import Pagination from '@mathesar/utils/Pagination'; +import { derived, get, writable } from 'svelte/store'; + +import { CancellablePromise, ImmutableMap } from '@mathesar-component-library'; import type { + QueryColumnMetaData, QueryResultRecord, - QueryRunResponse, QueryResultsResponse, - QueryColumnMetaData, + QueryRunResponse, } from '@mathesar/api/types/queries'; -import { runQuery, fetchQueryResults } from '@mathesar/stores/queries'; +import { ApiMultiError } from '@mathesar/api/utils/errors'; +import type { RequestStatus } from '@mathesar/api/utils/requestUtils'; import { LegacySheetSelection } from '@mathesar/components/sheet'; +import Plane from '@mathesar/components/sheet/selection/Plane'; +import Series from '@mathesar/components/sheet/selection/Series'; +import SheetSelection from '@mathesar/components/sheet/selection/SheetSelection'; import type { AbstractTypesMap } from '@mathesar/stores/abstract-types/types'; +import { fetchQueryResults, runQuery } from '@mathesar/stores/queries'; +import Pagination from '@mathesar/utils/Pagination'; import type { ShareConsumer } from '@mathesar/utils/shares'; -import type QueryModel from './QueryModel'; import QueryInspector from './QueryInspector'; +import type QueryModel from './QueryModel'; import { - processColumnMetaData, getProcessedOutputColumns, + processColumnMetaData, speculateColumnMetaData, + type InputColumnsStoreSubstance, type ProcessedQueryOutputColumn, - type ProcessedQueryResultColumnMap, type ProcessedQueryOutputColumnMap, - type InputColumnsStoreSubstance, + type ProcessedQueryResultColumnMap, } from './utils'; export interface QueryRow { @@ -40,6 +44,7 @@ export interface QueryRowsData { rows: QueryRow[]; } +/** @deprecated TODO_3037 remove */ export type QuerySheetSelection = LegacySheetSelection< QueryRow, ProcessedQueryOutputColumn @@ -70,7 +75,10 @@ export default class QueryRunner { /** Keys are row ids, values are records */ selectableRowsMap: Readable>>; - selection: QuerySheetSelection; + /** @deprecated TODO_3037 remove */ + legacySelection: QuerySheetSelection; + + selection: Writable; inspector: QueryInspector; @@ -84,6 +92,8 @@ export default class QueryRunner { private shareConsumer?: ShareConsumer; + private cleanupFunctions: (() => void)[] = []; + constructor({ query, abstractTypeMap, @@ -111,7 +121,8 @@ export default class QueryRunner { this.rowsData, ({ rows }) => new Map(rows.map((r) => [getRowSelectionId(r), r.record])), ); - this.selection = new LegacySheetSelection({ + + this.legacySelection = new LegacySheetSelection({ getColumns: () => [...get(this.processedColumns).values()], getColumnOrder: () => [...get(this.processedColumns).values()].map((column) => column.id), @@ -125,6 +136,27 @@ export default class QueryRunner { return Math.min(pageSize, totalCount - offset, rowLength) - 1; }, }); + + const plane = derived( + [this.rowsData, this.processedColumns], + ([{ rows }, columnsMap]) => { + const rowIds = new Series(rows.map(getRowSelectionId)); + const columns = [...columnsMap.values()]; + const columnIds = new Series(columns.map((c) => String(c.id))); + return new Plane(rowIds, columnIds); + }, + ); + + this.selection = writable(new SheetSelection()); + + this.cleanupFunctions.push( + plane.subscribe((p) => this.selection.update((s) => s.forNewPlane(p))), + ); + + this.cleanupFunctions.push( + plane.subscribe((p) => this.selection.update((s) => s.forNewPlane(p))), + ); + this.inspector = new QueryInspector(this.query); } @@ -237,7 +269,7 @@ export default class QueryRunner { async setPagination(pagination: Pagination): Promise { this.pagination.set(pagination); await this.run(); - this.selection.activateFirstCellInSelectedColumn(); + this.legacySelection.activateFirstCellInSelectedColumn(); } protected resetPagination(): void { @@ -262,30 +294,31 @@ export default class QueryRunner { protected async resetPaginationAndRun(): Promise { this.resetPagination(); await this.run(); - this.selection.activateFirstCellInSelectedColumn(); + this.legacySelection.activateFirstCellInSelectedColumn(); } selectColumn(alias: QueryColumnMetaData['alias']): void { const processedColumn = get(this.processedColumns).get(alias); if (!processedColumn) { - this.selection.resetSelection(); - this.selection.selectAndActivateFirstCellIfExists(); + this.legacySelection.resetSelection(); + this.legacySelection.selectAndActivateFirstCellIfExists(); this.inspector.selectCellTab(); return; } - const isSelected = this.selection.toggleColumnSelection(processedColumn); + const isSelected = + this.legacySelection.toggleColumnSelection(processedColumn); if (isSelected) { this.inspector.selectColumnTab(); return; } - this.selection.activateFirstCellInSelectedColumn(); + this.legacySelection.activateFirstCellInSelectedColumn(); this.inspector.selectCellTab(); } clearSelection(): void { - this.selection.resetSelection(); + this.legacySelection.resetSelection(); } getRows(): QueryRow[] { @@ -297,7 +330,8 @@ export default class QueryRunner { } destroy(): void { - this.selection.destroy(); + this.legacySelection.destroy(); this.runPromise?.cancel(); + this.cleanupFunctions.forEach((fn) => fn()); } } diff --git a/mathesar_ui/src/systems/data-explorer/exploration-inspector/CellTab.svelte b/mathesar_ui/src/systems/data-explorer/exploration-inspector/CellTab.svelte index e6fccb7554..e90240883c 100644 --- a/mathesar_ui/src/systems/data-explorer/exploration-inspector/CellTab.svelte +++ b/mathesar_ui/src/systems/data-explorer/exploration-inspector/CellTab.svelte @@ -3,7 +3,7 @@ import type QueryRunner from '../QueryRunner'; export let queryHandler: QueryRunner; - $: ({ selection, processedColumns } = queryHandler); + $: ({ legacySelection: selection, processedColumns } = queryHandler); $: ({ activeCell } = selection); $: selectedCellValue = (() => { diff --git a/mathesar_ui/src/systems/data-explorer/exploration-inspector/column-tab/ColumnTab.svelte b/mathesar_ui/src/systems/data-explorer/exploration-inspector/column-tab/ColumnTab.svelte index c7c3d42332..0f1847c6d1 100644 --- a/mathesar_ui/src/systems/data-explorer/exploration-inspector/column-tab/ColumnTab.svelte +++ b/mathesar_ui/src/systems/data-explorer/exploration-inspector/column-tab/ColumnTab.svelte @@ -14,7 +14,11 @@ $: queryManager = queryHandler instanceof QueryManager ? queryHandler : undefined; - $: ({ selection, columnsMetaData, processedColumns } = queryHandler); + $: ({ + legacySelection: selection, + columnsMetaData, + processedColumns, + } = queryHandler); $: ({ selectedCells, columnsSelectedWhenTheTableIsEmpty } = selection); $: selectedColumns = (() => { const ids = selection.getSelectedUniqueColumnsId( diff --git a/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte b/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte index da32a65f4d..8004a6bd0b 100644 --- a/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte +++ b/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte @@ -37,7 +37,7 @@ selectableRowsMap, pagination, runState, - selection, + legacySelection: selection, inspector, } = queryHandler); $: ({ initial_columns } = $query); From aeae784729b10752f03df40923dbe9a93697c60b Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Mon, 11 Sep 2023 20:26:59 -0400 Subject: [PATCH 19/85] Use new Selection code in data explorer cell tab --- .../src/systems/data-explorer/QueryRunner.ts | 6 +-- .../exploration-inspector/CellTab.svelte | 49 +++++++------------ 2 files changed, 18 insertions(+), 37 deletions(-) diff --git a/mathesar_ui/src/systems/data-explorer/QueryRunner.ts b/mathesar_ui/src/systems/data-explorer/QueryRunner.ts index e2089bee6d..7e69371554 100644 --- a/mathesar_ui/src/systems/data-explorer/QueryRunner.ts +++ b/mathesar_ui/src/systems/data-explorer/QueryRunner.ts @@ -35,7 +35,7 @@ export interface QueryRow { rowIndex: number; } -function getRowSelectionId(row: QueryRow): string { +export function getRowSelectionId(row: QueryRow): string { return String(row.rowIndex); } @@ -321,10 +321,6 @@ export default class QueryRunner { this.legacySelection.resetSelection(); } - getRows(): QueryRow[] { - return get(this.rowsData).rows; - } - getQueryModel(): QueryModel { return get(this.query); } diff --git a/mathesar_ui/src/systems/data-explorer/exploration-inspector/CellTab.svelte b/mathesar_ui/src/systems/data-explorer/exploration-inspector/CellTab.svelte index e90240883c..dfc26ed3d4 100644 --- a/mathesar_ui/src/systems/data-explorer/exploration-inspector/CellTab.svelte +++ b/mathesar_ui/src/systems/data-explorer/exploration-inspector/CellTab.svelte @@ -1,48 +1,33 @@ -
- {#if selectedCellValue !== undefined} +
+ {#if cellValue !== undefined}
Content
- {#if processedQueryColumn} + {#if column} {/if}
From efa0dcb2fc2b71283bd831db94e5e623769bd6cc Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Mon, 11 Sep 2023 20:58:09 -0400 Subject: [PATCH 20/85] Use new Selection code in DE col tab and results --- .../column-tab/ColumnTab.svelte | 34 +++---- .../result-pane/ResultRowCell.svelte | 96 ++++++++----------- .../data-explorer/result-pane/Results.svelte | 13 +-- 3 files changed, 55 insertions(+), 88 deletions(-) diff --git a/mathesar_ui/src/systems/data-explorer/exploration-inspector/column-tab/ColumnTab.svelte b/mathesar_ui/src/systems/data-explorer/exploration-inspector/column-tab/ColumnTab.svelte index 0f1847c6d1..e37d358fdc 100644 --- a/mathesar_ui/src/systems/data-explorer/exploration-inspector/column-tab/ColumnTab.svelte +++ b/mathesar_ui/src/systems/data-explorer/exploration-inspector/column-tab/ColumnTab.svelte @@ -1,12 +1,15 @@ - +
{#if row || recordRunState === 'processing'} { - if (row) { - selection.activateCell(row, processedQueryColumn); - inspector.selectCellTab(); - } + // // TODO_3037 + // if (row) { + // selection.activateCell(row, processedQueryColumn); + // inspector.selectCellTab(); + // } }} on:onSelectionStart={() => { - if (row) { - selection.onStartSelection(row, processedQueryColumn); - } + // // TODO_3037 + // if (row) { + // selection.onStartSelection(row, processedQueryColumn); + // } }} on:onMouseEnterCellWhileSelection={() => { - if (row) { - // This enables the click + drag to - // select multiple cells - selection.onMouseEnterCellWhileSelection(row, processedQueryColumn); - } + // // TODO_3037 + // if (row) { + // // This enables the click + drag to + // // select multiple cells + // selection.onMouseEnterCellWhileSelection(row, processedQueryColumn); + // } }} /> {/if} diff --git a/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte b/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte index 8004a6bd0b..63370db7c0 100644 --- a/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte +++ b/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte @@ -10,7 +10,6 @@ SheetHeader, SheetRow, SheetVirtualRows, - isColumnSelected, } from '@mathesar/components/sheet'; import { SheetClipboardHandler } from '@mathesar/components/sheet/SheetClipboardHandler'; import { rowHeaderWidthPx, rowHeightPx } from '@mathesar/geometry'; @@ -37,7 +36,7 @@ selectableRowsMap, pagination, runState, - legacySelection: selection, + selection, inspector, } = queryHandler); $: ({ initial_columns } = $query); @@ -53,7 +52,7 @@ }), showToastInfo: toast.info, }); - $: ({ selectedCells, columnsSelectedWhenTheTableIsEmpty } = selection); + $: ({ columnIds } = $selection); $: recordRunState = $runState?.state; $: errors = $runState?.state === 'failure' ? $runState.errors : undefined; $: columnList = [...$processedColumns.values()]; @@ -102,11 +101,7 @@ {/each} @@ -139,8 +134,8 @@ {#each columnList as processedQueryColumn (processedQueryColumn.id)} Date: Mon, 11 Sep 2023 21:24:54 -0400 Subject: [PATCH 21/85] Remove lots of dead code --- .../components/sheet/LegacySheetSelection.ts | 731 ------------------ mathesar_ui/src/components/sheet/index.ts | 8 +- .../components/sheet/sheetScollingUtils.ts | 49 ++ .../systems/data-explorer/ActionsPane.svelte | 1 - .../src/systems/data-explorer/QueryRunner.ts | 53 +- .../column-tab/DeleteColumnAction.svelte | 1 - 6 files changed, 52 insertions(+), 791 deletions(-) delete mode 100644 mathesar_ui/src/components/sheet/LegacySheetSelection.ts create mode 100644 mathesar_ui/src/components/sheet/sheetScollingUtils.ts diff --git a/mathesar_ui/src/components/sheet/LegacySheetSelection.ts b/mathesar_ui/src/components/sheet/LegacySheetSelection.ts deleted file mode 100644 index 0aeeeb31c6..0000000000 --- a/mathesar_ui/src/components/sheet/LegacySheetSelection.ts +++ /dev/null @@ -1,731 +0,0 @@ -import { ImmutableSet, WritableSet } from '@mathesar-component-library'; -import { get, writable, type Unsubscriber, type Writable } from 'svelte/store'; - -export interface SelectionColumn { - id: number | string; -} - -export interface SelectionRow { - rowIndex: number; -} - -// TODO: Select active cell using primary key instead of index -// Checkout scenarios with pk consisting multiple columns -export interface ActiveCell { - rowIndex: number; - columnId: number | string; -} - -enum Direction { - Up = 'up', - Down = 'down', - Left = 'left', - Right = 'right', -} - -function getDirection(event: KeyboardEvent): Direction | undefined { - const { key } = event; - const shift = event.shiftKey; - switch (true) { - case shift && key === 'Tab': - return Direction.Left; - case shift: - return undefined; - case key === 'ArrowUp': - return Direction.Up; - case key === 'ArrowDown': - return Direction.Down; - case key === 'ArrowLeft': - return Direction.Left; - case key === 'ArrowRight': - case key === 'Tab': - return Direction.Right; - default: - return undefined; - } -} - -function getHorizontalDelta(direction: Direction): number { - switch (direction) { - case Direction.Left: - return -1; - case Direction.Right: - return 1; - default: - return 0; - } -} - -function getVerticalDelta(direction: Direction): number { - switch (direction) { - case Direction.Up: - return -1; - case Direction.Down: - return 1; - default: - return 0; - } -} - -export function isCellActive( - activeCell: ActiveCell, - row: SelectionRow, - column: SelectionColumn, -): boolean { - return ( - activeCell && - activeCell?.columnId === column.id && - activeCell.rowIndex === row.rowIndex - ); -} - -// TODO: Create a common utility action to handle active element based scroll -function scrollToElement(htmlElement: HTMLElement | null): void { - const activeRow = htmlElement?.parentElement; - const container = document.querySelector('[data-sheet-body-element="list"]'); - if (!container || !activeRow) { - return; - } - // Vertical scroll - if ( - activeRow.offsetTop + activeRow.clientHeight + 40 > - container.scrollTop + container.clientHeight - ) { - const offsetValue: number = - container.getBoundingClientRect().bottom - - activeRow.getBoundingClientRect().bottom - - 40; - container.scrollTop -= offsetValue; - } else if (activeRow.offsetTop - 30 < container.scrollTop) { - container.scrollTop = activeRow.offsetTop - 30; - } - - // Horizontal scroll - if ( - htmlElement.offsetLeft + activeRow.clientWidth + 30 > - container.scrollLeft + container.clientWidth - ) { - const offsetValue: number = - container.getBoundingClientRect().right - - htmlElement.getBoundingClientRect().right - - 30; - container.scrollLeft -= offsetValue; - } else if (htmlElement.offsetLeft - 30 < container.scrollLeft) { - container.scrollLeft = htmlElement.offsetLeft - 30; - } -} - -export function scrollBasedOnActiveCell(): void { - const activeCell: HTMLElement | null = document.querySelector( - '[data-sheet-element="cell"].is-active', - ); - scrollToElement(activeCell); -} - -export function scrollBasedOnSelection(): void { - const selectedCell: HTMLElement | null = document.querySelector( - '[data-sheet-element="cell"].is-selected', - ); - scrollToElement(selectedCell); -} - -type SelectionBounds = { - startRowIndex: number; - endRowIndex: number; - startColumnId: number | string; - endColumnId: number | string; -}; - -type Cell = [ - Row, - Column, -]; - -const ROW_COLUMN_SEPARATOR = '-'; - -/** - * Creates Unique identifier for a cell using rowIndex and columnId - * Storing this identifier instead of an object {rowIndex: number, columnId: number} - * enables easier usage of the Set data type & faster equality checks - */ -const createSelectedCellIdentifier = ( - { rowIndex }: Pick, - { id }: Pick, -): string => `${rowIndex}${ROW_COLUMN_SEPARATOR}${id}`; - -export const isRowSelected = ( - selectedCells: ImmutableSet, - row: SelectionRow, -): boolean => - selectedCells - .valuesArray() - .some((cell) => cell.startsWith(`${row.rowIndex}-`)); - -export const isColumnSelected = ( - selectedCells: ImmutableSet, - columnsSelectedWhenTheTableIsEmpty: ImmutableSet, - column: Pick, -): boolean => - columnsSelectedWhenTheTableIsEmpty.has(column.id) || - selectedCells.valuesArray().some((cell) => cell.endsWith(`-${column.id}`)); - -export const isCellSelected = ( - selectedCells: ImmutableSet, - row: Pick, - column: Pick, -): boolean => selectedCells.has(createSelectedCellIdentifier(row, column)); - -// The following function is similar to splitting the string with max_split = 1. -// input "a-b-c" => output "b-c" -// input "a-b" => output "b" -function splitWithLimit(str: string): string { - const tokens = str.split(ROW_COLUMN_SEPARATOR); - return tokens.slice(1).join(ROW_COLUMN_SEPARATOR); -} - -function getSelectedColumnId(selectedCell: string): SelectionColumn['id'] { - const columnId = splitWithLimit(selectedCell); - const numericalColumnId = Number(columnId); - if (Number.isNaN(numericalColumnId)) { - return columnId; - } - return numericalColumnId; -} - -export function getSelectedRowIndex(selectedCell: string): number { - return Number(selectedCell.split(ROW_COLUMN_SEPARATOR)[0]); -} - -/** - * @deprecated - */ -export default class LegacySheetSelection< - Row extends SelectionRow, - Column extends SelectionColumn, -> { - private getColumns: () => Column[]; - - private getColumnOrder: () => string[] | number[]; - - private getRows: () => Row[]; - - // max index is inclusive - private getMaxSelectionRowIndex: () => number; - - activeCell: Writable; - - private selectionBounds: SelectionBounds | undefined; - - private activeCellUnsubscriber: Unsubscriber; - - selectedCells: WritableSet; - - /** - * When the table has a non-zero number of rows, we store the user's selection - * in the `selectedCells` store. But when the table has no rows (and thus no - * cells) we still need a way to select columns to configure the data types, - * so we use this store as a workaround. More elegant solutions are being - * discussed in [1732][1]. - * - * [1]: https://github.com/centerofci/mathesar/issues/1732 - */ - columnsSelectedWhenTheTableIsEmpty: WritableSet; - - freezeSelection: boolean; - - selectionInProgress: Writable; - - constructor(args: { - getColumns: () => Column[]; - getColumnOrder: () => string[] | number[]; - getRows: () => Row[]; - getMaxSelectionRowIndex: () => number; - }) { - this.selectedCells = new WritableSet(); - this.columnsSelectedWhenTheTableIsEmpty = new WritableSet(); - this.getColumns = args.getColumns; - this.getColumnOrder = args.getColumnOrder; - this.getRows = args.getRows; - this.getMaxSelectionRowIndex = args.getMaxSelectionRowIndex; - this.freezeSelection = false; - this.selectionInProgress = writable(false); - this.activeCell = writable(undefined); - - /** - * TODO: - * - This adds a document level event listener for each selection - * store instance, and the listener doesn't seem to get removed. - * - Refactor this logic and avoid such listeners within the store instance. - */ - // This event terminates the cell selection process - // specially useful when selecting multiple cells - // Adding this on document to enable boundry cells selection - // when the user drags the mouse out of the table view - document.addEventListener('mouseup', () => { - this.onEndSelection(); - }); - - // Keep active cell and selected cell in sync - this.activeCellUnsubscriber = this.activeCell.subscribe((activeCell) => { - if (activeCell) { - const activeCellRow = this.getRows().find( - (row) => row.rowIndex === activeCell.rowIndex, - ); - const activeCellColumn = this.getColumns().find( - (column) => column.id === activeCell.columnId, - ); - if (activeCellRow && activeCellColumn) { - /** - * This handles the very rare edge case - * when the user starts the selection using mouse - * but before ending(mouseup event) - * she change the active cell using keyboard - */ - this.selectionBounds = undefined; - this.selectMultipleCells([[activeCellRow, activeCellColumn]]); - } else { - // We need to unselect the Selected cells - // when navigating Placeholder cells - this.resetSelection(); - } - } else { - this.resetSelection(); - } - }); - } - - onStartSelection(row: SelectionRow, column: SelectionColumn): void { - if (this.freezeSelection) { - return; - } - - this.selectionInProgress.set(true); - // Initialize the bounds of the selection - this.selectionBounds = { - startColumnId: column.id, - endColumnId: column.id, - startRowIndex: row.rowIndex, - endRowIndex: row.rowIndex, - }; - - const cells = this.getIncludedCells(this.selectionBounds); - this.selectMultipleCells(cells); - } - - onMouseEnterCellWhileSelection( - row: SelectionRow, - column: SelectionColumn, - ): void { - const { rowIndex } = row; - const { id } = column; - - // If there is no selection start cell, - // this means the selection was never initiated - if (!this.selectionBounds || this.freezeSelection) { - return; - } - - this.selectionBounds.endRowIndex = rowIndex; - this.selectionBounds.endColumnId = id; - const cells = this.getIncludedCells(this.selectionBounds); - this.selectMultipleCells(cells); - } - - onEndSelection(): void { - if (this.selectionBounds) { - const cells = this.getIncludedCells(this.selectionBounds); - this.selectMultipleCells(cells); - this.selectionBounds = undefined; - } - this.selectionInProgress.set(false); - } - - selectAndActivateFirstCellIfExists(): void { - const firstRow = this.getRows()[0]; - const firstColumn = this.getColumns()[0]; - if (firstRow && firstColumn) { - this.selectMultipleCells([[firstRow, firstColumn]]); - this.activateCell(firstRow, firstColumn); - } - } - - selectAndActivateFirstDataEntryCellInLastRow(): void { - const currentRows = this.getRows(); - const currentColumns = this.getColumns(); - if (currentRows.length > 0 && currentColumns.length > 1) { - this.activateCell(currentRows[currentRows.length - 1], currentColumns[1]); - } - } - - getIncludedCells(selectionBounds: SelectionBounds): Cell[] { - const { startRowIndex, endRowIndex, startColumnId, endColumnId } = - selectionBounds; - const minRowIndex = Math.min(startRowIndex, endRowIndex); - const maxRowIndex = Math.max(startRowIndex, endRowIndex); - - const columnOrder = this.getColumnOrder(); - - const startOrderIndex = columnOrder.findIndex((id) => id === startColumnId); - const endOrderIndex = columnOrder.findIndex((id) => id === endColumnId); - - const minColumnPosition = Math.min(startOrderIndex, endOrderIndex); - const maxColumnPosition = Math.max(startOrderIndex, endOrderIndex); - const columnOrderSelected = columnOrder.slice( - minColumnPosition, - maxColumnPosition + 1, - ); - - const columns = this.getColumns(); - - const cells: Cell[] = []; - this.getRows().forEach((row) => { - const { rowIndex } = row; - if (rowIndex >= minRowIndex && rowIndex <= maxRowIndex) { - columnOrderSelected.forEach((columnId) => { - const column = columns.find((c) => c.id === columnId); - if (column) { - cells.push([row, column]); - } - }); - } - }); - - return cells; - } - - private selectMultipleCells(cells: Array>) { - const identifiers = cells.map(([row, column]) => - createSelectedCellIdentifier(row, column), - ); - this.selectedCells.reconstruct(identifiers); - } - - resetSelection(): void { - this.selectionBounds = undefined; - this.columnsSelectedWhenTheTableIsEmpty.clear(); - this.selectedCells.clear(); - } - - isCompleteColumnSelected(column: Pick): boolean { - if (this.getRows().length) { - return ( - this.columnsSelectedWhenTheTableIsEmpty.getHas(column.id) || - this.getRows().every((row) => - isCellSelected(get(this.selectedCells), row, column), - ) - ); - } - return this.columnsSelectedWhenTheTableIsEmpty.getHas(column.id); - } - - private isCompleteRowSelected(row: Pick): boolean { - const columns = this.getColumns(); - return ( - columns.length > 0 && - columns.every((column) => - isCellSelected(get(this.selectedCells), row, column), - ) - ); - } - - isAnyColumnCompletelySelected(): boolean { - const selectedCellsArray = get(this.selectedCells).valuesArray(); - const checkedColumns: (number | string)[] = []; - - for (const cell of selectedCellsArray) { - const columnId = getSelectedColumnId(cell); - if (!checkedColumns.includes(columnId)) { - if (this.isCompleteColumnSelected({ id: columnId })) { - return true; - } - checkedColumns.push(columnId); - } - } - - return false; - } - - isAnyRowCompletelySelected(): boolean { - const selectedCellsArray = get(this.selectedCells).valuesArray(); - const checkedRows: number[] = []; - - for (const cell of selectedCellsArray) { - const rowIndex = getSelectedRowIndex(cell); - if (!checkedRows.includes(rowIndex)) { - if (this.isCompleteRowSelected({ rowIndex })) { - return true; - } - checkedRows.push(rowIndex); - } - } - - return false; - } - - /** - * Modifies the selected cells, forming a new selection by maintaining the - * currently selected rows but altering the selected columns to match the - * supplied columns. - */ - intersectSelectedRowsWithGivenColumns(columns: Column[]): void { - const selectedRows = this.getSelectedUniqueRowsId( - new ImmutableSet(this.selectedCells.getValues()), - ); - const cells: Cell[] = []; - columns.forEach((column) => { - selectedRows.forEach((rowIndex) => { - const row = this.getRows()[rowIndex]; - cells.push([row, column]); - }); - }); - - this.selectMultipleCells(cells); - } - - /** - * Use this only for programmatic selection - * - * Prefer: onColumnSelectionStart when - * selection is done using - * user interactions - */ - toggleColumnSelection(column: Column): boolean { - const isCompleteColumnSelected = this.isCompleteColumnSelected(column); - this.activateCell({ rowIndex: 0 }, column); - - if (isCompleteColumnSelected) { - this.resetSelection(); - return false; - } - - const rows = this.getRows(); - - if (rows.length === 0) { - this.resetSelection(); - this.columnsSelectedWhenTheTableIsEmpty.add(column.id); - return true; - } - - const cells: Cell[] = []; - rows.forEach((row) => { - cells.push([row, column]); - }); - - // Clearing the selection - // since we do not have cmd+click to select - // disjointed cells - this.resetSelection(); - this.selectMultipleCells(cells); - return true; - } - - /** - * Use this only for programmatic selection - * - * Prefer: onRowSelectionStart when - * selection is done using - * user interactions - */ - toggleRowSelection(row: Row): void { - const isCompleteRowSelected = this.isCompleteRowSelected(row); - - if (isCompleteRowSelected) { - // Clear the selection - deselect the row - this.resetSelection(); - } else { - const cells: Cell[] = []; - this.getColumns().forEach((column) => { - cells.push([row, column]); - }); - - // Clearing the selection - // since we do not have cmd+click to select - // disjointed cells - this.resetSelection(); - this.selectMultipleCells(cells); - } - } - - onColumnSelectionStart(column: Column): boolean { - if (!this.isCompleteColumnSelected(column)) { - this.activateCell({ rowIndex: 0 }, { id: column.id }); - const rows = this.getRows(); - - if (rows.length === 0) { - this.resetSelection(); - this.columnsSelectedWhenTheTableIsEmpty.add(column.id); - return true; - } - - this.onStartSelection(rows[0], column); - this.onMouseEnterCellWhileSelection(rows[rows.length - 1], column); - } - return true; - } - - onMouseEnterColumnHeaderWhileSelection(column: Column): boolean { - const rows = this.getRows(); - - if (rows.length === 0) { - this.resetSelection(); - this.columnsSelectedWhenTheTableIsEmpty.add(column.id); - return true; - } - - this.onMouseEnterCellWhileSelection(rows[rows.length - 1], column); - return true; - } - - onRowSelectionStart(row: Row): boolean { - const columns = this.getColumns(); - const columnOrder = this.getColumnOrder(); - - if (!columns.length) { - // Not possible to have tables without columns - } - - const startColumnId = columnOrder[0]; - const endColumnId = columnOrder[columnOrder.length - 1]; - const startColumn = columns.find((c) => c.id === startColumnId); - const endColumn = columns.find((c) => c.id === endColumnId); - - if (startColumn && endColumn) { - this.activateCell(row, startColumn); - this.onStartSelection(row, startColumn); - this.onMouseEnterCellWhileSelection(row, endColumn); - } - - return true; - } - - onMouseEnterRowHeaderWhileSelection(row: Row): boolean { - const columns = this.getColumns(); - - if (!columns.length) { - // Not possible to have tables without columns - } - - const endColumn = columns[columns.length - 1]; - this.onMouseEnterCellWhileSelection(row, endColumn); - return true; - } - - resetActiveCell(): void { - this.activeCell.set(undefined); - } - - activateCell(row: Pick, column: Pick): void { - this.activeCell.set({ - rowIndex: row.rowIndex, - columnId: column.id, - }); - } - - focusCell(row: Pick, column: Pick): void { - const cellsInTheColumn = document.querySelectorAll( - `[data-column-identifier="${column.id}"]`, - ); - const targetCell = cellsInTheColumn.item(row.rowIndex); - (targetCell?.querySelector('.cell-wrapper') as HTMLElement)?.focus(); - } - - private getAdjacentCell( - activeCell: ActiveCell, - direction: Direction, - ): ActiveCell | undefined { - const columnOrder = this.getColumnOrder(); - - const rowIndex = (() => { - const delta = getVerticalDelta(direction); - if (delta === 0) { - return activeCell.rowIndex; - } - const minRowIndex = 0; - const maxRowIndex = this.getMaxSelectionRowIndex(); - const newRowIndex = activeCell.rowIndex + delta; - if (newRowIndex < minRowIndex || newRowIndex > maxRowIndex) { - return undefined; - } - return newRowIndex; - })(); - if (rowIndex === undefined) { - return undefined; - } - - const columnId = (() => { - const delta = getHorizontalDelta(direction); - if (delta === 0) { - return activeCell.columnId; - } - if (activeCell.columnId) { - const index = columnOrder.findIndex((id) => id === activeCell.columnId); - const targetId = columnOrder[index + delta]; - return targetId; - } - return ''; - })(); - if (!columnId) { - return undefined; - } - return { rowIndex, columnId }; - } - - handleKeyEventsOnActiveCell(key: KeyboardEvent): 'moved' | undefined { - const direction = getDirection(key); - if (!direction) { - return undefined; - } - let moved = false; - this.activeCell.update((activeCell) => { - if (!activeCell) { - return undefined; - } - const adjacentCell = this.getAdjacentCell(activeCell, direction); - if (adjacentCell) { - moved = true; - return adjacentCell; - } - return activeCell; - }); - - return moved ? 'moved' : undefined; - } - - activateFirstCellInSelectedColumn() { - const activeCell = get(this.activeCell); - if (activeCell) { - this.activateCell({ rowIndex: 0 }, { id: activeCell.columnId }); - } - } - - /** - * This method does not utilize class properties inorder - * to make it reactive during component usage. - * - * It is placed within the class inorder to make use of - * class types - */ - getSelectedUniqueColumnsId( - selectedCells: ImmutableSet, - columnsSelectedWhenTheTableIsEmpty: ImmutableSet, - ): Column['id'][] { - const setOfUniqueColumnIds = new Set([ - ...[...selectedCells].map(getSelectedColumnId), - ...columnsSelectedWhenTheTableIsEmpty, - ]); - return Array.from(setOfUniqueColumnIds); - } - - getSelectedUniqueRowsId( - selectedCells: ImmutableSet, - ): Row['rowIndex'][] { - const setOfUniqueRowIndex = new Set([ - ...[...selectedCells].map(getSelectedRowIndex), - ]); - return Array.from(setOfUniqueRowIndex); - } - - destroy(): void { - this.activeCellUnsubscriber(); - } -} diff --git a/mathesar_ui/src/components/sheet/index.ts b/mathesar_ui/src/components/sheet/index.ts index 0448df86aa..cae20e3d07 100644 --- a/mathesar_ui/src/components/sheet/index.ts +++ b/mathesar_ui/src/components/sheet/index.ts @@ -5,13 +5,7 @@ export { default as SheetPositionableCell } from './SheetPositionableCell.svelte export { default as SheetCellResizer } from './SheetCellResizer.svelte'; export { default as SheetVirtualRows } from './SheetVirtualRows.svelte'; export { default as SheetRow } from './SheetRow.svelte'; -export { default as LegacySheetSelection } from './LegacySheetSelection'; export { - isColumnSelected, - isRowSelected, - isCellSelected, - getSelectedRowIndex, - isCellActive, scrollBasedOnActiveCell, scrollBasedOnSelection, -} from './LegacySheetSelection'; +} from './sheetScollingUtils'; diff --git a/mathesar_ui/src/components/sheet/sheetScollingUtils.ts b/mathesar_ui/src/components/sheet/sheetScollingUtils.ts new file mode 100644 index 0000000000..130f87cefd --- /dev/null +++ b/mathesar_ui/src/components/sheet/sheetScollingUtils.ts @@ -0,0 +1,49 @@ +// TODO: Create a common utility action to handle active element based scroll +function scrollToElement(htmlElement: HTMLElement | null): void { + const activeRow = htmlElement?.parentElement; + const container = document.querySelector('[data-sheet-body-element="list"]'); + if (!container || !activeRow) { + return; + } + // Vertical scroll + if ( + activeRow.offsetTop + activeRow.clientHeight + 40 > + container.scrollTop + container.clientHeight + ) { + const offsetValue: number = + container.getBoundingClientRect().bottom - + activeRow.getBoundingClientRect().bottom - + 40; + container.scrollTop -= offsetValue; + } else if (activeRow.offsetTop - 30 < container.scrollTop) { + container.scrollTop = activeRow.offsetTop - 30; + } + + // Horizontal scroll + if ( + htmlElement.offsetLeft + activeRow.clientWidth + 30 > + container.scrollLeft + container.clientWidth + ) { + const offsetValue: number = + container.getBoundingClientRect().right - + htmlElement.getBoundingClientRect().right - + 30; + container.scrollLeft -= offsetValue; + } else if (htmlElement.offsetLeft - 30 < container.scrollLeft) { + container.scrollLeft = htmlElement.offsetLeft - 30; + } +} + +export function scrollBasedOnActiveCell(): void { + const activeCell: HTMLElement | null = document.querySelector( + '[data-sheet-element="cell"].is-active', + ); + scrollToElement(activeCell); +} + +export function scrollBasedOnSelection(): void { + const selectedCell: HTMLElement | null = document.querySelector( + '[data-sheet-element="cell"].is-selected', + ); + scrollToElement(selectedCell); +} diff --git a/mathesar_ui/src/systems/data-explorer/ActionsPane.svelte b/mathesar_ui/src/systems/data-explorer/ActionsPane.svelte index 77d06c808a..b13fc93274 100644 --- a/mathesar_ui/src/systems/data-explorer/ActionsPane.svelte +++ b/mathesar_ui/src/systems/data-explorer/ActionsPane.svelte @@ -50,7 +50,6 @@ void queryManager.update((q) => q.withBaseTable(tableEntry ? tableEntry.id : undefined), ); - queryManager.clearSelection(); linkCollapsibleOpenState = {}; } diff --git a/mathesar_ui/src/systems/data-explorer/QueryRunner.ts b/mathesar_ui/src/systems/data-explorer/QueryRunner.ts index 7e69371554..63a4cb6db0 100644 --- a/mathesar_ui/src/systems/data-explorer/QueryRunner.ts +++ b/mathesar_ui/src/systems/data-explorer/QueryRunner.ts @@ -10,7 +10,6 @@ import type { } from '@mathesar/api/types/queries'; import { ApiMultiError } from '@mathesar/api/utils/errors'; import type { RequestStatus } from '@mathesar/api/utils/requestUtils'; -import { LegacySheetSelection } from '@mathesar/components/sheet'; import Plane from '@mathesar/components/sheet/selection/Plane'; import Series from '@mathesar/components/sheet/selection/Series'; import SheetSelection from '@mathesar/components/sheet/selection/SheetSelection'; @@ -25,7 +24,6 @@ import { processColumnMetaData, speculateColumnMetaData, type InputColumnsStoreSubstance, - type ProcessedQueryOutputColumn, type ProcessedQueryOutputColumnMap, type ProcessedQueryResultColumnMap, } from './utils'; @@ -44,12 +42,6 @@ export interface QueryRowsData { rows: QueryRow[]; } -/** @deprecated TODO_3037 remove */ -export type QuerySheetSelection = LegacySheetSelection< - QueryRow, - ProcessedQueryOutputColumn ->; - type QueryRunMode = 'queryId' | 'queryObject'; export default class QueryRunner { @@ -75,9 +67,6 @@ export default class QueryRunner { /** Keys are row ids, values are records */ selectableRowsMap: Readable>>; - /** @deprecated TODO_3037 remove */ - legacySelection: QuerySheetSelection; - selection: Writable; inspector: QueryInspector; @@ -122,21 +111,6 @@ export default class QueryRunner { ({ rows }) => new Map(rows.map((r) => [getRowSelectionId(r), r.record])), ); - this.legacySelection = new LegacySheetSelection({ - getColumns: () => [...get(this.processedColumns).values()], - getColumnOrder: () => - [...get(this.processedColumns).values()].map((column) => column.id), - getRows: () => get(this.rowsData).rows, - getMaxSelectionRowIndex: () => { - const rowLength = get(this.rowsData).rows.length; - const totalCount = get(this.rowsData).totalCount ?? 0; - const pagination = get(this.pagination); - const { offset } = pagination; - const pageSize = pagination.size; - return Math.min(pageSize, totalCount - offset, rowLength) - 1; - }, - }); - const plane = derived( [this.rowsData, this.processedColumns], ([{ rows }, columnsMap]) => { @@ -153,10 +127,6 @@ export default class QueryRunner { plane.subscribe((p) => this.selection.update((s) => s.forNewPlane(p))), ); - this.cleanupFunctions.push( - plane.subscribe((p) => this.selection.update((s) => s.forNewPlane(p))), - ); - this.inspector = new QueryInspector(this.query); } @@ -269,7 +239,6 @@ export default class QueryRunner { async setPagination(pagination: Pagination): Promise { this.pagination.set(pagination); await this.run(); - this.legacySelection.activateFirstCellInSelectedColumn(); } protected resetPagination(): void { @@ -283,7 +252,6 @@ export default class QueryRunner { } protected resetResults(): void { - this.clearSelection(); this.runPromise?.cancel(); this.resetPagination(); this.rowsData.set({ totalCount: 0, rows: [] }); @@ -294,31 +262,15 @@ export default class QueryRunner { protected async resetPaginationAndRun(): Promise { this.resetPagination(); await this.run(); - this.legacySelection.activateFirstCellInSelectedColumn(); } selectColumn(alias: QueryColumnMetaData['alias']): void { const processedColumn = get(this.processedColumns).get(alias); if (!processedColumn) { - this.legacySelection.resetSelection(); - this.legacySelection.selectAndActivateFirstCellIfExists(); - this.inspector.selectCellTab(); - return; - } - - const isSelected = - this.legacySelection.toggleColumnSelection(processedColumn); - if (isSelected) { - this.inspector.selectColumnTab(); return; } - - this.legacySelection.activateFirstCellInSelectedColumn(); - this.inspector.selectCellTab(); - } - - clearSelection(): void { - this.legacySelection.resetSelection(); + this.selection.update((s) => s.ofOneColumn(processedColumn.id)); + this.inspector.selectColumnTab(); } getQueryModel(): QueryModel { @@ -326,7 +278,6 @@ export default class QueryRunner { } destroy(): void { - this.legacySelection.destroy(); this.runPromise?.cancel(); this.cleanupFunctions.forEach((fn) => fn()); } diff --git a/mathesar_ui/src/systems/data-explorer/exploration-inspector/column-tab/DeleteColumnAction.svelte b/mathesar_ui/src/systems/data-explorer/exploration-inspector/column-tab/DeleteColumnAction.svelte index 1b1c124bdb..2beb400e82 100644 --- a/mathesar_ui/src/systems/data-explorer/exploration-inspector/column-tab/DeleteColumnAction.svelte +++ b/mathesar_ui/src/systems/data-explorer/exploration-inspector/column-tab/DeleteColumnAction.svelte @@ -50,7 +50,6 @@ void queryManager.update((q) => q.withoutInitialColumns(selectedColumnAliases), ); - queryManager.clearSelection(); } From 9a9dbd8da4fc69c84ac2b355388b30efe137a6d4 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Mon, 11 Sep 2023 21:27:00 -0400 Subject: [PATCH 22/85] Rename `isSelectedInRange` to `isSelected` --- mathesar_ui/src/components/cell-fabric/CellFabric.svelte | 4 ++-- .../cell-fabric/data-types/components/CellWrapper.svelte | 4 ++-- .../data-types/components/SteppedInputCell.svelte | 4 ++-- .../components/__tests__/SteppedInputCell.test.ts | 2 +- .../data-types/components/array/ArrayCell.svelte | 4 ++-- .../data-types/components/checkbox/CheckboxCell.svelte | 4 ++-- .../data-types/components/date-time/DateTimeCell.svelte | 4 ++-- .../components/formatted-input/FormattedInputCell.svelte | 4 ++-- .../components/linked-record/LinkedRecordCell.svelte | 4 ++-- .../components/linked-record/LinkedRecordInput.svelte | 2 +- .../data-types/components/money/MoneyCell.svelte | 4 ++-- .../data-types/components/number/NumberCell.svelte | 4 ++-- .../data-types/components/primary-key/PrimaryKeyCell.svelte | 4 ++-- .../data-types/components/select/SingleSelectCell.svelte | 4 ++-- .../data-types/components/textarea/TextAreaCell.svelte | 4 ++-- .../data-types/components/textbox/TextBoxCell.svelte | 4 ++-- .../cell-fabric/data-types/components/typeDefinitions.ts | 2 +- .../cell-fabric/data-types/components/uri/UriCell.svelte | 4 ++-- .../systems/data-explorer/result-pane/ResultRowCell.svelte | 2 +- mathesar_ui/src/systems/table-view/row/RowCell.svelte | 6 +++--- 20 files changed, 37 insertions(+), 37 deletions(-) diff --git a/mathesar_ui/src/components/cell-fabric/CellFabric.svelte b/mathesar_ui/src/components/cell-fabric/CellFabric.svelte index 8a405123da..9046a74872 100644 --- a/mathesar_ui/src/components/cell-fabric/CellFabric.svelte +++ b/mathesar_ui/src/components/cell-fabric/CellFabric.svelte @@ -21,7 +21,7 @@ | ((recordId: string, recordSummary: string) => void) | undefined = undefined; export let isActive = false; - export let isSelectedInRange = false; + export let isSelected = false; export let disabled = false; export let showAsSkeleton = false; export let horizontalAlignment: HorizontalAlignment | undefined = undefined; @@ -56,7 +56,7 @@ this={component} {...props} {isActive} - {isSelectedInRange} + {isSelected} {disabled} {isIndependentOfSheet} {horizontalAlignment} diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte index a731edd395..ab639cc1d8 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte @@ -6,7 +6,7 @@ export let element: HTMLElement | undefined = undefined; export let isActive = false; - export let isSelectedInRange = false; + export let isSelected = false; export let disabled = false; export let mode: 'edit' | 'default' = 'default'; export let multiLineTruncate = false; @@ -116,7 +116,7 @@ {...$$restProps} > {#if mode !== 'edit'} - + ; export let isActive: Props['isActive']; - export let isSelectedInRange: Props['isSelectedInRange']; + export let isSelected: Props['isSelected']; export let value: Props['value']; export let disabled: Props['disabled']; export let multiLineTruncate = false; @@ -154,7 +154,7 @@ { class?: string; id?: string; diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/money/MoneyCell.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/money/MoneyCell.svelte index 54e9f7a556..ee81c06f06 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/money/MoneyCell.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/money/MoneyCell.svelte @@ -6,7 +6,7 @@ type $$Props = MoneyCellProps; export let isActive: $$Props['isActive']; - export let isSelectedInRange: $$Props['isSelectedInRange']; + export let isSelected: $$Props['isSelected']; export let value: $$Props['value']; export let disabled: $$Props['disabled']; export let searchValue: $$Props['searchValue'] = undefined; @@ -19,7 +19,7 @@ = ( export interface CellTypeProps { value: Value | null | undefined; isActive: boolean; - isSelectedInRange: boolean; + isSelected: boolean; disabled: boolean; searchValue?: unknown; isProcessing: boolean; diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/uri/UriCell.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/uri/UriCell.svelte index 08bfa74608..0ff4e5c8fd 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/uri/UriCell.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/uri/UriCell.svelte @@ -11,7 +11,7 @@ type $$Props = CellTypeProps; export let isActive: $$Props['isActive']; - export let isSelectedInRange: $$Props['isSelectedInRange']; + export let isSelected: $$Props['isSelected']; export let value: $$Props['value'] = undefined; export let disabled: $$Props['disabled']; export let searchValue: $$Props['searchValue'] = undefined; @@ -22,7 +22,7 @@ @@ -146,7 +146,7 @@ Date: Mon, 5 Feb 2024 20:26:58 -0500 Subject: [PATCH 23/85] Handle movement keys --- mathesar_ui/src/components/sheet/index.ts | 2 +- .../components/sheet/selection/Direction.ts | 22 ------- .../sheet/selection/SheetSelection.ts | 4 +- .../components/sheet/sheetKeyboardUtils.ts | 47 ++++++++++++++ ...collingUtils.ts => sheetScrollingUtils.ts} | 7 +++ mathesar_ui/src/components/sheet/utils.ts | 3 +- .../result-pane/ResultRowCell.svelte | 31 ++------- .../src/systems/table-view/row/RowCell.svelte | 26 ++------ mathesar_ui/src/utils/KeyboardShortcut.ts | 63 +++++++++++++++++++ 9 files changed, 131 insertions(+), 74 deletions(-) create mode 100644 mathesar_ui/src/components/sheet/sheetKeyboardUtils.ts rename mathesar_ui/src/components/sheet/{sheetScollingUtils.ts => sheetScrollingUtils.ts} (93%) create mode 100644 mathesar_ui/src/utils/KeyboardShortcut.ts diff --git a/mathesar_ui/src/components/sheet/index.ts b/mathesar_ui/src/components/sheet/index.ts index cae20e3d07..b0a1df34f4 100644 --- a/mathesar_ui/src/components/sheet/index.ts +++ b/mathesar_ui/src/components/sheet/index.ts @@ -8,4 +8,4 @@ export { default as SheetRow } from './SheetRow.svelte'; export { scrollBasedOnActiveCell, scrollBasedOnSelection, -} from './sheetScollingUtils'; +} from './sheetScrollingUtils'; diff --git a/mathesar_ui/src/components/sheet/selection/Direction.ts b/mathesar_ui/src/components/sheet/selection/Direction.ts index 2693dd9000..b53704f378 100644 --- a/mathesar_ui/src/components/sheet/selection/Direction.ts +++ b/mathesar_ui/src/components/sheet/selection/Direction.ts @@ -5,28 +5,6 @@ export enum Direction { Right = 'right', } -export function getDirection(event: KeyboardEvent): Direction | undefined { - const { key } = event; - const shift = event.shiftKey; - switch (true) { - case shift && key === 'Tab': - return Direction.Left; - case shift: - return undefined; - case key === 'ArrowUp': - return Direction.Up; - case key === 'ArrowDown': - return Direction.Down; - case key === 'ArrowLeft': - return Direction.Left; - case key === 'ArrowRight': - case key === 'Tab': - return Direction.Right; - default: - return undefined; - } -} - export function getColumnOffset(direction: Direction): number { switch (direction) { case Direction.Left: diff --git a/mathesar_ui/src/components/sheet/selection/SheetSelection.ts b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts index 53318a7af3..7c11c67476 100644 --- a/mathesar_ui/src/components/sheet/selection/SheetSelection.ts +++ b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts @@ -480,6 +480,8 @@ export default class SheetSelection { * is simpler. */ resized(direction: Direction): SheetSelection { - throw new Error('Not implemented'); + // TODO + console.log('Sheet selection resizing is not yet implemented'); + return this; } } diff --git a/mathesar_ui/src/components/sheet/sheetKeyboardUtils.ts b/mathesar_ui/src/components/sheet/sheetKeyboardUtils.ts new file mode 100644 index 0000000000..f2035c747b --- /dev/null +++ b/mathesar_ui/src/components/sheet/sheetKeyboardUtils.ts @@ -0,0 +1,47 @@ +import type { Writable } from 'svelte/store'; + +import { KeyboardShortcut } from '@mathesar/utils/KeyboardShortcut'; +import { Direction } from './selection/Direction'; +import type SheetSelection from './selection/SheetSelection'; +import { autoScroll } from './sheetScrollingUtils'; + +function move(selection: Writable, direction: Direction) { + selection.update((s) => s.collapsedAndMoved(direction)); + void autoScroll(); +} + +function resize(selection: Writable, direction: Direction) { + selection.update((s) => s.resized(direction)); + void autoScroll(); +} + +function key(...args: Parameters) { + return KeyboardShortcut.fromKey(...args).toString(); +} + +const shortcutMapData: [string, (s: Writable) => void][] = [ + [key('ArrowUp'), (s) => move(s, Direction.Up)], + [key('ArrowDown'), (s) => move(s, Direction.Down)], + [key('ArrowLeft'), (s) => move(s, Direction.Left)], + [key('ArrowRight'), (s) => move(s, Direction.Right)], + [key('Tab'), (s) => move(s, Direction.Right)], + [key('Tab', ['Shift']), (s) => move(s, Direction.Left)], + [key('ArrowUp', ['Shift']), (s) => resize(s, Direction.Up)], + [key('ArrowDown', ['Shift']), (s) => resize(s, Direction.Down)], + [key('ArrowLeft', ['Shift']), (s) => resize(s, Direction.Left)], + [key('ArrowRight', ['Shift']), (s) => resize(s, Direction.Right)], +]; + +const shortcutMap = new Map(shortcutMapData); + +export function handleKeyboardEventOnCell( + event: KeyboardEvent, + selection: Writable, +): void { + const shortcut = KeyboardShortcut.fromKeyboardEvent(event); + const action = shortcutMap.get(shortcut.toString()); + if (action) { + event.preventDefault(); + action(selection); + } +} diff --git a/mathesar_ui/src/components/sheet/sheetScollingUtils.ts b/mathesar_ui/src/components/sheet/sheetScrollingUtils.ts similarity index 93% rename from mathesar_ui/src/components/sheet/sheetScollingUtils.ts rename to mathesar_ui/src/components/sheet/sheetScrollingUtils.ts index 130f87cefd..1155a4ec7b 100644 --- a/mathesar_ui/src/components/sheet/sheetScollingUtils.ts +++ b/mathesar_ui/src/components/sheet/sheetScrollingUtils.ts @@ -1,3 +1,5 @@ +import { tick } from 'svelte'; + // TODO: Create a common utility action to handle active element based scroll function scrollToElement(htmlElement: HTMLElement | null): void { const activeRow = htmlElement?.parentElement; @@ -47,3 +49,8 @@ export function scrollBasedOnSelection(): void { ); scrollToElement(selectedCell); } + +export async function autoScroll() { + await tick(); + scrollBasedOnActiveCell(); +} diff --git a/mathesar_ui/src/components/sheet/utils.ts b/mathesar_ui/src/components/sheet/utils.ts index c05015864a..4f3915d8d0 100644 --- a/mathesar_ui/src/components/sheet/utils.ts +++ b/mathesar_ui/src/components/sheet/utils.ts @@ -1,5 +1,6 @@ -import { setContext, getContext } from 'svelte'; +import { getContext, setContext } from 'svelte'; import type { Readable } from 'svelte/store'; + import type { ImmutableMap } from '@mathesar-component-library/types'; export interface ColumnPosition { diff --git a/mathesar_ui/src/systems/data-explorer/result-pane/ResultRowCell.svelte b/mathesar_ui/src/systems/data-explorer/result-pane/ResultRowCell.svelte index 50620a2fbd..6010b66a53 100644 --- a/mathesar_ui/src/systems/data-explorer/result-pane/ResultRowCell.svelte +++ b/mathesar_ui/src/systems/data-explorer/result-pane/ResultRowCell.svelte @@ -1,15 +1,12 @@ @@ -55,7 +31,8 @@ value={row?.record[column.id]} showAsSkeleton={recordRunState === 'processing'} disabled={true} - on:movementKeyDown={moveThroughCells} + on:movementKeyDown={({ detail }) => + handleKeyboardEventOnCell(detail.originalEvent, selection)} on:activate={() => { // // TODO_3037 // if (row) { diff --git a/mathesar_ui/src/systems/table-view/row/RowCell.svelte b/mathesar_ui/src/systems/table-view/row/RowCell.svelte index 168a2bd019..61ce76d658 100644 --- a/mathesar_ui/src/systems/table-view/row/RowCell.svelte +++ b/mathesar_ui/src/systems/table-view/row/RowCell.svelte @@ -1,7 +1,7 @@
diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/SteppedInputCell.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/SteppedInputCell.svelte index 4544741629..98f1bdcd02 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/SteppedInputCell.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/SteppedInputCell.svelte @@ -143,12 +143,6 @@ resetEditMode(); } - function handleMouseDown() { - if (!isActive) { - dispatch('activate'); - } - } - onMount(initLastSavedValue); @@ -159,7 +153,6 @@ bind:element={cellRef} on:dblclick={setModeToEdit} on:keydown={handleKeyDown} - on:mousedown={handleMouseDown} on:mouseenter mode={isEditMode ? 'edit' : 'default'} {multiLineTruncate} diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/array/ArrayCell.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/array/ArrayCell.svelte index 1f25b9fc64..66121cef76 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/array/ArrayCell.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/array/ArrayCell.svelte @@ -34,12 +34,6 @@ break; } } - - function handleMouseDown() { - if (!isActive) { - dispatch('activate'); - } - } {#if isDefinedNonNullable(value)} diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/checkbox/CheckboxCell.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/checkbox/CheckboxCell.svelte index dd31115e7c..a6a469c9d3 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/checkbox/CheckboxCell.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/checkbox/CheckboxCell.svelte @@ -58,12 +58,15 @@ cellRef?.focus(); } - function handleMouseDown() { - if (!isActive) { - isFirstActivated = true; - dispatch('activate'); - } - } + // // TODO_3037: test checkbox cell thoroughly. The `isFirstActivated` + // // variable is no longer getting set. We need to figure out what to do to + // // handle this. + // function handleMouseDown() { + // if (!isActive) { + // isFirstActivated = true; + // dispatch('activate'); + // } + // } {#if value === undefined} diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/date-time/DateTimeCell.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/date-time/DateTimeCell.svelte index e3a8144221..b50c154cab 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/date-time/DateTimeCell.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/date-time/DateTimeCell.svelte @@ -31,7 +31,6 @@ let:handleInputKeydown formatValue={formatForDisplay} on:movementKeyDown - on:activate on:mouseenter on:update > diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/formatted-input/FormattedInputCell.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/formatted-input/FormattedInputCell.svelte index 1738bc4170..42df86179c 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/formatted-input/FormattedInputCell.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/formatted-input/FormattedInputCell.svelte @@ -32,7 +32,6 @@ let:handleInputKeydown formatValue={formatForDisplay} on:movementKeyDown - on:activate on:mouseenter on:update > diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/linked-record/LinkedRecordCell.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/linked-record/LinkedRecordCell.svelte index 2759539ba1..63de26a0ca 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/linked-record/LinkedRecordCell.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/linked-record/LinkedRecordCell.svelte @@ -80,10 +80,11 @@ } } - function handleMouseDown() { - wasActiveBeforeClick = isActive; - dispatch('activate'); - } + // // TODO_3037: test and see if we need `wasActiveBeforeClick` + // function handleMouseDown() { + // wasActiveBeforeClick = isActive; + // dispatch('activate'); + // } function handleClick() { if (wasActiveBeforeClick) { @@ -99,7 +100,6 @@ {isIndependentOfSheet} on:mouseenter on:keydown={handleWrapperKeyDown} - on:mousedown={handleMouseDown} on:click={handleClick} on:dblclick={launchRecordSelector} hasPadding={false} diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/money/MoneyCell.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/money/MoneyCell.svelte index ee81c06f06..6d271559d6 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/money/MoneyCell.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/money/MoneyCell.svelte @@ -30,7 +30,6 @@ let:handleInputKeydown on:movementKeyDown on:mouseenter - on:activate on:update > diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/primary-key/PrimaryKeyCell.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/primary-key/PrimaryKeyCell.svelte index bb51bc0540..dc01938b39 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/primary-key/PrimaryKeyCell.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/primary-key/PrimaryKeyCell.svelte @@ -28,10 +28,6 @@ e.stopPropagation(); } - function handleValueMouseDown() { - dispatch('activate'); - } - function handleKeyDown(e: KeyboardEvent) { switch (e.key) { case 'Tab': @@ -63,7 +59,7 @@ class="primary-key-cell" class:is-independent-of-sheet={isIndependentOfSheet} > - + {#if value === undefined} {:else} diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/select/SingleSelectCell.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/select/SingleSelectCell.svelte index 9e2c64e214..886f86c83f 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/select/SingleSelectCell.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/select/SingleSelectCell.svelte @@ -43,12 +43,13 @@ isInitiallyActivated = false; } - function handleMouseDown() { - if (!isActive) { - isInitiallyActivated = true; - dispatch('activate'); - } - } + // // TODO_3037 test an see how to fix `isInitiallyActivated` logic + // function handleMouseDown() { + // if (!isActive) { + // isInitiallyActivated = true; + // dispatch('activate'); + // } + // } function handleKeyDown( e: KeyboardEvent, @@ -116,7 +117,6 @@ {isSelected} {disabled} {isIndependentOfSheet} - on:mousedown={handleMouseDown} on:mouseenter on:click={() => checkAndToggle(api)} on:keydown={(e) => handleKeyDown(e, api, isOpen)} diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/textarea/TextAreaCell.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/textarea/TextAreaCell.svelte index 5e948ad64e..9ebc8b88a1 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/textarea/TextAreaCell.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/textarea/TextAreaCell.svelte @@ -42,7 +42,6 @@ let:handleInputBlur let:handleInputKeydown on:movementKeyDown - on:activate on:mouseenter on:update > diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/textbox/TextBoxCell.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/textbox/TextBoxCell.svelte index 1cecffa707..f560933a6f 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/textbox/TextBoxCell.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/textbox/TextBoxCell.svelte @@ -28,7 +28,6 @@ let:handleInputBlur let:handleInputKeydown on:movementKeyDown - on:activate on:mouseenter on:update > diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/uri/UriCell.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/uri/UriCell.svelte index 0ff4e5c8fd..2e919190ec 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/uri/UriCell.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/uri/UriCell.svelte @@ -30,7 +30,6 @@ let:handleInputBlur let:handleInputKeydown on:movementKeyDown - on:activate on:mouseenter on:update > diff --git a/mathesar_ui/src/systems/data-explorer/result-pane/ResultRowCell.svelte b/mathesar_ui/src/systems/data-explorer/result-pane/ResultRowCell.svelte index 6010b66a53..75d97df662 100644 --- a/mathesar_ui/src/systems/data-explorer/result-pane/ResultRowCell.svelte +++ b/mathesar_ui/src/systems/data-explorer/result-pane/ResultRowCell.svelte @@ -33,27 +33,6 @@ disabled={true} on:movementKeyDown={({ detail }) => handleKeyboardEventOnCell(detail.originalEvent, selection)} - on:activate={() => { - // // TODO_3037 - // if (row) { - // selection.activateCell(row, processedQueryColumn); - // inspector.selectCellTab(); - // } - }} - on:onSelectionStart={() => { - // // TODO_3037 - // if (row) { - // selection.onStartSelection(row, processedQueryColumn); - // } - }} - on:onMouseEnterCellWhileSelection={() => { - // // TODO_3037 - // if (row) { - // // This enables the click + drag to - // // select multiple cells - // selection.onMouseEnterCellWhileSelection(row, processedQueryColumn); - // } - }} /> {/if}
diff --git a/mathesar_ui/src/systems/table-view/row/RowCell.svelte b/mathesar_ui/src/systems/table-view/row/RowCell.svelte index 61ce76d658..0316d7d465 100644 --- a/mathesar_ui/src/systems/table-view/row/RowCell.svelte +++ b/mathesar_ui/src/systems/table-view/row/RowCell.svelte @@ -1,5 +1,4 @@ - export let isStatic = false; - export let isControlCell = false; +
+ +
- $: htmlAttributes = { - 'data-sheet-element': 'cell', - 'data-cell-static': isStatic ? true : undefined, - 'data-cell-control': isControlCell ? true : undefined, - }; - + diff --git a/mathesar_ui/src/components/sheet/SheetHeader.svelte b/mathesar_ui/src/components/sheet/SheetHeader.svelte index 7b5e7ac7d8..37203e70f8 100644 --- a/mathesar_ui/src/components/sheet/SheetHeader.svelte +++ b/mathesar_ui/src/components/sheet/SheetHeader.svelte @@ -41,7 +41,7 @@
@@ -50,7 +50,7 @@
diff --git a/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte b/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte index 05841970a8..1a6c80302e 100644 --- a/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte +++ b/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte @@ -37,7 +37,6 @@ pagination, runState, selection, - inspector, } = queryHandler); $: ({ initial_columns } = $query); $: clipboardHandler = new SheetClipboardHandler({ @@ -87,15 +86,9 @@ > -
- - + /> {#each columnList as processedQueryColumn (processedQueryColumn.id)} -
- - {$pagination.offset + item.index + 1} -
+ + {$pagination.offset + item.index + 1}
{#each columnList as processedQueryColumn (processedQueryColumn.id)} @@ -137,7 +125,6 @@ column={processedQueryColumn} {recordRunState} {selection} - {inspector} /> {/each}
@@ -234,7 +221,7 @@ :global(.column-name-wrapper.selected) { background: var(--slate-200) !important; } - :global([data-sheet-element='cell'].selected) { + :global([data-sheet-element='data-cell'].selected) { background: var(--slate-100); } } diff --git a/mathesar_ui/src/systems/table-view/TableView.svelte b/mathesar_ui/src/systems/table-view/TableView.svelte index d209587b2b..fb048a3460 100644 --- a/mathesar_ui/src/systems/table-view/TableView.svelte +++ b/mathesar_ui/src/systems/table-view/TableView.svelte @@ -94,7 +94,7 @@ // //Better document why we need id. // // const target = e.target as HTMLElement; - // if (!target.closest('[data-sheet-element="cell"')) { + // if (!target.closest('[data-sheet-element="data-cell"')) { // if ($activeCell) { // selection.focusCell( // // TODO make sure to use getRowSelectionId instead of rowIndex diff --git a/mathesar_ui/src/systems/table-view/header/Header.svelte b/mathesar_ui/src/systems/table-view/header/Header.svelte index 01d7fa1d3b..0dd73689d9 100644 --- a/mathesar_ui/src/systems/table-view/header/Header.svelte +++ b/mathesar_ui/src/systems/table-view/header/Header.svelte @@ -94,80 +94,46 @@ - + dropColumn()} on:dragover={(e) => e.preventDefault()} locationOfFirstDraggedColumn={0} columnLocation={-1} - > -
- + /> {#each [...$processedColumns] as [columnId, processedColumn] (columnId)} {@const isSelected = $selection.columnIds.has(String(columnId))} - + +
-
- dragColumn()} - column={processedColumn} - {selection} + dragColumn()} + column={processedColumn} + {selection} + > + dropColumn(processedColumn)} + on:dragover={(e) => e.preventDefault()} + {locationOfFirstDraggedColumn} + columnLocation={columnOrderString.indexOf(columnId.toString())} + {isSelected} > - dropColumn(processedColumn)} - on:dragover={(e) => e.preventDefault()} - {locationOfFirstDraggedColumn} - columnLocation={columnOrderString.indexOf(columnId.toString())} - {isSelected} - > - { - // // TODO_3037 - // selection.onColumnSelectionStart(processedColumn) - }} - on:mouseenter={() => { - // // TODO_3037 - // selection.onMouseEnterColumnHeaderWhileSelection( - // processedColumn, - // ) - }} - /> - - - - - - -
+ + + + + + +
{/each} {#if hasNewColumnButton} - -
- -
+ + {/if} - - diff --git a/mathesar_ui/src/systems/table-view/row/GroupHeader.svelte b/mathesar_ui/src/systems/table-view/row/GroupHeader.svelte index 06a739656c..a71cdfd10f 100644 --- a/mathesar_ui/src/systems/table-view/row/GroupHeader.svelte +++ b/mathesar_ui/src/systems/table-view/row/GroupHeader.svelte @@ -30,13 +30,8 @@ ); - -
+ +
{#each columnIds as columnId, index (columnId)} @@ -60,7 +55,6 @@ From 44dd519b5815e96ecfe774b86f9a8c4354cf2143 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Wed, 7 Feb 2024 09:05:21 -0500 Subject: [PATCH 26/85] Use dedicated components for sheet cell types --- .../components/cell-fabric/CellFabric.svelte | 5 + mathesar_ui/src/components/sheet/README.md | 25 +++-- .../src/components/sheet/SheetCell.svelte | 104 ------------------ .../cells/SheetColumnCreationCell.svelte | 29 +++++ .../sheet/cells/SheetColumnHeaderCell.svelte | 35 ++++++ .../sheet/cells/SheetDataCell.svelte | 45 ++++++++ .../sheet/cells/SheetOriginCell.svelte | 23 ++++ .../{ => cells}/SheetPositionableCell.svelte | 4 +- .../sheet/cells/SheetRowHeaderCell.svelte | 34 ++++++ .../src/components/sheet/cells/index.ts | 6 + .../components/sheet/cells/sheetCellUtils.ts | 13 +++ mathesar_ui/src/components/sheet/index.ts | 4 +- .../components/sheet/sheetScrollingUtils.ts | 12 +- mathesar_ui/src/components/sheet/types.ts | 39 ------- .../import/preview/ImportPreviewSheet.svelte | 11 +- .../result-pane/ResultHeaderCell.svelte | 18 +-- .../result-pane/ResultRowCell.svelte | 10 +- .../data-explorer/result-pane/Results.svelte | 18 ++- .../systems/table-view/header/Header.svelte | 22 ++-- .../src/systems/table-view/row/Row.svelte | 9 +- .../src/systems/table-view/row/RowCell.svelte | 10 +- 21 files changed, 262 insertions(+), 214 deletions(-) delete mode 100644 mathesar_ui/src/components/sheet/SheetCell.svelte create mode 100644 mathesar_ui/src/components/sheet/cells/SheetColumnCreationCell.svelte create mode 100644 mathesar_ui/src/components/sheet/cells/SheetColumnHeaderCell.svelte create mode 100644 mathesar_ui/src/components/sheet/cells/SheetDataCell.svelte create mode 100644 mathesar_ui/src/components/sheet/cells/SheetOriginCell.svelte rename mathesar_ui/src/components/sheet/{ => cells}/SheetPositionableCell.svelte (90%) create mode 100644 mathesar_ui/src/components/sheet/cells/SheetRowHeaderCell.svelte create mode 100644 mathesar_ui/src/components/sheet/cells/index.ts create mode 100644 mathesar_ui/src/components/sheet/cells/sheetCellUtils.ts diff --git a/mathesar_ui/src/components/cell-fabric/CellFabric.svelte b/mathesar_ui/src/components/cell-fabric/CellFabric.svelte index a5b8a90e60..70f52b80ab 100644 --- a/mathesar_ui/src/components/cell-fabric/CellFabric.svelte +++ b/mathesar_ui/src/components/cell-fabric/CellFabric.svelte @@ -23,6 +23,7 @@ export let isIndependentOfSheet = false; export let showTruncationPopover = false; export let canViewLinkedEntities = true; + export let lightText = false; $: ({ cellComponentAndProps } = columnFabric); $: ({ component } = cellComponentAndProps); @@ -34,6 +35,7 @@ data-column-identifier={columnFabric.id} class:show-as-skeleton={showAsSkeleton} class:is-independent={isIndependentOfSheet} + class:light-text={lightText} > diff --git a/mathesar_ui/src/components/sheet/README.md b/mathesar_ui/src/components/sheet/README.md index 711552011a..ec8340cbe1 100644 --- a/mathesar_ui/src/components/sheet/README.md +++ b/mathesar_ui/src/components/sheet/README.md @@ -1,9 +1,16 @@ -This is a placeholder directory for the Sheet component which is meant to be implemented for use with Tables, Views and Data Explorer. - -- This would be a lower-order component. -- It would encapsulate: - - Rows - - Cells -- It will _not_ include column headers. -- It would be a pure component and will _not_ hardcode requests from within the component. - - They would be made on the parent components utilizing the Sheet component using callbacks, events and/or by exposing a Sheet API through slot. +# Sheet + +The Sheet components help us display things in a spreadsheet-like format for the table page and the data explorer. + +## `data-sheet-element` values + +We use the `data-sheet-element` HTML attribute for CSS styling and JS functionality. The values are: + +- `header-row`: The top most row of the sheet. It contains the column header cells. +- `data-row`: Used for the remaining rows, including (for now) non-standard ones like grouping headers which don't contain data. +- `positionable-cell`: Cells that span multiple columns or are taken out of regular flow e.g. "New records" message, grouping headers, etc. +- `origin-cell`: The cell in the top-left corner of the sheet. +- `column-header-cell`: Contains the column names. +- `new-column-cell`: Contains the `+` button for adding a new column. +- `row-header-cell`: Contains the row numbers. +- `data-cell`: Regular data cells. diff --git a/mathesar_ui/src/components/sheet/SheetCell.svelte b/mathesar_ui/src/components/sheet/SheetCell.svelte deleted file mode 100644 index 9c9ad390b8..0000000000 --- a/mathesar_ui/src/components/sheet/SheetCell.svelte +++ /dev/null @@ -1,104 +0,0 @@ - - -
- -
- - diff --git a/mathesar_ui/src/components/sheet/cells/SheetColumnCreationCell.svelte b/mathesar_ui/src/components/sheet/cells/SheetColumnCreationCell.svelte new file mode 100644 index 0000000000..482f2bf5f0 --- /dev/null +++ b/mathesar_ui/src/components/sheet/cells/SheetColumnCreationCell.svelte @@ -0,0 +1,29 @@ + + +
+ +
+ + diff --git a/mathesar_ui/src/components/sheet/cells/SheetColumnHeaderCell.svelte b/mathesar_ui/src/components/sheet/cells/SheetColumnHeaderCell.svelte new file mode 100644 index 0000000000..0feba78d76 --- /dev/null +++ b/mathesar_ui/src/components/sheet/cells/SheetColumnHeaderCell.svelte @@ -0,0 +1,35 @@ + + +
+ +
+ + diff --git a/mathesar_ui/src/components/sheet/cells/SheetDataCell.svelte b/mathesar_ui/src/components/sheet/cells/SheetDataCell.svelte new file mode 100644 index 0000000000..93feef613d --- /dev/null +++ b/mathesar_ui/src/components/sheet/cells/SheetDataCell.svelte @@ -0,0 +1,45 @@ + + +
+ +
+ + diff --git a/mathesar_ui/src/components/sheet/cells/SheetOriginCell.svelte b/mathesar_ui/src/components/sheet/cells/SheetOriginCell.svelte new file mode 100644 index 0000000000..8f1096d02d --- /dev/null +++ b/mathesar_ui/src/components/sheet/cells/SheetOriginCell.svelte @@ -0,0 +1,23 @@ + + +
+ +
+ + diff --git a/mathesar_ui/src/components/sheet/SheetPositionableCell.svelte b/mathesar_ui/src/components/sheet/cells/SheetPositionableCell.svelte similarity index 90% rename from mathesar_ui/src/components/sheet/SheetPositionableCell.svelte rename to mathesar_ui/src/components/sheet/cells/SheetPositionableCell.svelte index 8c731acda8..27b7806069 100644 --- a/mathesar_ui/src/components/sheet/SheetPositionableCell.svelte +++ b/mathesar_ui/src/components/sheet/cells/SheetPositionableCell.svelte @@ -1,6 +1,6 @@ + +
+ +
+ + diff --git a/mathesar_ui/src/components/sheet/cells/index.ts b/mathesar_ui/src/components/sheet/cells/index.ts new file mode 100644 index 0000000000..4dc087562c --- /dev/null +++ b/mathesar_ui/src/components/sheet/cells/index.ts @@ -0,0 +1,6 @@ +export { default as SheetColumnHeaderCell } from './SheetColumnHeaderCell.svelte'; +export { default as SheetDataCell } from './SheetDataCell.svelte'; +export { default as SheetColumnCreationCell } from './SheetColumnCreationCell.svelte'; +export { default as SheetOriginCell } from './SheetOriginCell.svelte'; +export { default as SheetPositionableCell } from './SheetPositionableCell.svelte'; +export { default as SheetRowHeaderCell } from './SheetRowHeaderCell.svelte'; diff --git a/mathesar_ui/src/components/sheet/cells/sheetCellUtils.ts b/mathesar_ui/src/components/sheet/cells/sheetCellUtils.ts new file mode 100644 index 0000000000..c1312a0adc --- /dev/null +++ b/mathesar_ui/src/components/sheet/cells/sheetCellUtils.ts @@ -0,0 +1,13 @@ +import { derived, type Readable } from 'svelte/store'; +import { getSheetContext } from '../utils'; + +export function getSheetCellStyle( + columnIdentifierKey: ColumnIdentifierKey, +): Readable { + const { stores } = getSheetContext(); + const { columnStyleMap } = stores; + return derived(columnStyleMap, (map) => { + const columnPosition = map.get(columnIdentifierKey); + return columnPosition?.styleString; + }); +} diff --git a/mathesar_ui/src/components/sheet/index.ts b/mathesar_ui/src/components/sheet/index.ts index b0a1df34f4..e541c3f139 100644 --- a/mathesar_ui/src/components/sheet/index.ts +++ b/mathesar_ui/src/components/sheet/index.ts @@ -1,7 +1,5 @@ export { default as Sheet } from './Sheet.svelte'; export { default as SheetHeader } from './SheetHeader.svelte'; -export { default as SheetCell } from './SheetCell.svelte'; -export { default as SheetPositionableCell } from './SheetPositionableCell.svelte'; export { default as SheetCellResizer } from './SheetCellResizer.svelte'; export { default as SheetVirtualRows } from './SheetVirtualRows.svelte'; export { default as SheetRow } from './SheetRow.svelte'; @@ -9,3 +7,5 @@ export { scrollBasedOnActiveCell, scrollBasedOnSelection, } from './sheetScrollingUtils'; + +export * from './cells'; diff --git a/mathesar_ui/src/components/sheet/sheetScrollingUtils.ts b/mathesar_ui/src/components/sheet/sheetScrollingUtils.ts index 16bede15b2..b59cbc2bdf 100644 --- a/mathesar_ui/src/components/sheet/sheetScrollingUtils.ts +++ b/mathesar_ui/src/components/sheet/sheetScrollingUtils.ts @@ -37,17 +37,13 @@ function scrollToElement(htmlElement: HTMLElement | null): void { } export function scrollBasedOnActiveCell(): void { - const activeCell: HTMLElement | null = document.querySelector( - '[data-sheet-element="data-cell"].is-active', - ); - scrollToElement(activeCell); + const cell = document.querySelector('[data-cell-active]'); + scrollToElement(cell); } export function scrollBasedOnSelection(): void { - const selectedCell: HTMLElement | null = document.querySelector( - '[data-sheet-element="data-cell"].is-selected', - ); - scrollToElement(selectedCell); + const cell = document.querySelector('[data-cell-selected]'); + scrollToElement(cell); } export async function autoScroll() { diff --git a/mathesar_ui/src/components/sheet/types.ts b/mathesar_ui/src/components/sheet/types.ts index bbc97588a2..b69d3b24d3 100644 --- a/mathesar_ui/src/components/sheet/types.ts +++ b/mathesar_ui/src/components/sheet/types.ts @@ -4,42 +4,3 @@ export interface SheetVirtualRowsApi { scrollToPosition: (vScrollOffset: number, hScrollOffset: number) => void; recalculateHeightsAfterIndex: (index: number) => void; } - -/** - * These are the different kinds of cells that we can have within a sheet. - * - * - `origin-cell`: The cell in the top-left corner of the sheet. - * - * - `column-header-cell`: Contains the column names. - * - * - `new-column-cell`: Contains the `+` button for adding a new column. - * - * - `row-header-cell`: Contains the row numbers. - * - * - `data-cell`: Regular data cells. - */ -export type SheetCellType = - | 'origin-cell' - | 'column-header-cell' - | 'new-column-cell' - | 'row-header-cell' - | 'data-cell'; - -/** - * These are values used for the `data-sheet-element` attribute on the sheet - * elements. - * - * - `header-row`: The top most row of the sheet. It contains the column header - * cells. - * - * - `data-row`: Used for the remaining rows, including (for now) weird ones - * like grouping headers and such. - * - * - `positionable-cell`: Cells that span multiple columns or are taken out of - * regular flow e.g. "New records" message, grouping headers, etc. - */ -export type SheetElement = - | 'header-row' - | 'data-row' - | 'positionable-cell' - | SheetCellType; diff --git a/mathesar_ui/src/pages/import/preview/ImportPreviewSheet.svelte b/mathesar_ui/src/pages/import/preview/ImportPreviewSheet.svelte index 3ffd200dad..a775f29ab2 100644 --- a/mathesar_ui/src/pages/import/preview/ImportPreviewSheet.svelte +++ b/mathesar_ui/src/pages/import/preview/ImportPreviewSheet.svelte @@ -3,8 +3,9 @@ import CellFabric from '@mathesar/components/cell-fabric/CellFabric.svelte'; import { Sheet, - SheetCell, SheetCellResizer, + SheetColumnHeaderCell, + SheetDataCell, SheetHeader, SheetRow, } from '@mathesar/components/sheet'; @@ -25,7 +26,7 @@ c.id}> {#each columns as column (column.id)} - + - + {/each} {#each records as record (record)} @@ -48,14 +49,14 @@ >
{#each columns as column (column)} - + - + {/each}
diff --git a/mathesar_ui/src/systems/data-explorer/result-pane/ResultHeaderCell.svelte b/mathesar_ui/src/systems/data-explorer/result-pane/ResultHeaderCell.svelte index edc9ccbfc0..ba9855e119 100644 --- a/mathesar_ui/src/systems/data-explorer/result-pane/ResultHeaderCell.svelte +++ b/mathesar_ui/src/systems/data-explorer/result-pane/ResultHeaderCell.svelte @@ -1,6 +1,9 @@ - + - + diff --git a/mathesar_ui/src/systems/data-explorer/result-pane/ResultRowCell.svelte b/mathesar_ui/src/systems/data-explorer/result-pane/ResultRowCell.svelte index eda7fda532..502c388eee 100644 --- a/mathesar_ui/src/systems/data-explorer/result-pane/ResultRowCell.svelte +++ b/mathesar_ui/src/systems/data-explorer/result-pane/ResultRowCell.svelte @@ -3,7 +3,7 @@ import type { RequestStatus } from '@mathesar/api/utils/requestUtils'; import CellFabric from '@mathesar/components/cell-fabric/CellFabric.svelte'; - import { SheetCell } from '@mathesar/components/sheet'; + import { SheetDataCell } from '@mathesar/components/sheet'; import { makeCellId } from '@mathesar/components/sheet/cellIds'; import type SheetSelection from '@mathesar/components/sheet/selection/SheetSelection'; import { handleKeyboardEventOnCell } from '@mathesar/components/sheet/sheetKeyboardUtils'; @@ -19,7 +19,11 @@ $: isActive = cellId === $selection.activeCellId; - + {#if row || recordRunState === 'processing'} {/if} - + diff --git a/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte b/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte index 1a6c80302e..bb5c7b3d9a 100644 --- a/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte +++ b/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte @@ -1,15 +1,17 @@ - + dropColumn()} on:dragover={(e) => e.preventDefault()} locationOfFirstDraggedColumn={0} columnLocation={-1} /> - + {#each [...$processedColumns] as [columnId, processedColumn] (columnId)} {@const isSelected = $selection.columnIds.has(String(columnId))} - +
-
+ {/each} {#if hasNewColumnButton} - + - + {/if}
diff --git a/mathesar_ui/src/systems/table-view/row/Row.svelte b/mathesar_ui/src/systems/table-view/row/Row.svelte index 0e08c59f76..cddf79d67c 100644 --- a/mathesar_ui/src/systems/table-view/row/Row.svelte +++ b/mathesar_ui/src/systems/table-view/row/Row.svelte @@ -1,6 +1,6 @@ - @@ -135,6 +134,7 @@ handleKeyboardEventOnCell(detail.originalEvent, selection)} on:update={valueUpdated} horizontalAlignment={column.primary_key ? 'left' : undefined} + lightText={hasError || isProcessing} /> {#if canEditTableRecords || showLinkedRecordHyperLink} @@ -171,4 +171,4 @@ {#if errors.length} {/if} - + From f5448669a3348120184ab63c39ab4cc4bb21f0d2 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Wed, 7 Feb 2024 20:35:43 -0500 Subject: [PATCH 27/85] Add match utility function for TS pattern matching --- .../utils/__tests__/patternMatching.test.ts | 20 +++++++++++++++++++ mathesar_ui/src/utils/patternMatching.ts | 14 +++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 mathesar_ui/src/utils/__tests__/patternMatching.test.ts create mode 100644 mathesar_ui/src/utils/patternMatching.ts diff --git a/mathesar_ui/src/utils/__tests__/patternMatching.test.ts b/mathesar_ui/src/utils/__tests__/patternMatching.test.ts new file mode 100644 index 0000000000..b00d25a7fc --- /dev/null +++ b/mathesar_ui/src/utils/__tests__/patternMatching.test.ts @@ -0,0 +1,20 @@ +import { match } from '../patternMatching'; + +test('match', () => { + type Shape = + | { kind: 'circle'; radius: number } + | { kind: 'square'; x: number } + | { kind: 'triangle'; x: number; y: number }; + + function area(shape: Shape) { + return match(shape, 'kind', { + circle: ({ radius }) => Math.PI * radius ** 2, + square: ({ x }) => x ** 2, + triangle: ({ x, y }) => (x * y) / 2, + }); + } + + expect(area({ kind: 'circle', radius: 5 })).toBe(78.53981633974483); + expect(area({ kind: 'square', x: 5 })).toBe(25); + expect(area({ kind: 'triangle', x: 5, y: 6 })).toBe(15); +}); diff --git a/mathesar_ui/src/utils/patternMatching.ts b/mathesar_ui/src/utils/patternMatching.ts new file mode 100644 index 0000000000..9f349df973 --- /dev/null +++ b/mathesar_ui/src/utils/patternMatching.ts @@ -0,0 +1,14 @@ +export function match< + /** Discriminant Property name */ + P extends string, + /** The other stuff in the type (besides the discriminant property) */ + O, + /** The union valued type */ + V extends Record & O, + /** The match arms */ + C extends { [K in V[P]]: (v: Extract>) => unknown }, +>(value: V, property: P, cases: C) { + return cases[value[property]]( + value as Extract>, + ) as ReturnType; +} From e8e5e347c1220caf8702224362176db0d8794d8b Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Wed, 7 Feb 2024 20:57:06 -0500 Subject: [PATCH 28/85] Begin scaffolding to handle drag selection --- mathesar_ui/src/components/sheet/Sheet.svelte | 23 +++++- .../src/components/sheet/cells/index.ts | 1 + .../sheet/selection/SheetSelection.ts | 48 ++++++++++++- .../src/components/sheet/selection/index.ts | 1 + .../sheet/selection/selectionUtils.ts | 71 +++++++++++++++++++ .../src/systems/table-view/TableView.svelte | 13 ++-- 6 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 mathesar_ui/src/components/sheet/selection/index.ts create mode 100644 mathesar_ui/src/components/sheet/selection/selectionUtils.ts diff --git a/mathesar_ui/src/components/sheet/Sheet.svelte b/mathesar_ui/src/components/sheet/Sheet.svelte index 9e01daf36f..92b876876e 100644 --- a/mathesar_ui/src/components/sheet/Sheet.svelte +++ b/mathesar_ui/src/components/sheet/Sheet.svelte @@ -1,12 +1,14 @@
{#if columns.length} diff --git a/mathesar_ui/src/components/sheet/cells/index.ts b/mathesar_ui/src/components/sheet/cells/index.ts index 4dc087562c..2fd6716ac2 100644 --- a/mathesar_ui/src/components/sheet/cells/index.ts +++ b/mathesar_ui/src/components/sheet/cells/index.ts @@ -4,3 +4,4 @@ export { default as SheetColumnCreationCell } from './SheetColumnCreationCell.sv export { default as SheetOriginCell } from './SheetOriginCell.svelte'; export { default as SheetPositionableCell } from './SheetPositionableCell.svelte'; export { default as SheetRowHeaderCell } from './SheetRowHeaderCell.svelte'; +export * from './sheetCellUtils'; diff --git a/mathesar_ui/src/components/sheet/selection/SheetSelection.ts b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts index 7c11c67476..38dde70bda 100644 --- a/mathesar_ui/src/components/sheet/selection/SheetSelection.ts +++ b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts @@ -1,10 +1,12 @@ import { execPipe, filter, first, map } from 'iter-tools'; import { ImmutableSet } from '@mathesar/component-library'; +import { match } from '@mathesar/utils/patternMatching'; import { assertExhaustive } from '@mathesar/utils/typeUtils'; import { makeCells, parseCellId } from '../cellIds'; import { Direction, getColumnOffset } from './Direction'; import Plane from './Plane'; +import type { SheetCellDetails } from './selectionUtils'; /** * - `'dataCells'` means that the selection contains data cells. This is by far @@ -280,7 +282,7 @@ export default class SheetSelection { * selection is made only of data cells, and will never include cells in the * placeholder row, even if a user drags to select a cell in it. */ - ofCellRange(cellIdA: string, cellIdB: string): SheetSelection { + ofDataCellRange(cellIdA: string, cellIdB: string): SheetSelection { return this.withBasis( basisFromDataCells( this.plane.dataCellsInFlexibleCellRange(cellIdA, cellIdB), @@ -288,6 +290,46 @@ export default class SheetSelection { ); } + ofSheetCellRange( + cellA: SheetCellDetails, + cellB: SheetCellDetails, + ): SheetSelection { + // TODO_3037 finish implementation + return match(cellA, 'type', { + 'data-cell': (a) => + match(cellB, 'type', { + 'data-cell': (b) => this.ofDataCellRange(a.cellId, b.cellId), + 'column-header-cell': (b) => { + throw new Error('Not implemented'); + }, + 'row-header-cell': (b) => { + throw new Error('Not implemented'); + }, + }), + 'column-header-cell': (a) => + match(cellB, 'type', { + 'data-cell': (b) => { + throw new Error('Not implemented'); + }, + 'column-header-cell': (b) => + this.ofColumnRange(a.columnId, b.columnId), + 'row-header-cell': (b) => { + throw new Error('Not implemented'); + }, + }), + 'row-header-cell': (a) => + match(cellB, 'type', { + 'data-cell': (b) => { + throw new Error('Not implemented'); + }, + 'column-header-cell': (b) => { + throw new Error('Not implemented'); + }, + 'row-header-cell': (b) => this.ofRowRange(a.rowId, b.rowId), + }), + }); + } + /** * @returns a new selection formed from one cell within the data rows or the * placeholder row. @@ -391,8 +433,8 @@ export default class SheetSelection { * by the active cell (also the first cell selected when dragging) and the * provided cell. */ - drawnToCell(cellId: string): SheetSelection { - return this.ofCellRange(this.activeCellId ?? cellId, cellId); + drawnToDataCell(cellId: string): SheetSelection { + return this.ofDataCellRange(this.activeCellId ?? cellId, cellId); } /** diff --git a/mathesar_ui/src/components/sheet/selection/index.ts b/mathesar_ui/src/components/sheet/selection/index.ts new file mode 100644 index 0000000000..ec81a55c52 --- /dev/null +++ b/mathesar_ui/src/components/sheet/selection/index.ts @@ -0,0 +1 @@ +export * from './selectionUtils'; diff --git a/mathesar_ui/src/components/sheet/selection/selectionUtils.ts b/mathesar_ui/src/components/sheet/selection/selectionUtils.ts new file mode 100644 index 0000000000..32558f0884 --- /dev/null +++ b/mathesar_ui/src/components/sheet/selection/selectionUtils.ts @@ -0,0 +1,71 @@ +import type { Writable } from 'svelte/store'; + +import type SheetSelection from './SheetSelection'; + +export type SheetCellDetails = + | { type: 'data-cell'; cellId: string } + | { type: 'column-header-cell'; columnId: string } + | { type: 'row-header-cell'; rowId: string }; + +export function findContainingSheetCell( + element: HTMLElement, +): SheetCellDetails | undefined { + const containingElement = element.closest('[data-sheet-element]'); + if (!containingElement) return undefined; + + const elementType = containingElement.getAttribute('data-sheet-element'); + if (!elementType) return undefined; + + if (elementType === 'data-cell') { + const cellId = containingElement.getAttribute('data-cell-selection-id'); + if (!cellId) return undefined; + return { type: 'data-cell', cellId }; + } + + if (elementType === 'column-header-cell') { + const columnId = containingElement.getAttribute('data-column-identifier'); + if (!columnId) return undefined; + return { type: 'column-header-cell', columnId }; + } + + if (elementType === 'row-header-cell') { + const rowId = containingElement.getAttribute('data-row-selection-id'); + if (!rowId) return undefined; + return { type: 'row-header-cell', rowId }; + } + + return undefined; +} + +export function beginSelection({ + selection, + sheetElement, + startingCell, +}: { + startingCell: SheetCellDetails; + selection: Writable; + sheetElement: HTMLElement; +}) { + let previousTarget: HTMLElement | undefined; + + function drawToCell(endingCell: SheetCellDetails) { + selection.update((s) => s.ofSheetCellRange(startingCell, endingCell)); + } + + function drawToPoint(e: MouseEvent) { + const target = e.target as HTMLElement; + if (target === previousTarget) return; // For performance + const cell = findContainingSheetCell(target); + if (!cell) return; + drawToCell(cell); + } + + function finish() { + sheetElement.removeEventListener('mousemove', drawToPoint); + window.removeEventListener('mouseup', finish); + } + + drawToCell(startingCell); + sheetElement.addEventListener('mousemove', drawToPoint); + window.addEventListener('mouseup', finish); +} diff --git a/mathesar_ui/src/systems/table-view/TableView.svelte b/mathesar_ui/src/systems/table-view/TableView.svelte index fb048a3460..3f3dad227e 100644 --- a/mathesar_ui/src/systems/table-view/TableView.svelte +++ b/mathesar_ui/src/systems/table-view/TableView.svelte @@ -127,16 +127,17 @@
{#if $processedColumns.size} entry.column.id} - {usesVirtualList} - {columnWidths} {clipboardHandler} - hasBorder={sheetHasBorder} - restrictWidthToRowWidth={!usesVirtualList} + {columnWidths} + {selection} + {usesVirtualList} bind:horizontalScrollOffset={$horizontalScrollOffset} bind:scrollOffset={$scrollOffset} + columns={sheetColumns} + getColumnIdentifier={(entry) => entry.column.id} + hasBorder={sheetHasBorder} hasPaddingRight + restrictWidthToRowWidth={!usesVirtualList} >
From 4f2a253cc8c83a1bd231ae2158b31fbb4c1c4097 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Thu, 8 Feb 2024 20:10:42 -0500 Subject: [PATCH 29/85] Document match function --- mathesar_ui/src/utils/patternMatching.ts | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/mathesar_ui/src/utils/patternMatching.ts b/mathesar_ui/src/utils/patternMatching.ts index 9f349df973..8b69cbdaed 100644 --- a/mathesar_ui/src/utils/patternMatching.ts +++ b/mathesar_ui/src/utils/patternMatching.ts @@ -1,3 +1,31 @@ +/** + * Performs branching logic by exhaustively pattern-matching all variants of a + * TypeScript discriminated union. + * + * @param value The value to match + * @param property The name of the discriminant property + * @param cases An object representing the match arms. It should have one entry + * per variant of the union. The key should be the value of the discriminant + * property for that variant, and the value should be a function that takes the + * value of that variant and returns the result of the match arm. + * + * @example + * + * ```ts + * type Shape = + * | { kind: 'circle'; radius: number } + * | { kind: 'square'; x: number } + * | { kind: 'triangle'; x: number; y: number }; + * + * function area(shape: Shape) { + * return match(shape, 'kind', { + * circle: ({ radius }) => Math.PI * radius ** 2, + * square: ({ x }) => x ** 2, + * triangle: ({ x, y }) => (x * y) / 2, + * }); + * } + * ``` + */ export function match< /** Discriminant Property name */ P extends string, From ab4aca0c895d61b26462c91ff9e02de765beaf98 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Thu, 8 Feb 2024 20:12:35 -0500 Subject: [PATCH 30/85] Finish implementation of ofSheetCellRange method --- .../sheet/selection/SheetSelection.ts | 71 +++++++++---------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/mathesar_ui/src/components/sheet/selection/SheetSelection.ts b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts index 38dde70bda..f177794704 100644 --- a/mathesar_ui/src/components/sheet/selection/SheetSelection.ts +++ b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts @@ -3,7 +3,7 @@ import { execPipe, filter, first, map } from 'iter-tools'; import { ImmutableSet } from '@mathesar/component-library'; import { match } from '@mathesar/utils/patternMatching'; import { assertExhaustive } from '@mathesar/utils/typeUtils'; -import { makeCells, parseCellId } from '../cellIds'; +import { makeCellId, makeCells, parseCellId } from '../cellIds'; import { Direction, getColumnOffset } from './Direction'; import Plane from './Plane'; import type { SheetCellDetails } from './selectionUtils'; @@ -291,42 +291,41 @@ export default class SheetSelection { } ofSheetCellRange( - cellA: SheetCellDetails, - cellB: SheetCellDetails, + startingCell: SheetCellDetails, + endingCell: SheetCellDetails, ): SheetSelection { - // TODO_3037 finish implementation - return match(cellA, 'type', { - 'data-cell': (a) => - match(cellB, 'type', { - 'data-cell': (b) => this.ofDataCellRange(a.cellId, b.cellId), - 'column-header-cell': (b) => { - throw new Error('Not implemented'); - }, - 'row-header-cell': (b) => { - throw new Error('Not implemented'); - }, - }), - 'column-header-cell': (a) => - match(cellB, 'type', { - 'data-cell': (b) => { - throw new Error('Not implemented'); - }, - 'column-header-cell': (b) => - this.ofColumnRange(a.columnId, b.columnId), - 'row-header-cell': (b) => { - throw new Error('Not implemented'); - }, - }), - 'row-header-cell': (a) => - match(cellB, 'type', { - 'data-cell': (b) => { - throw new Error('Not implemented'); - }, - 'column-header-cell': (b) => { - throw new Error('Not implemented'); - }, - 'row-header-cell': (b) => this.ofRowRange(a.rowId, b.rowId), - }), + // Nullish coalescing is safe here since we know we'll have a first row and + // first column in the cases where we're selecting things. + const firstRow = () => this.plane.rowIds.first ?? ''; + const firstColumn = () => this.plane.columnIds.first ?? ''; + + return match(startingCell, 'type', { + 'data-cell': ({ cellId: startingCellId }) => { + const endingCellId = match(endingCell, 'type', { + 'data-cell': (b) => b.cellId, + 'column-header-cell': (b) => makeCellId(firstRow(), b.columnId), + 'row-header-cell': (b) => makeCellId(b.rowId, firstColumn()), + }); + return this.ofDataCellRange(startingCellId, endingCellId); + }, + + 'column-header-cell': ({ columnId: startingColumnId }) => { + const endingColumnId = match(endingCell, 'type', { + 'data-cell': (b) => parseCellId(b.cellId).columnId, + 'column-header-cell': (b) => b.columnId, + 'row-header-cell': () => firstColumn(), + }); + return this.ofColumnRange(startingColumnId, endingColumnId); + }, + + 'row-header-cell': ({ rowId: startingRowId }) => { + const endingRowId = match(endingCell, 'type', { + 'data-cell': (b) => parseCellId(b.cellId).rowId, + 'column-header-cell': () => firstRow(), + 'row-header-cell': (b) => b.rowId, + }); + return this.ofRowRange(startingRowId, endingRowId); + }, }); } From 4e83d8d3a2814165985220d4e257a60df1129d1f Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Sun, 11 Feb 2024 21:07:34 -0500 Subject: [PATCH 31/85] Lift isSelected prop way up Move from being used in CellWrapper to being used in SheetDataCell. --- .../components/cell-fabric/CellFabric.svelte | 2 -- .../data-types/components/CellWrapper.svelte | 2 -- .../components/SteppedInputCell.svelte | 2 -- .../components/array/ArrayCell.svelte | 2 -- .../components/checkbox/CheckboxCell.svelte | 2 -- .../components/date-time/DateTimeCell.svelte | 2 -- .../formatted-input/FormattedInputCell.svelte | 2 -- .../linked-record/LinkedRecordCell.svelte | 2 -- .../components/money/MoneyCell.svelte | 2 -- .../components/number/NumberCell.svelte | 2 -- .../primary-key/PrimaryKeyCell.svelte | 2 -- .../components/select/SingleSelectCell.svelte | 2 -- .../components/textarea/TextAreaCell.svelte | 2 -- .../components/textbox/TextBoxCell.svelte | 2 -- .../data-types/components/typeDefinitions.ts | 1 - .../data-types/components/uri/UriCell.svelte | 2 -- .../sheet/cells/SheetDataCell.svelte | 25 ++++++++++++++++--- .../result-pane/ResultRowCell.svelte | 5 ++-- .../src/systems/table-view/row/RowCell.svelte | 6 ++--- 19 files changed, 26 insertions(+), 41 deletions(-) diff --git a/mathesar_ui/src/components/cell-fabric/CellFabric.svelte b/mathesar_ui/src/components/cell-fabric/CellFabric.svelte index 70f52b80ab..41d9390317 100644 --- a/mathesar_ui/src/components/cell-fabric/CellFabric.svelte +++ b/mathesar_ui/src/components/cell-fabric/CellFabric.svelte @@ -14,7 +14,6 @@ | ((recordId: string, recordSummary: string) => void) | undefined = undefined; export let isActive = false; - export let isSelected = false; export let disabled = false; export let showAsSkeleton = false; export let horizontalAlignment: HorizontalAlignment | undefined = undefined; @@ -42,7 +41,6 @@ {...props} {columnFabric} {isActive} - {isSelected} {disabled} {isIndependentOfSheet} {horizontalAlignment} diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte index 8fe7f4bf02..3f4c9ca1d1 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte @@ -6,7 +6,6 @@ export let element: HTMLElement | undefined = undefined; export let isActive = false; - export let isSelected = false; export let disabled = false; export let mode: 'edit' | 'default' = 'default'; export let multiLineTruncate = false; @@ -116,7 +115,6 @@ {...$$restProps} > {#if mode !== 'edit'} - ; export let isActive: Props['isActive']; - export let isSelected: Props['isSelected']; export let value: Props['value']; export let disabled: Props['disabled']; export let multiLineTruncate = false; @@ -148,7 +147,6 @@ = ( export interface CellTypeProps { value: Value | null | undefined; isActive: boolean; - isSelected: boolean; disabled: boolean; searchValue?: unknown; isProcessing: boolean; diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/uri/UriCell.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/uri/UriCell.svelte index 2e919190ec..23782f18cb 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/uri/UriCell.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/uri/UriCell.svelte @@ -11,7 +11,6 @@ type $$Props = CellTypeProps; export let isActive: $$Props['isActive']; - export let isSelected: $$Props['isSelected']; export let value: $$Props['value'] = undefined; export let disabled: $$Props['disabled']; export let searchValue: $$Props['searchValue'] = undefined; @@ -22,7 +21,6 @@ + import type SheetSelection from '../selection/SheetSelection'; + + import CellBackground from '@mathesar/components/CellBackground.svelte'; import { getSheetCellStyle } from './sheetCellUtils'; type SheetColumnIdentifierKey = $$Generic; export let columnIdentifierKey: SheetColumnIdentifierKey; export let cellSelectionId: string | undefined = undefined; - export let isActive = false; - export let isSelected = false; + export let selection: SheetSelection | undefined = undefined; $: style = getSheetCellStyle(columnIdentifierKey); + $: ({ isActive, isSelected, hasSelectionBackground } = (() => { + if (!selection || !cellSelectionId) + return { + isActive: false, + isSelected: false, + hasSelectionBackground: false, + }; + const isSelected = selection.cellIds.has(cellSelectionId); + return { + isActive: selection.activeCellId === cellSelectionId, + isSelected: selection.cellIds.has(cellSelectionId), + hasSelectionBackground: isSelected && selection.cellIds.size > 1, + }; + })());
- + {#if hasSelectionBackground} + + {/if} +
diff --git a/mathesar_ui/src/systems/table-view/table-inspector/cell/CellMode.svelte b/mathesar_ui/src/systems/table-view/table-inspector/cell/CellMode.svelte index cb7a00bfd4..883a431fc0 100644 --- a/mathesar_ui/src/systems/table-view/table-inspector/cell/CellMode.svelte +++ b/mathesar_ui/src/systems/table-view/table-inspector/cell/CellMode.svelte @@ -8,73 +8,26 @@ * changes to both files as necessary. */ import { _ } from 'svelte-i18n'; + import { getTabularDataStoreFromContext } from '@mathesar/stores/table-data'; - import CellFabric from '@mathesar/components/cell-fabric/CellFabric.svelte'; + import ActiveCellValue from './ActiveCellValue.svelte'; const tabularData = getTabularDataStoreFromContext(); - $: ({ selection, recordsData, processedColumns } = $tabularData); - $: ({ activeCell } = selection); - $: ({ recordSummaries } = recordsData); - $: cell = $activeCell; - $: selectedCellValue = (() => { - if (cell) { - const rows = recordsData.getRecordRows(); - if (rows[cell.rowIndex]) { - return rows[cell.rowIndex].record[cell.columnId]; - } - } - return undefined; - })(); - $: column = (() => { - if (cell) { - const processedColumn = $processedColumns.get(Number(cell.columnId)); - if (processedColumn) { - return processedColumn; - } - } - return undefined; - })(); - $: recordSummary = - column && - $recordSummaries.get(String(column.id))?.get(String(selectedCellValue)); + $: ({ selection } = $tabularData); + $: ({ activeCellId } = $selection);
- {#if selectedCellValue !== undefined} -
-
{$_('content')}
-
- {#if column} - - {/if} -
-
+ {#if activeCellId} + {:else} {$_('select_cell_view_properties')} {/if}
- From 2608b222c954c361defb99b8bb6992e5cda4318d Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Sun, 18 Feb 2024 11:19:49 -0500 Subject: [PATCH 33/85] Remove table-view deps from CellInspector code --- .../inspector/cell/CellInspector.svelte | 69 +++++++++++++++++++ .../inspector/cell/cellInspectorUtils.ts | 16 +++++ .../cell/ActiveCellValue.svelte | 52 -------------- .../table-inspector/cell/CellMode.svelte | 37 +++------- .../table-inspector/cell/cellModeUtils.ts | 35 ++++++++++ 5 files changed, 131 insertions(+), 78 deletions(-) create mode 100644 mathesar_ui/src/components/inspector/cell/CellInspector.svelte create mode 100644 mathesar_ui/src/components/inspector/cell/cellInspectorUtils.ts delete mode 100644 mathesar_ui/src/systems/table-view/table-inspector/cell/ActiveCellValue.svelte create mode 100644 mathesar_ui/src/systems/table-view/table-inspector/cell/cellModeUtils.ts diff --git a/mathesar_ui/src/components/inspector/cell/CellInspector.svelte b/mathesar_ui/src/components/inspector/cell/CellInspector.svelte new file mode 100644 index 0000000000..f01da7a677 --- /dev/null +++ b/mathesar_ui/src/components/inspector/cell/CellInspector.svelte @@ -0,0 +1,69 @@ + + +
+ {#if activeCellData} + {@const { column, value, recordSummary } = activeCellData} +
+
{$_('content')}
+
+ {#if column} + + {/if} +
+
+ {:else} + {$_('select_cell_view_properties')} + {/if} +
+ + diff --git a/mathesar_ui/src/components/inspector/cell/cellInspectorUtils.ts b/mathesar_ui/src/components/inspector/cell/cellInspectorUtils.ts new file mode 100644 index 0000000000..b493f92069 --- /dev/null +++ b/mathesar_ui/src/components/inspector/cell/cellInspectorUtils.ts @@ -0,0 +1,16 @@ +import type { CellColumnFabric } from '@mathesar/components/cell-fabric/types'; + +interface ActiveCellData { + column: CellColumnFabric; + value: unknown; + recordSummary?: string; +} + +interface SelectionData { + cellCount: number; +} + +export interface SelectedCellData { + activeCellData?: ActiveCellData; + selectionData: SelectionData; +} diff --git a/mathesar_ui/src/systems/table-view/table-inspector/cell/ActiveCellValue.svelte b/mathesar_ui/src/systems/table-view/table-inspector/cell/ActiveCellValue.svelte deleted file mode 100644 index 368514e70f..0000000000 --- a/mathesar_ui/src/systems/table-view/table-inspector/cell/ActiveCellValue.svelte +++ /dev/null @@ -1,52 +0,0 @@ - - -
-
{$_('content')}
-
- {#if column} - - {/if} -
-
- - diff --git a/mathesar_ui/src/systems/table-view/table-inspector/cell/CellMode.svelte b/mathesar_ui/src/systems/table-view/table-inspector/cell/CellMode.svelte index 883a431fc0..dcdf6b3073 100644 --- a/mathesar_ui/src/systems/table-view/table-inspector/cell/CellMode.svelte +++ b/mathesar_ui/src/systems/table-view/table-inspector/cell/CellMode.svelte @@ -1,33 +1,18 @@ -
- {#if activeCellId} - - {:else} - {$_('select_cell_view_properties')} - {/if} -
- - + diff --git a/mathesar_ui/src/systems/table-view/table-inspector/cell/cellModeUtils.ts b/mathesar_ui/src/systems/table-view/table-inspector/cell/cellModeUtils.ts new file mode 100644 index 0000000000..870348eb0e --- /dev/null +++ b/mathesar_ui/src/systems/table-view/table-inspector/cell/cellModeUtils.ts @@ -0,0 +1,35 @@ +import type { SelectedCellData } from '@mathesar/components/inspector/cell/cellInspectorUtils'; +import { parseCellId } from '@mathesar/components/sheet/cellIds'; +import type SheetSelection from '@mathesar/components/sheet/selection/SheetSelection'; +import type { + ProcessedColumns, + RecordSummariesForSheet, +} from '@mathesar/stores/table-data'; + +export function getSelectedCellData( + selection: SheetSelection, + selectableRowsMap: Map>, + processedColumns: ProcessedColumns, + recordSummaries: RecordSummariesForSheet, +): SelectedCellData { + const { activeCellId } = selection; + const selectionData = { + cellCount: selection.cellIds.size, + }; + if (activeCellId === undefined) { + return { selectionData }; + } + const { rowId, columnId } = parseCellId(activeCellId); + const record = selectableRowsMap.get(rowId) ?? {}; + const value = record[columnId]; + const column = processedColumns.get(Number(columnId)); + const recordSummary = recordSummaries.get(columnId)?.get(String(value)); + return { + activeCellData: column && { + column, + value, + recordSummary, + }, + selectionData, + }; +} From ad9dd5eb78fb0596d5dd9b84413c8f9534a94acb Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Sun, 18 Feb 2024 12:56:15 -0500 Subject: [PATCH 34/85] Pass columnIdentifierKey via prop not import This is necessary because the table page and Data Explorer use different values (and types) here. --- .../sheet/cells/SheetColumnCreationCell.svelte | 7 +++++-- .../components/sheet/cells/SheetOriginCell.svelte | 7 +++++-- .../components/sheet/cells/SheetRowHeaderCell.svelte | 6 ++++-- .../systems/data-explorer/result-pane/Results.svelte | 7 +++++-- .../src/systems/table-view/header/Header.svelte | 12 ++++++++---- mathesar_ui/src/systems/table-view/row/Row.svelte | 5 ++++- 6 files changed, 31 insertions(+), 13 deletions(-) diff --git a/mathesar_ui/src/components/sheet/cells/SheetColumnCreationCell.svelte b/mathesar_ui/src/components/sheet/cells/SheetColumnCreationCell.svelte index 482f2bf5f0..9ca66fbe19 100644 --- a/mathesar_ui/src/components/sheet/cells/SheetColumnCreationCell.svelte +++ b/mathesar_ui/src/components/sheet/cells/SheetColumnCreationCell.svelte @@ -1,8 +1,11 @@
diff --git a/mathesar_ui/src/components/sheet/cells/SheetOriginCell.svelte b/mathesar_ui/src/components/sheet/cells/SheetOriginCell.svelte index 8f1096d02d..7af9d9748b 100644 --- a/mathesar_ui/src/components/sheet/cells/SheetOriginCell.svelte +++ b/mathesar_ui/src/components/sheet/cells/SheetOriginCell.svelte @@ -1,8 +1,11 @@
diff --git a/mathesar_ui/src/components/sheet/cells/SheetRowHeaderCell.svelte b/mathesar_ui/src/components/sheet/cells/SheetRowHeaderCell.svelte index b0df3b2a0a..bdf05fecf1 100644 --- a/mathesar_ui/src/components/sheet/cells/SheetRowHeaderCell.svelte +++ b/mathesar_ui/src/components/sheet/cells/SheetRowHeaderCell.svelte @@ -1,10 +1,12 @@
- + {#each columnList as processedQueryColumn (processedQueryColumn.id)} - + {$pagination.offset + item.index + 1} diff --git a/mathesar_ui/src/systems/table-view/header/Header.svelte b/mathesar_ui/src/systems/table-view/header/Header.svelte index df3c19d110..ad28034c9f 100644 --- a/mathesar_ui/src/systems/table-view/header/Header.svelte +++ b/mathesar_ui/src/systems/table-view/header/Header.svelte @@ -5,13 +5,17 @@ import { ContextMenu } from '@mathesar/component-library'; import { SheetCellResizer, + SheetColumnCreationCell, SheetColumnHeaderCell, SheetHeader, - SheetColumnCreationCell, } from '@mathesar/components/sheet'; import SheetOriginCell from '@mathesar/components/sheet/cells/SheetOriginCell.svelte'; import type { ProcessedColumn } from '@mathesar/stores/table-data'; - import { getTabularDataStoreFromContext } from '@mathesar/stores/table-data'; + import { + ID_ADD_NEW_COLUMN, + ID_ROW_CONTROL_COLUMN, + getTabularDataStoreFromContext, + } from '@mathesar/stores/table-data'; import { saveColumnOrder } from '@mathesar/stores/tables'; import { Draggable, Droppable } from './drag-and-drop'; import ColumnHeaderContextMenu from './header-cell/ColumnHeaderContextMenu.svelte'; @@ -92,7 +96,7 @@ - + dropColumn()} on:dragover={(e) => e.preventDefault()} @@ -130,7 +134,7 @@ {/each} {#if hasNewColumnButton} - + {/if} diff --git a/mathesar_ui/src/systems/table-view/row/Row.svelte b/mathesar_ui/src/systems/table-view/row/Row.svelte index cddf79d67c..dc2f228071 100644 --- a/mathesar_ui/src/systems/table-view/row/Row.svelte +++ b/mathesar_ui/src/systems/table-view/row/Row.svelte @@ -70,7 +70,10 @@ style="--cell-height:{rowHeightPx - 1}px;{styleString}" on:mousedown={checkAndCreateEmptyRow} > - + {#if rowHasRecord(row)} Date: Sun, 18 Feb 2024 13:16:38 -0500 Subject: [PATCH 35/85] Use common CellInspector component within DE --- .../inspector/cell/CellInspector.svelte | 8 --- .../exploration-inspector/CellTab.svelte | 67 ------------------- .../ExplorationInspector.svelte | 2 +- .../exploration-inspector/cell/CellTab.svelte | 16 +++++ .../cell/cellTabUtils.ts | 25 +++++++ 5 files changed, 42 insertions(+), 76 deletions(-) delete mode 100644 mathesar_ui/src/systems/data-explorer/exploration-inspector/CellTab.svelte create mode 100644 mathesar_ui/src/systems/data-explorer/exploration-inspector/cell/CellTab.svelte create mode 100644 mathesar_ui/src/systems/data-explorer/exploration-inspector/cell/cellTabUtils.ts diff --git a/mathesar_ui/src/components/inspector/cell/CellInspector.svelte b/mathesar_ui/src/components/inspector/cell/CellInspector.svelte index f01da7a677..0e6c3e8ad0 100644 --- a/mathesar_ui/src/components/inspector/cell/CellInspector.svelte +++ b/mathesar_ui/src/components/inspector/cell/CellInspector.svelte @@ -1,12 +1,4 @@ - -
- {#if cellValue !== undefined} -
-
{$_('content')}
-
- {#if column} - - {/if} -
-
- {:else} - {$_('select_cell_view_properties')} - {/if} -
- - diff --git a/mathesar_ui/src/systems/data-explorer/exploration-inspector/ExplorationInspector.svelte b/mathesar_ui/src/systems/data-explorer/exploration-inspector/ExplorationInspector.svelte index 6ef0dd00e8..9c33275db2 100644 --- a/mathesar_ui/src/systems/data-explorer/exploration-inspector/ExplorationInspector.svelte +++ b/mathesar_ui/src/systems/data-explorer/exploration-inspector/ExplorationInspector.svelte @@ -4,7 +4,7 @@ import type QueryManager from '../QueryManager'; import ExplorationTab from './ExplorationTab.svelte'; import ColumnTab from './column-tab/ColumnTab.svelte'; - import CellTab from './CellTab.svelte'; + import CellTab from './cell/CellTab.svelte'; export let queryHandler: QueryRunner | QueryManager; export let canEditMetadata: boolean; diff --git a/mathesar_ui/src/systems/data-explorer/exploration-inspector/cell/CellTab.svelte b/mathesar_ui/src/systems/data-explorer/exploration-inspector/cell/CellTab.svelte new file mode 100644 index 0000000000..8f57fa461b --- /dev/null +++ b/mathesar_ui/src/systems/data-explorer/exploration-inspector/cell/CellTab.svelte @@ -0,0 +1,16 @@ + + + diff --git a/mathesar_ui/src/systems/data-explorer/exploration-inspector/cell/cellTabUtils.ts b/mathesar_ui/src/systems/data-explorer/exploration-inspector/cell/cellTabUtils.ts new file mode 100644 index 0000000000..479a659136 --- /dev/null +++ b/mathesar_ui/src/systems/data-explorer/exploration-inspector/cell/cellTabUtils.ts @@ -0,0 +1,25 @@ +import type { SelectedCellData } from '@mathesar/components/inspector/cell/cellInspectorUtils'; +import { parseCellId } from '@mathesar/components/sheet/cellIds'; +import type SheetSelection from '@mathesar/components/sheet/selection/SheetSelection'; +import { getRowSelectionId, type QueryRow } from '../../QueryRunner'; +import type { ProcessedQueryOutputColumnMap } from '../../utils'; + +export function getSelectedCellData( + selection: SheetSelection, + rows: QueryRow[], + processedColumns: ProcessedQueryOutputColumnMap, +): SelectedCellData { + const { activeCellId } = selection; + const selectionData = { cellCount: selection.cellIds.size }; + const fallback = { selectionData }; + if (!activeCellId) return fallback; + const { rowId, columnId } = parseCellId(activeCellId); + // TODO: Usage of `find` is not ideal for perf here. Would be nice to store + // rows in a map for faster lookup. + const row = rows.find((r) => getRowSelectionId(r) === rowId); + if (!row) return fallback; + const value = row.record[columnId]; + const column = processedColumns.get(columnId); + const activeCellData = column && { column, value }; + return { activeCellData, selectionData }; +} From 63b6dd90a0c274e0f37a402a7252784bfb76db84 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Sun, 18 Feb 2024 13:44:11 -0500 Subject: [PATCH 36/85] Pass selection into data explorer sheet --- mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte b/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte index 323a92279c..a1786e55df 100644 --- a/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte +++ b/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte @@ -85,6 +85,7 @@ {columnWidths} {clipboardHandler} usesVirtualList + {selection} > From af35b896346e65b6690a332919535316fd1a2d42 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Sun, 18 Feb 2024 13:46:48 -0500 Subject: [PATCH 37/85] Re-enable copying data explorer cells --- .../src/systems/data-explorer/result-pane/Results.svelte | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte b/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte index a1786e55df..450825a910 100644 --- a/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte +++ b/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte @@ -46,10 +46,8 @@ rowsMap: get(selectableRowsMap), columnsMap: get(processedColumns), recordSummaries: new ImmutableMap(), - // TODO_3037 - selectedRowIds: new ImmutableSet(), - // TODO_3037 - selectedColumnIds: new ImmutableSet(), + selectedRowIds: get(selection).rowIds, + selectedColumnIds: get(selection).columnIds, }), showToastInfo: toast.info, }); From 748134dcb3932cf39b88950b51304ef6d77c1658 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Sun, 18 Feb 2024 14:27:45 -0500 Subject: [PATCH 38/85] Reinstate row header selection in table page --- mathesar_ui/src/systems/table-view/row/Row.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mathesar_ui/src/systems/table-view/row/Row.svelte b/mathesar_ui/src/systems/table-view/row/Row.svelte index dc2f228071..cacc964df4 100644 --- a/mathesar_ui/src/systems/table-view/row/Row.svelte +++ b/mathesar_ui/src/systems/table-view/row/Row.svelte @@ -39,6 +39,7 @@ $: ({ pkColumn } = columnsDataStore); $: primaryKeyColumnId = $pkColumn?.id; $: rowKey = getRowKey(row, primaryKeyColumnId); + $: rowSelectionId = getRowSelectionId(row); $: creationStatus = $rowCreationStatus.get(rowKey)?.state; $: status = $rowStatus.get(rowKey); $: wholeRowState = status?.wholeRowState; @@ -71,7 +72,7 @@ on:mousedown={checkAndCreateEmptyRow} > {#if rowHasRecord(row)} From 7fded1c2107089df475c265d6edb77a4079f0c19 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Sun, 18 Feb 2024 14:40:13 -0500 Subject: [PATCH 39/85] Reinstate row header selection in data explorer --- .../result-pane/ResultRowCell.svelte | 5 ++-- .../data-explorer/result-pane/Results.svelte | 23 ++++++++++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/mathesar_ui/src/systems/data-explorer/result-pane/ResultRowCell.svelte b/mathesar_ui/src/systems/data-explorer/result-pane/ResultRowCell.svelte index 30f947fcee..30f10f7d2d 100644 --- a/mathesar_ui/src/systems/data-explorer/result-pane/ResultRowCell.svelte +++ b/mathesar_ui/src/systems/data-explorer/result-pane/ResultRowCell.svelte @@ -7,15 +7,16 @@ import { makeCellId } from '@mathesar/components/sheet/cellIds'; import type SheetSelection from '@mathesar/components/sheet/selection/SheetSelection'; import { handleKeyboardEventOnCell } from '@mathesar/components/sheet/sheetKeyboardUtils'; - import { getRowSelectionId, type QueryRow } from '../QueryRunner'; + import type { QueryRow } from '../QueryRunner'; import type { ProcessedQueryOutputColumn } from '../utils'; export let column: ProcessedQueryOutputColumn; export let row: QueryRow | undefined; + export let rowSelectionId: string; export let recordRunState: RequestStatus['state'] | undefined; export let selection: Writable; - $: cellId = row && makeCellId(getRowSelectionId(row), column.id); + $: cellId = row && makeCellId(rowSelectionId, column.id);
@@ -103,14 +111,16 @@ let:items > {#each items as item (item.key)} - {#if rows[item.index] || showDummyGhostRow} + {@const row = getRow(item.index)} + {@const rowSelectionId = (row && getRowSelectionId(row)) ?? ''} + {#if row || showDummyGhostRow}
@@ -119,7 +129,8 @@ {#each columnList as processedQueryColumn (processedQueryColumn.id)} Date: Mon, 4 Mar 2024 13:07:07 -0500 Subject: [PATCH 40/85] Reinstate deleting records --- mathesar_ui/src/stores/table-data/records.ts | 31 ++++----- .../src/systems/data-explorer/QueryRunner.ts | 6 +- .../data-explorer/result-pane/Results.svelte | 3 +- .../src/systems/table-view/TableView.svelte | 5 +- .../table-view/row/RowContextOptions.svelte | 4 +- .../table-inspector/cell/cellModeUtils.ts | 13 ++-- .../table-inspector/record/RecordMode.svelte | 5 +- .../table-inspector/record/RowActions.svelte | 64 ++++++++----------- mathesar_ui/src/utils/iterUtils.ts | 12 ++++ 9 files changed, 79 insertions(+), 64 deletions(-) diff --git a/mathesar_ui/src/stores/table-data/records.ts b/mathesar_ui/src/stores/table-data/records.ts index da90576fd9..25dd965a4d 100644 --- a/mathesar_ui/src/stores/table-data/records.ts +++ b/mathesar_ui/src/stores/table-data/records.ts @@ -302,8 +302,8 @@ export class RecordsData { error: Writable; - /** Keys are row ids, values are records */ - selectableRowsMap: Readable>>; + /** Keys are row selection ids */ + selectableRowsMap: Readable>; private promise: CancellablePromise | undefined; @@ -364,7 +364,7 @@ export class RecordsData { [this.savedRecords, this.newRecords], ([savedRecords, newRecords]) => { const records = [...savedRecords, ...newRecords]; - return new Map(records.map((r) => [getRowSelectionId(r), r.record])); + return new Map(records.map((r) => [getRowSelectionId(r), r])); }, ); @@ -457,22 +457,23 @@ export class RecordsData { return undefined; } - async deleteSelected(selectedRowIndices: number[]): Promise { - const recordRows = this.getRecordRows(); + async deleteSelected(rowSelectionIds: Iterable): Promise { + const ids = + typeof rowSelectionIds === 'string' ? [rowSelectionIds] : rowSelectionIds; const pkColumn = get(this.columnsDataStore.pkColumn); const primaryKeysOfSavedRows: string[] = []; const identifiersOfUnsavedRows: string[] = []; - selectedRowIndices.forEach((index) => { - const row = recordRows[index]; - if (row) { - const rowKey = getRowKey(row, pkColumn?.id); - if (pkColumn?.id && isDefinedNonNullable(row.record[pkColumn?.id])) { - primaryKeysOfSavedRows.push(rowKey); - } else { - identifiersOfUnsavedRows.push(rowKey); - } + const selectableRows = get(this.selectableRowsMap); + for (const rowId of ids) { + const row = selectableRows.get(rowId); + if (!row) continue; + const rowKey = getRowKey(row, pkColumn?.id); + if (pkColumn?.id && isDefinedNonNullable(row.record[pkColumn?.id])) { + primaryKeysOfSavedRows.push(rowKey); + } else { + identifiersOfUnsavedRows.push(rowKey); } - }); + } const rowKeys = [...primaryKeysOfSavedRows, ...identifiersOfUnsavedRows]; if (rowKeys.length === 0) { diff --git a/mathesar_ui/src/systems/data-explorer/QueryRunner.ts b/mathesar_ui/src/systems/data-explorer/QueryRunner.ts index 63a4cb6db0..baee9cb275 100644 --- a/mathesar_ui/src/systems/data-explorer/QueryRunner.ts +++ b/mathesar_ui/src/systems/data-explorer/QueryRunner.ts @@ -64,8 +64,8 @@ export default class QueryRunner { new ImmutableMap(), ); - /** Keys are row ids, values are records */ - selectableRowsMap: Readable>>; + /** Keys are row selection ids */ + selectableRowsMap: Readable>; selection: Writable; @@ -108,7 +108,7 @@ export default class QueryRunner { void this.run(); this.selectableRowsMap = derived( this.rowsData, - ({ rows }) => new Map(rows.map((r) => [getRowSelectionId(r), r.record])), + ({ rows }) => new Map(rows.map((r) => [getRowSelectionId(r), r])), ); const plane = derived( diff --git a/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte b/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte index ca067b624f..6c0ba4d5c2 100644 --- a/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte +++ b/mathesar_ui/src/systems/data-explorer/result-pane/Results.svelte @@ -1,4 +1,5 @@
- {#if showOpenRecordLink} + {#if recordPageLink}
@@ -90,7 +82,7 @@ {/if} diff --git a/mathesar_ui/src/utils/iterUtils.ts b/mathesar_ui/src/utils/iterUtils.ts index 36553b2d7d..805370b416 100644 --- a/mathesar_ui/src/utils/iterUtils.ts +++ b/mathesar_ui/src/utils/iterUtils.ts @@ -32,3 +32,15 @@ export function mapExactlyOne( } return p.whenMany; } + +/** + * If the iterable contains exactly one element, returns that element. Otherwise + * returns undefined. + */ +export function takeFirstAndOnly(iterable: Iterable): T | undefined { + return mapExactlyOne(iterable, { + whenZero: undefined, + whenOne: (v) => v, + whenMany: undefined, + }); +} From 53b8317454870c960068257342b74fa5d2ddcdd7 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Mon, 4 Mar 2024 14:04:58 -0500 Subject: [PATCH 41/85] Fix checkbox toggle on click --- .../data-types/components/CellWrapper.svelte | 2 ++ .../components/checkbox/CheckboxCell.svelte | 30 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte index 3f4c9ca1d1..e97c8f8646 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte @@ -106,7 +106,9 @@ on:click on:dblclick on:mousedown + on:mouseup on:mouseenter + on:mouseleave on:keydown on:copy={handleCopy} on:focus={handleFocus} diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/checkbox/CheckboxCell.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/checkbox/CheckboxCell.svelte index d85f256b1f..a5f14189f0 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/checkbox/CheckboxCell.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/checkbox/CheckboxCell.svelte @@ -17,7 +17,7 @@ export let isIndependentOfSheet: $$Props['isIndependentOfSheet']; let cellRef: HTMLElement; - let isFirstActivated = false; + let shouldToggleOnMouseUp = false; $: valueComparisonOutcome = compareWholeValues(searchValue, value); @@ -48,24 +48,20 @@ } } - function checkAndToggle(e: Event) { - if (!disabled && isActive && e.target === cellRef && !isFirstActivated) { + function handleMouseDown() { + shouldToggleOnMouseUp = isActive; + } + + function handleMouseLeave() { + shouldToggleOnMouseUp = false; + } + + function handleMouseUp() { + if (!disabled && isActive && shouldToggleOnMouseUp) { value = !value; dispatchUpdate(); } - isFirstActivated = false; - cellRef?.focus(); } - - // // TODO_3037: test checkbox cell thoroughly. The `isFirstActivated` - // // variable is no longer getting set. We need to figure out what to do to - // // handle this. - // function handleMouseDown() { - // if (!isActive) { - // isFirstActivated = true; - // dispatch('activate'); - // } - // } {#if value === undefined} From c1fee48fdd18b6978ff8964e1e238b975c300bac Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Mon, 4 Mar 2024 14:20:03 -0500 Subject: [PATCH 42/85] Fix LinkedRecordCell launch on click --- .../components/linked-record/LinkedRecordCell.svelte | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/linked-record/LinkedRecordCell.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/linked-record/LinkedRecordCell.svelte index 9f71177829..df14de41f1 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/linked-record/LinkedRecordCell.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/linked-record/LinkedRecordCell.svelte @@ -79,11 +79,10 @@ } } - // // TODO_3037: test and see if we need `wasActiveBeforeClick` - // function handleMouseDown() { - // wasActiveBeforeClick = isActive; - // dispatch('activate'); - // } + function handleMouseDown() { + wasActiveBeforeClick = isActive; + dispatch('activate'); + } function handleClick() { if (wasActiveBeforeClick) { @@ -98,6 +97,7 @@ {isIndependentOfSheet} on:mouseenter on:keydown={handleWrapperKeyDown} + on:mousedown={handleMouseDown} on:click={handleClick} on:dblclick={launchRecordSelector} hasPadding={false} From 7bee2ed008c71941bf37bbf5b591294bf6c38f38 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Mon, 4 Mar 2024 14:27:09 -0500 Subject: [PATCH 43/85] Fix SingleSelectCell opening --- .../components/select/SingleSelectCell.svelte | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/select/SingleSelectCell.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/select/SingleSelectCell.svelte index 2469775798..c9c70ea850 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/select/SingleSelectCell.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/select/SingleSelectCell.svelte @@ -42,13 +42,12 @@ isInitiallyActivated = false; } - // // TODO_3037 test an see how to fix `isInitiallyActivated` logic - // function handleMouseDown() { - // if (!isActive) { - // isInitiallyActivated = true; - // dispatch('activate'); - // } - // } + function handleMouseDown() { + if (!isActive) { + isInitiallyActivated = true; + dispatch('activate'); + } + } function handleKeyDown( e: KeyboardEvent, @@ -115,6 +114,7 @@ {isActive} {disabled} {isIndependentOfSheet} + on:mousedown={handleMouseDown} on:mouseenter on:click={() => checkAndToggle(api)} on:keydown={(e) => handleKeyDown(e, api, isOpen)} From 16fdd6372014a3e2797f992e176049384b9b97c1 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Mon, 4 Mar 2024 15:25:20 -0500 Subject: [PATCH 44/85] Implement shift+click to select range --- mathesar_ui/src/components/sheet/Sheet.svelte | 29 ++++++++++++++---- .../sheet/selection/selectionUtils.ts | 6 ++-- mathesar_ui/src/utils/pointerUtils.ts | 30 +++++++++++++++++++ 3 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 mathesar_ui/src/utils/pointerUtils.ts diff --git a/mathesar_ui/src/components/sheet/Sheet.svelte b/mathesar_ui/src/components/sheet/Sheet.svelte index 92b876876e..0bf2344765 100644 --- a/mathesar_ui/src/components/sheet/Sheet.svelte +++ b/mathesar_ui/src/components/sheet/Sheet.svelte @@ -4,7 +4,12 @@ import { ImmutableMap } from '@mathesar-component-library/types'; import type { ClipboardHandler } from '@mathesar/stores/clipboard'; import { getClipboardHandlerStoreFromContext } from '@mathesar/stores/clipboard'; - import { beginSelection, findContainingSheetCell } from './selection'; + import { getModifierKeyCombo } from '@mathesar/utils/pointerUtils'; + import { + beginSelection, + findContainingSheetCell, + type SheetCellDetails, + } from './selection'; import type SheetSelection from './selection/SheetSelection'; import { calculateColumnStyleMapAndRowWidth, @@ -122,13 +127,25 @@ function handleMouseDown(e: MouseEvent) { if (!selection) return; - // TODO_3037: - // - handle mouse events with other buttons - // - handle Shift/Alt/Ctrl key modifiers + const target = e.target as HTMLElement; - const startingCell = findContainingSheetCell(target); + const targetCell = findContainingSheetCell(target); + if (!targetCell) return; + + const startingCell: SheetCellDetails | undefined = (() => { + const modifierKeyCombo = getModifierKeyCombo(e); + if (modifierKeyCombo === '') return targetCell; + if (modifierKeyCombo === 'Shift') { + if (!$selection) return undefined; + const { activeCellId } = $selection; + if (!activeCellId) return undefined; + return { type: 'data-cell', cellId: activeCellId }; + } + return undefined; + })(); if (!startingCell) return; - beginSelection({ selection, sheetElement, startingCell }); + + beginSelection({ selection, sheetElement, startingCell, targetCell }); } diff --git a/mathesar_ui/src/components/sheet/selection/selectionUtils.ts b/mathesar_ui/src/components/sheet/selection/selectionUtils.ts index 32558f0884..f79f894597 100644 --- a/mathesar_ui/src/components/sheet/selection/selectionUtils.ts +++ b/mathesar_ui/src/components/sheet/selection/selectionUtils.ts @@ -41,10 +41,12 @@ export function beginSelection({ selection, sheetElement, startingCell, + targetCell, }: { - startingCell: SheetCellDetails; selection: Writable; sheetElement: HTMLElement; + startingCell: SheetCellDetails; + targetCell: SheetCellDetails; }) { let previousTarget: HTMLElement | undefined; @@ -65,7 +67,7 @@ export function beginSelection({ window.removeEventListener('mouseup', finish); } - drawToCell(startingCell); + drawToCell(targetCell); sheetElement.addEventListener('mousemove', drawToPoint); window.addEventListener('mouseup', finish); } diff --git a/mathesar_ui/src/utils/pointerUtils.ts b/mathesar_ui/src/utils/pointerUtils.ts new file mode 100644 index 0000000000..82db98c0a7 --- /dev/null +++ b/mathesar_ui/src/utils/pointerUtils.ts @@ -0,0 +1,30 @@ +type ModifierKeyCombo = + | '' + // 1 modifier + | 'Alt' + | 'Ctrl' + | 'Meta' + | 'Shift' + // 2 modifiers + | 'Alt+Ctrl' + | 'Alt+Meta' + | 'Alt+Shift' + | 'Ctrl+Meta' + | 'Ctrl+Shift' + | 'Meta+Shift' + // 3 modifiers + | 'Alt+Ctrl+Meta' + | 'Alt+Ctrl+Shift' + | 'Alt+Meta+Shift' + | 'Ctrl+Meta+Shift' + // 4 modifiers + | 'Alt+Ctrl+Meta+Shift'; + +export function getModifierKeyCombo(e: MouseEvent) { + return [ + ...(e.altKey ? ['Alt'] : []), + ...(e.ctrlKey ? ['Ctrl'] : []), + ...(e.metaKey ? ['Meta'] : []), + ...(e.shiftKey ? ['Shift'] : []), + ].join('+') as ModifierKeyCombo; +} From 610c138a3fd7921456749f236e60baee0f087f7d Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Mon, 4 Mar 2024 15:37:30 -0500 Subject: [PATCH 45/85] Order a table's processed columns sooner --- .../src/stores/table-data/tabularData.ts | 44 +++++++++---------- .../src/systems/table-view/TableView.svelte | 7 +-- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/mathesar_ui/src/stores/table-data/tabularData.ts b/mathesar_ui/src/stores/table-data/tabularData.ts index a16572f80f..80700931a6 100644 --- a/mathesar_ui/src/stores/table-data/tabularData.ts +++ b/mathesar_ui/src/stores/table-data/tabularData.ts @@ -47,11 +47,8 @@ export class TabularData { columnsDataStore: ColumnsDataStore; - /** TODO_3037 eliminate `processedColumns` in favor of `orderedProcessedColumns` */ processedColumns: ProcessedColumnsStore; - orderedProcessedColumns: ProcessedColumnsStore; - constraintsDataStore: ConstraintsDataStore; recordsData: RecordsData; @@ -96,35 +93,34 @@ export class TabularData { this.recordsData, ); + this.table = props.table; + this.processedColumns = derived( [this.columnsDataStore.columns, this.constraintsDataStore], ([columns, constraintsData]) => - new Map( - columns.map((column, columnIndex) => [ - column.id, - processColumn({ - tableId: this.id, - column, - columnIndex, - constraints: constraintsData.constraints, - abstractTypeMap: props.abstractTypesMap, - hasEnhancedPrimaryKeyCell: props.hasEnhancedPrimaryKeyCell, - }), - ]), + orderProcessedColumns( + new Map( + columns.map((column, columnIndex) => [ + column.id, + processColumn({ + tableId: this.id, + column, + columnIndex, + constraints: constraintsData.constraints, + abstractTypeMap: props.abstractTypesMap, + hasEnhancedPrimaryKeyCell: props.hasEnhancedPrimaryKeyCell, + }), + ]), + ), + this.table, ), ); - this.table = props.table; - - this.orderedProcessedColumns = derived(this.processedColumns, (p) => - orderProcessedColumns(p, this.table), - ); - const plane = derived( - [this.recordsData.selectableRowsMap, this.orderedProcessedColumns], - ([selectableRowsMap, orderedProcessedColumns]) => { + [this.recordsData.selectableRowsMap, this.processedColumns], + ([selectableRowsMap, processedColumns]) => { const rowIds = new Series([...selectableRowsMap.keys()]); - const columns = [...orderedProcessedColumns.values()]; + const columns = [...processedColumns.values()]; const columnIds = new Series(columns.map((c) => String(c.id))); return new Plane(rowIds, columnIds); }, diff --git a/mathesar_ui/src/systems/table-view/TableView.svelte b/mathesar_ui/src/systems/table-view/TableView.svelte index 55f7822027..cb5052b530 100644 --- a/mathesar_ui/src/systems/table-view/TableView.svelte +++ b/mathesar_ui/src/systems/table-view/TableView.svelte @@ -22,7 +22,6 @@ import { toast } from '@mathesar/stores/toast'; import { getUserProfileStoreFromContext } from '@mathesar/stores/userProfile'; import { stringifyMapKeys } from '@mathesar/utils/collectionUtils'; - import { orderProcessedColumns } from '@mathesar/utils/tables'; import Body from './Body.svelte'; import Header from './header/Header.svelte'; import StatusPane from './StatusPane.svelte'; @@ -72,13 +71,9 @@ */ $: supportsTableInspector = context === 'page'; $: sheetColumns = (() => { - const orderedProcessedColumns = orderProcessedColumns( - $processedColumns, - table, - ); const columns = [ { column: { id: ID_ROW_CONTROL_COLUMN, name: 'ROW_CONTROL' } }, - ...orderedProcessedColumns.values(), + ...$processedColumns.values(), ]; if (hasNewColumnButton) { columns.push({ column: { id: ID_ADD_NEW_COLUMN, name: 'ADD_NEW' } }); From a8be26d442ed8e611ca25a46124b2275338754c0 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Wed, 20 Mar 2024 21:37:08 -0400 Subject: [PATCH 46/85] Clean up some dead code --- .../table-view/table-inspector/record/RecordMode.svelte | 5 ----- 1 file changed, 5 deletions(-) diff --git a/mathesar_ui/src/systems/table-view/table-inspector/record/RecordMode.svelte b/mathesar_ui/src/systems/table-view/table-inspector/record/RecordMode.svelte index bdc8fa956a..5f6f4967ed 100644 --- a/mathesar_ui/src/systems/table-view/table-inspector/record/RecordMode.svelte +++ b/mathesar_ui/src/systems/table-view/table-inspector/record/RecordMode.svelte @@ -20,11 +20,6 @@ ); $: selectedRowIds = $selection.rowIds; $: selectedRowCount = selectedRowIds.size; - - // TODO_3037 Need to calculate selectedRowIndices. This might be a deeper - // problem. Seems like we might need access to the row index here instead of - // the row identifier. - $: selectedRowIndices = [];
From 6c18123a8513493f8dc8eeecfdcf2d8a553ab5a9 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Wed, 20 Mar 2024 21:44:48 -0400 Subject: [PATCH 47/85] Remove TODO code comment --- .../column/column-extraction/ExtractColumnsModal.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/mathesar_ui/src/systems/table-view/table-inspector/column/column-extraction/ExtractColumnsModal.svelte b/mathesar_ui/src/systems/table-view/table-inspector/column/column-extraction/ExtractColumnsModal.svelte index b8dff19c08..7b482c9342 100644 --- a/mathesar_ui/src/systems/table-view/table-inspector/column/column-extraction/ExtractColumnsModal.svelte +++ b/mathesar_ui/src/systems/table-view/table-inspector/column/column-extraction/ExtractColumnsModal.svelte @@ -119,7 +119,6 @@ // unmounting this component. return; } - // TODO_3037 test to verify that selected columns are updated const columnIds = _columns.map((c) => String(c.id)); selection.update((s) => s.ofRowColumnIntersection(s.rowIds, columnIds)); } From 0f8cf9da299d2ab1a9fefa99d08a661c5dfd4222 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Wed, 20 Mar 2024 21:50:22 -0400 Subject: [PATCH 48/85] Improve code comments --- .../table-inspector/column/ColumnMode.svelte | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/mathesar_ui/src/systems/table-view/table-inspector/column/ColumnMode.svelte b/mathesar_ui/src/systems/table-view/table-inspector/column/ColumnMode.svelte index 0685493d86..337206c2dd 100644 --- a/mathesar_ui/src/systems/table-view/table-inspector/column/ColumnMode.svelte +++ b/mathesar_ui/src/systems/table-view/table-inspector/column/ColumnMode.svelte @@ -22,12 +22,18 @@ $: database = $currentDatabase; $: schema = $currentSchema; $: ({ processedColumns, selection } = $tabularData); - // TODO_3037 verify that table inspector shows selected columns $: selectedColumns = (() => { const ids = $selection.columnIds; const columns = []; for (const id of ids) { - // TODO_3037 add code comments explaining why this is necessary + // This is a little annoying that we need to parse the id as a string to + // a number. The reason is tricky. The cell selection system uses strings + // as column ids because they're more general purpose and can work with + // the string-based ids that the data explorer uses. However the table + // page stores processed columns with numeric ids. We could avoid this + // parsing by either making the selection system generic over the id type + // (which would be a pain, ergonomically), or by using string-based ids + // for columns in the table page too (which would require refactoring). const parsedId = parseInt(id, 10); const column = $processedColumns.get(parsedId); if (column !== undefined) { From 141d27ce3003138a0cf610a3da467f991f347f0e Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Thu, 4 Apr 2024 13:10:54 -0400 Subject: [PATCH 49/85] Fix positioning of new record message --- .../components/sheet/cells/SheetPositionableCell.svelte | 8 ++++++++ .../src/systems/table-view/row/NewRecordMessage.svelte | 3 +++ 2 files changed, 11 insertions(+) diff --git a/mathesar_ui/src/components/sheet/cells/SheetPositionableCell.svelte b/mathesar_ui/src/components/sheet/cells/SheetPositionableCell.svelte index 27b7806069..67c902a4e3 100644 --- a/mathesar_ui/src/components/sheet/cells/SheetPositionableCell.svelte +++ b/mathesar_ui/src/components/sheet/cells/SheetPositionableCell.svelte @@ -30,3 +30,11 @@
+ + diff --git a/mathesar_ui/src/systems/table-view/row/NewRecordMessage.svelte b/mathesar_ui/src/systems/table-view/row/NewRecordMessage.svelte index cb73fbe93f..3d807219c0 100644 --- a/mathesar_ui/src/systems/table-view/row/NewRecordMessage.svelte +++ b/mathesar_ui/src/systems/table-view/row/NewRecordMessage.svelte @@ -17,9 +17,12 @@ From e4f1ec50f63ee5c8799869069bd195609d919798 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Thu, 4 Apr 2024 13:11:29 -0400 Subject: [PATCH 50/85] Only render row control cell for rows with records --- mathesar_ui/src/systems/table-view/row/Row.svelte | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mathesar_ui/src/systems/table-view/row/Row.svelte b/mathesar_ui/src/systems/table-view/row/Row.svelte index cacc964df4..f22f7d333e 100644 --- a/mathesar_ui/src/systems/table-view/row/Row.svelte +++ b/mathesar_ui/src/systems/table-view/row/Row.svelte @@ -71,11 +71,11 @@ style="--cell-height:{rowHeightPx - 1}px;{styleString}" on:mousedown={checkAndCreateEmptyRow} > - - {#if rowHasRecord(row)} + {#if rowHasRecord(row)} + - {/if} - + + {/if} {#if isHelpTextRow(row)} From 218c59bd48a4939468765cee484db725dcec8447 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Thu, 4 Apr 2024 14:44:39 -0400 Subject: [PATCH 51/85] Add new record row when clicking placeholder --- .../src/components/sheet/selection/Series.ts | 11 +++++++++-- .../sheet/selection/SheetSelection.ts | 17 +++++++++++++++++ .../src/stores/table-data/tabularData.ts | 5 +++++ .../src/systems/table-view/StatusPane.svelte | 6 +----- .../src/systems/table-view/row/Row.svelte | 13 ++++++------- 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/mathesar_ui/src/components/sheet/selection/Series.ts b/mathesar_ui/src/components/sheet/selection/Series.ts index b0e897aab3..c7654a8f29 100644 --- a/mathesar_ui/src/components/sheet/selection/Series.ts +++ b/mathesar_ui/src/components/sheet/selection/Series.ts @@ -53,12 +53,19 @@ export default class Series { return this.values.slice(startIndex, endIndex + 1); } + /** + * Get the value at a specific index. + */ + at(index: number): Value | undefined { + return this.values[index]; + } + get first(): Value | undefined { - return this.values[0]; + return this.at(0); } get last(): Value | undefined { - return this.values[this.values.length - 1]; + return this.at(this.values.length - 1); } has(value: Value): boolean { diff --git a/mathesar_ui/src/components/sheet/selection/SheetSelection.ts b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts index f177794704..c116f90b1c 100644 --- a/mathesar_ui/src/components/sheet/selection/SheetSelection.ts +++ b/mathesar_ui/src/components/sheet/selection/SheetSelection.ts @@ -232,6 +232,23 @@ export default class SheetSelection { return this.withBasis(basisFromDataCells([firstCellId])); } + /** + * @returns a new selection formed by selecting the one cell that we think + * users are most likely to want selected after choosing to add a new record. + * + * We use the last row because that's where we add new records. If there is + * only one column, then we select the first cell in that column. Otherwise, + * we select the cell in the second column (because we assume the first column + * is probably a PK column which can't accept data entry.) + */ + ofNewRecordDataEntryCell(): SheetSelection { + const rowId = this.plane.rowIds.last; + if (!rowId) return this; + const columnId = this.plane.columnIds.at(1) ?? this.plane.columnIds.first; + if (!columnId) return this; + return this.ofOneCell(makeCellId(rowId, columnId)); + } + /** * @returns a new selection with all rows selected between (and including) the * provided rows. diff --git a/mathesar_ui/src/stores/table-data/tabularData.ts b/mathesar_ui/src/stores/table-data/tabularData.ts index 80700931a6..b54378caaf 100644 --- a/mathesar_ui/src/stores/table-data/tabularData.ts +++ b/mathesar_ui/src/stores/table-data/tabularData.ts @@ -215,6 +215,11 @@ export class TabularData { return this.refresh(); } + addEmptyRecord() { + void this.recordsData.addEmptyRecord(); + this.selection.update((s) => s.ofNewRecordDataEntryCell()); + } + destroy(): void { this.recordsData.destroy(); this.constraintsDataStore.destroy(); diff --git a/mathesar_ui/src/systems/table-view/StatusPane.svelte b/mathesar_ui/src/systems/table-view/StatusPane.svelte index 4edf76dbaf..b4cf4a8b7e 100644 --- a/mathesar_ui/src/systems/table-view/StatusPane.svelte +++ b/mathesar_ui/src/systems/table-view/StatusPane.svelte @@ -68,11 +68,7 @@ disabled={$isLoading} size="medium" appearance="primary" - on:click={() => { - void recordsData.addEmptyRecord(); - // // TODO_3037 - // selection.selectAndActivateFirstDataEntryCellInLastRow(); - }} + on:click={() => $tabularData.addEmptyRecord()} > {$_('new_record')} diff --git a/mathesar_ui/src/systems/table-view/row/Row.svelte b/mathesar_ui/src/systems/table-view/row/Row.svelte index f22f7d333e..ed31fd4b8d 100644 --- a/mathesar_ui/src/systems/table-view/row/Row.svelte +++ b/mathesar_ui/src/systems/table-view/row/Row.svelte @@ -48,12 +48,11 @@ /** Including whole row errors and individual cell errors */ $: hasAnyErrors = !!status?.errorsFromWholeRowAndCells?.length; - function checkAndCreateEmptyRow() { - // // TODO_3037 - // if (isPlaceholderRow(row)) { - // void recordsData.addEmptyRecord(); - // selection.selectAndActivateFirstDataEntryCellInLastRow(); - // } + function handleMouseDown(e: MouseEvent) { + if (isPlaceholderRow(row)) { + $tabularData.addEmptyRecord(); + e.stopPropagation(); // Prevents cell selection from starting + } } @@ -69,7 +68,7 @@ class:is-add-placeholder={isPlaceholderRow(row)} {...htmlAttributes} style="--cell-height:{rowHeightPx - 1}px;{styleString}" - on:mousedown={checkAndCreateEmptyRow} + on:mousedown={handleMouseDown} > {#if rowHasRecord(row)} Date: Wed, 10 Apr 2024 10:47:44 -0400 Subject: [PATCH 52/85] Implement SheetSelectionStore --- .../sheet/selection/SheetSelectionStore.ts | 66 ++++++++++++++++ .../src/stores/PreventableEffectsStore.ts | 76 +++++++++++++++++++ .../src/stores/table-data/tabularData.ts | 17 ++--- .../src/systems/data-explorer/QueryRunner.ts | 15 +--- 4 files changed, 151 insertions(+), 23 deletions(-) create mode 100644 mathesar_ui/src/components/sheet/selection/SheetSelectionStore.ts create mode 100644 mathesar_ui/src/stores/PreventableEffectsStore.ts diff --git a/mathesar_ui/src/components/sheet/selection/SheetSelectionStore.ts b/mathesar_ui/src/components/sheet/selection/SheetSelectionStore.ts new file mode 100644 index 0000000000..780e4ad684 --- /dev/null +++ b/mathesar_ui/src/components/sheet/selection/SheetSelectionStore.ts @@ -0,0 +1,66 @@ +import type { Readable, Writable } from 'svelte/store'; + +import { EventHandler } from '@mathesar/component-library'; +import PreventableEffectsStore from '@mathesar/stores/PreventableEffectsStore'; +import type Plane from './Plane'; +import SheetSelection from './SheetSelection'; + +export default class SheetSelectionStore + extends EventHandler<{ + focus: void; + }> + implements Writable +{ + private selection: PreventableEffectsStore; + + private cleanupFunctions: (() => void)[] = []; + + constructor(plane: Readable) { + super(); + this.selection = new PreventableEffectsStore(new SheetSelection(), { + focus: () => this.focus(), + }); + this.cleanupFunctions.push( + plane.subscribe((p) => this.selection.update((s) => s.forNewPlane(p))), + ); + } + + private focus(): void { + void this.dispatch('focus'); + } + + subscribe(run: (value: SheetSelection) => void): () => void { + return this.selection.subscribe(run); + } + + update(getNewValue: (oldValue: SheetSelection) => SheetSelection): void { + this.selection.update(getNewValue); + } + + /** + * Updates the selection while skipping the side effect of focusing the active + * cell. + */ + updateWithoutFocus( + getNewValue: (oldValue: SheetSelection) => SheetSelection, + ): void { + this.selection.update(getNewValue, { prevent: ['focus'] }); + } + + set(value: SheetSelection): void { + this.selection.set(value); + } + + /** + * Sets the selection while skipping the side effect of focusing the active + * cell. + */ + setWithoutFocus(value: SheetSelection): void { + this.selection.update(() => value, { prevent: ['focus'] }); + } + + destroy(): void { + super.destroy(); + this.cleanupFunctions.forEach((f) => f()); + } +} diff --git a/mathesar_ui/src/stores/PreventableEffectsStore.ts b/mathesar_ui/src/stores/PreventableEffectsStore.ts new file mode 100644 index 0000000000..109b33de66 --- /dev/null +++ b/mathesar_ui/src/stores/PreventableEffectsStore.ts @@ -0,0 +1,76 @@ +import { writable, type Writable } from 'svelte/store'; + +interface Effect { + name: EffectNames; + run: (v: Value) => void; +} + +/** + * This is a writable store that holds effects which run when the value changes. + * Unlike other effect mechanisms in Svelte, this store allows you to prevent + * certain effects from running when the value changes — and control over that + * prevention is delegated to the call site of the update. + * + * The use case for this store is when you have an imperative effect that you + * want to run _almost_ all the time. By default you want the effect to run. But + * for some cases (when you do a little extra work), then you can prevent the + * effect from running while performing an update. + * + * ## Example + * + * ```ts + * const store = new PreventableEffectsStore(0, { + * log: (v) => console.log(v), + * }); + * + * store.update((v) => v + 1); // logs 1 + * store.update((v) => v + 1, { prevent: ['log'] }); // does not log + * ``` + */ +export default class PreventableEffectsStore< + Value, + EffectNames extends string, +> { + private value: Writable; + + private effects: Effect[] = []; + + constructor( + initialValue: Value, + effectMap: Record void>, + ) { + this.value = writable(initialValue); + this.effects = Object.entries(effectMap).map(([name, run]) => ({ + name: name as EffectNames, + run: run as (v: Value) => void, + })); + } + + private runEffects( + value: Value, + options: { prevent?: EffectNames[] } = {}, + ): void { + this.effects + .filter(({ name }) => !options.prevent?.includes(name)) + .forEach(({ run }) => run(value)); + } + + subscribe(run: (value: Value) => void): () => void { + return this.value.subscribe(run); + } + + update( + getNewValue: (oldValue: Value) => Value, + options: { prevent?: EffectNames[] } = {}, + ): void { + this.value.update((oldValue) => { + const newValue = getNewValue(oldValue); + this.runEffects(newValue, options); + return newValue; + }); + } + + set(value: Value): void { + this.value.set(value); + } +} diff --git a/mathesar_ui/src/stores/table-data/tabularData.ts b/mathesar_ui/src/stores/table-data/tabularData.ts index b54378caaf..ebfa7c3156 100644 --- a/mathesar_ui/src/stores/table-data/tabularData.ts +++ b/mathesar_ui/src/stores/table-data/tabularData.ts @@ -7,7 +7,7 @@ import type { Column } from '@mathesar/api/types/tables/columns'; import { States } from '@mathesar/api/utils/requestUtils'; import Plane from '@mathesar/components/sheet/selection/Plane'; import Series from '@mathesar/components/sheet/selection/Series'; -import SheetSelection from '@mathesar/components/sheet/selection/SheetSelection'; +import SheetSelectionStore from '@mathesar/components/sheet/selection/SheetSelectionStore'; import type { AbstractTypesMap } from '@mathesar/stores/abstract-types/types'; import type { ShareConsumer } from '@mathesar/utils/shares'; import { orderProcessedColumns } from '@mathesar/utils/tables'; @@ -57,12 +57,10 @@ export class TabularData { isLoading: Readable; - selection: Writable; + selection: SheetSelectionStore; table: TableEntry; - private cleanupFunctions: (() => void)[] = []; - shareConsumer?: ShareConsumer; constructor(props: TabularDataProps) { @@ -122,16 +120,11 @@ export class TabularData { const rowIds = new Series([...selectableRowsMap.keys()]); const columns = [...processedColumns.values()]; const columnIds = new Series(columns.map((c) => String(c.id))); + // TODO_3037 incorporate placeholder row into plane return new Plane(rowIds, columnIds); }, ); - - // TODO_3037 add id of placeholder row to selection - this.selection = writable(new SheetSelection()); - - this.cleanupFunctions.push( - plane.subscribe((p) => this.selection.update((s) => s.forNewPlane(p))), - ); + this.selection = new SheetSelectionStore(plane); this.isLoading = derived( [ @@ -224,7 +217,7 @@ export class TabularData { this.recordsData.destroy(); this.constraintsDataStore.destroy(); this.columnsDataStore.destroy(); - this.cleanupFunctions.forEach((f) => f()); + this.selection.destroy(); } } diff --git a/mathesar_ui/src/systems/data-explorer/QueryRunner.ts b/mathesar_ui/src/systems/data-explorer/QueryRunner.ts index baee9cb275..51c100a931 100644 --- a/mathesar_ui/src/systems/data-explorer/QueryRunner.ts +++ b/mathesar_ui/src/systems/data-explorer/QueryRunner.ts @@ -12,7 +12,7 @@ import { ApiMultiError } from '@mathesar/api/utils/errors'; import type { RequestStatus } from '@mathesar/api/utils/requestUtils'; import Plane from '@mathesar/components/sheet/selection/Plane'; import Series from '@mathesar/components/sheet/selection/Series'; -import SheetSelection from '@mathesar/components/sheet/selection/SheetSelection'; +import SheetSelectionStore from '@mathesar/components/sheet/selection/SheetSelectionStore'; import type { AbstractTypesMap } from '@mathesar/stores/abstract-types/types'; import { fetchQueryResults, runQuery } from '@mathesar/stores/queries'; import Pagination from '@mathesar/utils/Pagination'; @@ -67,7 +67,7 @@ export default class QueryRunner { /** Keys are row selection ids */ selectableRowsMap: Readable>; - selection: Writable; + selection: SheetSelectionStore; inspector: QueryInspector; @@ -81,8 +81,6 @@ export default class QueryRunner { private shareConsumer?: ShareConsumer; - private cleanupFunctions: (() => void)[] = []; - constructor({ query, abstractTypeMap, @@ -120,12 +118,7 @@ export default class QueryRunner { return new Plane(rowIds, columnIds); }, ); - - this.selection = writable(new SheetSelection()); - - this.cleanupFunctions.push( - plane.subscribe((p) => this.selection.update((s) => s.forNewPlane(p))), - ); + this.selection = new SheetSelectionStore(plane); this.inspector = new QueryInspector(this.query); } @@ -279,6 +272,6 @@ export default class QueryRunner { destroy(): void { this.runPromise?.cancel(); - this.cleanupFunctions.forEach((fn) => fn()); + this.selection.destroy(); } } From 26a8c51d32f2afc3ca774e0453d5ca53318f9831 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Wed, 10 Apr 2024 10:53:37 -0400 Subject: [PATCH 53/85] Fix some linting errors --- .../components/inspector/cell/CellInspector.svelte | 2 +- .../src/components/sheet/cells/SheetDataCell.svelte | 12 ++++++------ .../table-inspector/record/RowActions.svelte | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mathesar_ui/src/components/inspector/cell/CellInspector.svelte b/mathesar_ui/src/components/inspector/cell/CellInspector.svelte index 0e6c3e8ad0..7f205da4cd 100644 --- a/mathesar_ui/src/components/inspector/cell/CellInspector.svelte +++ b/mathesar_ui/src/components/inspector/cell/CellInspector.svelte @@ -1,8 +1,8 @@ diff --git a/mathesar_ui/src/systems/table-view/table-inspector/record/RowActions.svelte b/mathesar_ui/src/systems/table-view/table-inspector/record/RowActions.svelte index 2a3acc3b46..4c66583175 100644 --- a/mathesar_ui/src/systems/table-view/table-inspector/record/RowActions.svelte +++ b/mathesar_ui/src/systems/table-view/table-inspector/record/RowActions.svelte @@ -31,7 +31,7 @@ const id = takeFirstAndOnly(selectedRowIds); if (!id) return undefined; const row = $selectableRowsMap.get(id); - if (!row) return; + if (!row) return undefined; try { const recordId = getPkValueInRecord(row.record, $columns); return $storeToGetRecordPageUrl({ recordId }); From 10f0aa283cadec7b18d1e6d955aee6f0b6740134 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Wed, 10 Apr 2024 10:39:23 -0400 Subject: [PATCH 54/85] Handle CellWrapper focus in pure CSS --- .../data-types/components/CellWrapper.svelte | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte index e97c8f8646..e51a0011d1 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte @@ -23,8 +23,6 @@ */ export let horizontalAlignment: HorizontalAlignment = 'left'; - let isFocused = false; - function shouldAutoFocus( _isActive: boolean, _mode: 'edit' | 'default', @@ -60,23 +58,6 @@ } $: void handleStateChange(isActive, mode); - function handleFocus() { - isFocused = true; - // Note: you might think we ought to automatically activate the cell at this - // point to ensure that we don't have any cells which are focused but not - // active. I tried this and it caused bugs with selecting columns and rows - // via header cells. I didn't want to spend time tracking them down because - // we are planning to refactor the cell selection logic soon anyway. It - // doesn't _seem_ like we have any code which focuses the cell without - // activating it, but it would be nice to eventually build a better - // guarantee into the codebase which prevents cells from being focused - // without being activated. - } - - function handleBlur() { - isFocused = false; - } - function handleCopy(e: ClipboardEvent) { if (e.target !== element) { // When the user copies text _within_ a cell (e.g. from within an input @@ -93,7 +74,6 @@
@@ -146,7 +124,7 @@ box-shadow: 0 0 0 2px var(--slate-300); border-radius: 2px; - &.is-focused { + &:focus { box-shadow: 0 0 0 2px var(--sky-700); } } From bbd1e64362c0ff9d4082f6e770f720a6261c6c6e Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Wed, 10 Apr 2024 12:28:14 -0400 Subject: [PATCH 55/85] Improve cell focus behavior --- .../data-types/components/CellWrapper.svelte | 48 +++++++------------ mathesar_ui/src/components/sheet/Sheet.svelte | 22 +++++++-- .../ExtractColumnsModal.svelte | 4 +- 3 files changed, 40 insertions(+), 34 deletions(-) diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte index e51a0011d1..fe9f1e0ebe 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte @@ -1,7 +1,6 @@
String(c.id)); - selection.update((s) => s.ofRowColumnIntersection(s.rowIds, columnIds)); + selection.updateWithoutFocus((s) => + s.ofRowColumnIntersection(s.rowIds, columnIds), + ); } $: handleColumnsChange($columns); From 894065a2cf68e1494c92466f189cc817fc7b7c4a Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Wed, 10 Apr 2024 12:52:20 -0400 Subject: [PATCH 56/85] Fix mousedown event on cell input element --- .../data-types/components/CellWrapper.svelte | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte b/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte index fe9f1e0ebe..29594fe1d4 100644 --- a/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte +++ b/mathesar_ui/src/components/cell-fabric/data-types/components/CellWrapper.svelte @@ -1,8 +1,12 @@
Date: Wed, 10 Apr 2024 13:08:16 -0400 Subject: [PATCH 57/85] Prevent column resize from altering selection --- mathesar_ui/src/component-library/common/actions/slider.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mathesar_ui/src/component-library/common/actions/slider.ts b/mathesar_ui/src/component-library/common/actions/slider.ts index 695a591db0..8a929d3b01 100644 --- a/mathesar_ui/src/component-library/common/actions/slider.ts +++ b/mathesar_ui/src/component-library/common/actions/slider.ts @@ -128,6 +128,8 @@ export default function slider( } function start(e: MouseEvent | TouchEvent) { + e.stopPropagation(); + e.preventDefault(); opts.onStart(); startingValue = opts.getStartingValue(); startingPosition = getPosition(e, opts.axis); From 2dff73f8ca7f47b7b6f5ce54b729b5d1e362a7b7 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Wed, 10 Apr 2024 15:07:59 -0400 Subject: [PATCH 58/85] Improve parsing/serialization of cellId values --- mathesar_ui/src/components/sheet/cellIds.ts | 24 ++++++++------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/mathesar_ui/src/components/sheet/cellIds.ts b/mathesar_ui/src/components/sheet/cellIds.ts index fd6a263637..c39c1077d9 100644 --- a/mathesar_ui/src/components/sheet/cellIds.ts +++ b/mathesar_ui/src/components/sheet/cellIds.ts @@ -2,29 +2,23 @@ import { map } from 'iter-tools'; import { cartesianProduct } from '@mathesar/utils/iterUtils'; -const CELL_ID_DELIMITER = '-'; - -/** - * We can serialize a cell id this way only because we're confident that the - * rowId will never contain the delimiter. Some columnIds _do_ contain - * delimiters (e.g. in the Data Explorer), but that's okay because we can still - * separate the values based on the first delimiter. - */ export function makeCellId(rowId: string, columnId: string): string { - return `${rowId}${CELL_ID_DELIMITER}${columnId}`; + return JSON.stringify([rowId, columnId]); } export function parseCellId(cellId: string): { rowId: string; columnId: string; } { - const delimiterIndex = cellId.indexOf(CELL_ID_DELIMITER); - if (delimiterIndex === -1) { - throw new Error(`Unable to parse cell id without a delimiter: ${cellId}.`); + try { + const [rowId, columnId] = JSON.parse(cellId) as unknown[]; + if (typeof rowId !== 'string' || typeof columnId !== 'string') { + throw new Error(); + } + return { rowId, columnId }; + } catch { + throw new Error(`Unable to parse cell id: ${cellId}.`); } - const rowId = cellId.slice(0, delimiterIndex); - const columnId = cellId.slice(delimiterIndex + 1); - return { rowId, columnId }; } export function makeCells( From 0938b6582bf5a5650b91a5f99987d3bf40c46478 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Wed, 10 Apr 2024 16:16:14 -0400 Subject: [PATCH 59/85] Add placeholder row to Plane --- mathesar_ui/src/stores/table-data/display.ts | 31 +++++++++++++++---- .../src/stores/table-data/tabularData.ts | 11 ++++--- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/mathesar_ui/src/stores/table-data/display.ts b/mathesar_ui/src/stores/table-data/display.ts index 0a969e23db..d4f98202ce 100644 --- a/mathesar_ui/src/stores/table-data/display.ts +++ b/mathesar_ui/src/stores/table-data/display.ts @@ -1,9 +1,10 @@ -import { writable, derived } from 'svelte/store'; -import type { Writable, Readable } from 'svelte/store'; +import type { Readable, Writable } from 'svelte/store'; +import { derived, writable } from 'svelte/store'; + import { WritableMap } from '@mathesar-component-library'; -import type { Meta } from './meta'; import type { ColumnsDataStore } from './columns'; -import { type Row, type RecordsData, filterRecordRows } from './records'; +import type { Meta } from './meta'; +import { filterRecordRows, type RecordsData, type Row } from './records'; // @deprecated export const DEFAULT_COLUMN_WIDTH = 160; @@ -63,6 +64,8 @@ export class Display { displayableRecords: Readable; + placeholderRowId: Readable; + constructor( meta: Meta, columnsDataStore: ColumnsDataStore, @@ -98,6 +101,9 @@ export class Display { ), ); + const placeholderRowId = writable(''); + this.placeholderRowId = placeholderRowId; + const { savedRecordRowsWithGroupHeaders, newRecords } = this.recordsData; this.displayableRecords = derived( [savedRecordRowsWithGroupHeaders, newRecords], @@ -120,11 +126,24 @@ export class Display { }) .concat($newRecords); } - allRecords = allRecords.concat({ + const placeholderRow = { ...this.recordsData.getNewEmptyRecord(), rowIndex: savedRecords.length + $newRecords.length, isAddPlaceholder: true, - }); + }; + + // This is really hacky! We have a side effect (mutating state) within a + // derived store, which I don't like. I put this here during a large + // refactor of the cell selection code because the Plane needs to know + // the id of the placeholder row since cell selection behaves + // differently in the placeholder row. I think we have some major + // refactoring to do across all the code that handles "rows" and + // "records" and things like that. There is a ton of mess there and I + // didn't want to lump any of that refactoring into an already-large + // refactor. + placeholderRowId.set(placeholderRow.identifier); + + allRecords = allRecords.concat(placeholderRow); return allRecords; }, ); diff --git a/mathesar_ui/src/stores/table-data/tabularData.ts b/mathesar_ui/src/stores/table-data/tabularData.ts index ebfa7c3156..22fa91bd52 100644 --- a/mathesar_ui/src/stores/table-data/tabularData.ts +++ b/mathesar_ui/src/stores/table-data/tabularData.ts @@ -115,13 +115,16 @@ export class TabularData { ); const plane = derived( - [this.recordsData.selectableRowsMap, this.processedColumns], - ([selectableRowsMap, processedColumns]) => { + [ + this.recordsData.selectableRowsMap, + this.processedColumns, + this.display.placeholderRowId, + ], + ([selectableRowsMap, processedColumns, placeholderRowId]) => { const rowIds = new Series([...selectableRowsMap.keys()]); const columns = [...processedColumns.values()]; const columnIds = new Series(columns.map((c) => String(c.id))); - // TODO_3037 incorporate placeholder row into plane - return new Plane(rowIds, columnIds); + return new Plane(rowIds, columnIds, placeholderRowId); }, ); this.selection = new SheetSelectionStore(plane); From 2f9cc960854f48ffc2efe60fa2fb0664334b41a8 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Wed, 10 Apr 2024 16:31:37 -0400 Subject: [PATCH 60/85] Allow typing into placeholder cells --- mathesar_ui/src/systems/table-view/row/Row.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mathesar_ui/src/systems/table-view/row/Row.svelte b/mathesar_ui/src/systems/table-view/row/Row.svelte index ed31fd4b8d..4cbbd0820b 100644 --- a/mathesar_ui/src/systems/table-view/row/Row.svelte +++ b/mathesar_ui/src/systems/table-view/row/Row.svelte @@ -135,9 +135,9 @@ cursor: pointer; :global( - [data-sheet-element='data-cell']:not(.is-active) + [data-sheet-element='data-cell'] .cell-fabric - .cell-wrapper + .cell-wrapper:not(.is-edit-mode) > * ) { visibility: hidden; From 659c9e210cfd601398d67b434be9024f50481d17 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Wed, 10 Apr 2024 21:38:58 -0400 Subject: [PATCH 61/85] Remove some dead code --- .../src/systems/table-view/TableView.svelte | 30 ++++--------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/mathesar_ui/src/systems/table-view/TableView.svelte b/mathesar_ui/src/systems/table-view/TableView.svelte index cb5052b530..64bc2cdb59 100644 --- a/mathesar_ui/src/systems/table-view/TableView.svelte +++ b/mathesar_ui/src/systems/table-view/TableView.svelte @@ -87,42 +87,22 @@ ]); $: showTableInspector = $isTableInspectorVisible && supportsTableInspector; - function checkAndReinstateFocusOnActiveCell(e: Event) { - // // TODO_3037 Figure out what is actually broken without this code. - // //Better document why we need id. - // - // const target = e.target as HTMLElement; - // if (!target.closest('[data-sheet-element="data-cell"')) { - // if ($activeCell) { - // selection.focusCell( - // // TODO make sure to use getRowSelectionId instead of rowIndex - // { rowIndex: $activeCell.rowIndex }, - // { id: Number($activeCell.columnId) }, - // ); - // } - // } - } - - // function selectAndActivateFirstCellOnTableLoad( - // _isLoading: boolean, - // _selection: TabularDataSelection, - // _context: Context, - // ) { + // // TODO_3037 + // function selectFirstCellOnTableLoad(_isLoading: boolean, _context: Context) { // // We only activate the first cell on the page, not in the widget. Doing so // // on the widget causes the cell to focus and the page to scroll down to // // bring that element into view. // if (_context !== 'widget' && !_isLoading) { - // _selection.selectAndActivateFirstCellIfExists(); + // selection.update((s) => s.ofFirstDataCell()); // } // } - // // TODO_3037 Figure out what is actually broken without this code. - // $: void selectAndActivateFirstCellOnTableLoad($isLoading, selection, context); + // $: void selectFirstCellOnTableLoad($isLoading, context);
-
+
{#if $processedColumns.size} Date: Wed, 10 Apr 2024 21:52:14 -0400 Subject: [PATCH 62/85] Hide column resize handles during cell selection --- mathesar_ui/src/components/sheet/Sheet.svelte | 9 ++++++++- mathesar_ui/src/components/sheet/SheetCellResizer.svelte | 8 +++++++- .../src/components/sheet/selection/selectionUtils.ts | 4 ++++ mathesar_ui/src/components/sheet/utils.ts | 1 + 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/mathesar_ui/src/components/sheet/Sheet.svelte b/mathesar_ui/src/components/sheet/Sheet.svelte index dde0f20681..d55728ca30 100644 --- a/mathesar_ui/src/components/sheet/Sheet.svelte +++ b/mathesar_ui/src/components/sheet/Sheet.svelte @@ -90,6 +90,7 @@ horizontalScrollOffset: writable(horizontalScrollOffset), scrollOffset: writable(scrollOffset), paddingRight: writable(paddingRight), + selectionInProgress: writable(false), }; // Setting these values in stores for reactivity in context @@ -154,7 +155,13 @@ // cells in the column. e.preventDefault(); - beginSelection({ selection, sheetElement, startingCell, targetCell }); + beginSelection({ + selection, + sheetElement, + startingCell, + targetCell, + selectionInProgress: stores.selectionInProgress, + }); } async function focusActiveCell() { diff --git a/mathesar_ui/src/components/sheet/SheetCellResizer.svelte b/mathesar_ui/src/components/sheet/SheetCellResizer.svelte index db09ee920f..e4c94fa140 100644 --- a/mathesar_ui/src/components/sheet/SheetCellResizer.svelte +++ b/mathesar_ui/src/components/sheet/SheetCellResizer.svelte @@ -4,16 +4,19 @@ type SheetColumnIdentifierKey = $$Generic; - const { api } = getSheetContext(); + const { api, stores } = getSheetContext(); export let minColumnWidth = 50; export let columnIdentifierKey: SheetColumnIdentifierKey; let isResizing = false; + + $: ({ selectionInProgress } = stores);
api.getColumnWidth(columnIdentifierKey), @@ -53,4 +56,7 @@ .column-resizer:not(:hover):not(.is-resizing) .indicator { display: none; } + .column-resizer.selection-in-progress { + display: none; + } diff --git a/mathesar_ui/src/components/sheet/selection/selectionUtils.ts b/mathesar_ui/src/components/sheet/selection/selectionUtils.ts index f79f894597..b8a54af1ab 100644 --- a/mathesar_ui/src/components/sheet/selection/selectionUtils.ts +++ b/mathesar_ui/src/components/sheet/selection/selectionUtils.ts @@ -42,11 +42,13 @@ export function beginSelection({ sheetElement, startingCell, targetCell, + selectionInProgress, }: { selection: Writable; sheetElement: HTMLElement; startingCell: SheetCellDetails; targetCell: SheetCellDetails; + selectionInProgress: Writable; }) { let previousTarget: HTMLElement | undefined; @@ -65,8 +67,10 @@ export function beginSelection({ function finish() { sheetElement.removeEventListener('mousemove', drawToPoint); window.removeEventListener('mouseup', finish); + selectionInProgress.set(false); } + selectionInProgress.set(true); drawToCell(targetCell); sheetElement.addEventListener('mousemove', drawToPoint); window.addEventListener('mouseup', finish); diff --git a/mathesar_ui/src/components/sheet/utils.ts b/mathesar_ui/src/components/sheet/utils.ts index 4f3915d8d0..489f0cf1bc 100644 --- a/mathesar_ui/src/components/sheet/utils.ts +++ b/mathesar_ui/src/components/sheet/utils.ts @@ -15,6 +15,7 @@ export interface SheetContextStores { horizontalScrollOffset: Readable; scrollOffset: Readable; paddingRight: Readable; + selectionInProgress: Readable; } export interface SheetContext { From d7f91d89a567df92a9d6a391af7a209ebdb9da38 Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Wed, 10 Apr 2024 22:03:37 -0400 Subject: [PATCH 63/85] Use default cursor everywhere in sheet during selection --- mathesar_ui/src/components/sheet/Sheet.svelte | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/mathesar_ui/src/components/sheet/Sheet.svelte b/mathesar_ui/src/components/sheet/Sheet.svelte index d55728ca30..1899d5aaaf 100644 --- a/mathesar_ui/src/components/sheet/Sheet.svelte +++ b/mathesar_ui/src/components/sheet/Sheet.svelte @@ -84,13 +84,14 @@ }, }; + const selectionInProgress = writable(false); const stores = { columnStyleMap: writable(columnStyleMap), rowWidth: writable(rowWidth), horizontalScrollOffset: writable(horizontalScrollOffset), scrollOffset: writable(scrollOffset), paddingRight: writable(paddingRight), - selectionInProgress: writable(false), + selectionInProgress, }; // Setting these values in stores for reactivity in context @@ -160,7 +161,7 @@ sheetElement, startingCell, targetCell, - selectionInProgress: stores.selectionInProgress, + selectionInProgress, }); } @@ -177,6 +178,7 @@ class:has-border={hasBorder} class:uses-virtual-list={usesVirtualList} class:set-to-row-width={restrictWidthToRowWidth} + class:selection-in-progress={$selectionInProgress} {style} on:mousedown={handleMouseDown} on:focusin={enableClipboard} @@ -229,5 +231,9 @@ :global([data-sheet-element='data-row']) { transition: all 0.2s cubic-bezier(0, 0, 0.2, 1); } + + &.selection-in-progress :global(*) { + cursor: default; + } } From 1557b559ab39924d1eac99a0c6daa5aa969ef30f Mon Sep 17 00:00:00 2001 From: Sean Colsen Date: Wed, 10 Apr 2024 22:16:13 -0400 Subject: [PATCH 64/85] Fix drag to re-order columns --- .../systems/table-view/header/Header.svelte | 39 +++++++++---------- .../header/drag-and-drop/Draggable.svelte | 13 ++++--- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/mathesar_ui/src/systems/table-view/header/Header.svelte b/mathesar_ui/src/systems/table-view/header/Header.svelte index ad28034c9f..11a7f77b41 100644 --- a/mathesar_ui/src/systems/table-view/header/Header.svelte +++ b/mathesar_ui/src/systems/table-view/header/Header.svelte @@ -108,28 +108,25 @@ {#each [...$processedColumns] as [columnId, processedColumn] (columnId)} {@const isSelected = $selection.columnIds.has(String(columnId))} - -
- dragColumn()} - column={processedColumn} - {selection} + dragColumn()} + column={processedColumn} + {selection} + > + dropColumn(processedColumn)} + on:dragover={(e) => e.preventDefault()} + {locationOfFirstDraggedColumn} + columnLocation={columnOrderString.indexOf(columnId.toString())} + {isSelected} > - dropColumn(processedColumn)} - on:dragover={(e) => e.preventDefault()} - {locationOfFirstDraggedColumn} - columnLocation={columnOrderString.indexOf(columnId.toString())} - {isSelected} - > - - - - - - - -
+ + + + + + +
{/each} diff --git a/mathesar_ui/src/systems/table-view/header/drag-and-drop/Draggable.svelte b/mathesar_ui/src/systems/table-view/header/drag-and-drop/Draggable.svelte index 1347bd2f5a..0c46402caa 100644 --- a/mathesar_ui/src/systems/table-view/header/drag-and-drop/Draggable.svelte +++ b/mathesar_ui/src/systems/table-view/header/drag-and-drop/Draggable.svelte @@ -7,17 +7,18 @@ export let selection: Writable; export let column: ProcessedColumn; - // // TODO_3037: Verify that we're not losing functionality here by removing - // `selectionInProgress` logic - // - // $: draggable = !selectionInProgress && selection && - // selection.isCompleteColumnSelected(column); - $: draggable = $selection.fullySelectedColumnIds.has(String(column.id)); + + function handleMouseDown(event: MouseEvent) { + if (draggable) { + event.stopPropagation(); + } + }
Date: Wed, 10 Apr 2024 23:28:04 -0400 Subject: [PATCH 65/85] Auto-activate inspector tabs based on selection --- mathesar_ui/src/components/sheet/Sheet.svelte | 7 +++ .../pages/exploration/ExplorationPage.svelte | 13 +++- .../systems/data-explorer/DataExplorer.svelte | 8 ++- .../systems/data-explorer/QueryInspector.ts | 10 --- .../src/systems/data-explorer/QueryRunner.ts | 10 --- .../ExplorationInspector.svelte | 13 +++- .../WithExplorationInspector.svelte | 5 ++ .../result-pane/ResultHeaderCell.svelte | 3 - .../result-pane/ResultPane.svelte | 11 +++- .../data-explorer/result-pane/Results.svelte | 5 ++ .../src/systems/table-view/TableView.svelte | 6 +- .../table-inspector/TableInspector.svelte | 63 +++++++------------ .../table-inspector/WithTableInspector.svelte | 6 +- mathesar_ui/src/utils/MessageBus.ts | 39 ++++++++++++ 14 files changed, 124 insertions(+), 75 deletions(-) create mode 100644 mathesar_ui/src/utils/MessageBus.ts diff --git a/mathesar_ui/src/components/sheet/Sheet.svelte b/mathesar_ui/src/components/sheet/Sheet.svelte index 1899d5aaaf..4a221044d1 100644 --- a/mathesar_ui/src/components/sheet/Sheet.svelte +++ b/mathesar_ui/src/components/sheet/Sheet.svelte @@ -5,6 +5,7 @@ import { ImmutableMap } from '@mathesar-component-library/types'; import type { ClipboardHandler } from '@mathesar/stores/clipboard'; import { getClipboardHandlerStoreFromContext } from '@mathesar/stores/clipboard'; + import type MessageBus from '@mathesar/utils/MessageBus'; import { getModifierKeyCombo } from '@mathesar/utils/pointerUtils'; import { beginSelection, @@ -30,6 +31,8 @@ export let hasPaddingRight = false; export let clipboardHandler: ClipboardHandler | undefined = undefined; export let selection: SheetSelectionStore | undefined = undefined; + export let cellSelectionStarted: MessageBus | undefined = + undefined; export let getColumnIdentifier: ( c: SheetColumnType, @@ -163,6 +166,10 @@ targetCell, selectionInProgress, }); + + if (startingCell === targetCell) { + cellSelectionStarted?.send(targetCell); + } } async function focusActiveCell() { diff --git a/mathesar_ui/src/pages/exploration/ExplorationPage.svelte b/mathesar_ui/src/pages/exploration/ExplorationPage.svelte index b71566c299..17c6a4ac3e 100644 --- a/mathesar_ui/src/pages/exploration/ExplorationPage.svelte +++ b/mathesar_ui/src/pages/exploration/ExplorationPage.svelte @@ -1,8 +1,10 @@