save dialog

This commit is contained in:
Jan Prochazka 2021-03-06 18:01:39 +01:00
parent b64b6be68a
commit 423644e9d9
11 changed files with 573 additions and 29 deletions

View File

@ -98,9 +98,6 @@
position: relative;
overflow: hidden;
}
td.isSelected {
background: var(--theme-bg-selected);
}
td.isFrameSelected {
outline: 3px solid var(--theme-bg-selected);
outline-offset: -3px;
@ -127,6 +124,9 @@
background-repeat: repeat-x;
background-position: 50% 50%;
}
td.isSelected {
background: var(--theme-bg-selected);
}
.hint {
color: var(--theme-font-3);

View File

@ -12,6 +12,17 @@
onClick: () => get(currentDataGrid).refresh(),
});
registerCommand({
id: 'dataGrid.save',
category: 'Data grid',
name: 'Save',
keyText: 'Ctrl+S',
toolbar: true,
icon: 'icon save',
enabledStore: derived([currentDataGrid], ([grid]) => grid?.getGrider()?.allowSave),
onClick: () => get(currentDataGrid).save(),
});
function getRowCountInfo(selectedCells, grider, realColumnUniqueNames, selectedRowData, allRowCount) {
if (selectedCells.length > 1 && selectedCells.every(x => _.isNumber(x[0]) && _.isNumber(x[1]))) {
let sum = _.sumBy(selectedCells, cell => {
@ -62,6 +73,9 @@
import InlineButton from '../elements/InlineButton.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import DataFilterControl from './DataFilterControl.svelte';
import createReducer from '../utility/createReducer';
import keycodes from '../utility/keycodes';
import { nullStore } from '../stores';
export let loadNextData = undefined;
export let grider = undefined;
@ -73,6 +87,7 @@
export let allRowCount = undefined;
export let onReferenceSourceChanged = undefined;
export let onReferenceClick = undefined;
export let onSave;
export let isLoadedAll;
export let loadedTime;
@ -97,6 +112,18 @@
let autofillDragStartCell = nullCell;
let autofillSelectedCells = emptyCellArray;
export function refresh() {
display.reload();
}
export function save() {
if (onSave) onSave();
}
export function getGrider() {
return grider;
}
$: 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]))]
@ -228,6 +255,7 @@
if (autofill) {
autofillDragStartCell = cell;
} else {
const oldCurrentCell = currentCell;
currentCell = cell;
if (event.ctrlKey) {
@ -242,13 +270,11 @@
selectedCells = getCellRange(cell, cell);
dragStartCell = cell;
// if (isRegularCell(cell) && !_.isEqual(cell, inplaceEditorState.cell) && _.isEqual(cell, currentCell)) {
// // @ts-ignore
// dispatchInsplaceEditor({ type: 'show', cell, selectAll: true });
// } else if (!_.isEqual(cell, inplaceEditorState.cell)) {
// // @ts-ignore
// dispatchInsplaceEditor({ type: 'close' });
// }
if (isRegularCell(cell) && !_.isEqual(cell, inplaceEditorState.cell) && _.isEqual(cell, oldCurrentCell)) {
dispatchInsplaceEditor({ type: 'show', cell, selectAll: true });
} else if (!_.isEqual(cell, inplaceEditorState.cell)) {
dispatchInsplaceEditor({ type: 'close' });
}
}
}
@ -314,10 +340,6 @@
domVerticalScroll.scroll(newFirstVisibleRowScrollIndex);
}
export function refresh() {
display.reload();
}
function getSelectedRowIndexes() {
if (selectedCells.find(x => x[0] == 'header')) return _.range(0, grider.rowCount);
return _.uniq((selectedCells || []).map(x => x[0])).filter(x => _.isNumber(x));
@ -339,6 +361,206 @@
}))
);
}
function handleGridKeyDown(event) {
if (event.keyCode == keycodes.f5) {
event.preventDefault();
display.reload();
}
// if (event.keyCode == keycodes.f4) {
// event.preventDefault();
// handleSwitchToFormView();
// }
// if (event.keyCode == keycodes.s && event.ctrlKey) {
// event.preventDefault();
// handleSave();
// // this.saveAndFocus();
// }
if (event.keyCode == keycodes.n0 && event.ctrlKey) {
event.preventDefault();
setNull();
}
// if (event.keyCode == keycodes.r && event.ctrlKey) {
// event.preventDefault();
// revertRowChanges();
// }
// if (event.keyCode == keycodes.f && event.ctrlKey) {
// event.preventDefault();
// filterSelectedValue();
// }
// if (event.keyCode == keycodes.z && event.ctrlKey) {
// event.preventDefault();
// undo();
// }
// if (event.keyCode == keycodes.y && event.ctrlKey) {
// event.preventDefault();
// redo();
// }
// if (event.keyCode == keycodes.c && event.ctrlKey) {
// event.preventDefault();
// copyToClipboard();
// }
// if (event.keyCode == keycodes.delete && event.ctrlKey) {
// event.preventDefault();
// deleteSelectedRows();
// // this.saveAndFocus();
// }
// if (event.keyCode == keycodes.insert && !event.ctrlKey) {
// event.preventDefault();
// insertNewRow();
// // this.saveAndFocus();
// }
if ($inplaceEditorState.cell) return;
if (
!event.ctrlKey &&
!event.altKey &&
((event.keyCode >= keycodes.a && event.keyCode <= keycodes.z) ||
(event.keyCode >= keycodes.n0 && event.keyCode <= keycodes.n9) ||
event.keyCode == keycodes.dash)
) {
// @ts-ignore
event.preventDefault();
dispatchInsplaceEditor({ type: 'show', text: event.key, cell: currentCell });
// console.log('event', event.nativeEvent);
}
if (event.keyCode == keycodes.f2) {
// @ts-ignore
dispatchInsplaceEditor({ type: 'show', cell: currentCell, selectAll: true });
}
if (event.shiftKey) {
if (!isRegularCell(shiftDragStartCell)) {
shiftDragStartCell = currentCell;
}
} else {
shiftDragStartCell = nullCell;
}
handleCursorMove(event);
if (event.shiftKey) {
selectedCells = getCellRange(shiftDragStartCell || currentCell, currentCell);
}
}
function handleCursorMove(event) {
if (!isRegularCell(currentCell)) return null;
let rowCount = grider.rowCount;
if (event.ctrlKey) {
switch (event.keyCode) {
case keycodes.upArrow:
case keycodes.pageUp:
return moveCurrentCell(0, currentCell[1], event);
case keycodes.downArrow:
case keycodes.pageDown:
return moveCurrentCell(rowCount - 1, currentCell[1], event);
case keycodes.leftArrow:
return moveCurrentCell(currentCell[0], 0, event);
case keycodes.rightArrow:
return moveCurrentCell(currentCell[0], columnSizes.realCount - 1, event);
case keycodes.home:
return moveCurrentCell(0, 0, event);
case keycodes.end:
return moveCurrentCell(rowCount - 1, columnSizes.realCount - 1, event);
case keycodes.a:
selectedCells = [['header', 'header']];
event.preventDefault();
return ['header', 'header'];
}
} else {
switch (event.keyCode) {
case keycodes.upArrow:
// if (currentCell[0] == 0) return focusFilterEditor(currentCell[1]);
return moveCurrentCell(currentCell[0] - 1, currentCell[1], event);
case keycodes.downArrow:
case keycodes.enter:
return moveCurrentCell(currentCell[0] + 1, currentCell[1], event);
case keycodes.leftArrow:
return moveCurrentCell(currentCell[0], currentCell[1] - 1, event);
case keycodes.rightArrow:
return moveCurrentCell(currentCell[0], currentCell[1] + 1, event);
case keycodes.home:
return moveCurrentCell(currentCell[0], 0, event);
case keycodes.end:
return moveCurrentCell(currentCell[0], columnSizes.realCount - 1, event);
case keycodes.pageUp:
return moveCurrentCell(currentCell[0] - visibleRowCountLowerBound, currentCell[1], event);
case keycodes.pageDown:
return moveCurrentCell(currentCell[0] + visibleRowCountLowerBound, currentCell[1], event);
}
}
return null;
}
function setNull() {
grider.beginUpdate();
selectedCells.filter(isRegularCell).forEach(cell => {
setCellValue(cell, null);
});
grider.endUpdate();
}
function setCellValue(cell, value) {
grider.setCellValue(cell[0], realColumnUniqueNames[cell[1]], value);
}
function moveCurrentCell(row, col, event = null) {
const rowCount = grider.rowCount;
if (row < 0) row = 0;
if (row >= rowCount) row = rowCount - 1;
if (col < 0) col = 0;
if (col >= columnSizes.realCount) col = columnSizes.realCount - 1;
currentCell = [row, col];
// setSelectedCells([...(event.ctrlKey ? selectedCells : []), [row, col]]);
selectedCells = [[row, col]];
scrollIntoView([row, col]);
// this.selectedCells.push(this.currentCell);
// this.scrollIntoView(this.currentCell);
if (event) event.preventDefault();
return [row, col];
}
const [inplaceEditorState, dispatchInsplaceEditor] = createReducer((state, action) => {
switch (action.type) {
case 'show':
if (!grider.editable) return {};
return {
cell: action.cell,
text: action.text,
selectAll: action.selectAll,
};
case 'close': {
const [row, col] = currentCell || [];
if (domFocusField) domFocusField.focus();
// @ts-ignore
if (action.mode == 'enter' && row) setTimeout(() => moveCurrentCell(row + 1, col), 0);
// if (action.mode == 'save') setTimeout(handleSave, 0);
return {};
}
case 'shouldSave': {
return {
...state,
shouldSave: true,
};
}
}
return {};
}, {});
</script>
<div class="container" bind:clientWidth={containerWidth} bind:clientHeight={containerHeight}>
@ -346,6 +568,7 @@
type="text"
class="focus-field"
bind:this={domFocusField}
on:keydown={handleGridKeyDown}
on:focus={() => {
currentDataGrid.set(instance);
}}
@ -436,6 +659,8 @@
selectedCells={filterCellsForRow(selectedCells, rowIndex)}
autofillMarkerCell={filterCellForRow(autofillMarkerCell, rowIndex)}
focusedColumn={display.focusedColumn}
inplaceEditorState={$inplaceEditorState}
{dispatchInsplaceEditor}
{frameSelection}
/>
{/each}

