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