diff --git a/packages/datalib/src/TableFormViewDisplay.ts b/packages/datalib/src/TableFormViewDisplay.ts index 0554fab06..9b7f90ec7 100644 --- a/packages/datalib/src/TableFormViewDisplay.ts +++ b/packages/datalib/src/TableFormViewDisplay.ts @@ -3,9 +3,18 @@ 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 } from 'dbgate-sqltree'; +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'; export class TableFormViewDisplay extends FormViewDisplay { public table: TableInfo; @@ -28,7 +37,7 @@ export class TableFormViewDisplay extends FormViewDisplay { this.columns = this.gridDisplay.columns; } - getPrimaryKeyCondition(): Condition { + getPrimaryKeyEqualCondition(): Condition { if (!this.config.formViewKey) return null; return { conditionType: 'and', @@ -50,12 +59,130 @@ export class TableFormViewDisplay extends FormViewDisplay { }; } - getCurrentRowQuery() { + getPrimaryKeyOperatorCondition(operator): Condition { + if (!this.config.formViewKey) return null; + const conditions = []; + + const { primaryKey } = this.gridDisplay.baseTable; + 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; - select.where = mergeConditions(select.where, this.getPrimaryKeyCondition()); + 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; + } + + extractKey(row) { + 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) { + 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; } diff --git a/packages/web/src/formview/FormView.js b/packages/web/src/formview/FormView.js index 158e74d14..48d3dfa00 100644 --- a/packages/web/src/formview/FormView.js +++ b/packages/web/src/formview/FormView.js @@ -62,7 +62,7 @@ const NullSpan = styled.span` `; export default function FormView(props) { - const { rowData, toolbarPortalRef, tabVisible, config, setConfig } = props; + const { rowData, toolbarPortalRef, tabVisible, config, setConfig, onNavigate } = props; /** @type {import('dbgate-datalib').FormViewDisplay} */ const formDisplay = props.formDisplay; const theme = useTheme(); @@ -81,7 +81,10 @@ export default function FormView(props) { toolbarPortalRef && toolbarPortalRef.current && tabVisible && - ReactDOM.createPortal(, toolbarPortalRef.current); + ReactDOM.createPortal( + , + toolbarPortalRef.current + ); // console.log('display', display); diff --git a/packages/web/src/formview/FormViewToolbar.js b/packages/web/src/formview/FormViewToolbar.js index 280c0124a..a0d070f0b 100644 --- a/packages/web/src/formview/FormViewToolbar.js +++ b/packages/web/src/formview/FormViewToolbar.js @@ -1,12 +1,24 @@ import React from 'react'; import ToolbarButton from '../widgets/ToolbarButton'; -export default function FormViewToolbar({ switchToTable }) { +export default function FormViewToolbar({ switchToTable, onNavigate }) { 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 + ); } diff --git a/packages/web/src/formview/SqlFormView.js b/packages/web/src/formview/SqlFormView.js index e8ca69b2c..9f252e2ed 100644 --- a/packages/web/src/formview/SqlFormView.js +++ b/packages/web/src/formview/SqlFormView.js @@ -6,11 +6,10 @@ import useExtensions from '../utility/useExtensions'; import FormView from './FormView'; import axios from '../utility/axios'; -async function loadCurrentRow(props) { - const { formDisplay, conid, database } = props; +async function loadRow(props, sql) { + const { conid, database } = props; /** @type {import('dbgate-datalib').TableFormViewDisplay} */ - - const sql = formDisplay.getCurrentRowQuery(); + const formDisplay = props.formDisplay; const response = await axios.request({ url: 'database-connections/query-data', @@ -31,12 +30,22 @@ export default function SqlFormView(props) { const [rowData, setRowData] = React.useState(null); const handleLoadCurrentRow = async () => { - const row = await loadCurrentRow(props); + const row = await loadRow(props, formDisplay.getCurrentRowQuery()); if (row) setRowData(row); }; + const handleNavigate = async (command) => { + const row = await loadRow(props, formDisplay.navigateRowQuery(command)); + if (row) { + setRowData(row); + formDisplay.navigate(row); + } + }; + React.useEffect(() => { - handleLoadCurrentRow(); + if (formDisplay && !formDisplay.isLoadedCurrentRow(rowData)) { + handleLoadCurrentRow(); + } }, [formDisplay]); // const { config, setConfig, cache, setCache, schemaName, pureName, conid, database } = props; @@ -67,5 +76,5 @@ export default function SqlFormView(props) { // setDisplay(newDisplay); // }, [config, cache, conid, database, schemaName, pureName, dbinfo, extensions]); - return ; + return ; } diff --git a/packages/web/src/icons.js b/packages/web/src/icons.js index 357bde17b..369ce50e8 100644 --- a/packages/web/src/icons.js +++ b/packages/web/src/icons.js @@ -39,6 +39,8 @@ const iconNames = { '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',