diff --git a/packages/api/src/controllers/databaseConnections.js b/packages/api/src/controllers/databaseConnections.js
index f8a322a4..a7394f0e 100644
--- a/packages/api/src/controllers/databaseConnections.js
+++ b/packages/api/src/controllers/databaseConnections.js
@@ -28,6 +28,7 @@ module.exports = {
handle_status(conid, database, { status }) {
const existing = this.opened.find((x) => x.conid == conid && x.database == database);
if (!existing) return;
+ if (existing.status == status) return;
existing.status = status;
socket.emitChanged(`database-status-changed-${conid}-${database}`);
},
diff --git a/packages/api/src/proc/databaseConnectionProcess.js b/packages/api/src/proc/databaseConnectionProcess.js
index 6611fcdc..58b96526 100644
--- a/packages/api/src/proc/databaseConnectionProcess.js
+++ b/packages/api/src/proc/databaseConnectionProcess.js
@@ -38,6 +38,7 @@ async function handleIncrementalRefresh() {
analysedStructure = newStructure;
process.send({ msgtype: 'structure', structure: analysedStructure });
}
+ setStatusName('ok');
}
function setStatus(status) {
diff --git a/packages/datalib/src/FormViewDisplay.ts b/packages/datalib/src/FormViewDisplay.ts
new file mode 100644
index 00000000..cfbe5cd1
--- /dev/null
+++ b/packages/datalib/src/FormViewDisplay.ts
@@ -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
+ ) {}
+
+}
diff --git a/packages/datalib/src/GridConfig.ts b/packages/datalib/src/GridConfig.ts
index 13d7a11b..80a37edd 100644
--- a/packages/datalib/src/GridConfig.ts
+++ b/packages/datalib/src/GridConfig.ts
@@ -29,6 +29,8 @@ export interface GridConfig extends GridConfigColumns {
grouping: { [uniqueName: string]: GroupFunc };
childConfig?: GridConfig;
reference?: GridReferenceDefinition;
+ isFormView?: boolean;
+ formViewKey?: { [uniqueName: string]: string };
}
export interface GridCache {
diff --git a/packages/datalib/src/GridDisplay.ts b/packages/datalib/src/GridDisplay.ts
index 2c4d638d..66d86e16 100644
--- a/packages/datalib/src/GridDisplay.ts
+++ b/packages/datalib/src/GridDisplay.ts
@@ -518,4 +518,20 @@ export abstract class GridDisplay {
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)
+ ),
+ }));
+ }
}
diff --git a/packages/datalib/src/TableFormViewDisplay.ts b/packages/datalib/src/TableFormViewDisplay.ts
new file mode 100644
index 00000000..b7447383
--- /dev/null
+++ b/packages/datalib/src/TableFormViewDisplay.ts
@@ -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,
+ };
+ }
+}
diff --git a/packages/datalib/src/index.ts b/packages/datalib/src/index.ts
index 5185f22f..04472789 100644
--- a/packages/datalib/src/index.ts
+++ b/packages/datalib/src/index.ts
@@ -1,11 +1,13 @@
-export * from "./GridDisplay";
-export * from "./GridConfig";
-export * from "./TableGridDisplay";
-export * from "./ViewGridDisplay";
-export * from "./JslGridDisplay";
-export * from "./ChangeSet";
-export * from "./filterName";
-export * from "./FreeTableGridDisplay";
-export * from "./FreeTableModel";
-export * from "./MacroDefinition";
-export * from "./runMacro";
+export * from './GridDisplay';
+export * from './GridConfig';
+export * from './TableGridDisplay';
+export * from './ViewGridDisplay';
+export * from './JslGridDisplay';
+export * from './ChangeSet';
+export * from './filterName';
+export * from './FreeTableGridDisplay';
+export * from './FreeTableModel';
+export * from './MacroDefinition';
+export * from './runMacro';
+export * from './FormViewDisplay';
+export * from './TableFormViewDisplay';
diff --git a/packages/web/src/celldata/CellDataView.js b/packages/web/src/celldata/CellDataView.js
index 9f8aa773..c79a3904 100644
--- a/packages/web/src/celldata/CellDataView.js
+++ b/packages/web/src/celldata/CellDataView.js
@@ -52,15 +52,18 @@ function autodetect(selection, grider, value) {
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 theme = useTheme();
let value = null;
- if (grider && selection.length == 1) {
+ if (grider && selection && selection.length == 1) {
const rowData = grider.getRowData(selection[0].row);
const { column } = selection[0];
if (rowData) value = rowData[column];
}
+ if (selectedValue) {
+ value = selectedValue;
+ }
const autodetectFormatType = React.useMemo(() => autodetect(selection, grider, value), [selection, grider, value]);
const autodetectFormat = formats.find((x) => x.type == autodetectFormatType);
diff --git a/packages/web/src/datagrid/DataGrid.js b/packages/web/src/datagrid/DataGrid.js
index 383a88a3..125bd458 100644
--- a/packages/web/src/datagrid/DataGrid.js
+++ b/packages/web/src/datagrid/DataGrid.js
@@ -21,31 +21,50 @@ const DataGridContainer = styled.div`
`;
export default function DataGrid(props) {
- const { GridCore } = props;
+ const { GridCore, FormView, config, formDisplay } = props;
const theme = useTheme();
const [managerSize, setManagerSize] = React.useState(0);
const [selection, setSelection] = React.useState([]);
+ const [formSelection, setFormSelection] = React.useState(null);
const [grider, setGrider] = React.useState(null);
+ // const [formViewData, setFormViewData] = React.useState(null);
+ const isFormView = !!(config && config.isFormView);
+
return (
-
-
-
+ {!isFormView && (
+
+
+
+ )}
{props.showReferences && props.display.hasReferences && (
)}
-
+ {isFormView ? (
+
+ ) : (
+
+ )}
-
+ {isFormView ? (
+
+ ) : (
+
+ )}
);
diff --git a/packages/web/src/datagrid/DataGridContextMenu.js b/packages/web/src/datagrid/DataGridContextMenu.js
index 1201c95a..f95dbff3 100644
--- a/packages/web/src/datagrid/DataGridContextMenu.js
+++ b/packages/web/src/datagrid/DataGridContextMenu.js
@@ -14,6 +14,7 @@ export default function DataGridContextMenu({
openFreeTable,
openChartSelection,
openActiveChart,
+ switchToForm,
}) {
return (
<>
@@ -57,6 +58,11 @@ export default function DataGridContextMenu({
Open selection in free table editor
Open chart from selection
{openActiveChart && Open active chart}
+ {!!switchToForm && (
+
+ Form view
+
+ )}
>
);
}
diff --git a/packages/web/src/datagrid/DataGridCore.js b/packages/web/src/datagrid/DataGridCore.js
index c3a97520..d68ccb13 100644
--- a/packages/web/src/datagrid/DataGridCore.js
+++ b/packages/web/src/datagrid/DataGridCore.js
@@ -116,6 +116,7 @@ export default function DataGridCore(props) {
onSelectionChanged,
frameSelection,
onKeyDown,
+ formViewAvailable,
} = props;
// console.log('RENDER GRID', display.baseTable.pureName);
const columns = React.useMemo(() => display.allColumns, [display]);
@@ -381,6 +382,7 @@ export default function DataGridCore(props) {
openFreeTable={handleOpenFreeTable}
openChartSelection={handleOpenChart}
openActiveChart={openActiveChart}
+ switchToForm={handleSwitchToFormView}
/>
);
};
@@ -719,6 +721,11 @@ export default function DataGridCore(props) {
display.reload();
}
+ if (event.keyCode == keycodes.f4) {
+ event.preventDefault();
+ handleSwitchToFormView();
+ }
+
if (event.keyCode == keycodes.s && event.ctrlKey) {
event.preventDefault();
handleSave();
@@ -942,6 +949,24 @@ export default function DataGridCore(props) {
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(
// 'gridScrollAreaWidth / columnSizes.getVisibleScrollSizeSum()',
@@ -1047,6 +1072,7 @@ export default function DataGridCore(props) {
display={display}
focusedColumn={display.focusedColumn}
frameSelection={frameSelection}
+ onSetFormView={handleSetFormView}
/>
)
)}
@@ -1081,6 +1107,7 @@ export default function DataGridCore(props) {
await axios.post('database-connections/refresh', { conid, database });
display.reload();
}}
+ switchToForm={handleSwitchToFormView}
/>,
props.toolbarPortalRef.current
)}
diff --git a/packages/web/src/datagrid/DataGridRow.js b/packages/web/src/datagrid/DataGridRow.js
index cecb5001..690a924f 100644
--- a/packages/web/src/datagrid/DataGridRow.js
+++ b/packages/web/src/datagrid/DataGridRow.js
@@ -7,6 +7,7 @@ import InplaceEditor from './InplaceEditor';
import { cellIsSelected } from './gridutil';
import { isTypeLogical } from 'dbgate-tools';
import useTheme from '../theme/useTheme';
+import { FontIcon } from '../icons';
const TableBodyCell = styled.td`
font-weight: normal;
@@ -114,6 +115,7 @@ const TableHeaderCell = styled.td`
padding: 2px;
background-color: ${(props) => props.theme.gridheader_background};
overflow: hidden;
+ position: relative;
`;
const AutoFillPoint = styled.div`
@@ -127,6 +129,16 @@ const AutoFillPoint = styled.div`
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) {
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?$/;
-function CellFormattedValue({ value, dataType }) {
+export function CellFormattedValue({ value, dataType }) {
if (value == null) return (NULL);
if (_.isDate(value)) return moment(value).format('YYYY-MM-DD HH:mm:ss');
if (value === true) return '1';
@@ -167,6 +179,33 @@ function CellFormattedValue({ value, dataType }) {
return value.toString();
}
+function RowHeaderCell({ rowIndex, theme, onSetFormView, rowData }) {
+ const [mouseIn, setMouseIn] = React.useState(false);
+
+ return (
+ setMouseIn(true) : null}
+ onMouseLeave={onSetFormView ? () => setMouseIn(false) : null}
+ >
+ {rowIndex + 1}
+ {!!onSetFormView && mouseIn && (
+ {
+ e.stopPropagation();
+ onSetFormView(rowData);
+ }}
+ >
+
+
+ )}
+
+ );
+}
+
/** @param props {import('./types').DataGridProps} */
function DataGridRow(props) {
const {
@@ -181,6 +220,7 @@ function DataGridRow(props) {
focusedColumn,
grider,
frameSelection,
+ onSetFormView,
} = props;
// usePropsCompare({
// rowHeight,
@@ -217,9 +257,8 @@ function DataGridRow(props) {
return (
-
- {rowIndex + 1}
-
+
+
{visibleRealColumns.map((col) => (
grider.setCellValue(rowIndex, col.uniqueName, value)}
/>
) : (
<>
diff --git a/packages/web/src/datagrid/DataGridToolbar.js b/packages/web/src/datagrid/DataGridToolbar.js
index 180f931a..800aa858 100644
--- a/packages/web/src/datagrid/DataGridToolbar.js
+++ b/packages/web/src/datagrid/DataGridToolbar.js
@@ -1,9 +1,14 @@
import React from 'react';
import ToolbarButton from '../widgets/ToolbarButton';
-export default function DataGridToolbar({ reload, reconnect, grider, save }) {
+export default function DataGridToolbar({ reload, reconnect, grider, save, switchToForm }) {
return (
<>
+ {switchToForm && (
+
+ Form view
+
+ )}
Refresh
diff --git a/packages/web/src/datagrid/InplaceEditor.js b/packages/web/src/datagrid/InplaceEditor.js
index 645c5822..e32ab10e 100644
--- a/packages/web/src/datagrid/InplaceEditor.js
+++ b/packages/web/src/datagrid/InplaceEditor.js
@@ -14,14 +14,16 @@ const StyledInput = styled.input`
export default function InplaceEditor({
widthPx,
- rowIndex,
- uniqueName,
- grider,
+ // rowIndex,
+ // uniqueName,
+ // grider,
cellValue,
inplaceEditorState,
dispatchInsplaceEditor,
+ onSetValue,
}) {
const editorRef = React.useRef();
+ const widthRef = React.useRef(widthPx);
const isChangedRef = React.useRef(!!inplaceEditorState.text);
React.useEffect(() => {
const editor = editorRef.current;
@@ -34,7 +36,8 @@ export default function InplaceEditor({
function handleBlur() {
if (isChangedRef.current) {
const editor = editorRef.current;
- grider.setCellValue(rowIndex, uniqueName, editor.value);
+ onSetValue(editor.value);
+ // grider.setCellValue(rowIndex, uniqueName, editor.value);
isChangedRef.current = false;
}
dispatchInsplaceEditor({ type: 'close' });
@@ -42,7 +45,8 @@ export default function InplaceEditor({
if (inplaceEditorState.shouldSave) {
const editor = editorRef.current;
if (isChangedRef.current) {
- grider.setCellValue(rowIndex, uniqueName, editor.value);
+ onSetValue(editor.value);
+ // grider.setCellValue(rowIndex, uniqueName, editor.value);
isChangedRef.current = false;
}
editor.blur();
@@ -57,7 +61,8 @@ export default function InplaceEditor({
break;
case keycodes.enter:
if (isChangedRef.current) {
- grider.setCellValue(rowIndex, uniqueName, editor.value);
+ // grider.setCellValue(rowIndex, uniqueName, editor.value);
+ onSetValue(editor.value);
isChangedRef.current = false;
}
editor.blur();
@@ -66,7 +71,8 @@ export default function InplaceEditor({
case keycodes.s:
if (event.ctrlKey) {
if (isChangedRef.current) {
- grider.setCellValue(rowIndex, uniqueName, editor.value);
+ onSetValue(editor.value);
+ // grider.setCellValue(rowIndex, uniqueName, editor.value);
isChangedRef.current = false;
}
event.preventDefault();
@@ -83,9 +89,9 @@ export default function InplaceEditor({
onChange={() => (isChangedRef.current = true)}
onKeyDown={handleKeyDown}
style={{
- width: widthPx,
- minWidth: widthPx,
- maxWidth: widthPx,
+ width: widthRef.current,
+ minWidth: widthRef.current,
+ maxWidth: widthRef.current,
}}
/>
);
diff --git a/packages/web/src/datagrid/TableDataGrid.js b/packages/web/src/datagrid/TableDataGrid.js
index 1af9b587..0c117faa 100644
--- a/packages/web/src/datagrid/TableDataGrid.js
+++ b/packages/web/src/datagrid/TableDataGrid.js
@@ -2,7 +2,7 @@ import React from 'react';
import _ from 'lodash';
import DataGrid from './DataGrid';
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 { findEngineDriver } from 'dbgate-tools';
import { useConnectionInfo, getTableInfo, useDatabaseInfo } from '../utility/metadataLoaders';
@@ -12,6 +12,7 @@ import stableStringify from 'json-stable-stringify';
import ReferenceHeader from './ReferenceHeader';
import SqlDataGridCore from './SqlDataGridCore';
import useExtensions from '../utility/useExtensions';
+import SqlFormView from '../formview/SqlFormView';
const ReferenceContainer = styled.div`
position: absolute;
@@ -87,7 +88,22 @@ export default function TableDataGrid({
: 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 [formDisplay, setFormDisplay] = React.useState(createFormDisplay());
React.useEffect(() => {
setRefReloadToken((v) => v + 1);
@@ -101,6 +117,13 @@ export default function TableDataGrid({
setDisplay(newDisplay);
}, [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(() => {
(setCache || setMyCache)(createGridCache());
}, []);
@@ -158,9 +181,12 @@ export default function TableDataGrid({
x.pureName == pureName && x.schemaName == schemaName)
+ // }
/>
{reference && (
diff --git a/packages/web/src/formview/ChangeSetFormer.ts b/packages/web/src/formview/ChangeSetFormer.ts
new file mode 100644
index 00000000..739ce111
--- /dev/null
+++ b/packages/web/src/formview/ChangeSetFormer.ts
@@ -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);
+ }
+}
diff --git a/packages/web/src/formview/FormView.js b/packages/web/src/formview/FormView.js
new file mode 100644
index 00000000..47dfd903
--- /dev/null
+++ b/packages/web/src/formview/FormView.js
@@ -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,
+
+ );
+ };
+
+ 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(
+ ,
+ toolbarPortalRef.current
+ );
+
+ if (!formDisplay || !formDisplay.isLoadedCorrectly) return toolbar;
+
+ return (
+
+ {columnChunks.map((chunk, chunkIndex) => (
+
+ {chunk.map((col, rowIndex) => (
+
+ setCellRef(rowIndex, chunkIndex * 2, element)}
+ >
+
+
+ setCellRef(rowIndex, chunkIndex * 2 + 1, element)}
+ >
+ {inplaceEditorState.cell &&
+ rowIndex == inplaceEditorState.cell[0] &&
+ chunkIndex * 2 + 1 == inplaceEditorState.cell[1] ? (
+ {
+ former.setCellValue(col.uniqueName, value);
+ }}
+ // grider={grider}
+ // rowIndex={rowIndex}
+ // uniqueName={col.uniqueName}
+ />
+ ) : (
+ <>
+
+ {!!col.hintColumnName &&
+ rowData &&
+ !(rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)) && (
+ {rowData[col.hintColumnName]}
+ )}
+ >
+ )}
+
+
+ ))}
+
+ ))}
+
+
+ {rowCountInfo && {rowCountInfo}}
+
+ {toolbar}
+
+ );
+}
diff --git a/packages/web/src/formview/FormViewContextMenu.js b/packages/web/src/formview/FormViewContextMenu.js
new file mode 100644
index 00000000..02e25012
--- /dev/null
+++ b/packages/web/src/formview/FormViewContextMenu.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
+
+export default function FormViewContextMenu({ switchToTable, onNavigate }) {
+ return (
+ <>
+
+ Table view
+
+
+ onNavigate('begin')} keyText="Ctrl+Home">
+ Navigate to begin
+
+ onNavigate('previous')} keyText="Ctrl+Up">
+ Navigate to previous
+
+ onNavigate('next')} keyText="Ctrl+Down">
+ Navigate to next
+
+ onNavigate('end')} keyText="Ctrl+End">
+ Navigate to end
+
+ >
+ );
+}
diff --git a/packages/web/src/formview/FormViewToolbar.js b/packages/web/src/formview/FormViewToolbar.js
new file mode 100644
index 00000000..d418a684
--- /dev/null
+++ b/packages/web/src/formview/FormViewToolbar.js
@@ -0,0 +1,42 @@
+import React from 'react';
+import ToolbarButton from '../widgets/ToolbarButton';
+
+export default function FormViewToolbar({ switchToTable, onNavigate, reload, reconnect, former, save }) {
+ return (
+ <>
+
+ Table view
+
+ onNavigate('begin')} icon="icon arrow-begin">
+ First
+
+ onNavigate('previous')} icon="icon arrow-left">
+ Previous
+
+ onNavigate('next')} icon="icon arrow-right">
+ Next
+
+ onNavigate('end')} icon="icon arrow-end">
+ Last
+
+
+ Refresh
+
+
+ Reconnect
+
+ former.undo()} icon="icon undo">
+ Undo
+
+ former.redo()} icon="icon redo">
+ Redo
+
+
+ Save
+
+ former.revertAllChanges()} icon="icon close">
+ Revert
+
+ >
+ );
+}
diff --git a/packages/web/src/formview/Former.ts b/packages/web/src/formview/Former.ts
new file mode 100644
index 00000000..cbd1508d
--- /dev/null
+++ b/packages/web/src/formview/Former.ts
@@ -0,0 +1,53 @@
+// export interface GriderRowStatus {
+// status: 'regular' | 'updated' | 'deleted' | 'inserted';
+// modifiedFields?: Set;
+// insertedFields?: Set;
+// deletedFields?: Set;
+// }
+
+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]);
+ }
+ }
+}
diff --git a/packages/web/src/formview/SqlFormView.js b/packages/web/src/formview/SqlFormView.js
new file mode 100644
index 00000000..9df44fef
--- /dev/null
+++ b/packages/web/src/formview/SqlFormView.js
@@ -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) => (
+
+ ));
+ } 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 (
+ <>
+ setReloadToken((x) => x + 1)}
+ onReconnect={async () => {
+ await axios.post('database-connections/refresh', { conid, database });
+ formDisplay.reload();
+ }}
+ {...rowCountInfo}
+ />
+
+ >
+ );
+}
diff --git a/packages/web/src/icons.js b/packages/web/src/icons.js
index 143231dc..369ce50e 100644
--- a/packages/web/src/icons.js
+++ b/packages/web/src/icons.js
@@ -32,12 +32,15 @@ const iconNames = {
'icon web': 'mdi mdi-web',
'icon home': 'mdi mdi-home',
'icon query-design': 'mdi mdi-vector-polyline-edit',
+ 'icon form': 'mdi mdi-form-select',
'icon edit': 'mdi mdi-pencil',
'icon delete': 'mdi mdi-delete',
'icon arrow-up': 'mdi mdi-arrow-up',
'icon arrow-down': 'mdi mdi-arrow-down',
'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 format-code': 'mdi mdi-code-tags-check',
'icon show-wizard': 'mdi mdi-comment-edit',