Merge remote-tracking branch 'origin/devel' into devel

This commit is contained in:
Serge Rider
2021-12-01 14:11:13 +03:00
15 changed files with 167 additions and 32 deletions
@@ -13,11 +13,12 @@ import { TLocalizationToken, Translate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
import { CommonDialogWrapper } from './CommonDialog/CommonDialogWrapper';
import type { DialogComponent } from './CommonDialogService';
import type { DialogComponent, DialogueStateResult } from './CommonDialogService';
const style = css`
footer {
align-items: center;
gap: 16px;
}
fill {
@@ -34,9 +35,11 @@ export interface ConfirmationDialogPayload {
message: TLocalizationToken;
confirmActionText?: TLocalizationToken;
cancelActionText?: TLocalizationToken;
extraActionText?: TLocalizationToken;
extraStatus?: string;
}
export const ConfirmationDialog: DialogComponent<ConfirmationDialogPayload> = function ConfirmationDialog({
export const ConfirmationDialog: DialogComponent<ConfirmationDialogPayload, DialogueStateResult | string> = function ConfirmationDialog({
payload,
resolveDialog,
rejectDialog,
@@ -64,6 +67,15 @@ export const ConfirmationDialog: DialogComponent<ConfirmationDialogPayload> = fu
<Translate token={cancelActionText || 'ui_processing_cancel'} />
</Button>
<fill />
{payload.extraStatus !== undefined && (
<Button
type="button"
mod={['outlined']}
onClick={() => resolveDialog(payload.extraStatus)}
>
<Translate token={cancelActionText || 'ui_no'} />
</Button>
)}
<Button
type="button"
mod={['unelevated']}
@@ -6,11 +6,11 @@
* you may not use this file except in compliance with the License.
*/
import type { DialogComponent } from '../CommonDialog/CommonDialogService';
import type { DialogComponent, DialogueStateResult } from '../CommonDialog/CommonDialogService';
import { ConfirmationDialog } from '../CommonDialog/ConfirmationDialog';
import type { ConfirmationDialogPayload } from '../CommonDialog/ConfirmationDialog';
export const ConfirmationDialogDelete: DialogComponent<ConfirmationDialogPayload> = function ConfirmationDialogDelete({
export const ConfirmationDialogDelete: DialogComponent<ConfirmationDialogPayload, DialogueStateResult | string> = function ConfirmationDialogDelete({
payload,
...rest
}) {
@@ -60,7 +60,9 @@ export class DataGridContextMenuFilterService {
const data = model.source.getAction(resultIndex, ResultSetDataAction);
const columnLabel = data.getColumn(column)?.label || '';
constraints.setFilter(columnLabel, operator, filterValue);
await model.requestDataAction(() =>
constraints.setFilter(columnLabel, operator, filterValue)
);
await model.refresh();
}
@@ -156,7 +158,9 @@ export class DataGridContextMenuFilterService {
const { model, resultIndex } = context.data;
const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction);
constraints.deleteData();
await model.requestDataAction(() =>
constraints.deleteData()
);
await model.refresh();
},
}
@@ -397,7 +401,9 @@ export class DataGridContextMenuFilterService {
const data = model.source.getAction(resultIndex, ResultSetDataAction);
const columnLabel = data.getColumn(key.column)?.label || '';
constraints.deleteFilter(columnLabel);
await model.requestDataAction(() =>
constraints.deleteFilter(columnLabel)
);
await model.refresh();
},
}
@@ -422,7 +428,9 @@ export class DataGridContextMenuFilterService {
const { model, resultIndex } = context.data;
const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction);
constraints.deleteDataFilters();
await model.requestDataAction(() =>
constraints.deleteDataFilters()
);
await model.refresh();
},
}
@@ -33,7 +33,9 @@ export class DataGridContextMenuOrderService {
const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction);
const columnLabel = data.getColumn(column)?.label || '';
constraints.setOrder(columnLabel, order, true);
await model.requestDataAction(() =>
constraints.setOrder(columnLabel, order, true)
);
await model.refresh();
}
@@ -139,7 +141,9 @@ export class DataGridContextMenuOrderService {
isDisabled: context => context.data.model.isLoading(),
onClick: async context => {
const constraints = context.data.model.source.getAction(context.data.resultIndex, ResultSetConstraintAction);
constraints.deleteOrders();
await context.data.model.requestDataAction(() =>
constraints.deleteOrders()
);
await context.data.model.refresh();
},
}
@@ -62,14 +62,16 @@ export const OrderButton = observer<Props>(function OrderButton({
icon = 'order-arrow-down';
}
const handleSort = (e: React.MouseEvent<HTMLDivElement>) => {
const handleSort = async (e: React.MouseEvent<HTMLDivElement>) => {
if (loading) {
return;
}
const nextOrder = getNextOrder(currentOrder);
constraints.setOrder(attribute, nextOrder, e.ctrlKey || e.metaKey);
model.refresh();
await model.requestDataAction(async () => {
constraints.setOrder(attribute, nextOrder, e.ctrlKey || e.metaKey);
});
await model.refresh();
};
return styled(styles)(
@@ -8,10 +8,11 @@
import { observable, makeObservable } from 'mobx';
import { Executor, ExecutorInterrupter, IExecutor } from '@cloudbeaver/core-executor';
import type { ResultDataFormat } from '@cloudbeaver/core-sdk';
import { uuid } from '@cloudbeaver/core-utils';
import type { IDatabaseDataModel } from './IDatabaseDataModel';
import type { IDatabaseDataModel, IRequestEventData } from './IDatabaseDataModel';
import type { IDatabaseDataResult } from './IDatabaseDataResult';
import type { DatabaseDataAccessMode, IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource';
@@ -29,10 +30,18 @@ implements IDatabaseDataModel<TOptions, TResult> {
return this.source.supportedDataFormats;
}
readonly onOptionsChange: IExecutor;
readonly onRequest: IExecutor<IRequestEventData>;
private currentTask: Promise<void> | null;
constructor(source: IDatabaseDataSource<TOptions, TResult>) {
this.id = uuid();
this.source = source;
this.countGain = 0;
this.onOptionsChange = new Executor();
this.onRequest = new Executor();
this.currentTask = null;
makeObservable(this, {
countGain: observable,
@@ -94,8 +103,18 @@ implements IDatabaseDataModel<TOptions, TResult> {
return this;
}
async requestOptionsChange(): Promise<boolean> {
const contexts = await this.onOptionsChange.execute();
return ExecutorInterrupter.isInterrupted(contexts) === false;
}
async save(): Promise<void> {
await this.requestSaveAction(() => this.source.saveData());
}
async retry(): Promise<void> {
await this.source.retry();
await this.requestDataAction(() => this.source.retry());
}
async refresh(): Promise<void> {
@@ -103,21 +122,23 @@ implements IDatabaseDataModel<TOptions, TResult> {
}
async reload(): Promise<void> {
await this
await this.requestDataAction(() => this
.setSlice(0, this.countGain)
.requestData();
.requestData()
);
}
async requestDataPortion(offset: number, count: number): Promise<void> {
if (!this.isDataAvailable(offset, count)) {
await this.source
await this.requestDataAction(() => this.source
.setSlice(offset, count)
.requestData();
.requestData()
);
}
}
async requestData(): Promise<void> {
await this.source.requestData();
await this.requestDataAction(() => this.source.requestData());
}
cancel(): Promise<void> | void {
@@ -131,4 +152,32 @@ implements IDatabaseDataModel<TOptions, TResult> {
async dispose(): Promise<void> {
await this.source.dispose();
}
async requestSaveAction(action: () => Promise<void> | void): Promise<void> {
return action();
}
async requestDataAction(action: () => Promise<void> | void, concurrent?: boolean): Promise<void> {
if (this.currentTask && !concurrent) {
return this.currentTask;
}
try {
this.currentTask = this.requestDataActionTask(action);
return await this.currentTask;
} finally {
this.currentTask = null;
}
}
private async requestDataActionTask(action: () => Promise<void> | void): Promise<void> {
const contexts = await this.onRequest.execute({ type: 'before' });
if (ExecutorInterrupter.isInterrupted(contexts)) {
return;
}
await action();
await this.onRequest.execute({ type: 'after' });
}
}
@@ -30,10 +30,11 @@ implements IDatabaseDataSource<TOptions, TResult> {
requestInfo: IRequestInfo;
error: Error | null;
executionContext: IConnectionExecutionContext | null;
abstract get canCancel(): boolean;
protected disabled: boolean;
private activeRequest: Promise<TResult[]> | null;
private activeRequest: Promise<TResult[] | null> | null;
private activeSave: Promise<TResult[]> | null;
private activeTask: Promise<any> | null;
private lastAction: () => Promise<void>;
@@ -263,12 +264,13 @@ implements IDatabaseDataSource<TOptions, TResult> {
this.lastAction = this.requestData.bind(this);
try {
const promise = this.request(this.results);
this.activeRequest = this.requestDataAction();
if (promise instanceof Promise) {
this.activeRequest = promise;
const data = await this.activeRequest;
if (data !== null) {
this.setResults(data);
}
this.setResults(await promise);
} finally {
this.activeRequest = null;
}
@@ -314,4 +316,8 @@ implements IDatabaseDataSource<TOptions, TResult> {
abstract save(prevResults: TResult[]): Promise<TResult[]> | TResult[];
abstract dispose(): Promise<void>;
async requestDataAction(): Promise<TResult[] | null> {
return this.request(this.results);
}
}
@@ -6,11 +6,16 @@
* you may not use this file except in compliance with the License.
*/
import type { IExecutor } from '@cloudbeaver/core-executor';
import type { ResultDataFormat } from '@cloudbeaver/core-sdk';
import type { IDatabaseDataResult } from './IDatabaseDataResult';
import type { DatabaseDataAccessMode, IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource';
export interface IRequestEventData {
type: 'before' | 'after';
}
export interface IDatabaseDataModel<TOptions = any, TResult extends IDatabaseDataResult = IDatabaseDataResult> {
readonly id: string;
readonly source: IDatabaseDataSource<TOptions, TResult>;
@@ -18,6 +23,9 @@ export interface IDatabaseDataModel<TOptions = any, TResult extends IDatabaseDat
readonly supportedDataFormats: ResultDataFormat[];
readonly countGain: number;
readonly onOptionsChange: IExecutor;
readonly onRequest: IExecutor<IRequestEventData>;
isReadonly: () => boolean;
isDisabled: (resultIndex: number) => boolean;
isLoading: () => boolean;
@@ -32,7 +40,10 @@ export interface IDatabaseDataModel<TOptions = any, TResult extends IDatabaseDat
setDataFormat: (dataFormat: ResultDataFormat) => this;
setSupportedDataFormats: (dataFormats: ResultDataFormat[]) => this;
requestOptionsChange: () => Promise<boolean>;
requestDataAction: (action: () => Promise<void> | void) => Promise<void>;
retry: () => Promise<void>;
save: (concurrent?: boolean) => Promise<void>;
refresh: () => Promise<void>;
reload: () => Promise<void>;
requestDataPortion: (offset: number, count: number) => Promise<void>;
@@ -84,8 +84,8 @@ export const ScriptPreviewDialog = observer<DialogComponentProps<Payload>>(funct
});
}, [sqlDialectInfoService, connectionId]);
const apply = () => {
payload.model.source.saveData();
const apply = async () => {
await payload.model.save();
rejectDialog();
};
@@ -225,7 +225,7 @@ export class TableFooterMenuService {
return !editor?.isEdited();
},
onClick: context => context.data.model.source.saveData(),
onClick: context => context.data.model.save(),
});
this.registerMenuItem({
@@ -9,9 +9,13 @@
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { TextPlaceholder } from '@cloudbeaver/core-blocks';
import { TextPlaceholder, useExecutor } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { ExecutorInterrupter } from '@cloudbeaver/core-executor';
import type { ResultDataFormat } from '@cloudbeaver/core-sdk';
import { DatabaseEditAction } from '../DatabaseDataModel/Actions/DatabaseEditAction';
import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel';
import type { IDataPresentationOptions } from '../DataPresentationService';
import type { IDataTableActions } from './IDataTableActions';
@@ -39,6 +43,37 @@ export const TableGrid = observer<Props>(function TableGrid({
presentation,
resultIndex,
}) {
const commonDialogService = useService(CommonDialogService);
useExecutor({
executor: model.onRequest,
handlers: [async function checkUnsavedData(data, contexts) {
if (data.type === 'before') {
const editor = model.source.getActionImplementation(
resultIndex,
DatabaseEditAction
);
if (editor?.isEdited()) {
const result = await commonDialogService.open(ConfirmationDialog, {
title: 'data_viewer_result_edited_title',
message: 'data_viewer_result_edited_message',
confirmActionText: 'ui_yes',
extraStatus: 'no',
});
if (result === DialogueStateResult.Rejected) {
ExecutorInterrupter.interrupt(contexts);
} else if (result === DialogueStateResult.Resolved) {
await model.save();
} else {
editor.clear();
}
}
}
}],
});
if (
(presentation.dataFormat !== undefined && dataFormat !== presentation.dataFormat)
|| !model.source.hasResult(resultIndex)
@@ -66,18 +66,20 @@ export const TableWhereFilter: PlaceholderComponent<ITableHeaderPlaceholderProps
model.refresh();
}, []);
const resetFilter = useCallback(() => {
const resetFilter = useCallback(async () => {
const { model, resultIndex } = props;
const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction);
if (model.isLoading() || model.isDisabled(resultIndex)) {
return;
}
constraints.deleteDataFilters();
await model.requestDataAction(() =>
constraints.deleteDataFilters()
);
const applyNeeded = !!model.requestInfo.requestFilter;
if (applyNeeded) {
model.refresh();
await model.refresh();
}
}, []);
@@ -12,6 +12,8 @@ export default [
['data_viewer_action_edit_delete', 'Delete selected'],
['data_viewer_action_edit_add', 'Add'],
['data_viewer_action_edit_revert', 'Revert selected'],
['data_viewer_result_edited_title', 'Save result set edit'],
['data_viewer_result_edited_message', 'Result set was edited. Do you want to save changes in database?'],
['data_viewer_presentation_value_title', 'Value'],
['data_viewer_presentation_value_text_title', 'Text'],
['data_viewer_presentation_value_text_plain_title', 'Text'],
@@ -9,6 +9,8 @@ export default [
['data_viewer_statistics_status', 'Status:'],
['data_viewer_statistics_duration', 'Durata:'],
['data_viewer_statistics_updated_rows', 'Righe Aggiornate:'],
['data_viewer_result_edited_title', 'Save result set edit'],
['data_viewer_result_edited_message', 'Result set was edited. Do you want to save changes in database?'],
['data_viewer_presentation_value_title', 'Valore'],
['data_viewer_presentation_value_text_title', 'Testo'],
['data_viewer_presentation_value_text_plain_title', 'Testo'],
@@ -12,6 +12,8 @@ export default [
['data_viewer_action_edit_delete', 'Удалить'],
['data_viewer_action_edit_add', 'Добавить'],
['data_viewer_action_edit_revert', 'Отменить'],
['data_viewer_result_edited_title', 'Сохранить изменения'],
['data_viewer_result_edited_message', 'Данные результата запроса были изменены. Вы хотите сохранить изменения?'],
['data_viewer_presentation_value_title', 'Значение'],
['data_viewer_presentation_value_text_title', 'Текст'],
['data_viewer_presentation_value_image_title', 'Изображение'],