mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge remote-tracking branch 'origin/devel' into devel
This commit is contained in:
@@ -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
|
||||
}) {
|
||||
|
||||
+12
-4
@@ -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();
|
||||
},
|
||||
}
|
||||
|
||||
+6
-2
@@ -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();
|
||||
},
|
||||
}
|
||||
|
||||
+5
-3
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -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', 'Изображение'],
|
||||
|
||||
Reference in New Issue
Block a user