From 9099ce42b9fccbda9c516801f84d15c8d8eb50db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Pivo=C5=88ka?= Date: Mon, 8 Dec 2025 13:37:55 +0100 Subject: [PATCH 1/7] 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 --- .../web/src/celldata/TableCellView.svelte | 295 ++++++++++++++++++ packages/web/src/datagrid/DataGridCore.svelte | 12 + .../web/src/widgets/CellDataWidget.svelte | 7 + 3 files changed, 314 insertions(+) create mode 100644 packages/web/src/celldata/TableCellView.svelte diff --git a/packages/web/src/celldata/TableCellView.svelte b/packages/web/src/celldata/TableCellView.svelte new file mode 100644 index 000000000..f56ae85e2 --- /dev/null +++ b/packages/web/src/celldata/TableCellView.svelte @@ -0,0 +1,295 @@ + + +
+
+ {#if !rowData} +
No data selected
+ {:else} + {#each orderedFields as field (field.uniqueName)} +
+
{field.columnName}
+
handleDoubleClick(field)} + > + {#if editingColumn === field.uniqueName} +
+ isChangedRef.set(true)} + on:keydown={e => handleKeyDown(e, field)} + on:blur={() => handleBlur(field)} + class="inline-editor" + /> + {#if editable} + { + editingColumn = null; + openEditModal(field); + }} + /> + {/if} +
+ {:else} + + {#if isJsonValue(field.value)} + openJsonInNewTab(field)} + /> + {/if} + {/if} +
+
+ {/each} + {/if} +
+
+ + diff --git a/packages/web/src/datagrid/DataGridCore.svelte b/packages/web/src/datagrid/DataGridCore.svelte index 7eb775059..d24356fc2 100644 --- a/packages/web/src/datagrid/DataGridCore.svelte +++ b/packages/web/src/datagrid/DataGridCore.svelte @@ -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; } diff --git a/packages/web/src/widgets/CellDataWidget.svelte b/packages/web/src/widgets/CellDataWidget.svelte index 8175b86e8..6c60dddda 100644 --- a/packages/web/src/widgets/CellDataWidget.svelte +++ b/packages/web/src/widgets/CellDataWidget.svelte @@ -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'; From 5e4a631ff2d351c7376973b94ce22bf07cd028b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Pivo=C5=88ka?= Date: Mon, 8 Dec 2025 13:43:41 +0100 Subject: [PATCH 2/7] Remove comments and apply early return pattern --- .../web/src/celldata/TableCellView.svelte | 80 +++++++------------ packages/web/src/datagrid/DataGridCore.svelte | 6 +- 2 files changed, 34 insertions(+), 52 deletions(-) diff --git a/packages/web/src/celldata/TableCellView.svelte b/packages/web/src/celldata/TableCellView.svelte index f56ae85e2..aed68ab1c 100644 --- a/packages/web/src/celldata/TableCellView.svelte +++ b/packages/web/src/celldata/TableCellView.svelte @@ -12,20 +12,14 @@ 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); @@ -40,7 +34,6 @@ }) .filter(Boolean); - // Editing state let editingColumn = null; let editValue = ''; let domEditor = null; @@ -51,31 +44,24 @@ 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; + if (typeof value !== 'string') return false; + if (!isJsonLikeLongString(value)) return false; + const parsed = safeJsonParse(value); + return parsed !== null && (_.isPlainObject(parsed) || _.isArray(parsed)); } function getJsonObject(value) { if (_.isPlainObject(value) || _.isArray(value)) return value; - if (typeof value === 'string') { - return safeJsonParse(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); } @@ -85,10 +71,9 @@ editValue = stringifyCellValue(field.value, 'inlineEditorIntent', editorTypes).value; isChangedRef.set(false); tick().then(() => { - if (domEditor) { - domEditor.focus(); - domEditor.select(); - } + if (!domEditor) return; + domEditor.focus(); + domEditor.select(); }); } @@ -99,39 +84,36 @@ editingColumn = null; break; case keycodes.enter: - if (isChangedRef.get()) { - saveValue(field); - } + if (isChangedRef.get()) saveValue(field); editingColumn = null; event.preventDefault(); break; case keycodes.tab: - if (isChangedRef.get()) { - saveValue(field); - } + 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); - } - }); - } + moveToNextField(field, event.shiftKey); break; } } + function moveToNextField(field, reverse) { + const currentIndex = orderedFields.findIndex(f => f.uniqueName === field.uniqueName); + const nextIndex = reverse ? currentIndex - 1 : currentIndex + 1; + if (nextIndex < 0 || nextIndex >= orderedFields.length) return; + + tick().then(() => { + const nextField = orderedFields[nextIndex]; + if (isJsonValue(nextField.value)) { + openEditModal(nextField); + } else { + startEditing(nextField); + } + }); + } + function handleBlur(field) { - if (isChangedRef.get()) { - saveValue(field); - } + if (isChangedRef.get()) saveValue(field); editingColumn = null; } @@ -153,13 +135,13 @@ function openJsonInNewTab(field) { const jsonObj = getJsonObject(field.value); - if (jsonObj) { - openJsonDocument(jsonObj, undefined, true); - } + if (jsonObj) openJsonDocument(jsonObj, undefined, true); } function getJsonParsedValue(value) { - return !editorTypes?.explicitDataType && isJsonLikeLongString(value) ? safeJsonParse(value) : null; + if (editorTypes?.explicitDataType) return null; + if (!isJsonLikeLongString(value)) return null; + return safeJsonParse(value); } diff --git a/packages/web/src/datagrid/DataGridCore.svelte b/packages/web/src/datagrid/DataGridCore.svelte index d24356fc2..bc29172a1 100644 --- a/packages/web/src/datagrid/DataGridCore.svelte +++ b/packages/web/src/datagrid/DataGridCore.svelte @@ -1258,21 +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; } From d220525ac733ac599543db0c5db51839349c4a94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Pivo=C5=88ka?= Date: Mon, 8 Dec 2025 13:47:35 +0100 Subject: [PATCH 3/7] Use braces for isChangedRef.get() blocks to match codebase style --- packages/web/src/celldata/TableCellView.svelte | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/web/src/celldata/TableCellView.svelte b/packages/web/src/celldata/TableCellView.svelte index aed68ab1c..a2f022552 100644 --- a/packages/web/src/celldata/TableCellView.svelte +++ b/packages/web/src/celldata/TableCellView.svelte @@ -84,12 +84,16 @@ editingColumn = null; break; case keycodes.enter: - if (isChangedRef.get()) saveValue(field); + if (isChangedRef.get()) { + saveValue(field); + } editingColumn = null; event.preventDefault(); break; case keycodes.tab: - if (isChangedRef.get()) saveValue(field); + if (isChangedRef.get()) { + saveValue(field); + } editingColumn = null; event.preventDefault(); moveToNextField(field, event.shiftKey); @@ -113,7 +117,9 @@ } function handleBlur(field) { - if (isChangedRef.get()) saveValue(field); + if (isChangedRef.get()) { + saveValue(field); + } editingColumn = null; } From 190c6104663e0ced35929138031684b0e5c55492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Pivo=C5=88ka?= Date: Mon, 8 Dec 2025 14:31:38 +0100 Subject: [PATCH 4/7] Add column filter/search to Table cell data view Adds a search input at the top of the Table view that filters columns by name with regex support. --- .../web/src/celldata/TableCellView.svelte | 51 +++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/packages/web/src/celldata/TableCellView.svelte b/packages/web/src/celldata/TableCellView.svelte index a2f022552..0c97a6c95 100644 --- a/packages/web/src/celldata/TableCellView.svelte +++ b/packages/web/src/celldata/TableCellView.svelte @@ -9,6 +9,9 @@ import EditCellDataModal from '../modals/EditCellDataModal.svelte'; import ShowFormButton from '../formview/ShowFormButton.svelte'; import { openJsonDocument } from '../tabs/JsonTab.svelte'; + import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte'; + import SearchInput from '../elements/SearchInput.svelte'; + import CloseSearchButton from '../buttons/CloseSearchButton.svelte'; export let selection; @@ -20,6 +23,8 @@ $: realColumnUniqueNames = selection?.realColumnUniqueNames || []; $: setCellValue = selection?.setCellValue; + let filter = ''; + $: orderedFields = realColumnUniqueNames .map(colName => { const col = columns.find(c => c.uniqueName === colName); @@ -34,6 +39,16 @@ }) .filter(Boolean); + $: filteredFields = orderedFields.filter(field => { + if (!filter) return true; + try { + const regex = new RegExp(filter, 'i'); + return regex.test(field.columnName); + } catch (e) { + return field.columnName.toLowerCase().includes(filter.toLowerCase()); + } + }); + let editingColumn = null; let editValue = ''; let domEditor = null; @@ -102,12 +117,12 @@ } function moveToNextField(field, reverse) { - const currentIndex = orderedFields.findIndex(f => f.uniqueName === field.uniqueName); + const currentIndex = filteredFields.findIndex(f => f.uniqueName === field.uniqueName); const nextIndex = reverse ? currentIndex - 1 : currentIndex + 1; - if (nextIndex < 0 || nextIndex >= orderedFields.length) return; + if (nextIndex < 0 || nextIndex >= filteredFields.length) return; tick().then(() => { - const nextField = orderedFields[nextIndex]; + const nextField = filteredFields[nextIndex]; if (isJsonValue(nextField.value)) { openEditModal(nextField); } else { @@ -116,6 +131,14 @@ }); } + function handleSearchKeyDown(e) { + if (e.keyCode === keycodes.backspace && (e.metaKey || e.ctrlKey)) { + filter = ''; + e.stopPropagation(); + e.preventDefault(); + } + } + function handleBlur(field) { if (isChangedRef.get()) { saveValue(field); @@ -152,11 +175,19 @@
+ {#if rowData} +
+ + + + +
+ {/if}
{#if !rowData}
No data selected
{:else} - {#each orderedFields as field (field.uniqueName)} + {#each filteredFields as field (field.uniqueName)}
{field.columnName}
Date: Mon, 8 Dec 2025 15:01:02 +0100 Subject: [PATCH 5/7] Add multi-row selection support with bulk editing - Show "(Multiple values)" when selected rows have different values - Allow bulk editing: changes apply to all selected rows - Rename format to "Table - Row" for clarity --- .../web/src/celldata/TableCellView.svelte | 38 ++++++++++++++++--- packages/web/src/datagrid/DataGridCore.svelte | 10 ++++- .../web/src/widgets/CellDataWidget.svelte | 2 +- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/packages/web/src/celldata/TableCellView.svelte b/packages/web/src/celldata/TableCellView.svelte index 0c97a6c95..3b1b3f2af 100644 --- a/packages/web/src/celldata/TableCellView.svelte +++ b/packages/web/src/celldata/TableCellView.svelte @@ -23,17 +23,38 @@ $: realColumnUniqueNames = selection?.realColumnUniqueNames || []; $: setCellValue = selection?.setCellValue; + $: uniqueRows = _.uniqBy(selection || [], 'row'); + $: isMultipleRows = uniqueRows.length > 1; + + function areValuesEqual(val1, val2) { + if (val1 === val2) return true; + if (val1 == null && val2 == null) return true; + if (val1 == null || val2 == null) return false; + return _.isEqual(val1, val2); + } + + function getFieldValue(colName) { + if (!isMultipleRows) return { value: rowData?.[colName], hasMultipleValues: false }; + + const values = uniqueRows.map(sel => sel.rowData?.[colName]); + const firstValue = values[0]; + const allSame = values.every(v => areValuesEqual(v, firstValue)); + + return allSame ? { value: firstValue, hasMultipleValues: false } : { value: null, hasMultipleValues: true }; + } + let filter = ''; $: orderedFields = realColumnUniqueNames .map(colName => { const col = columns.find(c => c.uniqueName === colName); if (!col) return null; - const value = rowData?.[colName]; + const { value, hasMultipleValues } = getFieldValue(colName); return { columnName: col.columnName || colName, uniqueName: colName, value, + hasMultipleValues, col, }; }) @@ -73,7 +94,7 @@ function handleDoubleClick(field) { if (!editable || !setCellValue) return; - if (isJsonValue(field.value)) { + if (isJsonValue(field.value) && !field.hasMultipleValues) { openEditModal(field); return; } @@ -83,12 +104,12 @@ function startEditing(field) { if (!editable || !setCellValue) return; editingColumn = field.uniqueName; - editValue = stringifyCellValue(field.value, 'inlineEditorIntent', editorTypes).value; + editValue = field.hasMultipleValues ? '' : stringifyCellValue(field.value, 'inlineEditorIntent', editorTypes).value; isChangedRef.set(false); tick().then(() => { if (!domEditor) return; domEditor.focus(); - domEditor.select(); + if (!field.hasMultipleValues) domEditor.select(); }); } @@ -206,7 +227,7 @@ on:blur={() => handleBlur(field)} class="inline-editor" /> - {#if editable} + {#if editable && !field.hasMultipleValues} { @@ -216,6 +237,8 @@ /> {/if}
+ {:else if field.hasMultipleValues} + (Multiple values) {:else} diff --git a/packages/web/src/datagrid/DataGridCore.svelte b/packages/web/src/datagrid/DataGridCore.svelte index bc29172a1..c3f1c07af 100644 --- a/packages/web/src/datagrid/DataGridCore.svelte +++ b/packages/web/src/datagrid/DataGridCore.svelte @@ -1269,8 +1269,14 @@ res.realColumnUniqueNames = realColumnUniqueNames; if (res.length > 0) { - const firstRow = res[0].row; - res.setCellValue = (columnName, value) => grider.setCellValue(firstRow, columnName, value); + const uniqueRowIndices = _.uniq(res.map(x => x.row)); + res.setCellValue = (columnName, value) => { + grider.beginUpdate(); + for (const row of uniqueRowIndices) { + grider.setCellValue(row, columnName, value); + } + grider.endUpdate(); + }; } return res; diff --git a/packages/web/src/widgets/CellDataWidget.svelte b/packages/web/src/widgets/CellDataWidget.svelte index 6c60dddda..22eb15d96 100644 --- a/packages/web/src/widgets/CellDataWidget.svelte +++ b/packages/web/src/widgets/CellDataWidget.svelte @@ -16,7 +16,7 @@ }, { type: 'table', - title: 'Table', + title: 'Table - Row', component: TableCellView, single: false, }, From 142ebe3d2700bf5000d719c2e17a9692829681a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Pivo=C5=88ka?= Date: Mon, 8 Dec 2025 15:23:59 +0100 Subject: [PATCH 6/7] Fix scrolling in Table - Row view Use absolute positioning pattern for proper scrolling behavior when many columns are displayed. --- .../web/src/celldata/TableCellView.svelte | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/web/src/celldata/TableCellView.svelte b/packages/web/src/celldata/TableCellView.svelte index 3b1b3f2af..303a4df1b 100644 --- a/packages/web/src/celldata/TableCellView.svelte +++ b/packages/web/src/celldata/TableCellView.svelte @@ -196,19 +196,20 @@
- {#if rowData} -
- - - - -
- {/if} -
- {#if !rowData} -
No data selected
- {:else} - {#each filteredFields as field (field.uniqueName)} +
+ {#if rowData} +
+ + + + +
+ {/if} +
+ {#if !rowData} +
No data selected
+ {:else} + {#each filteredFields as field (field.uniqueName)}
{field.columnName}
{/each} - {/if} + {/if} +
@@ -264,12 +266,21 @@ .outer { flex: 1; position: relative; + } + + .content { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; display: flex; flex-direction: column; } .search-wrapper { padding: 4px 4px 0 4px; + flex-shrink: 0; } .inner { From 2a88ed38c4280f2b518ba2cee9256a037c13c2c9 Mon Sep 17 00:00:00 2001 From: Stela Augustinova Date: Mon, 8 Dec 2025 16:45:18 +0100 Subject: [PATCH 7/7] Added translation tags to TableCellView component, updated decimal handling --- packages/web/src/celldata/TableCellView.svelte | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/web/src/celldata/TableCellView.svelte b/packages/web/src/celldata/TableCellView.svelte index 303a4df1b..11e2ed021 100644 --- a/packages/web/src/celldata/TableCellView.svelte +++ b/packages/web/src/celldata/TableCellView.svelte @@ -12,6 +12,7 @@ import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte'; import SearchInput from '../elements/SearchInput.svelte'; import CloseSearchButton from '../buttons/CloseSearchButton.svelte'; + import { _t } from '../translations' export let selection; @@ -76,7 +77,7 @@ const isChangedRef = createRef(false); function isJsonValue(value) { - if (_.isPlainObject(value) && !(value?.type == 'Buffer' && _.isArray(value.data)) && !value.$oid && !value.$bigint) { + if (_.isPlainObject(value) && !(value?.type == 'Buffer' && _.isArray(value.data)) && !value.$oid && !value.$bigint && !value.$decimal) { return true; } if (_.isArray(value)) return true; @@ -200,14 +201,14 @@ {#if rowData}
- +
{/if}
{#if !rowData} -
No data selected
+
{_t('tableCell.noDataSelected', { defaultMessage: "No data selected" })}
{:else} {#each filteredFields as field (field.uniqueName)}
@@ -239,7 +240,7 @@ {/if}
{:else if field.hasMultipleValues} - (Multiple values) + ({_t('tableCell.multipleValues', { defaultMessage: "Multiple values" })}) {:else}