From 28cade86aa12ab9303664da2ea757b255f885579 Mon Sep 17 00:00:00 2001 From: Sychev Andrey <44414066+SychevAndrey@users.noreply.github.com> Date: Thu, 23 Oct 2025 11:04:41 +0200 Subject: [PATCH] dbeaver/pro#6026 feat: merge value panel tabs in one group (#3829) * dbeaver/pro#6026 feat: merge value panel tabs in one group move all text representations to the first tab level specific representation is used if present (boolean, image) auto type detection is used only for text * dbeaver/pro#6026 refactor: remove unused CSS files for TextValuePresentation * dbeaver/pro#6026 refactor: remove automatic state update in ValuePanel component * dbeaver/pro#6026 refactor: update content type handling in ValuePanel Move all the content type determination logic into the hook Simplify the component * dbeaver/pro#6026 refactor: consolidate content type handling in useAutoContentType hook * dbeaver/pro#6026 fix: show scroll on tabs in value panel when overflow --- .../ValuePanel/DataValuePanelService.ts | 3 +- .../src/TableViewer/ValuePanel/ValuePanel.tsx | 18 ++- .../ValuePanel/shared/ValuePanel.module.css | 2 + .../TextValue/TextValuePresentation.tsx | 57 +-------- .../TextValuePresentationBootstrap.ts | 121 +++++++++--------- .../TextValue/TextValuePresentationService.ts | 34 ----- .../isTextValuePresentationAvailable.ts | 4 +- .../shared/TextValuePresentation.module.css | 13 -- .../TextValuePresentationTab.module.css | 18 --- .../TextValue/useAutoContentType.ts | 44 +++---- .../TextValue/useContentType.ts | 40 ++++++ .../packages/plugin-data-viewer/src/module.ts | 2 - 12 files changed, 139 insertions(+), 217 deletions(-) delete mode 100644 webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationService.ts delete mode 100644 webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/shared/TextValuePresentation.module.css delete mode 100644 webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/shared/TextValuePresentationTab.module.css create mode 100644 webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useContentType.ts diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/DataValuePanelService.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/DataValuePanelService.ts index 86bbc2f771..1c463611fa 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/DataValuePanelService.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/DataValuePanelService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataMo export interface IDataValuePanelOptions { dataFormat: ResultDataFormat[]; + isTextPresentation?: boolean; } export interface IDataValuePanelProps { diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx index fdb8405686..8b43ddecb1 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx @@ -22,6 +22,9 @@ import ValuePanelTab from './shared/ValuePanelTab.module.css'; import { IDatabaseDataSelectAction } from '../../DatabaseDataModel/Actions/IDatabaseDataSelectAction.js'; import { IDatabaseDataResultAction } from '../../DatabaseDataModel/Actions/IDatabaseDataResultAction.js'; import { IDatabaseDataMetadataAction } from '../../DatabaseDataModel/Actions/IDatabaseDataMetadataAction.js'; +import { IDatabaseDataFormatAction } from '../../DatabaseDataModel/Actions/IDatabaseDataFormatAction.js'; +import type { IGridDataKey } from '../../DatabaseDataModel/Actions/Grid/IGridDataKey.js'; +import { useContentType } from '../../ValuePanelPresentation/TextValue/useContentType.js'; const tabListRegistry: StyleRegistry = [[TabStyles, { mode: 'append', styles: [ValuePanelTab] }]]; @@ -35,7 +38,7 @@ export const ValuePanel: DataPresentationComponent = observer(function ValuePane const selectAction = model.source.tryGetAction(resultIndex, IDatabaseDataSelectAction); const dataResultAction = model.source.tryGetAction(resultIndex, IDatabaseDataResultAction); const metadataAction = model.source.getAction(resultIndex, IDatabaseDataMetadataAction); - const activeElements = selectAction?.getActiveElements(); + const activeElements = selectAction?.getActiveElements() as IGridDataKey[] | undefined; let elementKey: string | null = null; const style = useS(styles); @@ -58,13 +61,14 @@ export const ValuePanel: DataPresentationComponent = observer(function ValuePane ); const displayed = service.getDisplayed({ dataFormat, model, resultIndex }); - let currentTabId = state.currentTabId; - const hasCurrentTabCells = currentTabId && displayed.some(tab => tab.key === currentTabId); - - if (displayed.length > 0 && !hasCurrentTabCells) { - currentTabId = displayed[0]!.key; - } + const currentTabId = useContentType({ + model: model as any, + currentContentType: state.currentTabId, + elementKey: activeElements && activeElements.length > 0 ? activeElements[0] : undefined, + formatAction: model.source.tryGetAction(resultIndex, IDatabaseDataFormatAction), + displayed, + }); return ( = observer(function TextValuePresentation({ + tabId, model: unknownModel, resultIndex, dataFormat, @@ -44,9 +39,7 @@ export const TextValuePresentation: TabContainerPanelComponent observable({ lineWrapping: null as boolean | null, - currentContentType: null as string | null, - - setContentType(contentType: string | null) { - this.currentContentType = contentType; - }, setLineWrapping(lineWrapping: boolean | null) { this.lineWrapping = lineWrapping; }, }), ); - const contentType = useAutoContentType({ - dataFormat, - model, - resultIndex, - currentContentType: state.currentContentType, - elementKey: firstSelectedCell, - formatAction, - }); + const textValueGetter = useTextValueGetter({ contentAction, editAction, formatAction, dataFormat, - contentType, + contentType: tabId, elementKey: firstSelectedCell, }); - const autoLineWrapping = getDefaultLineWrapping(contentType); + const autoLineWrapping = getDefaultLineWrapping(tabId); const lineWrapping = state.lineWrapping ?? autoLineWrapping; const isReadonly = isTextValueReadonly({ model, resultIndex, contentAction, cell: firstSelectedCell, formatAction, editAction }); const canSave = firstSelectedCell && contentAction.isDownloadable(firstSelectedCell) && dataViewerService.canExportData; @@ -106,40 +87,16 @@ export const TextValuePresentation: TabContainerPanelComponent - - - selectTabHandler(tab.tabId)} - > - - - - - - import('./TextValuePresentation.js').then(module => module.TextValuePresentation)); -@injectable(() => [TextValuePresentationService, DataValuePanelService]) +const PRESENTATIONS = [ + { + key: 'text/plain', + name: 'data_viewer_presentation_value_text_title', + }, + { + key: 'text/html', + name: 'data_viewer_presentation_value_text_html_title', + }, + { + key: 'text/xml', + name: 'data_viewer_presentation_value_text_xml_title', + }, + { + key: 'application/json', + name: 'data_viewer_presentation_value_text_json_title', + }, + { + key: 'application/octet-stream;type=hex', + name: 'data_viewer_presentation_value_text_hex_title', + isHidden: (context: IDataValuePanelProps | undefined) => !isBlobPresentationAvailable(context), + }, + { + key: 'application/octet-stream;type=base64', + name: 'data_viewer_presentation_value_text_base64_title', + isHidden: (context: IDataValuePanelProps | undefined) => !isBlobPresentationAvailable(context), + }, +]; + +@injectable(() => [DataValuePanelService]) export class TextValuePresentationBootstrap extends Bootstrap { - constructor( - private readonly textValuePresentationService: TextValuePresentationService, - private readonly dataValuePanelService: DataValuePanelService, - ) { + constructor(private readonly dataValuePanelService: DataValuePanelService) { super(); } override register(): void { - this.dataValuePanelService.add({ - key: 'text-presentation', - options: { - dataFormat: [ResultDataFormat.Resultset], - }, - name: 'data_viewer_presentation_value_text_title', - order: Number.MAX_SAFE_INTEGER, - panel: () => TextValuePresentation, - isHidden(_, props) { - return !props || !props.model.source.hasResult(props.resultIndex) || !isResultSetDataSource(props.model.source); - }, - }); + for (const presentation of PRESENTATIONS) { + this.dataValuePanelService.add({ + key: presentation.key, + options: { + dataFormat: [ResultDataFormat.Resultset], + isTextPresentation: true, + }, + name: presentation.name, + order: Number.MAX_SAFE_INTEGER, + panel: () => TextValuePresentation, + isHidden: (_, props) => { + if (!props || !props.model.source.hasResult(props.resultIndex) || !isResultSetDataSource(props.model.source)) { + return true; + } - this.textValuePresentationService.add({ - key: 'text/plain', - name: 'data_viewer_presentation_value_text_plain_title', - order: Number.MAX_SAFE_INTEGER, - panel: () => React.Fragment, - }); - this.textValuePresentationService.add({ - key: 'text/html', - name: 'data_viewer_presentation_value_text_html_title', - order: Number.MAX_SAFE_INTEGER, - panel: () => React.Fragment, - // isHidden: (_, context) => isBlobPresentationAvailable(context), - }); - this.textValuePresentationService.add({ - key: 'text/xml', - name: 'data_viewer_presentation_value_text_xml_title', - order: Number.MAX_SAFE_INTEGER, - panel: () => React.Fragment, - // isHidden: (_, context) => isBlobPresentationAvailable(context), - }); - this.textValuePresentationService.add({ - key: 'application/json', - name: 'data_viewer_presentation_value_text_json_title', - order: Number.MAX_SAFE_INTEGER, - panel: () => React.Fragment, - // isHidden: (_, context) => isBlobPresentationAvailable(context), - }); + if (!presentation.isHidden) { + return false; + } - this.textValuePresentationService.add({ - key: 'application/octet-stream;type=hex', - name: 'data_viewer_presentation_value_text_hex_title', - order: Number.MAX_SAFE_INTEGER, - panel: () => React.Fragment, - isHidden: (_, context) => !isBlobPresentationAvailable(context), - }); - this.textValuePresentationService.add({ - key: 'application/octet-stream;type=base64', - name: 'data_viewer_presentation_value_text_base64_title', - order: Number.MAX_SAFE_INTEGER, - panel: () => React.Fragment, - isHidden: (_, context) => !isBlobPresentationAvailable(context), - }); + return presentation.isHidden({ + dataFormat: props.dataFormat, + model: props.model, + resultIndex: props.resultIndex, + }); + }, + }); + } } } diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationService.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationService.ts deleted file mode 100644 index 4a583185fc..0000000000 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationService.ts +++ /dev/null @@ -1,34 +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 { injectable } from '@cloudbeaver/core-di'; -import { type ITabInfo, type ITabInfoOptions, TabsContainer } from '@cloudbeaver/core-ui'; - -import { type IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel.js'; -import { ResultSetDataSource } from '../../ResultSet/ResultSetDataSource.js'; -import type { IDataValuePanelOptions, IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService.js'; - -export interface ITextValuePanelProps extends Omit { - model: IDatabaseDataModel; -} - -@injectable() -export class TextValuePresentationService { - readonly tabs: TabsContainer; - - constructor() { - this.tabs = new TabsContainer('Value presentation'); - } - - get(tabId: string): ITabInfo | undefined { - return this.tabs.getTabInfo(tabId); - } - - add(tabInfo: ITabInfoOptions): void { - this.tabs.add(tabInfo); - } -} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/isTextValuePresentationAvailable.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/isTextValuePresentationAvailable.ts index 43d4169337..f4c82963ff 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/isTextValuePresentationAvailable.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/isTextValuePresentationAvailable.ts @@ -9,9 +9,9 @@ import { isResultSetBinaryValue } from '@dbeaver/result-set-api'; import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue.js'; import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.js'; import { ResultSetViewAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetViewAction.js'; -import { type ITextValuePanelProps } from './TextValuePresentationService.js'; +import type { IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService.js'; -export function isBlobPresentationAvailable(context: ITextValuePanelProps | undefined): boolean { +export function isBlobPresentationAvailable(context: IDataValuePanelProps | undefined): boolean { const source = context?.model.source; if (!context || !source?.hasResult(context.resultIndex)) { return true; diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/shared/TextValuePresentation.module.css b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/shared/TextValuePresentation.module.css deleted file mode 100644 index a26001f695..0000000000 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/shared/TextValuePresentation.module.css +++ /dev/null @@ -1,13 +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. - */ - -.tabList { - composes: theme-border-color-background theme-background-background from global; - overflow: auto; - border-radius: var(--theme-group-element-radius); -} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/shared/TextValuePresentationTab.module.css b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/shared/TextValuePresentationTab.module.css deleted file mode 100644 index e473724bad..0000000000 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/shared/TextValuePresentationTab.module.css +++ /dev/null @@ -1,18 +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. - */ -.tab { - composes: theme-ripple theme-background-surface theme-text-text-primary-on-light from global; -} - -.textValuePresentationTab.underline .tab { - border-bottom: 0; - - &:global([aria-selected='false']) { - border-bottom: 0 !important; - } -} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useAutoContentType.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useAutoContentType.ts index fdbe9c6413..1d064d60d2 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useAutoContentType.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useAutoContentType.ts @@ -5,25 +5,23 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { useService } from '@cloudbeaver/core-di'; -import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; +import type { ITabInfo } from '@cloudbeaver/core-ui'; import { isResultSetContentValue } from '@dbeaver/result-set-api'; import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue.js'; import type { IResultSetValue } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.js'; import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel.js'; -import { ResultSetDataSource } from '../../ResultSet/ResultSetDataSource.js'; -import { TextValuePresentationService } from './TextValuePresentationService.js'; import type { IDatabaseDataFormatAction } from '../../DatabaseDataModel/Actions/IDatabaseDataFormatAction.js'; import type { IGridDataKey } from '../../DatabaseDataModel/Actions/Grid/IGridDataKey.js'; +import { isResultSetDataModel } from '../../ResultSet/isResultSetDataModel.js'; +import type { IDatabaseDataSource } from '../../DatabaseDataModel/IDatabaseDataSource.js'; +import type { IDataValuePanelOptions, IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService.js'; -interface Args { - resultIndex: number; - model: IDatabaseDataModel; - dataFormat: ResultDataFormat | null; - currentContentType: string | null; +interface UseAutoContentTypeArgs { + model: IDatabaseDataModel; elementKey?: IGridDataKey; - formatAction: IDatabaseDataFormatAction; + formatAction?: IDatabaseDataFormatAction; + displayed: Array>; } const DEFAULT_CONTENT_TYPE = 'text/plain'; @@ -55,24 +53,16 @@ function preprocessDefaultContentType(contentType: string | null | undefined) { return DEFAULT_CONTENT_TYPE; } -export function useAutoContentType({ dataFormat, model, formatAction, resultIndex, currentContentType, elementKey }: Args) { - const textValuePresentationService = useService(TextValuePresentationService); - const activeTabs = textValuePresentationService.tabs.getDisplayed({ - dataFormat: dataFormat, - model, - resultIndex: resultIndex, - }); +export function useAutoContentType({ displayed, formatAction, model, elementKey }: UseAutoContentTypeArgs): string | null { + const isAllTextType = displayed.every(tab => tab.options?.isTextPresentation); + const isAutoType = isAllTextType && formatAction && isResultSetDataModel(model); + + if (!isAutoType) { + return null; + } + const contentValue = elementKey ? formatAction.get(elementKey) : null; const contentValueType = getContentTypeFromResultSetValue(contentValue); - const defaultContentType = preprocessDefaultContentType(contentValueType); - if (currentContentType === null) { - currentContentType = defaultContentType; - } - - if (activeTabs.length > 0 && !activeTabs.some(tab => tab.key === currentContentType)) { - currentContentType = activeTabs[0]!.key; - } - - return currentContentType; + return preprocessDefaultContentType(contentValueType); } diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useContentType.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useContentType.ts new file mode 100644 index 0000000000..5eebbb769f --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useContentType.ts @@ -0,0 +1,40 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { ITabInfo } from '@cloudbeaver/core-ui'; +import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel.js'; +import type { IDatabaseDataFormatAction } from '../../DatabaseDataModel/Actions/IDatabaseDataFormatAction.js'; +import type { IGridDataKey } from '../../DatabaseDataModel/Actions/Grid/IGridDataKey.js'; +import type { IDatabaseDataSource } from '../../DatabaseDataModel/IDatabaseDataSource.js'; +import type { IDataValuePanelOptions, IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService.js'; +import { useAutoContentType } from './useAutoContentType.js'; + +interface UseContentTypeArgs { + model: IDatabaseDataModel; + currentContentType: string | null; + elementKey?: IGridDataKey; + formatAction?: IDatabaseDataFormatAction; + displayed: Array>; +} + +const DEFAULT_CONTENT_TYPE = 'text/plain'; + +export function useContentType({ model, formatAction, currentContentType, elementKey, displayed }: UseContentTypeArgs): string { + const autoContentType = useAutoContentType({ displayed, formatAction, model, elementKey }); + if (displayed.length === 0) { + return currentContentType || DEFAULT_CONTENT_TYPE; + } + + currentContentType = currentContentType || autoContentType; + const hasCurrentTab = currentContentType && displayed.some(tab => tab.key === currentContentType); + + if (!hasCurrentTab) { + currentContentType = currentContentType || displayed[0]!.key; + } + + return currentContentType || DEFAULT_CONTENT_TYPE; +} diff --git a/webapp/packages/plugin-data-viewer/src/module.ts b/webapp/packages/plugin-data-viewer/src/module.ts index 1393d605e8..7812eb2206 100644 --- a/webapp/packages/plugin-data-viewer/src/module.ts +++ b/webapp/packages/plugin-data-viewer/src/module.ts @@ -8,7 +8,6 @@ import { Bootstrap, conditional, Dependency, external, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; import { TextValuePresentationBootstrap } from './ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.js'; -import { TextValuePresentationService } from './ValuePanelPresentation/TextValue/TextValuePresentationService.js'; import { ImageValuePresentationBootstrap } from './ValuePanelPresentation/ImageValue/ImageValuePresentationBootstrap.js'; import { BooleanValuePresentationBootstrap } from './ValuePanelPresentation/BooleanValue/BooleanValuePresentationBootstrap.js'; import { TableViewerStorageService } from './TableViewer/TableViewerStorageService.js'; @@ -72,7 +71,6 @@ export default ModuleRegistry.add({ .addSingleton(Dependency, proxy(DataViewerSettingsService)) .addSingleton(DataPresentationService) .addSingleton(TextValuePresentationBootstrap) - .addSingleton(TextValuePresentationService) .addSingleton(ImageValuePresentationBootstrap) .addSingleton(BooleanValuePresentationBootstrap) .addSingleton(TableViewerStorageService)