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)