diff --git a/webapp/packages/core-app/public/icons/erase.png b/webapp/packages/core-app/public/icons/erase.png new file mode 100644 index 0000000000..b69785aad7 Binary files /dev/null and b/webapp/packages/core-app/public/icons/erase.png differ diff --git a/webapp/packages/core-app/public/icons/filter.png b/webapp/packages/core-app/public/icons/filter.png new file mode 100644 index 0000000000..955f59934e Binary files /dev/null and b/webapp/packages/core-app/public/icons/filter.png differ diff --git a/webapp/packages/core-app/public/icons/filter_clipboard.png b/webapp/packages/core-app/public/icons/filter_clipboard.png new file mode 100644 index 0000000000..110b3824f7 Binary files /dev/null and b/webapp/packages/core-app/public/icons/filter_clipboard.png differ diff --git a/webapp/packages/core-app/public/icons/filter_custom.png b/webapp/packages/core-app/public/icons/filter_custom.png new file mode 100644 index 0000000000..3acf6afe00 Binary files /dev/null and b/webapp/packages/core-app/public/icons/filter_custom.png differ diff --git a/webapp/packages/core-app/public/icons/filter_reset.png b/webapp/packages/core-app/public/icons/filter_reset.png new file mode 100644 index 0000000000..399bd24f5c Binary files /dev/null and b/webapp/packages/core-app/public/icons/filter_reset.png differ diff --git a/webapp/packages/core-app/public/icons/filter_value.png b/webapp/packages/core-app/public/icons/filter_value.png new file mode 100644 index 0000000000..261cbc484f Binary files /dev/null and b/webapp/packages/core-app/public/icons/filter_value.png differ diff --git a/webapp/packages/core-app/public/icons/icons.svg b/webapp/packages/core-app/public/icons/icons.svg index cfa9e11531..884042fb2a 100644 --- a/webapp/packages/core-app/public/icons/icons.svg +++ b/webapp/packages/core-app/public/icons/icons.svg @@ -177,10 +177,13 @@ - - - - + + + + + + + + - diff --git a/webapp/packages/core-app/public/icons/permissions.png b/webapp/packages/core-app/public/icons/permissions.png new file mode 100644 index 0000000000..57e0ed0311 Binary files /dev/null and b/webapp/packages/core-app/public/icons/permissions.png differ diff --git a/webapp/packages/core-app/public/icons/sort_decrease.png b/webapp/packages/core-app/public/icons/sort_decrease.png deleted file mode 100644 index 69e46d483c..0000000000 Binary files a/webapp/packages/core-app/public/icons/sort_decrease.png and /dev/null differ diff --git a/webapp/packages/core-app/public/icons/sort_increase.png b/webapp/packages/core-app/public/icons/sort_increase.png deleted file mode 100644 index 3bdf742b87..0000000000 Binary files a/webapp/packages/core-app/public/icons/sort_increase.png and /dev/null differ diff --git a/webapp/packages/core-app/public/icons/sort_unknown.png b/webapp/packages/core-app/public/icons/sort_unknown.png deleted file mode 100644 index 7815511cb7..0000000000 Binary files a/webapp/packages/core-app/public/icons/sort_unknown.png and /dev/null differ diff --git a/webapp/packages/core-bootstrap/src/manifest.ts b/webapp/packages/core-bootstrap/src/manifest.ts index 0d540b85e8..36f31b25ae 100644 --- a/webapp/packages/core-bootstrap/src/manifest.ts +++ b/webapp/packages/core-bootstrap/src/manifest.ts @@ -94,7 +94,7 @@ import { RouterService, ScreenService } from '@cloudbeaver/core-routing'; import { EnvironmentService, GraphQLService } from '@cloudbeaver/core-sdk'; import { LocalStorageSaveService, SettingsService } from '@cloudbeaver/core-settings'; import { ThemeService } from '@cloudbeaver/core-theming'; -import { NavigationService, OptionsPanelService } from '@cloudbeaver/core-ui'; +import { NavigationService, OptionsPanelService, ClipboardBootstrap, ClipboardService } from '@cloudbeaver/core-ui'; import { ActiveViewService } from '@cloudbeaver/core-view'; export const coreManifest: PluginManifest = { @@ -135,6 +135,8 @@ export const coreManifest: PluginManifest = { PermissionsService, CoreSettingsService, CommonDialogService, + ClipboardService, + ClipboardBootstrap, SessionExpireService, SessionExpiredDialogService, ConnectionsLocaleService, diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index 61691e047a..355ee5e076 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -25,9 +25,12 @@ export const defaultENLocale = [ ['ui_data_delete_confirmation', 'Delete confirmation'], ['ui_no_matches_placeholder', 'Your search returned no matches.'], ['ui_information', 'Information'], + ['ui_clipboard', 'Clipboard'], ['ui_copy_to_clipboard', 'Copy'], ['ui_copy_to_clipboard_copied', 'Copied'], ['ui_copy_to_clipboard_failed_to_copy', 'Failed to copy'], + ['ui_clipboard_access_denied_title', 'Clipboard access denied'], + ['ui_clipboard_access_denied_message', 'You need to give access to clipboard to use some of the app features'], ['root_permission_denied', 'You have no permissions'], ['app_root_session_expired_title', 'Session expired'], ['app_root_session_expired_message', 'Session has been expired. Do you want to reload?'], diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index 5ad7af55af..87bd355056 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -23,9 +23,12 @@ export const defaultRULocale = [ ['ui_no_matches_placeholder', 'По вашему запросу ничего не найдено.'], ['ui_data_delete_confirmation', 'Подтвердите удаление'], ['ui_information', 'Информация'], + ['ui_clipboard', 'Буфер обмена'], ['ui_copy_to_clipboard', 'Копировать'], ['ui_copy_to_clipboard_copied', 'Скопировано'], ['ui_copy_to_clipboard_failed_to_copy', 'Не удалось скопировать'], + ['ui_clipboard_access_denied_title', 'Доступ к буферу обмена запрещен'], + ['ui_clipboard_access_denied_message', 'Чтобы использовать некоторые функции приложения, необходим доступ к буферу обмена'], ['root_permission_denied', 'Отказано в доступе'], ['app_root_session_expired_title', 'Сессия истекла'], ['app_root_session_expired_message', 'Сессия истекла. Хотите перезагрузить страницу?'], diff --git a/webapp/packages/core-sdk/src/queries/grid/getSqlExecuteTaskResults.gql b/webapp/packages/core-sdk/src/queries/grid/getSqlExecuteTaskResults.gql index 3ecb4d8746..a6a6574d6b 100644 --- a/webapp/packages/core-sdk/src/queries/grid/getSqlExecuteTaskResults.gql +++ b/webapp/packages/core-sdk/src/queries/grid/getSqlExecuteTaskResults.gql @@ -6,6 +6,7 @@ mutation getSqlExecuteTaskResults( ) { duration statusMessage + filterText results { title updateRowCount @@ -27,6 +28,11 @@ mutation getSqlExecuteTaskResults( readOnlyStatus scale typeName + supportedOperations { + id + expression + argumentCount + } } rows hasMoreData diff --git a/webapp/packages/core-sdk/src/queries/grid/updateResultsData.gql b/webapp/packages/core-sdk/src/queries/grid/updateResultsData.gql index bb41a25bfc..25708872a5 100644 --- a/webapp/packages/core-sdk/src/queries/grid/updateResultsData.gql +++ b/webapp/packages/core-sdk/src/queries/grid/updateResultsData.gql @@ -15,6 +15,7 @@ mutation updateResultsData( updateValues: $values ) { duration + filterText results { updateRowCount resultSet { diff --git a/webapp/packages/core-sdk/src/queries/grid/updateResultsDataBatch.gql b/webapp/packages/core-sdk/src/queries/grid/updateResultsDataBatch.gql index d590a29d2a..eb49271b7b 100644 --- a/webapp/packages/core-sdk/src/queries/grid/updateResultsDataBatch.gql +++ b/webapp/packages/core-sdk/src/queries/grid/updateResultsDataBatch.gql @@ -16,6 +16,7 @@ mutation updateResultsDataBatch( addedRows: $addedRows ) { duration + filterText results { updateRowCount resultSet { diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index 6c3a0f3964..2d84efa68b 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -65,6 +65,7 @@ export interface Query { sqlCompletionProposals?: Maybe>>; sqlDialectInfo?: Maybe; sqlListContexts?: Maybe>>; + sqlSupportedOperations: DataTypeLogicalOperation[]; templateConnections: ConnectionInfo[]; updateConnectionConfiguration: ConnectionInfo; userConnections: ConnectionInfo[]; @@ -247,6 +248,13 @@ export interface QuerySqlListContextsArgs { connectionId: Scalars['ID']; } +export interface QuerySqlSupportedOperationsArgs { + connectionId: Scalars['ID']; + contextId: Scalars['ID']; + resultsId: Scalars['ID']; + attributeIndex: Scalars['Int']; +} + export interface QueryUpdateConnectionConfigurationArgs { id: Scalars['ID']; config: ConnectionConfig; @@ -810,6 +818,7 @@ export interface SqlResultColumn { precision?: Maybe; readOnly: Scalars['Boolean']; readOnlyStatus?: Maybe; + supportedOperations: DataTypeLogicalOperation[]; } export interface DatabaseDocument { @@ -836,7 +845,8 @@ export interface SqlQueryResults { export interface SqlExecuteInfo { statusMessage?: Maybe; - duration?: Maybe; + duration: Scalars['Int']; + filterText?: Maybe; results: SqlQueryResults[]; } @@ -845,6 +855,12 @@ export interface SqlResultRow { updateValues?: Maybe; } +export interface DataTypeLogicalOperation { + id: Scalars['ID']; + expression: Scalars['String']; + argumentCount?: Maybe; +} + export enum AdminSubjectType { User = 'user', Role = 'role' @@ -1423,12 +1439,15 @@ export type GetSqlExecuteTaskResultsMutationVariables = Exact<{ export interface GetSqlExecuteTaskResultsMutation { result: ( - Pick + Pick & { results: Array<( Pick & { resultSet?: Maybe<( Pick - & { columns?: Maybe>>> } + & { columns?: Maybe + & { supportedOperations: Array> } + )>>>; } )>; } )>; } ); @@ -1444,7 +1463,7 @@ export type UpdateResultsDataMutationVariables = Exact<{ export interface UpdateResultsDataMutation { result?: Maybe<( - Pick + Pick & { results: Array<( Pick & { resultSet?: Maybe> } @@ -1463,7 +1482,7 @@ export type UpdateResultsDataBatchMutationVariables = Exact<{ export interface UpdateResultsDataBatchMutation { result?: Maybe<( - Pick + Pick & { results: Array<( Pick & { resultSet?: Maybe> } @@ -2308,6 +2327,7 @@ export const GetSqlExecuteTaskResultsDocument = ` result: asyncSqlExecuteResults(taskId: $taskId) { duration statusMessage + filterText results { title updateRowCount @@ -2329,6 +2349,11 @@ export const GetSqlExecuteTaskResultsDocument = ` readOnlyStatus scale typeName + supportedOperations { + id + expression + argumentCount + } } rows hasMoreData @@ -2347,6 +2372,7 @@ export const UpdateResultsDataDocument = ` updateValues: $values ) { duration + filterText results { updateRowCount resultSet { @@ -2368,6 +2394,7 @@ export const UpdateResultsDataBatchDocument = ` addedRows: $addedRows ) { duration + filterText results { updateRowCount resultSet { diff --git a/webapp/packages/core-ui/package.json b/webapp/packages/core-ui/package.json index 3298d98f50..c66cdaeec4 100644 --- a/webapp/packages/core-ui/package.json +++ b/webapp/packages/core-ui/package.json @@ -21,6 +21,8 @@ "@cloudbeaver/core-blocks": "^0.1.0", "@cloudbeaver/core-sdk": "^0.1.0", "@cloudbeaver/core-dialogs": "^0.1.0", + "@cloudbeaver/core-events": "^0.1.0", + "@cloudbeaver/core-utils": "^0.1.0", "mobx": "^5.x.x", "mobx-react-lite": "^3.x.x" } diff --git a/webapp/packages/core-ui/src/Clipboard/ClipboardBootstrap.ts b/webapp/packages/core-ui/src/Clipboard/ClipboardBootstrap.ts new file mode 100644 index 0000000000..9886d5e021 --- /dev/null +++ b/webapp/packages/core-ui/src/Clipboard/ClipboardBootstrap.ts @@ -0,0 +1,26 @@ +/* + * 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 { Bootstrap, injectable } from '@cloudbeaver/core-di'; + +import { ClipboardService } from './ClipboardService'; + +@injectable() +export class ClipboardBootstrap extends Bootstrap { + constructor( + private clipboardService: ClipboardService, + ) { + super(); + } + + register(): void { } + + async load(): Promise { + await this.clipboardService.tryActivateClipboard(); + } +} diff --git a/webapp/packages/core-ui/src/Clipboard/ClipboardService.ts b/webapp/packages/core-ui/src/Clipboard/ClipboardService.ts new file mode 100644 index 0000000000..5402d6027f --- /dev/null +++ b/webapp/packages/core-ui/src/Clipboard/ClipboardService.ts @@ -0,0 +1,95 @@ +/* + * 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 { makeObservable, observable } from 'mobx'; + +import { injectable } from '@cloudbeaver/core-di'; +import { ENotificationType, NotificationService } from '@cloudbeaver/core-events'; +import { copyToClipboard } from '@cloudbeaver/core-utils'; + +@injectable() +export class ClipboardService { + private readPermission: PermissionStatus | null; + private permissionUpdate: number; + clipboardAvailable: boolean; + clipboardValue: string | null; + + get state(): PermissionState { + if (this.permissionUpdate > 0 && this.readPermission) { + return this.readPermission.state; + } + + return 'prompt'; + } + + constructor( + private notificationService: NotificationService + ) { + makeObservable(this, { + clipboardValue: observable, + permissionUpdate: observable, + }); + this.permissionUpdate = 1; + this.readPermission = null; + this.clipboardValue = null; + this.clipboardAvailable = false; + } + + async read(): Promise { + if (!this.clipboardAvailable || this.state === 'denied') { + return null; + } + + try { + const value = await navigator.clipboard.readText(); + this.clipboardValue = value; + } catch (exeption) { + this.clipboardAvailable = false; + this.clipboardValue = null; + this.notificationService.logException(exeption, 'Failed to read from clipboard', '', true); + this.notificationService.logInfo({ title: 'ui_clipboard_access_denied_title', message: 'ui_clipboard_access_denied_message' }); + } + + return this.clipboardValue; + } + + write(value: string, notify: boolean): void { + try { + copyToClipboard(value); + if (notify) { + this.notificationService.notify({ title: 'ui_copy_to_clipboard_copied' }, ENotificationType.Info); + } + } catch (exception) { + this.notificationService.logException(exception, 'ui_copy_to_clipboard_failed_to_copy'); + } + } + + async tryActivateClipboard(): Promise { + if (!navigator.clipboard) { + this.clipboardAvailable = false; + return; + } + + this.clipboardAvailable = true; + await this.updatePermissionStatus(); + if (this.readPermission) { + this.readPermission.addEventListener('change', () => { + this.permissionUpdate++; + }); + } + } + + async updatePermissionStatus(): Promise { + try { + this.readPermission = await navigator.permissions.query({ name: 'clipboard-read' }); + } catch (exeption) { + this.notificationService.logException(exeption, 'Failed to get permission status', '', true); + this.readPermission = null; + } + } +} diff --git a/webapp/packages/core-ui/src/index.ts b/webapp/packages/core-ui/src/index.ts index 19bc2275fd..f68342462d 100644 --- a/webapp/packages/core-ui/src/index.ts +++ b/webapp/packages/core-ui/src/index.ts @@ -2,3 +2,5 @@ export * from './Screens/AppScreen/NavigationService'; export * from './Screens/AppScreen/OptionsPanelService'; export * from './AuthenticationProvider'; export * from './useAuthenticationAction'; +export * from './Clipboard/ClipboardBootstrap'; +export * from './Clipboard/ClipboardService'; diff --git a/webapp/packages/core-utils/src/index.ts b/webapp/packages/core-utils/src/index.ts index ae34073384..0fca1d2e8b 100644 --- a/webapp/packages/core-utils/src/index.ts +++ b/webapp/packages/core-utils/src/index.ts @@ -12,6 +12,7 @@ export * from './md5'; export * from './TextTools'; export * from './copyToClipboard'; export * from './declensionOfNumber'; +export * from './replaceMiddle'; export * from './Promises/CancellablePromise'; export * from './Promises/Deferred'; diff --git a/webapp/packages/core-utils/src/replaceMiddle.ts b/webapp/packages/core-utils/src/replaceMiddle.ts new file mode 100644 index 0000000000..830ab1586c --- /dev/null +++ b/webapp/packages/core-utils/src/replaceMiddle.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. + */ + +export function replaceMiddle(value: string, newMiddle: string, sideLength: number, limiter: number): string { + if (value.length < limiter) { + return value; + } + + return value.substr(0, sideLength) + newMiddle + value.substr(value.length - sideLength); +} 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 new file mode 100644 index 0000000000..0570be8f92 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts @@ -0,0 +1,407 @@ +/* + * 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 { injectable } from '@cloudbeaver/core-di'; +import { CommonDialogService, DialogueStateResult, IContextMenuItem, IMenuContext } from '@cloudbeaver/core-dialogs'; +import { ComputedContextMenuModel } from '@cloudbeaver/core-dialogs'; +import { ClipboardService } from '@cloudbeaver/core-ui'; +import { replaceMiddle } from '@cloudbeaver/core-utils'; +import { + wrapOperationArgument, + IDatabaseDataModel, + nullOperationsFilter, + ResultSetConstraintAction, + IS_NOT_NULL_ID, + IS_NULL_ID, + ResultSetDataAction, + IDatabaseResultSet, + IDatabaseDataOptions, + ResultSetFormatAction, + isFilterConstraint +} from '@cloudbeaver/plugin-data-viewer'; + +import { DataGridContextMenuService, IDataGridCellMenuContext } from '../DataGridContextMenuService'; +import { FilterCustomValueDialog } from './FilterCustomValueDialog'; + +@injectable() +export class DataGridContextMenuFilterService { + private static menuFilterToken = 'menuFilter'; + + constructor( + private dataGridContextMenuService: DataGridContextMenuService, + private commonDialogService: CommonDialogService, + private clipboardService: ClipboardService, + ) { + this.dataGridContextMenuService.onRootMenuOpen.addHandler(this.getClipboardValue.bind(this)); + } + + getMenuFilterToken(): string { + return DataGridContextMenuFilterService.menuFilterToken; + } + + private async applyFilter( + model: IDatabaseDataModel, + resultIndex: number, + columnIndex: number, + operator: string, + filterValue?: any, + ) { + if (model.isLoading() || model.isDisabled(resultIndex)) { + return; + } + + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + const data = model.source.getAction(resultIndex, ResultSetDataAction); + const columnLabel = data.getColumn(columnIndex)?.label || ''; + + constraints.setFilter(columnLabel, operator, filterValue); + await model.refresh(); + } + + private async getClipboardValue() { + if (this.clipboardService.state === 'granted') { + await this.clipboardService.read(); + } + } + + private getGeneralizedMenuItems( + context: IMenuContext, + value: any | (() => any), + icon: string, + ): Array> { + const { model, resultIndex, column } = context.data; + const data = model.source.getAction(resultIndex, ResultSetDataAction); + const format = model.source.getAction(resultIndex, ResultSetFormatAction); + const supportedOperations = data.getColumnOperations(column); + const columnLabel = data.getColumn(column)?.label || ''; + + return supportedOperations + .filter(operation => !nullOperationsFilter(operation)) + .map(operation => ({ + id: operation.id, + isPresent: () => true, + isDisabled(context) { + return context.data.model.isLoading(); + }, + titleGetter() { + const val = typeof value === 'function' ? value() : value; + const stringifyValue = format.toString(val); + const wrappedValue = wrapOperationArgument(operation.id, stringifyValue); + const clippeddValue = replaceMiddle(wrappedValue, ' ... ', 8, 30); + return `${columnLabel} ${operation.expression} ${clippeddValue}`; + }, + icon, + onClick: async () => { + const val = typeof value === 'function' ? value() : value; + await this.applyFilter(model, resultIndex, column, operation.id, val); + }, + })); + } + + register(): void { + this.dataGridContextMenuService.add( + this.dataGridContextMenuService.getMenuToken(), + { + id: this.getMenuFilterToken(), + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isHidden(context) { + return context.data.model.isDisabled(context.data.resultIndex) + || context.data.model.source.results.length > 1; + }, + order: 1, + title: 'data_grid_table_filter', + icon: '/icons/filter.png', + isPanel: true, + } + ); + this.dataGridContextMenuService.add( + this.getMenuFilterToken(), + { + id: 'clipboardValue', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isHidden: context => { + if (!this.clipboardService.clipboardAvailable || this.clipboardService.state === 'denied') { + return true; + } + + const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction); + const supportedOperations = data.getColumnOperations(context.data.column); + + return supportedOperations.length === 0; + }, + order: 0, + title: 'ui_clipboard', + icon: '/icons/filter_clipboard.png', + panel: new ComputedContextMenuModel({ + id: 'clipboardValuePanel', + menuItemsGetter: context => { + if (context.contextType !== DataGridContextMenuService.cellContext) { + return []; + } + + if (this.clipboardService.state === 'prompt') { + return [{ + id: 'permission', + isPresent: () => true, + isDisabled(context) { + return context.data.model.isLoading(); + }, + title: 'data_grid_table_context_menu_filter_clipboard_permission', + icon: '/icons/permissions.png', + onClick: async () => { + await this.clipboardService.read(); + }, + }]; + } + + const valueGetter = () => this.clipboardService.clipboardValue || ''; + const items = this.getGeneralizedMenuItems(context, valueGetter, '/icons/filter_clipboard.png'); + return items; + }, + }), + } + ); + this.dataGridContextMenuService.add( + this.getMenuFilterToken(), + { + id: 'cellValue', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isHidden: context => { + const { model, resultIndex, column, row } = context.data; + const data = model.source.getAction(resultIndex, ResultSetDataAction); + const format = model.source.getAction(resultIndex, ResultSetFormatAction); + const supportedOperations = data.getColumnOperations(column); + + return supportedOperations.length === 0 || format.isNull(data.getCellValue({ column, row })); + }, + order: 1, + title: 'data_grid_table_filter_cell_value', + icon: '/icons/filter_value.png', + panel: new ComputedContextMenuModel({ + id: 'cellValuePanel', + menuItemsGetter: context => { + const { model, resultIndex, column, row } = context.data; + const data = model.source.getAction(resultIndex, ResultSetDataAction); + const cellValue = data.getCellValue({ column, row }); + const items = this.getGeneralizedMenuItems(context, cellValue, '/icons/filter_value.png'); + return items; + }, + }), + } + ); + this.dataGridContextMenuService.add( + this.getMenuFilterToken(), + { + id: 'customValue', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isHidden: context => { + const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction); + const supportedOperations = data.getColumnOperations(context.data.column); + + return supportedOperations.length === 0; + }, + order: 2, + title: 'data_grid_table_filter_custom_value', + icon: '/icons/filter_custom.png', + separator: true, + panel: new ComputedContextMenuModel({ + id: 'customValuePanel', + menuItemsGetter: context => { + const { model, resultIndex, column, row } = context.data; + const format = model.source.getAction(resultIndex, ResultSetFormatAction); + const data = model.source.getAction(resultIndex, ResultSetDataAction); + const supportedOperations = data.getColumnOperations(column); + const cellValue = data.getCellValue({ column, row }); + const columnLabel = data.getColumn(column)?.label || ''; + + return supportedOperations + .filter(operation => !nullOperationsFilter(operation)) + .map(operation => { + const title = `${columnLabel} ${operation.expression}`; + + return { + id: operation.id, + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isDisabled(context) { + return context.data.model.isLoading(); + }, + title: title + ' ..', + icon: '/icons/filter_custom.png', + onClick: async () => { + const isNull = format.isNull(cellValue); + const stringifyCellValue = format.toString(cellValue); + const customValue = await this.commonDialogService.open( + FilterCustomValueDialog, + { + defaultValue: isNull ? '' : stringifyCellValue, + inputTitle: title + ':', + } + ); + + if (customValue === DialogueStateResult.Rejected || customValue === DialogueStateResult.Resolved) { + return; + } + + await this.applyFilter( + model, + resultIndex, + column, + operation.id, + customValue + ); + }, + }; + }); + }, + }), + } + ); + this.dataGridContextMenuService.add( + this.getMenuFilterToken(), + { + id: 'isNullValue', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isHidden: context => { + const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction); + const supportedOperations = data.getColumnOperations(context.data.column); + + return !supportedOperations.some(operation => operation.id === IS_NULL_ID); + }, + order: 3, + icon: '/icons/filter_value.png', + titleGetter: context => { + const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction); + const columnLabel = data.getColumn(context.data.column)?.label || ''; + return `${columnLabel} IS NULL`; + }, + onClick: async context => { + await this.applyFilter(context.data.model, context.data.resultIndex, context.data.column, IS_NULL_ID); + }, + + } + ); + this.dataGridContextMenuService.add( + this.getMenuFilterToken(), + { + id: 'isNotNullValue', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isHidden: context => { + const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction); + const supportedOperations = data.getColumnOperations(context.data.column); + + return !supportedOperations.some(operation => operation.id === IS_NOT_NULL_ID); + }, + order: 4, + icon: '/icons/filter_value.png', + separator: true, + titleGetter: context => { + const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction); + const columnLabel = data.getColumn(context.data.column)?.label || ''; + return `${columnLabel} IS NOT NULL`; + }, + onClick: async context => { + await this.applyFilter(context.data.model, context.data.resultIndex, context.data.column, IS_NOT_NULL_ID); + }, + } + ); + this.dataGridContextMenuService.add( + this.getMenuFilterToken(), + { + id: 'deleteFilter', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isHidden: context => { + const { model, resultIndex, column } = context.data; + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + const data = model.source.getAction(resultIndex, ResultSetDataAction); + + const columnLabel = data.getColumn(column)?.label || ''; + const currentConstraint = constraints.get(columnLabel); + + return !currentConstraint || !isFilterConstraint(currentConstraint); + }, + order: 5, + icon: '/icons/filter_reset.png', + titleGetter: context => { + const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction); + const columnLabel = data.getColumn(context.data.column)?.name || ''; + return `Delete filter for ${columnLabel}`; + }, + onClick: async context => { + const { model, resultIndex, column } = context.data; + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + const data = model.source.getAction(resultIndex, ResultSetDataAction); + const columnLabel = data.getColumn(column)?.label || ''; + + constraints.deleteFilter(columnLabel); + await model.refresh(); + }, + } + ); + this.dataGridContextMenuService.add( + this.getMenuFilterToken(), + { + id: 'deleteAllFilters', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isHidden: context => { + const { model, resultIndex } = context.data; + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + const filterConstraints = constraints.getFilterConstraints(); + + return filterConstraints.length === 0 && !model.requestInfo.requestFilter; + }, + order: 6, + title: 'data_grid_table_filter_reset_all_filters', + onClick: async context => { + const { model, resultIndex } = context.data; + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + + constraints.deleteDataFilters(); + await model.refresh(); + }, + } + ); + this.dataGridContextMenuService.add( + this.getMenuFilterToken(), + { + id: 'deleteFiltersAndSorting', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + order: 7, + title: 'data_grid_table_delete_filters_and_sorting', + icon: '/icons/erase.png', + onClick: async context => { + const { model, resultIndex } = context.data; + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + + constraints.deleteAll(); + model.source.options.whereFilter = ''; + await model.refresh(); + }, + } + ); + } +} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx new file mode 100644 index 0000000000..cc00caf188 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx @@ -0,0 +1,99 @@ +/* + * 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 { useState, useCallback, useRef, useEffect } from 'react'; +import styled, { css } from 'reshadow'; + +import { Button, InputFieldNew } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { CommonDialogWrapper, DialogComponent, DialogComponentProps } from '@cloudbeaver/core-dialogs'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { ClipboardService } from '@cloudbeaver/core-ui'; + +const styles = css` + CommonDialogWrapper { + min-width: 400px; + } + controls { + display: flex; + flex: 1; + height: 100%; + align-items: center; + margin: auto; + flex-direction: row-reverse; + } + Button { + margin-left: 24px; + } +`; + +interface IPayload { + inputTitle: string; + defaultValue: string | number; +} + +export const FilterCustomValueDialog: DialogComponent = observer( + function FilterCustomValueDialog({ + payload, + resolveDialog, + rejectDialog, + }: DialogComponentProps) { + const clipboardService = useService(ClipboardService); + const inputRef = useRef(null); + + const [value, setValue] = useState(payload.defaultValue); + const handleApply = useCallback(() => resolveDialog(value), [value, resolveDialog]); + const translate = useTranslate(); + + const getValueFromClipboard = useCallback(async () => { + const value = await clipboardService.read(); + if (value) { + setValue(value); + } + if (inputRef.current) { + inputRef.current.focus(); + } + }, [clipboardService]); + + useEffect(() => { + inputRef.current?.focus(); + }, []); + + return styled(styles)( + + + + {clipboardService.clipboardAvailable && clipboardService.state !== 'denied' && ( + + )} + + )} + onReject={rejectDialog} + > + + {payload.inputTitle} + + + ); + } +); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSortingService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.ts similarity index 58% rename from webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSortingService.ts rename to webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.ts index f916695176..6299c130d4 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSortingService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.ts @@ -7,22 +7,20 @@ */ import { injectable } from '@cloudbeaver/core-di'; -import { ContextMenuService } from '@cloudbeaver/core-dialogs'; -import { ESortMode, getNextSortMode, IDatabaseDataModel, ResultSetDataAction, ResultSetSortAction, SortMode } from '@cloudbeaver/plugin-data-viewer'; +import { EOrder, IDatabaseDataModel, ResultSetConstraintAction, ResultSetDataAction, Order } from '@cloudbeaver/plugin-data-viewer'; import { DataGridContextMenuService } from './DataGridContextMenuService'; @injectable() -export class DataGridContextMenuSortingService { - private static menuSortingToken = 'menuSorting'; +export class DataGridContextMenuOrderService { + private static menuOrderToken = 'menuOrder'; constructor( private dataGridContextMenuService: DataGridContextMenuService, - private contextMenuService: ContextMenuService, ) { } - getMenuSortingToken(): string { - return DataGridContextMenuSortingService.menuSortingToken; + getMenuOrderToken(): string { + return DataGridContextMenuOrderService.menuOrderToken; } private getColumnName(model: IDatabaseDataModel, resultIndex: number, columnIndex: number) { @@ -30,43 +28,43 @@ export class DataGridContextMenuSortingService { return data.getColumn(columnIndex)?.name; } - private async changeSortMode( + private async changeOrder( model: IDatabaseDataModel, resultIndex: number, columnIndex: number, - sortMode: SortMode + order: Order ) { const columnName = this.getColumnName(model, resultIndex, columnIndex)!; - const sorting = model.source.getAction(resultIndex, ResultSetSortAction); + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - sorting.setSortMode(columnName, sortMode, true); + constraints.setOrder(columnName, order, true); await model.refresh(); } - private getSortMode(model: IDatabaseDataModel, resultIndex: number, columnIndex: number) { + private getOrder(model: IDatabaseDataModel, resultIndex: number, columnIndex: number) { const columnName = this.getColumnName(model, resultIndex, columnIndex)!; - const sorting = model.source.getAction(resultIndex, ResultSetSortAction); + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - return sorting.getSortMode(columnName); + return constraints.getOrder(columnName); } private getSortingConstraints(model: IDatabaseDataModel, resultIndex: number) { - const sorting = model.source.getAction(resultIndex, ResultSetSortAction); - return sorting.getSortingConstraints(); + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + return constraints.getOrderConstraints(); } - private async removeSortingConstraints(model: IDatabaseDataModel, resultIndex: number) { - const sorting = model.source.getAction(resultIndex, ResultSetSortAction); + private async removeSortingFromConstraints(model: IDatabaseDataModel, resultIndex: number) { + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - sorting.removeSortingConstraints(); + constraints.deleteOrders(); await model.refresh(); } register(): void { this.dataGridContextMenuService.add( - this.contextMenuService.getRootMenuToken(), + this.dataGridContextMenuService.getMenuToken(), { - id: this.getMenuSortingToken(), + id: this.getMenuOrderToken(), isPresent(context) { return context.contextType === DataGridContextMenuService.cellContext; }, @@ -77,7 +75,7 @@ export class DataGridContextMenuSortingService { } ); this.dataGridContextMenuService.add( - this.getMenuSortingToken(), + this.getMenuOrderToken(), { id: 'asc', isPresent(context) { @@ -85,18 +83,18 @@ export class DataGridContextMenuSortingService { }, isDisabled: context => context.data.model.isLoading(), onClick: async context => { - await this.changeSortMode(context.data.model, context.data.resultIndex, context.data.column, ESortMode.asc); + await this.changeOrder(context.data.model, context.data.resultIndex, context.data.column, EOrder.asc); }, type: 'radio', - isChecked: context => this.getSortMode( + isChecked: context => this.getOrder( context.data.model, context.data.resultIndex, - context.data.column) === ESortMode.asc, + context.data.column) === EOrder.asc, title: 'ASC', } ); this.dataGridContextMenuService.add( - this.getMenuSortingToken(), + this.getMenuOrderToken(), { id: 'desc', isPresent(context) { @@ -104,29 +102,29 @@ export class DataGridContextMenuSortingService { }, isDisabled: context => context.data.model.isLoading(), onClick: async context => { - await this.changeSortMode(context.data.model, context.data.resultIndex, context.data.column, ESortMode.desc); + await this.changeOrder(context.data.model, context.data.resultIndex, context.data.column, EOrder.desc); }, type: 'radio', - isChecked: context => this.getSortMode( + isChecked: context => this.getOrder( context.data.model, context.data.resultIndex, - context.data.column) === ESortMode.desc, + context.data.column) === EOrder.desc, title: 'DESC', } ); this.dataGridContextMenuService.add( - this.getMenuSortingToken(), + this.getMenuOrderToken(), { - id: 'disableSorting', + id: 'disableOrder', 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, null); + await this.changeOrder(context.data.model, context.data.resultIndex, context.data.column, null); }, type: 'radio', - isChecked: context => this.getSortMode( + isChecked: context => this.getOrder( context.data.model, context.data.resultIndex, context.data.column) === null, @@ -134,16 +132,19 @@ export class DataGridContextMenuSortingService { } ); this.dataGridContextMenuService.add( - this.getMenuSortingToken(), + this.getMenuOrderToken(), { - id: 'disableAllSorting', - isPresent: context => { + id: 'disableOrders', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isHidden: context => { const sortingConstraints = this.getSortingConstraints(context.data.model, context.data.resultIndex); - return context.contextType === DataGridContextMenuService.cellContext && sortingConstraints.length > 1; + return sortingConstraints.length === 0; }, isDisabled: context => context.data.model.isLoading(), onClick: async context => { - await this.removeSortingConstraints(context.data.model, context.data.resultIndex); + await this.removeSortingFromConstraints(context.data.model, context.data.resultIndex); }, title: 'data_grid_table_disable_all_sorting', } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuService.ts index ac174fcd04..4991f94225 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuService.ts @@ -8,6 +8,7 @@ import { injectable } from '@cloudbeaver/core-di'; import { ContextMenuService, IContextMenuItem, IMenuPanel } from '@cloudbeaver/core-dialogs'; +import { Executor, IExecutor } from '@cloudbeaver/core-executor'; import type { IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer'; export interface IDataGridCellMenuContext { @@ -19,12 +20,15 @@ export interface IDataGridCellMenuContext { @injectable() export class DataGridContextMenuService { + onRootMenuOpen: IExecutor; static cellContext = 'data-grid-cell-context-menu'; private static menuToken = 'dataGridCell'; constructor( private contextMenuService: ContextMenuService, - ) { } + ) { + this.onRootMenuOpen = new Executor(); + } getMenuToken(): string { return DataGridContextMenuService.menuToken; @@ -40,7 +44,16 @@ export class DataGridContextMenuService { menuId: this.getMenuToken(), contextType: DataGridContextMenuService.cellContext, data: { model, resultIndex, row, column }, - }); + }, this.getMenuToken()); + } + + openMenu( + model: IDatabaseDataModel, + resultIndex: number, + row: number, + column: number + ): void { + this.onRootMenuOpen.execute({ model, resultIndex, row, column }); } add(panelId: string, menuItem: IContextMenuItem): void { diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx index aafcabcd9b..9de68d6dff 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx @@ -53,7 +53,11 @@ export const CellMenu: React.FC = observer(function TreeNodeMenu({ return styled(cellMenuStyles)( e.stopPropagation()} onDoubleClick={e => e.stopPropagation()}> - + dataGridContextMenuService.openMenu(model, resultIndex, row, column)} + onVisibleSwitch={onStateSwitch} + > diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/SortButton.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/OrderButton.tsx similarity index 60% rename from webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/SortButton.tsx rename to webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/OrderButton.tsx index a4259cd96f..3d0c5e8da9 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/SortButton.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/OrderButton.tsx @@ -11,10 +11,10 @@ import styled, { css, use } from 'reshadow'; import { IconOrImage } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; -import { ESortMode, getNextSortMode, IDatabaseDataModel, IDatabaseDataResult, ResultSetSortAction } from '@cloudbeaver/plugin-data-viewer'; +import { EOrder, getNextOrder, IDatabaseDataModel, IDatabaseDataResult, ResultSetConstraintAction } from '@cloudbeaver/plugin-data-viewer'; const styles = css` - sort-button { + order-button { margin-left: 4px; display: flex; padding: 2px 4px; @@ -25,13 +25,13 @@ const styles = css` box-sizing: border-box; cursor: pointer; } - sort-button > IconOrImage { - width: 8px; + order-button > IconOrImage { + width: 12px; } - sort-button:hover > IconOrImage { - width: 9px; + order-button:hover > IconOrImage { + width: 13px; } - sort-button[|disabled] { + order-button[|disabled] { opacity: 0.7; cursor: default; } @@ -44,22 +44,22 @@ interface Props { className?: string; } -export const SortButton: React.FC = observer(function SortButtton({ +export const OrderButton: React.FC = observer(function OrderButtton({ model, resultIndex, columnName, className, }) { const translate = useTranslate(); - const sorting = model.source.getAction(resultIndex, ResultSetSortAction); - const currentSortMode = sorting.getSortMode(columnName); + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + const currentOrder = constraints.getOrder(columnName); const loading = model.isLoading(); - let icon = '/icons/sort_unknown.png'; - if (currentSortMode === ESortMode.asc) { - icon = '/icons/sort_increase.png'; - } else if (currentSortMode === ESortMode.desc) { - icon = '/icons/sort_decrease.png'; + let icon = 'sort-arrow-unknown'; + if (currentOrder === EOrder.asc) { + icon = 'sort-arrow-up'; + } else if (currentOrder === EOrder.desc) { + icon = 'sort-arrow-down'; } const handleSort = (e: React.MouseEvent) => { @@ -68,20 +68,20 @@ export const SortButton: React.FC = observer(function SortButtton({ return; } - const nextSortMode = getNextSortMode(currentSortMode); - sorting.setSortMode(columnName, nextSortMode, e.ctrlKey || e.metaKey); + const nextOrder = getNextOrder(currentOrder); + constraints.setOrder(columnName, nextOrder, e.ctrlKey || e.metaKey); model.refresh(); }; 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 c596da55cf..5b147232c7 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 @@ -18,7 +18,7 @@ import { ResultSetDataAction } from '@cloudbeaver/plugin-data-viewer'; import { DataGridContext } from '../DataGridContext'; import { DataGridSelectionContext } from '../DataGridSelection/DataGridSelectionContext'; import { TableDataContext } from '../TableDataContext'; -import { SortButton } from './SortButton'; +import { OrderButton } from './OrderButton'; const headerStyles = composes( css` @@ -101,7 +101,7 @@ export const TableColumnHeader: React.FC> = observer(fu {columnName} {sortable && column?.name && ( - + )} ); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts b/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts index 461e1028f8..279ad2cda0 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts @@ -11,7 +11,8 @@ import { ExceptionsCatcherService } from '@cloudbeaver/core-events'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; import { DataPresentationService } from '@cloudbeaver/plugin-data-viewer'; -import { DataGridContextMenuSortingService } from './DataGrid/DataGridContextMenu/DataGridContextMenuSortingService'; +import { DataGridContextMenuFilterService } from './DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService'; +import { DataGridContextMenuOrderService } from './DataGrid/DataGridContextMenu/DataGridContextMenuOrderService'; import { DataGridSettingsService } from './DataGridSettingsService'; import { SpreadsheetGrid } from './SpreadsheetGrid'; @@ -20,7 +21,8 @@ export class SpreadsheetBootstrap extends Bootstrap { constructor( private dataPresentationService: DataPresentationService, private dataGridSettingsService: DataGridSettingsService, - private dataGridContextMenuSortingService: DataGridContextMenuSortingService, + private dataGridContextMenuSortingService: DataGridContextMenuOrderService, + private dataGridContextMenuFilterService: DataGridContextMenuFilterService, exceptionsCatcherService: ExceptionsCatcherService ) { super(); @@ -37,6 +39,7 @@ export class SpreadsheetBootstrap extends Bootstrap { icon: '/icons/grid.png', }); this.dataGridContextMenuSortingService.register(); + this.dataGridContextMenuFilterService.register(); } load(): void | Promise { } 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 01d38ef3d3..a4dffa132d 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts @@ -1,7 +1,14 @@ export default [ ['data_grid_table_sorting', 'Sorting'], + ['data_grid_table_filter', 'Filters'], + ['data_grid_table_filter_cell_value', 'Cell value'], + ['data_grid_table_filter_custom_value', 'Custom'], + ['data_grid_table_filter_reset_all_filters', 'Reset all filters'], ['data_grid_table_disable_sorting', 'Disabled'], ['data_grid_table_disable_all_sorting', 'Disable all'], + ['data_grid_table_delete_filters_and_sorting', 'Reset filters / sorting'], ['data_grid_table_tooltip_column_header_sort', 'Sort by column'], + ['data_grid_table_context_menu_filter_dialog_title', 'Edit value'], + ['data_grid_table_context_menu_filter_clipboard_permission', 'Give access to clipboard'], ['data_grid_table_index_column_tooltip', 'Select whole table'], ]; 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 3fc543875e..f9e0d41ad3 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts @@ -1,7 +1,14 @@ export default [ ['data_grid_table_sorting', 'Сортировка'], + ['data_grid_table_filter', 'Фильтры'], + ['data_grid_table_filter_cell_value', 'Значение ячейки'], + ['data_grid_table_filter_custom_value', 'Произвольный'], + ['data_grid_table_filter_reset_all_filters', 'Сбросить все фильтры'], ['data_grid_table_disable_sorting', 'Отключена'], ['data_grid_table_disable_all_sorting', 'Отменить сортировку всех колонок'], + ['data_grid_table_delete_filters_and_sorting', 'Отменить фильтры / сортировки'], ['data_grid_table_tooltip_column_header_sort', 'Сортировка по колонке'], + ['data_grid_table_context_menu_filter_dialog_title', 'Редактировать значение'], + ['data_grid_table_context_menu_filter_clipboard_permission', 'Дать доступ к буферу обмена'], ['data_grid_table_index_column_tooltip', 'Выбрать всю таблицу'], ]; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/manifest.ts b/webapp/packages/plugin-data-spreadsheet-new/src/manifest.ts index 7d4d6a2f59..7f608fd7ed 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/manifest.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/manifest.ts @@ -8,8 +8,9 @@ import type { PluginManifest } from '@cloudbeaver/core-di'; +import { DataGridContextMenuFilterService } from './DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService'; +import { DataGridContextMenuOrderService } from './DataGrid/DataGridContextMenu/DataGridContextMenuOrderService'; import { DataGridContextMenuService } from './DataGrid/DataGridContextMenu/DataGridContextMenuService'; -import { DataGridContextMenuSortingService } from './DataGrid/DataGridContextMenu/DataGridContextMenuSortingService'; import { DataGridSettingsService } from './DataGridSettingsService'; import { LocaleService } from './LocaleService'; import { SpreadsheetBootstrap } from './SpreadsheetBootstrap'; @@ -21,6 +22,7 @@ export const manifest: PluginManifest = { DataGridSettingsService, LocaleService, DataGridContextMenuService, - DataGridContextMenuSortingService, + DataGridContextMenuOrderService, + DataGridContextMenuFilterService, ], }; diff --git a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts index 46ca3f080b..f258958b46 100644 --- a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts @@ -92,6 +92,7 @@ export class ContainerDataSource extends DatabaseDataSource { getCellValue: (cell: IResultSetElementKey) => any; getColumn: (columnIndex: number) => SqlResultColumn | undefined; + getColumnOperations: (columnIndex: number) => DataTypeLogicalOperation[]; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataConstraintAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataConstraintAction.ts new file mode 100644 index 0000000000..14377ba5ea --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataConstraintAction.ts @@ -0,0 +1,29 @@ +/* + * 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 { SqlDataFilterConstraint } from '@cloudbeaver/core-sdk'; + +import type { IDatabaseDataAction } from '../IDatabaseDataAction'; +import type { IDatabaseDataResult } from '../IDatabaseDataResult'; +import type { Order } from '../Order'; + +export interface IDatabaseDataConstraintAction + extends IDatabaseDataAction { + setFilter: (attribute: string, operator: string, value?: any) => void; + setOrder: (attribute: string, order: Order, multiple: boolean) => void; + deleteAll: () => void; + deleteFilter: (attribute: string) => void; + deleteFilters: () => void; + deleteOrder: (attribute: string) => void; + deleteOrders: () => void; + deleteDataFilters: () => void; + get: (attribute: string) => SqlDataFilterConstraint | undefined; + getOrder: (attribute: string) => Order; + getFilterConstraints: () => SqlDataFilterConstraint[]; + getOrderConstraints: () => SqlDataFilterConstraint[]; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts index 64f08de732..2973233645 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts @@ -13,5 +13,6 @@ export interface IDatabaseDataFormatAction { isReadOnly: (key: TKey) => boolean; get: (value: any) => any; + isNull: (value: any) => boolean; toString: (value: any) => string; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataSortAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataSortAction.ts deleted file mode 100644 index 2b5533cc47..0000000000 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataSortAction.ts +++ /dev/null @@ -1,17 +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 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/ResultSetConstraintAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction.ts new file mode 100644 index 0000000000..2ad0828a5f --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction.ts @@ -0,0 +1,269 @@ +/* + * 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 { DataTypeLogicalOperation, ResultDataFormat, SqlDataFilterConstraint } from '@cloudbeaver/core-sdk'; + +import { DatabaseDataAction } from '../../DatabaseDataAction'; +import type { IDatabaseDataOptions } from '../../IDatabaseDataOptions'; +import type { IDatabaseResultSet } from '../../IDatabaseResultSet'; +import { EOrder, Order } from '../../Order'; +import { databaseDataAction } from '../DatabaseDataActionDecorator'; +import type { IDatabaseDataConstraintAction } from '../IDatabaseDataConstraintAction'; +import type { IResultSetElementKey } from './IResultSetElementKey'; + +export const IS_NULL_ID = 'IS_NULL'; +export const IS_NOT_NULL_ID = 'IS_NOT_NULL'; + +@databaseDataAction() +export class ResultSetConstraintAction extends DatabaseDataAction + implements IDatabaseDataConstraintAction { + static dataFormat = ResultDataFormat.Resultset; + + private deleteConstraint(attribute: string) { + if (!this.source.options) { + return; + } + + this.source.options.constraints = this.source.options.constraints + .filter(constraint => constraint.attribute !== attribute); + } + + private reorderConstraints() { + if (!this.source.options) { + return; + } + + this.source.options.constraints = this.source.options.constraints + .map((constaint, idx) => ({ ...constaint, orderPosition: idx })); + } + + private deleteEmptyConstraint(attribute: string) { + const constraint = this.get(attribute); + + if (constraint && !isFilterConstraint(constraint) && !isOrderConstraint(constraint)) { + this.deleteConstraint(attribute); + this.reorderConstraints(); + } + } + + get(attribute: string): SqlDataFilterConstraint | undefined { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + return this.source.options.constraints.find(constaint => constaint.attribute === attribute); + } + + deleteAll(): void { + if (!this.source.options) { + return; + } + + this.source.options.constraints = []; + } + + deleteFilter(attribute: string): void { + const constraint = this.get(attribute); + if (constraint) { + deleteLogicalOperationFromConstraint(constraint); + this.deleteEmptyConstraint(attribute); + } + } + + deleteFilters(): void { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + const newConstraints: SqlDataFilterConstraint[] = []; + + for (const constraint of this.source.options.constraints) { + deleteLogicalOperationFromConstraint(constraint); + if (isOrderConstraint(constraint)) { + newConstraints.push({ ...constraint, orderPosition: newConstraints.length }); + } + } + + this.source.options.constraints = newConstraints; + } + + deleteOrders(): void { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + const newConstraints: SqlDataFilterConstraint[] = []; + + for (const constraint of this.source.options.constraints) { + deleteOrderFromConstraint(constraint); + if (isFilterConstraint(constraint)) { + newConstraints.push({ ...constraint, orderPosition: newConstraints.length }); + } + } + + this.source.options.constraints = newConstraints; + } + + deleteOrder(attribute: string): void { + const constraint = this.get(attribute); + if (constraint) { + deleteOrderFromConstraint(constraint); + this.deleteEmptyConstraint(attribute); + } + } + + deleteDataFilters(): void { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + this.deleteFilters(); + this.source.options.whereFilter = ''; + } + + setFilter(attribute: string, operator: string, value?: any): void { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + const currentConstraint = this.get(attribute); + + if (currentConstraint) { + currentConstraint.operator = operator; + if (value !== undefined) { + currentConstraint.value = value; + } else if (currentConstraint.value !== undefined) { + delete currentConstraint.value; + } + return; + } + + const constraint: SqlDataFilterConstraint = { + attribute, + operator, + orderPosition: this.source.options.constraints.length, + }; + + if (value !== undefined) { + constraint.value = value; + } + + this.source.options.constraints.push(constraint); + } + + getFilterConstraints(): SqlDataFilterConstraint[] { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + return this.source.options.constraints.filter(isFilterConstraint); + } + + getOrderConstraints(): SqlDataFilterConstraint[] { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + return this.source.options.constraints.filter(isOrderConstraint); + } + + setOrder(attribute: string, order: Order, multiple: boolean): void { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + const resetOrder = order === null; + + if (!multiple) { + this.deleteOrders(); + } + + const currentConstraint = this.get(attribute); + + if (!currentConstraint) { + if (!resetOrder) { + this.source.options.constraints.push({ + attribute, + orderPosition: this.source.options.constraints.length, + orderAsc: order === EOrder.asc, + }); + } + return; + } + + if (!resetOrder) { + currentConstraint.orderAsc = order === EOrder.asc; + } else { + if (isFilterConstraint(currentConstraint)) { + deleteOrderFromConstraint(currentConstraint); + } else { + this.deleteConstraint(currentConstraint.attribute); + this.reorderConstraints(); + } + } + } + + getOrder(attribute: string): Order { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + const currentConstraint = this.get(attribute); + + if (!currentConstraint || !isOrderConstraint(currentConstraint)) { + return null; + } + + return currentConstraint.orderAsc ? EOrder.asc : EOrder.desc; + } +} + +export function nullOperationsFilter(operation: DataTypeLogicalOperation): boolean { + return operation.id === IS_NULL_ID || operation.id === IS_NOT_NULL_ID; +} + +export function getNextOrder(order: Order): Order { + switch (order) { + case EOrder.asc: + return EOrder.desc; + case EOrder.desc: + return null; + default: + return EOrder.asc; + } +} + +export function wrapOperationArgument(operationId: string, argument: any): string { + let result = argument; + if (operationId === 'LIKE') { + result = `%${result} %`; + } + + result = `'${result}'`; + + return result; +} + +export function isFilterConstraint(constraint: SqlDataFilterConstraint): boolean { + return constraint.operator !== undefined; +} + +function isOrderConstraint(constraint: SqlDataFilterConstraint) { + return constraint.orderAsc !== undefined; +} + +function deleteOrderFromConstraint(constraint: SqlDataFilterConstraint) { + delete constraint.orderAsc; + return constraint; +} + +function deleteLogicalOperationFromConstraint(constraint: SqlDataFilterConstraint) { + delete constraint.operator; + delete constraint.value; + return constraint; +} 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 927ab760bb..22c57d2651 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,3 +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 { ResultDataFormat } from '@cloudbeaver/core-sdk'; import { DatabaseDataAction } from '../../DatabaseDataAction'; @@ -16,7 +24,7 @@ export class ResultSetDataAction extends DatabaseDataAction operation.argumentCount === 1 || operation.argumentCount === 0); + } } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.ts index 9a63db0589..bcfb4a50db 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.ts @@ -39,12 +39,16 @@ export class ResultSetFormatAction 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 })); - } - - getSortingConstraints() { - if (!this.source.options) { - throw new Error('Options must be provided'); - } - - return this.source.options.constraints - .filter(constraint => constraint.orderAsc !== undefined); - } - - removeSortingConstraints() { - if (!this.source.options) { - throw new Error('Options must be provided'); - } - - this.source.options.constraints = this.source.options.constraints - .filter(constaint => constaint.orderAsc === undefined); - } - - setSortMode(columnName: string, sortMode: SortMode, multiple: boolean): void { - if (!this.source.options) { - throw new Error('Options must be provided'); - } - - 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 currentConstraint = this.source.options.constraints.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/DatabaseDataModel/DatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts index 7677a780d6..040338c87a 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts @@ -19,7 +19,7 @@ import { DatabaseDataAccessMode, IDatabaseDataSource, IRequestInfo } from './IDa import type { IDatabaseExecutionContext } from './IDatabaseExecutionContext'; export abstract class DatabaseDataSource -implements IDatabaseDataSource { + implements IDatabaseDataSource { access: DatabaseDataAccessMode; dataFormat: ResultDataFormat; supportedDataFormats: ResultDataFormat[]; @@ -73,6 +73,7 @@ implements IDatabaseDataSource { this.requestInfo = { requestDuration: 0, requestMessage: '', + requestFilter: '', source: null, }; this.error = null; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts index b037787350..c93d9acd2c 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts @@ -17,6 +17,7 @@ import type { IDatabaseExecutionContext } from './IDatabaseExecutionContext'; export interface IRequestInfo { readonly requestDuration: number; readonly requestMessage: string; + readonly requestFilter: string; readonly source: string | null; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Order.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Order.ts new file mode 100644 index 0000000000..72f552e11d --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Order.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. + */ + +export enum EOrder { + 'asc' = 'asc', + 'desc' = 'desc' +} + +export type Order = EOrder | null; 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 93b331934f..b74e36d6f8 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx @@ -7,7 +7,7 @@ */ import { observer } from 'mobx-react-lite'; -import { useCallback, useState } from 'react'; +import { useCallback } from 'react'; import styled, { css } from 'reshadow'; import { InlineEditor } from '@cloudbeaver/core-app'; @@ -15,6 +15,7 @@ import { PlaceholderComponent, useObjectRef } 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'; const styles = composes( @@ -36,29 +37,44 @@ export const TableWhereFilter: PlaceholderComponent model.source.options?.whereFilter || ''); + const hasResult = model.source.hasResult(resultIndex); + let filterValue = model.source.options?.whereFilter || ''; + + if (hasResult) { + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + if (constraints.getFilterConstraints().length > 0 && model.source.requestInfo.requestFilter) { + filterValue = model.source.requestInfo.requestFilter; + } + } + + const setValue = useCallback((filterValue: string) => { + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + + model.source.options.whereFilter = filterValue; + constraints?.deleteFilters(); + }, [model.source.options]); + const props = useObjectRef({ model, resultIndex, filterValue }); const handleApply = useCallback(() => { - const { model, resultIndex, filterValue } = props; + const { model, resultIndex } = props; if (model.isLoading() || model.isDisabled(resultIndex)) { return; } - model.source.options!.whereFilter = filterValue; model.refresh(); }, []); const resetFilter = useCallback(() => { - const { model, resultIndex, filterValue } = props; + const { model, resultIndex } = props; + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); if (model.isLoading() || model.isDisabled(resultIndex)) { return; } - const applyNeeded = model.source.options?.whereFilter === filterValue; - setValue(''); + constraints.deleteDataFilters(); + const applyNeeded = !!model.requestInfo.requestFilter; if (applyNeeded) { - model.source.options!.whereFilter = ''; model.refresh(); } }, []); diff --git a/webapp/packages/plugin-data-viewer/src/index.ts b/webapp/packages/plugin-data-viewer/src/index.ts index 4e71be961b..96bfac0c01 100644 --- a/webapp/packages/plugin-data-viewer/src/index.ts +++ b/webapp/packages/plugin-data-viewer/src/index.ts @@ -6,10 +6,11 @@ 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/ResultSet/ResultSetConstraintAction'; export * from './DatabaseDataModel/Actions/DatabaseDataActionDecorator'; export * from './DatabaseDataModel/Actions/IDatabaseDataFormatAction'; export * from './DatabaseDataModel/Actions/IDatabaseDataSelectAction'; +export * from './DatabaseDataModel/Actions/IDatabaseDataConstraintAction'; export * from './DatabaseDataModel/DatabaseDataAction'; export * from './DatabaseDataModel/DatabaseDataActions'; export * from './DatabaseDataModel/DatabaseDataEditor'; @@ -25,6 +26,7 @@ export * from './DatabaseDataModel/IDatabaseDataResult'; export * from './DatabaseDataModel/IDatabaseDataSource'; export * from './DatabaseDataModel/IDatabaseExecutionContext'; export * from './DatabaseDataModel/IDatabaseResultSet'; +export * from './DatabaseDataModel/Order'; // All Services and Components that is provided by this plugin should be exported here export * from './TableViewer/TableViewerStorageService'; diff --git a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts index f4867bc779..6583a56492 100644 --- a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts @@ -87,6 +87,7 @@ export class QueryDataSource extends DatabaseDataSource