diff --git a/webapp/packages/core-di/src/App.ts b/webapp/packages/core-di/src/App.ts index 623e7e12c0..b6a8e225c9 100644 --- a/webapp/packages/core-di/src/App.ts +++ b/webapp/packages/core-di/src/App.ts @@ -17,6 +17,7 @@ import { IDiWrapper, inversifyWrapper } from './inversifyWrapper'; import type { PluginManifest } from './PluginManifest'; export interface IStartData { + restart: boolean; preload: boolean; } @@ -29,11 +30,14 @@ export class App { constructor(plugins: PluginManifest[] = []) { this.plugins = plugins; - this.onStart = new Executor(); + this.onStart = new Executor(undefined, () => true); this.loadedServices = new Map(); this.isAppServiceBound = false; - this.onStart.addHandler(async ({ preload }) => { + this.onStart.addHandler(async ({ restart, preload }) => { + if (preload && restart) { + this.dispose(); + } await this.registerServices(preload); await this.initializeServices(preload); await this.loadServices(preload); @@ -44,14 +48,13 @@ export class App { }); } - async start(): Promise { - await this.onStart.execute({ preload: true }); - await this.onStart.execute({ preload: false }); + async start(restart = false): Promise { + await this.onStart.execute({ preload: true, restart }); + await this.onStart.execute({ preload: false, restart }); } async restart(): Promise { - this.dispose(); - await this.start(); + await this.start(true); } dispose(): void { diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/useResultSetActions.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/useResultSetActions.ts deleted file mode 100644 index 36da10e1dc..0000000000 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/useResultSetActions.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { computed, observable } from 'mobx'; - -import { useObservableRef } from '@cloudbeaver/core-blocks'; - -import type { IDatabaseDataModel } from '../../IDatabaseDataModel'; -import type { IDatabaseResultSet } from '../../IDatabaseResultSet'; -import { ResultSetConstraintAction } from './ResultSetConstraintAction'; -import { ResultSetDataAction } from './ResultSetDataAction'; -import { ResultSetDataContentAction } from './ResultSetDataContentAction'; -import { ResultSetEditAction } from './ResultSetEditAction'; -import { ResultSetFormatAction } from './ResultSetFormatAction'; -import { ResultSetSelectAction } from './ResultSetSelectAction'; -import { ResultSetViewAction } from './ResultSetViewAction'; - -interface IResultActionsArgs { - resultIndex: number; - model: IDatabaseDataModel; -} - -export function useResultSetActions({ model, resultIndex }: IResultActionsArgs) { - return useObservableRef( - () => ({ - get dataAction(): ResultSetDataAction { - return this.model.source.getAction(this.resultIndex, ResultSetDataAction); - }, - get selectAction(): ResultSetSelectAction { - return this.model.source.getAction(this.resultIndex, ResultSetSelectAction); - }, - get editAction(): ResultSetEditAction { - return this.model.source.getAction(this.resultIndex, ResultSetEditAction); - }, - get contentAction(): ResultSetDataContentAction { - return this.model.source.getAction(this.resultIndex, ResultSetDataContentAction); - }, - get formatAction(): ResultSetFormatAction { - return this.model.source.getAction(this.resultIndex, ResultSetFormatAction); - }, - get constraintAction(): ResultSetConstraintAction { - return this.model.source.getAction(this.resultIndex, ResultSetConstraintAction); - }, - get viewAction(): ResultSetViewAction { - return this.model.source.getAction(this.resultIndex, ResultSetViewAction); - }, - }), - { - dataAction: computed, - selectAction: computed, - editAction: computed, - contentAction: computed, - formatAction: computed, - constraintAction: computed, - viewAction: computed, - model: observable.ref, - resultIndex: observable.ref, - }, - { model, resultIndex }, - ); -} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentation.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentation.tsx index adb957101b..5d97727c6e 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentation.tsx @@ -9,6 +9,7 @@ import { observer } from 'mobx-react-lite'; import { Radio, TextPlaceholder, useTranslate } from '@cloudbeaver/core-blocks'; import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui'; +import { isDefined } from '@cloudbeaver/core-utils'; import { ResultSetEditAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetEditAction'; import { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; @@ -17,41 +18,34 @@ import { ResultSetViewAction } from '../../DatabaseDataModel/Actions/ResultSet/R import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet'; import type { IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService'; import classes from './BooleanValuePresentation.module.css'; -import { isStringifiedBoolean } from './isBooleanValuePresentationAvailable'; +import { preprocessBooleanValue } from './preprocessBooleanValue'; export const BooleanValuePresentation: TabContainerPanelComponent> = observer( function BooleanValuePresentation({ model, resultIndex }) { const translate = useTranslate(); - const selection = model.source.getAction(resultIndex, ResultSetSelectAction); - const activeElements = selection.getActiveElements(); - if (activeElements.length === 0) { - return null; - } + const selectAction = model.source.getAction(resultIndex, ResultSetSelectAction); + const viewAction = model.source.getAction(resultIndex, ResultSetViewAction); + const editAction = model.source.getAction(resultIndex, ResultSetEditAction); + const formatAction = model.source.getAction(resultIndex, ResultSetFormatAction); - let value: boolean | null | undefined; + const activeElements = selectAction.getActiveElements(); - const view = model.source.getAction(resultIndex, ResultSetViewAction); - const editor = model.source.getAction(resultIndex, ResultSetEditAction); + if (activeElements.length === 0) { + return {translate('data_viewer_presentation_value_no_active_elements')}; + } const firstSelectedCell = activeElements[0]; - const cellValue = view.getCellValue(firstSelectedCell); + const cellValue = viewAction.getCellValue(firstSelectedCell); + const value = preprocessBooleanValue(cellValue); - if (typeof cellValue === 'string' && isStringifiedBoolean(cellValue)) { - value = cellValue.toLowerCase() === 'true'; - } else if (typeof cellValue === 'boolean' || cellValue === null) { - value = cellValue; - } - - if (value === undefined) { + if (!isDefined(value)) { return {translate('data_viewer_presentation_value_boolean_placeholder')}; } - const format = model.source.getAction(resultIndex, ResultSetFormatAction); - - const column = view.getColumn(firstSelectedCell.column); + const column = viewAction.getColumn(firstSelectedCell.column); const nullable = column?.required === false; - const readonly = model.isReadonly(resultIndex) || model.isDisabled(resultIndex) || format.isReadOnly(firstSelectedCell); + const readonly = model.isReadonly(resultIndex) || model.isDisabled(resultIndex) || formatAction.isReadOnly(firstSelectedCell); return (
@@ -61,7 +55,7 @@ export const BooleanValuePresentation: TabContainerPanelComponent editor.set(firstSelectedCell, true)} + onClick={() => editAction.set(firstSelectedCell, true)} > TRUE @@ -71,7 +65,7 @@ export const BooleanValuePresentation: TabContainerPanelComponent editor.set(firstSelectedCell, false)} + onClick={() => editAction.set(firstSelectedCell, false)} > FALSE @@ -82,7 +76,7 @@ export const BooleanValuePresentation: TabContainerPanelComponent editor.set(firstSelectedCell, null)} + onClick={() => editAction.set(firstSelectedCell, null)} > NULL diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentationBootstrap.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentationBootstrap.ts index ca8c277c23..b12d9126aa 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentationBootstrap.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentationBootstrap.ts @@ -38,17 +38,11 @@ export class BooleanValuePresentationBootstrap extends Bootstrap { if (activeElements.length > 0) { const view = context.model.source.getAction(context.resultIndex, ResultSetViewAction); - const firstSelectedCell = activeElements[0]; const cellValue = view.getCellValue(firstSelectedCell); - - if (cellValue === undefined) { - return true; - } - const column = view.getColumn(firstSelectedCell.column); - return column === undefined || !isBooleanValuePresentationAvailable(cellValue, column); + return cellValue === undefined || column === undefined || !isBooleanValuePresentationAvailable(cellValue, column); } return true; diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/preprocessBooleanValue.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/preprocessBooleanValue.ts new file mode 100644 index 0000000000..27860aeff1 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/preprocessBooleanValue.ts @@ -0,0 +1,21 @@ +/* + * 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 { IResultSetValue } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; +import { isStringifiedBoolean } from './isBooleanValuePresentationAvailable'; + +export function preprocessBooleanValue(cellValue: IResultSetValue): boolean | null | undefined { + if (typeof cellValue === 'string' && isStringifiedBoolean(cellValue)) { + return cellValue.toLowerCase() === 'true'; + } + + if (typeof cellValue === 'boolean' || cellValue === null) { + return cellValue; + } + + return undefined; +} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx index 768e6fc4e7..e32b393f31 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx @@ -5,39 +5,26 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { action, computed, observable } from 'mobx'; +import { action, observable } from 'mobx'; import { observer } from 'mobx-react-lite'; import { useMemo } from 'react'; -import { ActionIconButton, Button, Container, Fill, Loader, s, useObservableRef, useS, useSuspense, useTranslate } from '@cloudbeaver/core-blocks'; -import { selectFiles } from '@cloudbeaver/core-browser'; -import { useService } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; +import { ActionIconButton, Button, Container, Fill, Loader, s, useS, useSuspense, useTranslate } from '@cloudbeaver/core-blocks'; import { type TabContainerPanelComponent, useTabLocalState } from '@cloudbeaver/core-ui'; -import { blobToBase64, bytesToSize, download, getMIME, isImageFormat, isValidUrl, throttle } from '@cloudbeaver/core-utils'; +import { blobToBase64, bytesToSize, throttle } from '@cloudbeaver/core-utils'; -import { createResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/createResultSetBlobValue'; -import type { IResultSetElementKey } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetDataKey'; -import { isResultSetBinaryValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBinaryValue'; -import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue'; import { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; -import { isResultSetFileValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetFileValue'; -import { ResultSetDataContentAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction'; -import { ResultSetEditAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetEditAction'; -import { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; -import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet'; import type { IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService'; import { QuotaPlaceholder } from '../QuotaPlaceholder'; import styles from './ImageValuePresentation.module.css'; +import { useValuePanelImageValue } from './useValuePanelImageValue'; export const ImageValuePresentation: TabContainerPanelComponent> = observer( function ImageValuePresentation({ model, resultIndex }) { const translate = useTranslate(); const suspense = useSuspense(); - const notificationService = useService(NotificationService); const style = useS(styles); - const state = useTabLocalState(() => observable( { @@ -52,156 +39,12 @@ export const ImageValuePresentation: TabContainerPanelComponent ({ - get editAction(): ResultSetEditAction { - return this.model.source.getAction(this.resultIndex, ResultSetEditAction); - }, - get contentAction(): ResultSetDataContentAction { - return this.model.source.getAction(this.resultIndex, ResultSetDataContentAction); - }, - get selectAction(): ResultSetSelectAction { - return this.model.source.getAction(this.resultIndex, ResultSetSelectAction); - }, - get formatAction(): ResultSetFormatAction { - return this.model.source.getAction(this.resultIndex, ResultSetFormatAction); - }, - get selectedCell(): IResultSetElementKey | undefined { - const activeElements = this.selectAction.getActiveElements(); - - if (activeElements.length === 0) { - return undefined; - } - - return activeElements[0]; - }, - get cellValue() { - if (this.selectedCell === undefined) { - return null; - } - - return this.formatAction.get(this.selectedCell); - }, - get src(): string | Blob | null { - if (isResultSetBlobValue(this.cellValue)) { - // uploaded file preview - return this.cellValue.blob; - } - - if (this.staticSrc) { - return this.staticSrc; - } - - if (this.cacheBlob) { - // uploaded file preview - return this.cacheBlob; - } - - return null; - }, - get staticSrc(): string | null { - if (this.truncated) { - return null; - } - - if (isResultSetBinaryValue(this.cellValue)) { - return `data:${getMIME(this.cellValue.binary)};base64,${this.cellValue.binary}`; - } - - if (typeof this.cellValue === 'string' && isValidUrl(this.cellValue) && isImageFormat(this.cellValue)) { - return this.cellValue; - } - - return null; - }, - get cacheBlob() { - if (!this.selectedCell) { - return null; - } - return this.contentAction.retrieveBlobFromCache(this.selectedCell); - }, - get canSave() { - if (this.truncated && this.selectedCell) { - return this.contentAction.isDownloadable(this.selectedCell); - } - - return this.staticSrc && !this.truncated; - }, - get canUpload() { - if (!this.selectedCell) { - return false; - } - return this.formatAction.isBinary(this.selectedCell); - }, - get truncated() { - if (isResultSetFileValue(this.cellValue)) { - return false; - } - - return this.selectedCell && this.contentAction.isBlobTruncated(this.selectedCell); - }, - async download() { - try { - if (this.src) { - download(this.src, '', true); - } else if (this.selectedCell) { - await this.contentAction.downloadFileData(this.selectedCell); - } else { - throw new Error("Can't save image"); - } - } catch (exception: any) { - this.notificationService.logException(exception, 'data_viewer_presentation_value_content_download_error'); - } - }, - async upload() { - selectFiles(files => { - const file = files?.[0] ?? undefined; - if (file && this.selectedCell) { - this.editAction.set(this.selectedCell, createResultSetBlobValue(file)); - } - }); - }, - async loadFullImage() { - if (!this.selectedCell) { - return; - } - - try { - await this.contentAction.resolveFileDataUrl(this.selectedCell); - } catch (exception: any) { - this.notificationService.logException(exception, 'data_viewer_presentation_value_content_download_error'); - } - }, - }), - { - editAction: computed, - contentAction: computed, - selectAction: computed, - formatAction: computed, - selectedCell: computed, - cellValue: computed, - canUpload: computed, - src: computed, - cacheBlob: computed, - canSave: computed, - truncated: computed, - model: observable.ref, - resultIndex: observable.ref, - download: action.bound, - upload: action.bound, - loadFullImage: action.bound, - }, - { model, resultIndex, notificationService }, - ); - + const data = useValuePanelImageValue({ model, resultIndex }); const loading = model.isLoading(); - const valueSize = bytesToSize(isResultSetContentValue(data.cellValue) ? data.cellValue.contentLength ?? 0 : 0); const isTruncatedMessageDisplay = !!data.truncated && !data.src; const isDownloadable = isTruncatedMessageDisplay && !!data.selectedCell && data.contentAction.isDownloadable(data.selectedCell); const isCacheDownloading = isDownloadable && data.contentAction.isLoading(data.selectedCell); - const debouncedDownload = useMemo(() => throttle(() => data.download(), 1000, false), []); const srcGetter = suspense.observedValue( 'src', diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/useValuePanelImageValue.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/useValuePanelImageValue.ts new file mode 100644 index 0000000000..2beb0c44ce --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/useValuePanelImageValue.ts @@ -0,0 +1,163 @@ +/* + * 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, computed, observable } from 'mobx'; + +import { useObservableRef } from '@cloudbeaver/core-blocks'; +import { selectFiles } from '@cloudbeaver/core-browser'; +import { useService } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { download, getMIME, isImageFormat, isValidUrl } from '@cloudbeaver/core-utils'; + +import { createResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/createResultSetBlobValue'; +import type { IResultSetElementKey } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetDataKey'; +import { isResultSetBinaryValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBinaryValue'; +import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue'; +import { isResultSetFileValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetFileValue'; +import { ResultSetDataContentAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction'; +import { ResultSetEditAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetEditAction'; +import { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; +import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; +import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; +import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet'; + +interface Props { + model: IDatabaseDataModel; + resultIndex: number; +} + +export function useValuePanelImageValue({ model, resultIndex }: Props) { + const notificationService = useService(NotificationService); + const selectAction = model.source.getAction(resultIndex, ResultSetSelectAction); + const formatAction = model.source.getAction(resultIndex, ResultSetFormatAction); + const contentAction = model.source.getAction(resultIndex, ResultSetDataContentAction); + const editAction = model.source.getAction(resultIndex, ResultSetEditAction); + + return useObservableRef( + () => ({ + get selectedCell(): IResultSetElementKey | undefined { + return this.selectAction.getActiveElements()?.[0]; + }, + get cellValue() { + if (this.selectedCell === undefined) { + return null; + } + + return this.formatAction.get(this.selectedCell); + }, + get src(): string | Blob | null { + if (isResultSetBlobValue(this.cellValue)) { + // uploaded file preview + return this.cellValue.blob; + } + + if (this.staticSrc) { + return this.staticSrc; + } + + if (this.cacheBlob) { + // uploaded file preview + return this.cacheBlob; + } + + return null; + }, + get staticSrc(): string | null { + if (this.truncated) { + return null; + } + + if (isResultSetBinaryValue(this.cellValue)) { + return `data:${getMIME(this.cellValue.binary)};base64,${this.cellValue.binary}`; + } + + if (typeof this.cellValue === 'string' && isValidUrl(this.cellValue) && isImageFormat(this.cellValue)) { + return this.cellValue; + } + + return null; + }, + get cacheBlob() { + if (!this.selectedCell) { + return null; + } + return this.contentAction.retrieveBlobFromCache(this.selectedCell); + }, + get canSave() { + if (this.truncated && this.selectedCell) { + return this.contentAction.isDownloadable(this.selectedCell); + } + + return this.staticSrc && !this.truncated; + }, + get canUpload() { + if (!this.selectedCell) { + return false; + } + return this.formatAction.isBinary(this.selectedCell); + }, + get truncated() { + if (isResultSetFileValue(this.cellValue)) { + return false; + } + + return this.selectedCell && this.contentAction.isBlobTruncated(this.selectedCell); + }, + async download() { + try { + if (this.src) { + download(this.src, '', true); + return; + } + + if (this.selectedCell) { + await this.contentAction.downloadFileData(this.selectedCell); + return; + } + + throw new Error("Can't save image"); + } catch (exception: any) { + this.notificationService.logException(exception, 'data_viewer_presentation_value_content_download_error'); + } + }, + async upload() { + selectFiles(files => { + const file = files?.[0]; + if (file && this.selectedCell) { + this.editAction.set(this.selectedCell, createResultSetBlobValue(file)); + } + }); + }, + async loadFullImage() { + if (!this.selectedCell) { + return; + } + + try { + await this.contentAction.resolveFileDataUrl(this.selectedCell); + } catch (exception: any) { + this.notificationService.logException(exception, 'data_viewer_presentation_value_content_download_error'); + } + }, + }), + { + selectedCell: computed, + cellValue: computed, + canUpload: computed, + src: computed, + cacheBlob: computed, + canSave: computed, + truncated: computed, + model: observable.ref, + resultIndex: observable.ref, + download: action.bound, + upload: action.bound, + loadFullImage: action.bound, + }, + { model, resultIndex, notificationService, selectAction, formatAction, contentAction, editAction }, + ); +} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/QuotaPlaceholder.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/QuotaPlaceholder.tsx index c11f1a9838..aec0ce74d1 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/QuotaPlaceholder.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/QuotaPlaceholder.tsx @@ -11,7 +11,7 @@ import { Container, Link, s, usePermission, useS, useTranslate } from '@cloudbea import { EAdminPermission } from '@cloudbeaver/core-root'; import type { IResultSetElementKey } from '../DatabaseDataModel/Actions/ResultSet/IResultSetDataKey'; -import { useResultSetActions } from '../DatabaseDataModel/Actions/ResultSet/useResultSetActions'; +import { ResultSetDataContentAction } from '../DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction'; import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel'; import type { IDatabaseResultSet } from '../DatabaseDataModel/IDatabaseResultSet'; import styles from './QuotaPlaceholder.module.css'; @@ -35,7 +35,7 @@ export const QuotaPlaceholder: React.FC> = observ const translate = useTranslate(); const admin = usePermission(EAdminPermission.admin); const style = useS(styles); - const { contentAction } = useResultSetActions({ model, resultIndex }); + const contentAction = model.source.getAction(resultIndex, ResultSetDataContentAction); const limitInfo = elementKey ? contentAction.getLimitInfo(elementKey) : null; return ( diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValueEditor.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValueEditor.tsx index 3f4648d291..dd5ab7d803 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValueEditor.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValueEditor.tsx @@ -25,6 +25,7 @@ interface Props { export const TextValueEditor = observer(function TextValueEditor({ contentType, valueGetter, readonly, lineWrapping, onChange }) { const value = valueGetter(); const typeExtension = useMemo(() => getTypeExtension(contentType!) ?? [], [contentType]); + const extensions = useCodemirrorExtensions(undefined, typeExtension); const dataViewerService = useService(DataViewerService); diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx index 601cf58521..8ee7861d22 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx @@ -13,17 +13,21 @@ import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { TabContainerPanelComponent, TabList, TabsState, TabStyles, useTabLocalState } from '@cloudbeaver/core-ui'; +import { ResultSetDataContentAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction'; +import { ResultSetEditAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetEditAction'; +import { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; -import { useResultSetActions } from '../../DatabaseDataModel/Actions/ResultSet/useResultSetActions'; import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet'; import type { IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService'; import { getDefaultLineWrapping } from './getDefaultLineWrapping'; +import { isTextValueReadonly } from './isTextValueReadonly'; import styles from './shared/TextValuePresentation.module.css'; import TextValuePresentationTab from './shared/TextValuePresentationTab.module.css'; import { TextValueEditor } from './TextValueEditor'; import { TextValuePresentationService } from './TextValuePresentationService'; import { TextValueTruncatedMessage } from './TextValueTruncatedMessage'; -import { useTextValue } from './useTextValue'; +import { useAutoContentType } from './useAutoContentType'; +import { useTextValueGetter } from './useTextValueGetter'; const tabRegistry: StyleRegistry = [[TabStyles, { mode: 'append', styles: [TextValuePresentationTab] }]]; @@ -33,13 +37,13 @@ export const TextValuePresentation: TabContainerPanelComponent observable({ lineWrapping: null as boolean | null, @@ -53,25 +57,25 @@ export const TextValuePresentation: TabContainerPanelComponent selectTabHandler(tab.tabId)} @@ -125,10 +129,10 @@ export const TextValuePresentation: TabContainerPanelComponent diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValueTruncatedMessage.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValueTruncatedMessage.tsx index 6ed479d0de..fbbbf9c517 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValueTruncatedMessage.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValueTruncatedMessage.tsx @@ -15,7 +15,8 @@ import { bytesToSize, isNotNullDefined } from '@cloudbeaver/core-utils'; import type { IResultSetElementKey } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetDataKey'; import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue'; import { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; -import { useResultSetActions } from '../../DatabaseDataModel/Actions/ResultSet/useResultSetActions'; +import { ResultSetDataContentAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction'; +import { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet'; import { QuotaPlaceholder } from '../QuotaPlaceholder'; @@ -30,7 +31,8 @@ interface Props { export const TextValueTruncatedMessage = observer(function TextValueTruncatedMessage({ model, resultIndex, elementKey }) { const translate = useTranslate(); const notificationService = useService(NotificationService); - const { contentAction, formatAction } = useResultSetActions({ model, resultIndex }); + const contentAction = model.source.getAction(resultIndex, ResultSetDataContentAction); + const formatAction = model.source.getAction(resultIndex, ResultSetFormatAction); const contentValue = formatAction.get(elementKey); let isTruncated = contentAction.isTextTruncated(elementKey); const isCacheLoaded = !!contentAction.retrieveFullTextFromCache(elementKey); @@ -43,8 +45,8 @@ export const TextValueTruncatedMessage = observer(function TextValueTrunc if (!isTruncated || isCacheLoaded) { return null; } - const isTextColumn = formatAction.isText(elementKey); + const isTextColumn = formatAction.isText(elementKey); const valueSize = isResultSetContentValue(contentValue) && isNotNullDefined(contentValue.contentLength) ? bytesToSize(contentValue.contentLength) : undefined; diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/isTextValueReadonly.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/isTextValueReadonly.ts new file mode 100644 index 0000000000..c781d0c696 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/isTextValueReadonly.ts @@ -0,0 +1,35 @@ +/* + * 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 { IResultSetElementKey } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetDataKey'; +import type { ResultSetDataContentAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction'; +import type { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; +import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; +import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet'; + +interface Args { + contentAction: ResultSetDataContentAction; + formatAction: ResultSetFormatAction; + model: IDatabaseDataModel; + resultIndex: number; + cell: IResultSetElementKey | undefined; +} + +export function isTextValueReadonly({ contentAction, formatAction, model, resultIndex, cell }: Args) { + if (!cell) { + return true; + } + + return ( + formatAction.isReadOnly(cell) || + formatAction.isBinary(cell) || + formatAction.isGeometry(cell) || + contentAction.isTextTruncated(cell) || + model.isReadonly(resultIndex) || + model.isDisabled(resultIndex) + ); +} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useAutoContentType.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useAutoContentType.ts new file mode 100644 index 0000000000..f8c111f8d6 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useAutoContentType.ts @@ -0,0 +1,77 @@ +/* + * 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 { useService } from '@cloudbeaver/core-di'; +import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; + +import type { IResultSetElementKey } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetDataKey'; +import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue'; +import { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; +import type { IResultSetValue, ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; +import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; +import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet'; +import { TextValuePresentationService } from './TextValuePresentationService'; + +interface Args { + resultIndex: number; + model: IDatabaseDataModel; + dataFormat: ResultDataFormat | null; + currentContentType: string | null; + elementKey?: IResultSetElementKey; + formatAction: ResultSetFormatAction; +} + +const DEFAULT_CONTENT_TYPE = 'text/plain'; + +function getContentTypeFromResultSetValue(contentValue: IResultSetValue) { + if (isResultSetContentValue(contentValue)) { + return contentValue.contentType; + } + + if (isResultSetBlobValue(contentValue)) { + return contentValue.blob.type; + } + + return null; +} + +function preprocessDefaultContentType(contentType: string | null | undefined) { + if (contentType) { + switch (contentType) { + case 'text/json': + return 'application/json'; + case 'application/octet-stream': + return 'application/octet-stream;type=base64'; + default: + return contentType; + } + } + + 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, + }); + 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; +} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValueGetter.ts similarity index 57% rename from webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts rename to webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValueGetter.ts index 8a57c2d417..b87c169244 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValueGetter.ts @@ -8,47 +8,33 @@ import { observable } from 'mobx'; import { useObservableRef, useSuspense } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; import { blobToBase64, isNotNullDefined, removeMetadataFromDataURL } from '@cloudbeaver/core-utils'; import type { IResultSetElementKey } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetDataKey'; import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue'; import { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; -import { useResultSetActions } from '../../DatabaseDataModel/Actions/ResultSet/useResultSetActions'; -import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; -import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet'; +import type { ResultSetDataContentAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction'; +import type { ResultSetEditAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetEditAction'; +import type { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; import { formatText } from './formatText'; import { MAX_BLOB_PREVIEW_SIZE } from './MAX_BLOB_PREVIEW_SIZE'; -import { TextValuePresentationService } from './TextValuePresentationService'; interface IUseTextValueArgs { - resultIndex: number; - model: IDatabaseDataModel; dataFormat: ResultDataFormat | null; - currentContentType: string | null; - elementKey?: IResultSetElementKey; -} - -interface ITextValueInfo { - valueGetter: () => string; contentType: string; + elementKey?: IResultSetElementKey; + contentAction: ResultSetDataContentAction; + formatAction: ResultSetFormatAction; + editAction: ResultSetEditAction; } -const DEFAULT_CONTENT_TYPE = 'text/plain'; +type ValueGetter = () => string; -export function useTextValue({ model, dataFormat, resultIndex, currentContentType, elementKey }: IUseTextValueArgs): ITextValueInfo { - const { formatAction, editAction, contentAction } = useResultSetActions({ model, resultIndex }); +export function useTextValueGetter({ contentType, elementKey, formatAction, contentAction, editAction }: IUseTextValueArgs): ValueGetter { const suspense = useSuspense(); const contentValue = elementKey ? formatAction.get(elementKey) : null; - const textValuePresentationService = useService(TextValuePresentationService); - const activeTabs = textValuePresentationService.tabs.getDisplayed({ - dataFormat: dataFormat, - model: model, - resultIndex: resultIndex, - }); const limitInfo = elementKey ? contentAction.getLimitInfo(elementKey) : null; - const observedContentValue = useObservableRef( { contentValue, @@ -57,40 +43,6 @@ export function useTextValue({ model, dataFormat, resultIndex, currentContentTyp { contentValue: observable.ref, limitInfo: observable.ref }, ); - let contentType = currentContentType; - let autoContentType = DEFAULT_CONTENT_TYPE; - let contentValueType; - - if (isResultSetContentValue(contentValue)) { - contentValueType = contentValue.contentType; - } - - if (isResultSetBlobValue(contentValue)) { - contentValueType = contentValue.blob.type; - } - - if (contentValueType) { - switch (contentValueType) { - case 'text/json': - autoContentType = 'application/json'; - break; - case 'application/octet-stream': - autoContentType = 'application/octet-stream;type=base64'; - break; - default: - autoContentType = contentValueType; - break; - } - } - - if (contentType === null) { - contentType = autoContentType ?? DEFAULT_CONTENT_TYPE; - } - - if (activeTabs.length > 0 && !activeTabs.some(tab => tab.key === contentType)) { - contentType = activeTabs[0].key; - } - const parsedBlobValueGetter = suspense.observedValue( 'value-blob', () => ({ @@ -135,14 +87,11 @@ export function useTextValue({ model, dataFormat, resultIndex, currentContentTyp } if (!editAction.isElementEdited(elementKey) || isBinary) { - value = formatText(contentType!, value); + value = formatText(contentType, value); } return value; } - return { - valueGetter, - contentType, - }; + return valueGetter; } diff --git a/webapp/packages/plugin-data-viewer/src/locales/en.ts b/webapp/packages/plugin-data-viewer/src/locales/en.ts index c1eba19dd4..67e4d9a449 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/en.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/en.ts @@ -30,6 +30,7 @@ export default [ ['data_viewer_data_save_error_title', 'Error occurred while saving changes'], ['data_viewer_auto_refresh_settings', 'Auto refresh Settings'], ['data_viewer_auto_refresh_settings_stop_on_error', 'Stop on error'], + ['data_viewer_presentation_value_no_active_elements', 'No selected table cells'], ['data_viewer_presentation_value_title', 'Value'], ['data_viewer_presentation_value_text_line_wrapping_wrap', 'Wrap lines'], ['data_viewer_presentation_value_text_line_wrapping_no_wrap', "Don't wrap lines"], diff --git a/webapp/packages/plugin-data-viewer/src/locales/it.ts b/webapp/packages/plugin-data-viewer/src/locales/it.ts index a0b3a9f7ad..8a8272b794 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/it.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/it.ts @@ -26,6 +26,7 @@ export default [ ['data_viewer_data_save_error_title', 'Error occurred while saving changes'], ['data_viewer_auto_refresh_settings', 'Auto refresh Settings'], ['data_viewer_auto_refresh_settings_stop_on_error', 'Stop on error'], + ['data_viewer_presentation_value_no_active_elements', 'No selected table cells'], ['data_viewer_presentation_value_title', 'Valore'], ['data_viewer_presentation_value_text_line_wrapping_wrap', 'Wrap lines'], ['data_viewer_presentation_value_text_line_wrapping_no_wrap', "Don't wrap lines"], diff --git a/webapp/packages/plugin-data-viewer/src/locales/ru.ts b/webapp/packages/plugin-data-viewer/src/locales/ru.ts index 76629ec648..c06c0acb5b 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/ru.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/ru.ts @@ -30,6 +30,7 @@ export default [ ['data_viewer_data_save_error_title', 'Ошибка сохранения изменений'], ['data_viewer_auto_refresh_settings', 'Параметры автообновления'], ['data_viewer_auto_refresh_settings_stop_on_error', 'Остановить при ошибке'], + ['data_viewer_presentation_value_no_active_elements', 'Нет выбраных ячеек таблицы'], ['data_viewer_presentation_value_title', 'Значение'], ['data_viewer_presentation_value_text_line_wrapping_wrap', 'Переносить строки'], ['data_viewer_presentation_value_text_line_wrapping_no_wrap', 'Не переносить строки'], diff --git a/webapp/packages/plugin-data-viewer/src/locales/zh.ts b/webapp/packages/plugin-data-viewer/src/locales/zh.ts index cb02a92fc8..b11bcbe296 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/zh.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/zh.ts @@ -30,6 +30,7 @@ export default [ ['data_viewer_data_save_error_title', 'Error occurred while saving changes'], ['data_viewer_auto_refresh_settings', 'Auto refresh Settings'], ['data_viewer_auto_refresh_settings_stop_on_error', 'Stop on error'], + ['data_viewer_presentation_value_no_active_elements', 'No selected table cells'], ['data_viewer_presentation_value_title', '值'], ['data_viewer_presentation_value_text_line_wrapping_wrap', 'Wrap lines'], ['data_viewer_presentation_value_text_line_wrapping_no_wrap', "Don't wrap lines"],