View File

@ -1,6 +1,7 @@
<script lang="ts">
import DataGridCell from './DataGridCell.svelte';
import { cellIsSelected } from './gridutil';
import InplaceEditor from './InplaceEditor.svelte';
import RowHeaderCell from './RowHeaderCell.svelte';
@ -13,6 +14,8 @@
export let autofillSelectedCells = undefined;
export let autofillMarkerCell = undefined;
export let focusedColumn = undefined;
export let inplaceEditorState;
export let dispatchInsplaceEditor;
$: rowData = grider.getRowData(rowIndex);
$: rowStatus = grider.getRowStatus(rowIndex);
@ -29,16 +32,32 @@
<tr style={`height: ${rowHeight}px`}>
<RowHeaderCell {rowIndex} />
{#each visibleRealColumns as col (col.uniqueName)}
<DataGridCell
{rowIndex}
{rowData}
{col}
{hintFieldsAllowed}
isSelected={frameSelection ? false : cellIsSelected(rowIndex, col.colIndex, selectedCells)}
isFrameSelected={frameSelection ? cellIsSelected(rowIndex, col.colIndex, selectedCells) : false}
isAutofillSelected={cellIsSelected(rowIndex, col.colIndex, autofillSelectedCells)}
isFocusedColumn={col.uniqueName == focusedColumn}
/>
{#if inplaceEditorState.cell && rowIndex == inplaceEditorState.cell[0] && col.colIndex == inplaceEditorState.cell[1]}
<InplaceEditor
width={col.width}
{inplaceEditorState}
{dispatchInsplaceEditor}
cellValue={rowData[col.uniqueName]}
onSetValue={value => grider.setCellValue(rowIndex, col.uniqueName, value)}
/>
{:else}
<DataGridCell
{rowIndex}
{rowData}
{col}
{hintFieldsAllowed}
isSelected={frameSelection ? false : cellIsSelected(rowIndex, col.colIndex, selectedCells)}
isFrameSelected={frameSelection ? cellIsSelected(rowIndex, col.colIndex, selectedCells) : false}
isAutofillSelected={cellIsSelected(rowIndex, col.colIndex, autofillSelectedCells)}
isFocusedColumn={col.uniqueName == focusedColumn}
isModifiedCell={rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)}
isModifiedRow={rowStatus.status == 'updated'}
isInserted={rowStatus.status == 'inserted' ||
(rowStatus.insertedFields && rowStatus.insertedFields.has(col.uniqueName))}
isDeleted={rowStatus.status == 'deleted' ||
(rowStatus.deletedFields && rowStatus.deletedFields.has(col.uniqueName))}
/>
{/if}
{/each}
</tr>

View File

@ -0,0 +1,80 @@
<script lang="ts">
import keycodes from '../utility/keycodes';
import { onMount } from 'svelte';
export let inplaceEditorState;
export let dispatchInsplaceEditor;
export let onSetValue;
export let width;
export let cellValue;
let domEditor;
const widthCopy = width;
const isChangedRef = { current: !!inplaceEditorState.text };
function handleKeyDown(event) {
switch (event.keyCode) {
case keycodes.escape:
isChangedRef.current = false;
dispatchInsplaceEditor({ type: 'close' });
break;
case keycodes.enter:
if (isChangedRef.current) {
// grider.setCellValue(rowIndex, uniqueName, editor.value);
onSetValue(domEditor.value);
isChangedRef.current = false;
}
domEditor.blur();
dispatchInsplaceEditor({ type: 'close', mode: 'enter' });
break;
case keycodes.s:
if (event.ctrlKey) {
if (isChangedRef.current) {
onSetValue(domEditor.value);
// grider.setCellValue(rowIndex, uniqueName, editor.value);
isChangedRef.current = false;
}
event.preventDefault();
dispatchInsplaceEditor({ type: 'close', mode: 'save' });
}
break;
}
}
function handleBlur() {
if (isChangedRef.current) {
onSetValue(domEditor.value);
// grider.setCellValue(rowIndex, uniqueName, editor.value);
isChangedRef.current = false;
}
dispatchInsplaceEditor({ type: 'close' });
}
onMount(() => {
domEditor.value = inplaceEditorState.text || cellValue;
domEditor.focus();
if (inplaceEditorState.selectAll) {
domEditor.select();
}
});
</script>
<input
type="text"
on:change={() => (isChangedRef.current = true)}
on:keydown={handleKeyDown}
on:blur={handleBlur}
bind:this={domEditor}
style={`width:${widthCopy}px;min-width:${widthCopy}px;max-width:${widthCopy}px`}
/>
<style>
input {
border: 0px solid;
outline: none;
margin: 0px;
padding: 0px;
}
</style>

View File

@ -44,6 +44,12 @@
</script>
<script lang="ts">
import { changeSetToSql, createChangeSet } from 'dbgate-datalib';
import { scriptToSql } from 'dbgate-sqltree';
import ConfirmSqlModal from '../modals/ConfirmSqlModal.svelte';
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
import { showModal } from '../modals/modalTools';
import axios from '../utility/axios';
import ChangeSetGrider from './ChangeSetGrider';
@ -55,11 +61,47 @@
export let schemaName;
export let pureName;
export let config;
export let changeSetState;
export let dispatchChangeSet;
let loadedRows = [];
// $: console.log('loadedRows BIND', loadedRows);
$: grider = new ChangeSetGrider(loadedRows, null, null, display);
$: grider = new ChangeSetGrider(loadedRows, changeSetState, dispatchChangeSet, display);
// $: console.log('GRIDER', grider);
async function handleConfirmSql(sql) {
const resp = await axios.request({
url: 'database-connections/query-data',
method: 'post',
params: {
conid,
database,
},
data: { sql },
});
const { errorMessage } = resp.data || {};
if (errorMessage) {
showModal(ErrorMessageModal, { title: 'Error when saving', message: errorMessage });
} else {
dispatchChangeSet({ type: 'reset', value: createChangeSet() });
display.reload();
}
}
function handleSave() {
const script = changeSetToSql(changeSetState && changeSetState.value, display.dbinfo);
const sql = scriptToSql(display.driver, script);
showModal(ConfirmSqlModal, { sql, onConfirm: () => handleConfirmSql(sql) });
}
</script>
<LoadingDataGridCore {...$$props} {loadDataPage} {dataPageAvailable} {loadRowCount} bind:loadedRows {grider} />
<LoadingDataGridCore
{...$$props}
{loadDataPage}
{dataPageAvailable}
{loadRowCount}
bind:loadedRows
{grider}
onSave={handleSave}
/>

View File

@ -0,0 +1,38 @@
<script>
import FormStyledButton from '../elements/FormStyledButton.svelte';
import FormProvider from '../forms/FormProvider.svelte';
import FormSubmit from '../forms/FormSubmit.svelte';
import ModalBase from './ModalBase.svelte';
import { closeCurrentModal } from './modalTools';
export let sql;
export let onConfirm;
</script>
<FormProvider>
<ModalBase {...$$restProps}>
<div slot="header">Save changes</div>
<textarea class="editor" value={sql} />
<div slot="footer">
<FormSubmit
value="OK"
on:click={() => {
closeCurrentModal();
onConfirm();
}}
/>
<FormStyledButton type="button" value="Close" onClick={closeCurrentModal} />
</div>
</ModalBase>
</FormProvider>
<style>
.editor {
position: relative;
height: 30vh;
width: 40vw;
}
</style>

View File

@ -0,0 +1,41 @@
<script>
import FormStyledButton from '../elements/FormStyledButton.svelte';
import FormProvider from '../forms/FormProvider.svelte';
import FormSubmit from '../forms/FormSubmit.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import ModalBase from './ModalBase.svelte';
import { closeCurrentModal } from './modalTools';
export let title = 'Error';
export let message;
</script>
<FormProvider>
<ModalBase {...$$restProps}>
<div slot="header">{title}</div>
<div class="wrapper">
<div class="icon">
<FontIcon icon="img error" />
</div>
{message}
</div>
<div slot="footer">
<FormStyledButton type="button" value="Close" onClick={closeCurrentModal} />
</div>
</ModalBase>
</FormProvider>
<style>
.wrapper {
display: flex;
align-items: center;
}
.icon {
margin-right: 10px;
font-size: 20pt;
}
</style>

View File

@ -1,4 +1,4 @@
import { writable, derived } from 'svelte/store';
import { writable, derived, readable } from 'svelte/store';
import { ExtensionsDirectory } from 'dbgate-types';
interface TabDefinition {
@ -37,6 +37,7 @@ export const visibleToolbar = writableWithStorage(1, 'visibleToolbar');
export const leftPanelWidth = writable(300);
export const currentDropDownMenu = writable(null);
export const openedModals = writable([]);
export const nullStore = readable(null, () => {});
subscribeCssVariable(selectedWidget, x => (x ? 1 : 0), '--dim-visible-left-panel');
subscribeCssVariable(visibleToolbar, x => (x ? 1 : 0), '--dim-visible-toolbar');

View File

@ -2,9 +2,16 @@
import App from '../App.svelte';
import TableDataGrid from '../datagrid/TableDataGrid.svelte';
import useGridConfig from '../utility/useGridConfig';
import { createGridCache, createGridConfig, TableFormViewDisplay, TableGridDisplay } from 'dbgate-datalib';
import {
createChangeSet,
createGridCache,
createGridConfig,
TableFormViewDisplay,
TableGridDisplay,
} from 'dbgate-datalib';
import { findEngineDriver } from 'dbgate-tools';
import { writable } from 'svelte/store';
import createUndoReducer from '../utility/createUndoReducer';
export let tabid;
export let conid;
@ -14,6 +21,17 @@
const config = useGridConfig(tabid);
const cache = writable(createGridCache());
const [changeSetStore, dispatchChangeSet] = createUndoReducer(createChangeSet());
</script>
<TableDataGrid {...$$props} config={$config} setConfig={config.update} cache={$cache} setCache={cache.update} />
<TableDataGrid
{...$$props}
config={$config}
setConfig={config.update}
cache={$cache}
setCache={cache.update}
changeSetState={$changeSetStore}
{changeSetStore}
{dispatchChangeSet}
/>

View File

@ -0,0 +1,11 @@
import { writable } from 'svelte/store';
export default function createReducer(reducer, initialState): any {
const state = writable(initialState);
function dispatch(action) {
state.update(x => reducer(x, action));
}
return [state, dispatch];
}

View File

@ -0,0 +1,69 @@
import _ from 'lodash';
import createReducer from './createReducer';
const reducer = options => (state, action) => {
const { mergeNearActions } = options || {};
const useMerge =
action.useMerge || (mergeNearActions && state.lastActionTm && new Date().getTime() - state.lastActionTm < 100);
switch (action.type) {
case 'set':
return {
history: [...state.history.slice(0, useMerge ? state.current : state.current + 1), action.value],
current: useMerge ? state.current : state.current + 1,
value: action.value,
canUndo: true,
canRedo: false,
lastActionTm: new Date().getTime(),
};
case 'compute': {
const newValue = action.compute(state.history[state.current]);
return {
history: [...state.history.slice(0, useMerge ? state.current : state.current + 1), newValue],
current: useMerge ? state.current : state.current + 1,
value: newValue,
canUndo: true,
canRedo: false,
lastActionTm: new Date().getTime(),
};
}
case 'undo':
if (state.current > 0)
return {
history: state.history,
current: state.current - 1,
value: state.history[state.current - 1],
canUndo: state.current > 1,
canRedo: true,
lastActionTm: null,
};
return state;
case 'redo':
if (state.current < state.history.length - 1)
return {
history: state.history,
current: state.current + 1,
value: state.history[state.current + 1],
canUndo: true,
canRedo: state.current < state.history.length - 2,
lastActionTm: null,
};
return state;
case 'reset':
return {
history: [action.value],
current: 0,
value: action.value,
lastActionTm: null,
};
}
};
export default function createUndoReducer(initialValue, options = null) {
return createReducer(reducer(options), {
history: [initialValue],
current: 0,
value: initialValue,
});
}