Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a88ed38c4 | |||
| 6ebee92542 | |||
| 7024e4b40d | |||
| bc2e27d7da | |||
| 142ebe3d27 | |||
| 38c25cae74 | |||
| 190c610466 | |||
| d220525ac7 | |||
| 5e4a631ff2 | |||
| 9099ce42b9 | |||
| df226fea22 | |||
| 89121a2608 | |||
| 23cf264d4d |
+1
-1
@@ -79,7 +79,7 @@ Builds:
|
||||
- ADDED: SQL AI assistant - powered by database chat, could help you to write SQL queries (Premium)
|
||||
- ADDED: Explain SQL error (powered by AI) (Premium)
|
||||
- ADDED: Database chat (and SQL AI Assistant) now supports showing charts (Premium)
|
||||
- FIXED: Fxied editing new files and roles (Team Premium)
|
||||
- FIXED: Fixed editing new files and roles (Team Premium)
|
||||
- FIXED: Connection to standalone database could be now pinned
|
||||
- FIXED: Cannot open up large JSON file #1215
|
||||
|
||||
|
||||
@@ -237,7 +237,7 @@ describe('Import CSV - source error', () => {
|
||||
cy.testid('ImportExportTab_preview_content').contains('Invalid Closing Quote').should('be.visible');
|
||||
|
||||
cy.testid('ImportExportTab_executeButton').click();
|
||||
cy.testid('ImportExportConfigurator_errorInfoIcon_customers-20-err').click();
|
||||
cy.testid('ImportExportConfigurator_errorInfoIcon_customers-20-err', { timeout: 10000 }).click();
|
||||
|
||||
cy.testid('ErrorMessageModal_message').contains('Invalid Closing Quote').should('be.visible');
|
||||
});
|
||||
@@ -256,7 +256,7 @@ describe('Import CSV - target error', () => {
|
||||
cy.contains('customers-20');
|
||||
cy.testid('ImportExportConfigurator_targetName_customers-20').clear().type('system."]`');
|
||||
cy.testid('ImportExportTab_executeButton').click();
|
||||
cy.testid('ImportExportConfigurator_errorInfoIcon_customers-20').click();
|
||||
cy.testid('ImportExportConfigurator_errorInfoIcon_customers-20', { timeout: 10000 }).click();
|
||||
cy.testid('ErrorMessageModal_message').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
<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';
|
||||
import { _t } from '../translations'
|
||||
|
||||
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 && !value.$decimal) {
|
||||
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={_t('tableCell.filterColumns', { defaultMessage: "Filter columns (regex)" })} bind:value={filter} />
|
||||
<CloseSearchButton bind:filter />
|
||||
</SearchBoxWrapper>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="inner">
|
||||
{#if !rowData}
|
||||
<div class="no-data">{_t('tableCell.noDataSelected', { defaultMessage: "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">({_t('tableCell.multipleValues', { defaultMessage: "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),
|
||||
insertedRowIndex: grider?.getInsertedRowIndex(row),
|
||||
rowStatus: grider.getRowStatus(row),
|
||||
onSetValue: value => grider.setCellValue(row, column, value),
|
||||
editable: grider.editable,
|
||||
editorTypes: display?.driver?.dataEditorTypesBehaviour,
|
||||
};
|
||||
})
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,12 @@
|
||||
component: TextCellViewNoWrap,
|
||||
single: false,
|
||||
},
|
||||
{
|
||||
type: 'table',
|
||||
title: 'Table - Row',
|
||||
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';
|
||||
|
||||
@@ -6,6 +6,56 @@ const lineReader = require('line-reader');
|
||||
|
||||
let dbgateApi;
|
||||
|
||||
class StripUtf8BomTransform extends stream.Transform {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this._checkedBOM = false;
|
||||
this._pending = Buffer.alloc(0); // store initial bytes until we know if BOM is present
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, callback) {
|
||||
if (this._checkedBOM) {
|
||||
// We already handled BOM decision, just pass through
|
||||
this.push(chunk);
|
||||
return callback();
|
||||
}
|
||||
|
||||
// Accumulate into pending until we can decide
|
||||
this._pending = Buffer.concat([this._pending, chunk]);
|
||||
|
||||
if (this._pending.length < 3) {
|
||||
// Still don't know if it's BOM or not (need at least 3 bytes)
|
||||
return callback();
|
||||
}
|
||||
|
||||
// Now we can check the first 3 bytes
|
||||
const BOM = [0xef, 0xbb, 0xbf];
|
||||
const hasBom = this._pending[0] === BOM[0] && this._pending[1] === BOM[1] && this._pending[2] === BOM[2];
|
||||
|
||||
if (hasBom) {
|
||||
// Drop the BOM, push the rest
|
||||
this.push(this._pending.slice(3));
|
||||
} else {
|
||||
// No BOM, push everything as-is
|
||||
this.push(this._pending);
|
||||
}
|
||||
|
||||
this._pending = Buffer.alloc(0);
|
||||
this._checkedBOM = true;
|
||||
callback();
|
||||
}
|
||||
|
||||
_flush(callback) {
|
||||
// Stream ended but we never had enough bytes to decide (length < 3)
|
||||
if (!this._checkedBOM && this._pending.length > 0) {
|
||||
// If it's less than 3 bytes, it can't be a UTF-8 BOM, so just pass it through
|
||||
this.push(this._pending);
|
||||
}
|
||||
this._pending = Buffer.alloc(0);
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
function readFirstLine(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
lineReader.open(file, (err, reader) => {
|
||||
@@ -95,7 +145,7 @@ async function reader({ fileName, encoding = 'utf-8', header = true, delimiter,
|
||||
});
|
||||
const fileStream = fs.createReadStream(downloadedFile, encoding);
|
||||
const csvPrepare = new CsvPrepareStream({ header });
|
||||
return [fileStream, csvStream, csvPrepare];
|
||||
return [fileStream, new StripUtf8BomTransform(), csvStream, csvPrepare];
|
||||
// fileStream.pipe(csvStream);
|
||||
// csvStream.pipe(csvPrepare);
|
||||
// return csvPrepare;
|
||||
|
||||
Reference in New Issue
Block a user