Compare commits

...
Author SHA1 Message Date
Wroud 438475a99c chore: editing draft 2025-05-30 22:19:04 +08:00
Wroud f967a20edf dbeaver/pro#5802 feat: table api 2025-05-29 19:41:38 +08:00
26 changed files with 1001 additions and 2 deletions
+31 -1
View File
@@ -384,6 +384,20 @@ __metadata:
languageName: unknown
linkType: soft
"@dbeaver/table-data@workspace:../common-typescript/@dbeaver/table-data":
version: 0.0.0-use.local
resolution: "@dbeaver/table-data@workspace:../common-typescript/@dbeaver/table-data"
dependencies:
"@dbeaver/cli": "workspace:^"
"@dbeaver/tsconfig": "workspace:^"
async-mutex: "npm:^0"
nanoevents: "npm:^9"
rimraf: "npm:^6"
typescript: "npm:^5"
vitest: "npm:^3"
languageName: unknown
linkType: soft
"@dbeaver/tsconfig@workspace:../common-typescript/@dbeaver/tsconfig, @dbeaver/tsconfig@workspace:^":
version: 0.0.0-use.local
resolution: "@dbeaver/tsconfig@workspace:../common-typescript/@dbeaver/tsconfig"
@@ -1988,6 +2002,15 @@ __metadata:
languageName: node
linkType: hard
"async-mutex@npm:^0":
version: 0.5.0
resolution: "async-mutex@npm:0.5.0"
dependencies:
tslib: "npm:^2.4.0"
checksum: 10c0/9096e6ad6b674c894d8ddd5aa4c512b09bb05931b8746ebd634952b05685608b2b0820ed5c406e6569919ff5fe237ab3c491e6f2887d6da6b6ba906db3ee9c32
languageName: node
linkType: hard
"axe-core@npm:^4.10.2":
version: 4.10.3
resolution: "axe-core@npm:4.10.3"
@@ -5127,6 +5150,13 @@ __metadata:
languageName: node
linkType: hard
"nanoevents@npm:^9":
version: 9.1.0
resolution: "nanoevents@npm:9.1.0"
checksum: 10c0/5fb48e6fc1d3102daddaeffd6eada907c25b1c8554dcc648e9cb0a72979a1ab3ee56ffa2d2b3e566a8e561a9e2992a3783493c61cfaaa096b2986d56dbbc1ca5
languageName: node
linkType: hard
"nanoid@npm:^3.3.8":
version: 3.3.11
resolution: "nanoid@npm:3.3.11"
@@ -6512,7 +6542,7 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:^2, tslib@npm:^2.3.0":
"tslib@npm:^2, tslib@npm:^2.3.0, tslib@npm:^2.4.0":
version: 2.8.1
resolution: "tslib@npm:2.8.1"
checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
@@ -0,0 +1 @@
# @dbeaver/table-data
@@ -0,0 +1,40 @@
{
"name": "@dbeaver/table-data",
"type": "module",
"sideEffects": [],
"exports": {
".": "./lib/index.js",
"./*": "./lib/*.js"
},
"scripts": {
"clear": "rimraf lib",
"build": "tsc -b",
"test": "dbeaver-test"
},
"files": [
"package.json",
"LICENSE",
"README.md",
"CHANGELOG.md",
"lib",
"!lib/**/*.d.ts.map",
"!lib/**/*.test.js",
"!lib/**/*.test.d.ts",
"!lib/**/*.test.d.ts.map",
"!lib/**/*.test.js.map",
"!lib/tests",
"!.tsbuildinfo"
],
"packageManager": "yarn@4.6.0",
"devDependencies": {
"@dbeaver/cli": "workspace:^",
"@dbeaver/tsconfig": "workspace:^",
"rimraf": "^6",
"typescript": "^5",
"vitest": "^3"
},
"dependencies": {
"async-mutex": "^0",
"nanoevents": "^9"
}
}
@@ -0,0 +1,18 @@
import type { ITableDataset } from './interfaces/ITableDataset.js';
import type { ITableDatasetManager } from './interfaces/ITableDatasetManager.js';
export class TableDatasetManager<TColumn, TValue> implements ITableDatasetManager<TColumn, TValue> {
get datasets(): ITableDataset<TColumn, TValue>[] {
return this.#datasets;
}
#datasets: ITableDataset<TColumn, TValue>[] = [];
constructor() {
this.#datasets = [];
}
setDatasets(datasets: ITableDataset<TColumn, TValue>[]): void {
this.#datasets = datasets;
}
}
@@ -0,0 +1,110 @@
import { Mutex } from 'async-mutex';
import type { ITableSource } from './interfaces/ITableSource.js';
import { createNanoEvents, type Emitter, type Unsubscribe } from 'nanoevents';
import type { TableSourceEvents } from './interfaces/TableSourceEvents.js';
import type { ITableDatasetManager } from './interfaces/ITableDatasetManager.js';
import type { ITableSourceOptions } from './interfaces/ITableSourceOptions.js';
export abstract class TableSource<TColumn, TValue, TOptions extends ITableSourceOptions = ITableSourceOptions>
implements ITableSource<TColumn, TValue, TOptions>
{
get isLoading(): boolean {
return this.#loadingInProgress;
}
get isOutdated(): boolean {
return this.#outdated;
}
error: Error | null;
options: Partial<TOptions>;
#outdated: boolean;
#loadingInProgress = false;
#loadingPending = false;
readonly #mutex: Mutex;
readonly #emitter: Emitter<TableSourceEvents>;
constructor(readonly datasetManager: ITableDatasetManager<TColumn, TValue>) {
this.options = {};
this.error = null;
this.#outdated = false;
this.#mutex = new Mutex();
this.#emitter = createNanoEvents();
}
setOptions(options: TOptions): void {
this.options = options;
this.setOutdated();
}
setOutdated(): void {
this.setOutdatedValue(true);
if (this.#mutex.isLocked()) {
this.#mutex.runExclusive(() => {
this.setOutdatedValue(true);
});
}
}
async save(): Promise<void> {
const release = await this.#mutex.acquire();
try {
this.setError(null);
await this.saveData();
this.emit('saved');
} catch (error) {
this.setError(error instanceof Error ? error : new Error(String(error)));
throw this.error;
} finally {
release();
}
}
async load(): Promise<void> {
if (this.#loadingInProgress) {
this.#loadingPending = true;
return await this.#mutex.waitForUnlock();
}
this.setLoadingInProgress(true);
try {
// this waits for any save to finish, then runs loadData
await this.#mutex.runExclusive(async () => {
do {
this.#loadingPending = false;
this.setError(null);
await this.loadData();
this.emit('data');
} while (this.#loadingPending);
});
} catch (error) {
this.setError(error instanceof Error ? error : new Error(String(error)));
throw this.error;
} finally {
this.setOutdatedValue(false);
this.setLoadingInProgress(false);
}
}
on<TEvent extends keyof TableSourceEvents>(event: TEvent, listener: TableSourceEvents[TEvent]): Unsubscribe {
return this.#emitter.on(event, listener);
}
protected setLoadingInProgress(state: boolean): void {
this.emit('loading', state);
this.#loadingInProgress = state;
}
protected setError(error: Error | null): void {
this.error = error;
this.emit('error', error);
}
protected setOutdatedValue(state: boolean): void {
this.#outdated = state;
this.emit('outdated', state);
}
protected emit<TEvent extends keyof TableSourceEvents>(event: TEvent, ...args: Parameters<TableSourceEvents[TEvent]>): void {
this.#emitter.emit(event, ...args);
}
protected abstract saveData(): Promise<void>;
protected abstract loadData(): Promise<void>;
}
@@ -0,0 +1,5 @@
// Cell position types
export interface ICellPosition {
readonly rowIdx: number;
readonly colIdx: number;
}
@@ -0,0 +1,27 @@
import type { ICellPosition } from './ICellPosition.js';
import type { ITableEditorHistory } from './ITableEditorHistory.js';
import type { TableEditorEventEmitter } from './TableEditorEvents.js';
// Main table editor interface
export interface ITableEditor<TValue> extends TableEditorEventEmitter<TValue> {
// Data access
readonly data: readonly (readonly TValue[])[];
readonly isEdited: boolean;
readonly rowCount: number;
// Cell operations
getCellValue(position: ICellPosition): TValue | undefined;
setCellValue(position: ICellPosition, value: TValue): void;
// Row operations
insertRow(rowIdx: number, rowData?: TValue[]): void;
deleteRow(rowIdx: number): void;
// Data operations
resetData(newData: TValue[][]): void;
// History operations
readonly history: ITableEditorHistory<TValue>;
undo(): boolean;
redo(): boolean;
}
@@ -0,0 +1,5 @@
import type { ITableEditor } from './ITableEditor.js';
export interface ITableEditorFactory {
create<TValue>(initialData?: TValue[][]): ITableEditor<TValue>;
}
@@ -0,0 +1,38 @@
import type { ICellPosition } from './ICellPosition.js';
// History entry types
export interface ITableEditorHistoryEntry<TValue> {
readonly type: 'cell-edit' | 'row-insert' | 'row-delete';
readonly timestamp: number;
readonly data: ICellEditEntry<TValue> | IRowInsertEntry<TValue> | IRowDeleteEntry<TValue>;
}
export interface ICellEditEntry<TValue> {
readonly position: ICellPosition;
readonly oldValue: TValue;
readonly newValue: TValue;
}
export interface IRowInsertEntry<TValue> {
readonly rowIdx: number;
readonly rowData: TValue[];
}
export interface IRowDeleteEntry<TValue> {
readonly rowIdx: number;
readonly rowData: TValue[];
}
// History management interface
export interface ITableEditorHistory<TValue> {
readonly canUndo: boolean;
readonly canRedo: boolean;
readonly size: number;
readonly maxSize: number;
push(entry: ITableEditorHistoryEntry<TValue>): void;
undo(): ITableEditorHistoryEntry<TValue> | null;
redo(): ITableEditorHistoryEntry<TValue> | null;
clear(): void;
setMaxSize(size: number): void;
}
@@ -0,0 +1,193 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { describe, expect, test, beforeEach } from 'vitest';
import { TableEditor } from './TableEditor.js';
import type { ICellPosition } from './ICellPosition.js';
describe('TableEditor', () => {
let tableEditor: TableEditor<string>;
const initialData = [
['A1', 'B1', 'C1'],
['A2', 'B2', 'C2'],
['A3', 'B3', 'C3'],
];
beforeEach(() => {
tableEditor = new TableEditor(initialData);
});
describe('Basic functionality', () => {
test('should initialize with provided data', () => {
expect(tableEditor.rowCount).toBe(3);
expect(tableEditor.data).toEqual(initialData);
expect(tableEditor.isEdited).toBe(false);
});
test('should get cell values correctly', () => {
const position: ICellPosition = { rowIdx: 1, colIdx: 1 };
expect(tableEditor.getCellValue(position)).toBe('B2');
});
test('should return undefined for invalid positions', () => {
const invalidPosition: ICellPosition = { rowIdx: 10, colIdx: 10 };
expect(tableEditor.getCellValue(invalidPosition)).toBeUndefined();
});
});
describe('Cell editing', () => {
test('should set cell values and track changes', () => {
const position: ICellPosition = { rowIdx: 1, colIdx: 1 };
let changeCount = 0;
tableEditor.on('data-changed', () => {
changeCount++;
});
tableEditor.setCellValue(position, 'NEW_VALUE');
expect(tableEditor.getCellValue(position)).toBe('NEW_VALUE');
expect(tableEditor.isEdited).toBe(true);
expect(changeCount).toBe(1);
});
test('should not emit events when setting same value', () => {
const position: ICellPosition = { rowIdx: 1, colIdx: 1 };
let changeCount = 0;
tableEditor.on('data-changed', () => {
changeCount++;
});
tableEditor.setCellValue(position, 'B2'); // Same value
expect(changeCount).toBe(0);
expect(tableEditor.isEdited).toBe(false);
});
test('should throw error for invalid positions', () => {
const invalidPosition: ICellPosition = { rowIdx: 10, colIdx: 10 };
expect(() => {
tableEditor.setCellValue(invalidPosition, 'value');
}).toThrow();
});
});
describe('Row operations', () => {
test('should insert rows correctly', () => {
let changeCount = 0;
tableEditor.on('data-changed', () => {
changeCount++;
});
tableEditor.insertRow(1, ['X1', 'X2', 'X3']);
expect(tableEditor.rowCount).toBe(4);
expect(tableEditor.getCellValue({ rowIdx: 1, colIdx: 0 })).toBe('X1');
expect(tableEditor.getCellValue({ rowIdx: 2, colIdx: 0 })).toBe('A2'); // Shifted down
expect(tableEditor.isEdited).toBe(true);
expect(changeCount).toBe(1);
});
test('should delete rows correctly', () => {
let changeCount = 0;
tableEditor.on('data-changed', () => {
changeCount++;
});
tableEditor.deleteRow(1);
expect(tableEditor.rowCount).toBe(2);
expect(tableEditor.getCellValue({ rowIdx: 1, colIdx: 0 })).toBe('A3');
expect(tableEditor.isEdited).toBe(true);
expect(changeCount).toBe(1);
});
});
describe('History management', () => {
test('should support undo/redo for cell edits', () => {
const position: ICellPosition = { rowIdx: 1, colIdx: 1 };
// Make a change
tableEditor.setCellValue(position, 'NEW_VALUE');
expect(tableEditor.getCellValue(position)).toBe('NEW_VALUE');
expect(tableEditor.history.canUndo).toBe(true);
expect(tableEditor.history.canRedo).toBe(false);
// Undo
const undoResult = tableEditor.undo();
expect(undoResult).toBe(true);
expect(tableEditor.getCellValue(position)).toBe('B2');
expect(tableEditor.history.canUndo).toBe(false);
expect(tableEditor.history.canRedo).toBe(true);
// Redo
const redoResult = tableEditor.redo();
expect(redoResult).toBe(true);
expect(tableEditor.getCellValue(position)).toBe('NEW_VALUE');
expect(tableEditor.history.canUndo).toBe(true);
expect(tableEditor.history.canRedo).toBe(false);
});
test('should support undo/redo for row operations', () => {
// Insert row
tableEditor.insertRow(1, ['X1', 'X2', 'X3']);
expect(tableEditor.rowCount).toBe(4);
// Undo insert
tableEditor.undo();
expect(tableEditor.rowCount).toBe(3);
expect(tableEditor.getCellValue({ rowIdx: 1, colIdx: 0 })).toBe('A2');
// Redo insert
tableEditor.redo();
expect(tableEditor.rowCount).toBe(4);
expect(tableEditor.getCellValue({ rowIdx: 1, colIdx: 0 })).toBe('X1');
});
test('should not fire change notifications for undo/redo operations', () => {
let changeCount = 0;
tableEditor.on('data-changed', () => {
changeCount++;
});
const position: ICellPosition = { rowIdx: 1, colIdx: 1 };
// Make a change - should fire change notification
tableEditor.setCellValue(position, 'NEW_VALUE');
expect(changeCount).toBe(1);
// Undo - should NOT fire change notification
tableEditor.undo();
expect(changeCount).toBe(1);
// Redo - should NOT fire change notification
tableEditor.redo();
expect(changeCount).toBe(1);
});
});
describe('Data reset', () => {
test('should reset data and clear history', () => {
// Make some changes
tableEditor.setCellValue({ rowIdx: 0, colIdx: 0 }, 'CHANGED');
expect(tableEditor.isEdited).toBe(true);
expect(tableEditor.history.canUndo).toBe(true);
// Reset data
const newData = [['Z1', 'Z2'], ['Z3', 'Z4']];
tableEditor.resetData(newData);
expect(tableEditor.data).toEqual(newData);
expect(tableEditor.rowCount).toBe(2);
expect(tableEditor.isEdited).toBe(false);
expect(tableEditor.history.canUndo).toBe(false);
});
});
});
@@ -0,0 +1,253 @@
import { createNanoEvents, type Emitter, type Unsubscribe } from 'nanoevents';
import type { ITableEditor } from './ITableEditor.js';
import type { ICellPosition } from './ICellPosition.js';
import type { ITableEditorHistory, ITableEditorHistoryEntry } from './ITableEditorHistory.js';
import type { TableEditorEvents } from './TableEditorEvents.js';
import { TableEditorHistory } from './TableEditorHistory.js';
export class TableEditor<TValue> implements ITableEditor<TValue> {
private readonly _data: TValue[][];
private readonly _history: ITableEditorHistory<TValue>;
private readonly _emitter: Emitter<TableEditorEvents<TValue>>;
private _isEdited = false;
constructor(initialData: TValue[][] = [], historyMaxSize = 100) {
this._data = initialData.map(row => [...row]);
this._history = new TableEditorHistory<TValue>(historyMaxSize);
this._emitter = createNanoEvents();
}
get data(): readonly (readonly TValue[])[] {
return this._data.map(row => Object.freeze([...row]));
}
get isEdited(): boolean {
return this._isEdited;
}
get rowCount(): number {
return this._data.length;
}
get history(): ITableEditorHistory<TValue> {
return this._history;
}
getCellValue(position: ICellPosition): TValue | undefined {
if (!this.isValidPosition(position)) {
return undefined;
}
return this._data[position.rowIdx]?.[position.colIdx];
}
setCellValue(position: ICellPosition, value: TValue): void {
if (!this.isValidPosition(position)) {
throw new Error(`Invalid cell position: row ${position.rowIdx}, column ${position.colIdx}`);
}
const row = this._data[position.rowIdx];
if (!row) {
throw new Error(`Row ${position.rowIdx} does not exist`);
}
const oldValue = row[position.colIdx];
if (oldValue === value) {
return;
}
const historyEntry: ITableEditorHistoryEntry<TValue> = {
type: 'cell-edit',
timestamp: Date.now(),
data: {
position: { ...position },
oldValue: oldValue as TValue,
newValue: value,
},
};
this._history.push(historyEntry);
row[position.colIdx] = value;
this._isEdited = true;
this.emit('data-changed');
this.emitHistoryChanged();
}
insertRow(rowIdx: number, rowData?: TValue[]): void {
if (rowIdx < 0 || rowIdx > this._data.length) {
throw new Error(`Invalid row index: ${rowIdx}. Must be between 0 and ${this._data.length}`);
}
const newRowData = rowData ? [...rowData] : [];
if (this._data.length > 0 && newRowData.length === 0) {
const columnCount = Math.max(...this._data.map(row => row.length));
newRowData.length = columnCount;
newRowData.fill(undefined as any);
}
const historyEntry: ITableEditorHistoryEntry<TValue> = {
type: 'row-insert',
timestamp: Date.now(),
data: {
rowIdx,
rowData: [...newRowData],
},
};
this._history.push(historyEntry);
this._data.splice(rowIdx, 0, newRowData);
this._isEdited = true;
this.emit('data-changed');
this.emitHistoryChanged();
}
deleteRow(rowIdx: number): void {
if (rowIdx < 0 || rowIdx >= this._data.length) {
throw new Error(`Invalid row index: ${rowIdx}. Must be between 0 and ${this._data.length - 1}`);
}
const row = this._data[rowIdx];
if (!row) {
throw new Error(`Row ${rowIdx} does not exist`);
}
const deletedRowData = [...row];
const historyEntry: ITableEditorHistoryEntry<TValue> = {
type: 'row-delete',
timestamp: Date.now(),
data: {
rowIdx,
rowData: deletedRowData,
},
};
this._history.push(historyEntry);
this._data.splice(rowIdx, 1);
this._isEdited = true;
this.emit('data-changed');
this.emitHistoryChanged();
}
resetData(newData: TValue[][]): void {
this._data.length = 0;
newData.forEach(row => {
this._data.push([...row]);
});
this._history.clear();
this._isEdited = false;
this.emit('data-reset', newData);
this.emitHistoryChanged();
}
undo(): boolean {
const entry = this._history.undo();
if (!entry) {
return false;
}
this.applyHistoryEntryReverse(entry);
this.emitHistoryChanged();
return true;
}
redo(): boolean {
const entry = this._history.redo();
if (!entry) {
return false;
}
this.applyHistoryEntry(entry);
this.emitHistoryChanged();
return true;
}
on<TEvent extends keyof TableEditorEvents<TValue>>(
event: TEvent,
listener: TableEditorEvents<TValue>[TEvent]
): Unsubscribe {
return this._emitter.on(event, listener);
}
emit<TEvent extends keyof TableEditorEvents<TValue>>(
event: TEvent,
...args: Parameters<TableEditorEvents<TValue>[TEvent]>
): void {
this._emitter.emit(event, ...args);
}
private isValidPosition(position: ICellPosition): boolean {
return (
position.rowIdx >= 0 &&
position.rowIdx < this._data.length &&
position.colIdx >= 0 &&
position.colIdx < (this._data[position.rowIdx]?.length || 0)
);
}
private emitHistoryChanged(): void {
this.emit('history-changed', this._history.canUndo, this._history.canRedo);
}
private applyHistoryEntry(entry: ITableEditorHistoryEntry<TValue>): void {
switch (entry.type) {
case 'cell-edit': {
const data = entry.data as any;
const { position, newValue } = data;
const row = this._data[position.rowIdx];
if (row) {
row[position.colIdx] = newValue;
}
break;
}
case 'row-insert': {
const data = entry.data as any;
const { rowIdx, rowData } = data;
this._data.splice(rowIdx, 0, [...rowData]);
break;
}
case 'row-delete': {
const data = entry.data as any;
const { rowIdx } = data;
this._data.splice(rowIdx, 1);
break;
}
}
}
private applyHistoryEntryReverse(entry: ITableEditorHistoryEntry<TValue>): void {
switch (entry.type) {
case 'cell-edit': {
const data = entry.data as any;
const { position, oldValue } = data;
const row = this._data[position.rowIdx];
if (row) {
row[position.colIdx] = oldValue;
}
break;
}
case 'row-insert': {
const data = entry.data as any;
const { rowIdx } = data;
this._data.splice(rowIdx, 1);
break;
}
case 'row-delete': {
const data = entry.data as any;
const { rowIdx, rowData } = data;
this._data.splice(rowIdx, 0, [...rowData]);
break;
}
}
}
}
@@ -0,0 +1,9 @@
import type { EmitterMixin } from 'nanoevents';
export interface TableEditorEvents<TValue> {
'data-changed': () => void;
'data-reset': (newData: TValue[][]) => void;
'history-changed': (canUndo: boolean, canRedo: boolean) => void;
}
export type TableEditorEventEmitter<TValue> = EmitterMixin<TableEditorEvents<TValue>>;
@@ -0,0 +1,85 @@
import type { ITableEditorHistory, ITableEditorHistoryEntry } from './ITableEditorHistory.js';
/**
* Implementation of table editor history with undo/redo functionality.
* Maintains a circular buffer of history entries with a configurable maximum size.
*/
export class TableEditorHistory<TValue> implements ITableEditorHistory<TValue> {
private entries: ITableEditorHistoryEntry<TValue>[] = [];
private currentIndex: number = -1;
private _maxSize: number;
constructor(maxSize: number = 100) {
this._maxSize = Math.max(1, maxSize);
}
get canUndo(): boolean {
return this.currentIndex >= 0;
}
get canRedo(): boolean {
return this.currentIndex < this.entries.length - 1;
}
get size(): number {
return this.entries.length;
}
get maxSize(): number {
return this._maxSize;
}
push(entry: ITableEditorHistoryEntry<TValue>): void {
// Remove any entries after the current index (when pushing after undo)
if (this.currentIndex < this.entries.length - 1) {
this.entries = this.entries.slice(0, this.currentIndex + 1);
}
// Add the new entry
this.entries.push(entry);
this.currentIndex = this.entries.length - 1;
// Maintain max size by removing oldest entries
if (this.entries.length > this._maxSize) {
const removeCount = this.entries.length - this._maxSize;
this.entries = this.entries.slice(removeCount);
this.currentIndex -= removeCount;
}
}
undo(): ITableEditorHistoryEntry<TValue> | null {
if (!this.canUndo) {
return null;
}
const entry = this.entries[this.currentIndex];
this.currentIndex--;
return entry ?? null;
}
redo(): ITableEditorHistoryEntry<TValue> | null {
if (!this.canRedo) {
return null;
}
this.currentIndex++;
const entry = this.entries[this.currentIndex];
return entry ?? null;
}
clear(): void {
this.entries = [];
this.currentIndex = -1;
}
setMaxSize(size: number): void {
this._maxSize = Math.max(1, size);
// Trim entries if the new max size is smaller
if (this.entries.length > this._maxSize) {
const removeCount = this.entries.length - this._maxSize;
this.entries = this.entries.slice(removeCount);
this.currentIndex = Math.max(-1, this.currentIndex - removeCount);
}
}
}
@@ -0,0 +1,25 @@
// Main interfaces
export type { ITableEditor } from './ITableEditor.js';
export type { ICellPosition } from './ICellPosition.js';
export type { ITableEditorFactory } from './ITableEditorFactory.js';
// History interfaces
export type {
ITableEditorHistory,
ITableEditorHistoryEntry,
ICellEditEntry,
IRowInsertEntry,
IRowDeleteEntry,
} from './ITableEditorHistory.js';
// History implementation
export { TableEditorHistory } from './TableEditorHistory.js';
// Table editor implementation
export { TableEditor } from './TableEditor.js';
// Event interfaces
export type {
TableEditorEvents,
TableEditorEventEmitter,
} from './TableEditorEvents.js';
@@ -0,0 +1,12 @@
export * from './editor/TableEditor.js';
export * from './interfaces/ITableData.js';
export * from './interfaces/ITableDataset.js';
export * from './interfaces/ITableDatasetManager.js';
export * from './interfaces/ITableSource.js';
export * from './interfaces/ITableSourceOptions.js';
export * from './interfaces/ITableSorting.js';
export * from './interfaces/TableEvents.js';
export * from './interfaces/TableSourceEvents.js';
export * from './TableSource.js';
export * from './TableDatasetManager.js';
export * from './editor/index.js';
@@ -0,0 +1,18 @@
import type { EmitterMixin } from 'nanoevents';
import type { ITableDataset } from './ITableDataset.js';
import type { ITableSource } from './ITableSource.js';
import type { TableEvents } from './TableEvents.js';
import type { ITableSourceOptions } from './ITableSourceOptions.js';
export interface ITableData<TColumn, TValue, TOptions extends ITableSourceOptions = ITableSourceOptions> extends EmitterMixin<TableEvents> {
readonly id: string;
readonly isLoading: boolean;
readonly isOutdated: boolean;
readonly datasets: ITableDataset<TColumn, TValue>[];
readonly source: ITableSource<TColumn, TValue, TOptions>;
setSource(source: ITableSource<TColumn, TValue, TOptions>): void;
save(): Promise<void>;
}
@@ -0,0 +1,5 @@
export interface ITableDataset<TColumn, TValue> {
readonly id: string;
readonly columns: TColumn[];
readonly rows: TValue[][];
}
@@ -0,0 +1,7 @@
import type { ITableDataset } from './ITableDataset.js';
export interface ITableDatasetManager<TColumn, TValue> {
readonly datasets: ITableDataset<TColumn, TValue>[];
setDatasets(datasets: ITableDataset<TColumn, TValue>[]): void;
}
@@ -0,0 +1,4 @@
export interface ITableSorting {
column: number;
order: 'asc' | 'desc';
}
@@ -0,0 +1,18 @@
import type { EmitterMixin } from 'nanoevents';
import type { TableSourceEvents } from './TableSourceEvents.js';
import type { ITableDatasetManager } from './ITableDatasetManager.js';
import type { ITableSourceOptions } from './ITableSourceOptions.js';
export interface ITableSource<TColumn, TValue, TOptions extends ITableSourceOptions = ITableSourceOptions> extends EmitterMixin<TableSourceEvents> {
readonly datasetManager: ITableDatasetManager<TColumn, TValue>;
readonly options: Partial<TOptions>;
readonly isLoading: boolean;
readonly isOutdated: boolean;
readonly error: Error | null;
setOptions(options: TOptions): void;
setOutdated(): void;
save(): Promise<void>;
load(): Promise<void>;
}
@@ -0,0 +1,5 @@
import type { ITableSorting } from './ITableSorting.js';
export interface ITableSourceOptions {
sorting?: ITableSorting[];
}
@@ -0,0 +1,3 @@
import type { TableSourceEvents } from './TableSourceEvents.js';
export type TableEvents = TableSourceEvents & {};
@@ -0,0 +1,7 @@
export type TableSourceEvents = {
loading: (state: boolean) => void;
outdated: (state: boolean) => void;
saved: () => void;
data: () => void;
error: (error: Error | null) => void;
};
@@ -0,0 +1,21 @@
{
"extends": "@dbeaver/tsconfig/tsconfig.json",
"compilerOptions": {
"tsBuildInfoFile": "./lib/.tsbuildinfo",
"rootDir": "src",
"rootDirs": [
"src"
],
"outDir": "lib",
"incremental": true,
"composite": true
},
"include": [
"src"
],
"references": [
{
"path": "../cli"
}
]
}
+31 -1
View File
@@ -165,6 +165,20 @@ __metadata:
languageName: unknown
linkType: soft
"@dbeaver/table-data@workspace:@dbeaver/table-data":
version: 0.0.0-use.local
resolution: "@dbeaver/table-data@workspace:@dbeaver/table-data"
dependencies:
"@dbeaver/cli": "workspace:^"
"@dbeaver/tsconfig": "workspace:^"
async-mutex: "npm:^0"
nanoevents: "npm:^9"
rimraf: "npm:^6"
typescript: "npm:^5"
vitest: "npm:^3"
languageName: unknown
linkType: soft
"@dbeaver/tsconfig@workspace:@dbeaver/tsconfig, @dbeaver/tsconfig@workspace:^":
version: 0.0.0-use.local
resolution: "@dbeaver/tsconfig@workspace:@dbeaver/tsconfig"
@@ -949,6 +963,15 @@ __metadata:
languageName: node
linkType: hard
"async-mutex@npm:^0":
version: 0.5.0
resolution: "async-mutex@npm:0.5.0"
dependencies:
tslib: "npm:^2.4.0"
checksum: 10c0/9096e6ad6b674c894d8ddd5aa4c512b09bb05931b8746ebd634952b05685608b2b0820ed5c406e6569919ff5fe237ab3c491e6f2887d6da6b6ba906db3ee9c32
languageName: node
linkType: hard
"balanced-match@npm:^1.0.0":
version: 1.0.2
resolution: "balanced-match@npm:1.0.2"
@@ -2002,6 +2025,13 @@ __metadata:
languageName: node
linkType: hard
"nanoevents@npm:^9":
version: 9.1.0
resolution: "nanoevents@npm:9.1.0"
checksum: 10c0/5fb48e6fc1d3102daddaeffd6eada907c25b1c8554dcc648e9cb0a72979a1ab3ee56ffa2d2b3e566a8e561a9e2992a3783493c61cfaaa096b2986d56dbbc1ca5
languageName: node
linkType: hard
"nanoid@npm:^3.3.8":
version: 3.3.11
resolution: "nanoid@npm:3.3.11"
@@ -2749,7 +2779,7 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:^2, tslib@npm:^2.3.0":
"tslib@npm:^2, tslib@npm:^2.3.0, tslib@npm:^2.4.0":
version: 2.8.1
resolution: "tslib@npm:2.8.1"
checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
+30
View File
@@ -4528,6 +4528,20 @@ __metadata:
languageName: unknown
linkType: soft
"@dbeaver/table-data@workspace:../common-typescript/@dbeaver/table-data":
version: 0.0.0-use.local
resolution: "@dbeaver/table-data@workspace:../common-typescript/@dbeaver/table-data"
dependencies:
"@dbeaver/cli": "workspace:^"
"@dbeaver/tsconfig": "workspace:^"
async-mutex: "npm:^0"
nanoevents: "npm:^9"
rimraf: "npm:^6"
typescript: "npm:^5"
vitest: "npm:^3"
languageName: unknown
linkType: soft
"@dbeaver/tsconfig@workspace:*, @dbeaver/tsconfig@workspace:../common-typescript/@dbeaver/tsconfig, @dbeaver/tsconfig@workspace:^":
version: 0.0.0-use.local
resolution: "@dbeaver/tsconfig@workspace:../common-typescript/@dbeaver/tsconfig"
@@ -8401,6 +8415,15 @@ __metadata:
languageName: node
linkType: hard
"async-mutex@npm:^0":
version: 0.5.0
resolution: "async-mutex@npm:0.5.0"
dependencies:
tslib: "npm:^2.4.0"
checksum: 10c0/9096e6ad6b674c894d8ddd5aa4c512b09bb05931b8746ebd634952b05685608b2b0820ed5c406e6569919ff5fe237ab3c491e6f2887d6da6b6ba906db3ee9c32
languageName: node
linkType: hard
"async@npm:^3.2.3":
version: 3.2.6
resolution: "async@npm:3.2.6"
@@ -14598,6 +14621,13 @@ __metadata:
languageName: node
linkType: hard
"nanoevents@npm:^9":
version: 9.1.0
resolution: "nanoevents@npm:9.1.0"
checksum: 10c0/5fb48e6fc1d3102daddaeffd6eada907c25b1c8554dcc648e9cb0a72979a1ab3ee56ffa2d2b3e566a8e561a9e2992a3783493c61cfaaa096b2986d56dbbc1ca5
languageName: node
linkType: hard
"nanoid@npm:^3.3.8":
version: 3.3.11
resolution: "nanoid@npm:3.3.11"