diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerPage/useDataViewerPanel.ts b/webapp/packages/plugin-data-viewer/src/DataViewerPage/useDataViewerPanel.ts index 21991950f36..681fecf339f 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerPage/useDataViewerPanel.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerPage/useDataViewerPanel.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -57,18 +57,34 @@ export function useDataViewerPanel(tab: ITab) { model = dataViewerTableService.create(connectionInfo, node); tab.handlerState.tableId = model.id; - model.source.setOutdated(); - dataViewerDataChangeConfirmationService.trackTableDataUpdate(model.id); - const pageState = dataViewerTabService.page.getState(tab); + let pageState = dataViewerTabService.page.getState(tab); + + if (!pageState) { + dataViewerTabService.page.setState(tab, { + resultIndex: 0, + presentationId: '', + valuePresentationId: null, + }); + pageState = dataViewerTabService.page.getState(tab); + } if (pageState) { + if (!pageState.persistedState) { + pageState.persistedState = {}; + } + + model.source.persistedState.setStore(pageState.persistedState); + const presentation = dataPresentationService.get(pageState.presentationId); if (presentation?.dataFormat !== undefined) { model.setDataFormat(presentation.dataFormat); } } + + model.source.setOutdated(); + dataViewerDataChangeConfirmationService.trackTableDataUpdate(model.id); } if (node?.name) { diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts index 7ae1f4304d3..069822fe785 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2025 DBeaver Corp and others + * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -111,7 +111,11 @@ export class DataViewerTabService { await initTab(); if (tabInfo.isNewlyCreated) { - trySwitchPage(this.page); + trySwitchPage(this.page, { + resultIndex: 0, + presentationId: '', + valuePresentationId: null, + }); } } catch (exception: any) { this.notificationService.logException(exception, 'Data Editor Error', 'Error in Data Editor while processing action with database node'); diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerTableState/IDataViewerPersistedState.ts b/webapp/packages/plugin-data-viewer/src/DataViewerTableState/IDataViewerPersistedState.ts new file mode 100644 index 00000000000..ce37398fe44 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DataViewerTableState/IDataViewerPersistedState.ts @@ -0,0 +1,22 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export interface IDataViewerPersistedState { + constraints: IPersistedConstraint[]; + whereFilter: string; + pinnedColumns?: string[]; + columnOrder?: string[]; +} + +export interface IPersistedConstraint { + attributeName: string; + operator?: string; + value?: unknown; + orderAsc?: boolean; + orderPosition?: number; +} diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerTableState/validatePersistedState.ts b/webapp/packages/plugin-data-viewer/src/DataViewerTableState/validatePersistedState.ts new file mode 100644 index 00000000000..8aafa0624cb --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DataViewerTableState/validatePersistedState.ts @@ -0,0 +1,29 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2026 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 { schema } from '@cloudbeaver/core-utils'; + +import type { IDataViewerPersistedState } from './IDataViewerPersistedState.js'; + +const persistedConstraintSchema = schema.object({ + attributeName: schema.string().min(1), + operator: schema.string().optional(), + value: schema.unknown().optional(), + orderAsc: schema.boolean().optional(), + orderPosition: schema.number().optional(), +}); + +const persistedStateSchema = schema.object({ + constraints: schema.array(persistedConstraintSchema), + whereFilter: schema.string(), + pinnedColumns: schema.array(schema.string()).optional().default([]), + columnOrder: schema.array(schema.string()).optional(), +}); + +export function validatePersistedState(data: unknown): data is IDataViewerPersistedState { + return persistedStateSchema.safeParse(data).success; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataConstraintAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataConstraintAction.ts index 3ea37e12d36..eedd7d2b440 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataConstraintAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataConstraintAction.ts @@ -5,7 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { computed, makeObservable } from 'mobx'; +import { action, autorun, computed, type IReactionDisposer, makeObservable, runInAction } from 'mobx'; import { type DataTypeLogicalOperation, ResultDataFormat, type SqlDataFilterConstraint } from '@cloudbeaver/core-sdk'; @@ -17,16 +17,23 @@ import { EOrder, type Order } from '../Order.js'; import type { IDatabaseDataConstraintAction } from './IDatabaseDataConstraintAction.js'; import { injectable } from '@cloudbeaver/core-di'; import { IDatabaseDataResult } from '../IDatabaseDataResult.js'; +import type { IPersistedConstraint } from '../../DataViewerTableState/IDataViewerPersistedState.js'; export const IS_NULL_ID = 'IS_NULL'; export const IS_NOT_NULL_ID = 'IS_NOT_NULL'; +const CONSTRAINTS_KEY = 'constraints'; +const WHERE_FILTER_KEY = 'whereFilter'; + @injectable(() => [IDatabaseDataSource, IDatabaseDataResult]) export class DatabaseDataConstraintAction extends DatabaseDataAction - implements IDatabaseDataConstraintAction { + implements IDatabaseDataConstraintAction +{ static dataFormat = [ResultDataFormat.Resultset, ResultDataFormat.Document]; + private readonly persistDisposer: IReactionDisposer; + get supported(): boolean { return this.source.constraintsAvailable && this.source.results.length < 2; } @@ -54,7 +61,24 @@ export class DatabaseDataConstraintAction makeObservable(this, { orderConstraints: computed, filterConstraints: computed, + deleteAll: action, + deleteFilter: action, + deleteFilters: action, + deleteOrders: action, + deleteOrder: action, + deleteDataFilters: action, + deleteData: action, + setWhereFilter: action, + setFilter: action, + setOrder: action, }); + + this.persistDisposer = autorun(() => this.persistConstraints()); + } + + override dispose(): void { + this.persistDisposer(); + super.dispose(); } private deleteConstraint(attributePosition: number) { @@ -182,6 +206,7 @@ export class DatabaseDataConstraintAction if (currentConstraint) { currentConstraint.operator = operator; + currentConstraint.attributeName = this.getColumnNameAt(attributePosition); if (value !== undefined) { currentConstraint.value = value; } else if (currentConstraint.value !== undefined) { @@ -192,6 +217,7 @@ export class DatabaseDataConstraintAction const constraint: SqlDataFilterConstraint = { attributePosition, + attributeName: this.getColumnNameAt(attributePosition), operator, }; @@ -219,6 +245,7 @@ export class DatabaseDataConstraintAction if (!resetOrder) { this.source.options.constraints.push({ attributePosition, + attributeName: this.getColumnNameAt(attributePosition), orderPosition: this.getMaxOrderPosition(), orderAsc: order === EOrder.asc, }); @@ -257,6 +284,44 @@ export class DatabaseDataConstraintAction override updateResult(result: IDatabaseResultSet): void { updateConstraintsForResult(this.source, result); } + + private getColumnNameAt(colIdx: number): string | undefined { + return this.result.data?.columns?.find(c => c.position === colIdx)?.name; + } + + private persistConstraints(): void { + if (!this.source.options) { + return; + } + + const ps = this.source.persistedState; + const constraints: IPersistedConstraint[] = this.source.options.constraints + .map(c => { + const name = c.attributeName ?? this.getColumnNameAt(c.attributePosition!); + + if (!name) { + return null; + } + + const persisted: IPersistedConstraint = { attributeName: name }; + + if (isFilterConstraint(c)) { + persisted.operator = c.operator; + persisted.value = c.value; + } + + if (isOrderConstraint(c)) { + persisted.orderAsc = c.orderAsc; + persisted.orderPosition = c.orderPosition; + } + + return persisted; + }) + .filter((c): c is IPersistedConstraint => c !== null); + + ps.set(CONSTRAINTS_KEY, constraints); + ps.set(WHERE_FILTER_KEY, this.source.options.whereFilter || ''); + } } function updateConstraintsForResult(source: IDatabaseDataSource, result: IDatabaseResultSet) { @@ -264,31 +329,41 @@ function updateConstraintsForResult(source: IDatabaseDataSource column.position === constraint.attributePosition); + runInAction(() => { + for (const constraint of source.options!.constraints) { + let prevColumn = result.data?.columns?.find(c => c.position === constraint.attributePosition); - if (!prevColumn) { - return; - } + if (!prevColumn && constraint.attributeName) { + prevColumn = result.data?.columns?.find(c => c.name === constraint.attributeName); - let column = result.data?.columns?.find(column => column.position === prevColumn.position); + if (prevColumn) { + constraint.attributePosition = prevColumn.position; + } + } - if (!column || column.label !== prevColumn.label) { - column = result.data?.columns?.find(column => column.label === prevColumn.label); - } + if (!prevColumn) { + continue; + } - if (column && prevColumn.position !== column.position) { - const prevConstraint = source.prevOptions?.constraints.find( - prevConstraint => prevConstraint.attributePosition === constraint.attributePosition, - ); + let column = result.data?.columns?.find(c => c.position === prevColumn.position); - constraint.attributePosition = column.position; + if (!column || column.label !== prevColumn.label) { + column = result.data?.columns?.find(c => c.label === prevColumn.label); + } - if (prevConstraint) { - prevConstraint.attributePosition = constraint.attributePosition; + if (column && prevColumn.position !== column.position) { + const prevConstraint = source.prevOptions?.constraints.find( + prevConstraint => prevConstraint.attributePosition === constraint.attributePosition, + ); + + constraint.attributePosition = column.position; + + if (prevConstraint) { + prevConstraint.attributePosition = constraint.attributePosition; + } } } - } + }); } export function nullOperationsFilter(operation: DataTypeLogicalOperation): boolean { diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Grid/GridDataResultAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Grid/GridDataResultAction.ts index 258e366147e..758547d222a 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Grid/GridDataResultAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Grid/GridDataResultAction.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2025 DBeaver Corp and others + * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -56,6 +56,8 @@ export abstract class GridDataResultAction< } abstract getColumnName(key: IGridColumnKey): string | undefined; + + abstract getColumnNameAt(colIdx: number): string | undefined; abstract insertRow(row: IGridRowKey, value: TCell[], shift?: number): IGridRowKey | undefined; abstract removeRow(row: IGridRowKey, shift?: number): IGridRowKey | undefined; abstract setRowValue(row: IGridRowKey, value: TCell[], shift?: number): void; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Grid/GridViewAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Grid/GridViewAction.ts index 444ef53c9b3..df133427523 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Grid/GridViewAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Grid/GridViewAction.ts @@ -7,6 +7,7 @@ */ import { action, computed, makeObservable, observable, ObservableSet } from 'mobx'; +import { isDefined } from '@dbeaver/js-helpers'; import { DatabaseDataAction } from '../../DatabaseDataAction.js'; import { IDatabaseDataSource } from '../../IDatabaseDataSource.js'; import type { IGridColumnKey, IGridDataKey, IGridRowKey } from './IGridDataKey.js'; @@ -21,6 +22,10 @@ import type { IDatabaseDataViewAction } from '../IDatabaseDataViewAction.js'; import { IDatabaseDataResultAction } from '../IDatabaseDataResultAction.js'; import { IDatabaseDataEditAction } from '../IDatabaseDataEditAction.js'; import type { IDatabaseValueHolder } from '../IDatabaseValueHolder.js'; +import type { IRestoreViewState } from './IRestoreViewState.js'; + +const PINNED_COLUMNS_KEY = 'pinnedColumns'; +const COLUMN_ORDER_KEY = 'columnOrder'; @injectable(() => [IDatabaseDataSource, IDatabaseDataResult, IDatabaseDataResultAction, IDatabaseDataEditAction]) export class GridViewAction< @@ -52,6 +57,7 @@ export class GridViewAction< } private columnsOrder: number[]; + private viewStateRestored: boolean; readonly pinnedColumns: ObservableSet; protected readonly data: GridDataResultAction; protected readonly editor?: GridEditAction; @@ -67,6 +73,7 @@ export class GridViewAction< this.editor = editor as GridEditAction | undefined; this.columnsOrder = this.data.columns.map((key, index) => index); this.pinnedColumns = observable.set(); + this.viewStateRestored = false; makeObservable(this, { columnsOrder: observable, @@ -75,11 +82,14 @@ export class GridViewAction< pinColumns: action, unpinColumns: action, unpinAllColumns: action, + restoreViewState: action, rows: computed, rowKeys: computed, columns: computed, columnKeys: computed, }); + + this.tryRestoreViewState(); } has(cell: TKey): boolean { @@ -111,6 +121,7 @@ export class GridViewAction< this.columnsOrder.splice(this.columnsOrder.indexOf(columnIndex), 1); this.columnsOrder.splice(index, 0, columnIndex); + this.persistViewState(); } columnIndex(key: IGridColumnKey): number { @@ -183,6 +194,7 @@ export class GridViewAction< const serializedKey = GridDataKeysUtils.serialize(key); this.pinnedColumns.add(serializedKey); } + this.persistViewState(); } unpinColumns(keys: IGridColumnKey[]): void { @@ -190,10 +202,12 @@ export class GridViewAction< const serializedKey = GridDataKeysUtils.serialize(key); this.pinnedColumns.delete(serializedKey); } + this.persistViewState(); } unpinAllColumns(): void { this.pinnedColumns.clear(); + this.persistViewState(); } isColumnPinned(key: IGridColumnKey): boolean { @@ -205,6 +219,13 @@ export class GridViewAction< return this.pinnedColumns.size > 0; } + getPinnedColumnNames(): string[] { + return this.columnKeys + .filter(key => this.isColumnPinned(key)) + .map(key => this.getColumnName(key)) + .filter(isDefined); + } + protected mapRow(row: IGridRowKey): TCell[] { const edited = this.editor?.getRow(row); @@ -232,4 +253,100 @@ export class GridViewAction< this.columnsOrder = this.data.columns.map((key, index) => index); } } + + override afterResultUpdate(): void { + this.tryRestoreViewState(); + } + + private tryRestoreViewState(): void { + if (this.viewStateRestored) { + return; + } + + const ps = this.source.persistedState; + + if (!ps.has(PINNED_COLUMNS_KEY) && !ps.has(COLUMN_ORDER_KEY)) { + return; + } + + this.viewStateRestored = true; + const pinnedColumnNames = ps.get(PINNED_COLUMNS_KEY) ?? []; + const columnOrderNames = ps.get(COLUMN_ORDER_KEY); + + this.restoreViewState({ pinnedColumnNames, columnOrderNames }); + } + + private persistViewState(): void { + const ps = this.source.persistedState; + const keys = this.columnKeys; + let isCustomOrder = false; + const columnNames: string[] = []; + const pinnedNames: string[] = []; + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]!; + const name = this.getColumnName(key); + + if (name) { + columnNames.push(name); + + if (this.isColumnPinned(key)) { + pinnedNames.push(name); + } + } + + if (key.index !== i) { + isCustomOrder = true; + } + } + + ps.set(PINNED_COLUMNS_KEY, pinnedNames); + ps.set(COLUMN_ORDER_KEY, isCustomOrder ? columnNames : []); + } + + restoreViewState(state: IRestoreViewState): void { + const { pinnedColumnNames, columnOrderNames } = state; + + if (columnOrderNames?.length) { + const nameToIndex = new Map(); + + for (const key of this.columnKeys) { + const name = this.getColumnName(key); + + if (name) { + nameToIndex.set(name, key.index); + } + } + + const newOrder: number[] = []; + const usedIndices = new Set(); + + for (const name of columnOrderNames) { + const index = nameToIndex.get(name); + + if (index !== undefined) { + newOrder.push(index); + usedIndices.add(index); + } + } + + for (const key of this.data.columns.map((_, i) => i)) { + if (!usedIndices.has(key)) { + newOrder.push(key); + } + } + + if (newOrder.length === this.columnsOrder.length) { + this.columnsOrder = newOrder; + } + } + + for (const name of pinnedColumnNames) { + const key = this.columnKeys.find(k => this.getColumnName(k) === name); + + if (key) { + this.pinnedColumns.add(GridDataKeysUtils.serialize(key)); + } + } + } } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Grid/IRestoreViewState.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Grid/IRestoreViewState.ts new file mode 100644 index 00000000000..18c1d9fea09 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Grid/IRestoreViewState.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export interface IRestoreViewState { + pinnedColumnNames: string[]; + columnOrderNames?: string[]; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts index 0b861cc3793..83324c0a0f5 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2025 DBeaver Corp and others + * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -43,6 +43,10 @@ export class ResultSetDataAction extends GridDataResultAction< return this.getColumn(key)?.name; } + override getColumnNameAt(colIdx: number): string | undefined { + return this.columns.find(c => c.position === colIdx)?.name; + } + insertRow(row: IGridRowKey, value: IResultSetValue[], shift = 0): IGridRowKey | undefined { if (this.result.data?.rowsWithMetaData) { const index = row.index + shift; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts index af9b9ddb83c..7feb518bb93 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts @@ -11,6 +11,7 @@ import { withExternal, type IServiceProvider, type IServiceScope, type SingleSer import { Executor, ExecutorInterrupter, type IExecutor, type ISyncExecutor, type ITask, SyncExecutor, Task } from '@cloudbeaver/core-executor'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; +import { DatabasePersistedStateStore } from './DatabasePersistedStateStore.js'; import { IDatabaseDataActions } from './IDatabaseDataActions.js'; import type { IDatabaseDataResult } from './IDatabaseDataResult.js'; import { @@ -32,7 +33,8 @@ export abstract class DatabaseDataSource | null; - options: TOptions | null; + protected _options: TOptions | null; + readonly persistedState: DatabasePersistedStateStore; requestInfo: IRequestInfo; error: Error | null; private readonly features: Set; @@ -53,6 +55,10 @@ export abstract class DatabaseDataSource Promise; @@ -78,7 +84,8 @@ export abstract class DatabaseDataSource, 'disabled' | 'features' | 'activeOperationStack' | 'outdated'>(this, { + makeObservable, 'disabled' | 'features' | 'activeOperationStack' | 'outdated' | '_options'>(this, { access: observable, dataFormat: observable, features: observable, @@ -107,7 +114,7 @@ export abstract class DatabaseDataSource; + private readonly source: { options: any }; + + constructor(source: { options: any }) { + this.source = source; + this.store = {}; + + makeObservable(this, { + store: observable.ref, + }); + } + + setStore(store: Record): void { + this.store = store; + this.applyPersistedConstraints(); + } + + has(key: string): boolean { + return key in this.store; + } + + get(key: string): T | undefined { + return this.store[key] as T | undefined; + } + + set(key: string, value: unknown): void { + this.store[key] = value; + } + + delete(key: string): void { + delete this.store[key]; + } + + private applyPersistedConstraints(): void { + const options = this.source.options; + + if (!options || !validatePersistedState(this.store)) { + return; + } + + if ('constraints' in options && 'whereFilter' in options) { + options.constraints = this.store.constraints.map(c => ({ + attributeName: c.attributeName, + operator: c.operator, + value: c.value, + orderAsc: c.orderAsc, + orderPosition: c.orderPosition, + })); + options.whereFilter = this.store.whereFilter || ''; + } + } +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts index 60b1bb6f684..3aace1431b2 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts @@ -10,6 +10,7 @@ import type { IExecutor, ISyncExecutor } from '@cloudbeaver/core-executor'; import { type TLocalizationToken } from '@cloudbeaver/core-localization'; import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; +import type { IDatabasePersistedStateStore } from './IDatabasePersistedStateStore.js'; import type { IDatabaseDataActions } from './IDatabaseDataActions.js'; import type { IDatabaseDataResult } from './IDatabaseDataResult.js'; @@ -80,6 +81,7 @@ export interface IDatabaseDataSource | null; readonly options: TOptions | null; + readonly persistedState: IDatabasePersistedStateStore; readonly requestInfo: IRequestInfo; readonly error: Error | null; readonly canCancel: boolean; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabasePersistedStateStore.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabasePersistedStateStore.ts new file mode 100644 index 00000000000..18792c08ee4 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabasePersistedStateStore.ts @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export interface IDatabasePersistedStateStore { + get(key: string): T | undefined; + set(key: string, value: unknown): void; + delete(key: string): void; + has(key: string): boolean; + setStore(store: Record): void; +} diff --git a/webapp/packages/plugin-data-viewer/src/IDataViewerPageState.ts b/webapp/packages/plugin-data-viewer/src/IDataViewerPageState.ts index c1d13ee974c..78b2ecc8095 100644 --- a/webapp/packages/plugin-data-viewer/src/IDataViewerPageState.ts +++ b/webapp/packages/plugin-data-viewer/src/IDataViewerPageState.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -10,4 +10,5 @@ export interface IDataViewerPageState { resultIndex: number; presentationId: string; valuePresentationId: string | null; + persistedState?: Record; } diff --git a/webapp/packages/plugin-data-viewer/src/index.ts b/webapp/packages/plugin-data-viewer/src/index.ts index 4d25f5a4c0d..f97db85b6b1 100644 --- a/webapp/packages/plugin-data-viewer/src/index.ts +++ b/webapp/packages/plugin-data-viewer/src/index.ts @@ -37,6 +37,7 @@ export * from './DatabaseDataModel/Actions/IDatabaseValueHolder.js'; export * from './DatabaseDataModel/Actions/Grid/GridEditAction.js'; export * from './DatabaseDataModel/Actions/Grid/GridViewAction.js'; export * from './DatabaseDataModel/Actions/Grid/IGridDataKey.js'; +export * from './DatabaseDataModel/Actions/Grid/IRestoreViewState.js'; export * from './DatabaseDataModel/Actions/Grid/GridSelectAction.js'; export * from './DatabaseDataModel/Actions/Grid/GridHistoryAction.js'; export * from './DatabaseDataModel/Actions/Grid/GridHistoryTypes.js'; @@ -49,6 +50,8 @@ export * from './DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction. export * from './DatabaseDataModel/Actions/DatabaseDataResultAction.js'; export * from './DatabaseDataModel/Actions/DatabaseEditAction.js'; export * from './DatabaseDataModel/Actions/General/DatabaseMetadataAction.js'; +export * from './DatabaseDataModel/DatabasePersistedStateStore.js'; +export * from './DatabaseDataModel/IDatabasePersistedStateStore.js'; export * from './DatabaseDataModel/Actions/DatabaseSelectAction.js'; export * from './DatabaseDataModel/Actions/IDatabaseDataCacheAction.js'; export * from './DatabaseDataModel/Actions/IDatabaseDataViewAction.js'; diff --git a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts index a9a59b599f7..cde79b422f2 100644 --- a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts @@ -210,7 +210,7 @@ export class QueryDataSource