Add Table format to Cell data view sidebar

Adds a new "Table" format option to the Cell data view widget that
displays the selected row as a vertical list with column names above
values, inspired by TablePlus.

Features:
- Shows all columns from the selected row in grid display order
- Inline editing support for regular values (double-click to edit)
- JSON values open Edit Cell modal on double-click
- Open-in-new button for JSON values to view in JSON tab
This commit is contained in:
David Pivoňka
2025-12-08 13:37:55 +01:00
parent 89121a2608
commit 9099ce42b9
3 changed files with 314 additions and 0 deletions

View File

@@ -0,0 +1,295 @@
<script lang="ts">
import _ from 'lodash';
import { tick } from 'svelte';
import CellValue from '../datagrid/CellValue.svelte';
import { isJsonLikeLongString, safeJsonParse, parseCellValue, stringifyCellValue } from 'dbgate-tools';
import keycodes from '../utility/keycodes';
import createRef from '../utility/createRef';
import { showModal } from '../modals/modalTools';
import EditCellDataModal from '../modals/EditCellDataModal.svelte';
import ShowFormButton from '../formview/ShowFormButton.svelte';
import { openJsonDocument } from '../tabs/JsonTab.svelte';
export let selection;
// Get first row data
$: firstSelection = selection?.[0];
$: rowData = firstSelection?.rowData;
$: editable = firstSelection?.editable;
$: editorTypes = firstSelection?.editorTypes;
// Get columns in display order
$: columns = selection?.columns || [];
$: realColumnUniqueNames = selection?.realColumnUniqueNames || [];
// Get the general setCellValue function
$: setCellValue = selection?.setCellValue;
// Build ordered columns with values
$: orderedFields = realColumnUniqueNames
.map(colName => {
const col = columns.find(c => c.uniqueName === colName);
if (!col) return null;
const value = rowData?.[colName];
return {
columnName: col.columnName || colName,
uniqueName: colName,
value,
col,
};
})
.filter(Boolean);
// Editing state
let editingColumn = null;
let editValue = '';
let domEditor = null;
const isChangedRef = createRef(false);
function isJsonValue(value) {
if (_.isPlainObject(value) && !(value?.type == 'Buffer' && _.isArray(value.data)) && !value.$oid && !value.$bigint) {
return true;
}
if (_.isArray(value)) return true;
if (typeof value === 'string' && isJsonLikeLongString(value)) {
const parsed = safeJsonParse(value);
return parsed !== null && (_.isPlainObject(parsed) || _.isArray(parsed));
}
return false;
}
function getJsonObject(value) {
if (_.isPlainObject(value) || _.isArray(value)) return value;
if (typeof value === 'string') {
return safeJsonParse(value);
}
return null;
}
function handleDoubleClick(field) {
if (!editable || !setCellValue) return;
// For JSON values, open the edit modal directly
if (isJsonValue(field.value)) {
openEditModal(field);
return;
}
// For regular values, start inline editing
startEditing(field);
}
function startEditing(field) {
if (!editable || !setCellValue) return;
editingColumn = field.uniqueName;
editValue = stringifyCellValue(field.value, 'inlineEditorIntent', editorTypes).value;
isChangedRef.set(false);
tick().then(() => {
if (domEditor) {
domEditor.focus();
domEditor.select();
}
});
}
function handleKeyDown(event, field) {
switch (event.keyCode) {
case keycodes.escape:
isChangedRef.set(false);
editingColumn = null;
break;
case keycodes.enter:
if (isChangedRef.get()) {
saveValue(field);
}
editingColumn = null;
event.preventDefault();
break;
case keycodes.tab:
if (isChangedRef.get()) {
saveValue(field);
}
editingColumn = null;
event.preventDefault();
// Move to next field
const currentIndex = orderedFields.findIndex(f => f.uniqueName === field.uniqueName);
const nextIndex = event.shiftKey ? currentIndex - 1 : currentIndex + 1;
if (nextIndex >= 0 && nextIndex < orderedFields.length) {
tick().then(() => {
const nextField = orderedFields[nextIndex];
if (isJsonValue(nextField.value)) {
openEditModal(nextField);
} else {
startEditing(nextField);
}
});
}
break;
}
}
function handleBlur(field) {
if (isChangedRef.get()) {
saveValue(field);
}
editingColumn = null;
}
function saveValue(field) {
if (!setCellValue) return;
const parsedValue = parseCellValue(editValue, editorTypes);
setCellValue(field.uniqueName, parsedValue);
isChangedRef.set(false);
}
function openEditModal(field) {
if (!setCellValue) return;
showModal(EditCellDataModal, {
value: field.value,
dataEditorTypesBehaviour: editorTypes,
onSave: value => setCellValue(field.uniqueName, value),
});
}
function openJsonInNewTab(field) {
const jsonObj = getJsonObject(field.value);
if (jsonObj) {
openJsonDocument(jsonObj, undefined, true);
}
}
function getJsonParsedValue(value) {
return !editorTypes?.explicitDataType && isJsonLikeLongString(value) ? safeJsonParse(value) : null;
}
</script>
<div class="outer">
<div class="inner">
{#if !rowData}
<div class="no-data">No data selected</div>
{:else}
{#each orderedFields as field (field.uniqueName)}
<div class="field">
<div class="field-name">{field.columnName}</div>
<div
class="field-value"
class:editable
on:dblclick={() => handleDoubleClick(field)}
>
{#if editingColumn === field.uniqueName}
<div class="editor-wrapper">
<input
type="text"
bind:this={domEditor}
bind:value={editValue}
on:input={() => isChangedRef.set(true)}
on:keydown={e => handleKeyDown(e, field)}
on:blur={() => handleBlur(field)}
class="inline-editor"
/>
{#if editable}
<ShowFormButton
icon="icon edit"
on:click={() => {
editingColumn = null;
openEditModal(field);
}}
/>
{/if}
</div>
{:else}
<CellValue
{rowData}
value={field.value}
jsonParsedValue={getJsonParsedValue(field.value)}
{editorTypes}
/>
{#if isJsonValue(field.value)}
<ShowFormButton
icon="icon open-in-new"
on:click={() => openJsonInNewTab(field)}
/>
{/if}
{/if}
</div>
</div>
{/each}
{/if}
</div>
</div>
<style>
.outer {
flex: 1;
position: relative;
}
.inner {
overflow: auto;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
padding: 4px;
}
.no-data {
color: var(--theme-font-3);
font-style: italic;
padding: 8px;
}
.field {
margin-bottom: 8px;
border: 1px solid var(--theme-border);
border-radius: 3px;
overflow: hidden;
}
.field-name {
background: var(--theme-bg-1);
padding: 4px 8px;
font-weight: 500;
font-size: 11px;
color: var(--theme-font-2);
border-bottom: 1px solid var(--theme-border);
}
.field-value {
padding: 6px 8px;
background: var(--theme-bg-0);
min-height: 20px;
word-break: break-all;
position: relative;
}
.field-value.editable {
cursor: text;
}
.field-value.editable:hover {
background: var(--theme-bg-hover);
}
.editor-wrapper {
display: flex;
align-items: center;
}
.inline-editor {
flex: 1;
border: none;
outline: none;
background: var(--theme-bg-0);
color: var(--theme-font-1);
padding: 0;
margin: 0;
font-family: inherit;
font-size: inherit;
}
.inline-editor:focus {
outline: none;
}
</style>

View File

@@ -1258,9 +1258,21 @@
condition: display?.getChangeSetCondition(rowData),
insertedRowIndex: grider?.getInsertedRowIndex(row),
rowStatus: grider.getRowStatus(row),
// Additional data for TableCellView editing support
onSetValue: value => grider.setCellValue(row, column, value),
editable: grider.editable,
editorTypes: display?.driver?.dataEditorTypesBehaviour,
};
})
.filter(x => x.column);
// Add columns info for TableCellView (columns in display order)
res.columns = columns;
res.realColumnUniqueNames = realColumnUniqueNames;
// Add a general setCellValue function for editing any column in the first selected row
if (res.length > 0) {
const firstRow = res[0].row;
res.setCellValue = (columnName, value) => grider.setCellValue(firstRow, columnName, value);
}
return res;
}

View File

@@ -14,6 +14,12 @@
component: TextCellViewNoWrap,
single: false,
},
{
type: 'table',
title: 'Table',
component: TableCellView,
single: false,
},
{
type: 'json',
title: 'Json',
@@ -92,6 +98,7 @@
import JsonRowView from '../celldata/JsonRowView.svelte';
import MapCellView from '../celldata/MapCellView.svelte';
import PictureCellView from '../celldata/PictureCellView.svelte';
import TableCellView from '../celldata/TableCellView.svelte';
import TextCellViewNoWrap from '../celldata/TextCellViewNoWrap.svelte';
import TextCellViewWrap from '../celldata/TextCellViewWrap.svelte';
import ErrorInfo from '../elements/ErrorInfo.svelte';