diff --git a/packages/tools/src/driverBase.ts b/packages/tools/src/driverBase.ts index d76318b9d..3a304525e 100644 --- a/packages/tools/src/driverBase.ts +++ b/packages/tools/src/driverBase.ts @@ -161,4 +161,9 @@ export const driverBase = { getCollectionExportQueryJson(collection: string, condition: any, sort: any) { return null; }, + + dataEditorTypesBehaviour: { + parseSqlNull: true, + parseHexAsBuffer: true, + }, }; diff --git a/packages/tools/src/stringTools.ts b/packages/tools/src/stringTools.ts index 21be36385..723e7b110 100644 --- a/packages/tools/src/stringTools.ts +++ b/packages/tools/src/stringTools.ts @@ -1,6 +1,8 @@ import _isString from 'lodash/isString'; import _isArray from 'lodash/isArray'; +import _isNumber from 'lodash/isNumber'; import _isPlainObject from 'lodash/isPlainObject'; +import { DataEditorTypesBehaviour } from 'dbgate-types'; export function arrayToHexString(byteArray) { return byteArray.reduce((output, elem) => output + ('0' + elem.toString(16)).slice(-2), '').toUpperCase(); @@ -15,36 +17,103 @@ export function hexStringToArray(inputString) { return res; } -export function parseCellValue(value) { +export function parseCellValue(value, editorTypes?: DataEditorTypesBehaviour) { if (!_isString(value)) return value; - if (value == '(NULL)') return null; - - const mHex = value.match(/^0x([0-9a-fA-F][0-9a-fA-F])+$/); - if (mHex) { - return { - type: 'Buffer', - data: hexStringToArray(value.substring(2)), - }; + if (editorTypes?.parseSqlNull) { + if (value == '(NULL)') return null; } - const mOid = value.match(/^ObjectId\("([0-9a-f]{24})"\)$/); - if (mOid) { - return { $oid: mOid[1] }; + if (editorTypes?.parseHexAsBuffer) { + const mHex = value.match(/^0x([0-9a-fA-F][0-9a-fA-F])+$/); + if (mHex) { + return { + type: 'Buffer', + data: hexStringToArray(value.substring(2)), + }; + } + } + + if (editorTypes?.parseObjectIdAsDollar) { + const mOid = value.match(/^ObjectId\("([0-9a-f]{24})"\)$/); + if (mOid) { + return { $oid: mOid[1] }; + } + } + + if (editorTypes?.parseJsonNull) { + if (value == 'null') return null; + } + + if (editorTypes?.parseJsonBoolean) { + if (value == 'true') return true; + if (value == 'false') return false; + } + + if (editorTypes?.parseNumber) { + if (/^-?[0-9]+(?:\.[0-9]+)?$/.test(value)) { + return parseFloat(value); + } + } + + if (editorTypes?.parseJsonArray || editorTypes?.parseJsonObject) { + const jsonValue = safeJsonParse(value); + if (_isPlainObject(jsonValue) && editorTypes?.parseJsonObject) return jsonValue; + if (_isArray(jsonValue) && editorTypes?.parseJsonArray) return jsonValue; } return value; } -export function stringifyCellValue(value) { - if (value === null) return '(NULL)'; - if (value === undefined) return '(NoField)'; - if (value?.type == 'Buffer' && _isArray(value.data)) return '0x' + arrayToHexString(value.data); - if (value?.$oid) return `ObjectId("${value?.$oid}")`; - if (_isPlainObject(value) || _isArray(value)) return JSON.stringify(value); +function parseObjectIdAsDollar(value) { + if (value?.$oid) return value; + if (_isString(value)) { + if (value.match(/^[0-9a-f]{24}$/)) return { $oid: value }; + const mOid = value.match(/^ObjectId\("([0-9a-f]{24})"\)$/); + if (mOid) { + return { $oid: mOid[1] }; + } + } return value; } +export function stringifyCellValue(value, editorTypes?: DataEditorTypesBehaviour) { + if (editorTypes?.parseSqlNull) { + if (value === null) return '(NULL)'; + } + if (value === undefined) return '(NoField)'; + if (editorTypes?.parseJsonNull) { + if (value === null) return 'null'; + } + if (editorTypes?.parseJsonBoolean) { + if (value === true) return 'true'; + if (value === false) return 'false'; + } + if (editorTypes?.parseHexAsBuffer) { + if (value?.type == 'Buffer' && _isArray(value.data)) return '0x' + arrayToHexString(value.data); + } + if (editorTypes?.parseObjectIdAsDollar) { + if (value?.$oid) return `ObjectId("${value?.$oid}")`; + } + if (editorTypes?.parseJsonArray) { + if (_isArray(value)) return JSON.stringify(value); + } + if (editorTypes?.parseJsonObject) { + if (_isPlainObject(value)) return JSON.stringify(value); + } + if (editorTypes?.parseNumber) { + if (_isNumber(value)) return value.toString(); + } + + if (_isString(value)) return value; + + // fallback + if (_isNumber(value)) return value.toString(); + if (value === null || value === undefined) return ''; + + return ''; +} + export function safeJsonParse(json, defaultValue?, logError = false) { if (_isArray(json) || _isPlainObject(json)) { return json; @@ -127,3 +196,40 @@ export function parseSqlDefaultValue(value: string) { } return undefined; } + +export function detectTypeIcon(value) { + if (value === null) return 'icon type-null'; + if (value?.$oid) return 'icon type-objectid'; + if (_isString(value)) return 'icon type-string'; + if (_isNumber(value)) return 'icon type-number'; + if (_isPlainObject(value)) return 'icon type-object'; + if (_isArray(value)) return 'icon type-array'; + if (value === true || value === false) return 'icon type-boolean'; + return 'icon type-unknown'; +} + +export function getConvertValueMenu(value, onSetValue, editorTypes?: DataEditorTypesBehaviour) { + return [ + editorTypes?.supportStringType && { + text: 'String', + onClick: () => onSetValue(stringifyCellValue(value, editorTypes)), + }, + editorTypes?.supportNumberType && { text: 'Number', onClick: () => onSetValue(parseFloat(value)) }, + editorTypes?.supportNullType && { text: 'Null', onClick: () => onSetValue(null) }, + editorTypes?.supportBooleanType && { + text: 'Boolean', + onClick: () => onSetValue(value?.toString()?.toLowerCase() == 'true' || value == '1'), + }, + editorTypes?.supportObjectIdType && { text: 'ObjectId', onClick: () => onSetValue(parseObjectIdAsDollar(value)) }, + editorTypes?.supportJsonType && { + text: 'JSON', + onClick: () => { + const jsonValue = safeJsonParse(value); + if (jsonValue != null) { + console.log('**** ON SET VALUE', jsonValue); + onSetValue(jsonValue); + } + }, + }, + ]; +} diff --git a/packages/types/engines.d.ts b/packages/types/engines.d.ts index c828a4b9e..f6008c4a8 100644 --- a/packages/types/engines.d.ts +++ b/packages/types/engines.d.ts @@ -93,6 +93,26 @@ export interface CollectionSortDefinitionItem { export type CollectionSortDefinition = CollectionSortDefinitionItem[]; +export interface DataEditorTypesBehaviour { + parseSqlNull?: boolean; + parseJsonNull?: boolean; + parseJsonBoolean?: boolean; + parseNumber?: boolean; + parseJsonArray?: boolean; + parseJsonObject?: boolean; + parseHexAsBuffer?: boolean; + parseObjectIdAsDollar?: boolean; + + explicitDataType?: boolean; + supportNumberType?: boolean; + supportStringType?: boolean; + supportBooleanType?: boolean; + supportDateType?: boolean; + supportNullType?: boolean; + supportJsonType?: boolean; + supportObjectIdType?: boolean; +} + export interface FilterBehaviourProvider { getFilterBehaviour(dataType: string, standardFilterBehaviours: { [id: string]: FilterBehaviour }): FilterBehaviour; } @@ -105,6 +125,7 @@ export interface EngineDriver extends FilterBehaviourProvider { editorMode?: string; readOnlySessions: boolean; supportedKeyTypes: SupportedDbKeyType[]; + dataEditorTypesBehaviour: DataEditorTypesBehaviour; supportsDatabaseUrl?: boolean; supportsDatabaseDump?: boolean; supportsServerSummary?: boolean; diff --git a/packages/web/src/datagrid/DataGridCell.svelte b/packages/web/src/datagrid/DataGridCell.svelte index f22b394c2..cb49efa79 100644 --- a/packages/web/src/datagrid/DataGridCell.svelte +++ b/packages/web/src/datagrid/DataGridCell.svelte @@ -1,13 +1,14 @@ {/if} - {#if col.foreignKey && rowData && rowData[col.uniqueName] && !isCurrentCell} + {#if editorTypes?.explicitDataType} + {#if value !== undefined} + getConvertValueMenu(value, onSetValue, editorTypes)} + /> + {/if} + {:else if col.foreignKey && rowData && rowData[col.uniqueName] && !isCurrentCell} onSetFormView(rowData, col)} /> - {/if} - - {#if col.foreignKey && isCurrentCell && onDictionaryLookup} + {:else if col.foreignKey && isCurrentCell && onDictionaryLookup} - {/if} - - {#if isJson} + {:else if isJson} openJsonDocument(value, undefined, true)} /> - {/if} - - {#if jsonParsedValue && _.isPlainObject(jsonParsedValue)} + {:else if jsonParsedValue && _.isPlainObject(jsonParsedValue)} openJsonDocument(jsonParsedValue, undefined, true)} /> - {/if} - - {#if _.isArray(jsonParsedValue || value)} + {:else if _.isArray(jsonParsedValue || value)} { diff --git a/packages/web/src/datagrid/DataGridCore.svelte b/packages/web/src/datagrid/DataGridCore.svelte index ec51a266f..fc5031724 100644 --- a/packages/web/src/datagrid/DataGridCore.svelte +++ b/packages/web/src/datagrid/DataGridCore.svelte @@ -1557,7 +1557,7 @@ } let colIndex = startCol; for (const cell of rowData) { - setCellValue([rowIndex, colIndex], parseCellValue(cell)); + setCellValue([rowIndex, colIndex], parseCellValue(cell, display?.driver?.dataEditorTypesBehaviour)); colIndex += 1; } rowIndex += 1; diff --git a/packages/web/src/datagrid/DataGridRow.svelte b/packages/web/src/datagrid/DataGridRow.svelte index 9c6924a9d..f41f86a92 100644 --- a/packages/web/src/datagrid/DataGridRow.svelte +++ b/packages/web/src/datagrid/DataGridRow.svelte @@ -62,6 +62,7 @@ options="{col.options}" canSelectMultipleOptions="{col.canSelectMultipleOptions}" onSetValue={value => grider.setCellValue(rowIndex, col.uniqueName, value)} + {driver} /> {:else} @@ -21,6 +22,7 @@ {onSetValue} {options} {canSelectMultipleOptions} + {driver} /> {:else} {/if} diff --git a/packages/web/src/datagrid/InplaceInput.svelte b/packages/web/src/datagrid/InplaceInput.svelte index aec5d7b61..fdc8c22c4 100644 --- a/packages/web/src/datagrid/InplaceInput.svelte +++ b/packages/web/src/datagrid/InplaceInput.svelte @@ -14,6 +14,7 @@ export let onSetValue; export let width; export let cellValue; + export let driver; let domEditor; let showEditorButton = true; @@ -22,6 +23,8 @@ const isChangedRef = createRef(!!inplaceEditorState.text); + $: editorTypes = driver?.dataEditorTypesBehaviour; + function handleKeyDown(event) { showEditorButton = false; @@ -32,7 +35,7 @@ break; case keycodes.enter: if (isChangedRef.get()) { - onSetValue(parseCellValue(domEditor.value)); + onSetValue(parseCellValue(domEditor.value, editorTypes)); isChangedRef.set(false); } domEditor.blur(); @@ -41,7 +44,7 @@ break; case keycodes.tab: if (isChangedRef.get()) { - onSetValue(parseCellValue(domEditor.value)); + onSetValue(parseCellValue(domEditor.value, editorTypes)); isChangedRef.set(false); } domEditor.blur(); @@ -51,7 +54,7 @@ case keycodes.s: if (isCtrlOrCommandKey(event)) { if (isChangedRef.get()) { - onSetValue(parseCellValue(domEditor.value)); + onSetValue(parseCellValue(domEditor.value, editorTypes)); isChangedRef.set(false); } event.preventDefault(); @@ -63,7 +66,7 @@ function handleBlur() { if (isChangedRef.get()) { - onSetValue(parseCellValue(domEditor.value)); + onSetValue(parseCellValue(domEditor.value, editorTypes)); // grider.setCellValue(rowIndex, uniqueName, editor.value); isChangedRef.set(false); } @@ -71,7 +74,7 @@ } onMount(() => { - domEditor.value = inplaceEditorState.text || stringifyCellValue(cellValue); + domEditor.value = inplaceEditorState.text || stringifyCellValue(cellValue, editorTypes); domEditor.focus(); if (inplaceEditorState.selectAll) { domEditor.select(); @@ -102,7 +105,7 @@ dispatchInsplaceEditor({ type: 'close' }); showModal(EditCellDataModal, { - value: stringifyCellValue(cellValue), + value: stringifyCellValue(cellValue, editorTypes), onSave: onSetValue, }); }} diff --git a/packages/web/src/datagrid/InplaceSelect.svelte b/packages/web/src/datagrid/InplaceSelect.svelte index 7148c2837..a1d58a45f 100644 --- a/packages/web/src/datagrid/InplaceSelect.svelte +++ b/packages/web/src/datagrid/InplaceSelect.svelte @@ -11,6 +11,7 @@ export let cellValue; export let options; export let canSelectMultipleOptions; + export let driver; let value; let valueInit; @@ -18,7 +19,7 @@ let isOptionsHidden = false; onMount(() => { - value = inplaceEditorState.text || stringifyCellValue(cellValue); + value = inplaceEditorState.text || stringifyCellValue(cellValue, driver?.dataEditorTypesBehaviour); valueInit = value; const optionsSelected = value.split(','); diff --git a/packages/web/src/formview/FormView.svelte b/packages/web/src/formview/FormView.svelte index e24d90216..10e27a633 100644 --- a/packages/web/src/formview/FormView.svelte +++ b/packages/web/src/formview/FormView.svelte @@ -273,7 +273,10 @@ export function copyToClipboard() { const column = getCellColumn(currentCell); if (!column) return; - const text = currentCell[1] % 2 == 1 ? extractRowCopiedValue(rowData, column.uniqueName) : column.columnName; + const text = + currentCell[1] % 2 == 1 + ? extractRowCopiedValue(rowData, column.uniqueName, display?.driver?.dataEditorTypesBehaviour) + : column.columnName; copyTextToClipboard(text); } @@ -631,11 +634,12 @@ {#if rowData && $inplaceEditorState.cell && rowIndex == $inplaceEditorState.cell[0] && chunkIndex * 2 + 1 == $inplaceEditorState.cell[1]} { grider.setCellValue(0, col.uniqueName, value); }} @@ -644,6 +648,7 @@ handleLookup(col)} + onSetValue={value => { + grider.setCellValue(0, col.uniqueName, value); + }} /> {/if} diff --git a/packages/web/src/formview/ShowFormDropDownButton.svelte b/packages/web/src/formview/ShowFormDropDownButton.svelte new file mode 100644 index 000000000..d0977aaf3 --- /dev/null +++ b/packages/web/src/formview/ShowFormDropDownButton.svelte @@ -0,0 +1,44 @@ + + +
+ +
+ + diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index c64332de1..61be8eadd 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -186,6 +186,16 @@ 'icon num-9-outline': 'mdi mdi-numeric-9-circle-outline', 'icon num-9-plus-outline': 'mdi mdi-numeric-9-plus-circle-outline', + 'icon type-string': 'mdi mdi-code-string', + 'icon type-object': 'mdi mdi-code-braces-box', + 'icon type-array': 'mdi mdi-code-array', + 'icon type-number': 'mdi mdi-pound-box', + 'icon type-boolean': 'mdi mdi-code-equal', + 'icon type-date': 'mdi mdi-alpha-d-box', + 'icon type-objectid': 'mdi mdi-alpha-i-box', + 'icon type-null': 'mdi mdi-code-equal', + 'icon type-unknown': 'mdi mdi-help-box', + 'img ok': 'mdi mdi-check-circle color-icon-green', 'img ok-inv': 'mdi mdi-check-circle color-icon-inv-green', 'img alert': 'mdi mdi-alert-circle color-icon-blue', diff --git a/packages/web/src/utility/clipboard.ts b/packages/web/src/utility/clipboard.ts index f1885763d..516d69071 100644 --- a/packages/web/src/utility/clipboard.ts +++ b/packages/web/src/utility/clipboard.ts @@ -1,6 +1,7 @@ import _ from 'lodash'; import { arrayToHexString, stringifyCellValue } from 'dbgate-tools'; import yaml from 'js-yaml'; +import { DataEditorTypesBehaviour } from 'dbgate-types'; export function copyTextToClipboard(text) { const oldFocus = document.activeElement; @@ -71,13 +72,13 @@ export async function getClipboardText() { return await navigator.clipboard.readText(); } -export function extractRowCopiedValue(row, col) { +export function extractRowCopiedValue(row, col, editorTypes?: DataEditorTypesBehaviour) { let value = row[col]; if (value === undefined) value = _.get(row, col); - return stringifyCellValue(value); + return stringifyCellValue(value, editorTypes); } -const clipboardHeadersFormatter = (delimiter) => (columns) => { +const clipboardHeadersFormatter = delimiter => columns => { return columns.join(delimiter); }; diff --git a/plugins/dbgate-plugin-mongo/src/frontend/driver.js b/plugins/dbgate-plugin-mongo/src/frontend/driver.js index b24d07d14..6eb0db144 100644 --- a/plugins/dbgate-plugin-mongo/src/frontend/driver.js +++ b/plugins/dbgate-plugin-mongo/src/frontend/driver.js @@ -122,6 +122,24 @@ const driver = { sort: convertToMongoSort(sort) || {}, }; }, + + dataEditorTypesBehaviour: { + parseJsonNull: true, + parseJsonBoolean: true, + parseNumber: true, + parseJsonArray: true, + parseJsonObject: true, + parseObjectIdAsDollar: true, + + explicitDataType: true, + supportNumberType: true, + supportStringType: true, + supportBooleanType: true, + supportDateType: true, + supportJsonType: true, + supportObjectIdType: true, + supportNullType: true, + }, }; module.exports = driver;