From c39fae5b3b064d5ad4116a7899ab24bdf2e253c6 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Mon, 17 May 2021 19:26:11 +0300 Subject: [PATCH] feat(plugin-data-viewer): CB-665 sort by column from context menu --- .../core-app/public/icons/sorting.png | Bin 0 -> 3934 bytes .../ConnectionSelectorController.ts | 6 +- .../src/Menu/ContextMenu/ContextMenu.ts | 6 +- .../src/Menu/ContextMenu/IContextMenuItem.ts | 3 + .../src/Menu/MenuPanel/menuPanelStyles.ts | 2 + .../src/Menu/models/ComputedMenuItemModel.ts | 1 + .../src/Menu/models/MenuOptionsStore.ts | 3 +- .../DataGrid/DataGridContextMenuService.ts | 101 ++++++++++++++-- .../DataGridSorting/DataGridSortingContext.ts | 21 ---- .../DataGridSorting/useGridSortingContext.tsx | 56 --------- .../src/DataGrid/DataGridTable.tsx | 73 ++++++------ .../TableColumnHeader/TableColumnHeader.tsx | 28 ++--- .../src/locales/en.ts | 2 + .../src/locales/ru.ts | 2 + .../Actions/IDatabaseDataAction.ts | 3 + .../Actions/IDatabaseDataSortAction.ts | 17 +++ .../Actions/ResultSet/ResultSetDataAction.ts | 8 ++ .../Actions/ResultSet/ResultSetSortAction.ts | 112 ++++++++++++++++++ .../src/TableViewer/TableViewerModel.ts | 6 +- .../packages/plugin-data-viewer/src/index.ts | 1 + 20 files changed, 297 insertions(+), 154 deletions(-) create mode 100644 webapp/packages/core-app/public/icons/sorting.png delete mode 100644 webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSorting/DataGridSortingContext.ts delete mode 100644 webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSorting/useGridSortingContext.tsx create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataSortAction.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSortAction.ts diff --git a/webapp/packages/core-app/public/icons/sorting.png b/webapp/packages/core-app/public/icons/sorting.png new file mode 100644 index 0000000000000000000000000000000000000000..1d94297cfc6113b44f25dadba8ca3ef333a2bd84 GIT binary patch literal 3934 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 literal 0 HcmV?d00001 diff --git a/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelectorController.ts b/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelectorController.ts index 35c0d8b141..b6f0f03114 100644 --- a/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelectorController.ts +++ b/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelectorController.ts @@ -75,14 +75,14 @@ export class ConnectionSelectorController { || this.getObjectContainerItems().length === 0; } - private get currentConnectionTitle(): string | undefined { + private get currentConnectionTitle(): string { if (this.currentConnection) { return this.currentConnection.name; } return 'app_topnavbar_connection_schema_manager_not_selected'; } - private get currentObjectContainerTitle(): string | undefined { + private get currentObjectContainerTitle(): string { const value = NodeManagerUtils.concatSchemaAndCatalog( this.connectionSelectorService.currentObjectCatalogId, this.connectionSelectorService.currentObjectSchemaId @@ -97,7 +97,7 @@ export class ConnectionSelectorController { private get currentObjectContainerIcon(): string { if (this.connectionSelectorService.currentObjectSchema?.features?.includes(EObjectFeature.schema)) { - // TODO move such kind of icon paths to a set of constants + // TODO move such kind of icon paths to a set of constants return 'schema_system'; } if (this.connectionSelectorService.currentObjectCatalog?.features?.includes(EObjectFeature.catalog)) { diff --git a/webapp/packages/core-dialogs/src/Menu/ContextMenu/ContextMenu.ts b/webapp/packages/core-dialogs/src/Menu/ContextMenu/ContextMenu.ts index 406eb7f6a0..620fbee840 100644 --- a/webapp/packages/core-dialogs/src/Menu/ContextMenu/ContextMenu.ts +++ b/webapp/packages/core-dialogs/src/Menu/ContextMenu/ContextMenu.ts @@ -84,7 +84,7 @@ class ComputedMenuItemOptionsWithContext implements IComputedMenuItemOptions onClick?: () => void; // set title or getter title?: TLocalizationToken; - titleGetter?: () => TLocalizationToken | undefined; + titleGetter?: () => TLocalizationToken; tooltip?: TLocalizationToken; tooltipGetter?: () => TLocalizationToken | undefined; isDisabled?: () => boolean; @@ -100,7 +100,6 @@ class ComputedMenuItemOptionsWithContext implements IComputedMenuItemOptions private context: IMenuContext) { // doesn't depend on context this.title = options.title; - this.titleGetter = options.titleGetter; this.tooltip = options.tooltip; this.tooltipGetter = options.tooltipGetter; this.icon = options.icon; @@ -122,6 +121,9 @@ class ComputedMenuItemOptionsWithContext implements IComputedMenuItemOptions if (options.isChecked) { this.isChecked = () => options.isChecked!(this.context); } + if (options.titleGetter) { + this.titleGetter = () => options.titleGetter!(this.context); + } } } diff --git a/webapp/packages/core-dialogs/src/Menu/ContextMenu/IContextMenuItem.ts b/webapp/packages/core-dialogs/src/Menu/ContextMenu/IContextMenuItem.ts index 88822b08d3..b304934273 100644 --- a/webapp/packages/core-dialogs/src/Menu/ContextMenu/IContextMenuItem.ts +++ b/webapp/packages/core-dialogs/src/Menu/ContextMenu/IContextMenuItem.ts @@ -6,6 +6,8 @@ * you may not use this file except in compliance with the License. */ +import type { TLocalizationToken } from '@cloudbeaver/core-localization'; + import type { IMenuItemOptions } from '../models/MenuOptionsStore'; import type { IMenuContext } from './IMenuContext'; @@ -14,6 +16,7 @@ import type { IMenuContext } from './IMenuContext'; */ export interface IContextMenuItem extends IMenuItemOptions { onClick?: (context: IMenuContext) => void; + titleGetter?: (context: IMenuContext) => TLocalizationToken; // if isPresent is false menu item will not be included in resulting context menu isPresent: (context: IMenuContext) => boolean; isDisabled?: (context: IMenuContext) => boolean; diff --git a/webapp/packages/core-dialogs/src/Menu/MenuPanel/menuPanelStyles.ts b/webapp/packages/core-dialogs/src/Menu/MenuPanel/menuPanelStyles.ts index ef7629cb5e..528b96341c 100644 --- a/webapp/packages/core-dialogs/src/Menu/MenuPanel/menuPanelStyles.ts +++ b/webapp/packages/core-dialogs/src/Menu/MenuPanel/menuPanelStyles.ts @@ -38,6 +38,7 @@ export const menuPanelStyles = composes( & box { display: flex; align-items: center; + flex: 1; height: inherit; } } @@ -110,6 +111,7 @@ export const menuPanelStyles = composes( & IconOrImage { width: 16px; height: 16px; + object-fit: contain; } } ` diff --git a/webapp/packages/core-dialogs/src/Menu/models/ComputedMenuItemModel.ts b/webapp/packages/core-dialogs/src/Menu/models/ComputedMenuItemModel.ts index 334813b673..283e3181b1 100644 --- a/webapp/packages/core-dialogs/src/Menu/models/ComputedMenuItemModel.ts +++ b/webapp/packages/core-dialogs/src/Menu/models/ComputedMenuItemModel.ts @@ -15,6 +15,7 @@ import type { IMenuItemOptions, MenuItemType } from './MenuOptionsStore'; export interface IComputedMenuItemOptions extends IMenuItemOptions { onClick?: () => void; + titleGetter?: () => string; isDisabled?: () => boolean; isHidden?: () => boolean; isChecked?: () => boolean; diff --git a/webapp/packages/core-dialogs/src/Menu/models/MenuOptionsStore.ts b/webapp/packages/core-dialogs/src/Menu/models/MenuOptionsStore.ts index 9149336837..09488ffd60 100644 --- a/webapp/packages/core-dialogs/src/Menu/models/MenuOptionsStore.ts +++ b/webapp/packages/core-dialogs/src/Menu/models/MenuOptionsStore.ts @@ -9,6 +9,7 @@ import type { TLocalizationToken } from '@cloudbeaver/core-localization'; import { OrderedMap } from '@cloudbeaver/core-utils'; +import type { IMenuContext } from '../ContextMenu/IMenuContext'; import type { IMenuPanel } from '../IMenuPanel'; const DEFAULT_ITEM_ORDER = 100; @@ -18,7 +19,7 @@ export interface IMenuItemOptions { id: string; // set title or getter title?: TLocalizationToken; - titleGetter?: () => TLocalizationToken | undefined; + titleGetter?: (...args: any[]) => TLocalizationToken; // set icon or getter icon?: string; iconGetter?: () => string | undefined; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenuService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenuService.ts index 52759fb571..0324f387be 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenuService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenuService.ts @@ -8,7 +8,7 @@ import { injectable } from '@cloudbeaver/core-di'; import { ContextMenuService, IMenuPanel } from '@cloudbeaver/core-dialogs'; -import type { IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer'; +import { IDatabaseDataModel, ResultSetDataAction, ESortMode, getNextSortMode, ResultSetSortAction, SortMode } from '@cloudbeaver/plugin-data-viewer'; export interface IDataGridCellMenuContext { model: IDatabaseDataModel; @@ -21,15 +21,20 @@ export interface IDataGridCellMenuContext { export class DataGridContextMenuService { static cellContext = 'data-grid-cell-context-menu'; private static menuToken = 'dataGridCell'; + private static menuSortingToken = 'menuSorting'; constructor( - private contextMenuService: ContextMenuService + private contextMenuService: ContextMenuService, ) { } getMenuToken(): string { return DataGridContextMenuService.menuToken; } + getMenuSortingToken(): string { + return DataGridContextMenuService.menuSortingToken; + } + constructMenuWithContext( model: IDatabaseDataModel, resultIndex: number, @@ -43,28 +48,104 @@ export class DataGridContextMenuService { }); } + private getColumnName(model: IDatabaseDataModel, resultIndex: number, columnIndex: number) { + const data = model.source.getAction(resultIndex, ResultSetDataAction); + return data.getColumn(columnIndex)?.name; + } + + private async changeSortMode( + model: IDatabaseDataModel, + resultIndex: number, + columnIndex: number, + sortMode: SortMode + ) { + const columnName = this.getColumnName(model, resultIndex, columnIndex)!; + const sorting = model.source.getAction(resultIndex, ResultSetSortAction); + + sorting.setSortMode(columnName, sortMode, true); + await model.refresh(); + } + + private getSortMode(model: IDatabaseDataModel, resultIndex: number, columnIndex: number) { + const columnName = this.getColumnName(model, resultIndex, columnIndex)!; + const sorting = model.source.getAction(resultIndex, ResultSetSortAction); + + return sorting.getSortMode(columnName); + } + register() { this.contextMenuService.addMenuItem( this.contextMenuService.getRootMenuToken(), { - id: this.getMenuToken(), + id: this.getMenuSortingToken(), isPresent(context) { return context.contextType === DataGridContextMenuService.cellContext; }, - order: 2, - title: 'Cell', + order: 0, + title: 'data_grid_table_sorting', + icon: '/icons/sorting.png', + isPanel: true, } ); - this.contextMenuService.addMenuItem( - this.getMenuToken(), + this.getMenuSortingToken(), { - id: 'edit', + id: 'asc', isPresent(context) { return context.contextType === DataGridContextMenuService.cellContext; }, - order: 2, - title: 'Cell', + isDisabled: context => context.data.model.isLoading(), + onClick: async context => { + await this.changeSortMode(context.data.model, context.data.resultIndex, context.data.column, ESortMode.asc); + }, + type: 'radio', + isChecked: context => this.getSortMode( + context.data.model, + context.data.resultIndex, + context.data.column) === ESortMode.asc, + titleGetter: context => { + const columnName = this.getColumnName(context.data.model, context.data.resultIndex, context.data.column)!; + return `Order by ${columnName} ASC`; + }, + } + ); + this.contextMenuService.addMenuItem( + this.getMenuSortingToken(), + { + id: 'desc', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isDisabled: context => context.data.model.isLoading(), + onClick: async context => { + await this.changeSortMode(context.data.model, context.data.resultIndex, context.data.column, ESortMode.desc); + }, + type: 'radio', + isChecked: context => this.getSortMode( + context.data.model, + context.data.resultIndex, + context.data.column) === ESortMode.desc, + titleGetter: context => { + const columnName = this.getColumnName(context.data.model, context.data.resultIndex, context.data.column)!; + return `Order by ${columnName} DESC`; + }, + } + ); + this.contextMenuService.addMenuItem( + this.getMenuSortingToken(), + { + id: 'toggleSorting', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isDisabled: context => context.data.model.isLoading(), + onClick: async context => { + const nextSortMode = getNextSortMode( + this.getSortMode(context.data.model, context.data.resultIndex, context.data.column) + ); + await this.changeSortMode(context.data.model, context.data.resultIndex, context.data.column, nextSortMode); + }, + title: 'data_grid_table_sorting_toggle', } ); } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSorting/DataGridSortingContext.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSorting/DataGridSortingContext.ts deleted file mode 100644 index bbbc1a2fda..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSorting/DataGridSortingContext.ts +++ /dev/null @@ -1,21 +0,0 @@ - -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2021 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 { createContext } from 'react'; - -import type { SqlDataFilterConstraint } from '@cloudbeaver/core-sdk'; -import type { SortMode } from '@cloudbeaver/plugin-data-viewer'; - -export interface IDataGridSortingContext { - sortedColumns: Map; - setSortMode: (colId: string, sort: SortMode, multiple: boolean) => Promise | void; - getSortMode: (colId: string) => SortMode; -} - -export const DataGridSortingContext = createContext(null); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSorting/useGridSortingContext.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSorting/useGridSortingContext.tsx deleted file mode 100644 index 4c37a9fed1..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSorting/useGridSortingContext.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2021 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 { observable } from 'mobx'; -import { useCallback, useMemo, useState } from 'react'; - -import type { SqlDataFilterConstraint } from '@cloudbeaver/core-sdk'; -import type { IDatabaseDataModel, SortMode } from '@cloudbeaver/plugin-data-viewer'; - -import type { IDataGridSortingContext } from './DataGridSortingContext'; - -export function useGridSortingContext(model: IDatabaseDataModel) { - const [sortedColumns] = useState(() => observable.map()); - - const refreshModel = useCallback(async (constraints: SqlDataFilterConstraint[]) => { - await model.setOptions({ ...model.source.options, constraints }).refresh(); - }, [model]); - - const setColumnSortMode = useCallback(async (colId: string, sort: SortMode, multiple: boolean) => { - if (!multiple) { - sortedColumns.clear(); - } - - if (sort !== null) { - sortedColumns.set(colId, { attribute: colId, orderAsc: sort === 'asc' }); - } else { - sortedColumns.delete(colId); - } - - const constraints = Array.from(sortedColumns.values()) - .map((constrait, idx) => ({ ...constrait, orderPosition: idx })); - - await refreshModel(constraints); - }, [refreshModel, sortedColumns]); - - const getColumnSortMode = useCallback((colId: string): SortMode => { - if (!sortedColumns.has(colId)) { - return null; - } - - return sortedColumns.get(colId)!.orderAsc ? 'asc' : 'desc'; - }, [sortedColumns]); - - const gridSortingContext: IDataGridSortingContext = useMemo(() => ({ - sortedColumns, - setSortMode: setColumnSortMode, - getSortMode: getColumnSortMode, - }), [sortedColumns, setColumnSortMode, getColumnSortMode]); - - return gridSortingContext; -} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx index 73b1f9fe42..8189d872c2 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx @@ -23,8 +23,6 @@ import { reactGridStyles } from '../styles/styles'; import { DataGridContext, IColumnResizeInfo, IDataGridContext } from './DataGridContext'; import { DataGridSelectionContext } from './DataGridSelection/DataGridSelectionContext'; import { useGridSelectionContext } from './DataGridSelection/useGridSelectionContext'; -import { DataGridSortingContext } from './DataGridSorting/DataGridSortingContext'; -import { useGridSortingContext } from './DataGridSorting/useGridSortingContext'; import { CellFormatter } from './Formatters/CellFormatter'; import { RowRenderer } from './RowRenderer/RowRenderer'; import { TableDataContext } from './TableDataContext'; @@ -54,7 +52,6 @@ export const DataGridTable: React.FC = observer(function DataGridTable({ const tableData = useTableData(model, resultIndex); - const gridSortingContext = useGridSortingContext(model); const gridSelectionContext = useGridSelectionContext(tableData, selectionAction); const editingContext = useEditing({ readonly: model.isReadonly(), @@ -155,42 +152,40 @@ export const DataGridTable: React.FC = observer(function DataGridTable({ return styled(styles)( - - - - - - columnResize.execute({ column: idx, width })} - onScroll={handleScroll} - /> -
- - - - - + + + + + columnResize.execute({ column: idx, width })} + onScroll={handleScroll} + /> +
+ + + + ); }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx index a1bf8578e5..3a54f4d3c5 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx @@ -15,11 +15,10 @@ import { StaticImage, Icon } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; import type { SqlResultSet } from '@cloudbeaver/core-sdk'; import { composes, useStyles } from '@cloudbeaver/core-theming'; -import type { SortMode } from '@cloudbeaver/plugin-data-viewer'; +import { getNextSortMode, ResultSetSortAction } from '@cloudbeaver/plugin-data-viewer'; import { DataGridContext } from '../DataGridContext'; import { DataGridSelectionContext } from '../DataGridSelection/DataGridSelectionContext'; -import { DataGridSortingContext } from '../DataGridSorting/DataGridSortingContext'; const headerStyles = css` table-header { @@ -102,15 +101,15 @@ export const TableColumnHeader: React.FC> = observer(fu column: calculatedColumn, }) { const dataGridContext = useContext(DataGridContext); - const gridSortingContext = useContext(DataGridSortingContext); const gridSelectionContext = useContext(DataGridSelectionContext); const translate = useTranslate(); - if (!dataGridContext || !gridSortingContext || !gridSelectionContext) { - throw new Error('One of the following contexts are missed(data grid context, grid sorting context, grid selection context)'); + if (!dataGridContext || !gridSelectionContext) { + throw new Error('One of the following contexts are missed(data grid context, grid selection context)'); } const model = dataGridContext.model; + const sorting = model.source.getAction(dataGridContext.resultIndex, ResultSetSortAction); const columnName = calculatedColumn.name as string; const column = getColumn(Number(calculatedColumn.key), model.getResult(dataGridContext.resultIndex)?.data); @@ -118,7 +117,8 @@ export const TableColumnHeader: React.FC> = observer(fu // TODO we want to get "sortable" property from SqlResultColumn data const sortable = model.source.results.length === 1; - const currentSortMode = gridSortingContext.getSortMode(columnName); + const currentSortMode = sorting.getSortMode(columnName); + const columnTooltip = columnName + (column?.fullTypeName ? ': ' + column.fullTypeName : ''); const handleSort = (e: React.MouseEvent) => { @@ -126,19 +126,9 @@ export const TableColumnHeader: React.FC> = observer(fu if (loading) { return; } - - let nextSort: SortMode; - switch (currentSortMode) { - case 'asc': - nextSort = 'desc'; - break; - case 'desc': - nextSort = null; - break; - default: - nextSort = 'asc'; - } - gridSortingContext.setSortMode(columnName, nextSort, e.ctrlKey || e.metaKey); + const nextSortMode = getNextSortMode(sorting.getSortMode(columnName)); + sorting.setSortMode(columnName, nextSortMode, e.ctrlKey || e.metaKey); + model.refresh(); }; const handleColumnSelection = (e: React.MouseEvent) => { diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts index 8cb2385bd6..afddde70c7 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts @@ -1,3 +1,5 @@ export default [ + ['data_grid_table_sorting', 'Sorting'], + ['data_grid_table_sorting_toggle', 'Toggle sorting'], ['data_grid_table_tooltip_column_header_sort', 'Sort by column'], ]; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts index a091d87fd5..a9369744fd 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts @@ -1,3 +1,5 @@ export default [ + ['data_grid_table_sorting', 'Сортировка'], + ['data_grid_table_sorting_toggle', 'Переключить сортировку'], ['data_grid_table_tooltip_column_header_sort', 'Сортировка по колонке'], ]; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataAction.ts index afb6d6cd79..c58bcdf289 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataAction.ts @@ -6,6 +6,8 @@ * you may not use this file except in compliance with the License. */ +import type { SqlResultColumn } from '@cloudbeaver/core-sdk'; + import type { IDatabaseDataAction } from '../IDatabaseDataAction'; import type { IDatabaseDataResult } from '../IDatabaseDataResult'; import type { IResultSetElementKey } from './ResultSet/IResultSetElementKey'; @@ -13,4 +15,5 @@ import type { IResultSetElementKey } from './ResultSet/IResultSetElementKey'; export interface IDatabaseResultDataAction extends IDatabaseDataAction { getCellValue: (cell: IResultSetElementKey) => any; + getColumn: (columnIndex: number) => SqlResultColumn | undefined; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataSortAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataSortAction.ts new file mode 100644 index 0000000000..2b5533cc47 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataSortAction.ts @@ -0,0 +1,17 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 type { IDatabaseDataAction } from '../IDatabaseDataAction'; +import type { IDatabaseDataResult } from '../IDatabaseDataResult'; +import type { SortMode } from './ResultSet/ResultSetSortAction'; + +export interface IDatabaseDataSortAction + extends IDatabaseDataAction { + setSortMode: (columnName: string, sortMode: SortMode, multiple: boolean) => void; + getSortMode: (columnName: string) => SortMode; +} 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 6d520677cb..927ab760bb 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 @@ -18,4 +18,12 @@ export class ResultSetDataAction extends DatabaseDataAction + implements IDatabaseDataSortAction { + static dataFormat = ResultDataFormat.Resultset; + + private clear() { + if (!this.source.options) { + return; + } + + this.source.options.constraints = []; + } + + private deleteConstraint(columnName: string) { + if (!this.source.options) { + return; + } + + this.source.options.constraints = this.source.options.constraints + .filter(constraint => constraint.attribute !== columnName); + } + + private reorderConstraints() { + if (!this.source.options) { + return; + } + + this.source.options.constraints = this.source.options.constraints + .map((constaint, idx) => ({ ...constaint, orderPosition: idx })); + } + + setSortMode(columnName: string, sortMode: SortMode, multiple: boolean): void { + if (!this.source.options) { + return; + } + + const resetSortMode = sortMode === null; + const currentConstraint = this.source.options.constraints.find(constraint => constraint.attribute === columnName); + + if (currentConstraint && multiple) { + if (resetSortMode) { + this.deleteConstraint(currentConstraint.attribute); + this.reorderConstraints(); + } else { + currentConstraint.orderAsc = sortMode === ESortMode.asc; + } + return; + } + + if (!multiple) { + this.clear(); + } + + if (!resetSortMode) { + this.source.options.constraints.push({ + attribute: columnName, + orderPosition: this.source.options.constraints.length, + orderAsc: sortMode === ESortMode.asc, + }); + } + } + + getSortMode(columnName: string): SortMode { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + const currentConstraints = this.source.options.constraints; + const currentConstraint = currentConstraints.find(constraint => constraint.attribute === columnName); + + if (!currentConstraint) { + return null; + } + + return currentConstraint.orderAsc ? ESortMode.asc : ESortMode.desc; + } +} + +export function getNextSortMode(sortMode: SortMode): SortMode { + switch (sortMode) { + case ESortMode.asc: + return ESortMode.desc; + case ESortMode.desc: + return null; + default: + return ESortMode.asc; + } +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerModel.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerModel.ts index 0a8a8d279e..2594c6e49a 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerModel.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerModel.ts @@ -12,6 +12,8 @@ import { Subject, Observable } from 'rxjs'; import type { SqlDataFilterConstraint } from '@cloudbeaver/core-sdk'; import { MetadataMap } from '@cloudbeaver/core-utils'; +import type { SortMode } from '../DatabaseDataModel/Actions/ResultSet/ResultSetSortAction'; +import { ESortMode } from '../DatabaseDataModel/Actions/ResultSet/ResultSetSortAction'; import { DatabaseDataAccessMode } from '../DatabaseDataModel/IDatabaseDataSource'; import type { RowDiff } from './TableDataModel/EditedRow'; import type { TableColumn } from './TableDataModel/TableColumn'; @@ -27,8 +29,6 @@ export const fetchingSettings = { export type AgGridRow = any[]; -export type SortMode = 'asc' | 'desc' | null; - export type SortModel = Array<{ colId: string; sort: SortMode; @@ -257,7 +257,7 @@ export class TableViewerModel { getSortModel(): SortModel { return Array.from(this.sortedColumns.values()).map(v => ({ colId: v.attribute, - sort: v.orderAsc ? 'asc' : 'desc', + sort: v.orderAsc ? ESortMode.asc : ESortMode.desc, })); } diff --git a/webapp/packages/plugin-data-viewer/src/index.ts b/webapp/packages/plugin-data-viewer/src/index.ts index 926794d867..4e71be961b 100644 --- a/webapp/packages/plugin-data-viewer/src/index.ts +++ b/webapp/packages/plugin-data-viewer/src/index.ts @@ -6,6 +6,7 @@ export * from './DatabaseDataModel/Actions/ResultSet/IResultSetElementKey'; export * from './DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; export * from './DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; export * from './DatabaseDataModel/Actions/ResultSet/ResultSetDataAction'; +export * from './DatabaseDataModel/Actions/ResultSet/ResultSetSortAction'; export * from './DatabaseDataModel/Actions/DatabaseDataActionDecorator'; export * from './DatabaseDataModel/Actions/IDatabaseDataFormatAction'; export * from './DatabaseDataModel/Actions/IDatabaseDataSelectAction';