diff --git a/packages/tools/src/stringTools.ts b/packages/tools/src/stringTools.ts index 723e7b110..3b93522d2 100644 --- a/packages/tools/src/stringTools.ts +++ b/packages/tools/src/stringTools.ts @@ -2,8 +2,12 @@ import _isString from 'lodash/isString'; import _isArray from 'lodash/isArray'; import _isNumber from 'lodash/isNumber'; import _isPlainObject from 'lodash/isPlainObject'; +import _pad from 'lodash/pad'; import { DataEditorTypesBehaviour } from 'dbgate-types'; +const dateTimeRegex = + /^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|()|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))$/; + export function arrayToHexString(byteArray) { return byteArray.reduce((output, elem) => output + ('0' + elem.toString(16)).slice(-2), '').toUpperCase(); } @@ -77,41 +81,170 @@ function parseObjectIdAsDollar(value) { return value; } -export function stringifyCellValue(value, editorTypes?: DataEditorTypesBehaviour) { +function makeBulletString(value) { + return _pad('', value.length, '•'); +} + +function highlightSpecialCharacters(value) { + value = value.replace(/\n/g, '↲'); + value = value.replace(/\r/g, ''); + value = value.replace(/^(\s+)/, makeBulletString); + value = value.replace(/(\s+)$/, makeBulletString); + value = value.replace(/(\s\s+)/g, makeBulletString); + return value; +} + +function stringifyJsonToGrid(value): ReturnType { + if (_isPlainObject(value)) { + const svalue = JSON.stringify(value, undefined, 2); + if (svalue.length < 100) { + return { value: svalue, gridStyle: 'nullCellStyle' }; + } else { + return { value: '(JSON)', gridStyle: 'nullCellStyle', gridTitle: svalue }; + } + } + if (_isArray(value)) { + return { + value: `[${value.length} items]`, + gridStyle: 'nullCellStyle', + gridTitle: value.map(x => JSON.stringify(x)).join('\n'), + }; + } + return { value: '(JSON)', gridStyle: 'nullCellStyle' }; +} + +export function stringifyCellValue( + value, + intent: 'gridCellIntent' | 'inlineEditorIntent' | 'multilineEditorIntent' | 'stringConversionIntent' | 'exportIntent', + editorTypes?: DataEditorTypesBehaviour, + gridFormattingOptions?: { useThousandsSeparator?: boolean }, + jsonParsedValue?: any +): { + value: string; + gridStyle?: 'textCellStyle' | 'valueCellStyle' | 'nullCellStyle'; // only for gridCellIntent + gridTitle?: string; // only for gridCellIntent +} { if (editorTypes?.parseSqlNull) { - if (value === null) return '(NULL)'; + if (value === null) { + switch (intent) { + case 'exportIntent': + return { value: '' }; + default: + return { value: '(NULL)', gridStyle: 'nullCellStyle' }; + } + } + } + if (value === undefined) { + switch (intent) { + case 'gridCellIntent': + return { value: '(No Field)', gridStyle: 'nullCellStyle' }; + default: + return { value: '' }; + } } - 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 (value === null) { + return { value: 'null', gridStyle: 'valueCellStyle' }; + } } + + if (value === true) return { value: 'true', gridStyle: 'valueCellStyle' }; + if (value === false) return { value: 'false', gridStyle: 'valueCellStyle' }; + if (editorTypes?.parseHexAsBuffer) { - if (value?.type == 'Buffer' && _isArray(value.data)) return '0x' + arrayToHexString(value.data); + if (value?.type == 'Buffer' && _isArray(value.data)) { + return { value: '0x' + arrayToHexString(value.data), gridStyle: 'valueCellStyle' }; + } } if (editorTypes?.parseObjectIdAsDollar) { - if (value?.$oid) return `ObjectId("${value?.$oid}")`; + if (value?.$oid) { + switch (intent) { + case 'exportIntent': + case 'stringConversionIntent': + return { value: value.$oid }; + default: + return { value: `ObjectId("${value.$oid}")`, gridStyle: 'valueCellStyle' }; + } + } } if (editorTypes?.parseJsonArray) { - if (_isArray(value)) return JSON.stringify(value); + if (_isArray(value)) { + switch (intent) { + case 'gridCellIntent': + return stringifyJsonToGrid(value); + case 'multilineEditorIntent': + return { value: JSON.stringify(value) }; + default: + return { value: JSON.stringify(value, null, 2), gridStyle: 'valueCellStyle' }; + } + } } if (editorTypes?.parseJsonObject) { - if (_isPlainObject(value)) return JSON.stringify(value); - } - if (editorTypes?.parseNumber) { - if (_isNumber(value)) return value.toString(); + if (_isPlainObject(value)) { + switch (intent) { + case 'gridCellIntent': + return stringifyJsonToGrid(value); + case 'multilineEditorIntent': + return { value: JSON.stringify(value) }; + default: + return { value: JSON.stringify(value, null, 2), gridStyle: 'valueCellStyle' }; + } + } } - if (_isString(value)) return value; + if (_isNumber(value)) { + switch (intent) { + case 'gridCellIntent': + return { + value: + gridFormattingOptions?.useThousandsSeparator && (value >= 10000 || value <= -10000) + ? value.toLocaleString() + : value.toString(), + gridStyle: 'valueCellStyle', + }; + default: + return { value: value.toString() }; + } + } - // fallback - if (_isNumber(value)) return value.toString(); - if (value === null || value === undefined) return ''; + if (_isString(value)) { + switch (intent) { + case 'gridCellIntent': + if (jsonParsedValue && !editorTypes?.explicitDataType) { + return stringifyJsonToGrid(jsonParsedValue); + } else { + if (!editorTypes?.explicitDataType) { + // reformat datetime for implicit date types + const m = value.match(dateTimeRegex); + if (m) { + return { + value: `${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}:${m[6]}`, + gridStyle: 'valueCellStyle', + }; + } + } + return { value: highlightSpecialCharacters(value), gridStyle: 'textCellStyle' }; + } + default: + return { value: value }; + } + } - return ''; + if (value === null || value === undefined) { + switch (intent) { + case 'gridCellIntent': + return { value: '(n/a)', gridStyle: 'nullCellStyle' }; + default: + return { value: '' }; + } + } + + switch (intent) { + case 'gridCellIntent': + return { value: '(Unknown)', gridStyle: 'nullCellStyle' }; + default: + return { value: '' }; + } } export function safeJsonParse(json, defaultValue?, logError = false) { @@ -212,7 +345,7 @@ export function getConvertValueMenu(value, onSetValue, editorTypes?: DataEditorT return [ editorTypes?.supportStringType && { text: 'String', - onClick: () => onSetValue(stringifyCellValue(value, editorTypes)), + onClick: () => onSetValue(stringifyCellValue(value, 'stringConversionIntent', editorTypes).value), }, editorTypes?.supportNumberType && { text: 'Number', onClick: () => onSetValue(parseFloat(value)) }, editorTypes?.supportNullType && { text: 'Null', onClick: () => onSetValue(null) }, diff --git a/packages/web/src/datagrid/CellValue.svelte b/packages/web/src/datagrid/CellValue.svelte index e2e5eb1f2..7b6bcb7a1 100644 --- a/packages/web/src/datagrid/CellValue.svelte +++ b/packages/web/src/datagrid/CellValue.svelte @@ -1,105 +1,34 @@ - - {#if rowData == null} (No row) -{:else if value === null} - (NULL) -{:else if value === undefined} - (No field) -{:else if _.isDate(value)} - {value.toString()} -{:else if value === true} - true -{:else if value === false} - false -{:else if _.isNumber(value)} - {formatNumber(value)} -{:else if _.isString(value) && !jsonParsedValue} - {#if dateTimeRegex.test(value)} - - {formatDateTime(value)} - - {:else} - {highlightSpecialCharacters(value)} - {/if} -{:else if value?.type == 'Buffer' && _.isArray(value.data)} - {#if value.data.length <= 16} - {'0x' + arrayToHexString(value.data)} - {:else} - ({value.data.length} bytes) - {/if} -{:else if value.$oid} - ObjectId("{value.$oid}") -{:else if _.isPlainObject(value)} - {@const svalue = JSON.stringify(value, undefined, 2)} - {#if svalue.length < 100}{JSON.stringify(value)}{:else}(JSON){/if} -{:else if _.isArray(value)} - JSON.stringify(x)).join('\n')}>[{value.length} items] -{:else if _.isPlainObject(jsonParsedValue)} - {@const svalue = JSON.stringify(jsonParsedValue, undefined, 2)} - {#if svalue.length < 100}{JSON.stringify(jsonParsedValue)}{:else}(JSON){/if} -{:else if _.isArray(jsonParsedValue)} - JSON.stringify(x)).join('\n')} - >[{jsonParsedValue.length} items] {:else} - {value.toString()} + {stringified.value} {/if} diff --git a/packages/web/src/datagrid/DataGridCell.svelte b/packages/web/src/datagrid/DataGridCell.svelte index cb49efa79..f6d9b80e7 100644 --- a/packages/web/src/datagrid/DataGridCell.svelte +++ b/packages/web/src/datagrid/DataGridCell.svelte @@ -72,7 +72,7 @@ class:isFocusedColumn {style} > - + {#if allowHintField && rowData && _.some(col.hintColumnNames, hintColumnName => rowData[hintColumnName])} { - domEditor.value = inplaceEditorState.text || stringifyCellValue(cellValue, editorTypes); + domEditor.value = inplaceEditorState.text || stringifyCellValue(cellValue, 'inlineEditorIntent', editorTypes).value; domEditor.focus(); if (inplaceEditorState.selectAll) { domEditor.select(); @@ -105,7 +105,7 @@ dispatchInsplaceEditor({ type: 'close' }); showModal(EditCellDataModal, { - value: stringifyCellValue(cellValue, editorTypes), + value: stringifyCellValue(cellValue, 'multilineEditorIntent', editorTypes).value, onSave: onSetValue, }); }} diff --git a/packages/web/src/datagrid/InplaceSelect.svelte b/packages/web/src/datagrid/InplaceSelect.svelte index a1d58a45f..b95254fc9 100644 --- a/packages/web/src/datagrid/InplaceSelect.svelte +++ b/packages/web/src/datagrid/InplaceSelect.svelte @@ -19,22 +19,22 @@ let isOptionsHidden = false; onMount(() => { - value = inplaceEditorState.text || stringifyCellValue(cellValue, driver?.dataEditorTypesBehaviour); + value = + inplaceEditorState.text || stringifyCellValue(cellValue, 'inlineEditorIntent', driver?.dataEditorTypesBehaviour).value; valueInit = value; const optionsSelected = value.split(','); - optionsData = options - .map(function(option) { - return { - value: option, - isSelected: optionsSelected.includes(option) - }; - }); + optionsData = options.map(function (option) { + return { + value: option, + isSelected: optionsSelected.includes(option), + }; + }); }); function handleCheckboxChanged(e, option) { if (!canSelectMultipleOptions) { - optionsData.forEach(option => option.isSelected = false); + optionsData.forEach(option => (option.isSelected = false)); option.isSelected = true; } else { option.isSelected = e.target.checked; @@ -45,8 +45,7 @@ .map(option => option.value) .join(','); - if(!canSelectMultipleOptions) - handleConfirm(); + if (!canSelectMultipleOptions) handleConfirm(); } function handleConfirm() { @@ -70,13 +69,8 @@ } -
-
isOptionsHidden = !isOptionsHidden} class="value"> +
+
(isOptionsHidden = !isOptionsHidden)} class="value"> {value}
@@ -89,11 +83,12 @@
{#each optionsData ?? [] as option} {/each} @@ -113,7 +108,7 @@ background-color: var(--theme-bg-alt); max-height: 150px; overflow: auto; - box-shadow: 0 1px 10px 1px var(--theme-bg-inv-3);; + box-shadow: 0 1px 10px 1px var(--theme-bg-inv-3); } .value { diff --git a/packages/web/src/elements/SelectionMapView.svelte b/packages/web/src/elements/SelectionMapView.svelte index ec64fde5c..60ca253b0 100644 --- a/packages/web/src/elements/SelectionMapView.svelte +++ b/packages/web/src/elements/SelectionMapView.svelte @@ -37,7 +37,7 @@ function createColumnsTable(cells) { if (cells.length == 0) return ''; return `${cells - .map(cell => ``) + .map(cell => ``) .join('\n')}
${cell.column}${stringifyCellValue(cell.value)}
${cell.column}${stringifyCellValue(cell.value, 'exportIntent').value}
`; } diff --git a/packages/web/src/perspectives/PerspectiveCell.svelte b/packages/web/src/perspectives/PerspectiveCell.svelte index e86579cd9..f72f3968d 100644 --- a/packages/web/src/perspectives/PerspectiveCell.svelte +++ b/packages/web/src/perspectives/PerspectiveCell.svelte @@ -15,7 +15,7 @@ if (force && value?.type == 'Buffer' && _.isArray(value.data)) { return String.fromCharCode.apply(String, value.data); } - return stringifyCellValue(value); + return stringifyCellValue(value, 'gridCellIntent').value; } diff --git a/packages/web/src/utility/clipboard.ts b/packages/web/src/utility/clipboard.ts index 516d69071..7adf11eae 100644 --- a/packages/web/src/utility/clipboard.ts +++ b/packages/web/src/utility/clipboard.ts @@ -75,7 +75,7 @@ export async function getClipboardText() { export function extractRowCopiedValue(row, col, editorTypes?: DataEditorTypesBehaviour) { let value = row[col]; if (value === undefined) value = _.get(row, col); - return stringifyCellValue(value, editorTypes); + return stringifyCellValue(value, 'exportIntent', editorTypes).value; } const clipboardHeadersFormatter = delimiter => columns => {