Merge branch 'master' into develop

This commit is contained in:
Jan Prochazka
2021-10-14 11:19:33 +02:00
16 changed files with 372 additions and 16 deletions

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { filterName } from 'dbgate-tools';
import _ from 'lodash';
import { useDatabaseList } from '../utility/metadataLoaders';
import AppObjectList from './AppObjectList.svelte';
import * as databaseAppObject from './DatabaseAppObject.svelte';
@@ -11,6 +12,9 @@
</script>
<AppObjectList
list={($databases || []).filter(x => filterName(filter, x.name)).map(db => ({ ...db, connection: data }))}
list={_.sortBy(
($databases || []).filter(x => filterName(filter, x.name)),
'name'
).map(db => ({ ...db, connection: data }))}
module={databaseAppObject}
/>

View File

@@ -111,11 +111,29 @@
id: 'dataGrid.filterSelected',
category: 'Data grid',
name: 'Filter selected value',
keyText: 'Ctrl+F',
keyText: 'Ctrl+Shift+F',
testEnabled: () => getCurrentDataGrid()?.getDisplay().filterable,
onClick: () => getCurrentDataGrid().filterSelectedValue(),
});
registerCommand({
id: 'dataGrid.findColumn',
category: 'Data grid',
name: 'Find colunn',
keyText: 'Ctrl+F',
testEnabled: () => getCurrentDataGrid() != null,
getSubCommands: () => getCurrentDataGrid().buildFindMenu(),
});
registerCommand({
id: 'dataGrid.hideColumn',
category: 'Data grid',
name: 'Hide colunn',
keyText: 'Ctrl+H',
testEnabled: () => getCurrentDataGrid() != null,
onClick: () => getCurrentDataGrid().hideColumn(),
});
registerCommand({
id: 'dataGrid.clearFilter',
category: 'Data grid',
@@ -125,6 +143,15 @@
onClick: () => getCurrentDataGrid().clearFilter(),
});
registerCommand({
id: 'dataGrid.generateSqlFromData',
category: 'Data grid',
name: 'Generate SQL',
keyText: 'Ctrl+G',
testEnabled: () => getCurrentDataGrid()?.generateSqlFromDataEnabled(),
onClick: () => getCurrentDataGrid().generateSqlFromData(),
});
registerCommand({
id: 'dataGrid.openFreeTable',
category: 'Data grid',
@@ -215,6 +242,8 @@
import { editJsonRowDocument } from '../jsonview/CollectionJsonRow.svelte';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import CollapseButton from './CollapseButton.svelte';
import GenerateSqlFromDataModal from '../modals/GenerateSqlFromDataModal.svelte';
import { showModal } from '../modals/modalTools';
export let onLoadNextData = undefined;
export let grider = undefined;
@@ -347,6 +376,7 @@
});
const text = lines.join('\r\n');
copyTextToClipboard(text);
// if (domFocusField) domFocusField.focus();
}
export function loadNextDataIfNeeded() {
@@ -416,6 +446,119 @@
editJsonRowDocument(grider, rowIndex);
}
export function buildFindMenu() {
const res = [];
async function clickColumn(uniquePath) {
display.setColumnVisibility(uniquePath, true);
await tick();
const invMap = _.invert(realColumnUniqueNames);
const colIndex = invMap[uniquePath.join('.')];
scrollIntoView([null, colIndex]);
currentCell = [currentCell[0], parseInt(colIndex)];
selectedCells = [currentCell];
}
for (const column of display.columns) {
if (column.uniquePath.length > 1) continue;
res.push({
text: column.columnName,
icon: 'img column',
onClick: async () => {
clickColumn(column.uniquePath);
},
});
}
for (const column of display.columns) {
if (column.uniquePath.length > 1) continue;
if (column.isExpandable) {
const table = display.getFkTarget(column);
if (!table) continue;
for (const childColumn of table.columns) {
res.push({
text: `${column.columnName}.${childColumn.columnName}`,
icon: 'img column',
onClick: async () => {
display.toggleExpandedColumn(column.uniqueName, true);
clickColumn([...column.uniquePath, childColumn.columnName]);
},
});
}
}
}
for (const fk of display?.baseTable?.foreignKeys || []) {
res.push({
text: `${fk.refTableName} (${fk.columns.map(x => x.columnName).join(', ')})`,
icon: 'img link',
onClick: () => {
onReferenceClick({
schemaName: fk.refSchemaName,
pureName: fk.refTableName,
columns: fk.columns.map(col => ({
baseName: col.columnName,
refName: col.refColumnName,
})),
});
},
});
}
for (const fk of display?.baseTable?.dependencies || []) {
res.push({
text: `${fk.pureName} (${fk.columns.map(x => x.columnName).join(', ')})`,
icon: 'img reference',
onClick: () => {
onReferenceClick({
schemaName: fk.schemaName,
pureName: fk.pureName,
columns: fk.columns.map(col => ({
baseName: col.refColumnName,
refName: col.columnName,
})),
});
},
});
}
return res;
}
export function hideColumn() {
const columnIndexes = _.uniq(selectedCells.map(x => x[1]));
for (const index of columnIndexes) {
const name = realColumnUniqueNames[index];
const column = display.allColumns.find(x => x.uniqueName == name);
if (column) {
display.setColumnVisibility(column.uniquePath, false);
}
}
// selectedCells = [currentCell];
}
export function generateSqlFromDataEnabled() {
return !!display?.baseTable;
}
export function generateSqlFromData() {
const columnIndexes = _.uniq(selectedCells.map(x => x[1]));
columnIndexes.sort();
showModal(GenerateSqlFromDataModal, {
rows: getSelectedRowData(),
allColumns: display.baseTable.columns.map(x => x.columnName),
selectedColumns: columnIndexes.map(x => realColumnUniqueNames[x]),
keyColumns: display?.baseTable?.primaryKey?.columns?.map(x => x.columnName) || [
display.baseTable.columns[0].columnName,
],
engineDriver: display?.driver,
tableInfo: display.baseTable,
});
}
$: autofillMarkerCell =
selectedCells && selectedCells.length > 0 && _.uniq(selectedCells.map(x => x[0])).length == 1
? [_.max(selectedCells.map(x => x[0])), _.max(selectedCells.map(x => x[1]))]
@@ -611,6 +754,8 @@
selectedCells = [...selectedCells, cell];
}
}
} else if (event.shiftKey) {
selectedCells = getCellRange(oldCurrentCell, cell);
} else {
selectedCells = getCellRange(cell, cell);
dragStartCell = cell;
@@ -927,7 +1072,7 @@
currentCell = cell;
// @ts-ignore
selectedCells = [cell];
domFocusField.focus();
if (domFocusField) domFocusField.focus();
};
const [inplaceEditorState, dispatchInsplaceEditor] = createReducer((state, action) => {
@@ -977,6 +1122,8 @@
{ command: 'dataGrid.setNull' },
{ placeTag: 'edit' },
{ divider: true },
{ command: 'dataGrid.findColumn' },
{ command: 'dataGrid.hideColumn' },
{ command: 'dataGrid.filterSelected' },
{ command: 'dataGrid.clearFilter' },
{ command: 'dataGrid.undo' },
@@ -984,6 +1131,7 @@
{ command: 'dataGrid.editJsonDocument' },
{ divider: true },
{ placeTag: 'export' },
{ command: 'dataGrid.generateSqlFromData' },
{ command: 'dataGrid.openFreeTable' },
{ command: 'dataGrid.openChartFromSelection' },
{ placeTag: 'chart' }

View File

@@ -0,0 +1,44 @@
<script lang="ts">
import FormStyledButton from './FormStyledButton.svelte';
export let selectedColumns;
export let allColumns;
export let disabled = false;
function toggleColumn(column) {
if (selectedColumns.includes(column)) selectedColumns = selectedColumns.filter(x => x != column);
else selectedColumns = [...selectedColumns, column];
}
</script>
<div>
<FormStyledButton value="All" on:click={() => (selectedColumns = allColumns)} {disabled} />
<FormStyledButton value="None" on:click={() => (selectedColumns = [])} {disabled} />
</div>
<div class="list">
{#each allColumns as column}
<div>
<input
type="checkbox"
{disabled}
checked={selectedColumns.includes(column)}
on:change={() => toggleColumn(column)}
/>
<span on:click={() => toggleColumn(column)} class="label">
{column}
</span>
</div>
{/each}
</div>
<style>
.list {
max-height: 25vh;
overflow: scroll;
user-select: none;
}
.label {
cursor: pointer;
}
</style>

View File

@@ -3,7 +3,7 @@
import { getFormContext } from './FormProviderCore.svelte';
import { createEventDispatcher } from 'svelte';
export let disabled;
export let disabled = false;
const dispatch = createEventDispatcher();

View File

@@ -88,7 +88,7 @@
id: 'dataForm.filterSelected',
category: 'Data form',
name: 'Filter this value',
keyText: 'Ctrl+F',
keyText: 'Ctrl+Shift+F',
testEnabled: () => getCurrentDataForm() != null,
onClick: () => getCurrentDataForm().filterSelectedValue(),
});

View File

@@ -0,0 +1,140 @@
<script lang="ts">
import CheckableColumnList from '../elements/CheckableColumnList.svelte';
import FormStyledButton from '../elements/FormStyledButton.svelte';
import TableControl from '../elements/TableControl.svelte';
import FormProvider from '../forms/FormProvider.svelte';
import FormSubmit from '../forms/FormSubmit.svelte';
import TextField from '../forms/TextField.svelte';
import analyseQuerySources from '../query/analyseQuerySources';
import newQuery from '../query/newQuery';
import SqlEditor from '../query/SqlEditor.svelte';
import keycodes from '../utility/keycodes';
import ModalBase from './ModalBase.svelte';
import { closeCurrentModal } from './modalTools';
export let rows;
export let allColumns = [];
export let selectedColumns = [];
export let keyColumns = [];
export let tableInfo;
export let engineDriver;
let queryTypeIndex = 0;
let domQueryType = null;
let valueColumns = selectedColumns.filter(x => allColumns.includes(x));
let whereColumns = keyColumns.filter(x => allColumns.includes(x));
const QUERY_TYPES = ['INSERT', 'UPDATE', 'DELETE'];
const VALUE_QUERIES = ['INSERT', 'UPDATE'];
const WHERE_QUERIES = ['UPDATE', 'DELETE'];
$: sqlPreview = computePreview(rows, valueColumns, whereColumns, queryTypeIndex);
function computePreview(rows, valueColumns, whereColumns, queryTypeIndex) {
const queryType = QUERY_TYPES[queryTypeIndex];
const dmp = engineDriver.createDumper();
function putCondition(row) {
dmp.putCollection(' ^and ', whereColumns, col =>
row[col] == null ? dmp.put('%i ^is ^null', col) : dmp.put('%i=%v', col, row[col])
);
}
switch (queryType) {
case 'INSERT':
for (const row of rows) {
dmp.putCmd(
'^insert ^into %f (%,i) ^values (%,v)',
tableInfo,
valueColumns,
valueColumns.map(col => row[col])
);
}
break;
case 'UPDATE':
for (const row of rows) {
dmp.put('^update %f ^set', tableInfo);
dmp.putCollection(', ', valueColumns, col => dmp.put('%i=%v', col, row[col]));
dmp.put(' ^where ');
putCondition(row);
dmp.endCommand();
}
break;
case 'DELETE':
for (const row of rows) {
dmp.put('^delete ^from %f ^where ', tableInfo);
putCondition(row);
dmp.endCommand();
}
break;
}
return dmp.s;
}
</script>
<FormProvider>
<ModalBase {...$$restProps}>
<svelte:fragment slot="header">Generate SQL from data</svelte:fragment>
<div class="flex mb-3">
<div class="m-1 col-4">
<div class="m-1">Choose query type</div>
<TableControl
rows={QUERY_TYPES.map(name => ({ name }))}
bind:selectedIndex={queryTypeIndex}
bind:domTable={domQueryType}
focusOnCreate
selectable
columns={[{ fieldName: 'name', header: 'Query type' }]}
/>
</div>
<div class="m-1 col-4">
<div class="m-1">Value columns</div>
<CheckableColumnList
{allColumns}
bind:selectedColumns={valueColumns}
disabled={!VALUE_QUERIES.includes(QUERY_TYPES[queryTypeIndex])}
/>
</div>
<div class="m-1 col-4">
<div class="m-1">WHERE columns</div>
<CheckableColumnList
{allColumns}
bind:selectedColumns={whereColumns}
disabled={!WHERE_QUERIES.includes(QUERY_TYPES[queryTypeIndex])}
/>
</div>
</div>
<div class="sql">
<SqlEditor readOnly value={sqlPreview} engine={engineDriver?.engine} />
</div>
<svelte:fragment slot="footer">
<FormSubmit
value="OK"
on:click={() => {
newQuery({ initialData: sqlPreview });
closeCurrentModal();
}}
/>
<FormStyledButton type="button" value="Close" on:click={closeCurrentModal} />
</svelte:fragment>
</ModalBase>
</FormProvider>
<style>
.sql {
position: relative;
height: 25vh;
width: 40vw;
}
</style>

View File

@@ -104,7 +104,7 @@
<svelte:fragment slot="header">Insert join</svelte:fragment>
<div class="flex mb-3">
<div class="m-1">
<div class="m-1 col-3">
<div class="m-1">Existing table</div>
<TableControl
@@ -121,7 +121,7 @@
/>
</div>
<div class="m-1">
<div class="m-1 col-6">
<div class="m-1">New table</div>
<TableControl
@@ -138,7 +138,7 @@
/>
</div>
<div class="m-1">
<div class="m-1 col-3">
<div class="m-1">Join</div>
<TableControl

View File

@@ -12,10 +12,10 @@
import useEffect from '../utility/useEffect';
import { getContext } from 'svelte';
import { mountCodeCompletion } from './codeCompletion';
export let engine;
export let conid;
export let database;
export let readOnly;
export let engine = null;
export let conid = null;
export let database = null;
export let readOnly = false;
let domEditor;

View File

@@ -5,6 +5,7 @@
id: 'query.formatCode',
category: 'Query',
name: 'Format code',
keyText: 'Shift+Alt+F',
testEnabled: () => getCurrentEditor()?.isSqlEditor(),
onClick: () => getCurrentEditor().formatCode(),
});
@@ -203,6 +204,7 @@
onInsert: text => {
const editor = domEditor.getEditor();
editor.session.insert(editor.getCursorPosition(), text);
domEditor?.getEditor()?.focus();
},
});
}

View File

@@ -1,4 +1,6 @@
export function copyTextToClipboard(text) {
const oldFocus = document.activeElement;
const textArea = document.createElement('textarea');
//
@@ -53,4 +55,6 @@ export function copyTextToClipboard(text) {
}
document.body.removeChild(textArea);
if (oldFocus) oldFocus.focus();
}

View File

@@ -32,7 +32,7 @@
</script>
<SearchBoxWrapper>
<SearchInput placeholder="Search connection" bind:value={filter} />
<SearchInput placeholder="Search connection or database" bind:value={filter} />
<InlineButton on:click={handleRefreshConnections}>Refresh</InlineButton>
</SearchBoxWrapper>
<WidgetsInnerContainer>