feat: CB-907 JSON, XML, HTML value panel

This commit is contained in:
Wroud
2021-06-04 20:39:35 +03:00
parent 3e31687a9c
commit a7283a75bd
14 changed files with 270 additions and 45 deletions
@@ -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';
@@ -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<Pick<EditorProps<any, any>, 'rowIdx' | 'row'
const inputRef = useRef<HTMLInputElement>(null);
const [elementRef, setElementRef] = useState<HTMLDivElement | null>(null);
const [popperRef, setPopperRef] = useState<HTMLDivElement | null>(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<Pick<EditorProps<any, any>, '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 = () => {
@@ -33,7 +33,7 @@ export const CellFormatter: React.FC<FormatterProps> = 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);
@@ -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",
@@ -13,6 +13,7 @@ export interface IDatabaseDataFormatAction<TKey, TResult extends IDatabaseDataRe
extends IDatabaseDataAction<any, TResult> {
isReadOnly: (key: TKey) => boolean;
get: (value: any) => any;
getText: (value: any) => string | null;
isNull: (value: any) => boolean;
toString: (value: any) => string;
}
@@ -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<any, IDatabaseResultSet>
@@ -31,14 +32,10 @@ export class ResultSetDataAction extends DatabaseDataAction<any, IDatabaseResult
getContent(cell: IResultSetElementKey): IResultSetContentValue | null {
const value = this.getCellValue(cell);
if (
value !== null
&& typeof value === 'object'
&& '$type' in value
&& value.$type === 'content'
) {
if (isResultSetContentValue(value)) {
return value;
}
return null;
}
@@ -13,6 +13,8 @@ import type { IDatabaseResultSet } from '../../IDatabaseResultSet';
import { databaseDataAction } from '../DatabaseDataActionDecorator';
import type { IDatabaseDataFormatAction } from '../IDatabaseDataFormatAction';
import type { IResultSetElementKey } from './IResultSetElementKey';
import { isResultSetContentValue } from './isResultSetContentValue';
import { ResultSetDataAction } from './ResultSetDataAction';
@databaseDataAction()
export class ResultSetFormatAction extends DatabaseDataAction<any, IDatabaseResultSet>
@@ -23,22 +25,26 @@ export class ResultSetFormatAction extends DatabaseDataAction<any, IDatabaseResu
let columnReadonly = false;
let cellReadonly = false;
if (key.column !== undefined && this.result.data?.columns) {
columnReadonly = this.result.data.columns[key.column].readOnly;
const data = this.getAction(ResultSetDataAction);
if (key.column !== undefined) {
columnReadonly = data.getColumn(key.column)?.readOnly || false;
}
if (key.row !== undefined && key.column !== undefined && this.result.data?.rows) {
const value = this.result.data.rows[key.row][key.column];
cellReadonly = this.isValueReadonly(value);
const value = data.getCellValue(key);
if (isResultSetContentValue(value)) {
cellReadonly = (
value.binary !== undefined
|| value.contentLength !== value.text?.length
);
} else if (value !== null && typeof value === 'object') {
cellReadonly = true;
}
return columnReadonly || cellReadonly;
}
isValueReadonly(value: any): boolean {
return value !== null && typeof value === 'object';
}
isNull(value: any): boolean {
return this.get(value) === null;
}
@@ -48,11 +54,16 @@ export class ResultSetFormatAction extends DatabaseDataAction<any, IDatabaseResu
if ('text' in value) {
return value.text;
} else if ('value' in value) {
if (value.value !== null && typeof value.value === 'object') {
return JSON.stringify(value.value);
}
return value.value;
}
return value;
}
return value;
}
getText(value: any): string {
if (value !== null && typeof value === 'object') {
return JSON.stringify(value);
}
@@ -60,7 +71,7 @@ export class ResultSetFormatAction extends DatabaseDataAction<any, IDatabaseResu
}
toString(value: any): string {
value = this.get(value);
value = this.getText(this.get(value));
if (typeof value === 'string' && value.length > 1000) {
return value.split('').map(v => (v.charCodeAt(0) < 32 ? ' ' : v)).join('');
@@ -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';
}
@@ -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<TResult extends IDatabaseDataResult> 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<TResult extends IDatabaseDataResult> 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;
}
@@ -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<IDataValuePanelProps<any, IDatabaseResultSet>> = 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<IDataValuePanelPr
let value: any;
let stringValue: string | undefined;
let contentType = 'text/plain';
let firstSelectedCell: Required<IResultSetElementKey> | undefined;
let readonly = true;
@@ -44,31 +104,82 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
const format = model.source.getAction(resultIndex, ResultSetFormatAction);
firstSelectedCell = selectedCells[0] || focusCell;
value = model.source
.getEditor(resultIndex)
.getCell(firstSelectedCell.row, firstSelectedCell.column);
stringValue = format.get(value);
stringValue = format.getText(format.get(value));
readonly = format.isReadOnly(firstSelectedCell);
if (isResultSetContentValue(value)) {
if (value.contentType) {
contentType = value.contentType;
if (contentType === 'text/json') {
contentType = 'application/json';
}
if (!textValuePresentationService.tabs.has(contentType)) {
contentType = 'text/plain';
}
}
}
}
const handleChange = (value: string) => {
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))(
<TextareaNew
name="value"
rows={3}
value={stringValue}
disabled={stringValue === undefined}
readOnly={model.isReadonly() || readonly}
embedded
onChange={handleChange}
/>
const useCodeEditor = state.currentContentType !== 'text/plain';
return styled(style)(
<container>
<content-type>
<TabsState
container={textValuePresentationService.tabs}
currentTabId={state.currentContentType}
lazy
onChange={tab => state.setContentType(tab.tabId)}
>
<TabList style={[styles, UNDERLINE_TAB_STYLES]} />
</TabsState>
</content-type>
{useCodeEditor ? (
<CodeEditorLoader
readonly={readonly}
value={stringValue || ''}
options={{
mode: state.currentContentType,
theme: 'material',
lineNumbers: true,
indentWithTabs: true,
smartIndent: true,
lineWrapping: true,
}}
onBeforeChange={(editor, data, value) => handleChange(value)}
/>
) : (
<TextareaNew
name="value"
rows={3}
value={stringValue}
disabled={stringValue === undefined}
readOnly={readonly}
embedded
onChange={handleChange}
/>
)}
</container>
);
});
@@ -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 { }
@@ -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);
}
}
@@ -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'],
];
@@ -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,