mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-18 03:16:01 +00:00
598 lines
17 KiB
Svelte
598 lines
17 KiB
Svelte
<script lang="ts" context="module">
|
|
const getCurrentDataForm = () => getActiveComponent('FormView');
|
|
|
|
// registerCommand({
|
|
// id: 'dataForm.save',
|
|
// group: 'save',
|
|
// category: 'Data form',
|
|
// name: 'Save',
|
|
// toolbar: true,
|
|
// icon: 'icon save',
|
|
// testEnabled: () => getCurrentDataForm()?.getFormer()?.allowSave,
|
|
// onClick: () => getCurrentDataForm().save(),
|
|
// });
|
|
|
|
registerCommand({
|
|
id: 'dataForm.refresh',
|
|
category: 'Data form',
|
|
name: 'Refresh',
|
|
keyText: 'F5',
|
|
toolbar: true,
|
|
isRelatedToTab: true,
|
|
icon: 'icon reload',
|
|
testEnabled: () => getCurrentDataForm() != null,
|
|
onClick: () => getCurrentDataForm().refresh(),
|
|
});
|
|
|
|
registerCommand({
|
|
id: 'dataForm.copyToClipboard',
|
|
category: 'Data form',
|
|
name: 'Copy to clipboard',
|
|
keyText: 'Ctrl+C',
|
|
disableHandleKeyText: 'Ctrl+C',
|
|
testEnabled: () => getCurrentDataForm() != null,
|
|
onClick: () => getCurrentDataForm().copyToClipboard(),
|
|
});
|
|
|
|
registerCommand({
|
|
id: 'dataForm.revertRowChanges',
|
|
category: 'Data form',
|
|
name: 'Revert row changes',
|
|
keyText: 'Ctrl+R',
|
|
testEnabled: () => getCurrentDataForm()?.getFormer()?.containsChanges,
|
|
onClick: () => getCurrentDataForm().getFormer().revertRowChanges(),
|
|
});
|
|
|
|
registerCommand({
|
|
id: 'dataForm.setNull',
|
|
category: 'Data form',
|
|
name: 'Set NULL',
|
|
keyText: 'Ctrl+0',
|
|
testEnabled: () => getCurrentDataForm() != null,
|
|
onClick: () => getCurrentDataForm().setNull(),
|
|
});
|
|
|
|
registerCommand({
|
|
id: 'dataForm.undo',
|
|
category: 'Data form',
|
|
name: 'Undo',
|
|
group: 'undo',
|
|
icon: 'icon undo',
|
|
toolbar: true,
|
|
isRelatedToTab: true,
|
|
testEnabled: () => getCurrentDataForm()?.getFormer()?.canUndo,
|
|
onClick: () => getCurrentDataForm().getFormer().undo(),
|
|
});
|
|
|
|
registerCommand({
|
|
id: 'dataForm.redo',
|
|
category: 'Data form',
|
|
name: 'Redo',
|
|
group: 'redo',
|
|
icon: 'icon redo',
|
|
toolbar: true,
|
|
isRelatedToTab: true,
|
|
testEnabled: () => getCurrentDataForm()?.getFormer()?.canRedo,
|
|
onClick: () => getCurrentDataForm().getFormer().redo(),
|
|
});
|
|
|
|
registerCommand({
|
|
id: 'dataForm.reconnect',
|
|
category: 'Data grid',
|
|
name: 'Reconnect',
|
|
testEnabled: () => getCurrentDataForm() != null,
|
|
onClick: () => getCurrentDataForm().reconnect(),
|
|
});
|
|
|
|
registerCommand({
|
|
id: 'dataForm.filterSelected',
|
|
category: 'Data form',
|
|
name: 'Filter this value',
|
|
keyText: 'Ctrl+F',
|
|
testEnabled: () => getCurrentDataForm() != null,
|
|
onClick: () => getCurrentDataForm().filterSelectedValue(),
|
|
});
|
|
|
|
registerCommand({
|
|
id: 'dataForm.addToFilter',
|
|
category: 'Data form',
|
|
name: 'Add to filter',
|
|
testEnabled: () => getCurrentDataForm() != null,
|
|
onClick: () => getCurrentDataForm().addToFilter(),
|
|
});
|
|
|
|
registerCommand({
|
|
id: 'dataForm.goToFirst',
|
|
category: 'Data form',
|
|
name: 'First',
|
|
keyText: 'Ctrl+Home',
|
|
toolbar: true,
|
|
isRelatedToTab: true,
|
|
icon: 'icon arrow-begin',
|
|
testEnabled: () => getCurrentDataForm() != null,
|
|
onClick: () => getCurrentDataForm().navigate('begin'),
|
|
});
|
|
|
|
registerCommand({
|
|
id: 'dataForm.goToPrevious',
|
|
category: 'Data form',
|
|
name: 'Previous',
|
|
keyText: 'Ctrl+ArrowUp',
|
|
toolbar: true,
|
|
isRelatedToTab: true,
|
|
icon: 'icon arrow-left',
|
|
testEnabled: () => getCurrentDataForm() != null,
|
|
onClick: () => getCurrentDataForm().navigate('previous'),
|
|
});
|
|
|
|
registerCommand({
|
|
id: 'dataForm.goToNext',
|
|
category: 'Data form',
|
|
name: 'Next',
|
|
keyText: 'Ctrl+ArrowDown',
|
|
toolbar: true,
|
|
isRelatedToTab: true,
|
|
icon: 'icon arrow-right',
|
|
testEnabled: () => getCurrentDataForm() != null,
|
|
onClick: () => getCurrentDataForm().navigate('next'),
|
|
});
|
|
|
|
registerCommand({
|
|
id: 'dataForm.goToLast',
|
|
category: 'Data form',
|
|
name: 'Last',
|
|
keyText: 'Ctrl+End',
|
|
toolbar: true,
|
|
isRelatedToTab: true,
|
|
icon: 'icon arrow-end',
|
|
testEnabled: () => getCurrentDataForm() != null,
|
|
onClick: () => getCurrentDataForm().navigate('end'),
|
|
});
|
|
|
|
function isDataCell(cell) {
|
|
return cell[1] % 2 == 1;
|
|
}
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
import _ from 'lodash';
|
|
|
|
import { getContext } from 'svelte';
|
|
|
|
import invalidateCommands from '../commands/invalidateCommands';
|
|
|
|
import registerCommand from '../commands/registerCommand';
|
|
import DataGridCell from '../datagrid/DataGridCell.svelte';
|
|
import { dataGridRowHeight } from '../datagrid/DataGridRowHeightMeter.svelte';
|
|
import InplaceEditor from '../datagrid/InplaceEditor.svelte';
|
|
import { cellFromEvent } from '../datagrid/selection';
|
|
import ColumnLabel from '../elements/ColumnLabel.svelte';
|
|
import LoadingInfo from '../elements/LoadingInfo.svelte';
|
|
import { plusExpandIcon } from '../icons/expandIcons';
|
|
import FontIcon from '../icons/FontIcon.svelte';
|
|
|
|
import axiosInstance from '../utility/axiosInstance';
|
|
import { copyTextToClipboard } from '../utility/clipboard';
|
|
import contextMenu, { getContextMenu, registerMenu } from '../utility/contextMenu';
|
|
import createActivator, { getActiveComponent } from '../utility/createActivator';
|
|
import createReducer from '../utility/createReducer';
|
|
import keycodes from '../utility/keycodes';
|
|
import resizeObserver from '../utility/resizeObserver';
|
|
import openReferenceForm from './openReferenceForm';
|
|
|
|
export let conid;
|
|
export let database;
|
|
export let config;
|
|
export let setConfig;
|
|
export let focusOnVisible = false;
|
|
export let allRowCount;
|
|
export let rowCountBefore;
|
|
export let isLoading;
|
|
export let former;
|
|
export let formDisplay;
|
|
export let onNavigate;
|
|
|
|
let wrapperHeight = 1;
|
|
$: rowHeight = $dataGridRowHeight;
|
|
let currentCell = [0, 0];
|
|
|
|
const tabVisible: any = getContext('tabVisible');
|
|
const domCells = {};
|
|
|
|
let domFocusField;
|
|
|
|
$: if ($tabVisible && domFocusField && focusOnVisible) {
|
|
domFocusField.focus();
|
|
}
|
|
|
|
$: rowData = former?.rowData;
|
|
$: rowStatus = former?.rowStatus;
|
|
|
|
$: rowCount = Math.floor((wrapperHeight - 22) / (rowHeight + 2));
|
|
|
|
$: columnChunks = _.chunk(formDisplay.columns, rowCount) as any[][];
|
|
|
|
$: rowCountInfo = getRowCountInfo(rowCountBefore, allRowCount);
|
|
|
|
function getRowCountInfo(rowCountBefore, allRowCount) {
|
|
if (rowData == null) return 'No data';
|
|
if (allRowCount == null || rowCountBefore == null) return 'Loading row count...';
|
|
return `Row: ${(rowCountBefore + 1).toLocaleString()} / ${allRowCount.toLocaleString()}`;
|
|
}
|
|
|
|
export function getFormer() {
|
|
return former;
|
|
}
|
|
|
|
// export function getFormDisplay() {
|
|
// return formDisplay;
|
|
// }
|
|
|
|
export function navigate(command) {
|
|
if (onNavigate) onNavigate(command);
|
|
}
|
|
|
|
// export function save() {
|
|
// if ($inplaceEditorState.cell) {
|
|
// // @ts-ignore
|
|
// dispatchInsplaceEditor({ type: 'shouldSave' });
|
|
// return;
|
|
// }
|
|
// if (onSave) onSave();
|
|
// }
|
|
|
|
export function setNull() {
|
|
if (isDataCell(currentCell)) {
|
|
setCellValue(currentCell, null);
|
|
}
|
|
}
|
|
|
|
export function copyToClipboard() {
|
|
const column = getCellColumn(currentCell);
|
|
if (!column) return;
|
|
const text = currentCell[1] % 2 == 1 ? rowData[column.uniqueName] : column.columnName;
|
|
copyTextToClipboard(text);
|
|
}
|
|
|
|
export async function reconnect() {
|
|
await axiosInstance.post('database-connections/refresh', { conid, database });
|
|
formDisplay.reload();
|
|
}
|
|
|
|
export async function refresh() {
|
|
formDisplay.reload();
|
|
}
|
|
|
|
export function filterSelectedValue() {
|
|
formDisplay.filterCellValue(getCellColumn(currentCell), rowData);
|
|
}
|
|
|
|
export function addToFilter() {
|
|
formDisplay.addFilterColumn(getCellColumn(currentCell));
|
|
}
|
|
|
|
export const activator = createActivator('FormView', false);
|
|
|
|
const handleTableMouseDown = event => {
|
|
if (event.target.closest('.buttonLike')) return;
|
|
if (event.target.closest('.resizeHandleControl')) return;
|
|
if (event.target.closest('input')) return;
|
|
|
|
event.preventDefault();
|
|
if (domFocusField) domFocusField.focus();
|
|
|
|
// event.target.closest('table').focus();
|
|
event.preventDefault();
|
|
const cell = cellFromEvent(event);
|
|
|
|
if (isDataCell(cell) && !_.isEqual(cell, $inplaceEditorState.cell) && _.isEqual(cell, currentCell)) {
|
|
// @ts-ignore
|
|
if (rowData) {
|
|
dispatchInsplaceEditor({ type: 'show', cell, selectAll: true });
|
|
}
|
|
} else if (!_.isEqual(cell, $inplaceEditorState.cell)) {
|
|
// @ts-ignore
|
|
dispatchInsplaceEditor({ type: 'close' });
|
|
}
|
|
|
|
// @ts-ignore
|
|
currentCell = cell;
|
|
};
|
|
|
|
function getCellColumn(cell) {
|
|
const chunk = columnChunks[Math.floor(cell[1] / 2)];
|
|
if (!chunk) return;
|
|
const column = chunk[cell[0]];
|
|
return column;
|
|
}
|
|
|
|
function setCellValue(cell, value) {
|
|
const column = getCellColumn(cell);
|
|
if (!column) return;
|
|
former.setCellValue(column.uniqueName, value);
|
|
}
|
|
|
|
const getCellWidth = (row, col) => {
|
|
const element = domCells[`${row},${col}`];
|
|
if (element) return element.getBoundingClientRect().width;
|
|
return 100;
|
|
};
|
|
|
|
const [inplaceEditorState, dispatchInsplaceEditor] = createReducer((state, action) => {
|
|
switch (action.type) {
|
|
case 'show': {
|
|
const column = getCellColumn(action.cell);
|
|
if (!column) return state;
|
|
if (column.uniquePath.length > 1) return state;
|
|
|
|
// 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 {};
|
|
}, {});
|
|
registerMenu(
|
|
{ placeTag: 'switch' },
|
|
{ command: 'dataForm.copyToClipboard' },
|
|
{ divider: true },
|
|
{ command: 'dataForm.filterSelected' },
|
|
{ command: 'dataForm.addToFilter' },
|
|
{ divider: true },
|
|
{ placeTag: 'save' },
|
|
{ command: 'dataForm.revertRowChanges' },
|
|
{ command: 'dataForm.setNull' },
|
|
{ divider: true },
|
|
{ command: 'dataForm.undo' },
|
|
{ command: 'dataForm.redo' },
|
|
{ divider: true },
|
|
{ command: 'dataForm.goToFirst' },
|
|
{ command: 'dataForm.goToPrevious' },
|
|
{ command: 'dataForm.goToNext' },
|
|
{ command: 'dataForm.goToLast' }
|
|
);
|
|
const menu = getContextMenu();
|
|
|
|
function handleKeyDown(event) {
|
|
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 });
|
|
}
|
|
|
|
handleCursorMove(event);
|
|
}
|
|
|
|
const scrollIntoView = cell => {
|
|
const element = domCells[`${cell[0]},${cell[1]}`];
|
|
if (element) element.scrollIntoView();
|
|
};
|
|
|
|
const moveCurrentCell = (row, col) => {
|
|
if (row < 0) row = 0;
|
|
if (col < 0) col = 0;
|
|
if (col >= columnChunks.length * 2) col = columnChunks.length * 2 - 1;
|
|
const chunk = columnChunks[Math.floor(col / 2)];
|
|
if (chunk && row >= chunk.length) row = chunk.length - 1;
|
|
currentCell = [row, col];
|
|
scrollIntoView(currentCell);
|
|
};
|
|
|
|
const handleCursorMove = event => {
|
|
if (event.ctrlKey) {
|
|
switch (event.keyCode) {
|
|
case keycodes.leftArrow:
|
|
return moveCurrentCell(currentCell[0], 0);
|
|
case keycodes.rightArrow:
|
|
return moveCurrentCell(currentCell[0], columnChunks.length * 2 - 1);
|
|
}
|
|
}
|
|
switch (event.keyCode) {
|
|
case keycodes.leftArrow:
|
|
return moveCurrentCell(currentCell[0], currentCell[1] - 1);
|
|
case keycodes.rightArrow:
|
|
return moveCurrentCell(currentCell[0], currentCell[1] + 1);
|
|
case keycodes.upArrow:
|
|
return moveCurrentCell(currentCell[0] - 1, currentCell[1]);
|
|
case keycodes.downArrow:
|
|
return moveCurrentCell(currentCell[0] + 1, currentCell[1]);
|
|
case keycodes.pageUp:
|
|
return moveCurrentCell(0, currentCell[1]);
|
|
case keycodes.pageDown:
|
|
return moveCurrentCell(rowCount - 1, currentCell[1]);
|
|
case keycodes.home:
|
|
return moveCurrentCell(0, 0);
|
|
case keycodes.end:
|
|
return moveCurrentCell(rowCount - 1, columnChunks.length * 2 - 1);
|
|
}
|
|
};
|
|
|
|
function handleSetFormView(rowData, column) {
|
|
openReferenceForm(rowData, column, conid, database);
|
|
}
|
|
</script>
|
|
|
|
<div class="outer">
|
|
<div class="wrapper" use:contextMenu={menu} bind:clientHeight={wrapperHeight}>
|
|
{#each columnChunks as chunk, chunkIndex}
|
|
<table on:mousedown={handleTableMouseDown}>
|
|
{#each chunk as col, rowIndex}
|
|
<tr>
|
|
<td
|
|
class="header-cell"
|
|
data-row={rowIndex}
|
|
data-col={chunkIndex * 2}
|
|
style={rowHeight > 1 ? `height: ${rowHeight}px` : undefined}
|
|
class:isSelected={currentCell[0] == rowIndex && currentCell[1] == chunkIndex * 2}
|
|
bind:this={domCells[`${rowIndex},${chunkIndex * 2}`]}
|
|
>
|
|
<div class="header-cell-inner">
|
|
{#if col.foreignKey}
|
|
<FontIcon
|
|
icon={plusExpandIcon(formDisplay.isExpandedColumn(col.uniqueName))}
|
|
on:click={e => {
|
|
e.stopPropagation();
|
|
formDisplay.toggleExpandedColumn(col.uniqueName);
|
|
}}
|
|
/>
|
|
{:else}
|
|
<FontIcon icon="icon invisible-box" />
|
|
{/if}
|
|
<span style={`margin-left: ${(col.uniquePath.length - 1) * 20}px`} />
|
|
<ColumnLabel
|
|
{...col}
|
|
headerText={col.columnName}
|
|
extInfo={col.foreignKey ? ` -> ${col.foreignKey.refTableName}` : null}
|
|
/>
|
|
</div>
|
|
</td>
|
|
<DataGridCell
|
|
{rowIndex}
|
|
{col}
|
|
{rowData}
|
|
colIndex={chunkIndex * 2 + 1}
|
|
isSelected={currentCell[0] == rowIndex && currentCell[1] == chunkIndex * 2 + 1}
|
|
isModifiedCell={rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)}
|
|
allowHintField={!(rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName))}
|
|
bind:domCell={domCells[`${rowIndex},${chunkIndex * 2 + 1}`]}
|
|
onSetFormView={handleSetFormView}
|
|
hideContent={!rowData ||
|
|
($inplaceEditorState.cell &&
|
|
rowIndex == $inplaceEditorState.cell[0] &&
|
|
chunkIndex * 2 + 1 == $inplaceEditorState.cell[1])}
|
|
>
|
|
{#if $inplaceEditorState.cell && rowIndex == $inplaceEditorState.cell[0] && chunkIndex * 2 + 1 == $inplaceEditorState.cell[1]}
|
|
<InplaceEditor
|
|
width={getCellWidth(rowIndex, chunkIndex * 2 + 1)}
|
|
inplaceEditorState={$inplaceEditorState}
|
|
{dispatchInsplaceEditor}
|
|
cellValue={rowData[col.uniqueName]}
|
|
onSetValue={value => {
|
|
former.setCellValue(col.uniqueName, value);
|
|
}}
|
|
/>
|
|
{/if}
|
|
</DataGridCell>
|
|
</tr>
|
|
{/each}
|
|
</table>
|
|
{/each}
|
|
<input
|
|
type="text"
|
|
class="focus-field"
|
|
bind:this={domFocusField}
|
|
on:focus={() => {
|
|
activator.activate();
|
|
invalidateCommands();
|
|
}}
|
|
on:keydown={handleKeyDown}
|
|
on:copy={copyToClipboard}
|
|
/>
|
|
</div>
|
|
{#if rowCountInfo}
|
|
<div class="row-count-label">
|
|
{rowCountInfo}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if isLoading}
|
|
<LoadingInfo wrapper message="Loading data" />
|
|
{/if}
|
|
|
|
<style>
|
|
table {
|
|
border-collapse: collapse;
|
|
outline: none;
|
|
}
|
|
|
|
.outer {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
right: 0;
|
|
}
|
|
|
|
.wrapper {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
right: 0;
|
|
display: flex;
|
|
overflow-x: scroll;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
tr {
|
|
background-color: var(--theme-bg-0);
|
|
}
|
|
tr:nth-child(6n + 3) {
|
|
background-color: var(--theme-bg-1);
|
|
}
|
|
tr:nth-child(6n + 6) {
|
|
background-color: var(--theme-bg-alt);
|
|
}
|
|
|
|
.header-cell {
|
|
border: 1px solid var(--theme-border);
|
|
text-align: left;
|
|
padding: 0;
|
|
margin: 0;
|
|
background-color: var(--theme-bg-1);
|
|
overflow: hidden;
|
|
}
|
|
.header-cell.isSelected {
|
|
background: var(--theme-bg-selected);
|
|
}
|
|
|
|
.header-cell-inner {
|
|
display: flex;
|
|
}
|
|
|
|
.focus-field {
|
|
position: absolute;
|
|
left: -1000px;
|
|
top: -1000px;
|
|
}
|
|
|
|
.row-count-label {
|
|
position: absolute;
|
|
background-color: var(--theme-bg-2);
|
|
right: 40px;
|
|
bottom: 20px;
|
|
}
|
|
</style>
|