From a7283a75bd601f83d18fa2eecb9dabc8b5483294 Mon Sep 17 00:00:00 2001 From: Wroud Date: Fri, 4 Jun 2021 20:39:35 +0300 Subject: [PATCH] feat: CB-907 JSON, XML, HTML value panel --- .../plugin-codemirror/src/CodeEditor.tsx | 4 + .../src/DataGrid/CellEditor/CellEditor.tsx | 4 +- .../src/DataGrid/Formatters/CellFormatter.tsx | 2 +- .../packages/plugin-data-viewer/package.json | 1 + .../Actions/IDatabaseDataFormatAction.ts | 1 + .../Actions/ResultSet/ResultSetDataAction.ts | 9 +- .../ResultSet/ResultSetFormatAction.ts | 37 +++-- .../ResultSet/isResultSetContentValue.ts | 16 ++ .../DatabaseDataModel/DatabaseDataEditor.ts | 18 ++ .../TextValue/TextValuePresentation.tsx | 157 +++++++++++++++--- .../TextValuePresentationBootstrap.ts | 33 +++- .../TextValue/TextValuePresentationService.ts | 27 +++ .../plugin-data-viewer/src/locales/en.ts | 4 + .../plugin-data-viewer/src/manifest.ts | 2 + 14 files changed, 270 insertions(+), 45 deletions(-) create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetContentValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationService.ts diff --git a/webapp/packages/plugin-codemirror/src/CodeEditor.tsx b/webapp/packages/plugin-codemirror/src/CodeEditor.tsx index 2638f9d818..f8364f7267 100644 --- a/webapp/packages/plugin-codemirror/src/CodeEditor.tsx +++ b/webapp/packages/plugin-codemirror/src/CodeEditor.tsx @@ -10,6 +10,10 @@ import { observer } from 'mobx-react-lite'; import { Controlled as CodeMirror } from 'react-codemirror2'; import styled, { use } from 'reshadow'; +import 'codemirror/mode/javascript/javascript'; +import 'codemirror/mode/xml/xml'; +import 'codemirror/mode/htmlembedded/htmlembedded'; +import 'codemirror/mode/htmlmixed/htmlmixed'; import 'codemirror/mode/meta'; import 'codemirror/addon/hint/show-hint'; import 'codemirror/lib/codemirror.css'; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor/CellEditor.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor/CellEditor.tsx index f246c739c9..6263aedf7d 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor/CellEditor.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor/CellEditor.tsx @@ -14,6 +14,7 @@ import { usePopper } from 'react-popper'; import styled, { css } from 'reshadow'; import { InlineEditor } from '@cloudbeaver/core-app'; +import { ResultSetFormatAction } from '@cloudbeaver/plugin-data-viewer'; import { DataGridContext, IColumnResizeInfo } from '../DataGridContext'; import { TableDataContext } from '../TableDataContext'; @@ -50,6 +51,7 @@ export const CellEditor = observer, 'rowIdx' | 'row' const inputRef = useRef(null); const [elementRef, setElementRef] = useState(null); const [popperRef, setPopperRef] = useState(null); + const formatter = dataGridContext?.model.source.getAction(dataGridContext.resultIndex, ResultSetFormatAction); const popper = usePopper(elementRef, popperRef, { placement: 'right', }); @@ -85,7 +87,7 @@ export const CellEditor = observer, 'rowIdx' | 'row' } }); - const value = row[column.key]; + const value = formatter?.get(row[column.key]); const numericCell = typeof tableDataContext?.getCellValue(rowIdx, column.key) === 'number'; const handleSave = () => { diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx index ef76a7c339..f0eda14236 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx @@ -33,7 +33,7 @@ export const CellFormatter: React.FC = observer(function CellFor const context = useContext(DataGridContext); const editingContext = useContext(EditingContext); const formatter = context?.model.source.getAction(context.resultIndex, ResultSetFormatAction); - const rawValue = row[column.key]; + const rawValue = formatter?.get(row[column.key]) ?? row[column.key]; const classes = getClasses(rawValue); const [menuVisible, setMenuVisible] = useState(false); const value = formatter?.toString(rawValue) ?? String(rawValue); diff --git a/webapp/packages/plugin-data-viewer/package.json b/webapp/packages/plugin-data-viewer/package.json index feafe6b62f..91fe939bf8 100644 --- a/webapp/packages/plugin-data-viewer/package.json +++ b/webapp/packages/plugin-data-viewer/package.json @@ -15,6 +15,7 @@ "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix" }, "peerDependencies": { + "@cloudbeaver/plugin-codemirror": "^0.1.0", "@cloudbeaver/plugin-object-viewer": "^0.1.0", "@cloudbeaver/core-app": "^0.1.0", "@cloudbeaver/core-blocks": "^0.1.0", diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts index 2973233645..2012fffee8 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts @@ -13,6 +13,7 @@ export interface IDatabaseDataFormatAction { isReadOnly: (key: TKey) => boolean; get: (value: any) => any; + getText: (value: any) => string | null; isNull: (value: any) => boolean; toString: (value: any) => string; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts index aed5872145..8c39554bba 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts @@ -14,6 +14,7 @@ import { databaseDataAction } from '../DatabaseDataActionDecorator'; import type { IDatabaseDataResultAction } from '../IDatabaseDataResultAction'; import type { IResultSetContentValue } from './IResultSetContentValue'; import type { IResultSetElementKey } from './IResultSetElementKey'; +import { isResultSetContentValue } from './isResultSetContentValue'; @databaseDataAction() export class ResultSetDataAction extends DatabaseDataAction @@ -31,14 +32,10 @@ export class ResultSetDataAction extends DatabaseDataAction @@ -23,22 +25,26 @@ export class ResultSetFormatAction extends DatabaseDataAction 1000) { return value.split('').map(v => (v.charCodeAt(0) < 32 ? ' ' : v)).join(''); diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetContentValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetContentValue.ts new file mode 100644 index 0000000000..418e56fdf6 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetContentValue.ts @@ -0,0 +1,16 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 { IResultSetContentValue } from './IResultSetContentValue'; + +export function isResultSetContentValue(value: any): value is IResultSetContentValue { + return value !== null + && typeof value === 'object' + && '$type' in value + && value.$type === 'content'; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataEditor.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataEditor.ts index 8c17b81a74..d6294f6557 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataEditor.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataEditor.ts @@ -10,6 +10,7 @@ import { makeObservable, observable } from 'mobx'; import { Executor, IExecutor } from '@cloudbeaver/core-executor'; +import { isResultSetContentValue } from './Actions/ResultSet/isResultSetContentValue'; import { DataUpdateType, IDatabaseDataEditor, IDatabaseDataEditorActionsData, IDatabaseDataResultEditor, IDataUpdate, IResultEditingDiff } from './IDatabaseDataEditor'; import type { IDatabaseDataResult } from './IDatabaseDataResult'; @@ -127,6 +128,17 @@ export class DatabaseDataEditor implements */ setCell(result: TResult, row: number, column: number, value: any): void { const diff = this.getOrCreateDiff(result, row); + const prevValue = diff.source[column]; + + if (isResultSetContentValue(prevValue)) { + if ('text' in prevValue) { + value = { + ...prevValue, + text: String(value), + contentLength: String(value).length, + }; + } + } diff.update[column] = value; @@ -284,6 +296,12 @@ export class DatabaseDataEditor implements return Number(valueA) === Number(valueB); } + if (isResultSetContentValue(valueA) && isResultSetContentValue(valueB)) { + if ('text' in valueA && 'text' in valueB) { + return valueA.text === valueB.text; + } + } + return valueA === valueB; } 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 38572978da..3f6610a7f4 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx @@ -6,29 +6,88 @@ * you may not use this file except in compliance with the License. */ +import { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; -import styled from 'reshadow'; +import styled, { css } from 'reshadow'; -import { BASE_CONTAINERS_STYLES, TabContainerPanelComponent, TextareaNew } from '@cloudbeaver/core-blocks'; -import { useStyles } from '@cloudbeaver/core-theming'; -import { css } from '@reshadow/react'; +import { BASE_CONTAINERS_STYLES, TabContainerPanelComponent, TabList, TabsState, TextareaNew, UNDERLINE_TAB_STYLES, useObjectRef } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { composes, useStyles } from '@cloudbeaver/core-theming'; +import { CodeEditorLoader } from '@cloudbeaver/plugin-codemirror'; import type { IResultSetElementKey } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetElementKey'; +import { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; 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 { TextValuePresentationService } from './TextValuePresentationService'; -const styles = css` - TextareaNew { - flex: 1; - } -`; +const styles = composes( + css` + TabList { + composes: theme-border-color-background theme-background-background from global; + } + Tab { + composes: theme-ripple theme-background-surface theme-text-text-primary-on-light from global; + } + `, + css` + container { + display: flex; + flex-direction: column; + flex: 1; + } + content-type { + display: flex; + justify-content: flex-end; + flex: 0; + padding: 0 8px; + padding-bottom: 16px; + } + TextareaNew { + flex: 1; + } + CodeEditorLoader { + flex: 1; + overflow: auto; + } + TabList { + overflow: hidden; + border-radius: 16px; + + & Tab { + border-bottom: 0; + + &:global([aria-selected="false"]) { + border-bottom: 0 !important; + } + } + } + ` +); export const TextValuePresentation: TabContainerPanelComponent> = observer(function TextValuePresentation({ model, resultIndex, }) { + const textValuePresentationService = useService(TextValuePresentationService); + const style = useStyles(styles, BASE_CONTAINERS_STYLES, UNDERLINE_TAB_STYLES); + const state = useObjectRef({ + currentContentType: 'text/plain', + lastContentType: 'text/plain', + + setContentType(type: string) { + this.currentContentType = type; + }, + setDefaultContentType(type: string) { + this.currentContentType = type; + this.lastContentType = type; + }, + }, {}, { + currentContentType: observable, + lastContentType: observable, + }, ['setContentType', 'setDefaultContentType']); const result = model.getResult(resultIndex); const selection = model.source.getAction(resultIndex, ResultSetSelectAction); @@ -37,6 +96,7 @@ export const TextValuePresentation: TabContainerPanelComponent | undefined; let readonly = true; @@ -44,31 +104,82 @@ export const TextValuePresentation: TabContainerPanelComponent { - if (firstSelectedCell) { + readonly = model.isReadonly() || readonly; + + if (contentType !== state.lastContentType) { + state.setDefaultContentType(contentType); + } + + const handleChange = (newValue: string) => { + if (firstSelectedCell && !readonly) { model.source .getEditor(resultIndex) - .setCell(firstSelectedCell.row, firstSelectedCell.column, value); + .setCell(firstSelectedCell.row, firstSelectedCell.column, newValue); } }; - return styled(useStyles(styles, BASE_CONTAINERS_STYLES))( - + const useCodeEditor = state.currentContentType !== 'text/plain'; + + return styled(style)( + + + state.setContentType(tab.tabId)} + > + + + + {useCodeEditor ? ( + handleChange(value)} + /> + ) : ( + + )} + ); }); diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.ts index f1e60a69dd..9094f70ef8 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.ts @@ -6,15 +6,21 @@ * you may not use this file except in compliance with the License. */ +import React from 'react'; + import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; import { DataValuePanelService } from '../../TableViewer/ValuePanel/DataValuePanelService'; import { TextValuePresentation } from './TextValuePresentation'; +import { TextValuePresentationService } from './TextValuePresentationService'; @injectable() export class TextValuePresentationBootstrap extends Bootstrap { - constructor(private readonly dataValuePanelService: DataValuePanelService) { + constructor( + private readonly textValuePresentationService: TextValuePresentationService, + private readonly dataValuePanelService: DataValuePanelService + ) { super(); } @@ -26,6 +32,31 @@ export class TextValuePresentationBootstrap extends Bootstrap { order: Number.MAX_SAFE_INTEGER, panel: () => TextValuePresentation, }); + + 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, + }); + this.textValuePresentationService.add({ + key: 'text/xml', + name: 'data_viewer_presentation_value_text_xml_title', + order: Number.MAX_SAFE_INTEGER, + panel: () => React.Fragment, + }); + this.textValuePresentationService.add({ + key: 'application/json', + name: 'data_viewer_presentation_value_text_json_title', + order: Number.MAX_SAFE_INTEGER, + panel: () => React.Fragment, + }); } load(): void { } diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationService.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationService.ts new file mode 100644 index 0000000000..6c64e78089 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationService.ts @@ -0,0 +1,27 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 { ITabInfo, ITabInfoOptions, TabsContainer } from '@cloudbeaver/core-blocks'; +import { injectable } from '@cloudbeaver/core-di'; + +@injectable() +export class TextValuePresentationService { + readonly tabs: TabsContainer; + + constructor() { + this.tabs = new TabsContainer(); + } + + 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/locales/en.ts b/webapp/packages/plugin-data-viewer/src/locales/en.ts index 5cea24358e..1fb1a28246 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/en.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/en.ts @@ -6,5 +6,9 @@ export default [ ['data_viewer_statistics_updated_rows', 'Updated Rows:'], ['data_viewer_presentation_value_title', 'Value'], ['data_viewer_presentation_value_text_title', 'Text'], + ['data_viewer_presentation_value_text_plain_title', 'Text'], + ['data_viewer_presentation_value_text_html_title', 'HTML'], + ['data_viewer_presentation_value_text_xml_title', 'XML'], + ['data_viewer_presentation_value_text_json_title', 'JSON'], ['data_viewer_presentation_value_image_title', 'Image'], ]; diff --git a/webapp/packages/plugin-data-viewer/src/manifest.ts b/webapp/packages/plugin-data-viewer/src/manifest.ts index 48850534d8..beb9cbc5f2 100644 --- a/webapp/packages/plugin-data-viewer/src/manifest.ts +++ b/webapp/packages/plugin-data-viewer/src/manifest.ts @@ -20,6 +20,7 @@ import { DataValuePanelBootstrap } from './TableViewer/ValuePanel/DataValuePanel import { DataValuePanelService } from './TableViewer/ValuePanel/DataValuePanelService'; import { ImageValuePresentationBootstrap } from './ValuePanelPresentation/ImageValue/ImageValuePresentationBootstrap'; import { TextValuePresentationBootstrap } from './ValuePanelPresentation/TextValue/TextValuePresentationBootstrap'; +import { TextValuePresentationService } from './ValuePanelPresentation/TextValue/TextValuePresentationService'; export const manifest: PluginManifest = { info: { @@ -36,6 +37,7 @@ export const manifest: PluginManifest = { TableHeaderService, LocaleService, DataValuePanelService, + TextValuePresentationService, TextValuePresentationBootstrap, ImageValuePresentationBootstrap, DataValuePanelBootstrap,