Merge pull request #370 from dbeaver/feat/context-filter
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 546 B |
|
After Width: | Height: | Size: 238 B |
|
After Width: | Height: | Size: 246 B |
|
After Width: | Height: | Size: 859 B |
|
After Width: | Height: | Size: 229 B |
@@ -177,10 +177,13 @@
|
||||
</g>
|
||||
</symbol>
|
||||
|
||||
<symbol id="sort-arrow" viewBox="0 0 6 6">
|
||||
<g>
|
||||
<path d="M2.564 0L0 2.383 1.832 2.383 1.832 4.582 3.297 4.582 3.297 2.383 5.128 2.383z" transform="translate(-5 -.222) translate(5 .389)"/>
|
||||
</g>
|
||||
<symbol id="sort-arrow-up" viewBox="0 0 16 16">
|
||||
<path fill="#338ECC" fill-rule="evenodd" d="M1 7l7-6 7 6h-5v8H6V7H1z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="sort-arrow-down" viewBox="0 0 16 16">
|
||||
<path fill="#338ECC" fill-rule="evenodd" d="M15 9l-7 6-7-6h5V1h4v8h5z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="sort-arrow-unknown" viewBox="0 0 16 16">
|
||||
<path fill="#C4C4C4" fill-rule="evenodd" d="M11.5 2L8 6h2.333v8h2.334V6H15l-3.5-4zm-7 12L8 10H5.667V2H3.333v8H1l3.5 4z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -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,
|
||||
|
||||
@@ -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?'],
|
||||
|
||||
@@ -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', 'Сессия истекла. Хотите перезагрузить страницу?'],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,6 +15,7 @@ mutation updateResultsData(
|
||||
updateValues: $values
|
||||
) {
|
||||
duration
|
||||
filterText
|
||||
results {
|
||||
updateRowCount
|
||||
resultSet {
|
||||
|
||||
@@ -16,6 +16,7 @@ mutation updateResultsDataBatch(
|
||||
addedRows: $addedRows
|
||||
) {
|
||||
duration
|
||||
filterText
|
||||
results {
|
||||
updateRowCount
|
||||
resultSet {
|
||||
|
||||
@@ -65,6 +65,7 @@ export interface Query {
|
||||
sqlCompletionProposals?: Maybe<Array<Maybe<SqlCompletionProposal>>>;
|
||||
sqlDialectInfo?: Maybe<SqlDialectInfo>;
|
||||
sqlListContexts?: Maybe<Array<Maybe<SqlContextInfo>>>;
|
||||
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<Scalars['Int']>;
|
||||
readOnly: Scalars['Boolean'];
|
||||
readOnlyStatus?: Maybe<Scalars['String']>;
|
||||
supportedOperations: DataTypeLogicalOperation[];
|
||||
}
|
||||
|
||||
export interface DatabaseDocument {
|
||||
@@ -836,7 +845,8 @@ export interface SqlQueryResults {
|
||||
|
||||
export interface SqlExecuteInfo {
|
||||
statusMessage?: Maybe<Scalars['String']>;
|
||||
duration?: Maybe<Scalars['Int']>;
|
||||
duration: Scalars['Int'];
|
||||
filterText?: Maybe<Scalars['String']>;
|
||||
results: SqlQueryResults[];
|
||||
}
|
||||
|
||||
@@ -845,6 +855,12 @@ export interface SqlResultRow {
|
||||
updateValues?: Maybe<Scalars['Object']>;
|
||||
}
|
||||
|
||||
export interface DataTypeLogicalOperation {
|
||||
id: Scalars['ID'];
|
||||
expression: Scalars['String'];
|
||||
argumentCount?: Maybe<Scalars['Int']>;
|
||||
}
|
||||
|
||||
export enum AdminSubjectType {
|
||||
User = 'user',
|
||||
Role = 'role'
|
||||
@@ -1423,12 +1439,15 @@ export type GetSqlExecuteTaskResultsMutationVariables = Exact<{
|
||||
|
||||
export interface GetSqlExecuteTaskResultsMutation {
|
||||
result: (
|
||||
Pick<SqlExecuteInfo, 'duration' | 'statusMessage'>
|
||||
Pick<SqlExecuteInfo, 'duration' | 'statusMessage' | 'filterText'>
|
||||
& { results: Array<(
|
||||
Pick<SqlQueryResults, 'title' | 'updateRowCount' | 'sourceQuery' | 'dataFormat'>
|
||||
& { resultSet?: Maybe<(
|
||||
Pick<SqlResultSet, 'id' | 'rows' | 'hasMoreData'>
|
||||
& { columns?: Maybe<Array<Maybe<Pick<SqlResultColumn, 'dataKind' | 'entityName' | 'fullTypeName' | 'icon' | 'label' | 'maxLength' | 'name' | 'position' | 'precision' | 'readOnly' | 'scale' | 'typeName'>>>> }
|
||||
& { columns?: Maybe<Array<Maybe<(
|
||||
Pick<SqlResultColumn, 'dataKind' | 'entityName' | 'fullTypeName' | 'icon' | 'label' | 'maxLength' | 'name' | 'position' | 'precision' | 'readOnly' | 'scale' | 'typeName'>
|
||||
& { supportedOperations: Array<Pick<DataTypeLogicalOperation, 'id' | 'expression' | 'argumentCount'>> }
|
||||
)>>>; }
|
||||
)>; }
|
||||
)>; }
|
||||
);
|
||||
@@ -1444,7 +1463,7 @@ export type UpdateResultsDataMutationVariables = Exact<{
|
||||
|
||||
export interface UpdateResultsDataMutation {
|
||||
result?: Maybe<(
|
||||
Pick<SqlExecuteInfo, 'duration'>
|
||||
Pick<SqlExecuteInfo, 'duration' | 'filterText'>
|
||||
& { results: Array<(
|
||||
Pick<SqlQueryResults, 'updateRowCount'>
|
||||
& { resultSet?: Maybe<Pick<SqlResultSet, 'id' | 'rows'>> }
|
||||
@@ -1463,7 +1482,7 @@ export type UpdateResultsDataBatchMutationVariables = Exact<{
|
||||
|
||||
export interface UpdateResultsDataBatchMutation {
|
||||
result?: Maybe<(
|
||||
Pick<SqlExecuteInfo, 'duration'>
|
||||
Pick<SqlExecuteInfo, 'duration' | 'filterText'>
|
||||
& { results: Array<(
|
||||
Pick<SqlQueryResults, 'updateRowCount'>
|
||||
& { resultSet?: Maybe<Pick<SqlResultSet, 'id' | 'rows'>> }
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
await this.clipboardService.tryActivateClipboard();
|
||||
}
|
||||
}
|
||||
@@ -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<ClipboardService, 'permissionUpdate'>(this, {
|
||||
clipboardValue: observable,
|
||||
permissionUpdate: observable,
|
||||
});
|
||||
this.permissionUpdate = 1;
|
||||
this.readPermission = null;
|
||||
this.clipboardValue = null;
|
||||
this.clipboardAvailable = false;
|
||||
}
|
||||
|
||||
async read(): Promise<string | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<IDatabaseDataOptions, IDatabaseResultSet>,
|
||||
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<IDataGridCellMenuContext>,
|
||||
value: any | (() => any),
|
||||
icon: string,
|
||||
): Array<IContextMenuItem<IDataGridCellMenuContext>> {
|
||||
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<IDataGridCellMenuContext>({
|
||||
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<IDataGridCellMenuContext>({
|
||||
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<IDataGridCellMenuContext>({
|
||||
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();
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<IPayload, string | number> = observer(
|
||||
function FilterCustomValueDialog({
|
||||
payload,
|
||||
resolveDialog,
|
||||
rejectDialog,
|
||||
}: DialogComponentProps<IPayload, string | number>) {
|
||||
const clipboardService = useService(ClipboardService);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [value, setValue] = useState<string | number>(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)(
|
||||
<CommonDialogWrapper
|
||||
title={translate('data_grid_table_context_menu_filter_dialog_title')}
|
||||
footer={(
|
||||
<controls as="div">
|
||||
<Button type="button" mod={['unelevated']} onClick={handleApply}>
|
||||
{translate('ui_processing_ok')}
|
||||
</Button>
|
||||
<Button type="button" mod={['outlined']} onClick={rejectDialog}>
|
||||
{translate('ui_processing_cancel')}
|
||||
</Button>
|
||||
{clipboardService.clipboardAvailable && clipboardService.state !== 'denied' && (
|
||||
<Button type="button" mod={['outlined']} onClick={getValueFromClipboard}>
|
||||
{translate('ui_clipboard')}
|
||||
</Button>
|
||||
)}
|
||||
</controls>
|
||||
)}
|
||||
onReject={rejectDialog}
|
||||
>
|
||||
<InputFieldNew
|
||||
ref={inputRef}
|
||||
name='customValue'
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
>
|
||||
{payload.inputTitle}
|
||||
</InputFieldNew>
|
||||
</CommonDialogWrapper>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -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<any>, resultIndex: number, columnIndex: number) {
|
||||
@@ -30,43 +28,43 @@ export class DataGridContextMenuSortingService {
|
||||
return data.getColumn(columnIndex)?.name;
|
||||
}
|
||||
|
||||
private async changeSortMode(
|
||||
private async changeOrder(
|
||||
model: IDatabaseDataModel<any>,
|
||||
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<any>, resultIndex: number, columnIndex: number) {
|
||||
private getOrder(model: IDatabaseDataModel<any>, 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<any>, 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<any>, resultIndex: number) {
|
||||
const sorting = model.source.getAction(resultIndex, ResultSetSortAction);
|
||||
private async removeSortingFromConstraints(model: IDatabaseDataModel<any>, 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',
|
||||
}
|
||||
@@ -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<IDataGridCellMenuContext>;
|
||||
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<any>,
|
||||
resultIndex: number,
|
||||
row: number,
|
||||
column: number
|
||||
): void {
|
||||
this.onRootMenuOpen.execute({ model, resultIndex, row, column });
|
||||
}
|
||||
|
||||
add(panelId: string, menuItem: IContextMenuItem<IDataGridCellMenuContext>): void {
|
||||
|
||||
@@ -53,7 +53,11 @@ export const CellMenu: React.FC<Props> = observer(function TreeNodeMenu({
|
||||
|
||||
return styled(cellMenuStyles)(
|
||||
<cell-menu as='div' onClick={e => e.stopPropagation()} onDoubleClick={e => e.stopPropagation()}>
|
||||
<MenuTrigger panel={panel} onVisibleSwitch={onStateSwitch}>
|
||||
<MenuTrigger
|
||||
panel={panel}
|
||||
onClick={() => dataGridContextMenuService.openMenu(model, resultIndex, row, column)}
|
||||
onVisibleSwitch={onStateSwitch}
|
||||
>
|
||||
<Icon name="snack" viewBox="0 0 16 10" />
|
||||
</MenuTrigger>
|
||||
</cell-menu>
|
||||
|
||||
@@ -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<Props> = observer(function SortButtton({
|
||||
export const OrderButton: React.FC<Props> = 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<HTMLDivElement>) => {
|
||||
@@ -68,20 +68,20 @@ export const SortButton: React.FC<Props> = 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)(
|
||||
<sort-button
|
||||
<order-button
|
||||
as='div'
|
||||
title={translate('data_grid_table_tooltip_column_header_sort')}
|
||||
className={className}
|
||||
onClick={handleSort}
|
||||
{...use({ disabled: loading })}
|
||||
>
|
||||
<IconOrImage icon={icon} />
|
||||
</sort-button>
|
||||
<IconOrImage icon={icon} viewBox='0 0 16 16' />
|
||||
</order-button>
|
||||
);
|
||||
});
|
||||
@@ -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<HeaderRendererProps<any>> = observer(fu
|
||||
<name>{columnName}</name>
|
||||
</shrink-container>
|
||||
{sortable && column?.name && (
|
||||
<SortButton model={model} resultIndex={resultIndex} columnName={column.name} />
|
||||
<OrderButton model={model} resultIndex={resultIndex} columnName={column.name} />
|
||||
)}
|
||||
</table-header>
|
||||
);
|
||||
|
||||
@@ -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<void> { }
|
||||
|
||||
@@ -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'],
|
||||
];
|
||||
|
||||
@@ -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', 'Выбрать всю таблицу'],
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -92,6 +92,7 @@ export class ContainerDataSource extends DatabaseDataSource<IDataContainerOption
|
||||
this.requestInfo = {
|
||||
requestDuration: response?.duration || 0,
|
||||
requestMessage: response?.statusMessage || '',
|
||||
requestFilter: response.filterText || '',
|
||||
source: null,
|
||||
};
|
||||
|
||||
@@ -145,6 +146,7 @@ export class ContainerDataSource extends DatabaseDataSource<IDataContainerOption
|
||||
this.requestInfo = {
|
||||
requestDuration: response.result?.duration || 0,
|
||||
requestMessage: 'Saved successfully',
|
||||
requestFilter: response.result?.filterText || '',
|
||||
source: null,
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import type { SqlResultColumn } from '@cloudbeaver/core-sdk';
|
||||
import type { DataTypeLogicalOperation, SqlResultColumn } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import type { IDatabaseDataAction } from '../IDatabaseDataAction';
|
||||
import type { IDatabaseDataResult } from '../IDatabaseDataResult';
|
||||
@@ -16,4 +16,5 @@ export interface IDatabaseResultDataAction<TKey, TResult extends IDatabaseDataRe
|
||||
extends IDatabaseDataAction<any, TResult> {
|
||||
getCellValue: (cell: IResultSetElementKey) => any;
|
||||
getColumn: (columnIndex: number) => SqlResultColumn | undefined;
|
||||
getColumnOperations: (columnIndex: number) => DataTypeLogicalOperation[];
|
||||
}
|
||||
|
||||
@@ -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<TKey, TResult extends IDatabaseDataResult>
|
||||
extends IDatabaseDataAction<any, TResult> {
|
||||
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[];
|
||||
}
|
||||
@@ -13,5 +13,6 @@ export interface IDatabaseDataFormatAction<TKey, TResult extends IDatabaseDataRe
|
||||
extends IDatabaseDataAction<any, TResult> {
|
||||
isReadOnly: (key: TKey) => boolean;
|
||||
get: (value: any) => any;
|
||||
isNull: (value: any) => boolean;
|
||||
toString: (value: any) => string;
|
||||
}
|
||||
|
||||
@@ -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<TKey, TResult extends IDatabaseDataResult>
|
||||
extends IDatabaseDataAction<any, TResult> {
|
||||
setSortMode: (columnName: string, sortMode: SortMode, multiple: boolean) => void;
|
||||
getSortMode: (columnName: string) => SortMode;
|
||||
}
|
||||
@@ -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<IDatabaseDataOptions, IDatabaseResultSet>
|
||||
implements IDatabaseDataConstraintAction<IResultSetElementKey, IDatabaseResultSet> {
|
||||
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;
|
||||
}
|
||||
@@ -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<any, IDatabaseResult
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.result.data.rows[cell.row][cell.column];
|
||||
return this.result.data.rows?.[cell.row]?.[cell.column];
|
||||
}
|
||||
|
||||
getColumn(columnIndex: number) {
|
||||
@@ -26,4 +34,14 @@ export class ResultSetDataAction extends DatabaseDataAction<any, IDatabaseResult
|
||||
|
||||
return this.result.data.columns[columnIndex];
|
||||
}
|
||||
|
||||
getColumnOperations(columnIndex: number) {
|
||||
const column = this.getColumn(columnIndex);
|
||||
if (!column) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return column.supportedOperations
|
||||
.filter(operation => operation.argumentCount === 1 || operation.argumentCount === 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,12 +39,16 @@ export class ResultSetFormatAction extends DatabaseDataAction<any, IDatabaseResu
|
||||
return value !== null && typeof value === 'object';
|
||||
}
|
||||
|
||||
isNull(value: any): boolean {
|
||||
return this.get(value) === null;
|
||||
}
|
||||
|
||||
get(value: any): any {
|
||||
if (value !== null && typeof value === 'object') {
|
||||
if ('text' in value) {
|
||||
return value.text;
|
||||
} else if ('value' in value) {
|
||||
if (typeof value.value === 'object') {
|
||||
if (value.value !== null && typeof value.value === 'object') {
|
||||
return JSON.stringify(value.value);
|
||||
}
|
||||
return value.value;
|
||||
|
||||
@@ -1,129 +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 { ResultDataFormat } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import { DatabaseDataAction } from '../../DatabaseDataAction';
|
||||
import type { IDatabaseDataOptions } from '../../IDatabaseDataOptions';
|
||||
import type { IDatabaseResultSet } from '../../IDatabaseResultSet';
|
||||
import { databaseDataAction } from '../DatabaseDataActionDecorator';
|
||||
import type { IDatabaseDataSortAction } from '../IDatabaseDataSortAction';
|
||||
import type { IResultSetElementKey } from './IResultSetElementKey';
|
||||
|
||||
export enum ESortMode {
|
||||
'asc' = 'asc',
|
||||
'desc' = 'desc'
|
||||
}
|
||||
|
||||
export type SortMode = ESortMode | null;
|
||||
|
||||
@databaseDataAction()
|
||||
export class ResultSetSortAction extends DatabaseDataAction<IDatabaseDataOptions, IDatabaseResultSet>
|
||||
implements IDatabaseDataSortAction<IResultSetElementKey, IDatabaseResultSet> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import { DatabaseDataAccessMode, IDatabaseDataSource, IRequestInfo } from './IDa
|
||||
import type { IDatabaseExecutionContext } from './IDatabaseExecutionContext';
|
||||
|
||||
export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseDataResult>
|
||||
implements IDatabaseDataSource<TOptions, TResult> {
|
||||
implements IDatabaseDataSource<TOptions, TResult> {
|
||||
access: DatabaseDataAccessMode;
|
||||
dataFormat: ResultDataFormat;
|
||||
supportedDataFormats: ResultDataFormat[];
|
||||
@@ -73,6 +73,7 @@ implements IDatabaseDataSource<TOptions, TResult> {
|
||||
this.requestInfo = {
|
||||
requestDuration: 0,
|
||||
requestMessage: '',
|
||||
requestFilter: '',
|
||||
source: null,
|
||||
};
|
||||
this.error = null;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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<ITableHeaderPlaceholderProps
|
||||
resultIndex,
|
||||
}) {
|
||||
const translate = useTranslate();
|
||||
const [filterValue, setValue] = useState(() => 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();
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -87,6 +87,7 @@ export class QueryDataSource extends DatabaseDataSource<IDataQueryOptions, IData
|
||||
this.requestInfo = {
|
||||
requestDuration: response.result?.duration || 0,
|
||||
requestMessage: 'Saved successfully',
|
||||
requestFilter: response.result?.filterText || '',
|
||||
source: this.options.query || null,
|
||||
};
|
||||
|
||||
@@ -120,6 +121,7 @@ export class QueryDataSource extends DatabaseDataSource<IDataQueryOptions, IData
|
||||
this.requestInfo = {
|
||||
requestDuration: response.duration || 0,
|
||||
requestMessage: response.statusMessage || '',
|
||||
requestFilter: response.filterText || '',
|
||||
source: this.options?.query || null,
|
||||
};
|
||||
|
||||
|
||||