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