mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-27 16:06:23 +00:00
Merge branch 'formview'
This commit is contained in:
@@ -28,6 +28,7 @@ module.exports = {
|
|||||||
handle_status(conid, database, { status }) {
|
handle_status(conid, database, { status }) {
|
||||||
const existing = this.opened.find((x) => x.conid == conid && x.database == database);
|
const existing = this.opened.find((x) => x.conid == conid && x.database == database);
|
||||||
if (!existing) return;
|
if (!existing) return;
|
||||||
|
if (existing.status == status) return;
|
||||||
existing.status = status;
|
existing.status = status;
|
||||||
socket.emitChanged(`database-status-changed-${conid}-${database}`);
|
socket.emitChanged(`database-status-changed-${conid}-${database}`);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ async function handleIncrementalRefresh() {
|
|||||||
analysedStructure = newStructure;
|
analysedStructure = newStructure;
|
||||||
process.send({ msgtype: 'structure', structure: analysedStructure });
|
process.send({ msgtype: 'structure', structure: analysedStructure });
|
||||||
}
|
}
|
||||||
|
setStatusName('ok');
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStatus(status) {
|
function setStatus(status) {
|
||||||
|
|||||||
25
packages/datalib/src/FormViewDisplay.ts
Normal file
25
packages/datalib/src/FormViewDisplay.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import { GridConfig, GridCache, GridConfigColumns, createGridCache, GroupFunc } from './GridConfig';
|
||||||
|
import { ForeignKeyInfo, TableInfo, ColumnInfo, EngineDriver, NamedObjectInfo, DatabaseInfo } from 'dbgate-types';
|
||||||
|
import { parseFilter, getFilterType } from 'dbgate-filterparser';
|
||||||
|
import { filterName } from './filterName';
|
||||||
|
import { ChangeSetFieldDefinition, ChangeSetRowDefinition } from './ChangeSet';
|
||||||
|
import { Expression, Select, treeToSql, dumpSqlSelect, Condition } from 'dbgate-sqltree';
|
||||||
|
import { isTypeLogical } from 'dbgate-tools';
|
||||||
|
import { ChangeCacheFunc, ChangeConfigFunc, DisplayColumn } from './GridDisplay';
|
||||||
|
|
||||||
|
export class FormViewDisplay {
|
||||||
|
isLoadedCorrectly = true;
|
||||||
|
columns: DisplayColumn[];
|
||||||
|
public baseTable: TableInfo;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public config: GridConfig,
|
||||||
|
protected setConfig: ChangeConfigFunc,
|
||||||
|
public cache: GridCache,
|
||||||
|
protected setCache: ChangeCacheFunc,
|
||||||
|
public driver?: EngineDriver,
|
||||||
|
public dbinfo: DatabaseInfo = null
|
||||||
|
) {}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -29,6 +29,8 @@ export interface GridConfig extends GridConfigColumns {
|
|||||||
grouping: { [uniqueName: string]: GroupFunc };
|
grouping: { [uniqueName: string]: GroupFunc };
|
||||||
childConfig?: GridConfig;
|
childConfig?: GridConfig;
|
||||||
reference?: GridReferenceDefinition;
|
reference?: GridReferenceDefinition;
|
||||||
|
isFormView?: boolean;
|
||||||
|
formViewKey?: { [uniqueName: string]: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GridCache {
|
export interface GridCache {
|
||||||
|
|||||||
@@ -518,4 +518,20 @@ export abstract class GridDisplay {
|
|||||||
conditions,
|
conditions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switchToFormView(rowData) {
|
||||||
|
if (!this.baseTable) return;
|
||||||
|
const { primaryKey } = this.baseTable;
|
||||||
|
if (!primaryKey) return;
|
||||||
|
const { columns } = primaryKey;
|
||||||
|
|
||||||
|
this.setConfig((cfg) => ({
|
||||||
|
...cfg,
|
||||||
|
isFormView: true,
|
||||||
|
formViewKey: _.pick(
|
||||||
|
rowData,
|
||||||
|
columns.map((x) => x.columnName)
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
251
packages/datalib/src/TableFormViewDisplay.ts
Normal file
251
packages/datalib/src/TableFormViewDisplay.ts
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
import { FormViewDisplay } from './FormViewDisplay';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { GridDisplay, ChangeCacheFunc, DisplayColumn, DisplayedColumnInfo, ChangeConfigFunc } from './GridDisplay';
|
||||||
|
import { TableInfo, EngineDriver, ViewInfo, ColumnInfo, NamedObjectInfo, DatabaseInfo } from 'dbgate-types';
|
||||||
|
import { GridConfig, GridCache, createGridCache } from './GridConfig';
|
||||||
|
import {
|
||||||
|
Expression,
|
||||||
|
Select,
|
||||||
|
treeToSql,
|
||||||
|
dumpSqlSelect,
|
||||||
|
mergeConditions,
|
||||||
|
Condition,
|
||||||
|
OrderByExpression,
|
||||||
|
} from 'dbgate-sqltree';
|
||||||
|
import { filterName } from './filterName';
|
||||||
|
import { TableGridDisplay } from './TableGridDisplay';
|
||||||
|
import stableStringify from 'json-stable-stringify';
|
||||||
|
import { ChangeSetFieldDefinition, ChangeSetRowDefinition } from './ChangeSet';
|
||||||
|
|
||||||
|
export class TableFormViewDisplay extends FormViewDisplay {
|
||||||
|
// use utility functions from GridDisplay and publish result in FromViewDisplat interface
|
||||||
|
private gridDisplay: TableGridDisplay;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public tableName: NamedObjectInfo,
|
||||||
|
driver: EngineDriver,
|
||||||
|
config: GridConfig,
|
||||||
|
setConfig: ChangeConfigFunc,
|
||||||
|
cache: GridCache,
|
||||||
|
setCache: ChangeCacheFunc,
|
||||||
|
dbinfo: DatabaseInfo
|
||||||
|
) {
|
||||||
|
super(config, setConfig, cache, setCache, driver, dbinfo);
|
||||||
|
this.gridDisplay = new TableGridDisplay(tableName, driver, config, setConfig, cache, setCache, dbinfo);
|
||||||
|
|
||||||
|
this.isLoadedCorrectly = this.gridDisplay.isLoadedCorrectly;
|
||||||
|
this.columns = this.gridDisplay.columns;
|
||||||
|
this.baseTable = this.gridDisplay.baseTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPrimaryKeyEqualCondition(row = null): Condition {
|
||||||
|
if (!row) row = this.config.formViewKey;
|
||||||
|
if (!row) return null;
|
||||||
|
const { primaryKey } = this.gridDisplay.baseTable;
|
||||||
|
if (!primaryKey) return null;
|
||||||
|
return {
|
||||||
|
conditionType: 'and',
|
||||||
|
conditions: primaryKey.columns.map(({ columnName }) => ({
|
||||||
|
conditionType: 'binary',
|
||||||
|
operator: '=',
|
||||||
|
left: {
|
||||||
|
exprType: 'column',
|
||||||
|
columnName,
|
||||||
|
source: {
|
||||||
|
alias: 'basetbl',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
exprType: 'value',
|
||||||
|
value: this.config.formViewKey[columnName],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getPrimaryKeyOperatorCondition(operator): Condition {
|
||||||
|
if (!this.config.formViewKey) return null;
|
||||||
|
const conditions = [];
|
||||||
|
|
||||||
|
const { primaryKey } = this.gridDisplay.baseTable;
|
||||||
|
if (!primaryKey) return null;
|
||||||
|
for (let index = 0; index < primaryKey.columns.length; index++) {
|
||||||
|
conditions.push({
|
||||||
|
conditionType: 'and',
|
||||||
|
conditions: [
|
||||||
|
...primaryKey.columns.slice(0, index).map(({ columnName }) => ({
|
||||||
|
conditionType: 'binary',
|
||||||
|
operator: '=',
|
||||||
|
left: {
|
||||||
|
exprType: 'column',
|
||||||
|
columnName,
|
||||||
|
source: {
|
||||||
|
alias: 'basetbl',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
exprType: 'value',
|
||||||
|
value: this.config.formViewKey[columnName],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
...primaryKey.columns.slice(index).map(({ columnName }) => ({
|
||||||
|
conditionType: 'binary',
|
||||||
|
operator: operator,
|
||||||
|
left: {
|
||||||
|
exprType: 'column',
|
||||||
|
columnName,
|
||||||
|
source: {
|
||||||
|
alias: 'basetbl',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
exprType: 'value',
|
||||||
|
value: this.config.formViewKey[columnName],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.length == 1) {
|
||||||
|
return conditions[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
conditionType: 'or',
|
||||||
|
conditions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelect() {
|
||||||
|
if (!this.driver) return null;
|
||||||
|
const select = this.gridDisplay.createSelect();
|
||||||
|
if (!select) return null;
|
||||||
|
select.topRecords = 1;
|
||||||
|
return select;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentRowQuery() {
|
||||||
|
const select = this.getSelect();
|
||||||
|
if (!select) return null;
|
||||||
|
|
||||||
|
select.where = mergeConditions(select.where, this.getPrimaryKeyEqualCondition());
|
||||||
|
const sql = treeToSql(this.driver, select, dumpSqlSelect);
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCountSelect() {
|
||||||
|
const select = this.getSelect();
|
||||||
|
if (!select) return null;
|
||||||
|
select.orderBy = null;
|
||||||
|
select.columns = [
|
||||||
|
{
|
||||||
|
exprType: 'raw',
|
||||||
|
sql: 'COUNT(*)',
|
||||||
|
alias: 'count',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
select.topRecords = null;
|
||||||
|
return select;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCountQuery() {
|
||||||
|
if (!this.driver) return null;
|
||||||
|
const select = this.getCountSelect();
|
||||||
|
if (!select) return null;
|
||||||
|
const sql = treeToSql(this.driver, select, dumpSqlSelect);
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBeforeCountQuery() {
|
||||||
|
if (!this.driver) return null;
|
||||||
|
const select = this.getCountSelect();
|
||||||
|
if (!select) return null;
|
||||||
|
select.where = mergeConditions(select.where, this.getPrimaryKeyOperatorCondition('<'));
|
||||||
|
const sql = treeToSql(this.driver, select, dumpSqlSelect);
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
extractKey(row) {
|
||||||
|
if (!row || !this.gridDisplay.baseTable || !this.gridDisplay.baseTable.primaryKey) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const formViewKey = _.pick(
|
||||||
|
row,
|
||||||
|
this.gridDisplay.baseTable.primaryKey.columns.map((x) => x.columnName)
|
||||||
|
);
|
||||||
|
return formViewKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(row) {
|
||||||
|
const formViewKey = this.extractKey(row);
|
||||||
|
this.setConfig((cfg) => ({
|
||||||
|
...cfg,
|
||||||
|
formViewKey,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadedCurrentRow(row) {
|
||||||
|
console.log('isLoadedCurrentRow', row, this.config.formViewKey);
|
||||||
|
if (!row) return false;
|
||||||
|
const formViewKey = this.extractKey(row);
|
||||||
|
return stableStringify(formViewKey) == stableStringify(this.config.formViewKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateRowQuery(commmand: 'begin' | 'previous' | 'next' | 'end') {
|
||||||
|
if (!this.driver) return null;
|
||||||
|
const select = this.gridDisplay.createSelect();
|
||||||
|
if (!select) return null;
|
||||||
|
const { primaryKey } = this.gridDisplay.baseTable;
|
||||||
|
|
||||||
|
function getOrderBy(direction): OrderByExpression[] {
|
||||||
|
return primaryKey.columns.map(({ columnName }) => ({
|
||||||
|
exprType: 'column',
|
||||||
|
columnName,
|
||||||
|
direction,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
select.topRecords = 1;
|
||||||
|
switch (commmand) {
|
||||||
|
case 'begin':
|
||||||
|
select.orderBy = getOrderBy('ASC');
|
||||||
|
break;
|
||||||
|
case 'end':
|
||||||
|
select.orderBy = getOrderBy('DESC');
|
||||||
|
break;
|
||||||
|
case 'previous':
|
||||||
|
select.orderBy = getOrderBy('DESC');
|
||||||
|
select.where = mergeConditions(select.where, this.getPrimaryKeyOperatorCondition('<'));
|
||||||
|
break;
|
||||||
|
case 'next':
|
||||||
|
select.orderBy = getOrderBy('ASC');
|
||||||
|
select.where = mergeConditions(select.where, this.getPrimaryKeyOperatorCondition('>'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = treeToSql(this.driver, select, dumpSqlSelect);
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
getChangeSetRow(row): ChangeSetRowDefinition {
|
||||||
|
if (!this.baseTable) return null;
|
||||||
|
return {
|
||||||
|
pureName: this.baseTable.pureName,
|
||||||
|
schemaName: this.baseTable.schemaName,
|
||||||
|
condition: this.extractKey(row),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getChangeSetField(row, uniqueName): ChangeSetFieldDefinition {
|
||||||
|
const col = this.columns.find((x) => x.uniqueName == uniqueName);
|
||||||
|
if (!col) return null;
|
||||||
|
if (!this.baseTable) return null;
|
||||||
|
if (this.baseTable.pureName != col.pureName || this.baseTable.schemaName != col.schemaName) return null;
|
||||||
|
return {
|
||||||
|
...this.getChangeSetRow(row),
|
||||||
|
uniqueName: uniqueName,
|
||||||
|
columnName: col.columnName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
export * from "./GridDisplay";
|
export * from './GridDisplay';
|
||||||
export * from "./GridConfig";
|
export * from './GridConfig';
|
||||||
export * from "./TableGridDisplay";
|
export * from './TableGridDisplay';
|
||||||
export * from "./ViewGridDisplay";
|
export * from './ViewGridDisplay';
|
||||||
export * from "./JslGridDisplay";
|
export * from './JslGridDisplay';
|
||||||
export * from "./ChangeSet";
|
export * from './ChangeSet';
|
||||||
export * from "./filterName";
|
export * from './filterName';
|
||||||
export * from "./FreeTableGridDisplay";
|
export * from './FreeTableGridDisplay';
|
||||||
export * from "./FreeTableModel";
|
export * from './FreeTableModel';
|
||||||
export * from "./MacroDefinition";
|
export * from './MacroDefinition';
|
||||||
export * from "./runMacro";
|
export * from './runMacro';
|
||||||
|
export * from './FormViewDisplay';
|
||||||
|
export * from './TableFormViewDisplay';
|
||||||
|
|||||||
@@ -52,15 +52,18 @@ function autodetect(selection, grider, value) {
|
|||||||
return 'textWrap';
|
return 'textWrap';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CellDataView({ selection, grider }) {
|
export default function CellDataView({ selection = undefined, grider = undefined, selectedValue = undefined }) {
|
||||||
const [selectedFormatType, setSelectedFormatType] = React.useState('autodetect');
|
const [selectedFormatType, setSelectedFormatType] = React.useState('autodetect');
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
let value = null;
|
let value = null;
|
||||||
if (grider && selection.length == 1) {
|
if (grider && selection && selection.length == 1) {
|
||||||
const rowData = grider.getRowData(selection[0].row);
|
const rowData = grider.getRowData(selection[0].row);
|
||||||
const { column } = selection[0];
|
const { column } = selection[0];
|
||||||
if (rowData) value = rowData[column];
|
if (rowData) value = rowData[column];
|
||||||
}
|
}
|
||||||
|
if (selectedValue) {
|
||||||
|
value = selectedValue;
|
||||||
|
}
|
||||||
const autodetectFormatType = React.useMemo(() => autodetect(selection, grider, value), [selection, grider, value]);
|
const autodetectFormatType = React.useMemo(() => autodetect(selection, grider, value), [selection, grider, value]);
|
||||||
const autodetectFormat = formats.find((x) => x.type == autodetectFormatType);
|
const autodetectFormat = formats.find((x) => x.type == autodetectFormatType);
|
||||||
|
|
||||||
|
|||||||
@@ -21,31 +21,50 @@ const DataGridContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export default function DataGrid(props) {
|
export default function DataGrid(props) {
|
||||||
const { GridCore } = props;
|
const { GridCore, FormView, config, formDisplay } = props;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [managerSize, setManagerSize] = React.useState(0);
|
const [managerSize, setManagerSize] = React.useState(0);
|
||||||
const [selection, setSelection] = React.useState([]);
|
const [selection, setSelection] = React.useState([]);
|
||||||
|
const [formSelection, setFormSelection] = React.useState(null);
|
||||||
const [grider, setGrider] = React.useState(null);
|
const [grider, setGrider] = React.useState(null);
|
||||||
|
// const [formViewData, setFormViewData] = React.useState(null);
|
||||||
|
const isFormView = !!(config && config.isFormView);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HorizontalSplitter initialValue="300px" size={managerSize} setSize={setManagerSize}>
|
<HorizontalSplitter initialValue="300px" size={managerSize} setSize={setManagerSize}>
|
||||||
<LeftContainer theme={theme}>
|
<LeftContainer theme={theme}>
|
||||||
<WidgetColumnBar>
|
<WidgetColumnBar>
|
||||||
<WidgetColumnBarItem title="Columns" name="columns" height={props.showReferences ? '40%' : '60%'}>
|
{!isFormView && (
|
||||||
<ColumnManager {...props} managerSize={managerSize} />
|
<WidgetColumnBarItem title="Columns" name="columns" height={props.showReferences ? '40%' : '60%'}>
|
||||||
</WidgetColumnBarItem>
|
<ColumnManager {...props} managerSize={managerSize} />
|
||||||
|
</WidgetColumnBarItem>
|
||||||
|
)}
|
||||||
{props.showReferences && props.display.hasReferences && (
|
{props.showReferences && props.display.hasReferences && (
|
||||||
<WidgetColumnBarItem title="References" name="references" height="30%" collapsed={props.isDetailView}>
|
<WidgetColumnBarItem title="References" name="references" height="30%" collapsed={props.isDetailView}>
|
||||||
<ReferenceManager {...props} managerSize={managerSize} />
|
<ReferenceManager {...props} managerSize={managerSize} />
|
||||||
</WidgetColumnBarItem>
|
</WidgetColumnBarItem>
|
||||||
)}
|
)}
|
||||||
<WidgetColumnBarItem title="Cell data" name="cellData" collapsed={props.isDetailView}>
|
<WidgetColumnBarItem title="Cell data" name="cellData" collapsed={props.isDetailView}>
|
||||||
<CellDataView selection={selection} grider={grider} />
|
{isFormView ? (
|
||||||
|
<CellDataView selectedValue={formSelection} />
|
||||||
|
) : (
|
||||||
|
<CellDataView selection={selection} grider={grider} />
|
||||||
|
)}
|
||||||
</WidgetColumnBarItem>
|
</WidgetColumnBarItem>
|
||||||
</WidgetColumnBar>
|
</WidgetColumnBar>
|
||||||
</LeftContainer>
|
</LeftContainer>
|
||||||
|
|
||||||
<DataGridContainer>
|
<DataGridContainer>
|
||||||
<GridCore {...props} onSelectionChanged={setSelection} onChangeGrider={setGrider} />
|
{isFormView ? (
|
||||||
|
<FormView {...props} onSelectionChanged={setFormSelection} />
|
||||||
|
) : (
|
||||||
|
<GridCore
|
||||||
|
{...props}
|
||||||
|
onSelectionChanged={setSelection}
|
||||||
|
onChangeGrider={setGrider}
|
||||||
|
formViewAvailable={!!FormView && !!formDisplay}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</DataGridContainer>
|
</DataGridContainer>
|
||||||
</HorizontalSplitter>
|
</HorizontalSplitter>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export default function DataGridContextMenu({
|
|||||||
openFreeTable,
|
openFreeTable,
|
||||||
openChartSelection,
|
openChartSelection,
|
||||||
openActiveChart,
|
openActiveChart,
|
||||||
|
switchToForm,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -57,6 +58,11 @@ export default function DataGridContextMenu({
|
|||||||
<DropDownMenuItem onClick={openFreeTable}>Open selection in free table editor</DropDownMenuItem>
|
<DropDownMenuItem onClick={openFreeTable}>Open selection in free table editor</DropDownMenuItem>
|
||||||
<DropDownMenuItem onClick={openChartSelection}>Open chart from selection</DropDownMenuItem>
|
<DropDownMenuItem onClick={openChartSelection}>Open chart from selection</DropDownMenuItem>
|
||||||
{openActiveChart && <DropDownMenuItem onClick={openActiveChart}>Open active chart</DropDownMenuItem>}
|
{openActiveChart && <DropDownMenuItem onClick={openActiveChart}>Open active chart</DropDownMenuItem>}
|
||||||
|
{!!switchToForm && (
|
||||||
|
<DropDownMenuItem onClick={switchToForm} keyText="F4">
|
||||||
|
Form view
|
||||||
|
</DropDownMenuItem>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ export default function DataGridCore(props) {
|
|||||||
onSelectionChanged,
|
onSelectionChanged,
|
||||||
frameSelection,
|
frameSelection,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
|
formViewAvailable,
|
||||||
} = props;
|
} = props;
|
||||||
// console.log('RENDER GRID', display.baseTable.pureName);
|
// console.log('RENDER GRID', display.baseTable.pureName);
|
||||||
const columns = React.useMemo(() => display.allColumns, [display]);
|
const columns = React.useMemo(() => display.allColumns, [display]);
|
||||||
@@ -381,6 +382,7 @@ export default function DataGridCore(props) {
|
|||||||
openFreeTable={handleOpenFreeTable}
|
openFreeTable={handleOpenFreeTable}
|
||||||
openChartSelection={handleOpenChart}
|
openChartSelection={handleOpenChart}
|
||||||
openActiveChart={openActiveChart}
|
openActiveChart={openActiveChart}
|
||||||
|
switchToForm={handleSwitchToFormView}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -719,6 +721,11 @@ export default function DataGridCore(props) {
|
|||||||
display.reload();
|
display.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.keyCode == keycodes.f4) {
|
||||||
|
event.preventDefault();
|
||||||
|
handleSwitchToFormView();
|
||||||
|
}
|
||||||
|
|
||||||
if (event.keyCode == keycodes.s && event.ctrlKey) {
|
if (event.keyCode == keycodes.s && event.ctrlKey) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
handleSave();
|
handleSave();
|
||||||
@@ -942,6 +949,24 @@ export default function DataGridCore(props) {
|
|||||||
display.clearFilters();
|
display.clearFilters();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSetFormView =
|
||||||
|
formViewAvailable && display.baseTable && display.baseTable.primaryKey
|
||||||
|
? (rowData) => {
|
||||||
|
display.switchToFormView(rowData);
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const handleSwitchToFormView =
|
||||||
|
formViewAvailable && display.baseTable && display.baseTable.primaryKey
|
||||||
|
? () => {
|
||||||
|
const cell = currentCell;
|
||||||
|
if (!isRegularCell(cell)) return;
|
||||||
|
const rowData = grider.getRowData(cell[0]);
|
||||||
|
if (!rowData) return;
|
||||||
|
display.switchToFormView(rowData);
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
// console.log('visibleRealColumnIndexes', visibleRealColumnIndexes);
|
// console.log('visibleRealColumnIndexes', visibleRealColumnIndexes);
|
||||||
// console.log(
|
// console.log(
|
||||||
// 'gridScrollAreaWidth / columnSizes.getVisibleScrollSizeSum()',
|
// 'gridScrollAreaWidth / columnSizes.getVisibleScrollSizeSum()',
|
||||||
@@ -1047,6 +1072,7 @@ export default function DataGridCore(props) {
|
|||||||
display={display}
|
display={display}
|
||||||
focusedColumn={display.focusedColumn}
|
focusedColumn={display.focusedColumn}
|
||||||
frameSelection={frameSelection}
|
frameSelection={frameSelection}
|
||||||
|
onSetFormView={handleSetFormView}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@@ -1081,6 +1107,7 @@ export default function DataGridCore(props) {
|
|||||||
await axios.post('database-connections/refresh', { conid, database });
|
await axios.post('database-connections/refresh', { conid, database });
|
||||||
display.reload();
|
display.reload();
|
||||||
}}
|
}}
|
||||||
|
switchToForm={handleSwitchToFormView}
|
||||||
/>,
|
/>,
|
||||||
props.toolbarPortalRef.current
|
props.toolbarPortalRef.current
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import InplaceEditor from './InplaceEditor';
|
|||||||
import { cellIsSelected } from './gridutil';
|
import { cellIsSelected } from './gridutil';
|
||||||
import { isTypeLogical } from 'dbgate-tools';
|
import { isTypeLogical } from 'dbgate-tools';
|
||||||
import useTheme from '../theme/useTheme';
|
import useTheme from '../theme/useTheme';
|
||||||
|
import { FontIcon } from '../icons';
|
||||||
|
|
||||||
const TableBodyCell = styled.td`
|
const TableBodyCell = styled.td`
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
@@ -114,6 +115,7 @@ const TableHeaderCell = styled.td`
|
|||||||
padding: 2px;
|
padding: 2px;
|
||||||
background-color: ${(props) => props.theme.gridheader_background};
|
background-color: ${(props) => props.theme.gridheader_background};
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const AutoFillPoint = styled.div`
|
const AutoFillPoint = styled.div`
|
||||||
@@ -127,6 +129,16 @@ const AutoFillPoint = styled.div`
|
|||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const ShowFormButton = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
right: 2px;
|
||||||
|
top: 2px;
|
||||||
|
&:hover {
|
||||||
|
background-color: ${(props) => props.theme.gridheader_background_blue[4]};
|
||||||
|
border: 1px solid ${(props) => props.theme.border};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
function makeBulletString(value) {
|
function makeBulletString(value) {
|
||||||
return _.pad('', value.length, '•');
|
return _.pad('', value.length, '•');
|
||||||
}
|
}
|
||||||
@@ -142,7 +154,7 @@ function highlightSpecialCharacters(value) {
|
|||||||
|
|
||||||
const dateTimeRegex = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d\d)?Z?$/;
|
const dateTimeRegex = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d\d)?Z?$/;
|
||||||
|
|
||||||
function CellFormattedValue({ value, dataType }) {
|
export function CellFormattedValue({ value, dataType }) {
|
||||||
if (value == null) return <NullSpan>(NULL)</NullSpan>;
|
if (value == null) return <NullSpan>(NULL)</NullSpan>;
|
||||||
if (_.isDate(value)) return moment(value).format('YYYY-MM-DD HH:mm:ss');
|
if (_.isDate(value)) return moment(value).format('YYYY-MM-DD HH:mm:ss');
|
||||||
if (value === true) return '1';
|
if (value === true) return '1';
|
||||||
@@ -167,6 +179,33 @@ function CellFormattedValue({ value, dataType }) {
|
|||||||
return value.toString();
|
return value.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RowHeaderCell({ rowIndex, theme, onSetFormView, rowData }) {
|
||||||
|
const [mouseIn, setMouseIn] = React.useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableHeaderCell
|
||||||
|
data-row={rowIndex}
|
||||||
|
data-col="header"
|
||||||
|
theme={theme}
|
||||||
|
onMouseEnter={onSetFormView ? () => setMouseIn(true) : null}
|
||||||
|
onMouseLeave={onSetFormView ? () => setMouseIn(false) : null}
|
||||||
|
>
|
||||||
|
{rowIndex + 1}
|
||||||
|
{!!onSetFormView && mouseIn && (
|
||||||
|
<ShowFormButton
|
||||||
|
theme={theme}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onSetFormView(rowData);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontIcon icon="icon form" />
|
||||||
|
</ShowFormButton>
|
||||||
|
)}
|
||||||
|
</TableHeaderCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** @param props {import('./types').DataGridProps} */
|
/** @param props {import('./types').DataGridProps} */
|
||||||
function DataGridRow(props) {
|
function DataGridRow(props) {
|
||||||
const {
|
const {
|
||||||
@@ -181,6 +220,7 @@ function DataGridRow(props) {
|
|||||||
focusedColumn,
|
focusedColumn,
|
||||||
grider,
|
grider,
|
||||||
frameSelection,
|
frameSelection,
|
||||||
|
onSetFormView,
|
||||||
} = props;
|
} = props;
|
||||||
// usePropsCompare({
|
// usePropsCompare({
|
||||||
// rowHeight,
|
// rowHeight,
|
||||||
@@ -217,9 +257,8 @@ function DataGridRow(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TableBodyRow style={{ height: `${rowHeight}px` }} theme={theme}>
|
<TableBodyRow style={{ height: `${rowHeight}px` }} theme={theme}>
|
||||||
<TableHeaderCell data-row={rowIndex} data-col="header" theme={theme}>
|
<RowHeaderCell rowIndex={rowIndex} theme={theme} onSetFormView={onSetFormView} rowData={rowData} />
|
||||||
{rowIndex + 1}
|
|
||||||
</TableHeaderCell>
|
|
||||||
{visibleRealColumns.map((col) => (
|
{visibleRealColumns.map((col) => (
|
||||||
<TableBodyCell
|
<TableBodyCell
|
||||||
key={col.uniqueName}
|
key={col.uniqueName}
|
||||||
@@ -252,9 +291,10 @@ function DataGridRow(props) {
|
|||||||
inplaceEditorState={inplaceEditorState}
|
inplaceEditorState={inplaceEditorState}
|
||||||
dispatchInsplaceEditor={dispatchInsplaceEditor}
|
dispatchInsplaceEditor={dispatchInsplaceEditor}
|
||||||
cellValue={rowData[col.uniqueName]}
|
cellValue={rowData[col.uniqueName]}
|
||||||
grider={grider}
|
// grider={grider}
|
||||||
rowIndex={rowIndex}
|
// rowIndex={rowIndex}
|
||||||
uniqueName={col.uniqueName}
|
// uniqueName={col.uniqueName}
|
||||||
|
onSetValue={(value) => grider.setCellValue(rowIndex, col.uniqueName, value)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ToolbarButton from '../widgets/ToolbarButton';
|
import ToolbarButton from '../widgets/ToolbarButton';
|
||||||
|
|
||||||
export default function DataGridToolbar({ reload, reconnect, grider, save }) {
|
export default function DataGridToolbar({ reload, reconnect, grider, save, switchToForm }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{switchToForm && (
|
||||||
|
<ToolbarButton onClick={switchToForm} icon="icon form">
|
||||||
|
Form view
|
||||||
|
</ToolbarButton>
|
||||||
|
)}
|
||||||
<ToolbarButton onClick={reload} icon="icon reload">
|
<ToolbarButton onClick={reload} icon="icon reload">
|
||||||
Refresh
|
Refresh
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
|
|||||||
@@ -14,14 +14,16 @@ const StyledInput = styled.input`
|
|||||||
|
|
||||||
export default function InplaceEditor({
|
export default function InplaceEditor({
|
||||||
widthPx,
|
widthPx,
|
||||||
rowIndex,
|
// rowIndex,
|
||||||
uniqueName,
|
// uniqueName,
|
||||||
grider,
|
// grider,
|
||||||
cellValue,
|
cellValue,
|
||||||
inplaceEditorState,
|
inplaceEditorState,
|
||||||
dispatchInsplaceEditor,
|
dispatchInsplaceEditor,
|
||||||
|
onSetValue,
|
||||||
}) {
|
}) {
|
||||||
const editorRef = React.useRef();
|
const editorRef = React.useRef();
|
||||||
|
const widthRef = React.useRef(widthPx);
|
||||||
const isChangedRef = React.useRef(!!inplaceEditorState.text);
|
const isChangedRef = React.useRef(!!inplaceEditorState.text);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const editor = editorRef.current;
|
const editor = editorRef.current;
|
||||||
@@ -34,7 +36,8 @@ export default function InplaceEditor({
|
|||||||
function handleBlur() {
|
function handleBlur() {
|
||||||
if (isChangedRef.current) {
|
if (isChangedRef.current) {
|
||||||
const editor = editorRef.current;
|
const editor = editorRef.current;
|
||||||
grider.setCellValue(rowIndex, uniqueName, editor.value);
|
onSetValue(editor.value);
|
||||||
|
// grider.setCellValue(rowIndex, uniqueName, editor.value);
|
||||||
isChangedRef.current = false;
|
isChangedRef.current = false;
|
||||||
}
|
}
|
||||||
dispatchInsplaceEditor({ type: 'close' });
|
dispatchInsplaceEditor({ type: 'close' });
|
||||||
@@ -42,7 +45,8 @@ export default function InplaceEditor({
|
|||||||
if (inplaceEditorState.shouldSave) {
|
if (inplaceEditorState.shouldSave) {
|
||||||
const editor = editorRef.current;
|
const editor = editorRef.current;
|
||||||
if (isChangedRef.current) {
|
if (isChangedRef.current) {
|
||||||
grider.setCellValue(rowIndex, uniqueName, editor.value);
|
onSetValue(editor.value);
|
||||||
|
// grider.setCellValue(rowIndex, uniqueName, editor.value);
|
||||||
isChangedRef.current = false;
|
isChangedRef.current = false;
|
||||||
}
|
}
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -57,7 +61,8 @@ export default function InplaceEditor({
|
|||||||
break;
|
break;
|
||||||
case keycodes.enter:
|
case keycodes.enter:
|
||||||
if (isChangedRef.current) {
|
if (isChangedRef.current) {
|
||||||
grider.setCellValue(rowIndex, uniqueName, editor.value);
|
// grider.setCellValue(rowIndex, uniqueName, editor.value);
|
||||||
|
onSetValue(editor.value);
|
||||||
isChangedRef.current = false;
|
isChangedRef.current = false;
|
||||||
}
|
}
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -66,7 +71,8 @@ export default function InplaceEditor({
|
|||||||
case keycodes.s:
|
case keycodes.s:
|
||||||
if (event.ctrlKey) {
|
if (event.ctrlKey) {
|
||||||
if (isChangedRef.current) {
|
if (isChangedRef.current) {
|
||||||
grider.setCellValue(rowIndex, uniqueName, editor.value);
|
onSetValue(editor.value);
|
||||||
|
// grider.setCellValue(rowIndex, uniqueName, editor.value);
|
||||||
isChangedRef.current = false;
|
isChangedRef.current = false;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -83,9 +89,9 @@ export default function InplaceEditor({
|
|||||||
onChange={() => (isChangedRef.current = true)}
|
onChange={() => (isChangedRef.current = true)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
style={{
|
style={{
|
||||||
width: widthPx,
|
width: widthRef.current,
|
||||||
minWidth: widthPx,
|
minWidth: widthRef.current,
|
||||||
maxWidth: widthPx,
|
maxWidth: widthRef.current,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import DataGrid from './DataGrid';
|
import DataGrid from './DataGrid';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { TableGridDisplay, createGridConfig, createGridCache } from 'dbgate-datalib';
|
import { TableGridDisplay, TableFormViewDisplay, createGridConfig, createGridCache } from 'dbgate-datalib';
|
||||||
import { getFilterValueExpression } from 'dbgate-filterparser';
|
import { getFilterValueExpression } from 'dbgate-filterparser';
|
||||||
import { findEngineDriver } from 'dbgate-tools';
|
import { findEngineDriver } from 'dbgate-tools';
|
||||||
import { useConnectionInfo, getTableInfo, useDatabaseInfo } from '../utility/metadataLoaders';
|
import { useConnectionInfo, getTableInfo, useDatabaseInfo } from '../utility/metadataLoaders';
|
||||||
@@ -12,6 +12,7 @@ import stableStringify from 'json-stable-stringify';
|
|||||||
import ReferenceHeader from './ReferenceHeader';
|
import ReferenceHeader from './ReferenceHeader';
|
||||||
import SqlDataGridCore from './SqlDataGridCore';
|
import SqlDataGridCore from './SqlDataGridCore';
|
||||||
import useExtensions from '../utility/useExtensions';
|
import useExtensions from '../utility/useExtensions';
|
||||||
|
import SqlFormView from '../formview/SqlFormView';
|
||||||
|
|
||||||
const ReferenceContainer = styled.div`
|
const ReferenceContainer = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -87,7 +88,22 @@ export default function TableDataGrid({
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createFormDisplay() {
|
||||||
|
return connection
|
||||||
|
? new TableFormViewDisplay(
|
||||||
|
{ schemaName, pureName },
|
||||||
|
findEngineDriver(connection, extensions),
|
||||||
|
config,
|
||||||
|
setConfig,
|
||||||
|
cache || myCache,
|
||||||
|
setCache || setMyCache,
|
||||||
|
dbinfo
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
const [display, setDisplay] = React.useState(createDisplay());
|
const [display, setDisplay] = React.useState(createDisplay());
|
||||||
|
const [formDisplay, setFormDisplay] = React.useState(createFormDisplay());
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setRefReloadToken((v) => v + 1);
|
setRefReloadToken((v) => v + 1);
|
||||||
@@ -101,6 +117,13 @@ export default function TableDataGrid({
|
|||||||
setDisplay(newDisplay);
|
setDisplay(newDisplay);
|
||||||
}, [connection, config, cache || myCache, conid, database, schemaName, pureName, dbinfo, extensions]);
|
}, [connection, config, cache || myCache, conid, database, schemaName, pureName, dbinfo, extensions]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const newDisplay = createFormDisplay();
|
||||||
|
if (!newDisplay) return;
|
||||||
|
if (formDisplay && formDisplay.isLoadedCorrectly && !newDisplay.isLoadedCorrectly) return;
|
||||||
|
setFormDisplay(newDisplay);
|
||||||
|
}, [connection, config, cache || myCache, conid, database, schemaName, pureName, dbinfo, extensions]);
|
||||||
|
|
||||||
const handleDatabaseStructureChanged = React.useCallback(() => {
|
const handleDatabaseStructureChanged = React.useCallback(() => {
|
||||||
(setCache || setMyCache)(createGridCache());
|
(setCache || setMyCache)(createGridCache());
|
||||||
}, []);
|
}, []);
|
||||||
@@ -158,9 +181,12 @@ export default function TableDataGrid({
|
|||||||
<VerticalSplitter>
|
<VerticalSplitter>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
// key={`${conid}, ${database}, ${schemaName}, ${pureName}`}
|
// key={`${conid}, ${database}, ${schemaName}, ${pureName}`}
|
||||||
|
config={config}
|
||||||
|
setConfig={setConfig}
|
||||||
conid={conid}
|
conid={conid}
|
||||||
database={database}
|
database={database}
|
||||||
display={display}
|
display={display}
|
||||||
|
formDisplay={formDisplay}
|
||||||
tabVisible={tabVisible}
|
tabVisible={tabVisible}
|
||||||
changeSetState={changeSetState}
|
changeSetState={changeSetState}
|
||||||
dispatchChangeSet={dispatchChangeSet}
|
dispatchChangeSet={dispatchChangeSet}
|
||||||
@@ -171,7 +197,11 @@ export default function TableDataGrid({
|
|||||||
refReloadToken={refReloadToken.toString()}
|
refReloadToken={refReloadToken.toString()}
|
||||||
masterLoadedTime={masterLoadedTime}
|
masterLoadedTime={masterLoadedTime}
|
||||||
GridCore={SqlDataGridCore}
|
GridCore={SqlDataGridCore}
|
||||||
|
FormView={SqlFormView}
|
||||||
isDetailView={isDetailView}
|
isDetailView={isDetailView}
|
||||||
|
// tableInfo={
|
||||||
|
// dbinfo && dbinfo.tables && dbinfo.tables.find((x) => x.pureName == pureName && x.schemaName == schemaName)
|
||||||
|
// }
|
||||||
/>
|
/>
|
||||||
{reference && (
|
{reference && (
|
||||||
<ReferenceContainer>
|
<ReferenceContainer>
|
||||||
|
|||||||
93
packages/web/src/formview/ChangeSetFormer.ts
Normal file
93
packages/web/src/formview/ChangeSetFormer.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import {
|
||||||
|
ChangeSet,
|
||||||
|
changeSetContainsChanges,
|
||||||
|
changeSetInsertNewRow,
|
||||||
|
createChangeSet,
|
||||||
|
deleteChangeSetRows,
|
||||||
|
findExistingChangeSetItem,
|
||||||
|
getChangeSetInsertedRows,
|
||||||
|
TableFormViewDisplay,
|
||||||
|
revertChangeSetRowChanges,
|
||||||
|
setChangeSetValue,
|
||||||
|
ChangeSetRowDefinition,
|
||||||
|
} from 'dbgate-datalib';
|
||||||
|
import Former from './Former';
|
||||||
|
|
||||||
|
export default class ChangeSetFormer extends Former {
|
||||||
|
public changeSet: ChangeSet;
|
||||||
|
public setChangeSet: Function;
|
||||||
|
private batchChangeSet: ChangeSet;
|
||||||
|
public rowDefinition: ChangeSetRowDefinition;
|
||||||
|
public rowStatus;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public sourceRow: any,
|
||||||
|
public changeSetState,
|
||||||
|
public dispatchChangeSet,
|
||||||
|
public display: TableFormViewDisplay
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.changeSet = changeSetState && changeSetState.value;
|
||||||
|
this.setChangeSet = (value) => dispatchChangeSet({ type: 'set', value });
|
||||||
|
this.batchChangeSet = null;
|
||||||
|
this.rowDefinition = display.getChangeSetRow(sourceRow);
|
||||||
|
const [matchedField, matchedChangeSetItem] = findExistingChangeSetItem(this.changeSet, this.rowDefinition);
|
||||||
|
this.rowData = matchedChangeSetItem ? { ...sourceRow, ...matchedChangeSetItem.fields } : sourceRow;
|
||||||
|
let status = 'regular';
|
||||||
|
if (matchedChangeSetItem && matchedField == 'updates') status = 'updated';
|
||||||
|
if (matchedField == 'deletes') status = 'deleted';
|
||||||
|
this.rowStatus = {
|
||||||
|
status,
|
||||||
|
modifiedFields:
|
||||||
|
matchedChangeSetItem && matchedChangeSetItem.fields ? new Set(Object.keys(matchedChangeSetItem.fields)) : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
applyModification(changeSetReducer) {
|
||||||
|
if (this.batchChangeSet) {
|
||||||
|
this.batchChangeSet = changeSetReducer(this.batchChangeSet);
|
||||||
|
} else {
|
||||||
|
this.setChangeSet(changeSetReducer(this.changeSet));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCellValue( uniqueName: string, value: any) {
|
||||||
|
const row = this.sourceRow;
|
||||||
|
const definition = this.display.getChangeSetField(row, uniqueName);
|
||||||
|
this.applyModification((chs) => setChangeSetValue(chs, definition, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteRow(index: number) {
|
||||||
|
this.applyModification((chs) => deleteChangeSetRows(chs, this.rowDefinition));
|
||||||
|
}
|
||||||
|
|
||||||
|
beginUpdate() {
|
||||||
|
this.batchChangeSet = this.changeSet;
|
||||||
|
}
|
||||||
|
endUpdate() {
|
||||||
|
this.setChangeSet(this.batchChangeSet);
|
||||||
|
this.batchChangeSet = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
revertRowChanges() {
|
||||||
|
this.applyModification((chs) => revertChangeSetRowChanges(chs, this.rowDefinition));
|
||||||
|
}
|
||||||
|
revertAllChanges() {
|
||||||
|
this.applyModification((chs) => createChangeSet());
|
||||||
|
}
|
||||||
|
undo() {
|
||||||
|
this.dispatchChangeSet({ type: 'undo' });
|
||||||
|
}
|
||||||
|
redo() {
|
||||||
|
this.dispatchChangeSet({ type: 'redo' });
|
||||||
|
}
|
||||||
|
get canUndo() {
|
||||||
|
return this.changeSetState.canUndo;
|
||||||
|
}
|
||||||
|
get canRedo() {
|
||||||
|
return this.changeSetState.canRedo;
|
||||||
|
}
|
||||||
|
get containsChanges() {
|
||||||
|
return changeSetContainsChanges(this.changeSet);
|
||||||
|
}
|
||||||
|
}
|
||||||
499
packages/web/src/formview/FormView.js
Normal file
499
packages/web/src/formview/FormView.js
Normal file
@@ -0,0 +1,499 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import ColumnLabel from '../datagrid/ColumnLabel';
|
||||||
|
import { findForeignKeyForColumn } from 'dbgate-tools';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import useTheme from '../theme/useTheme';
|
||||||
|
import useDimensions from '../utility/useDimensions';
|
||||||
|
import FormViewToolbar from './FormViewToolbar';
|
||||||
|
import { useShowMenu } from '../modals/showMenu';
|
||||||
|
import FormViewContextMenu from './FormViewContextMenu';
|
||||||
|
import keycodes from '../utility/keycodes';
|
||||||
|
import { CellFormattedValue } from '../datagrid/DataGridRow';
|
||||||
|
import { cellFromEvent } from '../datagrid/selection';
|
||||||
|
import InplaceEditor from '../datagrid/InplaceEditor';
|
||||||
|
import { copyTextToClipboard } from '../utility/clipboard';
|
||||||
|
|
||||||
|
const Table = styled.table`
|
||||||
|
border-collapse: collapse;
|
||||||
|
outline: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
overflow-x: scroll;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TableRow = styled.tr`
|
||||||
|
background-color: ${(props) => props.theme.gridbody_background};
|
||||||
|
&:nth-child(6n + 3) {
|
||||||
|
background-color: ${(props) => props.theme.gridbody_background_alt2};
|
||||||
|
}
|
||||||
|
&:nth-child(6n + 6) {
|
||||||
|
background-color: ${(props) => props.theme.gridbody_background_alt3};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TableHeaderCell = styled.td`
|
||||||
|
border: 1px solid ${(props) => props.theme.border};
|
||||||
|
text-align: left;
|
||||||
|
padding: 2px;
|
||||||
|
background-color: ${(props) => props.theme.gridheader_background};
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
props.isSelected &&
|
||||||
|
`
|
||||||
|
background: initial;
|
||||||
|
background-color: ${props.theme.gridbody_selection[4]};
|
||||||
|
color: ${props.theme.gridbody_invfont1};`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TableBodyCell = styled.td`
|
||||||
|
font-weight: normal;
|
||||||
|
border: 1px solid ${(props) => props.theme.border};
|
||||||
|
// border-collapse: collapse;
|
||||||
|
padding: 2px;
|
||||||
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
|
max-width: 500px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
props.isSelected &&
|
||||||
|
`
|
||||||
|
background: initial;
|
||||||
|
background-color: ${props.theme.gridbody_selection[4]};
|
||||||
|
color: ${props.theme.gridbody_invfont1};`}
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
!props.isSelected &&
|
||||||
|
props.isModifiedCell &&
|
||||||
|
`
|
||||||
|
background-color: ${props.theme.gridbody_background_orange[1]};`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FocusField = styled.input`
|
||||||
|
// visibility: hidden
|
||||||
|
position: absolute;
|
||||||
|
left: -1000px;
|
||||||
|
top: -1000px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RowCountLabel = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
background-color: ${(props) => props.theme.gridbody_background_yellow[1]};
|
||||||
|
right: 40px;
|
||||||
|
bottom: 20px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HintSpan = styled.span`
|
||||||
|
color: gray;
|
||||||
|
margin-left: 5px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function isDataCell(cell) {
|
||||||
|
return cell[1] % 2 == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FormView(props) {
|
||||||
|
const {
|
||||||
|
toolbarPortalRef,
|
||||||
|
tabVisible,
|
||||||
|
config,
|
||||||
|
setConfig,
|
||||||
|
onNavigate,
|
||||||
|
former,
|
||||||
|
onSave,
|
||||||
|
conid,
|
||||||
|
database,
|
||||||
|
onReload,
|
||||||
|
onReconnect,
|
||||||
|
allRowCount,
|
||||||
|
rowCountBefore,
|
||||||
|
onSelectionChanged,
|
||||||
|
} = props;
|
||||||
|
/** @type {import('dbgate-datalib').FormViewDisplay} */
|
||||||
|
const formDisplay = props.formDisplay;
|
||||||
|
const theme = useTheme();
|
||||||
|
const [headerRowRef, { height: rowHeight }] = useDimensions();
|
||||||
|
const [wrapperRef, { height: wrapperHeight }] = useDimensions();
|
||||||
|
const showMenu = useShowMenu();
|
||||||
|
const focusFieldRef = React.useRef(null);
|
||||||
|
const [currentCell, setCurrentCell] = React.useState([0, 0]);
|
||||||
|
const cellRefs = React.useRef({});
|
||||||
|
|
||||||
|
const rowCount = Math.floor((wrapperHeight - 20) / rowHeight);
|
||||||
|
const columnChunks = _.chunk(formDisplay.columns, rowCount);
|
||||||
|
|
||||||
|
const { rowData, rowStatus } = former;
|
||||||
|
|
||||||
|
const handleSwitchToTable = () => {
|
||||||
|
setConfig((cfg) => ({
|
||||||
|
...cfg,
|
||||||
|
isFormView: false,
|
||||||
|
formViewKey: null,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContextMenu = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
showMenu(
|
||||||
|
event.pageX,
|
||||||
|
event.pageY,
|
||||||
|
<FormViewContextMenu switchToTable={handleSwitchToTable} onNavigate={onNavigate} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCellRef = (row, col, element) => {
|
||||||
|
cellRefs.current[`${row},${col}`] = element;
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (tabVisible) {
|
||||||
|
if (focusFieldRef.current) focusFieldRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [tabVisible, focusFieldRef.current]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!onSelectionChanged || !rowData) return;
|
||||||
|
const col = getCellColumn(currentCell);
|
||||||
|
if (!col) return;
|
||||||
|
onSelectionChanged(rowData[col.uniqueName]);
|
||||||
|
}, [onSelectionChanged, currentCell, rowData]);
|
||||||
|
|
||||||
|
const checkMoveCursorBounds = (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;
|
||||||
|
return [row, col];
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCursorMove = (event) => {
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
switch (event.keyCode) {
|
||||||
|
case keycodes.leftArrow:
|
||||||
|
return checkMoveCursorBounds(currentCell[0], 0);
|
||||||
|
case keycodes.rightArrow:
|
||||||
|
return checkMoveCursorBounds(currentCell[0], columnChunks.length * 2 - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (event.keyCode) {
|
||||||
|
case keycodes.leftArrow:
|
||||||
|
return checkMoveCursorBounds(currentCell[0], currentCell[1] - 1);
|
||||||
|
case keycodes.rightArrow:
|
||||||
|
return checkMoveCursorBounds(currentCell[0], currentCell[1] + 1);
|
||||||
|
case keycodes.upArrow:
|
||||||
|
return checkMoveCursorBounds(currentCell[0] - 1, currentCell[1]);
|
||||||
|
case keycodes.downArrow:
|
||||||
|
return checkMoveCursorBounds(currentCell[0] + 1, currentCell[1]);
|
||||||
|
case keycodes.pageUp:
|
||||||
|
return checkMoveCursorBounds(0, currentCell[1]);
|
||||||
|
case keycodes.pageDown:
|
||||||
|
return checkMoveCursorBounds(rowCount - 1, currentCell[1]);
|
||||||
|
case keycodes.home:
|
||||||
|
return checkMoveCursorBounds(0, 0);
|
||||||
|
case keycodes.end:
|
||||||
|
return checkMoveCursorBounds(rowCount - 1, columnChunks.length * 2 - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyNavigation = (event) => {
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
switch (event.keyCode) {
|
||||||
|
case keycodes.upArrow:
|
||||||
|
return 'previous';
|
||||||
|
case keycodes.downArrow:
|
||||||
|
return 'next';
|
||||||
|
case keycodes.home:
|
||||||
|
return 'begin';
|
||||||
|
case keycodes.end:
|
||||||
|
return 'end';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleSave() {
|
||||||
|
if (inplaceEditorState.cell) {
|
||||||
|
// @ts-ignore
|
||||||
|
dispatchInsplaceEditor({ type: 'shouldSave' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (onSave) onSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNull() {
|
||||||
|
if (isDataCell(currentCell)) {
|
||||||
|
setCellValue(currentCell, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollIntoView = (cell) => {
|
||||||
|
const element = cellRefs.current[`${cell[0]},${cell[1]}`];
|
||||||
|
if (element) element.scrollIntoView();
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
scrollIntoView(currentCell);
|
||||||
|
}, [rowData]);
|
||||||
|
|
||||||
|
const moveCurrentCell = (row, col) => {
|
||||||
|
const moved = checkMoveCursorBounds(row, col);
|
||||||
|
setCurrentCell(moved);
|
||||||
|
scrollIntoView(moved);
|
||||||
|
};
|
||||||
|
|
||||||
|
function copyToClipboard() {
|
||||||
|
const column = getCellColumn(currentCell);
|
||||||
|
if (!column) return;
|
||||||
|
const text = currentCell[1] % 2 == 1 ? rowData[column.uniqueName] : column.columnName;
|
||||||
|
copyTextToClipboard(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (event) => {
|
||||||
|
const navigation = handleKeyNavigation(event);
|
||||||
|
if (navigation) {
|
||||||
|
event.preventDefault();
|
||||||
|
onNavigate(navigation);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const moved = handleCursorMove(event);
|
||||||
|
if (moved) {
|
||||||
|
setCurrentCell(moved);
|
||||||
|
scrollIntoView(moved);
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
former.revertRowChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (event.keyCode == keycodes.f && event.ctrlKey) {
|
||||||
|
// event.preventDefault();
|
||||||
|
// filterSelectedValue();
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (event.keyCode == keycodes.z && event.ctrlKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
former.undo();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.keyCode == keycodes.y && event.ctrlKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
former.redo();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.keyCode == keycodes.c && event.ctrlKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
copyToClipboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.keyCode == keycodes.f5) {
|
||||||
|
event.preventDefault();
|
||||||
|
onReload();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.keyCode == keycodes.f4) {
|
||||||
|
event.preventDefault();
|
||||||
|
handleSwitchToTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
dispatchInsplaceEditor({ type: 'show', text: event.nativeEvent.key, cell: currentCell });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.keyCode == keycodes.f2) {
|
||||||
|
// @ts-ignore
|
||||||
|
dispatchInsplaceEditor({ type: 'show', cell: currentCell, selectAll: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTableMouseDown = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (focusFieldRef.current) focusFieldRef.current.focus();
|
||||||
|
|
||||||
|
if (event.target.closest('.buttonLike')) return;
|
||||||
|
if (event.target.closest('.resizeHandleControl')) return;
|
||||||
|
if (event.target.closest('input')) return;
|
||||||
|
|
||||||
|
// event.target.closest('table').focus();
|
||||||
|
event.preventDefault();
|
||||||
|
if (focusFieldRef.current) focusFieldRef.current.focus();
|
||||||
|
const cell = cellFromEvent(event);
|
||||||
|
|
||||||
|
if (isDataCell(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' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
setCurrentCell(cell);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCellWidth = (row, col) => {
|
||||||
|
const element = cellRefs.current[`${row},${col}`];
|
||||||
|
if (element) return element.getBoundingClientRect().width;
|
||||||
|
return 100;
|
||||||
|
};
|
||||||
|
|
||||||
|
const rowCountInfo = React.useMemo(() => {
|
||||||
|
if (allRowCount == null || rowCountBefore == null) return 'Loading row count...';
|
||||||
|
return `Row: ${(rowCountBefore + 1).toLocaleString()} / ${allRowCount.toLocaleString()}`;
|
||||||
|
}, [rowCountBefore, allRowCount]);
|
||||||
|
|
||||||
|
const [inplaceEditorState, dispatchInsplaceEditor] = React.useReducer((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 (focusFieldRef.current) focusFieldRef.current.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 {};
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const toolbar =
|
||||||
|
toolbarPortalRef &&
|
||||||
|
toolbarPortalRef.current &&
|
||||||
|
tabVisible &&
|
||||||
|
ReactDOM.createPortal(
|
||||||
|
<FormViewToolbar
|
||||||
|
switchToTable={handleSwitchToTable}
|
||||||
|
onNavigate={onNavigate}
|
||||||
|
reload={onReload}
|
||||||
|
reconnect={onReconnect}
|
||||||
|
save={handleSave}
|
||||||
|
former={former}
|
||||||
|
/>,
|
||||||
|
toolbarPortalRef.current
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!formDisplay || !formDisplay.isLoadedCorrectly) return toolbar;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper ref={wrapperRef} onContextMenu={handleContextMenu}>
|
||||||
|
{columnChunks.map((chunk, chunkIndex) => (
|
||||||
|
<Table key={chunkIndex} onMouseDown={handleTableMouseDown}>
|
||||||
|
{chunk.map((col, rowIndex) => (
|
||||||
|
<TableRow key={col.columnName} theme={theme} ref={headerRowRef} style={{ height: `${rowHeight}px` }}>
|
||||||
|
<TableHeaderCell
|
||||||
|
theme={theme}
|
||||||
|
data-row={rowIndex}
|
||||||
|
data-col={chunkIndex * 2}
|
||||||
|
// @ts-ignore
|
||||||
|
isSelected={currentCell[0] == rowIndex && currentCell[1] == chunkIndex * 2}
|
||||||
|
ref={(element) => setCellRef(rowIndex, chunkIndex * 2, element)}
|
||||||
|
>
|
||||||
|
<ColumnLabel {...col} />
|
||||||
|
</TableHeaderCell>
|
||||||
|
<TableBodyCell
|
||||||
|
theme={theme}
|
||||||
|
data-row={rowIndex}
|
||||||
|
data-col={chunkIndex * 2 + 1}
|
||||||
|
// @ts-ignore
|
||||||
|
isSelected={currentCell[0] == rowIndex && currentCell[1] == chunkIndex * 2 + 1}
|
||||||
|
isModifiedCell={rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)}
|
||||||
|
ref={(element) => setCellRef(rowIndex, chunkIndex * 2 + 1, element)}
|
||||||
|
>
|
||||||
|
{inplaceEditorState.cell &&
|
||||||
|
rowIndex == inplaceEditorState.cell[0] &&
|
||||||
|
chunkIndex * 2 + 1 == inplaceEditorState.cell[1] ? (
|
||||||
|
<InplaceEditor
|
||||||
|
widthPx={getCellWidth(rowIndex, chunkIndex * 2 + 1)}
|
||||||
|
inplaceEditorState={inplaceEditorState}
|
||||||
|
dispatchInsplaceEditor={dispatchInsplaceEditor}
|
||||||
|
cellValue={rowData[col.uniqueName]}
|
||||||
|
onSetValue={(value) => {
|
||||||
|
former.setCellValue(col.uniqueName, value);
|
||||||
|
}}
|
||||||
|
// grider={grider}
|
||||||
|
// rowIndex={rowIndex}
|
||||||
|
// uniqueName={col.uniqueName}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CellFormattedValue value={rowData && rowData[col.columnName]} dataType={col.dataType} />
|
||||||
|
{!!col.hintColumnName &&
|
||||||
|
rowData &&
|
||||||
|
!(rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)) && (
|
||||||
|
<HintSpan>{rowData[col.hintColumnName]}</HintSpan>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TableBodyCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</Table>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<FocusField type="text" ref={focusFieldRef} onKeyDown={handleKeyDown} />
|
||||||
|
{rowCountInfo && <RowCountLabel theme={theme}>{rowCountInfo}</RowCountLabel>}
|
||||||
|
|
||||||
|
{toolbar}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
packages/web/src/formview/FormViewContextMenu.js
Normal file
25
packages/web/src/formview/FormViewContextMenu.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
|
||||||
|
|
||||||
|
export default function FormViewContextMenu({ switchToTable, onNavigate }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DropDownMenuItem onClick={switchToTable} keyText="F4">
|
||||||
|
Table view
|
||||||
|
</DropDownMenuItem>
|
||||||
|
<DropDownMenuDivider />
|
||||||
|
<DropDownMenuItem onClick={() => onNavigate('begin')} keyText="Ctrl+Home">
|
||||||
|
Navigate to begin
|
||||||
|
</DropDownMenuItem>
|
||||||
|
<DropDownMenuItem onClick={() => onNavigate('previous')} keyText="Ctrl+Up">
|
||||||
|
Navigate to previous
|
||||||
|
</DropDownMenuItem>
|
||||||
|
<DropDownMenuItem onClick={() => onNavigate('next')} keyText="Ctrl+Down">
|
||||||
|
Navigate to next
|
||||||
|
</DropDownMenuItem>
|
||||||
|
<DropDownMenuItem onClick={() => onNavigate('end')} keyText="Ctrl+End">
|
||||||
|
Navigate to end
|
||||||
|
</DropDownMenuItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
packages/web/src/formview/FormViewToolbar.js
Normal file
42
packages/web/src/formview/FormViewToolbar.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ToolbarButton from '../widgets/ToolbarButton';
|
||||||
|
|
||||||
|
export default function FormViewToolbar({ switchToTable, onNavigate, reload, reconnect, former, save }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ToolbarButton onClick={switchToTable} icon="icon table">
|
||||||
|
Table view
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton onClick={() => onNavigate('begin')} icon="icon arrow-begin">
|
||||||
|
First
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton onClick={() => onNavigate('previous')} icon="icon arrow-left">
|
||||||
|
Previous
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton onClick={() => onNavigate('next')} icon="icon arrow-right">
|
||||||
|
Next
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton onClick={() => onNavigate('end')} icon="icon arrow-end">
|
||||||
|
Last
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton onClick={reload} icon="icon reload">
|
||||||
|
Refresh
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton onClick={reconnect} icon="icon connection">
|
||||||
|
Reconnect
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton disabled={!former.canUndo} onClick={() => former.undo()} icon="icon undo">
|
||||||
|
Undo
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton disabled={!former.canRedo} onClick={() => former.redo()} icon="icon redo">
|
||||||
|
Redo
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton disabled={!former.allowSave} onClick={save} icon="icon save">
|
||||||
|
Save
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton disabled={!former.containsChanges} onClick={() => former.revertAllChanges()} icon="icon close">
|
||||||
|
Revert
|
||||||
|
</ToolbarButton>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
packages/web/src/formview/Former.ts
Normal file
53
packages/web/src/formview/Former.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// export interface GriderRowStatus {
|
||||||
|
// status: 'regular' | 'updated' | 'deleted' | 'inserted';
|
||||||
|
// modifiedFields?: Set<string>;
|
||||||
|
// insertedFields?: Set<string>;
|
||||||
|
// deletedFields?: Set<string>;
|
||||||
|
// }
|
||||||
|
|
||||||
|
export default abstract class Former {
|
||||||
|
public rowData: any;
|
||||||
|
|
||||||
|
// getRowStatus(index): GriderRowStatus {
|
||||||
|
// const res: GriderRowStatus = {
|
||||||
|
// status: 'regular',
|
||||||
|
// };
|
||||||
|
// return res;
|
||||||
|
// }
|
||||||
|
beginUpdate() {}
|
||||||
|
endUpdate() {}
|
||||||
|
setCellValue(uniqueName: string, value: any) {}
|
||||||
|
revertRowChanges() {}
|
||||||
|
revertAllChanges() {}
|
||||||
|
undo() {}
|
||||||
|
redo() {}
|
||||||
|
get editable() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
get canInsert() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
get allowSave() {
|
||||||
|
return this.containsChanges;
|
||||||
|
}
|
||||||
|
get canUndo() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
get canRedo() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
get containsChanges() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
get disableLoadNextPage() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
get errors() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
updateRow(changeObject) {
|
||||||
|
for (const key of Object.keys(changeObject)) {
|
||||||
|
this.setCellValue(key, changeObject[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
180
packages/web/src/formview/SqlFormView.js
Normal file
180
packages/web/src/formview/SqlFormView.js
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import { changeSetToSql, createChangeSet, TableFormViewDisplay } from 'dbgate-datalib';
|
||||||
|
import { findEngineDriver } from 'dbgate-tools';
|
||||||
|
import React from 'react';
|
||||||
|
import { useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
|
||||||
|
import useExtensions from '../utility/useExtensions';
|
||||||
|
import FormView from './FormView';
|
||||||
|
import axios from '../utility/axios';
|
||||||
|
import ChangeSetFormer from './ChangeSetFormer';
|
||||||
|
import ConfirmSqlModal from '../modals/ConfirmSqlModal';
|
||||||
|
import ErrorMessageModal from '../modals/ErrorMessageModal';
|
||||||
|
import { scriptToSql } from 'dbgate-sqltree';
|
||||||
|
import useModalState from '../modals/useModalState';
|
||||||
|
import useShowModal from '../modals/showModal';
|
||||||
|
|
||||||
|
async function loadRow(props, sql) {
|
||||||
|
const { conid, database } = props;
|
||||||
|
|
||||||
|
if (!sql) return null;
|
||||||
|
|
||||||
|
const response = await axios.request({
|
||||||
|
url: 'database-connections/query-data',
|
||||||
|
method: 'post',
|
||||||
|
params: {
|
||||||
|
conid,
|
||||||
|
database,
|
||||||
|
},
|
||||||
|
data: { sql },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.errorMessage) return response.data;
|
||||||
|
return response.data.rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SqlFormView(props) {
|
||||||
|
const { formDisplay, changeSetState, dispatchChangeSet, conid, database, onReferenceSourceChanged } = props;
|
||||||
|
const [rowData, setRowData] = React.useState(null);
|
||||||
|
const [reloadToken, setReloadToken] = React.useState(0);
|
||||||
|
const [rowCountInfo, setRowCountInfo] = React.useState(null);
|
||||||
|
|
||||||
|
const confirmSqlModalState = useModalState();
|
||||||
|
const [confirmSql, setConfirmSql] = React.useState('');
|
||||||
|
const showModal = useShowModal();
|
||||||
|
|
||||||
|
const changeSet = changeSetState && changeSetState.value;
|
||||||
|
const changeSetRef = React.useRef(changeSet);
|
||||||
|
changeSetRef.current = changeSet;
|
||||||
|
|
||||||
|
const handleLoadCurrentRow = async () => {
|
||||||
|
const row = await loadRow(props, formDisplay.getCurrentRowQuery());
|
||||||
|
if (row) setRowData(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoadRowCount = async () => {
|
||||||
|
const countRow = await loadRow(props, formDisplay.getCountQuery());
|
||||||
|
const countBeforeRow = await loadRow(props, formDisplay.getBeforeCountQuery());
|
||||||
|
|
||||||
|
if (countRow && countBeforeRow) {
|
||||||
|
setRowCountInfo({
|
||||||
|
allRowCount: parseInt(countRow.count),
|
||||||
|
rowCountBefore: parseInt(countBeforeRow.count),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNavigate = async (command) => {
|
||||||
|
const row = await loadRow(props, formDisplay.navigateRowQuery(command));
|
||||||
|
if (row) {
|
||||||
|
setRowData(row);
|
||||||
|
formDisplay.navigate(row);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (onReferenceSourceChanged && rowData) onReferenceSourceChanged([rowData]);
|
||||||
|
}, [rowData]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (formDisplay) handleLoadCurrentRow();
|
||||||
|
setRowCountInfo(null);
|
||||||
|
handleLoadRowCount();
|
||||||
|
}, [reloadToken]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!formDisplay.isLoadedCorrectly) return;
|
||||||
|
|
||||||
|
if (formDisplay && !formDisplay.isLoadedCurrentRow(rowData)) {
|
||||||
|
handleLoadCurrentRow();
|
||||||
|
}
|
||||||
|
setRowCountInfo(null);
|
||||||
|
handleLoadRowCount();
|
||||||
|
}, [formDisplay]);
|
||||||
|
|
||||||
|
const former = React.useMemo(() => new ChangeSetFormer(rowData, changeSetState, dispatchChangeSet, formDisplay), [
|
||||||
|
rowData,
|
||||||
|
changeSetState,
|
||||||
|
dispatchChangeSet,
|
||||||
|
formDisplay,
|
||||||
|
]);
|
||||||
|
|
||||||
|
function handleSave() {
|
||||||
|
const script = changeSetToSql(changeSetRef.current, formDisplay.dbinfo);
|
||||||
|
const sql = scriptToSql(formDisplay.driver, script);
|
||||||
|
setConfirmSql(sql);
|
||||||
|
confirmSqlModalState.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConfirmSql() {
|
||||||
|
const resp = await axios.request({
|
||||||
|
url: 'database-connections/query-data',
|
||||||
|
method: 'post',
|
||||||
|
params: {
|
||||||
|
conid,
|
||||||
|
database,
|
||||||
|
},
|
||||||
|
data: { sql: confirmSql },
|
||||||
|
});
|
||||||
|
const { errorMessage } = resp.data || {};
|
||||||
|
if (errorMessage) {
|
||||||
|
showModal((modalState) => (
|
||||||
|
<ErrorMessageModal modalState={modalState} message={errorMessage} title="Error when saving" />
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
dispatchChangeSet({ type: 'reset', value: createChangeSet() });
|
||||||
|
setConfirmSql(null);
|
||||||
|
setReloadToken((x) => x + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// const { config, setConfig, cache, setCache, schemaName, pureName, conid, database } = props;
|
||||||
|
// const { formViewKey } = config;
|
||||||
|
|
||||||
|
// const [display, setDisplay] = React.useState(null);
|
||||||
|
|
||||||
|
// const connection = useConnectionInfo({ conid });
|
||||||
|
// const dbinfo = useDatabaseInfo({ conid, database });
|
||||||
|
// const extensions = useExtensions();
|
||||||
|
|
||||||
|
// console.log('SqlFormView.props', props);
|
||||||
|
|
||||||
|
// React.useEffect(() => {
|
||||||
|
// const newDisplay = connection
|
||||||
|
// ? new TableFormViewDisplay(
|
||||||
|
// { schemaName, pureName },
|
||||||
|
// findEngineDriver(connection, extensions),
|
||||||
|
// config,
|
||||||
|
// setConfig,
|
||||||
|
// cache,
|
||||||
|
// setCache,
|
||||||
|
// dbinfo
|
||||||
|
// )
|
||||||
|
// : null;
|
||||||
|
// if (!newDisplay) return;
|
||||||
|
// if (display && display.isLoadedCorrectly && !newDisplay.isLoadedCorrectly) return;
|
||||||
|
// setDisplay(newDisplay);
|
||||||
|
// }, [config, cache, conid, database, schemaName, pureName, dbinfo, extensions]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormView
|
||||||
|
{...props}
|
||||||
|
rowData={rowData}
|
||||||
|
onNavigate={handleNavigate}
|
||||||
|
former={former}
|
||||||
|
onSave={handleSave}
|
||||||
|
onReload={() => setReloadToken((x) => x + 1)}
|
||||||
|
onReconnect={async () => {
|
||||||
|
await axios.post('database-connections/refresh', { conid, database });
|
||||||
|
formDisplay.reload();
|
||||||
|
}}
|
||||||
|
{...rowCountInfo}
|
||||||
|
/>
|
||||||
|
<ConfirmSqlModal
|
||||||
|
modalState={confirmSqlModalState}
|
||||||
|
sql={confirmSql}
|
||||||
|
engine={formDisplay.engine}
|
||||||
|
onConfirm={handleConfirmSql}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -32,12 +32,15 @@ const iconNames = {
|
|||||||
'icon web': 'mdi mdi-web',
|
'icon web': 'mdi mdi-web',
|
||||||
'icon home': 'mdi mdi-home',
|
'icon home': 'mdi mdi-home',
|
||||||
'icon query-design': 'mdi mdi-vector-polyline-edit',
|
'icon query-design': 'mdi mdi-vector-polyline-edit',
|
||||||
|
'icon form': 'mdi mdi-form-select',
|
||||||
|
|
||||||
'icon edit': 'mdi mdi-pencil',
|
'icon edit': 'mdi mdi-pencil',
|
||||||
'icon delete': 'mdi mdi-delete',
|
'icon delete': 'mdi mdi-delete',
|
||||||
'icon arrow-up': 'mdi mdi-arrow-up',
|
'icon arrow-up': 'mdi mdi-arrow-up',
|
||||||
'icon arrow-down': 'mdi mdi-arrow-down',
|
'icon arrow-down': 'mdi mdi-arrow-down',
|
||||||
'icon arrow-left': 'mdi mdi-arrow-left',
|
'icon arrow-left': 'mdi mdi-arrow-left',
|
||||||
|
'icon arrow-begin': 'mdi mdi-arrow-collapse-left',
|
||||||
|
'icon arrow-end': 'mdi mdi-arrow-collapse-right',
|
||||||
'icon arrow-right': 'mdi mdi-arrow-right',
|
'icon arrow-right': 'mdi mdi-arrow-right',
|
||||||
'icon format-code': 'mdi mdi-code-tags-check',
|
'icon format-code': 'mdi mdi-code-tags-check',
|
||||||
'icon show-wizard': 'mdi mdi-comment-edit',
|
'icon show-wizard': 'mdi mdi-comment-edit',
|
||||||
|
|||||||
Reference in New Issue
Block a user