diff --git a/webapp/packages/core-authentication/src/DATA_CONTEXT_USER.ts b/webapp/packages/core-authentication/src/DATA_CONTEXT_USER.ts index 03e321694b..56d86b0c8b 100644 --- a/webapp/packages/core-authentication/src/DATA_CONTEXT_USER.ts +++ b/webapp/packages/core-authentication/src/DATA_CONTEXT_USER.ts @@ -8,4 +8,4 @@ import { createDataContext } from '@cloudbeaver/core-data-context'; import type { UserInfo } from '@cloudbeaver/core-sdk'; -export const DATA_CONTEXT_USER = createDataContext('user-info', () => null); +export const DATA_CONTEXT_USER = createDataContext('user-info'); diff --git a/webapp/packages/core-data-context/src/DataContext/DataContext.ts b/webapp/packages/core-data-context/src/DataContext/DataContext.ts index eb21e7aef7..5817ac8822 100644 --- a/webapp/packages/core-data-context/src/DataContext/DataContext.ts +++ b/webapp/packages/core-data-context/src/DataContext/DataContext.ts @@ -7,27 +7,26 @@ */ import { action, makeObservable, observable } from 'mobx'; -import { MetadataMap } from '@cloudbeaver/core-utils'; - import type { DataContextGetter } from './DataContextGetter'; -import type { DeleteVersionedContextCallback, IDataContext } from './IDataContext'; +import type { IDataContext } from './IDataContext'; import type { IDataContextProvider } from './IDataContextProvider'; +const NOT_FOUND = Symbol('not found'); + export class DataContext implements IDataContext { - readonly map: Map, any>; - private readonly versions: MetadataMap, number>; - fallback?: IDataContextProvider; + private readonly store: Map, Map>; + private fallback?: IDataContextProvider; constructor(fallback?: IDataContextProvider) { - this.map = new Map(); - this.versions = new MetadataMap(() => 0); + this.store = new Map(); this.fallback = fallback; - makeObservable(this, { + makeObservable(this, { set: action, delete: action, clear: action, - map: observable.shallow, + deleteForId: action, + store: observable.shallow, fallback: observable.ref, }); } @@ -37,119 +36,108 @@ export class DataContext implements IDataContext { } hasOwn(context: DataContextGetter): boolean { - return this.map.has(context); + return this.store.has(context); } - has(context: DataContextGetter, nested = true): boolean { - if (this.hasOwn(context)) { - return true; - } - - if (nested && this.fallback?.has(context)) { - return true; - } + has(context: DataContextGetter): boolean { + return this.hasOwn(context) || this.fallback?.has(context) || false; + } - return false; + hasOwnValue(context: DataContextGetter, value: T): boolean { + return this.getOwn(context) === value; } - hasValue(context: DataContextGetter, value: T, nested = true): boolean { - // eslint-disable-next-line @typescript-eslint/no-this-alias - let provider: IDataContextProvider = this; + hasValue(context: DataContextGetter, value: T): boolean { + return this.hasOwnValue(context, value) || this.fallback?.hasOwnValue(context, value) || false; + } - while (true) { - if (provider.getOwn(context) === value) { - return true; - } + find(context: DataContextGetter, predicate: (value: T) => boolean): T | undefined { + const value = this.internalGet(context); - if (provider.fallback && nested) { - provider = provider.fallback; - } else { - return false; - } + if (value !== NOT_FOUND && predicate(value)) { + return value; } - } - find(context: DataContextGetter, predicate: (value: T) => boolean): T | undefined { - // eslint-disable-next-line @typescript-eslint/no-this-alias - let provider: IDataContextProvider = this; + if (this.fallback) { + return this.fallback.find(context, predicate); + } - while (true) { - if (provider.hasOwn(context)) { - const value = provider.getOwn(context)!; + return undefined; + } - if (predicate(value)) { - return value; - } - } + set(context: DataContextGetter, value: T, id: string): this { + let data = this.store.get(context); - if (provider.fallback) { - provider = provider.fallback; - } else { - return undefined; - } + if (!data) { + data = observable(new Map(), { deep: false }); + this.store.set(context, data); } + + data.set(id, value); + return this; } - set(context: DataContextGetter, value: T): DeleteVersionedContextCallback { - const data = this.getOwn(context); - let version = this.versions.get(context); + delete(context: DataContextGetter, id?: string): this { + if (id) { + const data = this.store.get(context); + data?.delete(id); - if (data === value) { - return this.delete.bind(this, context, version); + if (data?.size) { + return this; + } } + this.store.delete(context); - version++; - this.map.set(context, value); - this.versions.set(context, version); - - return this.delete.bind(this, context, version); + return this; } - delete(context: DataContextGetter, version?: number): this { - if (version !== this.versions.get(context)) { - return this; - } + deleteForId(id: string): this { + for (const [context, data] of this.store) { + data.delete(id); - this.map.delete(context); + if (data.size === 0) { + this.store.delete(context); + } + } return this; } getOwn(context: DataContextGetter): T | undefined { - return this.map.get(context); - } + const value = this.internalGet(context); - get(context: DataContextGetter): T { - if (!this.hasOwn(context)) { - const defaultValue = context(this); + if (value === NOT_FOUND) { + return undefined; + } - if (defaultValue !== undefined) { - this.set(context, defaultValue); - return defaultValue; - } + return value; + } - if (this.fallback) { - return this.fallback.get(context); - } + get(context: DataContextGetter): T | undefined { + const value = this.internalGet(context); - throw new Error("Context doesn't exists"); + if (value === NOT_FOUND && this.fallback) { + return this.fallback.get(context); } - return this.getOwn(context)!; - } - - tryGet(context: DataContextGetter): T | undefined { - if (!this.map.has(context)) { - if (this.fallback) { - return this.fallback.tryGet(context); - } + if (value === NOT_FOUND) { + return undefined; } - return this.getOwn(context); + return value; } clear(): void { - this.map.clear(); - this.versions.clear(); + this.store.clear(); + } + + private internalGet(context: DataContextGetter): T | typeof NOT_FOUND { + const data = this.store.get(context); + + if (data?.size) { + return [...data.values()][data.size - 1] as T; + } + + return NOT_FOUND; } } diff --git a/webapp/packages/core-data-context/src/DataContext/DataContextGetter.ts b/webapp/packages/core-data-context/src/DataContext/DataContextGetter.ts index c946bf9fce..c3268250cf 100644 --- a/webapp/packages/core-data-context/src/DataContext/DataContextGetter.ts +++ b/webapp/packages/core-data-context/src/DataContext/DataContextGetter.ts @@ -5,9 +5,11 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { IDataContextProvider } from './IDataContextProvider'; + +const typescriptTypeLink = Symbol('typescript type link'); export type DataContextGetter = { - (provider: IDataContextProvider): T; id: string; + name: string; + [typescriptTypeLink]: T; }; diff --git a/webapp/packages/core-data-context/src/DataContext/DynamicDataContext.ts b/webapp/packages/core-data-context/src/DataContext/DynamicDataContext.ts deleted file mode 100644 index 5ea6c06170..0000000000 --- a/webapp/packages/core-data-context/src/DataContext/DynamicDataContext.ts +++ /dev/null @@ -1,94 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { action, makeObservable, observable } from 'mobx'; - -import type { DataContextGetter } from './DataContextGetter'; -import type { DeleteVersionedContextCallback, IDataContext } from './IDataContext'; -import type { IDataContextProvider } from './IDataContextProvider'; - -export class DynamicDataContext implements IDataContext { - fallback: IDataContext; - contexts: Map, DeleteVersionedContextCallback>; - - get map() { - return this.fallback.map; - } - - constructor(fallback: IDataContext) { - this.fallback = fallback; - this.contexts = new Map(); - - makeObservable(this, { - fallback: observable.ref, - set: action, - clear: action, - }); - } - - setFallBack(fallback?: IDataContextProvider): void; - setFallBack(fallback: IDataContext): void { - if (this.fallback === fallback) { - return; - } - this.clear(); - this.fallback = fallback; - } - - set(context: DataContextGetter, value: T): DeleteVersionedContextCallback { - const dynamicContext = this.contexts.get(context); - - if (this.tryGet(context) === value) { - return dynamicContext ?? (() => {}); - } - - const deleteCallback = this.fallback.set(context, value); - this.contexts.set(context, deleteCallback); - return deleteCallback; - } - - delete(context: DataContextGetter, version?: number): this { - this.fallback.delete(context, version); - this.contexts.delete(context); - return this; - } - - hasOwn(context: DataContextGetter): boolean { - return this.fallback.hasOwn(context); - } - - has(context: DataContextGetter): boolean { - return this.fallback.has(context); - } - - get(context: DataContextGetter): T { - return this.fallback.get(context); - } - - getOwn(context: DataContextGetter): T | undefined { - return this.fallback.getOwn(context); - } - - hasValue(context: DataContextGetter, value: T): boolean { - return this.fallback.hasValue(context, value); - } - - find(context: DataContextGetter, predicate: (value: T) => boolean): T | undefined { - return this.fallback.find(context, predicate); - } - - tryGet(context: DataContextGetter): T | undefined { - return this.fallback.tryGet(context); - } - - clear(): void { - for (const deleteCallback of this.contexts.values()) { - deleteCallback(); - } - this.contexts.clear(); - } -} diff --git a/webapp/packages/core-data-context/src/DataContext/IDataContext.ts b/webapp/packages/core-data-context/src/DataContext/IDataContext.ts index a3d7173dc2..c69be02bbe 100644 --- a/webapp/packages/core-data-context/src/DataContext/IDataContext.ts +++ b/webapp/packages/core-data-context/src/DataContext/IDataContext.ts @@ -11,8 +11,9 @@ import type { IDataContextProvider } from './IDataContextProvider'; export type DeleteVersionedContextCallback = () => void; export interface IDataContext extends IDataContextProvider { - set: (context: DataContextGetter, value: T) => DeleteVersionedContextCallback; - delete: (context: DataContextGetter, version?: number) => this; + set: (context: DataContextGetter, value: T, id: string) => this; + delete: (context: DataContextGetter, id?: string) => this; + deleteForId: (id: string) => this; clear: () => void; setFallBack: (fallback?: IDataContextProvider) => void; } diff --git a/webapp/packages/core-data-context/src/DataContext/IDataContextProvider.ts b/webapp/packages/core-data-context/src/DataContext/IDataContextProvider.ts index 85e86c3579..391baa24b8 100644 --- a/webapp/packages/core-data-context/src/DataContext/IDataContextProvider.ts +++ b/webapp/packages/core-data-context/src/DataContext/IDataContextProvider.ts @@ -8,13 +8,11 @@ import type { DataContextGetter } from './DataContextGetter'; export interface IDataContextProvider { - fallback?: IDataContextProvider; - readonly map: Map, any>; - has: (context: DataContextGetter, nested?: boolean) => boolean; + has: (context: DataContextGetter) => boolean; hasOwn: (context: DataContextGetter) => boolean; - get: (context: DataContextGetter) => T; + get: (context: DataContextGetter) => T | undefined; getOwn: (context: DataContextGetter) => T | undefined; find: (context: DataContextGetter, predicate: (item: T) => boolean) => T | undefined; - hasValue: (context: DataContextGetter, value: T, nested?: boolean) => boolean; - tryGet: (context: DataContextGetter) => T | undefined; + hasOwnValue: (context: DataContextGetter, value: T) => boolean; + hasValue: (context: DataContextGetter, value: T) => boolean; } diff --git a/webapp/packages/core-data-context/src/DataContext/TempDataContext.ts b/webapp/packages/core-data-context/src/DataContext/TempDataContext.ts deleted file mode 100644 index 283c8cc907..0000000000 --- a/webapp/packages/core-data-context/src/DataContext/TempDataContext.ts +++ /dev/null @@ -1,187 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { action, makeObservable, observable } from 'mobx'; - -import { MetadataMap } from '@cloudbeaver/core-utils'; - -import { DataContext } from './DataContext'; -import type { DataContextGetter } from './DataContextGetter'; -import type { DeleteVersionedContextCallback, IDataContext } from './IDataContext'; -import type { IDataContextProvider } from './IDataContextProvider'; - -export class TempDataContext implements IDataContext { - readonly map: Map, any>; - private readonly versions: MetadataMap, number>; - target: IDataContext; - fallback?: IDataContextProvider; - private flushTimeout: any; - - constructor(fallback?: IDataContextProvider) { - this.map = new Map(); - this.versions = new MetadataMap(() => 0); - this.target = new DataContext(fallback); - this.fallback = fallback; - - makeObservable(this, { - target: observable.ref, - set: action, - delete: action, - clear: action, - flush: action, - }); - } - - setFallBack(fallback?: IDataContextProvider): void { - this.fallback = fallback; - this.planFlush(); - } - - hasOwn(context: DataContextGetter): boolean { - return this.map.has(context) || this.target.hasOwn(context); - } - - has(context: DataContextGetter, nested = true): boolean { - if (this.hasOwn(context)) { - return true; - } - - if (nested && this.fallback?.has(context)) { - return true; - } - - return false; - } - - hasValue(context: DataContextGetter, value: T, nested = true): boolean { - // eslint-disable-next-line @typescript-eslint/no-this-alias - let provider: IDataContextProvider = this; - - while (true) { - if (provider.getOwn(context) === value) { - return true; - } - - if (provider.fallback && nested) { - provider = provider.fallback; - } else { - return false; - } - } - } - - find(context: DataContextGetter, predicate: (value: T) => boolean): T | undefined { - // eslint-disable-next-line @typescript-eslint/no-this-alias - let provider: IDataContextProvider = this; - - while (true) { - if (provider.hasOwn(context)) { - const value = provider.getOwn(context)!; - - if (predicate(value)) { - return value; - } - } - - if (provider.fallback) { - provider = provider.fallback; - } else { - return undefined; - } - } - } - - getOwn(context: DataContextGetter): T | undefined { - if (this.map.has(context)) { - return this.map.get(context); - } - - return this.target.getOwn(context); - } - - set(context: DataContextGetter, value: T): DeleteVersionedContextCallback { - const data = this.getOwn(context); - let version = this.versions.get(context); - - if (data === value) { - return this.delete.bind(this, context, version); - } - - version++; - this.map.set(context, value); - this.versions.set(context, version); - this.planFlush(); - - return this.delete.bind(this, context, version); - } - - delete(context: DataContextGetter, version?: number): this { - if (version !== this.versions.get(context)) { - return this; - } - - this.map.delete(context); - this.planFlush(); - - return this; - } - - get(context: DataContextGetter): T { - if (!this.hasOwn(context)) { - const defaultValue = context(this); - - if (defaultValue !== undefined) { - this.set(context, defaultValue); - return defaultValue; - } - - if (this.fallback) { - return this.fallback.get(context); - } - - throw new Error("Context doesn't exists"); - } - - return this.getOwn(context)!; - } - - tryGet(context: DataContextGetter): T | undefined { - if (!this.hasOwn(context)) { - if (this.fallback) { - return this.fallback.tryGet(context); - } - } - - return this.getOwn(context); - } - - clear(): void { - this.map.clear(); - this.versions.clear(); - this.planFlush(); - } - - flush(): void { - clearTimeout(this.flushTimeout); - this.target.clear(); - this.target.setFallBack(this.fallback); - - for (const [key, value] of this.map) { - this.target.set(key, value); - } - } - - private planFlush(): void { - if (this.flushTimeout) { - clearTimeout(this.flushTimeout); - } - - this.flushTimeout = setTimeout(() => { - this.flush(); - }, 0); - } -} diff --git a/webapp/packages/core-data-context/src/DataContext/createDataContext.ts b/webapp/packages/core-data-context/src/DataContext/createDataContext.ts index 6f45ca86a6..c31ca85e13 100644 --- a/webapp/packages/core-data-context/src/DataContext/createDataContext.ts +++ b/webapp/packages/core-data-context/src/DataContext/createDataContext.ts @@ -8,18 +8,10 @@ import { uuid } from '@cloudbeaver/core-utils'; import type { DataContextGetter } from './DataContextGetter'; -import type { IDataContextProvider } from './IDataContextProvider'; -export function createDataContext( - name: string, - defaultValue?: (context: IDataContextProvider) => T extends any ? T : undefined, -): DataContextGetter { - name = `@context/${name}`; - const obj = { - [name](context: IDataContextProvider): T { - return defaultValue?.(context) as T; - }, - }; - Object.defineProperty(obj[name], 'id', { value: uuid() }); - return obj[name] as DataContextGetter; +export function createDataContext(name: string): DataContextGetter { + return { + id: uuid(), + name: `@context/${name}`, + } as Partial> as DataContextGetter; } diff --git a/webapp/packages/core-data-context/src/DataContext/dataContextAddDIProvider.ts b/webapp/packages/core-data-context/src/DataContext/dataContextAddDIProvider.ts index 6a595b7d20..70430c86e2 100644 --- a/webapp/packages/core-data-context/src/DataContext/dataContextAddDIProvider.ts +++ b/webapp/packages/core-data-context/src/DataContext/dataContextAddDIProvider.ts @@ -10,7 +10,7 @@ import type { App } from '@cloudbeaver/core-di'; import { DATA_CONTEXT_DI_PROVIDER } from './DATA_CONTEXT_DI_PROVIDER'; import type { IDataContext } from './IDataContext'; -export function dataContextAddDIProvider(context: IDataContext, app: App): IDataContext { - context.set(DATA_CONTEXT_DI_PROVIDER, app.getServiceInjector()); +export function dataContextAddDIProvider(context: IDataContext, app: App, id: string): IDataContext { + context.set(DATA_CONTEXT_DI_PROVIDER, app.getServiceInjector(), id); return context; } diff --git a/webapp/packages/core-data-context/src/DataContext/useDataContext.ts b/webapp/packages/core-data-context/src/DataContext/useDataContext.ts index 5dfb59eaff..53bbbc3dd7 100644 --- a/webapp/packages/core-data-context/src/DataContext/useDataContext.ts +++ b/webapp/packages/core-data-context/src/DataContext/useDataContext.ts @@ -5,16 +5,18 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { useState } from 'react'; +import { useLayoutEffect, useState } from 'react'; +import { DataContext } from './DataContext'; import type { IDataContext } from './IDataContext'; import type { IDataContextProvider } from './IDataContextProvider'; -import { TempDataContext } from './TempDataContext'; export function useDataContext(fallback?: IDataContextProvider): IDataContext { - const [context] = useState(() => new TempDataContext()); + const [context] = useState(() => new DataContext()); - context.setFallBack(fallback); + useLayoutEffect(() => { + context.setFallBack(fallback); + }); return context; } diff --git a/webapp/packages/core-data-context/src/DataContext/useDataContextLink.ts b/webapp/packages/core-data-context/src/DataContext/useDataContextLink.ts new file mode 100644 index 0000000000..164e9d2028 --- /dev/null +++ b/webapp/packages/core-data-context/src/DataContext/useDataContextLink.ts @@ -0,0 +1,32 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { autorun } from 'mobx'; +import { useLayoutEffect, useState } from 'react'; + +import { uuid } from '@cloudbeaver/core-utils'; + +import type { IDataContext } from './IDataContext'; + +export function useDataContextLink(context: IDataContext | undefined, update: (context: IDataContext, id: string) => void): void { + const [id] = useState(() => uuid()); + + useLayoutEffect(() => + autorun(() => { + if (context) { + update(context, id); + } + }), + ); + + useLayoutEffect( + () => () => { + context?.deleteForId(id); + }, + [context, id], + ); +} diff --git a/webapp/packages/core-data-context/src/DataContext/useDynamicDataContext.ts b/webapp/packages/core-data-context/src/DataContext/useDynamicDataContext.ts deleted file mode 100644 index e2969840d3..0000000000 --- a/webapp/packages/core-data-context/src/DataContext/useDynamicDataContext.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 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 { untracked } from 'mobx'; -import { useEffect, useState } from 'react'; - -import { DynamicDataContext } from './DynamicDataContext'; -import type { IDataContext } from './IDataContext'; -import { TempDataContext } from './TempDataContext'; - -export function useDynamicDataContext(context: IDataContext | undefined, capture: (context: IDataContext) => void): void { - const [state] = useState(() => new DynamicDataContext(context || new TempDataContext())); - - untracked(() => { - if (context) { - state.setFallBack(context); - } - }); - - useEffect(() => { - capture(state); - }); - - useEffect(() => () => state.clear(), []); -} diff --git a/webapp/packages/core-data-context/src/index.ts b/webapp/packages/core-data-context/src/index.ts index dfe60d3993..48ab305b2a 100644 --- a/webapp/packages/core-data-context/src/index.ts +++ b/webapp/packages/core-data-context/src/index.ts @@ -1,22 +1,17 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 * from './DataContext/createDataContext'; export * from './DataContext/DATA_CONTEXT_DI_PROVIDER'; export * from './DataContext/DataContext'; export * from './DataContext/dataContextAddDIProvider'; -export * from './DataContext/DynamicDataContext'; export * from './DataContext/IDataContext'; export * from './DataContext/IDataContextProvider'; -export * from './DataContext/TempDataContext'; export * from './DataContext/useDataContext'; -export * from './DataContext/useDynamicDataContext'; -export * from './DataContext/createDataContext'; -export * from './DataContext/DATA_CONTEXT_DI_PROVIDER'; -export * from './DataContext/DataContext'; -export * from './DataContext/dataContextAddDIProvider'; export * from './DataContext/DataContextGetter'; -export * from './DataContext/DynamicDataContext'; -export * from './DataContext/IDataContext'; -export * from './DataContext/IDataContextProvider'; -export * from './DataContext/TempDataContext'; -export * from './DataContext/useDataContext'; -export * from './DataContext/useDynamicDataContext'; +export * from './DataContext/useDataContextLink'; export { coreDataContextManifest } from './manifest'; diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index aedb81da43..d2cdff1589 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -52,6 +52,10 @@ export default [ ['ui_search', 'Search...'], ['ui_delete', 'Delete'], ['ui_add', 'Add'], + ['ui_revert', 'Revert'], + ['ui_undo', 'Undo'], + ['ui_redo', 'Redo'], + ['ui_duplicate', 'Duplicate'], ['ui_refresh', 'Refresh'], ['ui_data_saving_error', 'Save error'], ['ui_data_remove_confirmation', 'Remove confirmation'], diff --git a/webapp/packages/core-localization/src/locales/it.ts b/webapp/packages/core-localization/src/locales/it.ts index ae1c482cb0..98c13ed403 100644 --- a/webapp/packages/core-localization/src/locales/it.ts +++ b/webapp/packages/core-localization/src/locales/it.ts @@ -49,6 +49,10 @@ export default [ ['ui_search', 'Cerca...'], ['ui_delete', 'Elimina'], ['ui_add', 'Aggiungi'], + ['ui_revert', 'Revert'], + ['ui_undo', 'Undo'], + ['ui_redo', 'Redo'], + ['ui_duplicate', 'Duplicate'], ['ui_refresh', 'Aggiorna'], ['ui_data_saving_error', 'Errore di salvataggio'], ['ui_data_remove_confirmation', 'Remove confirmation'], diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index 493917324e..f765a90b56 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -48,6 +48,10 @@ export default [ ['ui_search', 'Поиск...'], ['ui_delete', 'Удалить'], ['ui_add', 'Добавить'], + ['ui_revert', 'Отменить изменения'], + ['ui_undo', 'Отменить'], + ['ui_redo', 'Повторить'], + ['ui_duplicate', 'Дублировать'], ['ui_refresh', 'Обновить'], ['ui_data_saving_error', 'Ошибка при сохранении изменений'], ['ui_no_matches_placeholder', 'По вашему запросу ничего не найдено.'], diff --git a/webapp/packages/core-localization/src/locales/zh.ts b/webapp/packages/core-localization/src/locales/zh.ts index cb171094ad..c0c89bb2bc 100644 --- a/webapp/packages/core-localization/src/locales/zh.ts +++ b/webapp/packages/core-localization/src/locales/zh.ts @@ -49,6 +49,10 @@ export default [ ['ui_search', '搜索...'], ['ui_delete', '删除'], ['ui_add', '添加'], + ['ui_revert', 'Revert'], + ['ui_undo', 'Undo'], + ['ui_redo', 'Redo'], + ['ui_duplicate', 'Duplicate'], ['ui_refresh', '刷新'], ['ui_data_saving_error', '保存出错'], ['ui_data_remove_confirmation', 'Remove confirmation'], diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/getNodesFromContext.ts b/webapp/packages/core-navigation-tree/src/NodesManager/getNodesFromContext.ts index 146ab2b64f..58009c1efb 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/getNodesFromContext.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/getNodesFromContext.ts @@ -12,8 +12,8 @@ import { DATA_CONTEXT_NAV_NODES } from './DATA_CONTEXT_NAV_NODES'; import type { NavNode } from './EntityTypes'; export function getNodesFromContext(context: IDataContextProvider): NavNode[] { - const node = context.tryGet(DATA_CONTEXT_NAV_NODE); - const getNodes = context.tryGet(DATA_CONTEXT_NAV_NODES); + const node = context.get(DATA_CONTEXT_NAV_NODE); + const getNodes = context.get(DATA_CONTEXT_NAV_NODES); let nodes = getNodes?.(); if (!nodes || nodes.length < 2) { diff --git a/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.tsx b/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.tsx index 156ac27b51..26388e5d80 100644 --- a/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.tsx +++ b/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.tsx @@ -9,6 +9,7 @@ import { observer } from 'mobx-react-lite'; import { forwardRef, useCallback } from 'react'; import { getComputed, MenuSeparator, MenuSeparatorStyles, s, SContext, StyleRegistry, useAutoLoad, useS } from '@cloudbeaver/core-blocks'; +import { useDataContextLink } from '@cloudbeaver/core-data-context'; import { DATA_CONTEXT_MENU_NESTED, DATA_CONTEXT_SUBMENU_ITEM, @@ -150,16 +151,19 @@ interface ISubMenuItemProps { rtl?: boolean; } -const SubMenuItem = observer(function SubmenuItem({ item, menuData, nestedMenuSettings, className, rtl }) { +const SubMenuItem = observer(function SubMenuItem({ item, menuData, nestedMenuSettings, className, rtl }) { const subMenuData = useMenu({ menu: item.menu, context: menuData.context }); - subMenuData.context.set(DATA_CONTEXT_MENU_NESTED, true); - subMenuData.context.set(DATA_CONTEXT_SUBMENU_ITEM, item); + useDataContextLink(subMenuData.context, (context, id) => { + subMenuData.context.set(DATA_CONTEXT_MENU_NESTED, true, id); + subMenuData.context.set(DATA_CONTEXT_SUBMENU_ITEM, item, id); + }); const handler = subMenuData.handler; const hideIfEmpty = handler?.hideIfEmpty?.(subMenuData.context) ?? true; const hidden = getComputed(() => subMenuData.items.every(item => item.hidden)); + // TODO: need to be fixed, in case when menu depend on data from loaders this may be always true if (hideIfEmpty && hidden) { return null; } diff --git a/webapp/packages/core-ui/src/ContextMenu/SubMenuElement.tsx b/webapp/packages/core-ui/src/ContextMenu/SubMenuElement.tsx index 50778176fe..6a61190284 100644 --- a/webapp/packages/core-ui/src/ContextMenu/SubMenuElement.tsx +++ b/webapp/packages/core-ui/src/ContextMenu/SubMenuElement.tsx @@ -9,6 +9,7 @@ import { observer } from 'mobx-react-lite'; import { forwardRef, useRef, useState } from 'react'; import { getComputed, IMenuState, Menu, MenuItemElement, useAutoLoad, useObjectRef } from '@cloudbeaver/core-blocks'; +import { useDataContextLink } from '@cloudbeaver/core-data-context'; import { DATA_CONTEXT_MENU_NESTED, DATA_CONTEXT_SUBMENU_ITEM, IMenuData, IMenuSubMenuItem, MenuActionItem, useMenu } from '@cloudbeaver/core-view'; import type { IMenuItemRendererProps } from './MenuItemRenderer'; @@ -27,8 +28,11 @@ export const SubMenuElement = observer( const menu = useRef(); const subMenuData = useMenu({ menu: subMenu.menu, context: menuData.context }); const [visible, setVisible] = useState(false); - subMenuData.context.set(DATA_CONTEXT_MENU_NESTED, true); - subMenuData.context.set(DATA_CONTEXT_SUBMENU_ITEM, subMenu); + + useDataContextLink(subMenuData.context, (context, id) => { + context.set(DATA_CONTEXT_MENU_NESTED, true, id); + context.set(DATA_CONTEXT_SUBMENU_ITEM, subMenu, id); + }); const handler = subMenuData.handler; const hidden = getComputed(() => handler?.isHidden?.(subMenuData.context)); diff --git a/webapp/packages/core-ui/src/Form/FormState.ts b/webapp/packages/core-ui/src/Form/FormState.ts index bc1aee5f50..3d620b2b48 100644 --- a/webapp/packages/core-ui/src/Form/FormState.ts +++ b/webapp/packages/core-ui/src/Form/FormState.ts @@ -7,7 +7,7 @@ */ import { action, computed, makeObservable, observable } from 'mobx'; -import { dataContextAddDIProvider, DataContextGetter, type IDataContext, TempDataContext } from '@cloudbeaver/core-data-context'; +import { DataContext, dataContextAddDIProvider, DataContextGetter, type IDataContext } from '@cloudbeaver/core-data-context'; import type { App } from '@cloudbeaver/core-di'; import type { ENotificationType } from '@cloudbeaver/core-events'; import { Executor, ExecutorInterrupter, IExecutionContextProvider, type IExecutor } from '@cloudbeaver/core-executor'; @@ -51,7 +51,7 @@ export class FormState implements IFormState { constructor(app: App, service: FormBaseService, state: TState) { this.id = uuid(); this.service = service; - this.dataContext = new TempDataContext(); + this.dataContext = new DataContext(); this.mode = FormMode.Create; this.parts = new MetadataMap(); @@ -82,9 +82,9 @@ export class FormState implements IFormState { this.submitTask = new Executor(this as IFormState, () => true); this.submitTask.addCollection(service.onSubmit).before(this.validationTask); - this.dataContext.set(DATA_CONTEXT_LOADABLE_STATE, loadableStateContext()); - this.dataContext.set(DATA_CONTEXT_FORM_STATE, this); - dataContextAddDIProvider(this.dataContext, app); + this.dataContext.set(DATA_CONTEXT_LOADABLE_STATE, loadableStateContext(), this.id); + this.dataContext.set(DATA_CONTEXT_FORM_STATE, this, this.id); + dataContextAddDIProvider(this.dataContext, app, this.id); makeObservable(this, { mode: observable, @@ -102,34 +102,42 @@ export class FormState implements IFormState { } isLoading(): boolean { - return this.promise !== null || this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE).loaders.some(loader => loader.isLoading()); + return this.promise !== null || this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders.some(loader => loader.isLoading()); } isLoaded(): boolean { if (this.promise) { return false; } - return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE).loaders.every(loader => loader.isLoaded()); + return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders.every(loader => loader.isLoaded()); } isError(): boolean { - return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE).loaders.some(loader => loader.isError()); + return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders.some(loader => loader.isError()); } isOutdated(): boolean { - return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE).loaders.some(loader => loader.isOutdated?.() === true); + return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders.some(loader => loader.isOutdated?.() === true); } isCancelled(): boolean { - return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE).loaders.some(loader => loader.isCancelled?.() === true); + return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders.some(loader => loader.isCancelled?.() === true); } isChanged(): boolean { return Array.from(this.parts.values()).some(part => part.isChanged()); } - getPart>(getter: DataContextGetter): T { - return this.parts.get(getter.id, () => this.dataContext.get(getter)) as T; + getPart>(getter: DataContextGetter, init: (context: IDataContext, id: string) => T): T { + return this.parts.get(getter.id, () => { + if (this.dataContext.has(getter)) { + return this.dataContext.get(getter)!; + } + + const part = init(this.dataContext, this.id); + this.dataContext.set(getter, part, this.id); + return part; + }) as T; } async load(refresh?: boolean): Promise { @@ -145,7 +153,7 @@ export class FormState implements IFormState { try { await this.configureTask.execute(this); - const loaders = this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE).loaders; + const loaders = this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders; for (const loader of loaders) { if (isLoadableStateHasException(loader)) { @@ -177,7 +185,7 @@ export class FormState implements IFormState { } cancel(): void { - const loaders = this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE).loaders; + const loaders = this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders; for (const loader of loaders) { if (loader.isCancelled?.() !== true) { diff --git a/webapp/packages/core-ui/src/Form/IFormState.ts b/webapp/packages/core-ui/src/Form/IFormState.ts index 5feb223cfd..67b2029e1f 100644 --- a/webapp/packages/core-ui/src/Form/IFormState.ts +++ b/webapp/packages/core-ui/src/Form/IFormState.ts @@ -42,7 +42,7 @@ export interface IFormState extends ILoadableState { setException(exception: Error | (Error | null)[] | null): this; setState(state: TState): this; - getPart>(getter: DataContextGetter): T; + getPart>(getter: DataContextGetter, init: (context: IDataContext, id: string) => T): T; isLoading(): boolean; isLoaded(): boolean; diff --git a/webapp/packages/core-ui/src/Tabs/Tab/TabMenu.tsx b/webapp/packages/core-ui/src/Tabs/Tab/TabMenu.tsx index af8d6c2c60..bbeac60ac6 100644 --- a/webapp/packages/core-ui/src/Tabs/Tab/TabMenu.tsx +++ b/webapp/packages/core-ui/src/Tabs/Tab/TabMenu.tsx @@ -8,7 +8,7 @@ import { observer } from 'mobx-react-lite'; import { getComputed, s, useS } from '@cloudbeaver/core-blocks'; -import type { IDataContext } from '@cloudbeaver/core-data-context'; +import { type IDataContext, useDataContextLink } from '@cloudbeaver/core-data-context'; import { useMenu } from '@cloudbeaver/core-view'; import { ContextMenu } from '../../ContextMenu/ContextMenu'; @@ -31,8 +31,10 @@ export const TabMenu = observer(function TabMenu({ children, tabId context: menuContext, }); - menu.context.set(DATA_CONTEXT_TABS_CONTEXT, state); - menu.context.set(DATA_CONTEXT_TAB_ID, tabId); + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_TABS_CONTEXT, state, id); + context.set(DATA_CONTEXT_TAB_ID, tabId, id); + }); const hidden = getComputed(() => !menu.items.length || menu.items.every(item => item.hidden)); diff --git a/webapp/packages/core-ui/src/Tabs/TabsBootstrap.ts b/webapp/packages/core-ui/src/Tabs/TabsBootstrap.ts index 92411d917c..7affbea2d6 100644 --- a/webapp/packages/core-ui/src/Tabs/TabsBootstrap.ts +++ b/webapp/packages/core-ui/src/Tabs/TabsBootstrap.ts @@ -29,12 +29,13 @@ export class TabsBootstrap extends Bootstrap { register(): void | Promise { this.actionService.addHandler({ id: 'tabs-base-handler', + contexts: [DATA_CONTEXT_TAB_ID, DATA_CONTEXT_TABS_CONTEXT], isActionApplicable: (context, action) => { const menu = context.hasValue(DATA_CONTEXT_MENU, MENU_TAB); - const state = context.tryGet(DATA_CONTEXT_TABS_CONTEXT); - const tab = context.tryGet(DATA_CONTEXT_TAB_ID); + const state = context.get(DATA_CONTEXT_TABS_CONTEXT); + const tab = context.get(DATA_CONTEXT_TAB_ID)!; - if (!menu || !state?.tabList || !tab) { + if (!menu || !state?.tabList) { return false; } @@ -59,8 +60,8 @@ export class TabsBootstrap extends Bootstrap { return [ACTION_TAB_CLOSE].includes(action); }, handler: async (context, action) => { - const state = context.get(DATA_CONTEXT_TABS_CONTEXT); - const tab = context.get(DATA_CONTEXT_TAB_ID); + const state = context.get(DATA_CONTEXT_TABS_CONTEXT)!; + const tab = context.get(DATA_CONTEXT_TAB_ID)!; switch (action) { case ACTION_TAB_CLOSE: @@ -87,8 +88,8 @@ export class TabsBootstrap extends Bootstrap { this.menuService.addCreator({ menus: [MENU_TAB], isApplicable: context => { - const tab = context.tryGet(DATA_CONTEXT_TAB_ID); - const state = context.tryGet(DATA_CONTEXT_TABS_CONTEXT); + const tab = context.get(DATA_CONTEXT_TAB_ID); + const state = context.get(DATA_CONTEXT_TABS_CONTEXT); return !!tab && !!state?.enabledBaseActions && state.canClose(tab); }, getItems: (context, items) => [ diff --git a/webapp/packages/core-view/src/Action/ActionService.ts b/webapp/packages/core-view/src/Action/ActionService.ts index 776edf6835..aed9fa3122 100644 --- a/webapp/packages/core-view/src/Action/ActionService.ts +++ b/webapp/packages/core-view/src/Action/ActionService.ts @@ -49,7 +49,7 @@ export class ActionService { } if (handler.contexts.size > 0) { for (const context of handler.contexts) { - if (!contexts.has(context, true)) { + if (!contexts.has(context)) { continue handlers; } } diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterRowCount.module.css b/webapp/packages/core-view/src/Action/Actions/ACTION_ADD.ts similarity index 64% rename from webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterRowCount.module.css rename to webapp/packages/core-view/src/Action/Actions/ACTION_ADD.ts index 294039e523..53662e2970 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterRowCount.module.css +++ b/webapp/packages/core-view/src/Action/Actions/ACTION_ADD.ts @@ -5,7 +5,8 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -.wrapper { - display: flex; - height: 100%; -} +import { createAction } from '../createAction'; + +export const ACTION_ADD = createAction('add', { + label: 'ui_add', +}); diff --git a/webapp/packages/core-view/src/Action/Actions/ACTION_CANCEL.ts b/webapp/packages/core-view/src/Action/Actions/ACTION_CANCEL.ts new file mode 100644 index 0000000000..185565f41e --- /dev/null +++ b/webapp/packages/core-view/src/Action/Actions/ACTION_CANCEL.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '../createAction'; + +export const ACTION_CANCEL = createAction('cancel', { + label: 'ui_processing_cancel', + icon: 'cross', +}); diff --git a/webapp/packages/core-view/src/Action/Actions/ACTION_DUPLICATE.ts b/webapp/packages/core-view/src/Action/Actions/ACTION_DUPLICATE.ts new file mode 100644 index 0000000000..fa7570fad0 --- /dev/null +++ b/webapp/packages/core-view/src/Action/Actions/ACTION_DUPLICATE.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '../createAction'; + +export const ACTION_DUPLICATE = createAction('duplicate', { + label: 'ui_duplicate', +}); diff --git a/webapp/packages/core-view/src/Action/Actions/ACTION_REVERT.ts b/webapp/packages/core-view/src/Action/Actions/ACTION_REVERT.ts new file mode 100644 index 0000000000..010d7f5600 --- /dev/null +++ b/webapp/packages/core-view/src/Action/Actions/ACTION_REVERT.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '../createAction'; + +export const ACTION_REVERT = createAction('revert', { + label: 'ui_revert', +}); diff --git a/webapp/packages/core-view/src/Action/KeyBinding/KeyBindingService.ts b/webapp/packages/core-view/src/Action/KeyBinding/KeyBindingService.ts index d6e2d396c6..b5647ad1f0 100644 --- a/webapp/packages/core-view/src/Action/KeyBinding/KeyBindingService.ts +++ b/webapp/packages/core-view/src/Action/KeyBinding/KeyBindingService.ts @@ -37,7 +37,7 @@ export class KeyBindingService { } if (handler.contexts.size > 0) { for (const context of handler.contexts) { - if (!contexts.has(context, true)) { + if (!contexts.has(context)) { continue handlers; } } diff --git a/webapp/packages/core-view/src/LoadableStateContext/DATA_CONTEXT_LOADABLE_STATE.ts b/webapp/packages/core-view/src/LoadableStateContext/DATA_CONTEXT_LOADABLE_STATE.ts index 7738b7b2c5..0a70918004 100644 --- a/webapp/packages/core-view/src/LoadableStateContext/DATA_CONTEXT_LOADABLE_STATE.ts +++ b/webapp/packages/core-view/src/LoadableStateContext/DATA_CONTEXT_LOADABLE_STATE.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2021 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -36,4 +36,4 @@ export function loadableStateContext(): ILoadableStateContext { }; } -export const DATA_CONTEXT_LOADABLE_STATE = createDataContext('loadable-state', loadableStateContext); +export const DATA_CONTEXT_LOADABLE_STATE = createDataContext('loadable-state'); diff --git a/webapp/packages/core-view/src/Menu/DATA_CONTEXT_MENU_NESTED.ts b/webapp/packages/core-view/src/Menu/DATA_CONTEXT_MENU_NESTED.ts index 10ce5fdd07..632cc0c5e3 100644 --- a/webapp/packages/core-view/src/Menu/DATA_CONTEXT_MENU_NESTED.ts +++ b/webapp/packages/core-view/src/Menu/DATA_CONTEXT_MENU_NESTED.ts @@ -7,4 +7,4 @@ */ import { createDataContext } from '@cloudbeaver/core-data-context'; -export const DATA_CONTEXT_MENU_NESTED = createDataContext('menu-nested', () => false); +export const DATA_CONTEXT_MENU_NESTED = createDataContext('menu-nested'); diff --git a/webapp/packages/core-view/src/Menu/MenuService.ts b/webapp/packages/core-view/src/Menu/MenuService.ts index 40cf229a04..30f6796ac5 100644 --- a/webapp/packages/core-view/src/Menu/MenuService.ts +++ b/webapp/packages/core-view/src/Menu/MenuService.ts @@ -70,11 +70,9 @@ export class MenuService { continue; } } - if (handler.contexts.size > 0) { - for (const context of handler.contexts) { - if (!contexts.has(context, true)) { - continue handlers; - } + for (const context of handler.contexts) { + if (!contexts.has(context)) { + continue handlers; } } if (handler.isApplicable?.(contexts) !== false) { @@ -156,7 +154,7 @@ function filterApplicable(contexts: IDataContextProvider): (creator: IMenuItemsC if (creator.contexts.size > 0) { for (const context of creator.contexts) { - if (!contexts.has(context, true)) { + if (!contexts.has(context)) { return false; } } diff --git a/webapp/packages/core-view/src/Menu/useMenuContext.ts b/webapp/packages/core-view/src/Menu/useMenuContext.ts index e7fca0b64e..ac7daa5425 100644 --- a/webapp/packages/core-view/src/Menu/useMenuContext.ts +++ b/webapp/packages/core-view/src/Menu/useMenuContext.ts @@ -7,7 +7,7 @@ */ import { useContext } from 'react'; -import { type IDataContext, useDataContext } from '@cloudbeaver/core-data-context'; +import { type IDataContext, useDataContext, useDataContextLink } from '@cloudbeaver/core-data-context'; import { DATA_CONTEXT_LOADABLE_STATE, loadableStateContext } from '../LoadableStateContext/DATA_CONTEXT_LOADABLE_STATE'; import { CaptureViewContext } from '../View/CaptureViewContext'; @@ -17,12 +17,15 @@ import type { IMenu } from './IMenu'; export function useMenuContext(menu: IMenu, _menuContext?: IDataContext): IDataContext { const viewContext = useContext(CaptureViewContext); const context = useDataContext(_menuContext || viewContext); + const hasLoadableState = context.hasOwn(DATA_CONTEXT_LOADABLE_STATE); - context.set(DATA_CONTEXT_MENU, menu); + useDataContextLink(context, (context, id) => { + context.set(DATA_CONTEXT_MENU, menu, id); - if (!context.has(DATA_CONTEXT_LOADABLE_STATE, false)) { - context.set(DATA_CONTEXT_LOADABLE_STATE, loadableStateContext()); - } + if (!hasLoadableState) { + context.set(DATA_CONTEXT_LOADABLE_STATE, loadableStateContext(), id); + } + }); return context; } diff --git a/webapp/packages/core-view/src/View/CaptureViewScope.tsx b/webapp/packages/core-view/src/View/CaptureViewScope.tsx new file mode 100644 index 0000000000..b1393beb3f --- /dev/null +++ b/webapp/packages/core-view/src/View/CaptureViewScope.tsx @@ -0,0 +1,20 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { type PropsWithChildren, useContext } from 'react'; + +import { useDataContext } from '@cloudbeaver/core-data-context'; + +import { CaptureViewContext } from './CaptureViewContext'; + +export const CaptureViewScope = observer(function CaptureViewScope({ children }) { + const context = useContext(CaptureViewContext); + const viewContext = useDataContext(context); + + return {children}; +}); diff --git a/webapp/packages/core-view/src/View/CaptureViewScopeLazy.ts b/webapp/packages/core-view/src/View/CaptureViewScopeLazy.ts new file mode 100644 index 0000000000..d8bf7067dc --- /dev/null +++ b/webapp/packages/core-view/src/View/CaptureViewScopeLazy.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { importLazyComponent } from '@cloudbeaver/core-blocks'; + +export const CaptureViewScope = importLazyComponent(() => import('./CaptureViewScope').then(m => m.CaptureViewScope)); diff --git a/webapp/packages/core-view/src/View/useCaptureViewContext.ts b/webapp/packages/core-view/src/View/useCaptureViewContext.ts index 90b7bc780c..72c691225e 100644 --- a/webapp/packages/core-view/src/View/useCaptureViewContext.ts +++ b/webapp/packages/core-view/src/View/useCaptureViewContext.ts @@ -7,11 +7,11 @@ */ import { useContext } from 'react'; -import { IDataContext, useDynamicDataContext } from '@cloudbeaver/core-data-context'; +import { IDataContext, useDataContextLink } from '@cloudbeaver/core-data-context'; import { CaptureViewContext } from './CaptureViewContext'; -export function useCaptureViewContext(capture: (context: IDataContext | undefined) => void): void { +export function useCaptureViewContext(capture: (context: IDataContext, id: string) => void): void { const context = useContext(CaptureViewContext); - useDynamicDataContext(context, capture); + useDataContextLink(context, capture); } diff --git a/webapp/packages/core-view/src/View/useViewContext.ts b/webapp/packages/core-view/src/View/useViewContext.ts index ce069d0f85..60967f348e 100644 --- a/webapp/packages/core-view/src/View/useViewContext.ts +++ b/webapp/packages/core-view/src/View/useViewContext.ts @@ -7,7 +7,7 @@ */ import { useContext } from 'react'; -import { IDataContext, useDataContext } from '@cloudbeaver/core-data-context'; +import { IDataContext, useDataContext, useDataContextLink } from '@cloudbeaver/core-data-context'; import { CaptureViewContext } from './CaptureViewContext'; import { DATA_CONTEXT_VIEW } from './DATA_CONTEXT_VIEW'; @@ -17,7 +17,9 @@ export function useViewContext(view: IView, parentContext: IDataContext | u const context = useContext(CaptureViewContext); const viewContext = useDataContext(parentContext ?? context); - viewContext.set(DATA_CONTEXT_VIEW, view); + useDataContextLink(viewContext, (context, id) => { + context.set(DATA_CONTEXT_VIEW, view, id); + }); return viewContext; } diff --git a/webapp/packages/core-view/src/index.ts b/webapp/packages/core-view/src/index.ts index 74483205cf..404ab38f94 100644 --- a/webapp/packages/core-view/src/index.ts +++ b/webapp/packages/core-view/src/index.ts @@ -5,10 +5,13 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +export * from './Action/Actions/ACTION_ADD'; +export * from './Action/Actions/ACTION_CANCEL'; export * from './Action/Actions/ACTION_COLLAPSE_ALL'; export * from './Action/Actions/ACTION_CREATE'; export * from './Action/Actions/ACTION_DELETE'; export * from './Action/Actions/ACTION_EDIT'; +export * from './Action/Actions/ACTION_DUPLICATE'; export * from './Action/Actions/ACTION_EXPORT'; export * from './Action/Actions/ACTION_FILTER'; export * from './Action/Actions/ACTION_LAYOUT'; @@ -18,6 +21,7 @@ export * from './Action/Actions/ACTION_OPEN'; export * from './Action/Actions/ACTION_REDO'; export * from './Action/Actions/ACTION_REFRESH'; export * from './Action/Actions/ACTION_RENAME'; +export * from './Action/Actions/ACTION_REVERT'; export * from './Action/Actions/ACTION_SAVE'; export * from './Action/Actions/ACTION_SETTINGS'; export * from './Action/Actions/ACTION_UNDO'; @@ -66,6 +70,7 @@ export * from './Menu/MenuService'; export * from './Menu/useMenu'; export * from './Menu/useMenuContext'; export * from './View/AppView'; +export * from './View/CaptureViewScopeLazy'; export * from './View/CaptureViewLazy'; export * from './View/CaptureViewContext'; export * from './View/IActiveView'; diff --git a/webapp/packages/plugin-administration/src/Administration/AdministrationCaptureViewContext.tsx b/webapp/packages/plugin-administration/src/Administration/AdministrationCaptureViewContext.tsx index b2e16677ae..96a29380a4 100644 --- a/webapp/packages/plugin-administration/src/Administration/AdministrationCaptureViewContext.tsx +++ b/webapp/packages/plugin-administration/src/Administration/AdministrationCaptureViewContext.tsx @@ -15,8 +15,8 @@ export const AdministrationCaptureViewContext = observer(function Administration const administrationScreenService = useService(AdministrationScreenService); const route = administrationScreenService.activeScreen; - useCaptureViewContext(context => { - context?.set(DATA_CONTEXT_ADMINISTRATION_ITEM_ROUTE, route); + useCaptureViewContext((context, id) => { + context.set(DATA_CONTEXT_ADMINISTRATION_ITEM_ROUTE, route, id); }); return null; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.tsx index 17bb56e7c4..5c006048a7 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.tsx @@ -16,7 +16,7 @@ import { getFirstException } from '@cloudbeaver/core-utils'; import style from './AdministrationUserForm.module.css'; import { AdministrationUserFormDeleteButton } from './AdministrationUserFormDeleteButton'; import { AdministrationUserFormService, IUserFormState } from './AdministrationUserFormService'; -import { DATA_CONTEXT_USER_FORM_INFO_PART } from './Info/DATA_CONTEXT_USER_FORM_INFO_PART'; +import { getUserFormInfoPart } from './Info/getUserFormInfoPart'; interface Props { state: IFormState; @@ -24,13 +24,13 @@ interface Props { } export const AdministrationUserForm = observer(function AdministrationUserForm({ state, onClose }) { + const userFormInfoPart = getUserFormInfoPart(state); const styles = useS(style); const translate = useTranslate(); const notificationService = useService(NotificationService); const administrationUserFormService = useService(AdministrationUserFormService); const editing = state.mode === FormMode.Edit; - const userFormInfoPart = state.dataContext.get(DATA_CONTEXT_USER_FORM_INFO_PART); const form = useForm({ async onSubmit() { diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART.ts deleted file mode 100644 index 12a176c05c..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 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 { UsersResource } from '@cloudbeaver/core-authentication'; -import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; -import { DATA_CONTEXT_FORM_STATE } from '@cloudbeaver/core-ui'; - -import type { AdministrationUserFormState } from '../AdministrationUserFormState'; -import { DATA_CONTEXT_USER_FORM_INFO_PART } from '../Info/DATA_CONTEXT_USER_FORM_INFO_PART'; -import { UserFormConnectionAccessPart } from './UserFormConnectionAccessPart'; -import { ProjectInfoResource } from '@cloudbeaver/core-projects'; - -export const DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART = createDataContext( - 'User Form Connection Access Part', - context => { - context.get(DATA_CONTEXT_USER_FORM_INFO_PART); // ensure that info part is loaded first - - const form = context.get(DATA_CONTEXT_FORM_STATE) as AdministrationUserFormState; - const di = context.get(DATA_CONTEXT_DI_PROVIDER); - const usersResource = di.getServiceByClass(UsersResource); - const projectInfoResource = di.getServiceByClass(ProjectInfoResource); - - return new UserFormConnectionAccessPart(form, usersResource, projectInfoResource); - }, -); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPanel.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPanel.tsx index b102127d84..1c0b19b408 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPanel.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPanel.tsx @@ -7,11 +7,11 @@ */ import { observer } from 'mobx-react-lite'; -import { ColoredContainer, Container, Group, TextPlaceholder, useAutoLoad, useTranslate } from '@cloudbeaver/core-blocks'; +import { Container, Group, useAutoLoad, useTranslate } from '@cloudbeaver/core-blocks'; import { type TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui'; import type { UserFormProps } from '../AdministrationUserFormService'; -import { DATA_CONTEXT_USER_FORM_INFO_PART } from '../Info/DATA_CONTEXT_USER_FORM_INFO_PART'; +import { getUserFormInfoPart } from '../Info/getUserFormInfoPart'; import { UserFormConnectionAccess } from './UserFormConnectionAccess'; export const UserFormConnectionAccessPanel: TabContainerPanelComponent = observer(function UserFormConnectionAccessPanel({ @@ -21,7 +21,7 @@ export const UserFormConnectionAccessPanel: TabContainerPanelComponent { constructor( - formState: AdministrationUserFormState, + formState: IFormState, private readonly usersResource: UsersResource, private readonly projectInfoResource: ProjectInfoResource, + private readonly userFormInfoPart: UserFormInfoPart, ) { super(formState, []); } @@ -65,8 +65,6 @@ export class UserFormConnectionAccessPart extends FormPart 0) { - await this.usersResource.deleteConnectionsAccess(globalProject.id, userFormInfoPart.state.userId, connectionsToRevoke); + await this.usersResource.deleteConnectionsAccess(globalProject.id, this.userFormInfoPart.state.userId, connectionsToRevoke); } if (connectionsToGrant.length > 0) { - await this.usersResource.addConnectionsAccess(globalProject.id, userFormInfoPart.state.userId, connectionsToGrant); + await this.usersResource.addConnectionsAccess(globalProject.id, this.userFormInfoPart.state.userId, connectionsToGrant); } } @@ -94,11 +92,10 @@ export class UserFormConnectionAccessPart extends FormPart { const { UserFormConnectionAccessPanel } = await import('./UserFormConnectionAccessPanel'); @@ -36,7 +36,7 @@ export class UserFormConnectionAccessPartBootstrap extends Bootstrap { order: 3, panel: () => UserFormConnectionAccessPanel, isHidden: () => !this.projectInfoResource.values.some(isGlobalProject), - stateGetter: props => () => props.formState.dataContext.get(DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART), + stateGetter: props => () => getUserFormConnectionAccessPart(props.formState), getLoader: () => getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey), }); } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/getUserFormConnectionAccessPart.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/getUserFormConnectionAccessPart.ts new file mode 100644 index 0000000000..362ef7c772 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/getUserFormConnectionAccessPart.ts @@ -0,0 +1,29 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { UsersResource } from '@cloudbeaver/core-authentication'; +import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; +import { ProjectInfoResource } from '@cloudbeaver/core-projects'; +import type { IFormState } from '@cloudbeaver/core-ui'; + +import type { IUserFormState } from '../AdministrationUserFormService'; +import { getUserFormInfoPart } from '../Info/getUserFormInfoPart'; +import { UserFormConnectionAccessPart } from './UserFormConnectionAccessPart'; + +const DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART = createDataContext('User Form Connection Access Part'); + +export function getUserFormConnectionAccessPart(formState: IFormState): UserFormConnectionAccessPart { + return formState.getPart(DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART, context => { + const userFormInfoPart = getUserFormInfoPart(formState); + + const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; + const usersResource = di.getServiceByClass(UsersResource); + const projectInfoResource = di.getServiceByClass(ProjectInfoResource); + + return new UserFormConnectionAccessPart(formState, usersResource, projectInfoResource, userFormInfoPart); + }); +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/DATA_CONTEXT_USER_FORM_INFO_PART.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/DATA_CONTEXT_USER_FORM_INFO_PART.ts deleted file mode 100644 index a3c017e3fa..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/DATA_CONTEXT_USER_FORM_INFO_PART.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 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 { AuthRolesResource, UsersResource } from '@cloudbeaver/core-authentication'; -import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; -import { ServerConfigResource } from '@cloudbeaver/core-root'; -import { DATA_CONTEXT_FORM_STATE } from '@cloudbeaver/core-ui'; - -import type { AdministrationUserFormState } from '../AdministrationUserFormState'; -import { UserFormInfoPart } from './UserFormInfoPart'; - -export const DATA_CONTEXT_USER_FORM_INFO_PART = createDataContext('User Form Info Part', context => { - const form = context.get(DATA_CONTEXT_FORM_STATE) as AdministrationUserFormState; - const di = context.get(DATA_CONTEXT_DI_PROVIDER); - const usersResource = di.getServiceByClass(UsersResource); - const serverConfigResource = di.getServiceByClass(ServerConfigResource); - const authRolesResource = di.getServiceByClass(AuthRolesResource); - - return new UserFormInfoPart(authRolesResource, serverConfigResource, form, usersResource); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoPart.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoPart.ts index 0912862d4f..e4bcf5ba5f 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoPart.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoPart.ts @@ -17,7 +17,6 @@ import { isArraysEqual, isDefined, isObjectsEqual, isValuesEqual } from '@cloudb import { DATA_CONTEXT_LOADABLE_STATE } from '@cloudbeaver/core-view'; import type { IUserFormState } from '../AdministrationUserFormService'; -import type { AdministrationUserFormState } from '../AdministrationUserFormState'; import type { IUserFormInfoState } from './IUserFormInfoState'; const DEFAULT_ENABLED = true; @@ -27,7 +26,7 @@ export class UserFormInfoPart extends FormPart, private readonly usersResource: UsersResource, ) { super(formState, { @@ -140,7 +139,7 @@ export class UserFormInfoPart extends FormPart [ getCachedDataResourceLoaderState(this.serverConfigResource, () => undefined), diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoPartBootstrap.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoPartBootstrap.ts index 0d93f329f0..730c74b634 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoPartBootstrap.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoPartBootstrap.ts @@ -10,7 +10,7 @@ import React from 'react'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { AdministrationUserFormService } from '../AdministrationUserFormService'; -import { DATA_CONTEXT_USER_FORM_INFO_PART } from './DATA_CONTEXT_USER_FORM_INFO_PART'; +import { getUserFormInfoPart } from './getUserFormInfoPart'; const UserFormInfo = React.lazy(async () => { const { UserFormInfo } = await import('./UserFormInfo'); @@ -30,7 +30,7 @@ export class UserFormInfoPartBootstrap extends Bootstrap { title: 'authentication_administration_user_info', order: 1, panel: () => UserFormInfo, - stateGetter: props => () => props.formState.dataContext.get(DATA_CONTEXT_USER_FORM_INFO_PART), + stateGetter: props => () => getUserFormInfoPart(props.formState), }); } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/getUserFormInfoPart.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/getUserFormInfoPart.ts new file mode 100644 index 0000000000..64f3d4b7ae --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/getUserFormInfoPart.ts @@ -0,0 +1,27 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { AuthRolesResource, UsersResource } from '@cloudbeaver/core-authentication'; +import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; +import type { IFormState } from '@cloudbeaver/core-ui'; + +import type { IUserFormState } from '../AdministrationUserFormService'; +import { UserFormInfoPart } from './UserFormInfoPart'; + +const DATA_CONTEXT_USER_FORM_INFO_PART = createDataContext('User Form Info Part'); + +export function getUserFormInfoPart(formState: IFormState): UserFormInfoPart { + return formState.getPart(DATA_CONTEXT_USER_FORM_INFO_PART, context => { + const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; + const usersResource = di.getServiceByClass(UsersResource); + const serverConfigResource = di.getServiceByClass(ServerConfigResource); + const authRolesResource = di.getServiceByClass(AuthRolesResource); + + return new UserFormInfoPart(authRolesResource, serverConfigResource, formState, usersResource); + }); +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUserBootstrap.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUserBootstrap.ts index f8d555debb..3506881d68 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUserBootstrap.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUserBootstrap.ts @@ -48,7 +48,7 @@ export class CreateUserBootstrap extends Bootstrap { }, isDisabled: (context, action) => { if (action === ACTION_CREATE) { - const administrationItemRoute = context.tryGet(DATA_CONTEXT_ADMINISTRATION_ITEM_ROUTE); + const administrationItemRoute = context.get(DATA_CONTEXT_ADMINISTRATION_ITEM_ROUTE); return administrationItemRoute?.param === ADMINISTRATION_ITEM_USER_CREATE_PARAM && !!this.createUserService.state; } diff --git a/webapp/packages/plugin-authentication-administration/src/index.ts b/webapp/packages/plugin-authentication-administration/src/index.ts index 4ddde22274..8d392bcc29 100644 --- a/webapp/packages/plugin-authentication-administration/src/index.ts +++ b/webapp/packages/plugin-authentication-administration/src/index.ts @@ -1,3 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { manifest } from './manifest'; export default manifest; @@ -10,7 +17,8 @@ export * from './Administration/Users/UsersTable/CreateUserService'; export * from './Administration/Users/UsersAdministrationService'; export * from './Administration/Users/UserForm/AdministrationUserFormService'; export * from './Administration/Users/UserForm/AdministrationUserFormState'; -export * from './Administration/Users/UserForm/Info/DATA_CONTEXT_USER_FORM_INFO_PART'; +export * from './Administration/Users/UserForm/Info/getUserFormInfoPart'; +export * from './Administration/Users/UserForm/Info/UserFormInfoPart'; export * from './Administration/Users/UserForm/Info/UserFormInfoPartService'; export * from './Menus/MENU_USERS_ADMINISTRATION'; export * from './AdministrationUsersManagementService'; diff --git a/webapp/packages/plugin-connections/src/ContextMenu/ConnectionMenuBootstrap.ts b/webapp/packages/plugin-connections/src/ContextMenu/ConnectionMenuBootstrap.ts index 563c55f722..6db9e3610e 100644 --- a/webapp/packages/plugin-connections/src/ContextMenu/ConnectionMenuBootstrap.ts +++ b/webapp/packages/plugin-connections/src/ContextMenu/ConnectionMenuBootstrap.ts @@ -68,13 +68,13 @@ export class ConnectionMenuBootstrap extends Bootstrap { return false; } - const connection = context.tryGet(DATA_CONTEXT_CONNECTION); + const connection = context.get(DATA_CONTEXT_CONNECTION); if (!connection?.connected) { return false; } - const node = context.tryGet(DATA_CONTEXT_NAV_NODE); + const node = context.get(DATA_CONTEXT_NAV_NODE); if (node && !node.objectFeatures.includes(EObjectFeature.dataSource)) { return false; @@ -99,8 +99,9 @@ export class ConnectionMenuBootstrap extends Bootstrap { this.actionService.addHandler({ id: 'connection-view', actions: [ACTION_CONNECTION_VIEW_SIMPLE, ACTION_CONNECTION_VIEW_ADVANCED, ACTION_CONNECTION_VIEW_SYSTEM_OBJECTS], + contexts: [DATA_CONTEXT_CONNECTION], isChecked: (context, action) => { - const connection = context.get(DATA_CONTEXT_CONNECTION); + const connection = context.get(DATA_CONTEXT_CONNECTION)!; switch (action) { case ACTION_CONNECTION_VIEW_SIMPLE: { @@ -117,7 +118,7 @@ export class ConnectionMenuBootstrap extends Bootstrap { return false; }, handler: async (context, action) => { - const connection = context.get(DATA_CONTEXT_CONNECTION); + const connection = context.get(DATA_CONTEXT_CONNECTION)!; switch (action) { case ACTION_CONNECTION_VIEW_SIMPLE: { @@ -155,13 +156,14 @@ export class ConnectionMenuBootstrap extends Bootstrap { this.actionService.addHandler({ id: 'connection-management', + contexts: [DATA_CONTEXT_CONNECTION], isActionApplicable: (context, action) => { - const connection = context.tryGet(DATA_CONTEXT_CONNECTION); + const connection = context.get(DATA_CONTEXT_CONNECTION); if (!connection) { return false; } - const node = context.tryGet(DATA_CONTEXT_NAV_NODE); + const node = context.get(DATA_CONTEXT_NAV_NODE); if (node && !node.objectFeatures.includes(EObjectFeature.dataSource)) { return false; @@ -190,7 +192,7 @@ export class ConnectionMenuBootstrap extends Bootstrap { return false; }, isHidden: (context, action) => { - const connection = context.tryGet(DATA_CONTEXT_CONNECTION); + const connection = context.get(DATA_CONTEXT_CONNECTION); if (action === ACTION_CONNECTION_CHANGE_CREDENTIALS) { return !connection?.credentialsSaved; @@ -199,7 +201,7 @@ export class ConnectionMenuBootstrap extends Bootstrap { return false; }, getLoader: (context, action) => { - const connection = context.get(DATA_CONTEXT_CONNECTION); + const connection = context.get(DATA_CONTEXT_CONNECTION)!; if (action === ACTION_CONNECTION_CHANGE_CREDENTIALS) { return getCachedMapResourceLoaderState( @@ -213,7 +215,7 @@ export class ConnectionMenuBootstrap extends Bootstrap { return []; }, handler: async (context, action) => { - const connection = context.get(DATA_CONTEXT_CONNECTION); + const connection = context.get(DATA_CONTEXT_CONNECTION)!; switch (action) { case ACTION_CONNECTION_DISCONNECT: { diff --git a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts index db45b36ba5..ba67aab8f5 100644 --- a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts +++ b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts @@ -122,10 +122,11 @@ export class ConnectionFoldersBootstrap extends Bootstrap { this.actionService.addHandler({ id: 'tree-tools-menu-folders-handler', + contexts: [DATA_CONTEXT_ELEMENTS_TREE], isActionApplicable: (context, action) => { - const tree = context.tryGet(DATA_CONTEXT_ELEMENTS_TREE); + const tree = context.get(DATA_CONTEXT_ELEMENTS_TREE)!; - if (action !== ACTION_NEW_FOLDER || !tree || !this.userInfoResource.data || tree.baseRoot !== ROOT_NODE_PATH) { + if (action !== ACTION_NEW_FOLDER || !this.userInfoResource.data || tree.baseRoot !== ROOT_NODE_PATH) { return false; } @@ -134,7 +135,7 @@ export class ConnectionFoldersBootstrap extends Bootstrap { return targetNode !== undefined; }, // isDisabled: (context, action) => { - // const tree = context.tryGet(DATA_CONTEXT_ELEMENTS_TREE); + // const tree = context.get(DATA_CONTEXT_ELEMENTS_TREE); // if (!tree) { // return true; diff --git a/webapp/packages/plugin-data-export/src/DataExportMenuService.ts b/webapp/packages/plugin-data-export/src/DataExportMenuService.ts index 2505755ed9..a488fb5c6e 100644 --- a/webapp/packages/plugin-data-export/src/DataExportMenuService.ts +++ b/webapp/packages/plugin-data-export/src/DataExportMenuService.ts @@ -13,11 +13,13 @@ import { LocalizationService } from '@cloudbeaver/core-localization'; import { DATA_CONTEXT_NAV_NODE, EObjectFeature } from '@cloudbeaver/core-navigation-tree'; import { EAdminPermission, SessionPermissionsResource } from '@cloudbeaver/core-root'; import { withTimestamp } from '@cloudbeaver/core-utils'; -import { ACTION_EXPORT, ActionService, DATA_CONTEXT_MENU, menuExtractItems, MenuService } from '@cloudbeaver/core-view'; +import { ACTION_EXPORT, ActionService, menuExtractItems, MenuService } from '@cloudbeaver/core-view'; import { DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX, + DATA_CONTEXT_DV_PRESENTATION, DATA_VIEWER_DATA_MODEL_ACTIONS_MENU, + DataViewerPresentationType, IDatabaseDataSource, IDataContainerOptions, } from '@cloudbeaver/plugin-data-viewer'; @@ -39,22 +41,29 @@ export class DataExportMenuService { ) {} register(): void { + this.menuService.addCreator({ + menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + isApplicable: context => { + const presentation = context.get(DATA_CONTEXT_DV_PRESENTATION); + return !this.isExportDisabled() && (!presentation || presentation.type === DataViewerPresentationType.Data); + }, + getItems(context, items) { + return [...items, ACTION_EXPORT]; + }, + orderItems(context, items) { + const extracted = menuExtractItems(items, [ACTION_EXPORT]); + return [...items, ...extracted]; + }, + }); this.actionService.addHandler({ id: 'data-export-base-handler', - isActionApplicable(context, action) { - const menu = context.hasValue(DATA_CONTEXT_MENU, DATA_VIEWER_DATA_MODEL_ACTIONS_MENU); - const model = context.tryGet(DATA_CONTEXT_DV_DDM); - const resultIndex = context.tryGet(DATA_CONTEXT_DV_DDM_RESULT_INDEX); - - if (!menu || !model || resultIndex === undefined) { - return false; - } - - return [ACTION_EXPORT].includes(action); - }, + menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + actions: [ACTION_EXPORT], isDisabled(context) { - const model = context.get(DATA_CONTEXT_DV_DDM); - const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX); + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; return model.isLoading() || model.isDisabled(resultIndex) || !model.getResult(resultIndex); }, @@ -66,8 +75,8 @@ export class DataExportMenuService { return action.info; }, handler: (context, action) => { - const model = context.get(DATA_CONTEXT_DV_DDM); - const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX); + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; if (action === ACTION_EXPORT) { const result = model.getResult(resultIndex); @@ -98,24 +107,14 @@ export class DataExportMenuService { } }, }); - this.menuService.addCreator({ - menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], - isApplicable: () => !this.isExportDisabled(), - getItems(context, items) { - return [...items, ACTION_EXPORT]; - }, - orderItems(context, items) { - const extracted = menuExtractItems(items, [ACTION_EXPORT]); - return [...items, ...extracted]; - }, - }); this.menuService.addCreator({ root: true, + contexts: [DATA_CONTEXT_NAV_NODE], isApplicable: context => { - const node = context.tryGet(DATA_CONTEXT_NAV_NODE); + const node = context.get(DATA_CONTEXT_NAV_NODE)!; - if (node && !node.objectFeatures.includes(EObjectFeature.dataContainer)) { + if (!node.objectFeatures.includes(EObjectFeature.dataContainer)) { return false; } @@ -129,15 +128,15 @@ export class DataExportMenuService { actions: [ACTION_EXPORT], contexts: [DATA_CONTEXT_CONNECTION, DATA_CONTEXT_NAV_NODE], handler: async context => { - const node = context.get(DATA_CONTEXT_NAV_NODE); - const connection = context.get(DATA_CONTEXT_CONNECTION); - const fileName = withTimestamp(`${connection.name}${node?.name ? ` - ${node.name}` : ''}`); + const node = context.get(DATA_CONTEXT_NAV_NODE)!; + const connection = context.get(DATA_CONTEXT_CONNECTION)!; + const fileName = withTimestamp(`${connection.name}${node.name ? ` - ${node.name}` : ''}`); this.commonDialogService.open(DataExportDialog, { connectionKey: createConnectionParam(connection), - name: node?.name, + name: node.name, fileName, - containerNodePath: node?.id, + containerNodePath: node.id, }); }, }); diff --git a/webapp/packages/plugin-data-import/src/DataImportBootstrap.ts b/webapp/packages/plugin-data-import/src/DataImportBootstrap.ts index e93e016053..8c6d556735 100644 --- a/webapp/packages/plugin-data-import/src/DataImportBootstrap.ts +++ b/webapp/packages/plugin-data-import/src/DataImportBootstrap.ts @@ -7,12 +7,14 @@ */ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; -import { ACTION_IMPORT, ActionService, DATA_CONTEXT_MENU, menuExtractItems, MenuService } from '@cloudbeaver/core-view'; +import { ACTION_IMPORT, ActionService, menuExtractItems, MenuService } from '@cloudbeaver/core-view'; import { ContainerDataSource, DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX, + DATA_CONTEXT_DV_PRESENTATION, DATA_VIEWER_DATA_MODEL_ACTIONS_MENU, + DataViewerPresentationType, } from '@cloudbeaver/plugin-data-viewer'; import { DataImportDialogLazy } from './DataImportDialog/DataImportDialogLazy'; @@ -32,25 +34,11 @@ export class DataImportBootstrap extends Bootstrap { register() { this.actionService.addHandler({ id: 'data-import-base-handler', - isActionApplicable(context, action) { - const menu = context.hasValue(DATA_CONTEXT_MENU, DATA_VIEWER_DATA_MODEL_ACTIONS_MENU); - const model = context.tryGet(DATA_CONTEXT_DV_DDM); - const resultIndex = context.tryGet(DATA_CONTEXT_DV_DDM_RESULT_INDEX); - - if (!menu || !model || resultIndex === undefined) { - return false; - } - - if (action === ACTION_IMPORT) { - const isContainer = model.source instanceof ContainerDataSource; - return !model.isReadonly(resultIndex) && isContainer; - } - - return [ACTION_IMPORT].includes(action); - }, + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + actions: [ACTION_IMPORT], isDisabled(context) { - const model = context.get(DATA_CONTEXT_DV_DDM); - const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX); + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; return model.isLoading() || model.isDisabled(resultIndex) || !model.getResult(resultIndex); }, @@ -62,8 +50,8 @@ export class DataImportBootstrap extends Bootstrap { return action.info; }, handler: async (context, action) => { - const model = context.get(DATA_CONTEXT_DV_DDM); - const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX); + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; if (action === ACTION_IMPORT) { const result = model.getResult(resultIndex); @@ -102,7 +90,20 @@ export class DataImportBootstrap extends Bootstrap { this.menuService.addCreator({ menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], - isApplicable: () => !this.dataImportService.disabled, + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + isApplicable: context => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const presentation = context.get(DATA_CONTEXT_DV_PRESENTATION); + const isContainer = model.source instanceof ContainerDataSource; + return ( + !model.isReadonly(resultIndex) && + isContainer && + !this.dataImportService.disabled && + !presentation?.readonly && + (!presentation || presentation.type === DataViewerPresentationType.Data) + ); + }, getItems(_, items) { return [...items, ACTION_IMPORT]; }, diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx index 20f916b1f0..6e814c5460 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx @@ -13,9 +13,12 @@ import { useService } from '@cloudbeaver/core-di'; import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events'; import { Executor } from '@cloudbeaver/core-executor'; import { ClipboardService } from '@cloudbeaver/core-ui'; +import { useCaptureViewContext } from '@cloudbeaver/core-view'; import { + DATA_CONTEXT_DV_PRESENTATION, DatabaseDataSelectActionsData, DatabaseEditChangeType, + DataViewerPresentationType, IDatabaseResultSet, IDataPresentationProps, IResultSetEditActionData, @@ -187,6 +190,10 @@ export const DataGridTable = observer { + context.set(DATA_CONTEXT_DV_PRESENTATION, { type: DataViewerPresentationType.Data }, id); + }); + function handleKeyDown(event: React.KeyboardEvent) { gridSelectedCellCopy.onKeydownHandler(event); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/useTableColumnDnD.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/useTableColumnDnD.ts index c8bb6ae290..dc3556d042 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/useTableColumnDnD.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/useTableColumnDnD.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ import { useCombinedRef } from '@cloudbeaver/core-blocks'; -import { useDataContext } from '@cloudbeaver/core-data-context'; +import { useDataContext, useDataContextLink } from '@cloudbeaver/core-data-context'; import { IDNDBox, IDNDData, useDNDBox, useDNDData } from '@cloudbeaver/core-ui'; import { DATA_CONTEXT_DV_DDM, @@ -30,9 +30,11 @@ export function useTableColumnDnD(model: IDatabaseDataModel, resultIndex: number const context = useDataContext(); const resultSetViewAction = model.source.tryGetAction(resultIndex, ResultSetViewAction); - context.set(DATA_CONTEXT_DV_DDM, model); - context.set(DATA_CONTEXT_DV_DDM_RESULT_INDEX, resultIndex); - context.set(DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY, columnKey); + useDataContextLink(context, (context, id) => { + context.set(DATA_CONTEXT_DV_DDM, model, id); + context.set(DATA_CONTEXT_DV_DDM_RESULT_INDEX, resultIndex, id); + context.set(DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY, columnKey, id); + }); const dndData = useDNDData(context, { canDrag: () => !model.isDisabled(resultIndex), @@ -60,7 +62,7 @@ export function useTableColumnDnD(model: IDatabaseDataModel, resultIndex: number let side: TableColumnInsertPositionSide = null; if (columnKey && dndBox.state.isOver && dndBox.state.context) { - const dndColumnKey = dndBox.state.context.tryGet(DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY); + const dndColumnKey = dndBox.state.context.get(DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY); if (resultSetViewAction && dndColumnKey && resultSetViewAction.columnIndex(columnKey) > resultSetViewAction.columnIndex(dndColumnKey)) { side = 'right'; diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPluginBootstrap.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPluginBootstrap.ts index 74948b81df..503c769889 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPluginBootstrap.ts +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPluginBootstrap.ts @@ -13,9 +13,11 @@ import { ActionService, MenuService } from '@cloudbeaver/core-view'; import { DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX, + DATA_CONTEXT_DV_PRESENTATION, DATA_VIEWER_DATA_MODEL_ACTIONS_MENU, DataPresentationService, DataPresentationType, + DataViewerPresentationType, ResultSetDataAction, ResultSetSelectAction, } from '@cloudbeaver/plugin-data-viewer'; @@ -60,8 +62,23 @@ export class DVResultSetGroupingPluginBootstrap extends Bootstrap { ], contexts: [DATA_CONTEXT_DV_DDM_RS_GROUPING], menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], + isActionApplicable(context, action) { + const presentation = context.get(DATA_CONTEXT_DV_PRESENTATION); + if (presentation && presentation.type !== DataViewerPresentationType.Data) { + return false; + } + switch (action) { + case ACTION_DATA_VIEWER_GROUPING_REMOVE_COLUMN: + return context.has(DATA_CONTEXT_DV_DDM) && context.has(DATA_CONTEXT_DV_DDM_RESULT_INDEX); + case ACTION_DATA_VIEWER_GROUPING_CLEAR: + case ACTION_DATA_VIEWER_GROUPING_CONFIGURE: + case ACTION_DATA_VIEWER_GROUPING_SHOW_DUPLICATES: + return true; + } + return false; + }, getActionInfo(context, action) { - const grouping = context.get(DATA_CONTEXT_DV_DDM_RS_GROUPING); + const grouping = context.get(DATA_CONTEXT_DV_DDM_RS_GROUPING)!; const isShowDuplicatesOnly = grouping.getShowDuplicatesOnly(); if (action === ACTION_DATA_VIEWER_GROUPING_SHOW_DUPLICATES && isShowDuplicatesOnly) { @@ -76,14 +93,14 @@ export class DVResultSetGroupingPluginBootstrap extends Bootstrap { return action.info; }, isDisabled(context, action) { - const grouping = context.get(DATA_CONTEXT_DV_DDM_RS_GROUPING); - const model = context.get(DATA_CONTEXT_DV_DDM); - const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX); + const grouping = context.get(DATA_CONTEXT_DV_DDM_RS_GROUPING)!; switch (action) { case ACTION_DATA_VIEWER_GROUPING_CLEAR: return grouping.getColumns().length === 0; case ACTION_DATA_VIEWER_GROUPING_REMOVE_COLUMN: { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; if (!model.source.hasResult(resultIndex)) { return true; } @@ -112,15 +129,15 @@ export class DVResultSetGroupingPluginBootstrap extends Bootstrap { return false; }, handler: async (context, action) => { - const grouping = context.get(DATA_CONTEXT_DV_DDM_RS_GROUPING); - const model = context.get(DATA_CONTEXT_DV_DDM); - const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX); + const grouping = context.get(DATA_CONTEXT_DV_DDM_RS_GROUPING)!; switch (action) { case ACTION_DATA_VIEWER_GROUPING_CLEAR: grouping.clear(); break; case ACTION_DATA_VIEWER_GROUPING_REMOVE_COLUMN: { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; const selectionAction = model.source.getAction(resultIndex, ResultSetSelectAction); const dataAction = model.source.getAction(resultIndex, ResultSetDataAction); diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPresentation.tsx b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPresentation.tsx index 3f3f8bf2d2..953892e45e 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPresentation.tsx +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPresentation.tsx @@ -6,26 +6,20 @@ * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { useContext, useState } from 'react'; +import { useState } from 'react'; import { s, useS, useTranslate } from '@cloudbeaver/core-blocks'; -import { useDataContext } from '@cloudbeaver/core-data-context'; import { useTabLocalState } from '@cloudbeaver/core-ui'; -import { CaptureViewContext } from '@cloudbeaver/core-view'; +import { CaptureViewScope } from '@cloudbeaver/core-view'; import { DataPresentationComponent, IDatabaseResultSet, TableViewerLoader } from '@cloudbeaver/plugin-data-viewer'; -import { DATA_CONTEXT_DV_DDM_RS_GROUPING } from './DataContext/DATA_CONTEXT_DV_DDM_RS_GROUPING'; import { DEFAULT_GROUPING_QUERY_OPERATION } from './DEFAULT_GROUPING_QUERY_OPERATION'; import styles from './DVResultSetGroupingPresentation.module.css'; -import type { IGroupingQueryState } from './IGroupingQueryState'; -import { useGroupingData } from './useGroupingData'; +import { DVResultSetGroupingPresentationContext } from './DVResultSetGroupingPresentationContext'; +import type { IDVResultSetGroupingPresentationState } from './IDVResultSetGroupingPresentationState'; import { useGroupingDataModel } from './useGroupingDataModel'; import { useGroupingDnDColumns } from './useGroupingDnDColumns'; -export interface IDVResultSetGroupingPresentationState extends IGroupingQueryState { - presentationId: string; -} - export const DVResultSetGroupingPresentation: DataPresentationComponent = observer(function DVResultSetGroupingPresentation({ model: originalModel, resultIndex, @@ -38,27 +32,22 @@ export const DVResultSetGroupingPresentation: DataPresentationComponent(null); const model = useGroupingDataModel(originalModel, resultIndex, state); const dnd = useGroupingDnDColumns(state, originalModel, model); - const grouping = useGroupingData(state); - - context.set(DATA_CONTEXT_DV_DDM_RS_GROUPING, grouping); - return ( - <> + +
)}
- + ); }); diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPresentationContext.tsx b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPresentationContext.tsx new file mode 100644 index 0000000000..9595c7bc76 --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPresentationContext.tsx @@ -0,0 +1,28 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { useCaptureViewContext } from '@cloudbeaver/core-view'; + +import { DATA_CONTEXT_DV_DDM_RS_GROUPING } from './DataContext/DATA_CONTEXT_DV_DDM_RS_GROUPING'; +import type { IDVResultSetGroupingPresentationState } from './IDVResultSetGroupingPresentationState'; +import { useGroupingData } from './useGroupingData'; + +interface Props { + state: IDVResultSetGroupingPresentationState; +} + +export const DVResultSetGroupingPresentationContext = observer(function DVResultSetGroupingPresentationContext({ state }) { + const grouping = useGroupingData(state); + + useCaptureViewContext((context, id) => { + context.set(DATA_CONTEXT_DV_DDM_RS_GROUPING, grouping, id); + }); + + return null; +}); diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/IDVResultSetGroupingPresentationState.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/IDVResultSetGroupingPresentationState.ts new file mode 100644 index 0000000000..f22e795cbb --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/IDVResultSetGroupingPresentationState.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { IGroupingQueryState } from './IGroupingQueryState'; + +export interface IDVResultSetGroupingPresentationState extends IGroupingQueryState { + presentationId: string; +} diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/useGroupingData.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/useGroupingData.ts index 7f5524b6bc..7ae87f3162 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/useGroupingData.ts +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/useGroupingData.ts @@ -1,10 +1,17 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ import { action } from 'mobx'; import { useObservableRef } from '@cloudbeaver/core-blocks'; import type { IResultSetGroupingData } from './DataContext/DATA_CONTEXT_DV_DDM_RS_GROUPING'; -import type { IDVResultSetGroupingPresentationState } from './DVResultSetGroupingPresentation'; import { DEFAULT_GROUPING_QUERY_OPERATION } from './DEFAULT_GROUPING_QUERY_OPERATION'; +import type { IDVResultSetGroupingPresentationState } from './IDVResultSetGroupingPresentationState'; export interface IPrivateGroupingData extends IResultSetGroupingData { state: IDVResultSetGroupingPresentationState; diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/useGroupingDnDColumns.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/useGroupingDnDColumns.ts index 9b715ffffe..7723c7ecf3 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/useGroupingDnDColumns.ts +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/useGroupingDnDColumns.ts @@ -59,14 +59,14 @@ export function useGroupingDnDColumns( const dndBox = useDNDBox({ canDrop: context => { - const model = context.tryGet(DATA_CONTEXT_DV_DDM); + const model = context.get(DATA_CONTEXT_DV_DDM); return context.has(DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY) && model === sourceModel; }, onDrop: async context => { - const model = context.get(DATA_CONTEXT_DV_DDM); - const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX); - const columnKey = context.get(DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY); + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const columnKey = context.get(DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY)!; dropItem(model, resultIndex, columnKey, false); }, @@ -74,14 +74,14 @@ export function useGroupingDnDColumns( const dndThrowBox = useDNDBox({ canDrop: context => { - const model = context.tryGet(DATA_CONTEXT_DV_DDM); + const model = context.get(DATA_CONTEXT_DV_DDM); return context.has(DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY) && model?.id === groupingModel.model.id; }, onDrop: async context => { - const model = context.get(DATA_CONTEXT_DV_DDM); - const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX); - const columnKey = context.get(DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY); + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const columnKey = context.get(DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY)!; dropItem(model, resultIndex, columnKey, true); }, diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerBootstrap.ts b/webapp/packages/plugin-data-viewer/src/DataViewerBootstrap.ts index e35b95a47c..edc8b5e7c8 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerBootstrap.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerBootstrap.ts @@ -8,17 +8,23 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { DataViewerTabService } from './DataViewerTabService'; +import { ResultSetTableFooterMenuService } from './ResultSet/ResultSetTableFooterMenuService'; +import { TableFooterMenuService } from './TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService'; @injectable() export class DataViewerBootstrap extends Bootstrap { - constructor(private readonly dataViewerTabService: DataViewerTabService) { + constructor( + private readonly dataViewerTabService: DataViewerTabService, + private readonly tableFooterMenuService: TableFooterMenuService, + private readonly resultSetTableFooterMenuService: ResultSetTableFooterMenuService, + ) { super(); } register(): void | Promise { this.dataViewerTabService.registerTabHandler(); this.dataViewerTabService.register(); + this.tableFooterMenuService.register(); + this.resultSetTableFooterMenuService.register(); } - - load(): void {} } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.ts index 32a8edbc4f..8e38c814d3 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.ts @@ -532,7 +532,7 @@ export class ResultSetEditAction extends DatabaseEditAction('data-viewer-presentation'); diff --git a/webapp/packages/plugin-data-viewer/src/ResultSet/ACTION_COUNT_TOTAL_ELEMENTS.ts b/webapp/packages/plugin-data-viewer/src/ResultSet/ACTION_COUNT_TOTAL_ELEMENTS.ts new file mode 100644 index 0000000000..063b5809f8 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ResultSet/ACTION_COUNT_TOTAL_ELEMENTS.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_COUNT_TOTAL_ELEMENTS = createAction('data-count-total-elements', { + label: 'ui_count_total_elements', + tooltip: 'data_viewer_total_count_tooltip', + icon: '/icons/data_row_count.svg', +}); diff --git a/webapp/packages/plugin-data-viewer/src/ResultSet/ResultSetTableFooterMenuService.ts b/webapp/packages/plugin-data-viewer/src/ResultSet/ResultSetTableFooterMenuService.ts new file mode 100644 index 0000000000..132ecb52d1 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ResultSet/ResultSetTableFooterMenuService.ts @@ -0,0 +1,185 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { IDataContextProvider } from '@cloudbeaver/core-data-context'; +import { injectable } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { ResultDataFormat } from '@cloudbeaver/core-sdk'; +import { ActionService, menuExtractItems, MenuService } from '@cloudbeaver/core-view'; + +import { DatabaseMetadataAction } from '../DatabaseDataModel/Actions/DatabaseMetadataAction'; +import { ResultSetConstraintAction } from '../DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction'; +import { DATA_CONTEXT_DV_DDM } from '../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM'; +import { DATA_CONTEXT_DV_DDM_RESULT_INDEX } from '../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM_RESULT_INDEX'; +import { DATA_VIEWER_DATA_MODEL_ACTIONS_MENU } from '../TableViewer/TableFooter/TableFooterMenu/DATA_VIEWER_DATA_MODEL_ACTIONS_MENU'; +import { ACTION_COUNT_TOTAL_ELEMENTS } from './ACTION_COUNT_TOTAL_ELEMENTS'; + +interface IResultSetActionsMetadata { + totalCount: { + loading: boolean; + }; +} + +@injectable() +export class ResultSetTableFooterMenuService { + constructor( + private readonly actionService: ActionService, + private readonly menuService: MenuService, + private readonly notificationService: NotificationService, + ) {} + + register() { + this.menuService.addCreator({ + menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + isApplicable(context) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const result = model.getResult(resultIndex); + + return !!result && result.dataFormat === ResultDataFormat.Resultset; + }, + getItems(context, items) { + return [ACTION_COUNT_TOTAL_ELEMENTS, ...items]; + }, + orderItems(context, items) { + const extracted = menuExtractItems(items, [ACTION_COUNT_TOTAL_ELEMENTS]); + return [...extracted, ...items]; + }, + }); + this.actionService.addHandler({ + id: 'result-set-data-base-handler', + menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], + actions: [ACTION_COUNT_TOTAL_ELEMENTS], + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + isActionApplicable(context, action) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const result = model.getResult(resultIndex); + + if (!result || result.dataFormat !== ResultDataFormat.Resultset) { + return false; + } + const constraint = model.source.tryGetAction(resultIndex, ResultSetConstraintAction); + + switch (action) { + case ACTION_COUNT_TOTAL_ELEMENTS: { + return !!constraint?.supported; + } + } + return true; + }, + isDisabled: (context, action) => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + + if (model.isLoading() || model.isDisabled(resultIndex) || !model.getResult(resultIndex)) { + return true; + } + + switch (action) { + case ACTION_COUNT_TOTAL_ELEMENTS: { + const metadata = this.getState(context); + + return metadata.totalCount.loading && Boolean(model.source.totalCountRequestTask?.cancelled); + } + } + + return false; + }, + isLoading: (context, action) => { + const metadata = this.getState(context); + + switch (action) { + case ACTION_COUNT_TOTAL_ELEMENTS: { + return metadata.totalCount.loading; + } + } + + return false; + }, + getActionInfo: (context, action) => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const metadata = this.getState(context); + + switch (action) { + case ACTION_COUNT_TOTAL_ELEMENTS: { + const result = model.getResult(resultIndex); + if (!result) { + return action.info; + } + + let label = action.info.label; + let icon = action.info.icon; + + if (metadata.totalCount.loading) { + const cancelling = Boolean(model.source.totalCountRequestTask?.cancelled); + label = cancelling ? 'ui_processing_canceling' : 'ui_processing_cancel'; + icon = 'cross'; + } else { + const currentCount = result.loadedFully ? result.count : `${result.count}+`; + label = String(result.totalCount ?? currentCount); + } + + return { ...action.info, label, icon }; + } + } + + return action.info; + }, + handler: async (context, action) => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const metadata = this.getState(context); + + switch (action) { + case ACTION_COUNT_TOTAL_ELEMENTS: { + if (metadata.totalCount.loading) { + if (model.source.totalCountRequestTask?.cancelled) { + // Cancel request + return; + } + + try { + await model.source.cancelLoadTotalCount(); + } catch (e: any) { + if (!model.source.totalCountRequestTask?.cancelled) { + this.notificationService.logException(e); + } + } + } else { + try { + metadata.totalCount.loading = true; + await model.source.loadTotalCount(resultIndex); + } catch (exception: any) { + if (model.source.totalCountRequestTask?.cancelled) { + this.notificationService.logInfo({ + title: 'data_viewer_total_count_canceled_title', + message: 'data_viewer_total_count_canceled_message', + }); + } else { + this.notificationService.logException(exception, 'data_viewer_total_count_failed'); + } + } finally { + metadata.totalCount.loading = false; + } + } + break; + } + } + }, + }); + } + + private getState(context: IDataContextProvider) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const metadataAction = model.source.getAction(resultIndex, DatabaseMetadataAction); + return metadataAction.get('result-set-database-metadata', () => ({ totalCount: { loading: false } })); + } +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableGrid.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/DataPresentation.module.css similarity index 100% rename from webapp/packages/plugin-data-viewer/src/TableViewer/TableGrid.module.css rename to webapp/packages/plugin-data-viewer/src/TableViewer/DataPresentation.module.css diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableGrid.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/DataPresentation.tsx similarity index 87% rename from webapp/packages/plugin-data-viewer/src/TableViewer/TableGrid.tsx rename to webapp/packages/plugin-data-viewer/src/TableViewer/DataPresentation.tsx index 39975fe007..845d5f0f29 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableGrid.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/DataPresentation.tsx @@ -12,8 +12,8 @@ import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel'; import type { IDataPresentationOptions } from '../DataPresentationService'; +import styles from './DataPresentation.module.css'; import type { IDataTableActions } from './IDataTableActions'; -import styles from './TableGrid.module.css'; import { TableStatistics } from './TableStatistics'; interface Props { @@ -26,7 +26,15 @@ interface Props { isStatistics: boolean; } -export const TableGrid = observer(function TableGrid({ model, actions, dataFormat, presentation, resultIndex, simple, isStatistics }) { +export const DataPresentation = observer(function DataPresentation({ + model, + actions, + dataFormat, + presentation, + resultIndex, + simple, + isStatistics, +}) { if ((presentation.dataFormat !== undefined && dataFormat !== presentation.dataFormat) || !model.source.hasResult(resultIndex)) { if (model.isLoading()) { return null; diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/DataViewerViewService.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/DataViewerViewService.ts new file mode 100644 index 0000000000..4b1f1c89ae --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/DataViewerViewService.ts @@ -0,0 +1,22 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { ACTION_REDO, ACTION_SAVE, ACTION_UNDO, IActiveView, View } from '@cloudbeaver/core-view'; +import { ITab, NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; + +@injectable() +export class DataViewerViewService extends View { + constructor(private readonly navigationTabsService: NavigationTabsService) { + super(); + this.registerAction(ACTION_UNDO, ACTION_REDO, ACTION_SAVE); + } + + getView(): IActiveView | null { + return this.navigationTabsService.getView(); + } +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/CancelTotalCountAction.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/CancelTotalCountAction.module.css deleted file mode 100644 index 8531a5867f..0000000000 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/CancelTotalCountAction.module.css +++ /dev/null @@ -1,27 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -.action { - composes: theme-ripple from global; - padding: 0 8px; - cursor: pointer; - user-select: none; -} - -.action .loader { - margin-right: 8px; -} - -.icon { - composes: theme-text-error from global; -} - -.cancelText { - text-transform: uppercase; - font-weight: 700; - font-size: 12px; -} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/CancelTotalCountAction.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/CancelTotalCountAction.tsx deleted file mode 100644 index cf78954c9d..0000000000 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/CancelTotalCountAction.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 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 { Container, IconButton, Loader, s, useS, useTranslate } from '@cloudbeaver/core-blocks'; - -import styles from './CancelTotalCountAction.module.css'; - -interface Props { - onClick: VoidFunction; - loading: boolean; -} - -export const CancelTotalCountAction = observer(function CancelTotalCountAction({ onClick, loading }) { - const translate = useTranslate(); - const style = useS(styles); - - function handleClick() { - if (loading) { - return; - } - - onClick(); - } - - return ( - - {loading && } - {!loading && } - {translate('ui_processing_cancel')} - - ); -}); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx index ba8a749da4..ec0345eea7 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx @@ -9,25 +9,21 @@ import { observer } from 'mobx-react-lite'; import { useCallback, useEffect, useRef, useState } from 'react'; import { Form, getComputed, s, ToolsPanel, useS } from '@cloudbeaver/core-blocks'; -import type { IDataContext } from '@cloudbeaver/core-data-context'; import { useService } from '@cloudbeaver/core-di'; -import { ResultSetConstraintAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction'; import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; import { DataViewerSettingsService } from '../../DataViewerSettingsService'; import { AutoRefreshButton } from './AutoRefresh/AutoRefreshButton'; import styles from './TableFooter.module.css'; import { TableFooterMenu } from './TableFooterMenu/TableFooterMenu'; -import { TableFooterRowCount } from './TableFooterRowCount'; interface Props { resultIndex: number; model: IDatabaseDataModel; simple: boolean; - context?: IDataContext; } -export const TableFooter = observer(function TableFooter({ resultIndex, model, simple, context }) { +export const TableFooter = observer(function TableFooter({ resultIndex, model, simple }) { const ref = useRef(null); const [limit, setLimit] = useState(model.countGain + ''); const dataViewerSettingsService = useService(DataViewerSettingsService); @@ -53,7 +49,6 @@ export const TableFooter = observer(function TableFooter({ resultIndex, m }, [model.countGain]); const disabled = getComputed(() => model.isLoading() || model.isDisabled(resultIndex)); - const constraint = model.getResult(resultIndex) ? model.source.getAction(resultIndex, ResultSetConstraintAction) : null; return ( @@ -73,8 +68,7 @@ export const TableFooter = observer(function TableFooter({ resultIndex, m />
- {constraint?.supported && } - + {model.source.requestInfo.requestMessage.length > 0 && (
{model.source.requestInfo.requestMessage} - {model.source.requestInfo.requestDuration}ms diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx index a0ca1f4223..4dea3a5969 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx @@ -7,10 +7,9 @@ */ import { observer } from 'mobx-react-lite'; -import { s, useS } from '@cloudbeaver/core-blocks'; -import type { IDataContext } from '@cloudbeaver/core-data-context'; -import { useService } from '@cloudbeaver/core-di'; -import { MenuBar } from '@cloudbeaver/core-ui'; +import { s, SContext, StyleRegistry, useS } from '@cloudbeaver/core-blocks'; +import { useDataContextLink } from '@cloudbeaver/core-data-context'; +import { MenuBar, MenuBarItemStyles } from '@cloudbeaver/core-ui'; import { useMenu } from '@cloudbeaver/core-view'; import { DATA_CONTEXT_DV_DDM } from '../../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM'; @@ -19,32 +18,32 @@ import type { IDatabaseDataModel } from '../../../DatabaseDataModel/IDatabaseDat import { DATA_CONTEXT_DATA_VIEWER_SIMPLE } from '../../TableHeader/DATA_CONTEXT_DATA_VIEWER_SIMPLE'; import { DATA_VIEWER_DATA_MODEL_ACTIONS_MENU } from './DATA_VIEWER_DATA_MODEL_ACTIONS_MENU'; import style from './TableFooterMenu.module.css'; -import { TableFooterMenuItem } from './TableFooterMenuItem'; -import { TableFooterMenuService } from './TableFooterMenuService'; +import tableFooterMenuBarItemStyles from './TableFooterMenuBarItemStyles.module.css'; interface Props { resultIndex: number; model: IDatabaseDataModel; simple: boolean; - context?: IDataContext; className?: string; } -export const TableFooterMenu = observer(function TableFooterMenu({ resultIndex, model, simple, context, className }) { - const mainMenuService = useService(TableFooterMenuService); - const styles = useS(style); - const menu = useMenu({ menu: DATA_VIEWER_DATA_MODEL_ACTIONS_MENU, context }); +const registry: StyleRegistry = [[MenuBarItemStyles, { mode: 'append', styles: [tableFooterMenuBarItemStyles] }]]; - menu.context.set(DATA_CONTEXT_DV_DDM, model); - menu.context.set(DATA_CONTEXT_DV_DDM_RESULT_INDEX, resultIndex); - menu.context.set(DATA_CONTEXT_DATA_VIEWER_SIMPLE, simple); +export const TableFooterMenu = observer(function TableFooterMenu({ resultIndex, model, simple, className }) { + const styles = useS(style, tableFooterMenuBarItemStyles); + const menu = useMenu({ menu: DATA_VIEWER_DATA_MODEL_ACTIONS_MENU }); + + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_DV_DDM, model, id); + context.set(DATA_CONTEXT_DV_DDM_RESULT_INDEX, resultIndex, id); + context.set(DATA_CONTEXT_DATA_VIEWER_SIMPLE, simple, id); + }); return ( -
- {mainMenuService.constructMenuWithContext(model, resultIndex, simple).map((topItem, i) => ( - - ))} - +
+ + +
); }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuBarItemStyles.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuBarItemStyles.module.css new file mode 100644 index 0000000000..afd5437529 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuBarItemStyles.module.css @@ -0,0 +1,19 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.tableFooterMenu .menuBarItem { + padding: 0 4px; + + & .menuBarItemLabel { + padding: 0; + } + + & .menuBarItemIcon + .menuBarItemLabel { + padding-left: 0; + } +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuItem.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuItem.module.css deleted file mode 100644 index 48856c38ff..0000000000 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuItem.module.css +++ /dev/null @@ -1,35 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ - -.menu { - composes: theme-text-on-surface from global; -} -.menuTrigger { - composes: theme-ripple from global; - height: 100%; - padding: 0 12px; - display: flex; - align-items: center; - cursor: pointer; - &.hidden { - display: none; - } -} -.toolsAction.hidden { - display: none; -} -.menuTriggerIcon .iconOrImage { - display: block; - width: 16px; -} -.menuTriggerTitle { - display: block; -} -.menuTriggerIcon + .menuTriggerTitle { - padding-left: 8px; -} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuItem.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuItem.tsx deleted file mode 100644 index 98970f3006..0000000000 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuItem.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 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 type { ButtonHTMLAttributes } from 'react'; - -import { - IconOrImage, - MenuPanelItemAndTriggerStyles, - MenuTrigger, - s, - SContext, - StyleRegistry, - ToolsAction, - useS, - useTranslate, -} from '@cloudbeaver/core-blocks'; -import type { IMenuItem } from '@cloudbeaver/core-dialogs'; - -import styles from './TableFooterMenuItem.module.css'; - -type Props = ButtonHTMLAttributes & { - menuItem: IMenuItem; -}; - -const registry: StyleRegistry = [ - [ - MenuPanelItemAndTriggerStyles, - { - mode: 'append', - styles: [styles], - }, - ], -]; - -export const TableFooterMenuItem = observer(function TableFooterMenuItem({ menuItem, ...props }) { - const translate = useTranslate(); - const style = useS(styles); - - if (!menuItem.panel) { - return ( - - menuItem.onClick?.()} - > - {translate(menuItem.title)} - - - ); - } - - return ( - - - {menuItem.icon && ( -
- -
- )} - {menuItem.title &&
{translate(menuItem.title)}
} -
-
- ); -}); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts index 72cd42b403..edde1f5b97 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts @@ -6,278 +6,195 @@ * you may not use this file except in compliance with the License. */ import { injectable } from '@cloudbeaver/core-di'; -import { ContextMenuService, IContextMenuItem, IMenuContext, IMenuItem } from '@cloudbeaver/core-dialogs'; +import { + ACTION_ADD, + ACTION_CANCEL, + ACTION_DELETE, + ACTION_DUPLICATE, + ACTION_REVERT, + ACTION_SAVE, + ActionService, + MenuService, +} from '@cloudbeaver/core-view'; import { DatabaseEditAction } from '../../../DatabaseDataModel/Actions/DatabaseEditAction'; import { DatabaseSelectAction } from '../../../DatabaseDataModel/Actions/DatabaseSelectAction'; import { DatabaseEditChangeType } from '../../../DatabaseDataModel/Actions/IDatabaseDataEditAction'; +import { DATA_CONTEXT_DV_DDM } from '../../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM'; +import { DATA_CONTEXT_DV_DDM_RESULT_INDEX } from '../../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM_RESULT_INDEX'; +import { DATA_CONTEXT_DV_PRESENTATION, DataViewerPresentationType } from '../../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_PRESENTATION'; import type { IDatabaseDataModel } from '../../../DatabaseDataModel/IDatabaseDataModel'; - -export interface ITableFooterMenuContext { - model: IDatabaseDataModel; - resultIndex: number; - simple: boolean; -} +import { DATA_VIEWER_DATA_MODEL_ACTIONS_MENU } from './DATA_VIEWER_DATA_MODEL_ACTIONS_MENU'; @injectable() export class TableFooterMenuService { - static nodeContextType = 'NodeWithParent'; - private readonly tableFooterMenuToken = 'tableFooterMenu'; - - constructor(private readonly contextMenuService: ContextMenuService) { - this.contextMenuService.addPanel(this.tableFooterMenuToken); - - this.registerMenuItem({ - id: 'table_add', - order: 0.5, - icon: '/icons/data_add_sm.svg', - tooltip: 'data_viewer_action_edit_add', - isPresent(context) { - return context.contextType === TableFooterMenuService.nodeContextType; - }, - isHidden(context) { - if (context.data.model.isReadonly(context.data.resultIndex)) { - return true; - } + constructor( + private readonly actionService: ActionService, + private readonly menuService: MenuService, + ) {} - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); + register() { + this.menuService.addCreator({ + menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + isApplicable(context) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const presentation = context.get(DATA_CONTEXT_DV_PRESENTATION); - return !editor?.hasFeature('add'); + return !model.isReadonly(resultIndex) && !presentation?.readonly && (!presentation || presentation.type === DataViewerPresentationType.Data); }, - isDisabled(context) { - return ( - context.data.model.isLoading() || - context.data.model.isDisabled(context.data.resultIndex) || - !context.data.model.source.hasResult(context.data.resultIndex) - ); - }, - onClick(context) { - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); - - if (!editor) { - return; - } - - const select = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseSelectAction); - - editor.add(select?.getFocusedElement()); + getItems(context, items) { + return [ACTION_ADD, ACTION_DUPLICATE, ACTION_DELETE, ACTION_REVERT, ACTION_SAVE, ACTION_CANCEL, ...items]; }, }); - this.registerMenuItem({ - id: 'table_add_copy', - order: 0.55, - icon: '/icons/data_add_copy_sm.svg', - tooltip: 'data_viewer_action_edit_add_copy', - isPresent(context) { - return context.contextType === TableFooterMenuService.nodeContextType; - }, - isHidden(context) { - if (context.data.model.isReadonly(context.data.resultIndex)) { - return true; + this.actionService.addHandler({ + id: 'data-base-handler', + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], + actions: [ACTION_ADD, ACTION_DUPLICATE, ACTION_DELETE, ACTION_REVERT, ACTION_SAVE, ACTION_CANCEL], + isActionApplicable(context, action) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + + if (model.isReadonly(resultIndex)) { + return false; } - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); - - return !editor?.hasFeature('add'); - }, - isDisabled(context) { - if ( - context.data.model.isLoading() || - context.data.model.isDisabled(context.data.resultIndex) || - !context.data.model.source.hasResult(context.data.resultIndex) - ) { - return true; - } - - const selectedElements = getActiveElements(context.data.model, context.data.resultIndex); - - return selectedElements.length === 0; - }, - onClick(context) { - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); + const editor = model.source.getActionImplementation(resultIndex, DatabaseEditAction); if (!editor) { - return; + return false; } - const selectedElements = getActiveElements(context.data.model, context.data.resultIndex); - - editor.duplicate(...selectedElements); - }, - }); - this.registerMenuItem({ - id: 'table_delete', - order: 0.6, - icon: '/icons/data_delete_sm.svg', - tooltip: 'data_viewer_action_edit_delete', - isPresent(context) { - return context.contextType === TableFooterMenuService.nodeContextType; - }, - isHidden(context) { - if (context.data.model.isReadonly(context.data.resultIndex)) { - return true; + switch (action) { + case ACTION_DUPLICATE: + case ACTION_ADD: { + return editor.hasFeature('add'); + } + case ACTION_DELETE: { + return editor.hasFeature('delete'); + } + case ACTION_REVERT: { + return editor.hasFeature('revert'); + } } - - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); - - return !editor?.hasFeature('delete'); + return true; }, - isDisabled(context) { - if ( - context.data.model.isLoading() || - context.data.model.isDisabled(context.data.resultIndex) || - !context.data.model.source.hasResult(context.data.resultIndex) - ) { - return true; - } + isDisabled(context, action) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); - - if (!editor) { + if (model.isLoading() || model.isDisabled(resultIndex) || !model.getResult(resultIndex)) { return true; } - const selectedElements = getActiveElements(context.data.model, context.data.resultIndex); + switch (action) { + case ACTION_DUPLICATE: { + const selectedElements = getActiveElements(model, resultIndex); - if (selectedElements.length === 0) { - return true; - } + return selectedElements.length === 0; + } + case ACTION_DELETE: { + const editor = model.source.getActionImplementation(resultIndex, DatabaseEditAction); - return !selectedElements.some(key => editor.getElementState(key) !== DatabaseEditChangeType.delete); - }, - onClick(context) { - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); - - const selectedElements = getActiveElements(context.data.model, context.data.resultIndex); - - editor?.delete(...selectedElements); - }, - }); - this.registerMenuItem({ - id: 'table_revert', - order: 0.7, - icon: '/icons/data_revert_sm.svg', - tooltip: 'data_viewer_action_edit_revert', - isPresent(context) { - return context.contextType === TableFooterMenuService.nodeContextType; - }, - isHidden(context) { - if (context.data.model.isReadonly(context.data.resultIndex)) { - return true; - } - - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); + if (!editor) { + return true; + } - return !editor; - }, - isDisabled(context) { - if ( - context.data.model.isLoading() || - context.data.model.isDisabled(context.data.resultIndex) || - !context.data.model.source.hasResult(context.data.resultIndex) - ) { - return true; - } + const selectedElements = getActiveElements(model, resultIndex); - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); + return selectedElements.length === 0 || !selectedElements.some(key => editor.getElementState(key) !== DatabaseEditChangeType.delete); + } + case ACTION_REVERT: { + const editor = model.source.getActionImplementation(resultIndex, DatabaseEditAction); - const selectedElements = getActiveElements(context.data.model, context.data.resultIndex); + if (!editor) { + return true; + } - return ( - !editor || - selectedElements.length === 0 || - !selectedElements.some(key => { - const state = editor.getElementState(key); + const selectedElements = getActiveElements(model, resultIndex); - if (state === DatabaseEditChangeType.add) { - return editor.isElementEdited(key); - } + return ( + selectedElements.length === 0 || + !selectedElements.some(key => { + const state = editor.getElementState(key); - return state !== null; - }) - ); - }, - onClick(context) { - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); + if (state === DatabaseEditChangeType.add) { + return editor.isElementEdited(key); + } - const selectedElements = getActiveElements(context.data.model, context.data.resultIndex); + return state !== null; + }) + ); + } + case ACTION_SAVE: + case ACTION_CANCEL: { + const editor = model.source.getActionImplementation(resultIndex, DatabaseEditAction); - editor?.revert(...selectedElements); - }, - }); - this.registerMenuItem({ - id: 'save ', - order: 1, - title: 'ui_processing_save', - tooltip: 'ui_processing_save', - icon: 'table-save', - isPresent(context) { - return context.contextType === TableFooterMenuService.nodeContextType; - }, - isHidden(context) { - return context.data.model.isReadonly(context.data.resultIndex); - }, - isDisabled(context) { - if ( - context.data.model.isLoading() || - context.data.model.isDisabled(context.data.resultIndex) || - !context.data.model.source.hasResult(context.data.resultIndex) - ) { - return true; + return !editor?.isEdited(); + } } - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); + return false; + }, + getActionInfo(context, action) { + switch (action) { + case ACTION_ADD: + return { ...action.info, label: '', icon: '/icons/data_add_sm.svg', tooltip: 'data_viewer_action_edit_add' }; + case ACTION_DUPLICATE: + return { ...action.info, label: '', icon: '/icons/data_add_copy_sm.svg', tooltip: 'data_viewer_action_edit_add_copy' }; + case ACTION_DELETE: + return { ...action.info, label: '', icon: '/icons/data_delete_sm.svg', tooltip: 'data_viewer_action_edit_delete' }; + case ACTION_REVERT: + return { ...action.info, label: '', icon: '/icons/data_revert_sm.svg', tooltip: 'data_viewer_action_edit_revert' }; + case ACTION_SAVE: + return { ...action.info, icon: 'table-save' }; + case ACTION_CANCEL: + return { ...action.info, icon: '/icons/data_revert_all_sm.svg', tooltip: 'data_viewer_value_revert_title' }; + } - return !editor?.isEdited(); + return action.info; }, - onClick: context => context.data.model.save().catch(() => {}), - }); + handler: (context, action) => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const editor = model.source.getActionImplementation(resultIndex, DatabaseEditAction); - this.registerMenuItem({ - id: 'cancel ', - order: 2, - title: 'data_viewer_value_revert', - tooltip: 'data_viewer_value_revert_title', - icon: '/icons/data_revert_all_sm.svg', - isPresent(context) { - return context.contextType === TableFooterMenuService.nodeContextType; - }, - isHidden(context) { - return context.data.model.isReadonly(context.data.resultIndex); - }, - isDisabled(context) { - if ( - context.data.model.isLoading() || - context.data.model.isDisabled(context.data.resultIndex) || - !context.data.model.source.hasResult(context.data.resultIndex) - ) { - return true; + if (!editor) { + return; + } + const select = model.source.getActionImplementation(resultIndex, DatabaseSelectAction); + const selectedElements = getActiveElements(model, resultIndex); + + switch (action) { + case ACTION_ADD: { + editor.add(select?.getFocusedElement()); + break; + } + case ACTION_DUPLICATE: { + editor.duplicate(...selectedElements); + break; + } + case ACTION_DELETE: { + editor.delete(...selectedElements); + break; + } + case ACTION_REVERT: { + editor.revert(...selectedElements); + break; + } + case ACTION_SAVE: + model.save().catch(() => {}); + break; + case ACTION_CANCEL: { + editor.clear(); + break; + } } - - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); - - return !editor?.isEdited(); - }, - onClick: context => { - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); - editor?.clear(); }, }); } - - constructMenuWithContext(model: IDatabaseDataModel, resultIndex: number, simple: boolean): IMenuItem[] { - const context: IMenuContext = { - menuId: this.tableFooterMenuToken, - contextId: model.id, - contextType: TableFooterMenuService.nodeContextType, - data: { model, resultIndex, simple }, - }; - return this.contextMenuService.createContextMenu(context, this.tableFooterMenuToken).menuItems; - } - - registerMenuItem(options: IContextMenuItem): void { - this.contextMenuService.addMenuItem(this.tableFooterMenuToken, options); - } } function getActiveElements(model: IDatabaseDataModel, resultIndex: number): unknown[] { diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterRowCount.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterRowCount.tsx deleted file mode 100644 index f9abdac440..0000000000 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterRowCount.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 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 } from 'react'; - -import { useService } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { isNotNullDefined } from '@cloudbeaver/core-utils'; - -import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; -import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet'; -import { CancelTotalCountAction } from './CancelTotalCountAction'; -import { TotalCountAction } from './TotalCountAction'; - -interface Props { - resultIndex: number; - model: IDatabaseDataModel; -} - -export const TableFooterRowCount: React.FC = observer(function TableFooterRowCount({ resultIndex, model }) { - const notificationService = useService(NotificationService); - const [loading, setLoading] = useState(false); - - async function loadTotalCount() { - try { - setLoading(true); - await model.source.loadTotalCount(resultIndex); - } catch (exception: any) { - if (model.source.totalCountRequestTask?.cancelled) { - notificationService.logInfo({ - title: 'data_viewer_total_count_canceled_title', - message: 'data_viewer_total_count_canceled_message', - }); - } else { - notificationService.logException(exception, 'data_viewer_total_count_failed'); - } - } finally { - setLoading(false); - } - } - - async function cancelTotalCount() { - try { - await model.source.cancelLoadTotalCount(); - } catch (e: any) { - if (!model.source.totalCountRequestTask?.cancelled) { - notificationService.logException(e); - } - } - } - - if (loading) { - return ; - } - - return ; -}); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TotalCountAction.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TotalCountAction.tsx deleted file mode 100644 index 5221b768e3..0000000000 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TotalCountAction.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 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 { getComputed, s, ToolsAction, useS, useTranslate } from '@cloudbeaver/core-blocks'; - -import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; -import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet'; -import TableFooterMenuStyles from './TableFooterMenu/TableFooterMenuItem.module.css'; -import classes from './TableFooterRowCount.module.css'; - -interface Props { - onClick: VoidFunction; - loading: boolean; - resultIndex: number; - model: IDatabaseDataModel; -} - -export const TotalCountAction = observer(function TotalCountAction({ onClick, loading, resultIndex, model }) { - const result = model.getResult(resultIndex); - const translate = useTranslate(); - const disabled = getComputed(() => model.isLoading() || model.isDisabled(resultIndex)); - const style = useS(TableFooterMenuStyles, classes); - - if (!result) { - return null; - } - - const currentCount = result.loadedFully ? result.count : `${result.count}+`; - const count = result.totalCount ?? currentCount; - - return ( -
- - {count} - -
- ); -}); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderMenu.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderMenu.tsx index 9d7024a19f..fe9cdb3742 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderMenu.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderMenu.tsx @@ -8,6 +8,7 @@ import { observer } from 'mobx-react-lite'; import { PlaceholderComponent, useS } from '@cloudbeaver/core-blocks'; +import { useDataContextLink } from '@cloudbeaver/core-data-context'; import { MenuBar, MenuBarItemStyles, MenuBarStyles } from '@cloudbeaver/core-ui'; import { useMenu } from '@cloudbeaver/core-view'; @@ -21,9 +22,11 @@ export const TableHeaderMenu: PlaceholderComponent const menu = useMenu({ menu: DATA_VIEWER_DATA_MODEL_TOOLS_MENU }); const menuBarStyles = useS(MenuBarStyles, MenuBarItemStyles); - menu.context.set(DATA_CONTEXT_DV_DDM, model); - menu.context.set(DATA_CONTEXT_DV_DDM_RESULT_INDEX, resultIndex); - menu.context.set(DATA_CONTEXT_DATA_VIEWER_SIMPLE, simple); + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_DV_DDM, model, id); + context.set(DATA_CONTEXT_DV_DDM_RESULT_INDEX, resultIndex, id); + context.set(DATA_CONTEXT_DATA_VIEWER_SIMPLE, simple, id); + }); return ; }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts index 680064742b..fa15011bff 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts @@ -50,12 +50,11 @@ export class TableHeaderService extends Bootstrap { this.actionService.addHandler({ id: 'table-header-menu-base-handler', + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], isActionApplicable(context) { const menu = context.hasValue(DATA_CONTEXT_MENU, DATA_VIEWER_DATA_MODEL_TOOLS_MENU); - const model = context.tryGet(DATA_CONTEXT_DV_DDM); - const resultIndex = context.tryGet(DATA_CONTEXT_DV_DDM_RESULT_INDEX); - if (!menu || !model || resultIndex === undefined) { + if (!menu) { return false; } @@ -64,8 +63,8 @@ export class TableHeaderService extends Bootstrap { handler: async (context, action) => { switch (action) { case DATA_VIEWER_CONSTRAINTS_DELETE_ACTION: { - const model = context.get(DATA_CONTEXT_DV_DDM); - const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX); + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; const constraints = model.source.tryGetAction(resultIndex, ResultSetConstraintAction); if (constraints) { @@ -84,8 +83,8 @@ export class TableHeaderService extends Bootstrap { return action.info; }, isDisabled: (context, action) => { - const model = context.get(DATA_CONTEXT_DV_DDM); - const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX); + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; if (model.isLoading() || model.isDisabled(resultIndex)) { return true; diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.module.css index ae57c452cb..1822a00754 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.module.css +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.module.css @@ -18,6 +18,14 @@ border-radius: var(--theme-group-element-radius); } } + +.captureView { + flex: 1; + display: flex; + overflow: auto; + position: relative; +} + .tableViewer { composes: theme-background-secondary theme-text-on-secondary from global; position: relative; diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx index 10268778b9..8aeec73f4f 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx @@ -23,16 +23,17 @@ import { useSplitUserState, useTranslate, } from '@cloudbeaver/core-blocks'; -import type { IDataContext } from '@cloudbeaver/core-data-context'; import { useService } from '@cloudbeaver/core-di'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; +import { CaptureView } from '@cloudbeaver/core-view'; import { ResultSetConstraintAction } from '../DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction'; import { DataPresentationService, DataPresentationType } from '../DataPresentationService'; +import { DataPresentation } from './DataPresentation'; +import { DataViewerViewService } from './DataViewerViewService'; import type { IDataTableActionsPrivate } from './IDataTableActions'; import { TableError } from './TableError'; import { TableFooter } from './TableFooter/TableFooter'; -import { TableGrid } from './TableGrid'; import { TableHeader } from './TableHeader/TableHeader'; import { TablePresentationBar } from './TablePresentationBar/TablePresentationBar'; import { TableToolsPanel } from './TableToolsPanel'; @@ -46,7 +47,6 @@ export interface TableViewerProps { valuePresentationId: string | null | undefined; /** Display data in simple mode, some features will be hidden or disabled */ simple?: boolean; - context?: IDataContext; className?: string; onPresentationChange: (id: string) => void; onValuePresentationChange: (id: string | null) => void; @@ -54,21 +54,12 @@ export interface TableViewerProps { export const TableViewer = observer( forwardRef(function TableViewer( - { - tableId, - resultIndex = 0, - presentationId, - valuePresentationId, - simple = false, - context, - className, - onPresentationChange, - onValuePresentationChange, - }, + { tableId, resultIndex = 0, presentationId, valuePresentationId, simple = false, className, onPresentationChange, onValuePresentationChange }, ref, ) { const translate = useTranslate(); const styles = useS(style); + const dataViewerView = useService(DataViewerViewService); const dataPresentationService = useService(DataPresentationService); const tableViewerStorageService = useService(TableViewerStorageService); const dataModel = tableViewerStorageService.get(tableId); @@ -204,88 +195,90 @@ export const TableViewer = observer( !simple; return ( -
-
- {!isStatistics && ( - - )} -
- - - -
- - - - - dataModel.source.cancel()} - /> -
-
- - - -
- {resultExist && ( - +
+
+ {!isStatistics && ( + + )} +
+ + + +
+ + - )} + + + dataModel.source.cancel()} + />
- -
-
+ + + + +
+ {resultExist && ( + + )} +
+
+
+ +
+ {!simple && !isStatistics && ( + + )}
- {!simple && !isStatistics && ( - - )} +
- -
+ ); }), ); diff --git a/webapp/packages/plugin-data-viewer/src/index.ts b/webapp/packages/plugin-data-viewer/src/index.ts index 71b65ebc07..f326d6da82 100644 --- a/webapp/packages/plugin-data-viewer/src/index.ts +++ b/webapp/packages/plugin-data-viewer/src/index.ts @@ -12,6 +12,7 @@ export * from './DatabaseDataModel/Actions/Document/DocumentEditAction'; export * from './DatabaseDataModel/Actions/Document/IDatabaseDataDocument'; export * from './DatabaseDataModel/Actions/Document/IDocumentElementKey'; export * from './DatabaseDataModel/Actions/ResultSet/DataContext/DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY'; +export * from './DatabaseDataModel/DataContext/DATA_CONTEXT_DV_PRESENTATION'; export * from './DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM'; export * from './DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM_RESULT_INDEX'; export * from './DatabaseDataModel/Actions/ResultSet/compareResultSetRowKeys'; diff --git a/webapp/packages/plugin-data-viewer/src/manifest.ts b/webapp/packages/plugin-data-viewer/src/manifest.ts index 4421c69e9f..93974e092d 100644 --- a/webapp/packages/plugin-data-viewer/src/manifest.ts +++ b/webapp/packages/plugin-data-viewer/src/manifest.ts @@ -30,5 +30,7 @@ export const dataViewerManifest: PluginManifest = { () => import('./TableViewer/ValuePanel/DataValuePanelBootstrap').then(m => m.DataValuePanelBootstrap), () => import('./DataViewerSettingsService').then(m => m.DataViewerSettingsService), () => import('./DataViewerService').then(m => m.DataViewerService), + () => import('./ResultSet/ResultSetTableFooterMenuService').then(m => m.ResultSetTableFooterMenuService), + () => import('./TableViewer/DataViewerViewService').then(m => m.DataViewerViewService), ], }; diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerFooterService.ts b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerFooterService.ts index 01efedc7e9..9b0e2bf930 100644 --- a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerFooterService.ts +++ b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerFooterService.ts @@ -36,7 +36,7 @@ export class DDLViewerFooterService { handler: async (context, action) => { switch (action) { case ACTION_SAVE: { - const ddl = context.get(DATA_CONTEXT_DDL_VIEWER_VALUE); + const ddl = context.get(DATA_CONTEXT_DDL_VIEWER_VALUE)!; const nodeId = context.get(DATA_CONTEXT_DDL_VIEWER_NODE); const blob = new Blob([ddl], { @@ -50,8 +50,8 @@ export class DDLViewerFooterService { break; } case ACTION_SQL_EDITOR_OPEN: { - const ddl = context.get(DATA_CONTEXT_DDL_VIEWER_VALUE); - const nodeId = context.get(DATA_CONTEXT_DDL_VIEWER_NODE); + const ddl = context.get(DATA_CONTEXT_DDL_VIEWER_VALUE)!; + const nodeId = context.get(DATA_CONTEXT_DDL_VIEWER_NODE)!; const connection = this.connectionInfoResource.getConnectionForNode(nodeId); const container = this.navNodeManagerService.getNodeContainerInfo(nodeId); diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.tsx b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.tsx index 537c39c5bf..bc246c515c 100644 --- a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.tsx +++ b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.tsx @@ -14,6 +14,7 @@ import { ConnectionInfoResource, createConnectionParam, } from '@cloudbeaver/core-connections'; +import { useDataContextLink } from '@cloudbeaver/core-data-context'; import { MenuBar } from '@cloudbeaver/core-ui'; import { useMenu } from '@cloudbeaver/core-view'; import { useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6'; @@ -39,9 +40,12 @@ export const DDLViewerTabPanel: NavNodeTransformViewComponent = observer(functio const sqlDialect = useSqlDialectExtension(connectionDialectResource.data); const extensions = useCodemirrorExtensions(); extensions.set(...sqlDialect); + const ddlData = ddlResource.data; - menu.context.set(DATA_CONTEXT_DDL_VIEWER_NODE, nodeId); - menu.context.set(DATA_CONTEXT_DDL_VIEWER_VALUE, ddlResource.data); + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_DDL_VIEWER_NODE, nodeId, id); + context.set(DATA_CONTEXT_DDL_VIEWER_VALUE, ddlData, id); + }); return (
diff --git a/webapp/packages/plugin-ddl-viewer/src/ExtendedDDLViewer/ExtendedDDLViewerTabPanel.tsx b/webapp/packages/plugin-ddl-viewer/src/ExtendedDDLViewer/ExtendedDDLViewerTabPanel.tsx index 2e44200199..f7226ba4df 100644 --- a/webapp/packages/plugin-ddl-viewer/src/ExtendedDDLViewer/ExtendedDDLViewerTabPanel.tsx +++ b/webapp/packages/plugin-ddl-viewer/src/ExtendedDDLViewer/ExtendedDDLViewerTabPanel.tsx @@ -14,6 +14,7 @@ import { ConnectionInfoResource, createConnectionParam, } from '@cloudbeaver/core-connections'; +import { useDataContextLink } from '@cloudbeaver/core-data-context'; import { MenuBar } from '@cloudbeaver/core-ui'; import { useMenu } from '@cloudbeaver/core-view'; import { useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6'; @@ -39,18 +40,16 @@ export const ExtendedDDLViewerTabPanel: NavNodeTransformViewComponent = observer const sqlDialect = useSqlDialectExtension(connectionDialectResource.data); const extensions = useCodemirrorExtensions(); extensions.set(...sqlDialect); + const extendedDDlData = extendedDDLResource.data; - menu.context.set(DATA_CONTEXT_DDL_VIEWER_NODE, nodeId); - menu.context.set(DATA_CONTEXT_DDL_VIEWER_VALUE, extendedDDLResource.data); + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_DDL_VIEWER_NODE, nodeId, id); + context.set(DATA_CONTEXT_DDL_VIEWER_VALUE, extendedDDlData, id); + }); return (
- +
); diff --git a/webapp/packages/plugin-devtools/src/ContextMenu/DATA_CONTEXT_MENU_SEARCH.ts b/webapp/packages/plugin-devtools/src/ContextMenu/DATA_CONTEXT_MENU_SEARCH.ts index 23b9729de9..358da668fa 100644 --- a/webapp/packages/plugin-devtools/src/ContextMenu/DATA_CONTEXT_MENU_SEARCH.ts +++ b/webapp/packages/plugin-devtools/src/ContextMenu/DATA_CONTEXT_MENU_SEARCH.ts @@ -1,10 +1,10 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2021 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { createDataContext } from '@cloudbeaver/core-data-context'; -export const DATA_CONTEXT_MENU_SEARCH = createDataContext('menu-local', () => ''); +export const DATA_CONTEXT_MENU_SEARCH = createDataContext('menu-search'); diff --git a/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItemComponent.tsx b/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItemComponent.tsx index eec5e134df..61c26d0906 100644 --- a/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItemComponent.tsx +++ b/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItemComponent.tsx @@ -6,8 +6,10 @@ * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; +import { useRef } from 'react'; import { s, useS } from '@cloudbeaver/core-blocks'; +import { useDataContextLink } from '@cloudbeaver/core-data-context'; import type { IContextMenuItemProps } from '@cloudbeaver/core-ui'; import type { ICustomMenuItemComponent } from '@cloudbeaver/core-view'; @@ -21,9 +23,17 @@ export const SearchResourceMenuItemComponent: ICustomMenuItemComponent(null); + + useDataContextLink(menuData.context, (context, id) => { + contextRefId.current = id; + }); + function handleChange(value: string) { - menuData.context.set(DATA_CONTEXT_MENU_SEARCH, value); + if (contextRefId.current) { + menuData.context.set(DATA_CONTEXT_MENU_SEARCH, value, contextRefId.current); + } } return ( diff --git a/webapp/packages/plugin-devtools/src/PluginBootstrap.ts b/webapp/packages/plugin-devtools/src/PluginBootstrap.ts index b208034b1a..d58521a82d 100644 --- a/webapp/packages/plugin-devtools/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-devtools/src/PluginBootstrap.ts @@ -80,7 +80,7 @@ export class PluginBootstrap extends Bootstrap { this.menuService.addCreator({ menus: [MENU_DEVTOOLS], getItems: (context, items) => { - const search = context.tryGet(DATA_CONTEXT_MENU_SEARCH); + const search = context.get(DATA_CONTEXT_MENU_SEARCH); if (search) { return [ @@ -125,7 +125,7 @@ export class PluginBootstrap extends Bootstrap { this.menuService.addCreator({ menus: [MENU_PLUGIN], isApplicable: context => { - const item = context.tryGet(DATA_CONTEXT_SUBMENU_ITEM); + const item = context.get(DATA_CONTEXT_SUBMENU_ITEM); if (item instanceof PluginSubMenuItem) { return this.app.getServices(item.plugin).some(service => service.prototype instanceof CachedResource); diff --git a/webapp/packages/plugin-navigation-tree-filters/src/NavigationTreeFiltersBootstrap.ts b/webapp/packages/plugin-navigation-tree-filters/src/NavigationTreeFiltersBootstrap.ts index 481f4376d6..9f2ec81e9e 100644 --- a/webapp/packages/plugin-navigation-tree-filters/src/NavigationTreeFiltersBootstrap.ts +++ b/webapp/packages/plugin-navigation-tree-filters/src/NavigationTreeFiltersBootstrap.ts @@ -34,10 +34,11 @@ export class NavigationTreeFiltersBootstrap extends Bootstrap { register(): void { this.menuService.addCreator({ root: true, + contexts: [DATA_CONTEXT_NAV_NODE], isApplicable: context => { - const node = context.tryGet(DATA_CONTEXT_NAV_NODE); + const node = context.get(DATA_CONTEXT_NAV_NODE)!; - if (!node || !node.folder || !NodeManagerUtils.isDatabaseObject(node.id)) { + if (!node.folder || !NodeManagerUtils.isDatabaseObject(node.id)) { return false; } @@ -48,8 +49,9 @@ export class NavigationTreeFiltersBootstrap extends Bootstrap { this.menuService.addCreator({ menus: [MENU_NAVIGATION_TREE_FILTERS], + contexts: [DATA_CONTEXT_NAV_NODE], getItems: (context, items) => { - const node = context.get(DATA_CONTEXT_NAV_NODE); + const node = context.get(DATA_CONTEXT_NAV_NODE)!; const actions = [ new MenuBaseItem( { diff --git a/webapp/packages/plugin-navigation-tree-rm/src/NavNodes/ResourceFoldersBootstrap.ts b/webapp/packages/plugin-navigation-tree-rm/src/NavNodes/ResourceFoldersBootstrap.ts index 80304ce42d..4f21e50ff4 100644 --- a/webapp/packages/plugin-navigation-tree-rm/src/NavNodes/ResourceFoldersBootstrap.ts +++ b/webapp/packages/plugin-navigation-tree-rm/src/NavNodes/ResourceFoldersBootstrap.ts @@ -87,7 +87,7 @@ export class ResourceFoldersBootstrap extends Bootstrap { id: 'tree-tools-menu-resource-folders-handler', actions: [ACTION_NEW_FOLDER], isActionApplicable: context => { - const tree = context.tryGet(DATA_CONTEXT_ELEMENTS_TREE); + const tree = context.get(DATA_CONTEXT_ELEMENTS_TREE); if (!tree?.baseRoot.startsWith(RESOURCES_NODE_PATH) || !this.userInfoResource.data) { return false; @@ -164,7 +164,7 @@ export class ResourceFoldersBootstrap extends Bootstrap { } private async elementsTreeActionHandler(contexts: IDataContextProvider, action: IAction) { - const resourceTypeId = contexts.tryGet(DATA_CONTEXT_RESOURCE_MANAGER_TREE_RESOURCE_TYPE_ID); + const resourceTypeId = contexts.get(DATA_CONTEXT_RESOURCE_MANAGER_TREE_RESOURCE_TYPE_ID); switch (action) { case ACTION_NEW_FOLDER: { const targetNode = this.getTargetNode(contexts); diff --git a/webapp/packages/plugin-navigation-tree-rm/src/NavTreeRMContextMenuService.ts b/webapp/packages/plugin-navigation-tree-rm/src/NavTreeRMContextMenuService.ts index 1a92c09041..0f3557ac56 100644 --- a/webapp/packages/plugin-navigation-tree-rm/src/NavTreeRMContextMenuService.ts +++ b/webapp/packages/plugin-navigation-tree-rm/src/NavTreeRMContextMenuService.ts @@ -36,12 +36,9 @@ export class NavTreeRMContextMenuService extends Bootstrap { register(): void { this.actionService.addHandler({ id: 'nav-node-rm-handler', + contexts: [DATA_CONTEXT_NAV_NODE], isActionApplicable: (context, action): boolean => { - const node = context.tryGet(DATA_CONTEXT_NAV_NODE); - - if (!node) { - return false; - } + const node = context.get(DATA_CONTEXT_NAV_NODE)!; if (![NAV_NODE_TYPE_RM_RESOURCE, NAV_NODE_TYPE_RM_FOLDER].includes(node.nodeType as string)) { return false; @@ -58,7 +55,7 @@ export class NavTreeRMContextMenuService extends Bootstrap { return false; }, handler: async (context, action) => { - const node = context.get(DATA_CONTEXT_NAV_NODE); + const node = context.get(DATA_CONTEXT_NAV_NODE)!; const resourceKey = getResourceKeyFromNodeId(node.id); if (!resourceKey) { @@ -80,7 +77,7 @@ export class NavTreeRMContextMenuService extends Bootstrap { switch (action) { case ACTION_RENAME: { - const actions = context.tryGet(DATA_CONTEXT_NAV_NODE_ACTIONS); + const actions = context.get(DATA_CONTEXT_NAV_NODE_ACTIONS); const save = async (newName: string) => { if (key.name !== newName && newName.trim().length) { diff --git a/webapp/packages/plugin-navigation-tree-rm/src/Tree/ProjectsRenderer/NavigationNodeProjectControl.tsx b/webapp/packages/plugin-navigation-tree-rm/src/Tree/ProjectsRenderer/NavigationNodeProjectControl.tsx index 4205db01ff..465dc27e19 100644 --- a/webapp/packages/plugin-navigation-tree-rm/src/Tree/ProjectsRenderer/NavigationNodeProjectControl.tsx +++ b/webapp/packages/plugin-navigation-tree-rm/src/Tree/ProjectsRenderer/NavigationNodeProjectControl.tsx @@ -42,7 +42,7 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer navNodeInfoResource.isOutdated(node.id) && !treeNodeContext.loading); const selected = treeNodeContext.selected; - const resourceType = viewContext?.tryGet(DATA_CONTEXT_RESOURCE_MANAGER_TREE_RESOURCE_TYPE_ID); + const resourceType = viewContext?.get(DATA_CONTEXT_RESOURCE_MANAGER_TREE_RESOURCE_TYPE_ID); const isDragging = getComputed(() => { if (!node.projectId || !elementsTreeContext?.tree.activeDnDData) { diff --git a/webapp/packages/plugin-navigation-tree-rm/src/Tree/ResourceManagerTreeCaptureViewContext.tsx b/webapp/packages/plugin-navigation-tree-rm/src/Tree/ResourceManagerTreeCaptureViewContext.tsx index 3ee16106ba..7a192f398f 100644 --- a/webapp/packages/plugin-navigation-tree-rm/src/Tree/ResourceManagerTreeCaptureViewContext.tsx +++ b/webapp/packages/plugin-navigation-tree-rm/src/Tree/ResourceManagerTreeCaptureViewContext.tsx @@ -14,8 +14,8 @@ interface Props { } export const ResourceManagerTreeCaptureViewContext: React.FC = function ResourceManagerTreeCaptureViewContext({ resourceTypeId }) { - useCaptureViewContext(context => { - context?.set(DATA_CONTEXT_RESOURCE_MANAGER_TREE_RESOURCE_TYPE_ID, resourceTypeId); + useCaptureViewContext((context, id) => { + context.set(DATA_CONTEXT_RESOURCE_MANAGER_TREE_RESOURCE_TYPE_ID, resourceTypeId, id); }); return null; diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx index fed7cc29aa..a4ee870c89 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx @@ -48,13 +48,14 @@ interface Props { export const ElementsTreeTools = observer>(function ElementsTreeTools({ tree, settingsElements, children }) { const root = tree.root; + const baseRoot = tree.baseRoot; const translate = useTranslate(); const [opened, setOpen] = useState(false); const styles = useS(ElementsTreeToolsStyles, ElementsTreeToolsIconButtonStyles); - useCaptureViewContext(context => { - context?.set(DATA_CONTEXT_NAV_TREE_ROOT, tree.baseRoot); - context?.set(DATA_CONTEXT_ELEMENTS_TREE, tree); + useCaptureViewContext((context, id) => { + context.set(DATA_CONTEXT_NAV_TREE_ROOT, baseRoot, id); + context.set(DATA_CONTEXT_ELEMENTS_TREE, tree, id); }); const loading = tree.isLoading(); diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeToolsMenu.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeToolsMenu.tsx index 955bf5bf9a..b9d44569b8 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeToolsMenu.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeToolsMenu.tsx @@ -8,6 +8,7 @@ import { observer } from 'mobx-react-lite'; import { s, SContext, StyleRegistry, useS } from '@cloudbeaver/core-blocks'; +import { useDataContextLink } from '@cloudbeaver/core-data-context'; import { MenuBar, MenuBarItemStyles, MenuBarStyles } from '@cloudbeaver/core-ui'; import { useMenu } from '@cloudbeaver/core-view'; @@ -37,7 +38,9 @@ export const ElementsTreeToolsMenu = observer(function ElementsTreeToolsM const menuBarStyles = useS(MenuBarStyles, MenuBarItemStyles); const menu = useMenu({ menu: MENU_ELEMENTS_TREE_TOOLS }); - menu.context.set(DATA_CONTEXT_ELEMENTS_TREE, tree); + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_ELEMENTS_TREE, tree, id); + }); return ( diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeToolsMenuService.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeToolsMenuService.ts index 6172b7f970..3ac45b71a0 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeToolsMenuService.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeToolsMenuService.ts @@ -46,7 +46,7 @@ export class ElementsTreeToolsMenuService { this.actionService.addHandler({ id: 'tree-tools-menu-base-handler', isActionApplicable(context, action): boolean { - const tree = context.tryGet(DATA_CONTEXT_ELEMENTS_TREE); + const tree = context.get(DATA_CONTEXT_ELEMENTS_TREE); if (!tree) { return false; @@ -82,7 +82,7 @@ export class ElementsTreeToolsMenuService { return action.info; }, isHidden: (context, action) => { - const tree = context.tryGet(DATA_CONTEXT_ELEMENTS_TREE); + const tree = context.get(DATA_CONTEXT_ELEMENTS_TREE); if (action === ACTION_LINK_OBJECT && tree) { const navNode = this.connectionSchemaManagerService.activeNavNode; @@ -114,7 +114,7 @@ export class ElementsTreeToolsMenuService { this.actionService.addHandler({ id: 'elements-tree-base', isActionApplicable: (contexts, action): boolean => { - const tree = contexts.tryGet(DATA_CONTEXT_ELEMENTS_TREE); + const tree = contexts.get(DATA_CONTEXT_ELEMENTS_TREE); if (!tree) { return false; diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode.tsx index 50e62d8165..8be8212500 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode.tsx @@ -9,7 +9,7 @@ import { observer } from 'mobx-react-lite'; import { useDeferredValue, useEffect } from 'react'; import { getComputed, s, TreeNode, useMergeRefs, useS } from '@cloudbeaver/core-blocks'; -import { useDataContext } from '@cloudbeaver/core-data-context'; +import { useDataContext, useDataContextLink } from '@cloudbeaver/core-data-context'; import { useService } from '@cloudbeaver/core-di'; import { DATA_CONTEXT_NAV_NODE, DATA_CONTEXT_NAV_NODES, NavNodeManagerService } from '@cloudbeaver/core-navigation-tree'; import { useDNDData } from '@cloudbeaver/core-ui'; @@ -64,8 +64,10 @@ export const NavigationNode: NavigationNodeComponent = observer(function Navigat expand: navNode.expand, }); - context.set(DATA_CONTEXT_NAV_NODE, node); - context.set(DATA_CONTEXT_NAV_NODES, navNode.getSelected); + useDataContextLink(context, (context, id) => { + context.set(DATA_CONTEXT_NAV_NODE, node, id); + context.set(DATA_CONTEXT_NAV_NODES, navNode.getSelected, id); + }); if (navNode.leaf || !navNode.loaded) { externalExpanded = false; diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx index ee68062669..d5bd090d83 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx @@ -9,6 +9,7 @@ import { observer } from 'mobx-react-lite'; import { getComputed, Icon, IMouseContextMenu, s, useS } from '@cloudbeaver/core-blocks'; import { ConnectionInfoResource, DATA_CONTEXT_CONNECTION } from '@cloudbeaver/core-connections'; +import { useDataContextLink } from '@cloudbeaver/core-data-context'; import { useService } from '@cloudbeaver/core-di'; import { DATA_CONTEXT_NAV_NODE, type INodeActions, type NavNode } from '@cloudbeaver/core-navigation-tree'; import { ContextMenu } from '@cloudbeaver/core-ui'; @@ -30,14 +31,16 @@ export const TreeNodeMenu = observer(function TreeNodeMenu({ const styles = useS(style); const connectionsInfoResource = useService(ConnectionInfoResource); const menu = useMenu({ menu: MENU_NAV_TREE }); - menu.context.set(DATA_CONTEXT_NAV_NODE, node); - menu.context.set(DATA_CONTEXT_NAV_NODE_ACTIONS, actions); - const connection = getComputed(() => connectionsInfoResource.getConnectionForNode(node.id)); - if (connection) { - menu.context.set(DATA_CONTEXT_CONNECTION, connection); - } + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_NAV_NODE, node, id); + context.set(DATA_CONTEXT_NAV_NODE_ACTIONS, actions, id); + + if (connection) { + context.set(DATA_CONTEXT_CONNECTION, connection, id); + } + }); function handleVisibleSwitch(visible: boolean) { if (!visible) { diff --git a/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeContextMenuService.ts b/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeContextMenuService.ts index 187e810d66..ca362f4622 100644 --- a/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeContextMenuService.ts +++ b/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeContextMenuService.ts @@ -96,12 +96,9 @@ export class NavNodeContextMenuService extends Bootstrap { this.actionService.addHandler({ id: 'nav-node-base-handler', + contexts: [DATA_CONTEXT_NAV_NODE], isActionApplicable: (context, action): boolean => { - const node = context.tryGet(DATA_CONTEXT_NAV_NODE); - - if (!node) { - return false; - } + const node = context.get(DATA_CONTEXT_NAV_NODE)!; if (NodeManagerUtils.isDatabaseObject(node.id) || node.nodeType === NAV_NODE_TYPE_FOLDER) { if (action === ACTION_RENAME) { @@ -120,7 +117,7 @@ export class NavNodeContextMenuService extends Bootstrap { return [ACTION_OPEN, ACTION_REFRESH].includes(action); }, handler: async (context, action) => { - const node = context.get(DATA_CONTEXT_NAV_NODE); + const node = context.get(DATA_CONTEXT_NAV_NODE)!; const name = getNodePlainName(node); switch (action) { @@ -137,7 +134,7 @@ export class NavNodeContextMenuService extends Bootstrap { break; } case ACTION_RENAME: { - const actions = context.tryGet(DATA_CONTEXT_NAV_NODE_ACTIONS); + const actions = context.get(DATA_CONTEXT_NAV_NODE_ACTIONS); const save = async (newName: string) => { if (name !== newName && newName.trim().length) { diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooter.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooter.tsx index 938ee4d204..4d27b8358b 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooter.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooter.tsx @@ -8,6 +8,7 @@ import { observer } from 'mobx-react-lite'; import type { TableState } from '@cloudbeaver/core-blocks'; +import { useDataContextLink } from '@cloudbeaver/core-data-context'; import { useService } from '@cloudbeaver/core-di'; import { DATA_CONTEXT_NAV_NODES, type NavNode, NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree'; import { resourceKeyList } from '@cloudbeaver/core-resource'; @@ -29,7 +30,9 @@ export const ObjectPropertyTableFooter = observer(function ObjectProperty return navNodeInfoResource.get(resourceKeyList(state.selectedList)).filter(Boolean) as NavNode[]; } - menu.context.set(DATA_CONTEXT_NAV_NODES, getSelected); + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_NAV_NODES, getSelected, id); + }); return ; }); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts index 5429d2b255..420ad00194 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts @@ -48,7 +48,7 @@ export class ObjectPropertyTableFooterService { }, isDisabled: (context, action) => { if (action === ACTION_DELETE) { - const selected = context.get(DATA_CONTEXT_NAV_NODES)(); + const selected = context.get(DATA_CONTEXT_NAV_NODES)!(); return !selected.some(node => node.features?.includes(ENodeFeature.canDelete)) || this.navTreeResource.isLoading(); } @@ -56,7 +56,7 @@ export class ObjectPropertyTableFooterService { }, handler: async (context, action) => { if (action === ACTION_DELETE) { - const selected = context.get(DATA_CONTEXT_NAV_NODES)(); + const selected = context.get(DATA_CONTEXT_NAV_NODES)!(); const nodes = selected.filter(node => node.features?.includes(ENodeFeature.canDelete)); try { diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/Table/CellFormatter.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/Table/CellFormatter.tsx index ac73b1c900..b466909b2b 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/Table/CellFormatter.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/Table/CellFormatter.tsx @@ -10,6 +10,7 @@ import { useContext, useState } from 'react'; import { getComputed, Icon, s, useMouse, useS, useStateDelay } from '@cloudbeaver/core-blocks'; import { ConnectionInfoResource, DATA_CONTEXT_CONNECTION } from '@cloudbeaver/core-connections'; +import { useDataContextLink } from '@cloudbeaver/core-data-context'; import { useService } from '@cloudbeaver/core-di'; import { DATA_CONTEXT_NAV_NODE, type DBObject, type NavNode, NavNodeManagerService } from '@cloudbeaver/core-navigation-tree'; import { ContextMenu } from '@cloudbeaver/core-ui'; @@ -33,14 +34,15 @@ export const Menu = observer(function Menu({ value, node }) { const menu = useMenu({ menu: MENU_NAV_TREE }); const mouse = useMouse(); const [menuOpened, switchState] = useState(false); - - menu.context.set(DATA_CONTEXT_NAV_NODE, node); - const connection = connectionsInfoResource.getConnectionForNode(node.id); - if (connection) { - menu.context.set(DATA_CONTEXT_CONNECTION, connection); - } + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_NAV_NODE, node, id); + + if (connection) { + context.set(DATA_CONTEXT_CONNECTION, connection, id); + } + }); function openNode() { navNodeManagerService.navToNode(node.id, node.parentId); diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/PluginBootstrap.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/PluginBootstrap.ts index 7a4ddda3eb..dca7562347 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/PluginBootstrap.ts @@ -68,7 +68,7 @@ export class PluginBootstrap extends Bootstrap { actions: [ACTION_SAVE_AS_SCRIPT], contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], isActionApplicable: (context): boolean => { - const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; if (!this.projectsService.activeProjects.some(project => project.canEditResources)) { return false; @@ -79,7 +79,7 @@ export class PluginBootstrap extends Bootstrap { return dataSource instanceof MemorySqlDataSource || dataSource instanceof LocalStorageSqlDataSource; }, handler: async (context, action) => { - const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; let dataSource: ISqlDataSource | ResourceSqlDataSource | undefined = this.sqlDataSourceService.get(state.editorId); @@ -193,7 +193,7 @@ export class PluginBootstrap extends Bootstrap { menus: [SQL_EDITOR_TOOLS_MENU], contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], isApplicable: context => { - const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; const dataSource = this.sqlDataSourceService.get(state.editorId); diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts index 411acbb648..9502a84112 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts @@ -92,7 +92,7 @@ export class SqlEditorBootstrap extends Bootstrap { root: true, contexts: [DATA_CONTEXT_CONNECTION], isApplicable: context => { - const node = context.tryGet(DATA_CONTEXT_NAV_NODE); + const node = context.get(DATA_CONTEXT_NAV_NODE); if (node && !node.objectFeatures.includes(EObjectFeature.dataSource)) { return false; @@ -106,23 +106,27 @@ export class SqlEditorBootstrap extends Bootstrap { this.actionService.addHandler({ id: 'sql-editor', isActionApplicable: (context, action) => { - if (action === ACTION_RENAME) { - const editorState = context.tryGet(DATA_CONTEXT_SQL_EDITOR_STATE); + switch (action) { + case ACTION_RENAME: { + const editorState = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); - if (!editorState) { - return false; - } + if (!editorState) { + return false; + } - const dataSource = this.sqlDataSourceService.get(editorState.editorId); + const dataSource = this.sqlDataSourceService.get(editorState.editorId); - return dataSource?.hasFeature(ESqlDataSourceFeatures.setName) ?? false; + return dataSource?.hasFeature(ESqlDataSourceFeatures.setName) ?? false; + } + case ACTION_SQL_EDITOR_OPEN: + return context.has(DATA_CONTEXT_CONNECTION); } - return action === ACTION_SQL_EDITOR_OPEN && context.has(DATA_CONTEXT_CONNECTION); + return false; }, handler: async (context, action) => { switch (action) { case ACTION_RENAME: { - const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; const dataSource = this.sqlDataSourceService.get(state.editorId); const executionContext = dataSource?.executionContext; @@ -155,7 +159,7 @@ export class SqlEditorBootstrap extends Bootstrap { break; } case ACTION_SQL_EDITOR_OPEN: { - const connection = context.get(DATA_CONTEXT_CONNECTION); + const connection = context.get(DATA_CONTEXT_CONNECTION)!; this.sqlEditorNavigatorService.openNewEditor({ dataSourceKey: LocalStorageSqlDataSource.key, diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorPanel.tsx b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorPanel.tsx index 2e33da2888..66497f60b8 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorPanel.tsx +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorPanel.tsx @@ -14,11 +14,12 @@ import { DATA_CONTEXT_SQL_EDITOR_STATE, ISqlEditorTabState, SqlEditor } from '@c export const SqlEditorPanel: TabHandlerPanelComponent = observer(function SqlEditorPanel({ tab }) { const baseTab = useTab(tab.id); + const handlerState = tab.handlerState; - useCaptureViewContext(context => { + useCaptureViewContext((context, id) => { if (baseTab.selected) { - context?.set(DATA_CONTEXT_TAB_ID, tab.id); - context?.set(DATA_CONTEXT_SQL_EDITOR_STATE, tab.handlerState); + context.set(DATA_CONTEXT_TAB_ID, tab.id, id); + context.set(DATA_CONTEXT_SQL_EDITOR_STATE, handlerState, id); } }); @@ -31,5 +32,5 @@ export const SqlEditorPanel: TabHandlerPanelComponent = obse return null; } - return ; + return ; }); diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTab.tsx b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTab.tsx index 7d42d29807..6700e23211 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTab.tsx +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTab.tsx @@ -10,7 +10,7 @@ import { useContext } from 'react'; import { IconOrImage, s, useTranslate } from '@cloudbeaver/core-blocks'; import { Connection, ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections'; -import { useDataContext } from '@cloudbeaver/core-data-context'; +import { useDataContext, useDataContextLink } from '@cloudbeaver/core-data-context'; import { useService } from '@cloudbeaver/core-di'; import { ITabData, Tab, TabIcon, TabTitle } from '@cloudbeaver/core-ui'; import { CaptureViewContext } from '@cloudbeaver/core-view'; @@ -29,16 +29,19 @@ import sqlEditorTabStyles from './SqlEditorTab.module.css'; export const SqlEditorTab: TabHandlerTabComponent = observer(function SqlEditorTab({ tab, onSelect, onClose }) { const viewContext = useContext(CaptureViewContext); const tabMenuContext = useDataContext(viewContext); + const handlerState = tab.handlerState; - tabMenuContext.set(DATA_CONTEXT_SQL_EDITOR_TAB, true); - tabMenuContext.set(DATA_CONTEXT_SQL_EDITOR_STATE, tab.handlerState); + useDataContextLink(tabMenuContext, (context, id) => { + context.set(DATA_CONTEXT_SQL_EDITOR_TAB, true, id); + context.set(DATA_CONTEXT_SQL_EDITOR_STATE, handlerState, id); + }); const sqlDataSourceService = useService(SqlDataSourceService); const connectionInfo = useService(ConnectionInfoResource); const translate = useTranslate(); - const dataSource = sqlDataSourceService.get(tab.handlerState.editorId); + const dataSource = sqlDataSourceService.get(handlerState.editorId); let connection: Connection | undefined; const executionContext = dataSource?.executionContext; @@ -46,7 +49,7 @@ export const SqlEditorTab: TabHandlerTabComponent = observer connection = connectionInfo.get(createConnectionParam(executionContext.projectId, executionContext.connectionId)); } - const name = getSqlEditorName(tab.handlerState, dataSource, connection); + const name = getSqlEditorName(handlerState, dataSource, connection); const icon = dataSource?.icon ?? '/icons/sql_script_m.svg'; const saved = dataSource?.isSaved !== false; const isScript = dataSource?.hasFeature(ESqlDataSourceFeatures.script); diff --git a/webapp/packages/plugin-sql-editor-screen/src/PluginBootstrap.ts b/webapp/packages/plugin-sql-editor-screen/src/PluginBootstrap.ts index 7253de04c0..8c238efc4e 100644 --- a/webapp/packages/plugin-sql-editor-screen/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-screen/src/PluginBootstrap.ts @@ -34,7 +34,7 @@ export class PluginBootstrap extends Bootstrap { actions: [ACTION_OPEN_IN_TAB], contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], isDisabled: context => { - const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; const dataSource = this.sqlDataSourceService.get(state.editorId); return dataSource?.executionContext === undefined; @@ -46,6 +46,7 @@ export class PluginBootstrap extends Bootstrap { id: 'sql-editor', binding: KEY_BINDING_OPEN_IN_TAB, actions: [ACTION_OPEN_IN_TAB], + contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], handler: this.openTab.bind(this), }); @@ -57,7 +58,7 @@ export class PluginBootstrap extends Bootstrap { } private openTab(contexts: IDataContextProvider, action: IAction) { - const context = contexts.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const context = contexts.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; const dataSource = this.sqlDataSourceService.get(context.editorId); if (!dataSource?.executionContext) { diff --git a/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts b/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts index 0d42569aff..1087c98d20 100644 --- a/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts +++ b/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts @@ -60,7 +60,7 @@ export class MenuBootstrap extends Bootstrap { contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], actions: [ACTION_SAVE], isActionApplicable: (context, action): boolean => { - const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; const dataSource = this.sqlDataSourceService.get(state.editorId); @@ -72,7 +72,7 @@ export class MenuBootstrap extends Bootstrap { }, handler: async (context, action) => { if (action === ACTION_SAVE) { - const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; const source = this.sqlDataSourceService.get(state.editorId); if (!source) { @@ -84,7 +84,7 @@ export class MenuBootstrap extends Bootstrap { }, isDisabled: (context, action) => { if (action === ACTION_SAVE) { - const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; const source = this.sqlDataSourceService.get(state.editorId); if (!source) { @@ -112,7 +112,7 @@ export class MenuBootstrap extends Bootstrap { menus: [SQL_EDITOR_TOOLS_MENU], contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], isApplicable: context => { - const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; const dataSource = this.sqlDataSourceService.get(state.editorId); @@ -125,8 +125,9 @@ export class MenuBootstrap extends Bootstrap { id: 'sql-editor-save', binding: KEY_BINDING_SAVE, actions: [ACTION_SAVE], + contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], handler: async context => { - const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; const source = this.sqlDataSourceService.get(state.editorId); if (!source) { @@ -151,7 +152,7 @@ export class MenuBootstrap extends Bootstrap { ], contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isActionApplicable: (contexts, action): boolean => { - const sqlEditorData = contexts.get(DATA_CONTEXT_SQL_EDITOR_DATA); + const sqlEditorData = contexts.get(DATA_CONTEXT_SQL_EDITOR_DATA)!; if (sqlEditorData.readonly && [ACTION_SQL_EDITOR_FORMAT, ACTION_REDO, ACTION_UNDO].includes(action)) { return false; @@ -187,6 +188,7 @@ export class MenuBootstrap extends Bootstrap { this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-execute', binding: KEY_BINDING_SQL_EDITOR_EXECUTE, + contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isBindingApplicable: (contexts, action) => action === ACTION_SQL_EDITOR_EXECUTE, handler: this.sqlEditorActionHandler.bind(this), }); @@ -194,6 +196,7 @@ export class MenuBootstrap extends Bootstrap { this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-execute-new', binding: KEY_BINDING_SQL_EDITOR_EXECUTE_NEW, + contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isBindingApplicable: (contexts, action) => action === ACTION_SQL_EDITOR_EXECUTE_NEW, handler: this.sqlEditorActionHandler.bind(this), }); @@ -201,8 +204,9 @@ export class MenuBootstrap extends Bootstrap { this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-execute-script', binding: KEY_BINDING_SQL_EDITOR_EXECUTE_SCRIPT, + contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isBindingApplicable: (contexts, action) => { - const sqlEditorData = contexts.tryGet(DATA_CONTEXT_SQL_EDITOR_DATA); + const sqlEditorData = contexts.get(DATA_CONTEXT_SQL_EDITOR_DATA); return action === ACTION_SQL_EDITOR_EXECUTE_SCRIPT && sqlEditorData?.dataSource?.hasFeature(ESqlDataSourceFeatures.executable) === true; }, handler: this.sqlEditorActionHandler.bind(this), @@ -211,6 +215,7 @@ export class MenuBootstrap extends Bootstrap { this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-format', binding: KEY_BINDING_SQL_EDITOR_FORMAT, + contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isBindingApplicable: (contexts, action) => action === ACTION_SQL_EDITOR_FORMAT, handler: this.sqlEditorActionHandler.bind(this), }); @@ -218,6 +223,7 @@ export class MenuBootstrap extends Bootstrap { this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-redo', binding: KEY_BINDING_REDO, + contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isBindingApplicable: (contexts, action) => action === ACTION_REDO, handler: this.sqlEditorActionHandler.bind(this), }); @@ -225,6 +231,7 @@ export class MenuBootstrap extends Bootstrap { this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-undo', binding: KEY_BINDING_UNDO, + contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isBindingApplicable: (contexts, action) => action === ACTION_UNDO, handler: this.sqlEditorActionHandler.bind(this), }); @@ -232,13 +239,14 @@ export class MenuBootstrap extends Bootstrap { this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-show-execution-plan', binding: KEY_BINDING_SQL_EDITOR_SHOW_EXECUTION_PLAN, + contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isBindingApplicable: (contexts, action) => action === ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN, handler: this.sqlEditorActionHandler.bind(this), }); // this.menuService.addCreator({ // isApplicable: context => ( - // context.tryGet(DATA_CONTEXT_SQL_EDITOR_DATA) !== undefined + // context.get(DATA_CONTEXT_SQL_EDITOR_DATA) !== undefined // && context.get(DATA_CONTEXT_MENU) === MENU_TAB // ), // getItems: (context, items) => [ @@ -249,7 +257,7 @@ export class MenuBootstrap extends Bootstrap { } private sqlEditorActionHandler(context: IDataContextProvider, action: IAction): void { - const data = context.get(DATA_CONTEXT_SQL_EDITOR_DATA); + const data = context.get(DATA_CONTEXT_SQL_EDITOR_DATA)!; switch (action) { case ACTION_SQL_EDITOR_EXECUTE: diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx index c1147773cc..0f37a9af52 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx @@ -39,8 +39,8 @@ export const SqlEditor = observer(function SqlEditor({ state, c modesState.sync(state.modeState); }, [state]); - useCaptureViewContext(context => { - context?.set(DATA_CONTEXT_SQL_EDITOR_DATA, data); + useCaptureViewContext((context, id) => { + context.set(DATA_CONTEXT_SQL_EDITOR_DATA, data, id); }); function handleModeSelect(tab: ITabData) { diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorToolsMenu.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorToolsMenu.tsx index a3e3915125..a0ade30886 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorToolsMenu.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorToolsMenu.tsx @@ -8,7 +8,7 @@ import { observer } from 'mobx-react-lite'; import { s, SContext, type StyleRegistry, useS } from '@cloudbeaver/core-blocks'; -import type { IDataContext } from '@cloudbeaver/core-data-context'; +import { type IDataContext, useDataContextLink } from '@cloudbeaver/core-data-context'; import { MenuBar, MenuBarItemStyles, MenuBarStyles } from '@cloudbeaver/core-ui'; import { useMenu } from '@cloudbeaver/core-view'; @@ -46,8 +46,13 @@ const registry: StyleRegistry = [ export const SqlEditorToolsMenu = observer(function SqlEditorToolsMenu({ data, state, context, className }) { const menuBarStyles = useS(SqlEditorActionsMenuBarStyles, SqlEditorActionsMenuBarItemStyles, MenuBarStyles, MenuBarItemStyles); const menu = useMenu({ menu: SQL_EDITOR_TOOLS_MENU, context }); - menu.context.set(DATA_CONTEXT_SQL_EDITOR_STATE, state); - context?.set(DATA_CONTEXT_SQL_EDITOR_DATA, data); + + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_SQL_EDITOR_STATE, state, id); + }); + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_SQL_EDITOR_DATA, data, id); + }); return ( diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorGroupTabsBootstrap.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorGroupTabsBootstrap.ts index f696cc4d38..dcdc162561 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorGroupTabsBootstrap.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorGroupTabsBootstrap.ts @@ -49,13 +49,13 @@ export class SqlEditorGroupTabsBootstrap extends Bootstrap { id: 'result-tabs-group-base-handler', actions: [ACTION_TAB_CLOSE_SQL_RESULT_GROUP], menus: [MENU_TAB], - contexts: [DATA_CONTEXT_SQL_EDITOR_RESULT_ID, DATA_CONTEXT_SQL_EDITOR_STATE], + contexts: [DATA_CONTEXT_SQL_EDITOR_RESULT_ID, DATA_CONTEXT_SQL_EDITOR_STATE, DATA_CONTEXT_TABS_CONTEXT], isActionApplicable: context => { - const tab = context.get(DATA_CONTEXT_SQL_EDITOR_RESULT_ID); - const sqlEditorState = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const tab = context.get(DATA_CONTEXT_SQL_EDITOR_RESULT_ID)!; + const sqlEditorState = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; - const groupId = sqlEditorState?.resultTabs.find(tabState => tabState.tabId === tab?.id)?.groupId; - const hasTabsInGroup = (sqlEditorState?.resultTabs.filter(tabState => tabState.groupId === groupId) ?? []).length > 1; + const groupId = sqlEditorState?.resultTabs.find(tabState => tabState.tabId === tab.id)?.groupId; + const hasTabsInGroup = sqlEditorState.resultTabs.filter(tabState => tabState.groupId === groupId).length > 1; return hasTabsInGroup; }, @@ -72,9 +72,9 @@ export class SqlEditorGroupTabsBootstrap extends Bootstrap { } async closeResultTabGroup(context: IDataContextProvider) { - const tab = context.get(DATA_CONTEXT_SQL_EDITOR_RESULT_ID); - const sqlEditorState = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); - const tabsContext = context.get(DATA_CONTEXT_TABS_CONTEXT); + const tab = context.get(DATA_CONTEXT_SQL_EDITOR_RESULT_ID)!; + const sqlEditorState = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; + const tabsContext = context.get(DATA_CONTEXT_TABS_CONTEXT)!; const resultTabs = this.sqlResultTabsService.getResultTabs(sqlEditorState); const resultTab = resultTabs.find(tabState => tabState.tabId === tab.id); diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsMenu.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsMenu.tsx index 0a7ba240ae..d2fc95b13d 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsMenu.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/OutputLogs/OutputLogsMenu.tsx @@ -6,9 +6,9 @@ * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import React, { useEffect } from 'react'; import { s } from '@cloudbeaver/core-blocks'; +import { useDataContextLink } from '@cloudbeaver/core-data-context'; import { MenuBar, MenuBarItemStyles } from '@cloudbeaver/core-ui'; import { useMenu } from '@cloudbeaver/core-view'; @@ -26,9 +26,9 @@ export const OutputLogsMenu = observer(function OutputLogsMenu({ sqlEdito menu: OUTPUT_LOGS_MENU, }); - useEffect(() => { - menu.context.set(DATA_CONTEXT_SQL_EDITOR_STATE, sqlEditorTabState); - }, []); + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_SQL_EDITOR_STATE, sqlEditorTabState, id); + }); return ( { - const state = context.tryGet(DATA_CONTEXT_SQL_EDITOR_STATE); + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); if (!state) { return []; @@ -86,7 +86,7 @@ export class OutputMenuBootstrap extends Bootstrap { this.menuService.addCreator({ menus: [OUTPUT_LOGS_SETTINGS_MENU], getItems: (context, items) => { - const state = context.tryGet(DATA_CONTEXT_SQL_EDITOR_STATE); + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); if (!state) { return []; @@ -121,7 +121,7 @@ export class OutputMenuBootstrap extends Bootstrap { actions: [ACTION_SHOW_OUTPUT_LOGS], contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], isActionApplicable: (context): boolean => { - const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; const sqlDataSource = this.sqlDataSourceService.get(state.editorId); const isQuery = sqlDataSource?.hasFeature(ESqlDataSourceFeatures.query); @@ -131,7 +131,7 @@ export class OutputMenuBootstrap extends Bootstrap { }, handler: async (context, action) => { - const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; if (action === ACTION_SHOW_OUTPUT_LOGS) { this.outputLogsService.showOutputLogs(state); @@ -148,8 +148,9 @@ export class OutputMenuBootstrap extends Bootstrap { id: 'sql-editor-show-output', binding: KEY_BINDING_SQL_EDITOR_SHOW_OUTPUT, actions: [ACTION_SQL_EDITOR_SHOW_OUTPUT], + contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], handler: (context, action) => { - const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; if (action === ACTION_SQL_EDITOR_SHOW_OUTPUT) { this.outputLogsService.showOutputLogs(state); diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTab.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTab.tsx index bceffe41ab..a99f3df716 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTab.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTab.tsx @@ -8,7 +8,7 @@ import { observer } from 'mobx-react-lite'; import { useContext } from 'react'; -import { useDataContext } from '@cloudbeaver/core-data-context'; +import { useDataContext, useDataContextLink } from '@cloudbeaver/core-data-context'; import { ITabData, Tab, TabIcon, TabTitle } from '@cloudbeaver/core-ui'; import { CaptureViewContext } from '@cloudbeaver/core-view'; @@ -25,7 +25,9 @@ export const SqlResultTab = observer(function SqlResultTab({ result, clas const viewContext = useContext(CaptureViewContext); const tabMenuContext = useDataContext(viewContext); - tabMenuContext.set(DATA_CONTEXT_SQL_EDITOR_RESULT_ID, result); + useDataContextLink(tabMenuContext, (context, id) => { + context.set(DATA_CONTEXT_SQL_EDITOR_RESULT_ID, result, id); + }); return ( diff --git a/webapp/packages/plugin-sql-generator/src/GeneratorMenuBootstrap.ts b/webapp/packages/plugin-sql-generator/src/GeneratorMenuBootstrap.ts index 0c87415583..b4b31375f1 100644 --- a/webapp/packages/plugin-sql-generator/src/GeneratorMenuBootstrap.ts +++ b/webapp/packages/plugin-sql-generator/src/GeneratorMenuBootstrap.ts @@ -7,48 +7,66 @@ */ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import { DatabaseEditAction, TableFooterMenuService } from '@cloudbeaver/plugin-data-viewer'; +import { ActionService, MenuService } from '@cloudbeaver/core-view'; +import { + DATA_CONTEXT_DV_DDM, + DATA_CONTEXT_DV_DDM_RESULT_INDEX, + DATA_CONTEXT_DV_PRESENTATION, + DATA_VIEWER_DATA_MODEL_ACTIONS_MENU, + DatabaseEditAction, + DataViewerPresentationType, +} from '@cloudbeaver/plugin-data-viewer'; +import { ACTION_SQL_GENERATE } from './actions/ACTION_SQL_GENERATE'; import { ScriptPreviewService } from './ScriptPreview/ScriptPreviewService'; @injectable() export class GeneratorMenuBootstrap extends Bootstrap { - constructor(private readonly scriptPreviewService: ScriptPreviewService, private readonly tableFooterMenuService: TableFooterMenuService) { + constructor( + private readonly scriptPreviewService: ScriptPreviewService, + private readonly actionService: ActionService, + private readonly menuService: MenuService, + ) { super(); } register(): void { - this.tableFooterMenuService.registerMenuItem({ - id: 'script', - order: 3, - title: 'data_viewer_script_preview', - tooltip: 'data_viewer_script_preview', - icon: 'sql-script-preview', - isPresent(context) { - return context.contextType === TableFooterMenuService.nodeContextType; - }, - isHidden(context) { + this.menuService.addCreator({ + menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + isApplicable: context => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const presentation = context.get(DATA_CONTEXT_DV_PRESENTATION); return ( - context.data.model.isReadonly(context.data.resultIndex) || - context.data.model.source.getResult(context.data.resultIndex)?.dataFormat !== ResultDataFormat.Resultset + !model.isReadonly(resultIndex) && + model.source.getResult(resultIndex)?.dataFormat === ResultDataFormat.Resultset && + !presentation?.readonly && + (!presentation || presentation.type === DataViewerPresentationType.Data) ); }, + getItems(context, items) { + return [...items, ACTION_SQL_GENERATE]; + }, + }); + + this.actionService.addHandler({ + id: 'data-sql-tools-handler', + menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], + actions: [ACTION_SQL_GENERATE], + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], isDisabled(context) { - if ( - context.data.model.isLoading() || - context.data.model.isDisabled(context.data.resultIndex) || - !context.data.model.source.hasResult(context.data.resultIndex) - ) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + if (model.isLoading() || model.isDisabled(resultIndex) || !model.source.hasResult(resultIndex)) { return true; } - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); + const editor = model.source.getActionImplementation(resultIndex, DatabaseEditAction); return !editor?.isEdited(); }, - onClick: async context => { - await this.scriptPreviewService.open(context.data.model, context.data.resultIndex); + handler: context => { + this.scriptPreviewService.open(context.get(DATA_CONTEXT_DV_DDM)!, context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!); }, }); } - - load(): void | Promise {} } diff --git a/webapp/packages/plugin-sql-generator/src/SqlGenerators/SqlGeneratorsBootstrap.ts b/webapp/packages/plugin-sql-generator/src/SqlGenerators/SqlGeneratorsBootstrap.ts index 53efd7cd76..79726846ed 100644 --- a/webapp/packages/plugin-sql-generator/src/SqlGenerators/SqlGeneratorsBootstrap.ts +++ b/webapp/packages/plugin-sql-generator/src/SqlGenerators/SqlGeneratorsBootstrap.ts @@ -33,12 +33,12 @@ export class SqlGeneratorsBootstrap extends Bootstrap { menus: [MENU_SQL_GENERATORS], contexts: [DATA_CONTEXT_NAV_NODE], isDisabled: context => { - const node = context.get(DATA_CONTEXT_NAV_NODE); + const node = context.get(DATA_CONTEXT_NAV_NODE)!; return this.sqlGeneratorsResource.get(node.id)?.length === 0; }, getLoader: (context, action) => { - const node = context.get(DATA_CONTEXT_NAV_NODE); + const node = context.get(DATA_CONTEXT_NAV_NODE)!; return getCachedMapResourceLoaderState(this.sqlGeneratorsResource, () => node.id); }, @@ -47,7 +47,7 @@ export class SqlGeneratorsBootstrap extends Bootstrap { root: true, contexts: [DATA_CONTEXT_NAV_NODE], isApplicable: context => { - const node = context.get(DATA_CONTEXT_NAV_NODE); + const node = context.get(DATA_CONTEXT_NAV_NODE)!; if (!(node.objectFeatures.includes(EObjectFeature.entity) || node.objectFeatures.includes(EObjectFeature.script))) { return false; @@ -62,7 +62,7 @@ export class SqlGeneratorsBootstrap extends Bootstrap { menus: [MENU_SQL_GENERATORS], contexts: [DATA_CONTEXT_NAV_NODE], getItems: (context, items) => { - const node = context.get(DATA_CONTEXT_NAV_NODE); + const node = context.get(DATA_CONTEXT_NAV_NODE)!; const actions = this.sqlGeneratorsResource.get(node.id) || []; diff --git a/webapp/packages/plugin-sql-generator/src/actions/ACTION_SQL_GENERATE.ts b/webapp/packages/plugin-sql-generator/src/actions/ACTION_SQL_GENERATE.ts new file mode 100644 index 0000000000..7f1de3500e --- /dev/null +++ b/webapp/packages/plugin-sql-generator/src/actions/ACTION_SQL_GENERATE.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_SQL_GENERATE = createAction('sql-generate', { + label: 'data_viewer_script_preview', + tooltip: 'data_viewer_script_preview', + icon: 'sql-script-preview', +}); diff --git a/webapp/packages/plugin-user-profile/src/UserMenu/UserMenu.tsx b/webapp/packages/plugin-user-profile/src/UserMenu/UserMenu.tsx index 311f8d3c0d..6fa9412378 100644 --- a/webapp/packages/plugin-user-profile/src/UserMenu/UserMenu.tsx +++ b/webapp/packages/plugin-user-profile/src/UserMenu/UserMenu.tsx @@ -9,6 +9,7 @@ import { observer } from 'mobx-react-lite'; import { AuthInfoService, DATA_CONTEXT_USER } from '@cloudbeaver/core-authentication'; import { Icon, Loader, s, useS } from '@cloudbeaver/core-blocks'; +import { useDataContextLink } from '@cloudbeaver/core-data-context'; import { useService } from '@cloudbeaver/core-di'; import { ContextMenu } from '@cloudbeaver/core-ui'; import { useMenu } from '@cloudbeaver/core-view'; @@ -21,16 +22,19 @@ export const UserMenu = observer(function UserMenu() { const styles = useS(style); const authInfoService = useService(AuthInfoService); const menu = useMenu({ menu: MENU_USER_PROFILE }); + const userInfo = authInfoService.userInfo; - menu.context.set(DATA_CONTEXT_USER, authInfoService.userInfo); + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_USER, userInfo, id); + }); - if (!authInfoService.userInfo) { + if (!userInfo) { return null; } return ( - + diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/DATA_CONTEXT_USER_PROFILE_FORM_AUTHENTICATION_PART.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/DATA_CONTEXT_USER_PROFILE_FORM_AUTHENTICATION_PART.ts deleted file mode 100644 index 4064d7b3f7..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/DATA_CONTEXT_USER_PROFILE_FORM_AUTHENTICATION_PART.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 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 { PasswordPolicyService, UserInfoResource } from '@cloudbeaver/core-authentication'; -import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; -import { DATA_CONTEXT_FORM_STATE } from '@cloudbeaver/core-ui'; - -import type { UserProfileFormState } from '../UserProfileFormState'; -import { UserProfileFormAuthenticationPart } from './UserProfileFormAuthenticationPart'; - -export const DATA_CONTEXT_USER_PROFILE_FORM_AUTHENTICATION_PART = createDataContext( - 'User Profile Form Info Part', - context => { - const form = context.get(DATA_CONTEXT_FORM_STATE) as UserProfileFormState; - const di = context.get(DATA_CONTEXT_DI_PROVIDER); - const userInfoResource = di.getServiceByClass(UserInfoResource); - const passwordPolicyService = di.getServiceByClass(PasswordPolicyService); - - return new UserProfileFormAuthenticationPart(form, userInfoResource, passwordPolicyService); - }, -); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPart.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPart.ts index c8b53684ad..243bc0244f 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPart.ts +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPart.ts @@ -13,13 +13,12 @@ import { FormPart, formValidationContext, IFormState } from '@cloudbeaver/core-u import { isValuesEqual, schemaValidationError } from '@cloudbeaver/core-utils'; import type { IUserProfileFormState } from '../UserProfileFormService'; -import type { UserProfileFormState } from '../UserProfileFormState'; import { type IUserProfileFormAuthenticationState, USER_PROFILE_FORM_AUTHENTICATION_PART_STATE_SCHEMA } from './IUserProfileFormAuthenticationState'; export class UserProfileFormAuthenticationPart extends FormPart { private baseIncludes: CachedResourceIncludeArgs; constructor( - formState: UserProfileFormState, + formState: IFormState, private readonly userInfoResource: UserInfoResource, private readonly passwordPolicyService: PasswordPolicyService, ) { diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPartBootstrap.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPartBootstrap.ts index 075b8f18db..d93c83e7ee 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPartBootstrap.ts +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPartBootstrap.ts @@ -9,7 +9,7 @@ import { importLazyComponent } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { UserProfileFormService } from '../UserProfileFormService'; -import { DATA_CONTEXT_USER_PROFILE_FORM_AUTHENTICATION_PART } from './DATA_CONTEXT_USER_PROFILE_FORM_AUTHENTICATION_PART'; +import { getUserProfileFormAuthenticationPart } from './getUserProfileFormAuthenticationPart'; const AuthenticationPanel = importLazyComponent(() => import('./AuthenticationPanel').then(m => m.AuthenticationPanel)); @@ -25,7 +25,7 @@ export class UserProfileFormAuthenticationPartBootstrap extends Bootstrap { name: 'ui_authentication', order: 2, panel: () => AuthenticationPanel, - stateGetter: props => () => props.formState.dataContext.get(DATA_CONTEXT_USER_PROFILE_FORM_AUTHENTICATION_PART), + stateGetter: props => () => getUserProfileFormAuthenticationPart(props.formState), }); } } diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/getUserProfileFormAuthenticationPart.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/getUserProfileFormAuthenticationPart.ts new file mode 100644 index 0000000000..ed8073f81c --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/getUserProfileFormAuthenticationPart.ts @@ -0,0 +1,25 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { PasswordPolicyService, UserInfoResource } from '@cloudbeaver/core-authentication'; +import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; +import type { IFormState } from '@cloudbeaver/core-ui'; + +import type { IUserProfileFormState } from '../UserProfileFormService'; +import { UserProfileFormAuthenticationPart } from './UserProfileFormAuthenticationPart'; + +const DATA_CONTEXT_USER_PROFILE_FORM_AUTHENTICATION_PART = createDataContext('User Profile Form Info Part'); + +export function getUserProfileFormAuthenticationPart(formState: IFormState): UserProfileFormAuthenticationPart { + return formState.getPart(DATA_CONTEXT_USER_PROFILE_FORM_AUTHENTICATION_PART, context => { + const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; + const userInfoResource = di.getServiceByClass(UserInfoResource); + const passwordPolicyService = di.getServiceByClass(PasswordPolicyService); + + return new UserProfileFormAuthenticationPart(formState, userInfoResource, passwordPolicyService); + }); +} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/DATA_CONTEXT_USER_PROFILE_FORM_INFO_PART.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/DATA_CONTEXT_USER_PROFILE_FORM_INFO_PART.ts deleted file mode 100644 index 7a7b7e339b..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/DATA_CONTEXT_USER_PROFILE_FORM_INFO_PART.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 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 { UserInfoResource } from '@cloudbeaver/core-authentication'; -import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; -import { DATA_CONTEXT_FORM_STATE } from '@cloudbeaver/core-ui'; - -import type { UserProfileFormState } from '../UserProfileFormState'; -import { UserProfileFormInfoPart } from './UserProfileFormInfoPart'; - -export const DATA_CONTEXT_USER_PROFILE_FORM_INFO_PART = createDataContext('User Profile Form Info Part', context => { - const form = context.get(DATA_CONTEXT_FORM_STATE) as UserProfileFormState; - const di = context.get(DATA_CONTEXT_DI_PROVIDER); - const userInfoResource = di.getServiceByClass(UserInfoResource); - - return new UserProfileFormInfoPart(form, userInfoResource); -}); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPart.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPart.ts index 79fdc406c9..66475cd72b 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPart.ts +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPart.ts @@ -15,12 +15,14 @@ import { FormPart, IFormState } from '@cloudbeaver/core-ui'; import { isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils'; import type { IUserProfileFormState } from '../UserProfileFormService'; -import type { UserProfileFormState } from '../UserProfileFormState'; import { type IUserProfileFormInfoState, USER_PROFILE_FORM_INFO_PART_STATE_SCHEMA } from './IUserProfileFormInfoState'; export class UserProfileFormInfoPart extends FormPart { private baseIncludes: CachedResourceIncludeArgs; - constructor(formState: UserProfileFormState, private readonly userInfoResource: UserInfoResource) { + constructor( + formState: IFormState, + private readonly userInfoResource: UserInfoResource, + ) { super(formState, { userId: userInfoResource.data?.userId || '', displayName: userInfoResource.data?.displayName || '', diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPartBootstrap.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPartBootstrap.ts index 10b00821d0..51475b8bf3 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPartBootstrap.ts +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPartBootstrap.ts @@ -9,7 +9,7 @@ import { importLazyComponent } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { UserProfileFormService } from '../UserProfileFormService'; -import { DATA_CONTEXT_USER_PROFILE_FORM_INFO_PART } from './DATA_CONTEXT_USER_PROFILE_FORM_INFO_PART'; +import { getUserProfileFormInfoPart } from './getUserProfileFormInfoPart'; const UserProfileFormInfo = importLazyComponent(() => import('./UserProfileFormInfo').then(m => m.UserProfileFormInfo)); @@ -25,7 +25,7 @@ export class UserProfileFormInfoPartBootstrap extends Bootstrap { name: 'plugin_user_profile_info', order: 1, panel: () => UserProfileFormInfo, - stateGetter: props => () => props.formState.getPart(DATA_CONTEXT_USER_PROFILE_FORM_INFO_PART), + stateGetter: props => () => getUserProfileFormInfoPart(props.formState), }); } } diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/getUserProfileFormInfoPart.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/getUserProfileFormInfoPart.ts new file mode 100644 index 0000000000..1a92fa6115 --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/getUserProfileFormInfoPart.ts @@ -0,0 +1,24 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { UserInfoResource } from '@cloudbeaver/core-authentication'; +import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; +import type { IFormState } from '@cloudbeaver/core-ui'; + +import type { IUserProfileFormState } from '../UserProfileFormService'; +import { UserProfileFormInfoPart } from './UserProfileFormInfoPart'; + +const DATA_CONTEXT_USER_PROFILE_FORM_INFO_PART = createDataContext('User Profile Form Info Part'); + +export function getUserProfileFormInfoPart(formState: IFormState): UserProfileFormInfoPart { + return formState.getPart(DATA_CONTEXT_USER_PROFILE_FORM_INFO_PART, context => { + const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; + const userInfoResource = di.getServiceByClass(UserInfoResource); + + return new UserProfileFormInfoPart(formState, userInfoResource); + }); +}