cell display refactor

This commit is contained in:
SPRINX0\prochazka
2024-08-23 16:19:04 +02:00
parent 23a52dc79e
commit 3b813e93e7
8 changed files with 189 additions and 132 deletions

View File

@@ -2,8 +2,12 @@ import _isString from 'lodash/isString';
import _isArray from 'lodash/isArray'; import _isArray from 'lodash/isArray';
import _isNumber from 'lodash/isNumber'; import _isNumber from 'lodash/isNumber';
import _isPlainObject from 'lodash/isPlainObject'; import _isPlainObject from 'lodash/isPlainObject';
import _pad from 'lodash/pad';
import { DataEditorTypesBehaviour } from 'dbgate-types'; 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) { export function arrayToHexString(byteArray) {
return byteArray.reduce((output, elem) => output + ('0' + elem.toString(16)).slice(-2), '').toUpperCase(); return byteArray.reduce((output, elem) => output + ('0' + elem.toString(16)).slice(-2), '').toUpperCase();
} }
@@ -77,41 +81,170 @@ function parseObjectIdAsDollar(value) {
return 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<typeof stringifyCellValue> {
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 (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 (editorTypes?.parseJsonNull) {
if (value === null) return 'null'; if (value === null) {
return { value: 'null', gridStyle: 'valueCellStyle' };
} }
if (editorTypes?.parseJsonBoolean) {
if (value === true) return 'true';
if (value === false) return 'false';
} }
if (value === true) return { value: 'true', gridStyle: 'valueCellStyle' };
if (value === false) return { value: 'false', gridStyle: 'valueCellStyle' };
if (editorTypes?.parseHexAsBuffer) { 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 (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 (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 (editorTypes?.parseJsonObject) {
if (_isPlainObject(value)) return JSON.stringify(value); 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 (editorTypes?.parseNumber) {
if (_isNumber(value)) return value.toString();
} }
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 (_isString(value)) {
if (_isNumber(value)) return value.toString(); switch (intent) {
if (value === null || value === undefined) return ''; 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) { export function safeJsonParse(json, defaultValue?, logError = false) {
@@ -212,7 +345,7 @@ export function getConvertValueMenu(value, onSetValue, editorTypes?: DataEditorT
return [ return [
editorTypes?.supportStringType && { editorTypes?.supportStringType && {
text: 'String', text: 'String',
onClick: () => onSetValue(stringifyCellValue(value, editorTypes)), onClick: () => onSetValue(stringifyCellValue(value, 'stringConversionIntent', editorTypes).value),
}, },
editorTypes?.supportNumberType && { text: 'Number', onClick: () => onSetValue(parseFloat(value)) }, editorTypes?.supportNumberType && { text: 'Number', onClick: () => onSetValue(parseFloat(value)) },
editorTypes?.supportNullType && { text: 'Null', onClick: () => onSetValue(null) }, editorTypes?.supportNullType && { text: 'Null', onClick: () => onSetValue(null) },

View File

@@ -1,105 +1,34 @@
<script context="module">
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;
}
// const dateTimeRegex = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d\d)?Z?$/;
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]))$/;
function formatNumber(value) {
if (value >= 10000 || value <= -10000) {
if (getBoolSettingsValue('dataGrid.thousandsSeparator', false)) {
return value.toLocaleString();
} else {
return value.toString();
}
}
return value.toString();
}
function formatDateTime(testedString) {
const m = testedString.match(dateTimeRegex);
return `${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}:${m[6]}`;
}
</script>
<script lang="ts"> <script lang="ts">
import _ from 'lodash'; import _ from 'lodash';
import { getBoolSettingsValue } from '../settings/settingsTools'; import { getBoolSettingsValue } from '../settings/settingsTools';
import { arrayToHexString } from 'dbgate-tools'; import { stringifyCellValue } from 'dbgate-tools';
export let rowData; export let rowData;
export let value; export let value;
export let jsonParsedValue = undefined; export let jsonParsedValue = undefined;
export let editorTypes;
$: stringified = stringifyCellValue(
value,
'gridCellIntent',
editorTypes,
{ useThousandsSeparator: getBoolSettingsValue('dataGrid.thousandsSeparator', false) },
jsonParsedValue
);
</script> </script>
{#if rowData == null} {#if rowData == null}
<span class="null">(No row)</span> <span class="null">(No row)</span>
{:else if value === null}
<span class="null">(NULL)</span>
{:else if value === undefined}
<span class="null">(No field)</span>
{:else if _.isDate(value)}
{value.toString()}
{:else if value === true}
<span class="value">true</span>
{:else if value === false}
<span class="value">false</span>
{:else if _.isNumber(value)}
<span class="value">{formatNumber(value)}</span>
{:else if _.isString(value) && !jsonParsedValue}
{#if dateTimeRegex.test(value)}
<span class="value">
{formatDateTime(value)}
</span>
{:else} {:else}
{highlightSpecialCharacters(value)} <span class={stringified.gridStyle} title={stringified.gridTitle}>{stringified.value}</span>
{/if}
{:else if value?.type == 'Buffer' && _.isArray(value.data)}
{#if value.data.length <= 16}
<span class="value">{'0x' + arrayToHexString(value.data)}</span>
{:else}
<span class="null">({value.data.length} bytes)</span>
{/if}
{:else if value.$oid}
<span class="value">ObjectId("{value.$oid}")</span>
{:else if _.isPlainObject(value)}
{@const svalue = JSON.stringify(value, undefined, 2)}
<span class="null" title={svalue}
>{#if svalue.length < 100}{JSON.stringify(value)}{:else}(JSON){/if}</span
>
{:else if _.isArray(value)}
<span class="null" title={value.map(x => JSON.stringify(x)).join('\n')}>[{value.length} items]</span>
{:else if _.isPlainObject(jsonParsedValue)}
{@const svalue = JSON.stringify(jsonParsedValue, undefined, 2)}
<span class="null" title={svalue}
>{#if svalue.length < 100}{JSON.stringify(jsonParsedValue)}{:else}(JSON){/if}</span
>
{:else if _.isArray(jsonParsedValue)}
<span class="null" title={jsonParsedValue.map(x => JSON.stringify(x)).join('\n')}
>[{jsonParsedValue.length} items]</span
>
{:else}
{value.toString()}
{/if} {/if}
<style> <style>
.null { .nullCellStyle {
color: var(--theme-font-3); color: var(--theme-font-3);
font-style: italic; font-style: italic;
} }
.value { .valueCellStyle {
color: var(--theme-icon-green); color: var(--theme-icon-green);
} }
</style> </style>

View File

@@ -72,7 +72,7 @@
class:isFocusedColumn class:isFocusedColumn
{style} {style}
> >
<CellValue {rowData} {value} {jsonParsedValue} /> <CellValue {rowData} {value} {jsonParsedValue} {editorTypes} />
{#if allowHintField && rowData && _.some(col.hintColumnNames, hintColumnName => rowData[hintColumnName])} {#if allowHintField && rowData && _.some(col.hintColumnNames, hintColumnName => rowData[hintColumnName])}
<span class="hint" <span class="hint"

View File

@@ -74,7 +74,7 @@
} }
onMount(() => { onMount(() => {
domEditor.value = inplaceEditorState.text || stringifyCellValue(cellValue, editorTypes); domEditor.value = inplaceEditorState.text || stringifyCellValue(cellValue, 'inlineEditorIntent', editorTypes).value;
domEditor.focus(); domEditor.focus();
if (inplaceEditorState.selectAll) { if (inplaceEditorState.selectAll) {
domEditor.select(); domEditor.select();
@@ -105,7 +105,7 @@
dispatchInsplaceEditor({ type: 'close' }); dispatchInsplaceEditor({ type: 'close' });
showModal(EditCellDataModal, { showModal(EditCellDataModal, {
value: stringifyCellValue(cellValue, editorTypes), value: stringifyCellValue(cellValue, 'multilineEditorIntent', editorTypes).value,
onSave: onSetValue, onSave: onSetValue,
}); });
}} }}

View File

@@ -19,22 +19,22 @@
let isOptionsHidden = false; let isOptionsHidden = false;
onMount(() => { onMount(() => {
value = inplaceEditorState.text || stringifyCellValue(cellValue, driver?.dataEditorTypesBehaviour); value =
inplaceEditorState.text || stringifyCellValue(cellValue, 'inlineEditorIntent', driver?.dataEditorTypesBehaviour).value;
valueInit = value; valueInit = value;
const optionsSelected = value.split(','); const optionsSelected = value.split(',');
optionsData = options optionsData = options.map(function (option) {
.map(function(option) {
return { return {
value: option, value: option,
isSelected: optionsSelected.includes(option) isSelected: optionsSelected.includes(option),
}; };
}); });
}); });
function handleCheckboxChanged(e, option) { function handleCheckboxChanged(e, option) {
if (!canSelectMultipleOptions) { if (!canSelectMultipleOptions) {
optionsData.forEach(option => option.isSelected = false); optionsData.forEach(option => (option.isSelected = false));
option.isSelected = true; option.isSelected = true;
} else { } else {
option.isSelected = e.target.checked; option.isSelected = e.target.checked;
@@ -45,8 +45,7 @@
.map(option => option.value) .map(option => option.value)
.join(','); .join(',');
if(!canSelectMultipleOptions) if (!canSelectMultipleOptions) handleConfirm();
handleConfirm();
} }
function handleConfirm() { function handleConfirm() {
@@ -70,13 +69,8 @@
} }
</script> </script>
<div <div use:clickOutside on:clickOutside={handleClickOutside} on:keydown={handleKeyDown} class="inplaceselect">
use:clickOutside <div on:click={() => (isOptionsHidden = !isOptionsHidden)} class="value">
on:clickOutside={handleClickOutside}
on:keydown={handleKeyDown}
class="inplaceselect"
>
<div on:click={() => isOptionsHidden = !isOptionsHidden} class="value">
{value} {value}
</div> </div>
@@ -89,11 +83,12 @@
<div class="options" class:hidden={isOptionsHidden}> <div class="options" class:hidden={isOptionsHidden}>
{#each optionsData ?? [] as option} {#each optionsData ?? [] as option}
<label> <label>
<input type="checkbox" <input
type="checkbox"
on:change={e => handleCheckboxChanged(e, option)} on:change={e => handleCheckboxChanged(e, option)}
bind:checked={option.isSelected} bind:checked={option.isSelected}
class:hidden={!canSelectMultipleOptions} class:hidden={!canSelectMultipleOptions}
> />
{option.value} {option.value}
</label> </label>
{/each} {/each}
@@ -113,7 +108,7 @@
background-color: var(--theme-bg-alt); background-color: var(--theme-bg-alt);
max-height: 150px; max-height: 150px;
overflow: auto; 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 { .value {

View File

@@ -37,7 +37,7 @@
function createColumnsTable(cells) { function createColumnsTable(cells) {
if (cells.length == 0) return ''; if (cells.length == 0) return '';
return `<table>${cells return `<table>${cells
.map(cell => `<tr><td>${cell.column}</td><td>${stringifyCellValue(cell.value)}</td></tr>`) .map(cell => `<tr><td>${cell.column}</td><td>${stringifyCellValue(cell.value, 'exportIntent').value}</td></tr>`)
.join('\n')}</table>`; .join('\n')}</table>`;
} }

View File

@@ -15,7 +15,7 @@
if (force && value?.type == 'Buffer' && _.isArray(value.data)) { if (force && value?.type == 'Buffer' && _.isArray(value.data)) {
return String.fromCharCode.apply(String, value.data); return String.fromCharCode.apply(String, value.data);
} }
return stringifyCellValue(value); return stringifyCellValue(value, 'gridCellIntent').value;
} }
</script> </script>

View File

@@ -75,7 +75,7 @@ export async function getClipboardText() {
export function extractRowCopiedValue(row, col, editorTypes?: DataEditorTypesBehaviour) { export function extractRowCopiedValue(row, col, editorTypes?: DataEditorTypesBehaviour) {
let value = row[col]; let value = row[col];
if (value === undefined) value = _.get(row, col); if (value === undefined) value = _.get(row, col);
return stringifyCellValue(value, editorTypes); return stringifyCellValue(value, 'exportIntent', editorTypes).value;
} }
const clipboardHeadersFormatter = delimiter => columns => { const clipboardHeadersFormatter = delimiter => columns => {