mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-17 22:36:01 +00:00
form view
This commit is contained in:
@@ -32,6 +32,7 @@ body {
|
||||
visibility: hidden;
|
||||
}
|
||||
.space-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.flex {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import HorizontalSplitter from '../elements/HorizontalSplitter.svelte';
|
||||
import FormViewFilters from '../formview/FormViewFilters.svelte';
|
||||
import WidgetColumnBar from '../widgets/WidgetColumnBar.svelte';
|
||||
import WidgetColumnBarItem from '../widgets/WidgetColumnBarItem.svelte';
|
||||
import ColumnManager from './ColumnManager.svelte';
|
||||
@@ -21,10 +22,14 @@
|
||||
<HorizontalSplitter initialValue="300px" bind:size={managerSize}>
|
||||
<div class="left" slot="1">
|
||||
<WidgetColumnBar>
|
||||
<WidgetColumnBarItem title="Columns" name="columns" height={showReferences ? '40%' : '60%'}>
|
||||
<WidgetColumnBarItem title="Columns" name="columns" height={showReferences ? '40%' : '60%'} skip={isFormView}>
|
||||
<ColumnManager {...$$props} {managerSize} />
|
||||
</WidgetColumnBarItem>
|
||||
|
||||
<WidgetColumnBarItem title="Filters" name="filters" height="30%" skip={!isFormView}>
|
||||
<FormViewFilters {...$$props} {managerSize} />
|
||||
</WidgetColumnBarItem>
|
||||
|
||||
<WidgetColumnBarItem
|
||||
title="References"
|
||||
name="references"
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import { isTypeLogical } from 'dbgate-tools';
|
||||
import ShowFormButton from '../formview/ShowFormButton.svelte';
|
||||
|
||||
export let rowIndex;
|
||||
export let col;
|
||||
@@ -36,6 +37,7 @@
|
||||
export let isFocusedColumn = false;
|
||||
export let domCell = undefined;
|
||||
export let hideContent = false;
|
||||
export let onSetFormView;
|
||||
|
||||
$: value = (rowData || {})[col.uniqueName];
|
||||
</script>
|
||||
@@ -94,6 +96,10 @@
|
||||
{#if hintFieldsAllowed && hintFieldsAllowed.includes(col.uniqueName) && rowData}
|
||||
<span class="hint">{rowData[col.hintColumnName]}</span>
|
||||
{/if}
|
||||
|
||||
{#if col.foreignKey && rowData[col.uniqueName]}
|
||||
<ShowFormButton on:click={() => onSetFormView(rowData, col)} />
|
||||
{/if}
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
import invalidateCommands from '../commands/invalidateCommands';
|
||||
import createRef from '../utility/createRef';
|
||||
import { clearLastFocusedFormView } from '../formview/FormView.svelte';
|
||||
import openReferenceForm, { openPrimaryKeyForm } from '../formview/openReferenceForm';
|
||||
|
||||
export let onLoadNextData = undefined;
|
||||
export let grider = undefined;
|
||||
@@ -844,6 +845,14 @@
|
||||
return cells.filter(isRegularCell);
|
||||
}
|
||||
|
||||
function handleSetFormView(rowData, column) {
|
||||
if (column) {
|
||||
openReferenceForm(rowData, column, conid, database);
|
||||
} else {
|
||||
openPrimaryKeyForm(rowData, display.baseTable, conid, database);
|
||||
}
|
||||
}
|
||||
|
||||
const [inplaceEditorState, dispatchInsplaceEditor] = createReducer((state, action) => {
|
||||
switch (action.type) {
|
||||
case 'show':
|
||||
@@ -1000,6 +1009,7 @@
|
||||
inplaceEditorState={$inplaceEditorState}
|
||||
{dispatchInsplaceEditor}
|
||||
{frameSelection}
|
||||
onSetFormView={formViewAvailable && display?.baseTable?.primaryKey ? handleSetFormView : null}
|
||||
/>
|
||||
{/each}
|
||||
</tbody>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import openReferenceForm from '../formview/openReferenceForm';
|
||||
|
||||
import DataGridCell from './DataGridCell.svelte';
|
||||
import { cellIsSelected } from './gridutil';
|
||||
import InplaceEditor from './InplaceEditor.svelte';
|
||||
@@ -16,6 +18,7 @@
|
||||
export let focusedColumn = undefined;
|
||||
export let inplaceEditorState;
|
||||
export let dispatchInsplaceEditor;
|
||||
export let onSetFormView;
|
||||
|
||||
$: rowData = grider.getRowData(rowIndex);
|
||||
$: rowStatus = grider.getRowStatus(rowIndex);
|
||||
@@ -30,7 +33,7 @@
|
||||
</script>
|
||||
|
||||
<tr style={`height: ${rowHeight}px`}>
|
||||
<RowHeaderCell {rowIndex} />
|
||||
<RowHeaderCell {rowIndex} onShowForm={onSetFormView ? () => onSetFormView(rowData, null) : null} />
|
||||
{#each visibleRealColumns as col (col.uniqueName)}
|
||||
{#if inplaceEditorState.cell && rowIndex == inplaceEditorState.cell[0] && col.colIndex == inplaceEditorState.cell[1]}
|
||||
<td>
|
||||
@@ -58,6 +61,7 @@
|
||||
(rowStatus.insertedFields && rowStatus.insertedFields.has(col.uniqueName))}
|
||||
isDeleted={rowStatus.status == 'deleted' ||
|
||||
(rowStatus.deletedFields && rowStatus.deletedFields.has(col.uniqueName))}
|
||||
{onSetFormView}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
<script lang="ts">
|
||||
import ShowFormButton from '../formview/ShowFormButton.svelte';
|
||||
|
||||
export let rowIndex;
|
||||
export let onShowForm;
|
||||
|
||||
let mouseIn = false;
|
||||
</script>
|
||||
|
||||
<td data-row={rowIndex} data-col="header">
|
||||
<td
|
||||
data-row={rowIndex}
|
||||
data-col="header"
|
||||
on:mouseenter={() => (mouseIn = true)}
|
||||
on:mouseleave={() => (mouseIn = false)}
|
||||
>
|
||||
{rowIndex + 1}
|
||||
|
||||
{#if mouseIn && onShowForm}
|
||||
<ShowFormButton on:click={onShowForm} />
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -100,6 +100,50 @@
|
||||
onClick: () => getCurrentDataForm().addToFilter(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataForm.goToFirst',
|
||||
category: 'Data form',
|
||||
name: 'First',
|
||||
keyText: 'Ctrl+Home',
|
||||
toolbar: 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,
|
||||
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,
|
||||
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,
|
||||
icon: 'icon arrow-end',
|
||||
testEnabled: () => getCurrentDataForm() != null,
|
||||
onClick: () => getCurrentDataForm().navigate('end'),
|
||||
});
|
||||
|
||||
function isDataCell(cell) {
|
||||
return cell[1] % 2 == 1;
|
||||
}
|
||||
@@ -130,6 +174,7 @@
|
||||
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;
|
||||
@@ -142,6 +187,7 @@
|
||||
export let former;
|
||||
export let formDisplay;
|
||||
export let onSave;
|
||||
export let onNavigate;
|
||||
|
||||
let wrapperHeight = 1;
|
||||
let rowHeight = 1;
|
||||
@@ -161,10 +207,18 @@
|
||||
$: rowData = former?.rowData;
|
||||
$: rowStatus = former?.rowStatus;
|
||||
|
||||
$: rowCount = Math.floor((wrapperHeight - 20) / rowHeight);
|
||||
$: 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 getTabId() {
|
||||
return tabid;
|
||||
}
|
||||
@@ -173,6 +227,10 @@
|
||||
return former;
|
||||
}
|
||||
|
||||
export function navigate(command) {
|
||||
if (onNavigate) onNavigate(command);
|
||||
}
|
||||
|
||||
export function switchToTable() {
|
||||
setConfig(cfg => ({
|
||||
...cfg,
|
||||
@@ -293,7 +351,24 @@
|
||||
return {};
|
||||
}, {});
|
||||
function createMenu() {
|
||||
return [{ command: 'dataForm.switchToTable' }];
|
||||
return [
|
||||
{ command: 'dataForm.switchToTable' },
|
||||
{ divider: true },
|
||||
{ command: 'dataForm.filterSelected' },
|
||||
{ command: 'dataForm.addToFilter' },
|
||||
{ divider: true },
|
||||
{ command: 'dataForm.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' },
|
||||
];
|
||||
}
|
||||
|
||||
function handleKeyDown(event) {
|
||||
@@ -363,6 +438,10 @@
|
||||
return moveCurrentCell(rowCount - 1, columnChunks.length * 2 - 1);
|
||||
}
|
||||
};
|
||||
|
||||
function handleSetFormView(rowData, column) {
|
||||
openReferenceForm(rowData, column, conid, database);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isLoading}
|
||||
@@ -400,7 +479,11 @@
|
||||
<FontIcon icon="icon invisible-box" />
|
||||
{/if}
|
||||
<span style={`margin-left: ${(col.uniquePath.length - 1) * 20}px`} />
|
||||
<ColumnLabel {...col} headerText={col.columnName} />
|
||||
<ColumnLabel
|
||||
{...col}
|
||||
headerText={col.columnName}
|
||||
extInfo={col.foreignKey ? ` -> ${col.foreignKey.refTableName}` : null}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<DataGridCell
|
||||
@@ -411,9 +494,11 @@
|
||||
isSelected={currentCell[0] == rowIndex && currentCell[1] == chunkIndex * 2 + 1}
|
||||
isModifiedCell={rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)}
|
||||
bind:domCell={domCells[`${rowIndex},${chunkIndex * 2 + 1}`]}
|
||||
hideContent={$inplaceEditorState.cell &&
|
||||
rowIndex == $inplaceEditorState.cell[0] &&
|
||||
chunkIndex * 2 + 1 == $inplaceEditorState.cell[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
|
||||
@@ -443,6 +528,11 @@
|
||||
on:keydown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
{#if rowCountInfo}
|
||||
<div class="row-count-label">
|
||||
{rowCountInfo}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -501,4 +591,11 @@
|
||||
left: -1000px;
|
||||
top: -1000px;
|
||||
}
|
||||
|
||||
.row-count-label {
|
||||
position: absolute;
|
||||
background-color: var(--theme-bg-2);
|
||||
right: 40px;
|
||||
bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
34
packages/web/src/formview/FormViewFilterColumn.svelte
Normal file
34
packages/web/src/formview/FormViewFilterColumn.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { getFilterType } from 'dbgate-filterparser';
|
||||
|
||||
import DataFilterControl from '../datagrid/DataFilterControl.svelte';
|
||||
|
||||
import ColumnLabel from '../elements/ColumnLabel.svelte';
|
||||
import InlineButton from '../elements/InlineButton.svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
|
||||
export let column;
|
||||
export let formDisplay;
|
||||
export let filters;
|
||||
</script>
|
||||
|
||||
{#if column}
|
||||
<div class="m-1">
|
||||
<div class="space-between">
|
||||
<ColumnLabel {...column} />
|
||||
<InlineButton
|
||||
square
|
||||
on:click={() => {
|
||||
formDisplay.removeFilter(column.uniqueName);
|
||||
}}
|
||||
>
|
||||
<FontIcon icon="icon delete" />
|
||||
</InlineButton>
|
||||
</div>
|
||||
<DataFilterControl
|
||||
filterType={getFilterType(column.dataType)}
|
||||
filter={filters[column.uniqueName]}
|
||||
setFilter={value => formDisplay.setFilter(column.uniqueName, value)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
32
packages/web/src/formview/FormViewFilters.svelte
Normal file
32
packages/web/src/formview/FormViewFilters.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
|
||||
import ManagerInnerContainer from '../elements/ManagerInnerContainer.svelte';
|
||||
import FormViewFilterColumn from './FormViewFilterColumn.svelte';
|
||||
import PrimaryKeyFilterEditor from './PrimaryKeyFilterEditor.svelte';
|
||||
|
||||
export let managerSize;
|
||||
export let formDisplay;
|
||||
|
||||
$: baseTable = formDisplay?.baseTable;
|
||||
$: formFilterColumns = formDisplay?.config?.formFilterColumns;
|
||||
$: filters = formDisplay?.config?.filters;
|
||||
|
||||
$: allFilterNames = _.union(_.keys(filters || {}), formFilterColumns || []);
|
||||
</script>
|
||||
|
||||
{#if baseTable?.primaryKey}
|
||||
<ManagerInnerContainer width={managerSize}>
|
||||
{#each baseTable.primaryKey.columns as col}
|
||||
<PrimaryKeyFilterEditor {baseTable} column={col} {formDisplay} />
|
||||
{/each}
|
||||
|
||||
{#each allFilterNames as uniqueName}
|
||||
<FormViewFilterColumn
|
||||
column={formDisplay.columns.find(x => x.uniqueName == uniqueName)}
|
||||
{formDisplay}
|
||||
{filters}
|
||||
/>
|
||||
{/each}
|
||||
</ManagerInnerContainer>
|
||||
{/if}
|
||||
51
packages/web/src/formview/PrimaryKeyFilterEditor.svelte
Normal file
51
packages/web/src/formview/PrimaryKeyFilterEditor.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import ColumnLabel from '../elements/ColumnLabel.svelte';
|
||||
import InlineButton from '../elements/InlineButton.svelte';
|
||||
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import keycodes from '../utility/keycodes';
|
||||
|
||||
export let column;
|
||||
export let baseTable;
|
||||
export let formDisplay;
|
||||
|
||||
let domEditor;
|
||||
$: value = formDisplay.getKeyValue(column.columnName);
|
||||
|
||||
const applyFilter = () => {
|
||||
formDisplay.requestKeyValue(column.columnName, domEditor.value);
|
||||
};
|
||||
|
||||
const cancelFilter = () => {
|
||||
formDisplay.cancelRequestKey();
|
||||
formDisplay.reload();
|
||||
};
|
||||
|
||||
const handleKeyDown = ev => {
|
||||
if (ev.keyCode == keycodes.enter) {
|
||||
applyFilter();
|
||||
}
|
||||
if (ev.keyCode == keycodes.escape) {
|
||||
cancelFilter();
|
||||
}
|
||||
};
|
||||
|
||||
$: if (domEditor) domEditor.value = value;
|
||||
</script>
|
||||
|
||||
<div class="m-1">
|
||||
<div class="space-between">
|
||||
<div>
|
||||
<FontIcon icon="img primary-key" />
|
||||
<ColumnLabel {...baseTable.columns.find(x => x.columnName == column.columnName)} />
|
||||
</div>
|
||||
{#if formDisplay.config.formViewKeyRequested}
|
||||
<InlineButton square on:click={cancelFilter}>
|
||||
<FontIcon icon="icon delete" />
|
||||
</InlineButton>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex">
|
||||
<input bind:this={domEditor} type="text" on:blur={applyFilter} on:keydown={handleKeyDown} />
|
||||
</div>
|
||||
</div>
|
||||
25
packages/web/src/formview/ShowFormButton.svelte
Normal file
25
packages/web/src/formview/ShowFormButton.svelte
Normal file
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
</script>
|
||||
|
||||
<div on:click|stopPropagation>
|
||||
<FontIcon icon="icon form" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 1px;
|
||||
color: var(--theme-font-3);
|
||||
background-color: var(--theme-bg-1);
|
||||
border: 1px solid var(--theme-bg-1);
|
||||
}
|
||||
|
||||
div:hover {
|
||||
color: var(--theme-font-hover);
|
||||
border: var(--theme-border);
|
||||
top: 1px;
|
||||
right: 0px;
|
||||
}
|
||||
</style>
|
||||
@@ -38,6 +38,7 @@
|
||||
export let masterLoadedTime;
|
||||
export let conid;
|
||||
export let database;
|
||||
export let onReferenceSourceChanged;
|
||||
|
||||
let isLoadingData = false;
|
||||
let isLoadedData = false;
|
||||
@@ -128,6 +129,8 @@
|
||||
|
||||
$: former = new ChangeSetFormer(rowData, changeSetState, dispatchChangeSet, formDisplay);
|
||||
|
||||
$: if (onReferenceSourceChanged && rowData) onReferenceSourceChanged([rowData], loadedTime);
|
||||
|
||||
async function handleConfirmSql(sql) {
|
||||
const resp = await axiosInstance.request({
|
||||
url: 'database-connections/query-data',
|
||||
@@ -158,4 +161,12 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormView {...$$props} {former} isLoading={isLoadingData} {allRowCount} {rowCountBefore} onSave={handleSave} />
|
||||
<FormView
|
||||
{...$$props}
|
||||
{former}
|
||||
isLoading={isLoadingData}
|
||||
{allRowCount}
|
||||
{rowCountBefore}
|
||||
onSave={handleSave}
|
||||
onNavigate={handleNavigate}
|
||||
/>
|
||||
|
||||
@@ -29,3 +29,32 @@ export default function openReferenceForm(rowData, column, conid, database) {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function openPrimaryKeyForm(rowData, baseTable, conid, database) {
|
||||
const formViewKey = _.fromPairs(
|
||||
baseTable.primaryKey.columns.map(({ columnName }) => [columnName, rowData[columnName]])
|
||||
);
|
||||
openNewTab(
|
||||
{
|
||||
title: baseTable.pureName,
|
||||
icon: 'img table',
|
||||
tabComponent: 'TableDataTab',
|
||||
props: {
|
||||
schemaName: baseTable.schemaName,
|
||||
pureName: baseTable.pureName,
|
||||
conid,
|
||||
database,
|
||||
objectTypeField: 'tables',
|
||||
},
|
||||
},
|
||||
{
|
||||
grid: {
|
||||
isFormView: true,
|
||||
formViewKey,
|
||||
},
|
||||
},
|
||||
{
|
||||
forceNewTab: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user