Cb 4760 add to value panel more clear abstractions and separation of the logic (#2688)

* CB-4760 refactors value panel boolean presentation

* CB-4760 refactors value panel image presentation

* CB-4760 refactors value panel text presentation (part 1)

* CB-4760 refactors value panel text presentation (part 2)

* CB-4760 moves ValuePanel presentation layer to ValuePanel folder

* CB-4760 fixes type in file name

* CB-4760 modifies isBlob truncated logic

* CB-4760 booleanValuePresentation uses actions helper

* CB-4760 reverts value panel helpers location

* CB-4760 reverts getResultSetActions

* CB-4760 add preprocessBooleanValue helper instead of hook

* CB-4760 reverts truncated message isTruncated logic

* CB-4760 cleanup

* CB-4760 cleanup

* CB-4760 test commit for save file history

* CB-4760 test commit for save file history [2]

* CB-4760 test commit for save file history [3]

* CB-4760 moves back ValuePanelPresentation directory to src folder to save git history

* CB-4760 pr fixes

* CB-4760 adds placeholder for non selected elements in boolean value presentation (value panel)

* CB-4760 fixes placeholder translation for non selected elements in boolean value presentation (value panel)

* CB-4760 fix: app restart process

* CB-4760 fix: app restart process

---------

Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
Co-authored-by: Aleksei Potsetsuev <wrouds@gmail.com>
This commit is contained in:
sergeyteleshev
2024-06-13 08:04:22 +03:00
committed by GitHub
co-authored by mr-anton-t Aleksei Potsetsuev
parent 1c9b4f1ece
commit a65dff32d6
18 changed files with 382 additions and 350 deletions
+10 -7
View File
@@ -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<IStartData>(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<void> {
await this.onStart.execute({ preload: true });
await this.onStart.execute({ preload: false });
async start(restart = false): Promise<void> {
await this.onStart.execute({ preload: true, restart });
await this.onStart.execute({ preload: false, restart });
}
async restart(): Promise<void> {
this.dispose();
await this.start();
await this.start(true);
}
dispose(): void {
@@ -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<any, IDatabaseResultSet>;
}
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 },
);
}
@@ -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<IDataValuePanelProps<any, IDatabaseResultSet>> = observer(
function BooleanValuePresentation({ model, resultIndex }) {
const translate = useTranslate();
const selection = model.source.getAction(resultIndex, ResultSetSelectAction);
const activeElements = selection.getActiveElements();
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);
const activeElements = selectAction.getActiveElements();
if (activeElements.length === 0) {
return null;
return <TextPlaceholder>{translate('data_viewer_presentation_value_no_active_elements')}</TextPlaceholder>;
}
let value: boolean | null | undefined;
const view = model.source.getAction(resultIndex, ResultSetViewAction);
const editor = model.source.getAction(resultIndex, ResultSetEditAction);
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 <TextPlaceholder>{translate('data_viewer_presentation_value_boolean_placeholder')}</TextPlaceholder>;
}
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 (
<div className={classes.container}>
@@ -61,7 +55,7 @@ export const BooleanValuePresentation: TabContainerPanelComponent<IDataValuePane
mod={['primary']}
checked={value === true}
disabled={readonly}
onClick={() => editor.set(firstSelectedCell, true)}
onClick={() => editAction.set(firstSelectedCell, true)}
>
TRUE
</Radio>
@@ -71,7 +65,7 @@ export const BooleanValuePresentation: TabContainerPanelComponent<IDataValuePane
mod={['primary']}
checked={value === false}
disabled={readonly}
onClick={() => editor.set(firstSelectedCell, false)}
onClick={() => editAction.set(firstSelectedCell, false)}
>
FALSE
</Radio>
@@ -82,7 +76,7 @@ export const BooleanValuePresentation: TabContainerPanelComponent<IDataValuePane
mod={['primary']}
checked={value === null}
disabled={readonly}
onClick={() => editor.set(firstSelectedCell, null)}
onClick={() => editAction.set(firstSelectedCell, null)}
>
NULL
</Radio>
@@ -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;
@@ -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;
}
@@ -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<IDataValuePanelProps<any, IDatabaseResultSet>> = 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<IDataValuePanelP
},
),
);
const data = useObservableRef(
() => ({
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',
@@ -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<any, IDatabaseResultSet>;
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 },
);
}
@@ -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<React.PropsWithChildren<Props>> = 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 (
@@ -25,6 +25,7 @@ interface Props {
export const TextValueEditor = observer<Props>(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);
@@ -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<IDataValuePanelPr
const notificationService = useService(NotificationService);
const textValuePresentationService = useService(TextValuePresentationService);
const style = useS(styles, TextValuePresentationTab);
const selection = model.source.getAction(resultIndex, ResultSetSelectAction);
const activeElements = selection.getActiveElements();
const selectAction = model.source.getAction(resultIndex, ResultSetSelectAction);
const formatAction = model.source.getAction(resultIndex, ResultSetFormatAction);
const activeElements = selectAction.getActiveElements();
const firstSelectedCell = activeElements.length ? activeElements[0] : undefined;
const { contentAction, editAction, formatAction } = useResultSetActions({
model,
resultIndex,
});
const contentAction = model.source.getAction(resultIndex, ResultSetDataContentAction);
const editAction = model.source.getAction(resultIndex, ResultSetEditAction);
const state = useTabLocalState(() =>
observable({
lineWrapping: null as boolean | null,
@@ -53,25 +57,25 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
},
}),
);
const textValueInfo = useTextValue({
const contentType = useAutoContentType({
dataFormat,
model,
resultIndex,
dataFormat,
currentContentType: state.currentContentType,
elementKey: firstSelectedCell,
formatAction,
});
const autoLineWrapping = getDefaultLineWrapping(textValueInfo.contentType);
const textValueGetter = useTextValueGetter({
contentAction,
editAction,
formatAction,
dataFormat,
contentType,
elementKey: firstSelectedCell,
});
const autoLineWrapping = getDefaultLineWrapping(contentType);
const lineWrapping = state.lineWrapping ?? autoLineWrapping;
const isSelectedCellReadonly =
firstSelectedCell &&
(formatAction.isReadOnly(firstSelectedCell) ||
formatAction.isBinary(firstSelectedCell) ||
formatAction.isGeometry(firstSelectedCell) ||
contentAction.isTextTruncated(firstSelectedCell));
const isReadonlyByResultIndex = model.isReadonly(resultIndex) || model.isDisabled(resultIndex) || !firstSelectedCell;
const isReadonly = isSelectedCellReadonly || isReadonlyByResultIndex;
const isReadonly = isTextValueReadonly({ model, resultIndex, contentAction, cell: firstSelectedCell, formatAction });
const canSave = firstSelectedCell && contentAction.isDownloadable(firstSelectedCell);
function valueChangeHandler(newValue: string) {
@@ -94,7 +98,7 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
async function selectTabHandler(tabId: string) {
// currentContentType may be selected automatically we don't want to change state in this case
if (tabId !== textValueInfo.contentType) {
if (tabId !== contentType) {
state.setContentType(tabId);
}
}
@@ -111,7 +115,7 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
dataFormat={dataFormat}
resultIndex={resultIndex}
container={textValuePresentationService.tabs}
currentTabId={textValueInfo.contentType}
currentTabId={contentType}
model={model}
lazy
onChange={tab => selectTabHandler(tab.tabId)}
@@ -125,10 +129,10 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
<Loader suspense>
<Group overflow maximum box>
<TextValueEditor
contentType={textValueInfo.contentType}
contentType={contentType}
lineWrapping={lineWrapping}
readonly={isReadonly}
valueGetter={textValueInfo.valueGetter}
valueGetter={textValueGetter}
onChange={valueChangeHandler}
/>
</Group>
@@ -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<Props>(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<Props>(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;
@@ -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<any, IDatabaseResultSet>;
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)
);
}
@@ -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<any, IDatabaseResultSet>;
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;
}
@@ -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<any, IDatabaseResultSet>;
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;
}
@@ -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"],
@@ -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"],
@@ -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', 'Не переносить строки'],
@@ -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"],