mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-30 13:53:59 +00:00
Merge pull request #1290 from david-pivonka/feature/table-cell-data-view
Add Table format to Cell data view sidebar
This commit is contained in:
355
packages/web/src/celldata/TableCellView.svelte
Normal file
355
packages/web/src/celldata/TableCellView.svelte
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
<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';
|
||||||
|
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
|
||||||
|
import SearchInput from '../elements/SearchInput.svelte';
|
||||||
|
import CloseSearchButton from '../buttons/CloseSearchButton.svelte';
|
||||||
|
|
||||||
|
export let selection;
|
||||||
|
|
||||||
|
$: firstSelection = selection?.[0];
|
||||||
|
$: rowData = firstSelection?.rowData;
|
||||||
|
$: editable = firstSelection?.editable;
|
||||||
|
$: editorTypes = firstSelection?.editorTypes;
|
||||||
|
$: columns = selection?.columns || [];
|
||||||
|
$: 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, hasMultipleValues } = getFieldValue(colName);
|
||||||
|
return {
|
||||||
|
columnName: col.columnName || colName,
|
||||||
|
uniqueName: colName,
|
||||||
|
value,
|
||||||
|
hasMultipleValues,
|
||||||
|
col,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.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;
|
||||||
|
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') 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);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDoubleClick(field) {
|
||||||
|
if (!editable || !setCellValue) return;
|
||||||
|
if (isJsonValue(field.value) && !field.hasMultipleValues) {
|
||||||
|
openEditModal(field);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startEditing(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startEditing(field) {
|
||||||
|
if (!editable || !setCellValue) return;
|
||||||
|
editingColumn = field.uniqueName;
|
||||||
|
editValue = field.hasMultipleValues ? '' : stringifyCellValue(field.value, 'inlineEditorIntent', editorTypes).value;
|
||||||
|
isChangedRef.set(false);
|
||||||
|
tick().then(() => {
|
||||||
|
if (!domEditor) return;
|
||||||
|
domEditor.focus();
|
||||||
|
if (!field.hasMultipleValues) 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();
|
||||||
|
moveToNextField(field, event.shiftKey);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveToNextField(field, reverse) {
|
||||||
|
const currentIndex = filteredFields.findIndex(f => f.uniqueName === field.uniqueName);
|
||||||
|
const nextIndex = reverse ? currentIndex - 1 : currentIndex + 1;
|
||||||
|
if (nextIndex < 0 || nextIndex >= filteredFields.length) return;
|
||||||
|
|
||||||
|
tick().then(() => {
|
||||||
|
const nextField = filteredFields[nextIndex];
|
||||||
|
if (isJsonValue(nextField.value)) {
|
||||||
|
openEditModal(nextField);
|
||||||
|
} else {
|
||||||
|
startEditing(nextField);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
if (editorTypes?.explicitDataType) return null;
|
||||||
|
if (!isJsonLikeLongString(value)) return null;
|
||||||
|
return safeJsonParse(value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="outer">
|
||||||
|
<div class="content">
|
||||||
|
{#if rowData}
|
||||||
|
<div class="search-wrapper" on:keydown={handleSearchKeyDown}>
|
||||||
|
<SearchBoxWrapper noMargin>
|
||||||
|
<SearchInput placeholder="Filter columns (regex)" bind:value={filter} />
|
||||||
|
<CloseSearchButton bind:filter />
|
||||||
|
</SearchBoxWrapper>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="inner">
|
||||||
|
{#if !rowData}
|
||||||
|
<div class="no-data">No data selected</div>
|
||||||
|
{:else}
|
||||||
|
{#each filteredFields 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 && !field.hasMultipleValues}
|
||||||
|
<ShowFormButton
|
||||||
|
icon="icon edit"
|
||||||
|
on:click={() => {
|
||||||
|
editingColumn = null;
|
||||||
|
openEditModal(field);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else if field.hasMultipleValues}
|
||||||
|
<span class="multiple-values">(Multiple values)</span>
|
||||||
|
{: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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.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 {
|
||||||
|
overflow: auto;
|
||||||
|
flex: 1;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiple-values {
|
||||||
|
color: var(--theme-font-3);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1258,9 +1258,27 @@
|
|||||||
condition: display?.getChangeSetCondition(rowData),
|
condition: display?.getChangeSetCondition(rowData),
|
||||||
insertedRowIndex: grider?.getInsertedRowIndex(row),
|
insertedRowIndex: grider?.getInsertedRowIndex(row),
|
||||||
rowStatus: grider.getRowStatus(row),
|
rowStatus: grider.getRowStatus(row),
|
||||||
|
onSetValue: value => grider.setCellValue(row, column, value),
|
||||||
|
editable: grider.editable,
|
||||||
|
editorTypes: display?.driver?.dataEditorTypesBehaviour,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(x => x.column);
|
.filter(x => x.column);
|
||||||
|
|
||||||
|
res.columns = columns;
|
||||||
|
res.realColumnUniqueNames = realColumnUniqueNames;
|
||||||
|
|
||||||
|
if (res.length > 0) {
|
||||||
|
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;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,12 @@
|
|||||||
component: TextCellViewNoWrap,
|
component: TextCellViewNoWrap,
|
||||||
single: false,
|
single: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'table',
|
||||||
|
title: 'Table - Row',
|
||||||
|
component: TableCellView,
|
||||||
|
single: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'json',
|
type: 'json',
|
||||||
title: 'Json',
|
title: 'Json',
|
||||||
@@ -92,6 +98,7 @@
|
|||||||
import JsonRowView from '../celldata/JsonRowView.svelte';
|
import JsonRowView from '../celldata/JsonRowView.svelte';
|
||||||
import MapCellView from '../celldata/MapCellView.svelte';
|
import MapCellView from '../celldata/MapCellView.svelte';
|
||||||
import PictureCellView from '../celldata/PictureCellView.svelte';
|
import PictureCellView from '../celldata/PictureCellView.svelte';
|
||||||
|
import TableCellView from '../celldata/TableCellView.svelte';
|
||||||
import TextCellViewNoWrap from '../celldata/TextCellViewNoWrap.svelte';
|
import TextCellViewNoWrap from '../celldata/TextCellViewNoWrap.svelte';
|
||||||
import TextCellViewWrap from '../celldata/TextCellViewWrap.svelte';
|
import TextCellViewWrap from '../celldata/TextCellViewWrap.svelte';
|
||||||
import ErrorInfo from '../elements/ErrorInfo.svelte';
|
import ErrorInfo from '../elements/ErrorInfo.svelte';
|
||||||
|
|||||||
Reference in New Issue
Block a user