diff --git a/server/bundles/io.cloudbeaver.product.ce/plugin.xml b/server/bundles/io.cloudbeaver.product.ce/plugin.xml
index da48a6a55c..bb214bd59e 100644
--- a/server/bundles/io.cloudbeaver.product.ce/plugin.xml
+++ b/server/bundles/io.cloudbeaver.product.ce/plugin.xml
@@ -12,7 +12,7 @@
-
+
diff --git a/webapp/packages/core-blocks/src/useMouse.ts b/webapp/packages/core-blocks/src/useMouse.ts
index 7f9957df2f..8fa3cd2c1e 100644
--- a/webapp/packages/core-blocks/src/useMouse.ts
+++ b/webapp/packages/core-blocks/src/useMouse.ts
@@ -25,6 +25,7 @@ interface IState {
export interface IMouseHook {
state: IState;
+ ref: T | null;
reference: (obj: T) => void;
}
@@ -73,5 +74,5 @@ export function useMouse(options: IOptions = {}): IMouseH
};
}, [reference]);
- return { state, reference: setReference };
+ return { ref: reference, state, reference: setReference };
}
diff --git a/webapp/packages/core-cli/configs/webpack.product.dev.config.js b/webapp/packages/core-cli/configs/webpack.product.dev.config.js
index fdfbd5baac..408faf7b5a 100644
--- a/webapp/packages/core-cli/configs/webpack.product.dev.config.js
+++ b/webapp/packages/core-cli/configs/webpack.product.dev.config.js
@@ -14,17 +14,17 @@ const CopyWebpackPlugin = require('copy-webpack-plugin')
const package = require(resolve('package.json'));
-const certPath = resolve(__dirname, '../../../../../server-cert.pem')
-const keyPath = resolve(__dirname, '../../../../../server-key.pem')
+const certPath = resolve(__dirname, '../../../../../certs/private.pem')
+const keyPath = resolve(__dirname, '../../../../../certs/private.key')
let https = false;
-// if(fs.existsSync(certPath) && fs.existsSync(keyPath)) {
-// https = {
-// key: fs.readFileSync(keyPath),
-// cert: fs.readFileSync(certPath)
-// }
-// }
+if(fs.existsSync(certPath) && fs.existsSync(keyPath)) {
+ https = {
+ key: fs.readFileSync(keyPath),
+ cert: fs.readFileSync(certPath),
+ }
+}
module.exports = (env, argv) => merge(commonConfig(env, argv), {
entry: {
diff --git a/webapp/packages/plugin-administration/src/locales/en.ts b/webapp/packages/plugin-administration/src/locales/en.ts
index 08d51eaee1..f83c0bcde2 100644
--- a/webapp/packages/plugin-administration/src/locales/en.ts
+++ b/webapp/packages/plugin-administration/src/locales/en.ts
@@ -11,7 +11,7 @@ export default [
['administration_configuration_wizard_configuration', 'Server configuration'],
['administration_configuration_wizard_configuration_step_description', 'Main server configuration'],
['administration_configuration_wizard_configuration_title', 'You can configure the main server parameters here.'],
- ['administration_configuration_wizard_configuration_message', 'You can allow integration to external services such as SSO once the easy configuration is completed.\n\r Administrator is a super user who can configure server, set databases connections, manage other users and much more. Please, remember the entered password. It is not possible to recover administrator password automatically.'],
+ ['administration_configuration_wizard_configuration_message', 'You will be able to add additional services after the server configuration.\n\r Administrator is a super user who can configure server, set databases connections, manage other users and much more. Please, remember the entered password. It is not possible to recover administrator password automatically.'],
['administration_configuration_tools_save_tooltip', 'Save configuration'],
['administration_configuration_tools_cancel_tooltip', 'Reset changes'],
diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/AuthenticationProviders.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/AuthenticationProviders.tsx
index 1b6e4e7b00..ba844c4ddf 100644
--- a/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/AuthenticationProviders.tsx
+++ b/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/AuthenticationProviders.tsx
@@ -43,14 +43,19 @@ export const AuthenticationProviders: PlaceholderComponent
{providerList.map(provider => {
const links = authProviderService.getServiceDescriptionLinks(provider);
- const disabled = provider.requiredFeatures.some(feat => !serverConfig.enabledFeatures?.includes(feat));
+ let disabled = provider.requiredFeatures.some(feat => !serverConfig.enabledFeatures?.includes(feat));
const tooltip = disabled ? `Following services need to be enabled: "${provider.requiredFeatures.join(', ')}"` : '';
+ if (
+ !localProvider
+ && primaryProvider?.id === provider.id
+ && serverConfig.enabledAuthProviders?.length === 1
+ && serverConfig.enabledAuthProviders.includes(provider.id)
+ ) {
+ disabled = true;
+ }
+
if (
configurationWizard
&& (
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor/CellEditor.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor/CellEditor.tsx
index 46b2e1f5dc..c17b3dd23c 100644
--- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor/CellEditor.tsx
+++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor/CellEditor.tsx
@@ -41,7 +41,7 @@ export interface IEditorRef {
focus: () => void;
}
-const lockNavigation = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'];
+const lockNavigation = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'Enter'];
export const CellEditor = observer, 'row' | 'column' | 'onClose'>, IEditorRef>(function CellEditor({
row,
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx
index e85c99af73..e00d43c8ee 100644
--- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx
+++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx
@@ -8,11 +8,11 @@
import { computed, observable } from 'mobx';
import { observer } from 'mobx-react-lite';
-import { useContext, useEffect, useMemo } from 'react';
-import type { CellRendererProps } from 'react-data-grid';
+import { useContext, useEffect } from 'react';
+import type { CalculatedColumn, CellRendererProps } from 'react-data-grid';
import { Cell } from 'react-data-grid';
-import { useMouse, useObjectRef, useObservableRef } from '@cloudbeaver/core-blocks';
+import { getComputed, useMouse, useObjectRef, useObservableRef } from '@cloudbeaver/core-blocks';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { IResultSetElementKey, IResultSetRowKey, isBooleanValuePresentationAvailable, DatabaseEditChangeType } from '@cloudbeaver/plugin-data-viewer';
@@ -65,7 +65,7 @@ export const CellRenderer = observer>(functi
editionState: computed,
}, { row, column, rowIdx });
- const classes = useMemo(() => computed(() => {
+ const classes = getComputed(() => {
let classes = '';
if (cellContext.isSelected) {
classes += ' rdg-cell-custom-selected';
@@ -88,7 +88,31 @@ export const CellRenderer = observer>(functi
}
}
return classes;
- }), []).get();
+ });
+
+ function isEditable(column: CalculatedColumn): boolean {
+ if (
+ !editingContext
+ || !tableDataContext
+ || !cellContext.cell
+ ) {
+ return false;
+ }
+
+ const resultColumn = tableDataContext.getColumnInfo(cellContext.cell.column);
+ const value = tableDataContext.getCellValue(cellContext.cell);
+
+ if (!resultColumn || value === undefined) {
+ return false;
+ }
+
+ const handleByBooleanFormatter = isBooleanValuePresentationAvailable(value, resultColumn);
+
+ return !(
+ handleByBooleanFormatter
+ || tableDataContext.isCellReadonly(cellContext.cell)
+ );
+ }
const state = useObjectRef(() => ({
mouseUp(event: React.MouseEvent) {
@@ -112,27 +136,8 @@ export const CellRenderer = observer>(functi
},
doubleClick(event: React.MouseEvent) {
if (
- !this.editingContext
- || !this.tableDataContext
+ !this.isEditable(this.column)
|| EventContext.has(event, EventStopPropagationFlag)
- || !cellContext.cell
- ) {
- return;
- }
-
- const resultColumn = this.tableDataContext.getColumnInfo(cellContext.cell.column);
- const value = this.tableDataContext.getCellValue(cellContext.cell);
-
- if (!resultColumn || value === undefined) {
- return;
- }
-
- const handleByBooleanFormatter = isBooleanValuePresentationAvailable(value, resultColumn);
-
- if (
- !this.column.editable
- || handleByBooleanFormatter
- || this.tableDataContext.format.isReadOnly(cellContext.cell)
) {
return;
}
@@ -147,6 +152,7 @@ export const CellRenderer = observer>(functi
dataGridContext,
editingContext,
tableDataContext,
+ isEditable,
}, ['doubleClick', 'mouseUp']);
useEffect(() => () => editingContext?.closeEditor(cellContext.position), []);
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts
index fb9de9b3dd..b1e69b9259 100644
--- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts
+++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts
@@ -25,6 +25,7 @@ export interface IDataGridContext {
isGridInFocus: () => boolean;
getEditorPortal: () => HTMLDivElement | null;
getDataGridApi: () => DataGridHandle | null;
+ focus: () => void;
}
export const DataGridContext = createContext(undefined as any);
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.ts
index 5b1d192d98..e58cffca07 100644
--- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.ts
+++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.ts
@@ -7,7 +7,7 @@
*/
import { injectable } from '@cloudbeaver/core-di';
-import { isBooleanValuePresentationAvailable, DatabaseEditChangeType, ResultSetEditAction, ResultSetFormatAction, ResultSetViewAction, DatabaseSelectAction, ResultSetSelectAction } from '@cloudbeaver/plugin-data-viewer';
+import { isBooleanValuePresentationAvailable, DatabaseEditChangeType, ResultSetEditAction, ResultSetFormatAction, ResultSetViewAction, ResultSetSelectAction } from '@cloudbeaver/plugin-data-viewer';
import { DataGridContextMenuService } from './DataGridContextMenuService';
@@ -124,7 +124,7 @@ export class DataGridContextMenuCellEditingService {
},
onClick(context) {
const editor = context.data.model.source.getAction(context.data.resultIndex, ResultSetEditAction);
- editor.addRowCopy(context.data.key.row);
+ editor.duplicateRow(context.data.key.row);
},
}
);
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts
index 5937a2b323..c18f1f1985 100644
--- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts
+++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts
@@ -62,7 +62,7 @@ export class DataGridContextMenuFilterService {
await model.requestDataAction(async () => {
constraints.setFilter(columnLabel, operator, filterValue);
- await model.refresh(true);
+ await model.request(true);
});
}
@@ -160,7 +160,7 @@ export class DataGridContextMenuFilterService {
await model.requestDataAction(async () => {
constraints.deleteData();
- await model.refresh(true);
+ await model.request(true);
});
},
}
@@ -403,7 +403,7 @@ export class DataGridContextMenuFilterService {
await model.requestDataAction(async () => {
constraints.deleteFilter(columnLabel);
- await model.refresh(true);
+ await model.request(true);
});
},
}
@@ -430,7 +430,7 @@ export class DataGridContextMenuFilterService {
await model.requestDataAction(async () => {
constraints.deleteDataFilters();
- await model.refresh(true);
+ await model.request(true);
});
},
}
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.ts
index 6585324ca8..8aaa42ea8e 100644
--- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.ts
+++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.ts
@@ -35,7 +35,7 @@ export class DataGridContextMenuOrderService {
await model.requestDataAction(async () => {
constraints.setOrder(columnLabel, order, true);
- await model.refresh(true);
+ await model.request(true);
});
}
@@ -143,7 +143,7 @@ export class DataGridContextMenuOrderService {
const constraints = context.data.model.source.getAction(context.data.resultIndex, ResultSetConstraintAction);
await context.data.model.requestDataAction(async () => {
constraints.deleteOrders();
- await context.data.model.refresh(true);
+ await context.data.model.request(true);
});
},
}
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 fb4cd0be06..9f01658aac 100644
--- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx
+++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx
@@ -13,9 +13,12 @@ import type { DataGridHandle } from 'react-data-grid';
import styled from 'reshadow';
import { TextPlaceholder, useObjectRef } from '@cloudbeaver/core-blocks';
+import { useService } from '@cloudbeaver/core-di';
+import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { Executor } from '@cloudbeaver/core-executor';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
+import { ClipboardService } from '@cloudbeaver/core-ui';
import {
DatabaseDataSelectActionsData, DatabaseEditChangeType, IDatabaseResultSet, IDataPresentationProps,
IResultSetEditActionData, IResultSetElementKey, IResultSetPartialKey, ResultSetDataKeysUtils, ResultSetSelectAction
@@ -51,6 +54,7 @@ const headerHeight = 28;
export const DataGridTable = observer>(function DataGridTable({ model, actions, resultIndex, className }) {
const translate = useTranslate();
+ const clipboardService = useService(ClipboardService);
const gridContainerRef = useRef(null);
const editorRef = useRef(null);
const dataGridDivRef = useRef(null);
@@ -64,13 +68,11 @@ export const DataGridTable = observer(null);
- const gridSelectionContext = useGridSelectionContext(tableData, selectionAction);
const editingContext = useEditing({
readonly: model.isReadonly() || model.isDisabled(resultIndex),
- onEdit: (position, key) => {
+ onEdit: (position, code, key) => {
const column = tableData.getColumn(position.idx);
const row = tableData.getRow(position.rowIdx);
@@ -80,22 +82,42 @@ export const DataGridTable = observer {
+ restoreFocus();
+ },
});
+ const tableData = useTableData(model, resultIndex, dataGridDivRef);
+ const gridSelectionContext = useGridSelectionContext(tableData, selectionAction);
+
+ function restoreFocus() {
+ const gridDiv = gridContainerRef.current;
+ const focusSink = gridDiv?.querySelector('.rdg-focus-sink');
+ focusSink?.focus();
+ }
+
function isGridInFocus(): boolean {
const gridDiv = gridContainerRef.current;
const focusSink = gridDiv?.querySelector('.rdg-focus-sink');
@@ -121,7 +143,7 @@ export const DataGridTable = observer {
dataGridRef.current?.selectCell({ idx: startPosition.colIdx, rowIdx: startPosition.rowIdx });
@@ -134,6 +156,99 @@ export const DataGridTable = observer) {
+ gridSelectedCellCopy.onKeydownHandler(event);
+
+ if (EventContext.has(event, EventStopPropagationFlag)) {
+ return;
+ }
+
+ const cell = selectionAction.getFocusedElement();
+ const activeElements = selectionAction.getActiveElements();
+ const activeRows = selectionAction.getActiveRows();
+
+ if (!cell) {
+ return;
+ }
+
+ const idx = tableData.getColumnIndexFromColumnKey(cell.column);
+ const rowIdx = tableData.getRowIndexFromKey(cell.row);
+ const position: CellPosition = { idx, rowIdx };
+
+ if (editingContext.isEditing(position)) {
+ return;
+ }
+
+ switch (event.nativeEvent.code) {
+ case 'Escape': {
+ tableData.editor.revert(...activeElements);
+ return;
+ }
+ case 'Insert': {
+ if (event.altKey) {
+ if (event.ctrlKey || event.metaKey) {
+ tableData.editor.duplicate(...activeRows);
+ } else {
+ tableData.editor.add(cell);
+ }
+ return;
+ }
+ }
+ }
+
+ const editingState = tableData.editor.getElementState(cell);
+
+ switch (event.nativeEvent.code) {
+ case 'Delete': {
+ const filteredRows = activeRows
+ .filter(cell => tableData.editor.getElementState(cell) !== DatabaseEditChangeType.delete);
+
+ if (filteredRows.length > 0) {
+ const editor = tableData.editor;
+ const firstRow = filteredRows[0];
+ const editingState = tableData.editor.getElementState(firstRow);
+
+ editor.delete(...filteredRows);
+
+ if (editingState === DatabaseEditChangeType.add) {
+ if (rowIdx - 1 > 0) {
+ dataGridRef.current?.selectCell({ idx, rowIdx: rowIdx - 1 });
+ }
+ } else {
+ if (rowIdx + 1 < tableData.rows.length) {
+ dataGridRef.current?.selectCell({ idx, rowIdx: rowIdx + 1 });
+ }
+ }
+ }
+
+ return;
+ }
+ case 'KeyV': {
+ if (editingState === DatabaseEditChangeType.delete) {
+ return;
+ }
+
+ if (event.ctrlKey || event.metaKey) {
+ if (!clipboardService.clipboardAvailable || clipboardService.state === 'denied' || tableData.isCellReadonly(cell)) {
+ return;
+ }
+
+ clipboardService
+ .read()
+ .then(value => tableData.editor.set(cell, value))
+ .catch();
+ return;
+ }
+ }
+ }
+
+ if (editingState === DatabaseEditChangeType.delete) {
+ return;
+ }
+
+ editingContext.edit({ idx, rowIdx }, event.nativeEvent.code, event.key);
+ }
+
useEffect(() => {
function syncEditor(data: IResultSetEditActionData) {
const editor = tableData.editor;
@@ -234,7 +349,7 @@ export const DataGridTable = observer editorRef.current,
getDataGridApi: () => dataGridRef.current,
- }), [model, actions, resultIndex, editorRef, dataGridRef, gridContainerRef]);
+ focus: restoreFocus,
+ }), [model, actions, resultIndex, editorRef, dataGridRef, gridContainerRef, restoreFocus]);
if (!tableData.columns.length) {
return {translate('data_grid_table_empty_placeholder')};
@@ -282,7 +398,7 @@ export const DataGridTable = observer
@@ -290,7 +406,6 @@ export const DataGridTable = observer(function OrderButton({
const nextOrder = getNextOrder(currentOrder);
await model.requestDataAction(async () => {
constraints.setOrder(attribute, nextOrder, e.ctrlKey || e.metaKey);
- await model.refresh(true);
+ await model.request(true);
});
};
+ function preventFocus(event: React.MouseEvent) {
+ event.preventDefault();
+ }
+
return styled(styles)(
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 131e939ede..39d4ae005e 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
@@ -93,13 +93,18 @@ export const TableColumnHeader = observer>(function Tab
}
}
}
- const handleColumnSelection = (e: React.MouseEvent) => {
- gridSelectionContext.selectColumn(calculatedColumn.idx, e.ctrlKey || e.metaKey);
- };
+
+ function preventFocus(event: React.MouseEvent) {
+ event.preventDefault();
+ }
+
+ function handleClick(event: React.MouseEvent) {
+ gridSelectionContext.selectColumn(calculatedColumn.idx, event.ctrlKey || event.metaKey);
+ }
return styled(useStyles(headerStyles))(
-
+
{icon && }
{!dataReadonly && columnReadOnly && }
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.tsx
index 06c26b63a6..1253115a80 100644
--- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.tsx
+++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.tsx
@@ -43,12 +43,21 @@ export const TableIndexColumnHeader = observer>(functio
throw new Error('Contexts required');
}
- const readonly = getComputed(
- () => tableDataContext.isReadOnly() || dataGridContext.model.isReadonly()
- );
+ const readonly = getComputed(() => (
+ tableDataContext.isReadOnly()
+ || dataGridContext.model.isReadonly()
+ ));
+
+ function preventFocus(event: React.MouseEvent) {
+ event.preventDefault();
+ }
+
+ function handleClick(event: React.MouseEvent) {
+ selectionContext.selectTable();
+ }
return styled(styles)(
- selectionContext.selectTable()}>
+
{readonly && }
{props.column.name}
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableDataContext.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableDataContext.ts
index 659df20b5c..65bfeb14c6 100644
--- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableDataContext.ts
+++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableDataContext.ts
@@ -49,6 +49,7 @@ export interface ITableData {
isIndexColumn: (columnKey: string) => boolean;
isIndexColumnInRange: (columnsRange: Array>) => boolean;
isReadOnly: () => boolean;
+ isCellReadonly: (key: Partial) => boolean;
}
export const TableDataContext = createContext(undefined as any);
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts
index 38ada8d52a..5d672e310a 100644
--- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts
+++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts
@@ -9,6 +9,7 @@
import { useCallback } from 'react';
import { useObjectRef } from '@cloudbeaver/core-blocks';
+import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { copyToClipboard } from '@cloudbeaver/core-utils';
import { IResultSetColumnKey, IResultSetElementKey, ResultSetDataKeysUtils, ResultSetSelectAction } from '@cloudbeaver/plugin-data-viewer';
@@ -65,9 +66,6 @@ function getSelectedCellsValue(
return rowsValues.join('\r\n');
}
-// needed for event.code
-type IKeyboardEvent = React.KeyboardEvent & KeyboardEvent;
-
export function useGridSelectedCellsCopy(
tableData: ITableData,
resultSetSelectAction: ResultSetSelectAction,
@@ -75,8 +73,10 @@ export function useGridSelectedCellsCopy(
) {
const props = useObjectRef({ tableData, selectionContext, resultSetSelectAction });
- const onKeydownHandler = useCallback((event: IKeyboardEvent) => {
- if ((event.ctrlKey || event.metaKey) && event.code === EVENT_KEY_CODE.C) {
+ const onKeydownHandler = useCallback((event: React.KeyboardEvent) => {
+ if ((event.ctrlKey || event.metaKey) && event.nativeEvent.code === EVENT_KEY_CODE.C) {
+ EventContext.set(event, EventStopPropagationFlag);
+
const focusedElement = props.resultSetSelectAction.getFocusedElement();
let value: string | null = null;
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.ts
index 7d94d7e3a8..c19b371f0f 100644
--- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.ts
+++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.ts
@@ -11,7 +11,7 @@ import type { Column } from 'react-data-grid';
import { useObservableRef } from '@cloudbeaver/core-blocks';
import { TextTools, uuid } from '@cloudbeaver/core-utils';
-import { IDatabaseDataModel, IDatabaseResultSet, IResultSetColumnKey, IResultSetRowKey, ResultSetDataAction, ResultSetDataKeysUtils, ResultSetEditAction, ResultSetFormatAction, ResultSetViewAction } from '@cloudbeaver/plugin-data-viewer';
+import { IDatabaseDataModel, IDatabaseResultSet, IResultSetColumnKey, IResultSetElementKey, IResultSetRowKey, ResultSetDataAction, ResultSetDataKeysUtils, ResultSetEditAction, ResultSetFormatAction, ResultSetViewAction } from '@cloudbeaver/plugin-data-viewer';
import { IndexFormatter } from './Formatters/IndexFormatter';
import { TableColumnHeader } from './TableColumnHeader/TableColumnHeader';
@@ -33,7 +33,8 @@ export const indexColumn: Column = {
export function useTableData(
model: IDatabaseDataModel,
resultIndex: number,
- gridDIVElement: React.RefObject
+ gridDIVElement: React.RefObject,
+ onCellKeyDown?: (event: React.KeyboardEvent) => void
): ITableData {
const format = model.source.getAction(resultIndex, ResultSetFormatAction);
const data = model.source.getAction(resultIndex, ResultSetDataAction);
@@ -77,6 +78,9 @@ export function useTableData(
editable: true,
width: Math.min(300, measuredCells[index]),
headerRenderer: TableColumnHeader,
+ editorOptions: {
+ onCellKeyDown,
+ },
}));
columns.unshift(indexColumn);
@@ -152,6 +156,18 @@ export function useTableData(
isReadOnly() {
return this.columnKeys.every(column => this.getColumnInfo(column)?.readOnly);
},
+ isCellReadonly(key: Partial) {
+ if (!key.column) {
+ return true;
+ }
+
+ const column = this.getColumnByDataIndex(key.column);
+
+ return (
+ !column.editable
+ || this.format.isReadOnly(key)
+ );
+ },
}), {
columns: computed,
rows: computed,
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/Editing/EditingContext.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/Editing/EditingContext.tsx
index 1126fd493e..d0e3d273ce 100644
--- a/webapp/packages/plugin-data-spreadsheet-new/src/Editing/EditingContext.tsx
+++ b/webapp/packages/plugin-data-spreadsheet-new/src/Editing/EditingContext.tsx
@@ -10,7 +10,7 @@ import { createContext } from 'react';
export interface IEditingContext {
readonly readonly: boolean;
- edit: (position: CellPosition, key?: string) => void;
+ edit: (position: CellPosition, code?: string, key?: string) => void;
closeEditor: (position: CellPosition) => void;
close: () => void;
isEditorActive: () => boolean;
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/Editing/useEditing.ts b/webapp/packages/plugin-data-spreadsheet-new/src/Editing/useEditing.ts
index 65189850de..f8f42beafb 100644
--- a/webapp/packages/plugin-data-spreadsheet-new/src/Editing/useEditing.ts
+++ b/webapp/packages/plugin-data-spreadsheet-new/src/Editing/useEditing.ts
@@ -24,7 +24,8 @@ function getPositionHash(position: CellPosition): string {
interface IEditingOptions {
readonly?: boolean;
- onEdit: (position: CellPosition, key?: string) => boolean;
+ onEdit: (position: CellPosition, code?: string, key?: string) => boolean;
+ onCloseEditor?: (position: CellPosition) => void;
}
export function useEditing(options: IEditingOptions): IEditingContext {
@@ -40,18 +41,12 @@ export function useEditing(options: IEditingOptions): IEditingContext {
get readonly() {
return state.readonly;
},
- edit(position: CellPosition, key?: string) {
+ edit(position: CellPosition, code?: string, key?: string) {
if (state.options.readonly) {
return;
}
- // TODO: not works yet
- switch (key) {
- case 'Escape':
- state.editingCells.delete(getPositionHash(position));
- break;
- }
- if (!state.options.onEdit(position, key)) {
+ if (!state.options.onEdit(position, code, key)) {
return;
}
@@ -62,8 +57,15 @@ export function useEditing(options: IEditingOptions): IEditingContext {
},
closeEditor(position: CellPosition) {
const info = state.editingCells.get(getPositionHash(position));
+
+ if (!info.editing) {
+ return;
+ }
+
info.editing = false;
state.editorOpened = false;
+
+ state.options.onCloseEditor?.(position);
},
close() {
state.editingCells.clear();
diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts
index 0e55375980..79ea99a8cf 100644
--- a/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts
+++ b/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts
@@ -126,7 +126,7 @@ export class DataViewerTabService {
// TODO: used for initial data fetch, but can repeat request each time data tab is selected,
// so probably should be refactored and managed by presentation
if (model.source.error === null && model.source.results.length === 0) {
- model.refresh();
+ model.request();
}
}
@@ -143,7 +143,7 @@ export class DataViewerTabService {
await model.requestDataAction(() => {
canClose = true;
});
- } catch {}
+ } catch { }
return canClose;
}
diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseEditAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseEditAction.ts
index 34432c147f..b54d42b394 100644
--- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseEditAction.ts
+++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseEditAction.ts
@@ -42,7 +42,7 @@ export abstract class DatabaseEditAction TValue | undefined;
set: (key: TKey, value: TValue) => void;
add: (key?: TKey) => void;
- addCopy: (key: TKey) => void;
+ duplicate: (...key: TKey[]) => void;
delete: (key: TKey) => void;
applyUpdate: (result: TResult) => void;
revert: (key: TKey) => void;
diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/Actions/DATA_VIEWER_CONSTRAINTS_DELETE_ACTION.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/Actions/DATA_VIEWER_CONSTRAINTS_DELETE_ACTION.ts
new file mode 100644
index 0000000000..c965dc4db8
--- /dev/null
+++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/Actions/DATA_VIEWER_CONSTRAINTS_DELETE_ACTION.ts
@@ -0,0 +1,15 @@
+/*
+ * 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 { createAction } from '@cloudbeaver/core-view';
+
+export const DATA_VIEWER_CONSTRAINTS_DELETE_ACTION = createAction('data-viewer-constraints-delete', {
+ label: 'data_grid_table_delete_filters_and_orders',
+ tooltip: 'data_grid_table_delete_filters_and_orders',
+ icon: 'erase',
+});
diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/Actions/DATA_VIEWER_REFRESH_ACTION.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/Actions/DATA_VIEWER_REFRESH_ACTION.ts
new file mode 100644
index 0000000000..d26c35424f
--- /dev/null
+++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/Actions/DATA_VIEWER_REFRESH_ACTION.ts
@@ -0,0 +1,15 @@
+/*
+ * 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 { createAction } from '@cloudbeaver/core-view';
+
+export const DATA_VIEWER_REFRESH_ACTION = createAction('data-viewer-refresh', {
+ label: 'ui_refresh',
+ tooltip: 'data_viewer_refresh_result_set',
+ icon: 'reload',
+});
diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.ts
index deb6e53414..be0634fc3f 100644
--- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.ts
+++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.ts
@@ -208,21 +208,35 @@ export class ResultSetEditAction
}
}
- addCopy(key: IResultSetElementKey): void {
- this.addRowCopy(key.row);
- }
+ duplicate(...keys: IResultSetElementKey[]): void {
+ const rows: IResultSetRowKey[] = [];
+ const rowKeys = new Set();
- addRowCopy(row: IResultSetRowKey): void {
- let value = this.data.getRowValue(row);
+ for (const key of keys) {
+ const serialized = ResultSetDataKeysUtils.serialize(key.row);
- const editedValue = this.editorData
- .get(ResultSetDataKeysUtils.serialize(row));
-
- if (editedValue) {
- value = editedValue.update;
+ if (!rowKeys.has(serialized)) {
+ rows.push(key.row);
+ rowKeys.add(serialized);
+ }
}
- this.addRow(row, JSON.parse(JSON.stringify(value)));
+ this.duplicateRow(...rows);
+ }
+
+ duplicateRow(...rows: IResultSetRowKey[]): void {
+ for (const row of rows) {
+ let value = this.data.getRowValue(row);
+
+ const editedValue = this.editorData
+ .get(ResultSetDataKeysUtils.serialize(row));
+
+ if (editedValue) {
+ value = editedValue.update;
+ }
+
+ this.addRow(row, JSON.parse(JSON.stringify(value)));
+ }
}
delete(...keys: IResultSetElementKey[]): void {
diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts
index 3ade790767..5c570ebb0c 100644
--- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts
+++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
-import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction, toJS } from 'mobx';
import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
import { ResultDataFormat } from '@cloudbeaver/core-sdk';
@@ -160,6 +160,46 @@ export class ResultSetSelectAction extends DatabaseSelectAction();
+
+ const elements = this.getSelectedElements();
+
+ for (const cell of elements) {
+ const key = ResultSetDataKeysUtils.serialize(cell.row);
+
+ if (!rowsKeys.has(key)) {
+ cells.push(cell);
+ rowsKeys.add(key);
+ }
+ }
+
+ return cells;
+ }
+
+ getActiveRows(): IResultSetElementKey[] {
+ const elements = this.getSelectedRows();
+ const focus = this.getFocusedElement();
+
+ if (elements.length === 0 && focus) {
+ return [focus];
+ }
+
+ return elements;
+ }
+
set(key: IResultSetPartialKey, selected: boolean, silent?: boolean): void {
if (key.row === undefined) {
for (const row of this.view.rowKeys) {
@@ -233,7 +273,7 @@ export class ResultSetSelectAction extends DatabaseSelectAction {
}
async refresh(concurrent?: boolean): Promise {
+ if (concurrent) {
+ await this.source.refreshData();
+ return;
+ }
+ await this.requestDataAction(() => this.source.refreshData());
+ }
+
+ async request(concurrent?: boolean): Promise {
if (concurrent) {
await this.source.requestData();
return;
diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts
index 19ca8f307e..b3f7349812 100644
--- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts
+++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
-import { observable, makeObservable, action } from 'mobx';
+import { observable, makeObservable, action, toJS } from 'mobx';
import type { IConnectionExecutionContext } from '@cloudbeaver/core-connections';
import { ResultDataFormat } from '@cloudbeaver/core-sdk';
@@ -26,6 +26,7 @@ implements IDatabaseDataSource {
results: TResult[];
offset: number;
count: number;
+ prevOptions: Readonly | null;
options: TOptions | null;
requestInfo: IRequestInfo;
error: Error | null;
@@ -45,6 +46,7 @@ implements IDatabaseDataSource {
this.results = [];
this.offset = 0;
this.count = 0;
+ this.prevOptions = null;
this.options = null;
this.disabled = false;
this.activeRequest = null;
@@ -69,6 +71,7 @@ implements IDatabaseDataSource {
results: observable,
offset: observable,
count: observable,
+ prevOptions: observable,
options: observable,
requestInfo: observable,
error: observable.ref,
@@ -276,6 +279,13 @@ implements IDatabaseDataSource {
}
}
+ async refreshData(): Promise {
+ if (this.prevOptions) {
+ this.options = toJS(this.prevOptions);
+ }
+ await this.requestData();
+ }
+
async saveData(): Promise {
if (this.activeRequest) {
try {
@@ -318,6 +328,7 @@ implements IDatabaseDataSource {
abstract dispose(): Promise;
async requestDataAction(): Promise {
+ this.prevOptions = toJS(this.options);
return this.request(this.results);
}
}
diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts
index b4df88841c..b7568cd363 100644
--- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts
+++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts
@@ -45,6 +45,7 @@ export interface IDatabaseDataModel Promise;
save: () => Promise;
refresh: (concurrent?: boolean) => Promise;
+ request: (concurrent?: boolean) => Promise;
reload: () => Promise;
requestDataPortion: (offset: number, count: number) => Promise;
cancel: () => Promise | void;
diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts
index e5a6bee7da..6cd6388f42 100644
--- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts
+++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts
@@ -33,6 +33,7 @@ export interface IDatabaseDataSource | null;
readonly options: TOptions | null;
readonly requestInfo: IRequestInfo;
readonly error: Error | null;
@@ -80,6 +81,7 @@ export interface IDatabaseDataSource Promise;
runTask: (task: () => Promise) => Promise;
requestData: () => Promise | void;
+ refreshData: () => Promise | void;
saveData: () => Promise | void;
cancel: () => Promise | void;
clearError: () => void;
diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts
index 58f22b560d..f401719239 100644
--- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts
+++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts
@@ -101,19 +101,17 @@ export class TableFooterMenuService {
return !editor?.hasFeature('add');
},
isDisabled(context) {
- const select = context.data.model.source.getActionImplementation(
- context.data.resultIndex,
- DatabaseSelectAction
- );
-
- const focus = select?.getFocusedElement();
-
- return (
+ if (
context.data.model.isLoading()
|| context.data.model.isDisabled(context.data.resultIndex)
|| !context.data.model.source.hasResult(context.data.resultIndex)
- || !focus
- );
+ ) {
+ return true;
+ }
+
+ const selectedElements = getActiveElements(context.data.model, context.data.resultIndex);
+
+ return selectedElements.length === 0;
},
onClick(context) {
const editor = context.data.model.source.getActionImplementation(
@@ -125,16 +123,9 @@ export class TableFooterMenuService {
return;
}
- const select = context.data.model.source.getActionImplementation(
- context.data.resultIndex,
- DatabaseSelectAction
- );
+ const selectedElements = getActiveElements(context.data.model, context.data.resultIndex);
- const focus = select?.getFocusedElement();
-
- if (focus) {
- editor.addCopy(focus);
- }
+ editor.duplicate(...selectedElements);
},
});
this.registerMenuItem({
@@ -375,12 +366,5 @@ function getActiveElements(model: IDatabaseDataModel,
DatabaseSelectAction
);
- const selectedElements = select?.getSelectedElements() || [];
- const focus = select?.getFocusedElement();
-
- if (selectedElements.length === 0 && focus) {
- selectedElements.push(focus);
- }
-
- return selectedElements;
+ return select?.getActiveElements() ?? [];
}
diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL.ts
new file mode 100644
index 0000000000..2a8705a1ad
--- /dev/null
+++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL.ts
@@ -0,0 +1,14 @@
+/*
+ * 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 { createDataContext } from '@cloudbeaver/core-view';
+
+import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel';
+import type { IDatabaseDataOptions } from '../../DatabaseDataModel/IDatabaseDataOptions';
+
+export const DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL = createDataContext>('data-viewer-database-data-model');
diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX.ts
new file mode 100644
index 0000000000..e2d073e34c
--- /dev/null
+++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX.ts
@@ -0,0 +1,11 @@
+/*
+ * 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 { createDataContext } from '@cloudbeaver/core-view';
+
+export const DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX = createDataContext('data-viewer-database-data-model-result-index');
diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/DATA_VIEWER_DATA_MODEL_TOOLS_MENU.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/DATA_VIEWER_DATA_MODEL_TOOLS_MENU.ts
new file mode 100644
index 0000000000..7e3d491049
--- /dev/null
+++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/DATA_VIEWER_DATA_MODEL_TOOLS_MENU.ts
@@ -0,0 +1,11 @@
+/*
+ * 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 { createMenu } from '@cloudbeaver/core-view';
+
+export const DATA_VIEWER_DATA_MODEL_TOOLS_MENU = createMenu('data-viewer-data-model-tools', 'Data viewer data model tools menu');
diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.tsx
index 374b07ed37..02cc79a389 100644
--- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.tsx
+++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.tsx
@@ -27,16 +27,18 @@ const styles = css`
interface Props {
model: IDatabaseDataModel;
resultIndex: number;
+ className?: string;
}
export const TableHeader = observer(function TableHeader({
model,
resultIndex,
+ className,
}) {
const service = useService(TableHeaderService);
return styled(styles)(
-
+
);
diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderMenu.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderMenu.tsx
new file mode 100644
index 0000000000..1e501838cf
--- /dev/null
+++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderMenu.tsx
@@ -0,0 +1,98 @@
+/*
+ * 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 { observer } from 'mobx-react-lite';
+import { css } from 'reshadow';
+
+import type { PlaceholderComponent } from '@cloudbeaver/core-blocks';
+import { composes } from '@cloudbeaver/core-theming';
+import { MenuBar } from '@cloudbeaver/core-ui';
+import { useMenu } from '@cloudbeaver/core-view';
+
+import { DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL } from './DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL';
+import { DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX } from './DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX';
+import { DATA_VIEWER_DATA_MODEL_TOOLS_MENU } from './DATA_VIEWER_DATA_MODEL_TOOLS_MENU';
+import type { ITableHeaderPlaceholderProps } from './TableHeaderService';
+
+const TABLE_HEADER_MENU_BAR_STYLES = composes(
+ css`
+ menu-bar {
+ composes: theme-border-color-background theme-background-surface theme-text-on-surface from global;
+ }
+ menu-bar-item {
+ composes: theme-ripple from global;
+ }
+ MenuSeparator {
+ composes: theme-border-color-background from global;
+ }
+`,
+ css`
+ menu-bar {
+ composes: theme-typography--body2 from global;
+ display: flex;
+ margin-left: 8px;
+ box-sizing: border-box;
+ border: 1px solid;
+ height: 24px;
+ }
+
+ menu-bar-item {
+ padding: 4px;
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ background: transparent;
+ outline: none;
+ color: inherit;
+
+ &[use|hidden] {
+ display: none;
+ }
+
+ & IconOrImage {
+ display: block;
+ width: 16px;
+ }
+
+ & Loader {
+ width: 16px;
+ }
+
+ & item-label {
+ display: block;
+ text-transform: uppercase;
+ font-weight: 700;
+ }
+
+ & IconOrImage + item-label, & Loader + item-label {
+ padding-left: 8px
+ }
+ }
+
+ MenuSeparator {
+ height: 100%;
+ margin: 0;
+ border: 0;
+ border-right: 1px solid;
+ }
+ `
+);
+
+export const TableHeaderMenu: PlaceholderComponent = observer(function TableHeaderMenu({
+ model,
+ resultIndex,
+}) {
+ const menu = useMenu(DATA_VIEWER_DATA_MODEL_TOOLS_MENU);
+
+ menu.context.set(DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL, model);
+ menu.context.set(DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX, resultIndex);
+
+ return (
+
+ );
+});
diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts
index c076c11ccc..7957bdfff0 100644
--- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts
+++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts
@@ -8,8 +8,15 @@
import { PlaceholderContainer } from '@cloudbeaver/core-blocks';
import { injectable, Bootstrap } from '@cloudbeaver/core-di';
+import { MenuService, ActionService, DATA_CONTEXT_MENU } from '@cloudbeaver/core-view';
+import { DATA_VIEWER_CONSTRAINTS_DELETE_ACTION } from '../../DatabaseDataModel/Actions/ResultSet/Actions/DATA_VIEWER_CONSTRAINTS_DELETE_ACTION';
+import { ResultSetConstraintAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction';
import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel';
+import { DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL } from './DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL';
+import { DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX } from './DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX';
+import { DATA_VIEWER_DATA_MODEL_TOOLS_MENU } from './DATA_VIEWER_DATA_MODEL_TOOLS_MENU';
+import { TableHeaderMenu } from './TableHeaderMenu';
import { TableWhereFilter } from './TableWhereFilter';
export interface ITableHeaderPlaceholderProps {
@@ -21,8 +28,79 @@ export interface ITableHeaderPlaceholderProps {
export class TableHeaderService extends Bootstrap {
readonly tableHeaderPlaceholder = new PlaceholderContainer();
+ constructor(
+ private readonly menuService: MenuService,
+ private readonly actionService: ActionService
+ ) {
+ super();
+ }
+
register(): void {
this.tableHeaderPlaceholder.add(TableWhereFilter, 1);
+ this.tableHeaderPlaceholder.add(TableHeaderMenu, 2);
+
+ this.actionService.addHandler({
+ id: 'table-header-menu-base-handler',
+ isActionApplicable(context, action) {
+ const menu = context.find(DATA_CONTEXT_MENU, DATA_VIEWER_DATA_MODEL_TOOLS_MENU);
+ const model = context.tryGet(DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL);
+ const resultIndex = context.tryGet(DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX);
+
+ if (!menu || !model || resultIndex === undefined) {
+ return false;
+ }
+
+ return true;
+ },
+ handler: async (context, action) => {
+ switch (action) {
+ case DATA_VIEWER_CONSTRAINTS_DELETE_ACTION: {
+ const model = context.get(DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL);
+ const resultIndex = context.get(DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX);
+ const constraints = model.source.tryGetAction(resultIndex, ResultSetConstraintAction);
+
+ if (constraints) {
+ constraints.deleteData();
+ await model.request();
+ }
+ break;
+ }
+ }
+ },
+ getActionInfo: (context, action) => {
+ if (context.get(DATA_CONTEXT_MENU) === DATA_VIEWER_DATA_MODEL_TOOLS_MENU) {
+ return { ...action.info, label: '' };
+ }
+
+ return action.info;
+ },
+ isDisabled: (context, action) => {
+ const model = context.get(DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL);
+ const resultIndex = context.get(DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX);
+
+ if (model.isLoading() || model.isDisabled(resultIndex)) {
+ return true;
+ }
+
+ if (action === DATA_VIEWER_CONSTRAINTS_DELETE_ACTION) {
+ const constraints = model.source.tryGetAction(resultIndex, ResultSetConstraintAction);
+
+ if (constraints) {
+ return constraints.filterConstraints.length === 0 && constraints.orderConstraints.length === 0;
+ }
+ }
+
+ return true;
+ },
+ });
+
+ this.menuService.addCreator({
+ isApplicable: context => context.get(DATA_CONTEXT_MENU) === DATA_VIEWER_DATA_MODEL_TOOLS_MENU,
+ getItems: (context, items) => [
+ ...items,
+ DATA_VIEWER_CONSTRAINTS_DELETE_ACTION,
+ ],
+ });
}
load(): void | Promise { }
diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx
index 3575f6387e..2041a8a0ad 100644
--- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx
+++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx
@@ -7,16 +7,15 @@
*/
import { observer } from 'mobx-react-lite';
-import { useCallback } from 'react';
import styled, { css } from 'reshadow';
import { InlineEditor } from '@cloudbeaver/core-app';
-import { PlaceholderComponent, useObjectRef } from '@cloudbeaver/core-blocks';
+import type { PlaceholderComponent } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { composes, useStyles } from '@cloudbeaver/core-theming';
-import { ResultSetConstraintAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction';
import type { ITableHeaderPlaceholderProps } from './TableHeaderService';
+import { useWhereFilter } from './useWhereFilter';
const styles = composes(
css`
@@ -37,65 +36,20 @@ export const TableWhereFilter: PlaceholderComponent 0 && model.source.requestInfo.requestFilter) {
- filterValue = model.source.requestInfo.requestFilter;
- }
- }
-
- const setValue = useCallback((filterValue: string) => {
- model.source.options.whereFilter = filterValue;
-
- const constraints = model.source.tryGetAction(resultIndex, ResultSetConstraintAction);
- constraints?.deleteFilters();
- }, [model.source.options]);
-
- const props = useObjectRef({ model, resultIndex, filterValue });
-
- const handleApply = useCallback(() => {
- const { model, resultIndex } = props;
- if (model.isLoading() || model.isDisabled(resultIndex)) {
- return;
- }
- model.refresh();
- }, []);
-
- const resetFilter = useCallback(async () => {
- const { model, resultIndex } = props;
- const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction);
- if (model.isLoading() || model.isDisabled(resultIndex)) {
- return;
- }
-
- await model.requestDataAction(async () => {
- constraints.deleteDataFilters();
-
- const applyNeeded = !!model.requestInfo.requestFilter;
- if (applyNeeded) {
- await model.refresh(true);
- }
- });
- }, []);
+ const state = useWhereFilter(model, resultIndex);
return styled(useStyles(styles))(
);
});
diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/useWhereFilter.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/useWhereFilter.ts
new file mode 100644
index 0000000000..d5697c8a9d
--- /dev/null
+++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/useWhereFilter.ts
@@ -0,0 +1,81 @@
+/*
+ * 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 { action, computed } from 'mobx';
+
+import { useObservableRef } from '@cloudbeaver/core-blocks';
+
+import { ResultSetConstraintAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction';
+import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel';
+import type { IDatabaseDataOptions } from '../../DatabaseDataModel/IDatabaseDataOptions';
+
+interface IState {
+ model: IDatabaseDataModel;
+ resultIndex: number;
+ readonly filter: string;
+ readonly constraints: ResultSetConstraintAction | null;
+ readonly disabled: boolean;
+ readonly applicableFilter: boolean;
+ set: (value: string) => void;
+ apply: () => Promise;
+}
+
+export function useWhereFilter(
+ model: IDatabaseDataModel,
+ resultIndex: number
+): Readonly {
+ return useObservableRef(() => ({
+ get filter() {
+ if (this.constraints?.filterConstraints.length && this.model.source.requestInfo.requestFilter) {
+ return this.model.requestInfo.requestFilter;
+ }
+
+ return this.model.source.options?.whereFilter ?? '';
+ },
+ get constraints() {
+ if (!this.model.source.hasResult(this.resultIndex)) {
+ return null;
+ }
+
+ return this.model.source.tryGetAction(this.resultIndex, ResultSetConstraintAction) ?? null;
+ },
+ get disabled() {
+ const supported = this.constraints?.supported ?? false;
+ return !supported || this.model.isLoading() || this.model.isDisabled(resultIndex);
+ },
+ get applicableFilter() {
+ return this.model.source.prevOptions?.whereFilter !== this.model.source.options?.whereFilter
+ || this.model.source.options?.whereFilter !== this.model.source.requestInfo.requestFilter;
+ },
+ set(value: string) {
+ if (this.constraints) {
+ this.constraints.deleteFilters();
+ }
+
+ if (this.model.source.options) {
+ this.model.source.options.whereFilter = value;
+ }
+ },
+ async apply() {
+ if (!this.applicableFilter || this.model.isLoading() || this.model.isDisabled(this.resultIndex)) {
+ return;
+ }
+
+ await this.model.request();
+ },
+ }),
+ {
+ filter: computed,
+ constraints: computed,
+ disabled: computed,
+ applicableFilter: computed,
+ set: action.bound,
+ apply: action.bound,
+ },
+ { model, resultIndex });
+}
diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx
index 9878810717..b957934260 100644
--- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx
+++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx
@@ -77,6 +77,7 @@ const viewerStyles = composes(
margin: 0;
}
TablePresentationBar {
+ padding-top: 40px;
&:first-child {
margin-right: 4px;
}
@@ -235,7 +236,6 @@ export const TableViewer = observer(function TableViewer({
return styled(styles)(
-
(function TableViewer({
onPresentationChange={dataTableActions.setPresentation}
/>
+
@@ -257,6 +258,13 @@ export const TableViewer = observer(function TableViewer({
presentation={presentation}
resultIndex={resultIndex}
/>
+
+ dataModel.source.cancel()}
+ />
{valuePanelDisplayed && }
@@ -274,13 +282,7 @@ export const TableViewer = observer(function TableViewer({
-
- dataModel.source.cancel()}
- />
+