From 03c7bbb7aface727f302a1cef64211ee788f008a Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sat, 9 Jan 2021 16:35:36 +0100 Subject: [PATCH 01/15] formview - basic display --- packages/web/src/datagrid/DataGrid.js | 36 +++++++-- packages/web/src/datagrid/DataGridCore.js | 2 + packages/web/src/datagrid/DataGridRow.js | 45 ++++++++++- packages/web/src/datagrid/TableDataGrid.js | 5 ++ packages/web/src/formview/FormView.js | 88 ++++++++++++++++++++++ packages/web/src/formview/SqlFormView.js | 6 ++ packages/web/src/icons.js | 1 + 7 files changed, 172 insertions(+), 11 deletions(-) create mode 100644 packages/web/src/formview/FormView.js create mode 100644 packages/web/src/formview/SqlFormView.js diff --git a/packages/web/src/datagrid/DataGrid.js b/packages/web/src/datagrid/DataGrid.js index 383a88a3..226bf423 100644 --- a/packages/web/src/datagrid/DataGrid.js +++ b/packages/web/src/datagrid/DataGrid.js @@ -21,31 +21,51 @@ const DataGridContainer = styled.div` `; export default function DataGrid(props) { - const { GridCore } = props; + const { GridCore, FormView } = props; const theme = useTheme(); const [managerSize, setManagerSize] = React.useState(0); const [selection, setSelection] = React.useState([]); const [grider, setGrider] = React.useState(null); + const [formViewData, setFormViewData] = React.useState(null); + const isFormView = !!formViewData; + + const handleSetFormView = (rowData) => { + setFormViewData(rowData); + }; + return ( - - - + {!isFormView && ( + + + + )} {props.showReferences && props.display.hasReferences && ( )} - - - + {!isFormView && ( + + + + )} - + {isFormView ? ( + + ) : ( + + )} ); diff --git a/packages/web/src/datagrid/DataGridCore.js b/packages/web/src/datagrid/DataGridCore.js index c3a97520..a97ce1c0 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, + onSetFormView, } = props; // console.log('RENDER GRID', display.baseTable.pureName); const columns = React.useMemo(() => display.allColumns, [display]); @@ -1047,6 +1048,7 @@ export default function DataGridCore(props) { display={display} focusedColumn={display.focusedColumn} frameSelection={frameSelection} + onSetFormView={onSetFormView} /> ) )} diff --git a/packages/web/src/datagrid/DataGridRow.js b/packages/web/src/datagrid/DataGridRow.js index cecb5001..d213162e 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, '•'); } @@ -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) => ( x.pureName == pureName && x.schemaName == schemaName) + } /> {reference && ( diff --git a/packages/web/src/formview/FormView.js b/packages/web/src/formview/FormView.js new file mode 100644 index 00000000..457a5be2 --- /dev/null +++ b/packages/web/src/formview/FormView.js @@ -0,0 +1,88 @@ +import _ from 'lodash'; +import React from 'react'; +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'; + +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; +`; + +const TableBodyCell = styled.td` + font-weight: normal; + border: 1px solid ${(props) => props.theme.border}; + // border-collapse: collapse; + padding: 2px; + white-space: nowrap; + position: relative; + overflow: hidden; +`; + +const HintSpan = styled.span` + color: gray; + margin-left: 5px; +`; +const NullSpan = styled.span` + color: gray; + font-style: italic; +`; + +export default function FormView({ tableInfo, rowData }) { + const theme = useTheme(); + const [headerRowRef, { height: rowHeight }] = useDimensions(); + const [wrapperRef, { height: wrapperHeight }] = useDimensions(); + + if (!tableInfo || !rowData) return null; + + const rowCount = Math.floor((wrapperHeight - 20) / rowHeight); + const columnChunks = _.chunk(tableInfo.columns, rowCount); + + return ( + + {columnChunks.map((chunk, index) => ( + + {chunk.map((col) => ( + + + + + {rowData[col.columnName]} + + ))} +
+ ))} +
+ ); +} diff --git a/packages/web/src/formview/SqlFormView.js b/packages/web/src/formview/SqlFormView.js new file mode 100644 index 00000000..aa0f46c5 --- /dev/null +++ b/packages/web/src/formview/SqlFormView.js @@ -0,0 +1,6 @@ +import React from 'react'; +import FormView from './FormView'; + +export default function SqlFormView({ rowData, tableInfo }) { + return ; +} diff --git a/packages/web/src/icons.js b/packages/web/src/icons.js index 143231dc..357bde17 100644 --- a/packages/web/src/icons.js +++ b/packages/web/src/icons.js @@ -32,6 +32,7 @@ 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', From bb35a496f841764924bc62d890cc2614a35a8cbb Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sat, 9 Jan 2021 16:55:47 +0100 Subject: [PATCH 02/15] form view toolbar --- packages/web/src/datagrid/DataGrid.js | 2 +- packages/web/src/formview/FormView.js | 9 ++++++++- packages/web/src/formview/FormViewToolbar.js | 12 ++++++++++++ packages/web/src/formview/SqlFormView.js | 4 ++-- 4 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 packages/web/src/formview/FormViewToolbar.js diff --git a/packages/web/src/datagrid/DataGrid.js b/packages/web/src/datagrid/DataGrid.js index 226bf423..3ccb1400 100644 --- a/packages/web/src/datagrid/DataGrid.js +++ b/packages/web/src/datagrid/DataGrid.js @@ -57,7 +57,7 @@ export default function DataGrid(props) { {isFormView ? ( - + setFormViewData(null)} /> ) : ( ))} + + {toolbarPortalRef && + toolbarPortalRef.current && + tabVisible && + ReactDOM.createPortal(, toolbarPortalRef.current)} ); } diff --git a/packages/web/src/formview/FormViewToolbar.js b/packages/web/src/formview/FormViewToolbar.js new file mode 100644 index 00000000..280c0124 --- /dev/null +++ b/packages/web/src/formview/FormViewToolbar.js @@ -0,0 +1,12 @@ +import React from 'react'; +import ToolbarButton from '../widgets/ToolbarButton'; + +export default function FormViewToolbar({ switchToTable }) { + return ( + <> + + Table view + + + ); +} diff --git a/packages/web/src/formview/SqlFormView.js b/packages/web/src/formview/SqlFormView.js index aa0f46c5..15528424 100644 --- a/packages/web/src/formview/SqlFormView.js +++ b/packages/web/src/formview/SqlFormView.js @@ -1,6 +1,6 @@ import React from 'react'; import FormView from './FormView'; -export default function SqlFormView({ rowData, tableInfo }) { - return ; +export default function SqlFormView(props) { + return ; } From b71b58c93fea6588f5f32723ce680dd58d3b4e54 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sat, 9 Jan 2021 20:37:49 +0100 Subject: [PATCH 03/15] form view infrastructure, loading row from DB --- packages/datalib/src/FormViewDisplay.ts | 23 +++++++ packages/datalib/src/GridConfig.ts | 2 + packages/datalib/src/GridDisplay.ts | 16 +++++ packages/datalib/src/TableFormViewDisplay.ts | 62 ++++++++++++++++++ packages/datalib/src/index.ts | 24 +++---- packages/web/src/datagrid/DataGrid.js | 14 ++-- packages/web/src/datagrid/DataGridCore.js | 11 +++- packages/web/src/datagrid/TableDataGrid.js | 33 ++++++++-- packages/web/src/formview/FormView.js | 34 +++++++--- packages/web/src/formview/SqlFormView.js | 67 +++++++++++++++++++- 10 files changed, 250 insertions(+), 36 deletions(-) create mode 100644 packages/datalib/src/FormViewDisplay.ts create mode 100644 packages/datalib/src/TableFormViewDisplay.ts diff --git a/packages/datalib/src/FormViewDisplay.ts b/packages/datalib/src/FormViewDisplay.ts new file mode 100644 index 00000000..76634057 --- /dev/null +++ b/packages/datalib/src/FormViewDisplay.ts @@ -0,0 +1,23 @@ +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[]; + + 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..0554fab0 --- /dev/null +++ b/packages/datalib/src/TableFormViewDisplay.ts @@ -0,0 +1,62 @@ +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 } from 'dbgate-sqltree'; +import { filterName } from './filterName'; +import { TableGridDisplay } from './TableGridDisplay'; + +export class TableFormViewDisplay extends FormViewDisplay { + public table: TableInfo; + // 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; + } + + getPrimaryKeyCondition(): Condition { + if (!this.config.formViewKey) return null; + return { + conditionType: 'and', + conditions: _.keys(this.config.formViewKey).map((columnName) => ({ + conditionType: 'binary', + operator: '=', + left: { + exprType: 'column', + columnName, + source: { + alias: 'basetbl', + }, + }, + right: { + exprType: 'value', + value: this.config.formViewKey[columnName], + }, + })), + }; + } + + getCurrentRowQuery() { + if (!this.driver) return null; + const select = this.gridDisplay.createSelect(); + if (!select) return null; + select.topRecords = 1; + select.where = mergeConditions(select.where, this.getPrimaryKeyCondition()); + const sql = treeToSql(this.driver, select, dumpSqlSelect); + return sql; + } +} 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/datagrid/DataGrid.js b/packages/web/src/datagrid/DataGrid.js index 3ccb1400..be578cc3 100644 --- a/packages/web/src/datagrid/DataGrid.js +++ b/packages/web/src/datagrid/DataGrid.js @@ -21,17 +21,13 @@ const DataGridContainer = styled.div` `; export default function DataGrid(props) { - const { GridCore, FormView } = props; + const { GridCore, FormView, config, formDisplay } = props; const theme = useTheme(); const [managerSize, setManagerSize] = React.useState(0); const [selection, setSelection] = React.useState([]); const [grider, setGrider] = React.useState(null); - const [formViewData, setFormViewData] = React.useState(null); - const isFormView = !!formViewData; - - const handleSetFormView = (rowData) => { - setFormViewData(rowData); - }; + // const [formViewData, setFormViewData] = React.useState(null); + const isFormView = !!(config && config.isFormView); return ( @@ -57,13 +53,13 @@ export default function DataGrid(props) { {isFormView ? ( - setFormViewData(null)} /> + ) : ( )} diff --git a/packages/web/src/datagrid/DataGridCore.js b/packages/web/src/datagrid/DataGridCore.js index a97ce1c0..011bb54f 100644 --- a/packages/web/src/datagrid/DataGridCore.js +++ b/packages/web/src/datagrid/DataGridCore.js @@ -116,7 +116,7 @@ export default function DataGridCore(props) { onSelectionChanged, frameSelection, onKeyDown, - onSetFormView, + formViewAvailable, } = props; // console.log('RENDER GRID', display.baseTable.pureName); const columns = React.useMemo(() => display.allColumns, [display]); @@ -943,6 +943,13 @@ export default function DataGridCore(props) { display.clearFilters(); }; + const handleSetFormView = + formViewAvailable && display.baseTable && display.baseTable.primaryKey + ? (rowData) => { + display.switchToFormView(rowData); + } + : null; + // console.log('visibleRealColumnIndexes', visibleRealColumnIndexes); // console.log( // 'gridScrollAreaWidth / columnSizes.getVisibleScrollSizeSum()', @@ -1048,7 +1055,7 @@ export default function DataGridCore(props) { display={display} focusedColumn={display.focusedColumn} frameSelection={frameSelection} - onSetFormView={onSetFormView} + onSetFormView={handleSetFormView} /> ) )} diff --git a/packages/web/src/datagrid/TableDataGrid.js b/packages/web/src/datagrid/TableDataGrid.js index 9ecce4f5..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'; @@ -88,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); @@ -102,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()); }, []); @@ -159,9 +181,12 @@ export default function TableDataGrid({ x.pureName == pureName && x.schemaName == schemaName) - } + // tableInfo={ + // dbinfo && dbinfo.tables && dbinfo.tables.find((x) => x.pureName == pureName && x.schemaName == schemaName) + // } /> {reference && ( diff --git a/packages/web/src/formview/FormView.js b/packages/web/src/formview/FormView.js index 0457a13b..158e74d1 100644 --- a/packages/web/src/formview/FormView.js +++ b/packages/web/src/formview/FormView.js @@ -61,15 +61,34 @@ const NullSpan = styled.span` font-style: italic; `; -export default function FormView({ tableInfo, rowData, toolbarPortalRef, tabVisible, onSetTableView }) { +export default function FormView(props) { + const { rowData, toolbarPortalRef, tabVisible, config, setConfig } = props; + /** @type {import('dbgate-datalib').FormViewDisplay} */ + const formDisplay = props.formDisplay; const theme = useTheme(); const [headerRowRef, { height: rowHeight }] = useDimensions(); const [wrapperRef, { height: wrapperHeight }] = useDimensions(); - if (!tableInfo || !rowData) return null; + const handleSwitchToTable = () => { + setConfig((cfg) => ({ + ...cfg, + isFormView: false, + formViewKey: null, + })); + }; + + const toolbar = + toolbarPortalRef && + toolbarPortalRef.current && + tabVisible && + ReactDOM.createPortal(, toolbarPortalRef.current); + + // console.log('display', display); + + if (!formDisplay || !formDisplay.isLoadedCorrectly) return toolbar; const rowCount = Math.floor((wrapperHeight - 20) / rowHeight); - const columnChunks = _.chunk(tableInfo.columns, rowCount); + const columnChunks = _.chunk(formDisplay.columns, rowCount); return ( @@ -78,18 +97,15 @@ export default function FormView({ tableInfo, rowData, toolbarPortalRef, tabVisi {chunk.map((col) => ( - + - {rowData[col.columnName]} + {rowData && rowData[col.columnName]} ))} ))} - {toolbarPortalRef && - toolbarPortalRef.current && - tabVisible && - ReactDOM.createPortal(, toolbarPortalRef.current)} + {toolbar} ); } diff --git a/packages/web/src/formview/SqlFormView.js b/packages/web/src/formview/SqlFormView.js index 15528424..e8ca69b2 100644 --- a/packages/web/src/formview/SqlFormView.js +++ b/packages/web/src/formview/SqlFormView.js @@ -1,6 +1,71 @@ +import { 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'; + +async function loadCurrentRow(props) { + const { formDisplay, conid, database } = props; + /** @type {import('dbgate-datalib').TableFormViewDisplay} */ + + const sql = formDisplay.getCurrentRowQuery(); + + 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) { - return ; + const { formDisplay } = props; + const [rowData, setRowData] = React.useState(null); + + const handleLoadCurrentRow = async () => { + const row = await loadCurrentRow(props); + if (row) setRowData(row); + }; + + React.useEffect(() => { + handleLoadCurrentRow(); + }, [formDisplay]); + + // 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 ; } From 5ab2ed964670b5ebe9a15e2706b0e55f5e64b752 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sat, 9 Jan 2021 21:27:34 +0100 Subject: [PATCH 04/15] form view - loading, navigation --- packages/datalib/src/TableFormViewDisplay.ts | 135 ++++++++++++++++++- packages/web/src/formview/FormView.js | 7 +- packages/web/src/formview/FormViewToolbar.js | 14 +- packages/web/src/formview/SqlFormView.js | 23 +++- packages/web/src/icons.js | 2 + 5 files changed, 167 insertions(+), 14 deletions(-) diff --git a/packages/datalib/src/TableFormViewDisplay.ts b/packages/datalib/src/TableFormViewDisplay.ts index 0554fab0..9b7f90ec 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 158e74d1..48d3dfa0 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 280c0124..a0d070f0 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 e8ca69b2..9f252e2e 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 357bde17..369ce50e 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', From 46fbcf13ed4a43d5e84c68b68a1297ee5bc187b9 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sun, 10 Jan 2021 09:20:05 +0100 Subject: [PATCH 05/15] formview keyboard navigation --- packages/web/src/datagrid/DataGridRow.js | 2 +- packages/web/src/formview/FormView.js | 160 ++++++++++++++++-- .../web/src/formview/FormViewContextMenu.js | 15 ++ 3 files changed, 165 insertions(+), 12 deletions(-) create mode 100644 packages/web/src/formview/FormViewContextMenu.js diff --git a/packages/web/src/datagrid/DataGridRow.js b/packages/web/src/datagrid/DataGridRow.js index d213162e..a4759f60 100644 --- a/packages/web/src/datagrid/DataGridRow.js +++ b/packages/web/src/datagrid/DataGridRow.js @@ -154,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'; diff --git a/packages/web/src/formview/FormView.js b/packages/web/src/formview/FormView.js index 48d3dfa0..95b2a2e1 100644 --- a/packages/web/src/formview/FormView.js +++ b/packages/web/src/formview/FormView.js @@ -7,6 +7,11 @@ 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'; const Table = styled.table` border-collapse: collapse; @@ -40,6 +45,14 @@ const TableHeaderCell = styled.td` background-color: ${(props) => props.theme.gridheader_background}; overflow: hidden; position: relative; + + ${(props) => + // @ts-ignore + props.isSelected && + ` + background: initial; + background-color: ${props.theme.gridbody_selection[4]}; + color: ${props.theme.gridbody_invfont1};`} `; const TableBodyCell = styled.td` @@ -50,6 +63,14 @@ const TableBodyCell = styled.td` white-space: nowrap; position: relative; overflow: hidden; + + ${(props) => + // @ts-ignore + props.isSelected && + ` + background: initial; + background-color: ${props.theme.gridbody_selection[4]}; + color: ${props.theme.gridbody_invfont1};`} `; const HintSpan = styled.span` @@ -61,6 +82,13 @@ const NullSpan = styled.span` font-style: italic; `; +const FocusField = styled.input` + // visibility: hidden + position: absolute; + left: -1000px; + top: -1000px; +`; + export default function FormView(props) { const { rowData, toolbarPortalRef, tabVisible, config, setConfig, onNavigate } = props; /** @type {import('dbgate-datalib').FormViewDisplay} */ @@ -68,6 +96,12 @@ export default function FormView(props) { 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 rowCount = Math.floor((wrapperHeight - 20) / rowHeight); + const columnChunks = _.chunk(formDisplay.columns, rowCount); const handleSwitchToTable = () => { setConfig((cfg) => ({ @@ -77,6 +111,99 @@ export default function FormView(props) { })); }; + const handleContextMenu = (event) => { + event.preventDefault(); + showMenu( + event.pageX, + event.pageY, + + ); + }; + + React.useEffect(() => { + if (tabVisible) { + if (focusFieldRef.current) focusFieldRef.current.focus(); + } + }, [tabVisible, focusFieldRef.current]); + + const moveCursor = (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) => { + switch (event.keyCode) { + case keycodes.leftArrow: + return moveCursor(currentCell[0], currentCell[1] - 1); + case keycodes.rightArrow: + return moveCursor(currentCell[0], currentCell[1] + 1); + case keycodes.upArrow: + return moveCursor(currentCell[0] - 1, currentCell[1]); + case keycodes.downArrow: + return moveCursor(currentCell[0] + 1, currentCell[1]); + case keycodes.pageUp: + return moveCursor(0, currentCell[1]); + case keycodes.pageDown: + return moveCursor(rowCount - 1, currentCell[1]); + case keycodes.home: + return moveCursor(0, 0); + case keycodes.end: + return moveCursor(rowCount - 1, columnChunks.length * 2 - 1); + } + }; + + const handleKeyNavigation = (event) => { + if (event.ctrlKey) { + switch (event.keyCode) { + case keycodes.leftArrow: + case keycodes.upArrow: + return 'previous'; + case keycodes.rightArrow: + case keycodes.downArrow: + return 'next'; + case keycodes.home: + return 'begin'; + case keycodes.end: + return 'end'; + } + } + }; + + const handleKeyDown = (event) => { + const navigation = handleKeyNavigation(event); + if (navigation) { + event.preventDefault(); + onNavigate(navigation); + return; + } + const moved = handleCursorMove(event); + if (moved) { + setCurrentCell(moved); + event.preventDefault(); + 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); + // @ts-ignore + setCurrentCell(cell); + }; + const toolbar = toolbarPortalRef && toolbarPortalRef.current && @@ -86,28 +213,39 @@ export default function FormView(props) { toolbarPortalRef.current ); - // console.log('display', display); - if (!formDisplay || !formDisplay.isLoadedCorrectly) return toolbar; - const rowCount = Math.floor((wrapperHeight - 20) / rowHeight); - const columnChunks = _.chunk(formDisplay.columns, rowCount); - return ( - - {columnChunks.map((chunk, index) => ( - - {chunk.map((col) => ( + + {columnChunks.map((chunk, chunkIndex) => ( +
+ {chunk.map((col, rowIndex) => ( - + - {rowData && rowData[col.columnName]} + + + ))}
))} + + {toolbar}
); diff --git a/packages/web/src/formview/FormViewContextMenu.js b/packages/web/src/formview/FormViewContextMenu.js new file mode 100644 index 00000000..44ff1940 --- /dev/null +++ b/packages/web/src/formview/FormViewContextMenu.js @@ -0,0 +1,15 @@ +import React from 'react'; +import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu'; + +export default function FormViewContextMenu({ switchToTable, onNavigate }) { + return ( + <> + Table view + + onNavigate('begin')}>Navigate to begin + onNavigate('previous')}>Navigate to previous + onNavigate('next')}>Navigate to next + onNavigate('end')}>Navigate to end + + ); +} From b25cc146cb4c1ec3819913177309e03ef3f45c66 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sun, 10 Jan 2021 09:41:35 +0100 Subject: [PATCH 06/15] scroll in view --- packages/web/src/formview/FormView.js | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/web/src/formview/FormView.js b/packages/web/src/formview/FormView.js index 95b2a2e1..6a281ef1 100644 --- a/packages/web/src/formview/FormView.js +++ b/packages/web/src/formview/FormView.js @@ -99,6 +99,7 @@ export default function FormView(props) { 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); @@ -120,6 +121,10 @@ export default function FormView(props) { ); }; + const setCellRef = (row, col, element) => { + cellRefs.current[`${row},${col}`] = element; + }; + React.useEffect(() => { if (tabVisible) { if (focusFieldRef.current) focusFieldRef.current.focus(); @@ -136,6 +141,14 @@ export default function FormView(props) { }; const handleCursorMove = (event) => { + if (event.ctrlKey) { + switch (event.keyCode) { + case keycodes.leftArrow: + return moveCursor(currentCell[0], 0); + case keycodes.rightArrow: + return moveCursor(currentCell[0], columnChunks.length * 2 - 1); + } + } switch (event.keyCode) { case keycodes.leftArrow: return moveCursor(currentCell[0], currentCell[1] - 1); @@ -159,10 +172,8 @@ export default function FormView(props) { const handleKeyNavigation = (event) => { if (event.ctrlKey) { switch (event.keyCode) { - case keycodes.leftArrow: case keycodes.upArrow: return 'previous'; - case keycodes.rightArrow: case keycodes.downArrow: return 'next'; case keycodes.home: @@ -173,6 +184,15 @@ export default function FormView(props) { } }; + const scrollIntoView = (cell) => { + const element = cellRefs.current[`${cell[0]},${cell[1]}`]; + if (element) element.scrollIntoView(); + }; + + React.useEffect(() => { + scrollIntoView(currentCell); + }, [rowData]); + const handleKeyDown = (event) => { const navigation = handleKeyNavigation(event); if (navigation) { @@ -183,6 +203,7 @@ export default function FormView(props) { const moved = handleCursorMove(event); if (moved) { setCurrentCell(moved); + scrollIntoView(moved); event.preventDefault(); return; } @@ -227,6 +248,7 @@ export default function FormView(props) { data-col={chunkIndex * 2} // @ts-ignore isSelected={currentCell[0] == rowIndex && currentCell[1] == chunkIndex * 2} + ref={(element) => setCellRef(rowIndex, chunkIndex * 2, element)} > @@ -236,6 +258,7 @@ export default function FormView(props) { data-col={chunkIndex * 2 + 1} // @ts-ignore isSelected={currentCell[0] == rowIndex && currentCell[1] == chunkIndex * 2 + 1} + ref={(element) => setCellRef(rowIndex, chunkIndex * 2 + 1, element)} >
From c5c4b0b6deb328153f0889be474b5cb98a627992 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sun, 10 Jan 2021 10:50:22 +0100 Subject: [PATCH 07/15] form view - inplace editor --- packages/web/src/datagrid/DataGridRow.js | 7 +- packages/web/src/datagrid/InplaceEditor.js | 19 ++-- packages/web/src/formview/FormView.js | 119 +++++++++++++++++---- 3 files changed, 114 insertions(+), 31 deletions(-) diff --git a/packages/web/src/datagrid/DataGridRow.js b/packages/web/src/datagrid/DataGridRow.js index a4759f60..690a924f 100644 --- a/packages/web/src/datagrid/DataGridRow.js +++ b/packages/web/src/datagrid/DataGridRow.js @@ -291,9 +291,10 @@ function DataGridRow(props) { inplaceEditorState={inplaceEditorState} dispatchInsplaceEditor={dispatchInsplaceEditor} cellValue={rowData[col.uniqueName]} - grider={grider} - rowIndex={rowIndex} - uniqueName={col.uniqueName} + // grider={grider} + // rowIndex={rowIndex} + // uniqueName={col.uniqueName} + onSetValue={(value) => grider.setCellValue(rowIndex, col.uniqueName, value)} /> ) : ( <> diff --git a/packages/web/src/datagrid/InplaceEditor.js b/packages/web/src/datagrid/InplaceEditor.js index 645c5822..a2aca264 100644 --- a/packages/web/src/datagrid/InplaceEditor.js +++ b/packages/web/src/datagrid/InplaceEditor.js @@ -14,12 +14,13 @@ const StyledInput = styled.input` export default function InplaceEditor({ widthPx, - rowIndex, - uniqueName, - grider, + // rowIndex, + // uniqueName, + // grider, cellValue, inplaceEditorState, dispatchInsplaceEditor, + onSetValue, }) { const editorRef = React.useRef(); const isChangedRef = React.useRef(!!inplaceEditorState.text); @@ -34,7 +35,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 +44,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 +60,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 +70,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(); diff --git a/packages/web/src/formview/FormView.js b/packages/web/src/formview/FormView.js index 6a281ef1..ae206e70 100644 --- a/packages/web/src/formview/FormView.js +++ b/packages/web/src/formview/FormView.js @@ -12,6 +12,7 @@ import FormViewContextMenu from './FormViewContextMenu'; import keycodes from '../utility/keycodes'; import { CellFormattedValue } from '../datagrid/DataGridRow'; import { cellFromEvent } from '../datagrid/selection'; +import InplaceEditor from '../datagrid/InplaceEditor'; const Table = styled.table` border-collapse: collapse; @@ -73,15 +74,6 @@ const TableBodyCell = styled.td` color: ${props.theme.gridbody_invfont1};`} `; -const HintSpan = styled.span` - color: gray; - margin-left: 5px; -`; -const NullSpan = styled.span` - color: gray; - font-style: italic; -`; - const FocusField = styled.input` // visibility: hidden position: absolute; @@ -89,6 +81,10 @@ const FocusField = styled.input` top: -1000px; `; +function isDataCell(cell) { + return cell[1] % 2 == 1; +} + export default function FormView(props) { const { rowData, toolbarPortalRef, tabVisible, config, setConfig, onNavigate } = props; /** @type {import('dbgate-datalib').FormViewDisplay} */ @@ -131,7 +127,7 @@ export default function FormView(props) { } }, [tabVisible, focusFieldRef.current]); - const moveCursor = (row, col) => { + const checkMoveCursorBounds = (row, col) => { if (row < 0) row = 0; if (col < 0) col = 0; if (col >= columnChunks.length * 2) col = columnChunks.length * 2 - 1; @@ -144,28 +140,28 @@ export default function FormView(props) { if (event.ctrlKey) { switch (event.keyCode) { case keycodes.leftArrow: - return moveCursor(currentCell[0], 0); + return checkMoveCursorBounds(currentCell[0], 0); case keycodes.rightArrow: - return moveCursor(currentCell[0], columnChunks.length * 2 - 1); + return checkMoveCursorBounds(currentCell[0], columnChunks.length * 2 - 1); } } switch (event.keyCode) { case keycodes.leftArrow: - return moveCursor(currentCell[0], currentCell[1] - 1); + return checkMoveCursorBounds(currentCell[0], currentCell[1] - 1); case keycodes.rightArrow: - return moveCursor(currentCell[0], currentCell[1] + 1); + return checkMoveCursorBounds(currentCell[0], currentCell[1] + 1); case keycodes.upArrow: - return moveCursor(currentCell[0] - 1, currentCell[1]); + return checkMoveCursorBounds(currentCell[0] - 1, currentCell[1]); case keycodes.downArrow: - return moveCursor(currentCell[0] + 1, currentCell[1]); + return checkMoveCursorBounds(currentCell[0] + 1, currentCell[1]); case keycodes.pageUp: - return moveCursor(0, currentCell[1]); + return checkMoveCursorBounds(0, currentCell[1]); case keycodes.pageDown: - return moveCursor(rowCount - 1, currentCell[1]); + return checkMoveCursorBounds(rowCount - 1, currentCell[1]); case keycodes.home: - return moveCursor(0, 0); + return checkMoveCursorBounds(0, 0); case keycodes.end: - return moveCursor(rowCount - 1, columnChunks.length * 2 - 1); + return checkMoveCursorBounds(rowCount - 1, columnChunks.length * 2 - 1); } }; @@ -193,6 +189,12 @@ export default function FormView(props) { scrollIntoView(currentCell); }, [rowData]); + const moveCurrentCell = (row, col) => { + const moved = checkMoveCursorBounds(row, col); + setCurrentCell(moved); + scrollIntoView(moved); + }; + const handleKeyDown = (event) => { const navigation = handleKeyNavigation(event); if (navigation) { @@ -207,6 +209,22 @@ export default function FormView(props) { event.preventDefault(); return; } + if ( + !event.ctrlKey && + !event.altKey && + ((event.keyCode >= keycodes.a && event.keyCode <= keycodes.z) || + (event.keyCode >= keycodes.n0 && event.keyCode <= keycodes.n9) || + event.keyCode == keycodes.dash) + ) { + // @ts-ignore + 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) => { @@ -221,10 +239,52 @@ export default function FormView(props) { 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 [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 && @@ -260,7 +320,24 @@ export default function FormView(props) { isSelected={currentCell[0] == rowIndex && currentCell[1] == chunkIndex * 2 + 1} ref={(element) => setCellRef(rowIndex, chunkIndex * 2 + 1, element)} > - + {inplaceEditorState.cell && + rowIndex == inplaceEditorState.cell[0] && + chunkIndex * 2 + 1 == inplaceEditorState.cell[1] ? ( + {}} + // grider={grider} + // rowIndex={rowIndex} + // uniqueName={col.uniqueName} + /> + ) : ( + <> + + + )} ))} From fe1180d1e442ee53586da898bbde594cebf1c49d Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sun, 10 Jan 2021 16:50:08 +0100 Subject: [PATCH 08/15] editing in form view --- packages/datalib/src/FormViewDisplay.ts | 2 + packages/datalib/src/TableFormViewDisplay.ts | 33 ++++++- packages/web/src/datagrid/InplaceEditor.js | 7 +- packages/web/src/formview/ChangeSetFormer.ts | 93 ++++++++++++++++++++ packages/web/src/formview/FormView.js | 19 +++- packages/web/src/formview/Former.ts | 53 +++++++++++ packages/web/src/formview/SqlFormView.js | 14 ++- 7 files changed, 207 insertions(+), 14 deletions(-) create mode 100644 packages/web/src/formview/ChangeSetFormer.ts create mode 100644 packages/web/src/formview/Former.ts diff --git a/packages/datalib/src/FormViewDisplay.ts b/packages/datalib/src/FormViewDisplay.ts index 76634057..cfbe5cd1 100644 --- a/packages/datalib/src/FormViewDisplay.ts +++ b/packages/datalib/src/FormViewDisplay.ts @@ -11,6 +11,7 @@ import { ChangeCacheFunc, ChangeConfigFunc, DisplayColumn } from './GridDisplay' export class FormViewDisplay { isLoadedCorrectly = true; columns: DisplayColumn[]; + public baseTable: TableInfo; constructor( public config: GridConfig, @@ -20,4 +21,5 @@ export class FormViewDisplay { public driver?: EngineDriver, public dbinfo: DatabaseInfo = null ) {} + } diff --git a/packages/datalib/src/TableFormViewDisplay.ts b/packages/datalib/src/TableFormViewDisplay.ts index 9b7f90ec..4a26cd64 100644 --- a/packages/datalib/src/TableFormViewDisplay.ts +++ b/packages/datalib/src/TableFormViewDisplay.ts @@ -15,9 +15,9 @@ import { import { filterName } from './filterName'; import { TableGridDisplay } from './TableGridDisplay'; import stableStringify from 'json-stable-stringify'; +import { ChangeSetFieldDefinition, ChangeSetRowDefinition } from './ChangeSet'; export class TableFormViewDisplay extends FormViewDisplay { - public table: TableInfo; // use utility functions from GridDisplay and publish result in FromViewDisplat interface private gridDisplay: TableGridDisplay; @@ -35,13 +35,17 @@ export class TableFormViewDisplay extends FormViewDisplay { this.isLoadedCorrectly = this.gridDisplay.isLoadedCorrectly; this.columns = this.gridDisplay.columns; + this.baseTable = this.gridDisplay.baseTable; } - getPrimaryKeyEqualCondition(): Condition { - if (!this.config.formViewKey) return null; + 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: _.keys(this.config.formViewKey).map((columnName) => ({ + conditions: primaryKey.columns.map(({ columnName }) => ({ conditionType: 'binary', operator: '=', left: { @@ -186,4 +190,25 @@ export class TableFormViewDisplay extends FormViewDisplay { 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/web/src/datagrid/InplaceEditor.js b/packages/web/src/datagrid/InplaceEditor.js index a2aca264..e32ab10e 100644 --- a/packages/web/src/datagrid/InplaceEditor.js +++ b/packages/web/src/datagrid/InplaceEditor.js @@ -23,6 +23,7 @@ export default function InplaceEditor({ onSetValue, }) { const editorRef = React.useRef(); + const widthRef = React.useRef(widthPx); const isChangedRef = React.useRef(!!inplaceEditorState.text); React.useEffect(() => { const editor = editorRef.current; @@ -88,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/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 index ae206e70..10322d0b 100644 --- a/packages/web/src/formview/FormView.js +++ b/packages/web/src/formview/FormView.js @@ -1,3 +1,5 @@ +// @ts-nocheck + import _ from 'lodash'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -48,7 +50,6 @@ const TableHeaderCell = styled.td` position: relative; ${(props) => - // @ts-ignore props.isSelected && ` background: initial; @@ -66,12 +67,17 @@ const TableBodyCell = styled.td` overflow: hidden; ${(props) => - // @ts-ignore 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` @@ -86,7 +92,7 @@ function isDataCell(cell) { } export default function FormView(props) { - const { rowData, toolbarPortalRef, tabVisible, config, setConfig, onNavigate } = props; + const { toolbarPortalRef, tabVisible, config, setConfig, onNavigate, former } = props; /** @type {import('dbgate-datalib').FormViewDisplay} */ const formDisplay = props.formDisplay; const theme = useTheme(); @@ -100,6 +106,8 @@ export default function FormView(props) { const rowCount = Math.floor((wrapperHeight - 20) / rowHeight); const columnChunks = _.chunk(formDisplay.columns, rowCount); + const { rowData, rowStatus } = former; + const handleSwitchToTable = () => { setConfig((cfg) => ({ ...cfg, @@ -318,6 +326,7 @@ export default function FormView(props) { 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 && @@ -328,7 +337,9 @@ export default function FormView(props) { inplaceEditorState={inplaceEditorState} dispatchInsplaceEditor={dispatchInsplaceEditor} cellValue={rowData[col.uniqueName]} - onSetValue={(value) => {}} + onSetValue={(value) => { + former.setCellValue(col.uniqueName, value); + }} // grider={grider} // rowIndex={rowIndex} // uniqueName={col.uniqueName} 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 index 9f252e2e..c5f5b218 100644 --- a/packages/web/src/formview/SqlFormView.js +++ b/packages/web/src/formview/SqlFormView.js @@ -5,6 +5,7 @@ import { useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders'; import useExtensions from '../utility/useExtensions'; import FormView from './FormView'; import axios from '../utility/axios'; +import ChangeSetFormer from './ChangeSetFormer'; async function loadRow(props, sql) { const { conid, database } = props; @@ -26,7 +27,7 @@ async function loadRow(props, sql) { } export default function SqlFormView(props) { - const { formDisplay } = props; + const { formDisplay, changeSetState, dispatchChangeSet } = props; const [rowData, setRowData] = React.useState(null); const handleLoadCurrentRow = async () => { @@ -46,7 +47,14 @@ export default function SqlFormView(props) { if (formDisplay && !formDisplay.isLoadedCurrentRow(rowData)) { handleLoadCurrentRow(); } - }, [formDisplay]); + }, [formDisplay, rowData]); + + const former = React.useMemo(() => new ChangeSetFormer(rowData, changeSetState, dispatchChangeSet, formDisplay), [ + rowData, + changeSetState, + dispatchChangeSet, + formDisplay, + ]); // const { config, setConfig, cache, setCache, schemaName, pureName, conid, database } = props; // const { formViewKey } = config; @@ -76,5 +84,5 @@ export default function SqlFormView(props) { // setDisplay(newDisplay); // }, [config, cache, conid, database, schemaName, pureName, dbinfo, extensions]); - return ; + return ; } From 7c8c7467e10c9b06e640e9763e10f565fb0d6fea Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sun, 10 Jan 2021 17:04:04 +0100 Subject: [PATCH 09/15] save changes from form view --- packages/web/src/formview/FormView.js | 17 ++++++- packages/web/src/formview/SqlFormView.js | 63 ++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/packages/web/src/formview/FormView.js b/packages/web/src/formview/FormView.js index 10322d0b..0ad1fbee 100644 --- a/packages/web/src/formview/FormView.js +++ b/packages/web/src/formview/FormView.js @@ -92,7 +92,7 @@ function isDataCell(cell) { } export default function FormView(props) { - const { toolbarPortalRef, tabVisible, config, setConfig, onNavigate, former } = props; + const { toolbarPortalRef, tabVisible, config, setConfig, onNavigate, former, onSave } = props; /** @type {import('dbgate-datalib').FormViewDisplay} */ const formDisplay = props.formDisplay; const theme = useTheme(); @@ -188,6 +188,15 @@ export default function FormView(props) { } }; + function handleSave() { + if (inplaceEditorState.cell) { + // @ts-ignore + dispatchInsplaceEditor({ type: 'shouldSave' }); + return; + } + if (onSave) onSave(); + } + const scrollIntoView = (cell) => { const element = cellRefs.current[`${cell[0]},${cell[1]}`]; if (element) element.scrollIntoView(); @@ -217,6 +226,12 @@ export default function FormView(props) { event.preventDefault(); return; } + if (event.keyCode == keycodes.s && event.ctrlKey) { + event.preventDefault(); + handleSave(); + // this.saveAndFocus(); + } + if ( !event.ctrlKey && !event.altKey && diff --git a/packages/web/src/formview/SqlFormView.js b/packages/web/src/formview/SqlFormView.js index c5f5b218..326a2385 100644 --- a/packages/web/src/formview/SqlFormView.js +++ b/packages/web/src/formview/SqlFormView.js @@ -1,4 +1,4 @@ -import { TableFormViewDisplay } from 'dbgate-datalib'; +import { changeSetToSql, createChangeSet, TableFormViewDisplay } from 'dbgate-datalib'; import { findEngineDriver } from 'dbgate-tools'; import React from 'react'; import { useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders'; @@ -6,6 +6,11 @@ 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; @@ -27,8 +32,17 @@ async function loadRow(props, sql) { } export default function SqlFormView(props) { - const { formDisplay, changeSetState, dispatchChangeSet } = props; + const { formDisplay, changeSetState, dispatchChangeSet, conid, database } = props; const [rowData, setRowData] = React.useState(null); + const [reloadToken, setReloadToken] = React.useState(0); + + 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()); @@ -43,6 +57,10 @@ export default function SqlFormView(props) { } }; + React.useEffect(() => { + if (formDisplay) handleLoadCurrentRow(); + }, [reloadToken]); + React.useEffect(() => { if (formDisplay && !formDisplay.isLoadedCurrentRow(rowData)) { handleLoadCurrentRow(); @@ -56,6 +74,35 @@ export default function SqlFormView(props) { 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; @@ -84,5 +131,15 @@ export default function SqlFormView(props) { // setDisplay(newDisplay); // }, [config, cache, conid, database, schemaName, pureName, dbinfo, extensions]); - return ; + return ( + <> + + + + ); } From 9d50ac00933bc592b45f3e2b744557063e495cb6 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Mon, 11 Jan 2021 17:25:05 +0100 Subject: [PATCH 10/15] form view - row count --- packages/datalib/src/TableFormViewDisplay.ts | 37 +++++++- packages/web/src/formview/FormView.js | 95 +++++++++++++++++++- packages/web/src/formview/FormViewToolbar.js | 20 ++++- packages/web/src/formview/SqlFormView.js | 39 +++++++- 4 files changed, 183 insertions(+), 8 deletions(-) diff --git a/packages/datalib/src/TableFormViewDisplay.ts b/packages/datalib/src/TableFormViewDisplay.ts index 4a26cd64..eda82f1f 100644 --- a/packages/datalib/src/TableFormViewDisplay.ts +++ b/packages/datalib/src/TableFormViewDisplay.ts @@ -42,7 +42,7 @@ export class TableFormViewDisplay extends FormViewDisplay { if (!row) row = this.config.formViewKey; if (!row) return null; const { primaryKey } = this.gridDisplay.baseTable; - if (primaryKey) return null; + if (!primaryKey) return null; return { conditionType: 'and', conditions: primaryKey.columns.map(({ columnName }) => ({ @@ -68,6 +68,7 @@ export class TableFormViewDisplay extends FormViewDisplay { const conditions = []; const { primaryKey } = this.gridDisplay.baseTable; + if (!primaryKey) return null; for (let index = 0; index < primaryKey.columns.length; index++) { conditions.push({ conditionType: 'and', @@ -133,7 +134,40 @@ export class TableFormViewDisplay extends FormViewDisplay { 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(); + const sql = treeToSql(this.driver, select, dumpSqlSelect); + return sql; + } + + getBeforeCountQuery() { + if (!this.driver) return null; + const select = this.getCountSelect(); + 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) @@ -150,6 +184,7 @@ export class TableFormViewDisplay extends FormViewDisplay { } 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); diff --git a/packages/web/src/formview/FormView.js b/packages/web/src/formview/FormView.js index 0ad1fbee..5c9ca3b0 100644 --- a/packages/web/src/formview/FormView.js +++ b/packages/web/src/formview/FormView.js @@ -15,6 +15,7 @@ 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; @@ -87,12 +88,33 @@ const FocusField = styled.input` top: -1000px; `; +const RowCountLabel = styled.div` + position: absolute; + background-color: ${(props) => props.theme.gridbody_background_yellow[1]}; + right: 40px; + bottom: 20px; +`; + function isDataCell(cell) { return cell[1] % 2 == 1; } export default function FormView(props) { - const { toolbarPortalRef, tabVisible, config, setConfig, onNavigate, former, onSave } = props; + const { + toolbarPortalRef, + tabVisible, + config, + setConfig, + onNavigate, + former, + onSave, + conid, + database, + onReload, + onReconnect, + allRowCount, + rowCountBefore, + } = props; /** @type {import('dbgate-datalib').FormViewDisplay} */ const formDisplay = props.formDisplay; const theme = useTheme(); @@ -197,6 +219,25 @@ export default function FormView(props) { 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(); @@ -212,6 +253,13 @@ export default function FormView(props) { 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) { @@ -232,6 +280,36 @@ export default function FormView(props) { // 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.ctrlKey && !event.altKey && @@ -281,6 +359,11 @@ export default function FormView(props) { 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': @@ -313,7 +396,14 @@ export default function FormView(props) { toolbarPortalRef.current && tabVisible && ReactDOM.createPortal( - , + , toolbarPortalRef.current ); @@ -371,6 +461,7 @@ export default function FormView(props) { ))} + {rowCountInfo && {rowCountInfo}} {toolbar} diff --git a/packages/web/src/formview/FormViewToolbar.js b/packages/web/src/formview/FormViewToolbar.js index a0d070f0..d418a684 100644 --- a/packages/web/src/formview/FormViewToolbar.js +++ b/packages/web/src/formview/FormViewToolbar.js @@ -1,7 +1,7 @@ import React from 'react'; import ToolbarButton from '../widgets/ToolbarButton'; -export default function FormViewToolbar({ switchToTable, onNavigate }) { +export default function FormViewToolbar({ switchToTable, onNavigate, reload, reconnect, former, save }) { return ( <> @@ -19,6 +19,24 @@ export default function FormViewToolbar({ switchToTable, onNavigate }) { 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/SqlFormView.js b/packages/web/src/formview/SqlFormView.js index 326a2385..7a2b4a3e 100644 --- a/packages/web/src/formview/SqlFormView.js +++ b/packages/web/src/formview/SqlFormView.js @@ -14,8 +14,8 @@ import useShowModal from '../modals/showModal'; async function loadRow(props, sql) { const { conid, database } = props; - /** @type {import('dbgate-datalib').TableFormViewDisplay} */ - const formDisplay = props.formDisplay; + + if (!sql) return null; const response = await axios.request({ url: 'database-connections/query-data', @@ -35,6 +35,7 @@ export default function SqlFormView(props) { const { formDisplay, changeSetState, dispatchChangeSet, conid, database } = 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(''); @@ -49,6 +50,18 @@ export default function SqlFormView(props) { 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) { @@ -59,13 +72,19 @@ export default function SqlFormView(props) { React.useEffect(() => { if (formDisplay) handleLoadCurrentRow(); + setRowCountInfo(null); + handleLoadRowCount(); }, [reloadToken]); React.useEffect(() => { + if (!formDisplay.isLoadedCorrectly) return; + if (formDisplay && !formDisplay.isLoadedCurrentRow(rowData)) { handleLoadCurrentRow(); } - }, [formDisplay, rowData]); + setRowCountInfo(null); + handleLoadRowCount(); + }, [formDisplay]); const former = React.useMemo(() => new ChangeSetFormer(rowData, changeSetState, dispatchChangeSet, formDisplay), [ rowData, @@ -133,7 +152,19 @@ export default function SqlFormView(props) { return ( <> - + setReloadToken((x) => x + 1)} + onReconnect={async () => { + await axios.post('database-connections/refresh', { conid, database }); + formDisplay.reload(); + }} + {...rowCountInfo} + /> Date: Mon, 11 Jan 2021 17:34:09 +0100 Subject: [PATCH 11/15] form view --- packages/web/src/formview/FormView.js | 15 +++++++++++++++ packages/web/src/formview/SqlFormView.js | 6 +++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/web/src/formview/FormView.js b/packages/web/src/formview/FormView.js index 5c9ca3b0..322328ae 100644 --- a/packages/web/src/formview/FormView.js +++ b/packages/web/src/formview/FormView.js @@ -95,6 +95,11 @@ const RowCountLabel = styled.div` bottom: 20px; `; +const HintSpan = styled.span` + color: gray; + margin-left: 5px; +`; + function isDataCell(cell) { return cell[1] % 2 == 1; } @@ -310,6 +315,11 @@ export default function FormView(props) { copyToClipboard(); } + if (event.keyCode == keycodes.f5) { + event.preventDefault(); + onReload(); + } + if ( !event.ctrlKey && !event.altKey && @@ -452,6 +462,11 @@ export default function FormView(props) { ) : ( <> + {!!col.hintColumnName && + rowData && + !(rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)) && ( + {rowData[col.hintColumnName]} + )} )} diff --git a/packages/web/src/formview/SqlFormView.js b/packages/web/src/formview/SqlFormView.js index 7a2b4a3e..9df44fef 100644 --- a/packages/web/src/formview/SqlFormView.js +++ b/packages/web/src/formview/SqlFormView.js @@ -32,7 +32,7 @@ async function loadRow(props, sql) { } export default function SqlFormView(props) { - const { formDisplay, changeSetState, dispatchChangeSet, conid, database } = 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); @@ -70,6 +70,10 @@ export default function SqlFormView(props) { } }; + React.useEffect(() => { + if (onReferenceSourceChanged && rowData) onReferenceSourceChanged([rowData]); + }, [rowData]); + React.useEffect(() => { if (formDisplay) handleLoadCurrentRow(); setRowCountInfo(null); From 0541cbf1f6b8dff92fac38276996812152ffcb17 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Mon, 11 Jan 2021 17:49:18 +0100 Subject: [PATCH 12/15] fixed connection refresh --- packages/api/src/controllers/databaseConnections.js | 1 + packages/api/src/proc/databaseConnectionProcess.js | 1 + 2 files changed, 2 insertions(+) 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) { From 49849820e898247d35f5c72f696bb51d4474db63 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Mon, 11 Jan 2021 17:49:31 +0100 Subject: [PATCH 13/15] fix --- packages/datalib/src/TableFormViewDisplay.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/datalib/src/TableFormViewDisplay.ts b/packages/datalib/src/TableFormViewDisplay.ts index eda82f1f..b7447383 100644 --- a/packages/datalib/src/TableFormViewDisplay.ts +++ b/packages/datalib/src/TableFormViewDisplay.ts @@ -152,6 +152,7 @@ export class TableFormViewDisplay extends FormViewDisplay { getCountQuery() { if (!this.driver) return null; const select = this.getCountSelect(); + if (!select) return null; const sql = treeToSql(this.driver, select, dumpSqlSelect); return sql; } @@ -159,6 +160,7 @@ export class TableFormViewDisplay extends FormViewDisplay { 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; From 817efb1c72323757323db8010d28ac2fd0b3dc20 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Mon, 11 Jan 2021 18:03:22 +0100 Subject: [PATCH 14/15] cell data view for form view --- packages/web/src/celldata/CellDataView.js | 7 +++++-- packages/web/src/datagrid/DataGrid.js | 13 ++++++++----- packages/web/src/formview/FormView.js | 10 ++++++++++ 3 files changed, 23 insertions(+), 7 deletions(-) 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 be578cc3..125bd458 100644 --- a/packages/web/src/datagrid/DataGrid.js +++ b/packages/web/src/datagrid/DataGrid.js @@ -25,6 +25,7 @@ export default function DataGrid(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); @@ -43,17 +44,19 @@ export default function DataGrid(props) { )} - {!isFormView && ( - + + {isFormView ? ( + + ) : ( - - )} + )} + {isFormView ? ( - + ) : ( props.isSelected && @@ -119,6 +121,7 @@ export default function FormView(props) { onReconnect, allRowCount, rowCountBefore, + onSelectionChanged, } = props; /** @type {import('dbgate-datalib').FormViewDisplay} */ const formDisplay = props.formDisplay; @@ -162,6 +165,13 @@ export default function FormView(props) { } }, [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; From 87fde4185aa06cdc73557bc293dbb8b86e6b166a Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Mon, 11 Jan 2021 18:16:17 +0100 Subject: [PATCH 15/15] swithc to form keyboard shortcut --- .../web/src/datagrid/DataGridContextMenu.js | 6 ++++++ packages/web/src/datagrid/DataGridCore.js | 18 +++++++++++++++++ packages/web/src/datagrid/DataGridToolbar.js | 7 ++++++- packages/web/src/formview/FormView.js | 5 +++++ .../web/src/formview/FormViewContextMenu.js | 20 ++++++++++++++----- 5 files changed, 50 insertions(+), 6 deletions(-) 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 011bb54f..d68ccb13 100644 --- a/packages/web/src/datagrid/DataGridCore.js +++ b/packages/web/src/datagrid/DataGridCore.js @@ -382,6 +382,7 @@ export default function DataGridCore(props) { openFreeTable={handleOpenFreeTable} openChartSelection={handleOpenChart} openActiveChart={openActiveChart} + switchToForm={handleSwitchToFormView} /> ); }; @@ -720,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(); @@ -950,6 +956,17 @@ export default function DataGridCore(props) { } : 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()', @@ -1090,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/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/formview/FormView.js b/packages/web/src/formview/FormView.js index 5457bad7..47dfd903 100644 --- a/packages/web/src/formview/FormView.js +++ b/packages/web/src/formview/FormView.js @@ -330,6 +330,11 @@ export default function FormView(props) { onReload(); } + if (event.keyCode == keycodes.f4) { + event.preventDefault(); + handleSwitchToTable(); + } + if ( !event.ctrlKey && !event.altKey && diff --git a/packages/web/src/formview/FormViewContextMenu.js b/packages/web/src/formview/FormViewContextMenu.js index 44ff1940..02e25012 100644 --- a/packages/web/src/formview/FormViewContextMenu.js +++ b/packages/web/src/formview/FormViewContextMenu.js @@ -4,12 +4,22 @@ import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu'; export default function FormViewContextMenu({ switchToTable, onNavigate }) { return ( <> - Table view + + Table view + - onNavigate('begin')}>Navigate to begin - onNavigate('previous')}>Navigate to previous - onNavigate('next')}>Navigate to next - onNavigate('end')}>Navigate to end + 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 + ); }