- Add feature to allow collection header form using CustomControl. - Framework stability and bug fixes.

This commit is contained in:
Aditya Toshniwal 2021-08-09 17:36:17 +05:30 committed by Akshay Joshi
parent 08f2121544
commit 9bfef1f6e5
6 changed files with 231 additions and 64 deletions

View File

@ -78,6 +78,8 @@ const useStyles = makeStyles((theme)=>({
...theme.mixins.panelBorder.bottom,
...theme.mixins.panelBorder.right,
position: 'relative',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
tableCellHeader: {
fontWeight: theme.typography.fontWeightBold,
@ -159,6 +161,20 @@ function DataTableRow({row, totalRows, isResizing, schema, schemaRef, accessPath
let retVal = [];
/* Calculate the fields which depends on the current field
deps has info on fields which the current field depends on. */
schema.fields.forEach((field)=>{
(evalFunc(null, field.deps) || []).forEach((dep)=>{
let source = accessPath.concat(dep);
if(_.isArray(dep)) {
source = dep;
/* If its an array, then dep is from the top schema and external */
retVal.push(source);
}
});
});
return retVal;
}, []);
useEffect(()=>{
schema.fields.forEach((field)=>{
/* Self change is also dep change */
if(field.depChange) {
@ -168,13 +184,14 @@ function DataTableRow({row, totalRows, isResizing, schema, schemaRef, accessPath
let source = accessPath.concat(dep);
if(_.isArray(dep)) {
source = dep;
/* If its an array, then dep is from the top schema */
retVal.push(source);
}
depListener.addDepListener(source, accessPath.concat(field.id), field.depChange);
});
});
return retVal;
return ()=>{
/* Cleanup the listeners when unmounting */
depListener.removeDepListener(accessPath);
};
}, []);
/* External deps values are from top schema sess data */
@ -201,12 +218,28 @@ function DataTableRow({row, totalRows, isResizing, schema, schemaRef, accessPath
</div>, depsMap);
}
export function DataGridHeader({label, canAdd, onAddClick}) {
const classes = useStyles();
return (
<Box className={classes.gridHeader}>
<Box className={classes.gridHeaderText}>{label}</Box>
<Box className={classes.gridControls}>
{canAdd && <PgIconButton data-test="add-row" title={gettext('Add row')} onClick={onAddClick} icon={<AddIcon />} className={classes.gridControlsButton} />}
</Box>
</Box>
);
}
DataGridHeader.propTypes = {
label: PropTypes.string,
canAdd: PropTypes.bool,
onAddClick: PropTypes.func,
};
export default function DataGridView({
value, viewHelperProps, formErr, schema, accessPath, dataDispatch, containerClassName,
fixedRows, ...props}) {
const classes = useStyles();
const stateUtils = useContext(StateUtilsContext);
const depListener = useContext(DepListenerContext);
/* Using ref so that schema variable is not frozen in columns closure */
const schemaRef = useRef(schema);
@ -268,7 +301,6 @@ export default function DataGridView({
value: row.index,
});
depListener.removeDepListener(accessPath.concat(row.index));
}, ()=>{}, props.customDeleteTitle, props.customDeleteMsg);
}} className={classes.gridRowButton} disabled={!canDeleteRow} />
);
@ -349,22 +381,30 @@ export default function DataGridView({
})
);
return cols;
},[]
},[props.canEdit, props.canDelete]
);
const onAddClick = useCallback(()=>{
if(props.canAddRow) {
let state = schemaRef.current.top ? schemaRef.current.top.sessData : schemaRef.current.sessData;
let canAddRow = evalFunc(schemaRef.current, props.canAddRow, state || {});
if(!canAddRow) {
return;
}
}
let newRow = schemaRef.current.getNewData();
dataDispatch({
type: SCHEMA_STATE_ACTIONS.ADD_ROW,
path: accessPath,
value: newRow,
});
});
}, []);
const defaultColumn = useMemo(()=>({
minWidth: 175,
width: 0,
}));
}), []);
let tablePlugins = [
useBlockLayout,
@ -393,6 +433,8 @@ export default function DataGridView({
useEffect(()=>{
let rowsPromise = fixedRows, umounted=false;
/* If fixedRows is defined, fetch the details */
if(typeof rowsPromise === 'function') {
rowsPromise = rowsPromise();
}
@ -417,12 +459,7 @@ export default function DataGridView({
return (
<Box className={containerClassName}>
<Box className={classes.grid}>
{(props.label || props.canAdd) && <Box className={classes.gridHeader}>
<Box className={classes.gridHeaderText}>{props.label}</Box>
<Box className={classes.gridControls}>
{props.canAdd && <PgIconButton data-test="add-row" title={gettext('Add row')} onClick={onAddClick} icon={<AddIcon />} className={classes.gridControlsButton} />}
</Box>
</Box>}
{(props.label || props.canAdd) && <DataGridHeader label={props.label} canAdd={props.canAdd} onAddClick={onAddClick} />}
<div {...getTableProps()} className={classes.table}>
<DataTableHeader headerGroups={headerGroups} />
<div {...getTableBodyProps()}>
@ -460,6 +497,9 @@ DataGridView.propTypes = {
canAdd: PropTypes.bool,
canDelete: PropTypes.bool,
visible: PropTypes.bool,
canAddRow: PropTypes.oneOfType([
PropTypes.bool, PropTypes.func,
]),
canEditRow: PropTypes.oneOfType([
PropTypes.bool, PropTypes.func,
]),

View File

@ -25,7 +25,7 @@ export default function FieldSetView({
useEffect(()=>{
/* Calculate the fields which depends on the current field */
if(!isDataGridForm) {
if(!isDataGridForm && depListener) {
schema.fields.forEach((field)=>{
/* Self change is also dep change */
if(field.depChange || field.deferredDepChange) {

View File

@ -122,7 +122,7 @@ export function getFieldMetaData(field, schema, value, viewHelperProps) {
/* The first component of schema view form */
export default function FormView({
value, formErr, schema={}, viewHelperProps, isNested=false, accessPath, dataDispatch, hasSQLTab,
getSQLValue, onTabChange, firstEleRef, className, isDataGridForm=false, visible}) {
getSQLValue, onTabChange, firstEleRef, className, isDataGridForm=false, isTabView=true, visible}) {
let defaultTab = 'General';
let tabs = {};
let tabsClassname = {};
@ -164,6 +164,10 @@ export default function FormView({
}
});
});
return ()=>{
/* Cleanup the listeners when unmounting */
depListener.removeDepListener(accessPath);
};
}
}, []);
@ -175,7 +179,7 @@ export default function FormView({
getFieldMetaData(field, schema, value, viewHelperProps);
if(modeSupported) {
let {group} = field;
let {group, CustomControl} = field;
group = groupLabels[group] || group || defaultTab;
if(!tabs[group]) tabs[group] = [];
@ -190,7 +194,7 @@ export default function FormView({
}
tabs[group].push(
<FormView key={`nested${tabs[group].length}`} value={value} viewHelperProps={viewHelperProps} formErr={formErr}
schema={field.schema} accessPath={accessPath} dataDispatch={dataDispatch} isNested={true} isDataGridForm={isDataGridForm}
schema={field.schema} accessPath={accessPath} dataDispatch={dataDispatch} isNested={true} isDataGridForm={isDataGridForm}
{...field} visible={visible}/>
);
} else if(field.type === 'nested-fieldset') {
@ -223,11 +227,18 @@ export default function FormView({
canDelete = false;
}
tabs[group].push(
<DataGridView key={field.id} value={value[field.id]} viewHelperProps={viewHelperProps} formErr={formErr}
schema={field.schema} accessPath={accessPath.concat(field.id)} dataDispatch={dataDispatch} containerClassName={classes.controlRow}
{...field} canAdd={canAdd} canEdit={canEdit} canDelete={canDelete} visible={visible}/>
);
const props = {
key: field.id, value: value[field.id], viewHelperProps: viewHelperProps, formErr: formErr,
schema: field.schema, accessPath: accessPath.concat(field.id), dataDispatch: dataDispatch,
containerClassName: classes.controlRow, ...field, canAdd: canAdd, canEdit: canEdit, canDelete: canDelete,
visible: visible,
};
if(CustomControl) {
tabs[group].push(<CustomControl {...props}/>);
} else {
tabs[group].push(<DataGridView {...props}/>);
}
} else if(field.type === 'group') {
groupLabels[field.id] = field.label;
if(!visible) {
@ -311,35 +322,48 @@ export default function FormView({
return <></>;
}
return (
<>
<Box height="100%" display="flex" flexDirection="column" className={className} ref={formRef}>
<Box>
<Tabs
value={tabValue}
onChange={(event, selTabValue) => {
setTabValue(selTabValue);
}}
// indicatorColor="primary"
variant="scrollable"
scrollButtons="auto"
action={(ref)=>ref && ref.updateIndicator()}
>
{Object.keys(tabs).map((tabName)=>{
return <Tab key={tabName} label={tabName} />;
})}
</Tabs>
if(isTabView) {
return (
<>
<Box height="100%" display="flex" flexDirection="column" className={className} ref={formRef}>
<Box>
<Tabs
value={tabValue}
onChange={(event, selTabValue) => {
setTabValue(selTabValue);
}}
// indicatorColor="primary"
variant="scrollable"
scrollButtons="auto"
action={(ref)=>ref && ref.updateIndicator()}
>
{Object.keys(tabs).map((tabName)=>{
return <Tab key={tabName} label={tabName} />;
})}
</Tabs>
</Box>
{Object.keys(tabs).map((tabName, i)=>{
return (
<TabPanel key={tabName} value={tabValue} index={i} classNameRoot={clsx(tabsClassname[tabName], isNested ? classes.nestedTabPanel : null)}
className={fullTabs.indexOf(tabName) == -1 ? classes.nestedControl : null}>
{tabs[tabName]}
</TabPanel>
);
})}
</Box>
{Object.keys(tabs).map((tabName, i)=>{
return (
<TabPanel key={tabName} value={tabValue} index={i} classNameRoot={clsx(tabsClassname[tabName], isNested ? classes.nestedTabPanel : null)}
className={fullTabs.indexOf(tabName) == -1 ? classes.nestedControl : null}>
{tabs[tabName]}
</TabPanel>
);
})}
</Box>
</>);
</>);
} else {
return (
<>
<Box height="100%" display="flex" flexDirection="column" className={className} ref={formRef}>
{Object.keys(tabs).map((tabName)=>{
return (
<>{tabs[tabName]}</>
);
})}
</Box>
</>);
}
}
FormView.propTypes = {

View File

@ -126,4 +126,9 @@ export default class BaseUISchema {
});
return newRow;
}
/* Used in header schema */
addDisabled() {
return false;
}
}

View File

@ -28,7 +28,7 @@ import { minMaxValidator, numberValidator, integerValidator, emptyValidator, che
import { MappedFormControl } from './MappedControl';
import gettext from 'sources/gettext';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import FormView from './FormView';
import FormView, { getFieldMetaData } from './FormView';
import { pgAlertify } from '../helpers/legacyConnector';
import { evalFunc } from 'sources/utils';
import PropTypes from 'prop-types';
@ -113,9 +113,9 @@ const diffArrayOptions = {
compareFunction: objectComparator,
};
function getChangedData(topSchema, mode, sessData, stringify=false) {
function getChangedData(topSchema, viewHelperProps, sessData, stringify=false) {
let changedData = {};
let isEdit = mode === 'edit';
let isEdit = viewHelperProps.mode === 'edit';
/* The comparator and setter */
const attrChanged = (currPath, change, force=false)=>{
@ -136,6 +136,10 @@ function getChangedData(topSchema, mode, sessData, stringify=false) {
/* Will be called recursively as data can be nested */
const parseChanges = (schema, accessPath, changedData)=>{
schema.fields.forEach((field)=>{
let {modeSupported} = getFieldMetaData(field, schema, {}, viewHelperProps);
if(!modeSupported) {
return;
}
if(typeof(field.type) == 'string' && field.type.startsWith('nested-')) {
/* its nested */
parseChanges(field.schema, accessPath, changedData);
@ -324,7 +328,6 @@ const sessDataReducer = (state, action)=>{
/* If there is any dep listeners get the changes */
data = getDepChange(action.path, data, state, action);
deferredList = getDeferredDepChange(action.path, data, state, action);
// let deferredInfo = getDeferredDepChange(action.path, data, state, action);
data.__deferred__ = deferredList || [];
break;
case SCHEMA_STATE_ACTIONS.ADD_ROW:
@ -387,7 +390,7 @@ function prepareData(val) {
/* If its the dialog */
function SchemaDialogView({
getInitData, viewHelperProps, schema={}, ...props}) {
getInitData, viewHelperProps, schema={}, showFooter=true, isTabView=true, ...props}) {
const classes = useDialogStyles();
/* Some useful states */
const [dirty, setDirty] = useState(false);
@ -421,11 +424,12 @@ function SchemaDialogView({
if(!isNotValid) setFormErr({});
/* check if anything changed */
let dataChanged = Object.keys(getChangedData(schema, viewHelperProps.mode, sessData)).length > 0;
setDirty(dataChanged);
let changedData = getChangedData(schema, viewHelperProps, sessData);
let isDataChanged = Object.keys(changedData).length > 0;
setDirty(isDataChanged);
/* tell the callbacks the data has changed */
props.onDataChange && props.onDataChange(dataChanged);
props.onDataChange && props.onDataChange(isDataChanged, changedData);
}, [sessData]);
useEffect(()=>{
@ -518,7 +522,7 @@ function SchemaDialogView({
setSaving(true);
setLoaderText('Saving...');
/* Get the changed data */
let changeData = getChangedData(schema, viewHelperProps.mode, sessData);
let changeData = getChangedData(schema, viewHelperProps, sessData);
/* Add the id when in edit mode */
if(viewHelperProps.mode !== 'edit') {
@ -577,7 +581,7 @@ function SchemaDialogView({
/* Called when SQL tab is active */
if(dirty) {
if(!formErr.name) {
let changeData = getChangedData(schema, viewHelperProps.mode, sessData);
let changeData = getChangedData(schema, viewHelperProps, sessData);
if(viewHelperProps.mode !== 'edit') {
/* If new then merge the changed data with origData */
changeData = _.assign({}, schema.origData, changeData);
@ -626,11 +630,11 @@ function SchemaDialogView({
<Loader message={loaderText}/>
<FormView value={sessData} viewHelperProps={viewHelperProps} formErr={formErr}
schema={schema} accessPath={[]} dataDispatch={sessDispatchWithListener}
hasSQLTab={props.hasSQL} getSQLValue={getSQLValue} firstEleRef={firstEleRef} />
hasSQLTab={props.hasSQL} getSQLValue={getSQLValue} firstEleRef={firstEleRef} isTabView={isTabView} />
<FormFooterMessage type={MESSAGE_TYPE.ERROR} message={formErr.message}
onClose={onErrClose} />
</Box>
<Box className={classes.footer}>
{showFooter && <Box className={classes.footer}>
{useMemo(()=><Box>
<PgIconButton data-test="sql-help" onClick={()=>props.onHelp(true, isNew)} icon={<InfoIcon />}
disabled={props.disableSqlHelp} className={classes.buttonMargin} title="SQL help for this object type."/>
@ -648,7 +652,7 @@ function SchemaDialogView({
{gettext('Save')}
</PrimaryButton>
</Box>
</Box>
</Box>}
</Box>
</DepListenerContext.Provider>
</StateUtilsContext.Provider>
@ -671,10 +675,12 @@ SchemaDialogView.propTypes = {
onHelp: PropTypes.func,
onDataChange: PropTypes.func,
confirmOnCloseReset: PropTypes.bool,
isTabView: PropTypes.bool,
hasSQL: PropTypes.bool,
getSQLValue: PropTypes.func,
disableSqlHelp: PropTypes.bool,
disableDialogHelp: PropTypes.bool,
showFooter: PropTypes.bool,
};
const usePropsStyles = makeStyles((theme)=>({

View File

@ -0,0 +1,92 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Box, makeStyles } from '@material-ui/core';
import DataGridView, { DataGridHeader } from '../SchemaView/DataGridView';
import SchemaView, { SCHEMA_STATE_ACTIONS } from '../SchemaView';
import { DefaultButton } from '../components/Buttons';
import { evalFunc } from '../utils';
import PropTypes from 'prop-types';
import CustomPropTypes from '../custom_prop_types';
const useStyles = makeStyles((theme)=>({
formBorder: {
...theme.mixins.panelBorder,
borderBottom: 0,
},
form: {
padding: '0.25rem',
},
addBtn: {
marginLeft: 'auto',
}
}));
export default function DataGridViewWithHeaderForm(props) {
let {containerClassName, headerSchema, headerVisible, ...otherProps} = props;
const classes = useStyles();
const headerFormData = useRef({});
const schemaRef = useRef(otherProps.schema);
const [isAddDisabled, setAddDisabled] = useState(true);
const onAddClick = useCallback(()=>{
if(otherProps.canAddRow) {
let state = schemaRef.current.top ? schemaRef.current.top.sessData : schemaRef.current.sessData;
let canAddRow = evalFunc(schemaRef.current, otherProps.canAddRow, state || {});
if(!canAddRow) {
return;
}
}
let newRow = headerSchema.getNewData(headerFormData.current);
otherProps.dataDispatch({
type: SCHEMA_STATE_ACTIONS.ADD_ROW,
path: otherProps.accessPath,
value: newRow,
});
}, []);
useEffect(()=>{
headerSchema.top = schemaRef.current.top;
}, []);
let state = schemaRef.current.top ? schemaRef.current.top.origData : schemaRef.current.origData;
headerVisible = headerVisible && evalFunc(null, headerVisible, state);
return (
<Box className={containerClassName}>
<Box className={classes.formBorder}>
<DataGridHeader label={props.label} />
{headerVisible && <Box className={classes.form}>
<SchemaView
formType={'dialog'}
getInitData={()=>Promise.resolve({})}
schema={headerSchema}
viewHelperProps={props.viewHelperProps}
showFooter={false}
onDataChange={(isDataChanged, dataChanged)=>{
headerFormData.current = dataChanged;
setAddDisabled(headerSchema.addDisabled(headerFormData.current));
}}
hasSQL={false}
isTabView={false}
/>
<Box display="flex">
<DefaultButton className={classes.addBtn} onClick={onAddClick} disabled={isAddDisabled}>Add</DefaultButton>
</Box>
</Box>}
</Box>
<DataGridView {...otherProps} canAdd={false}/>
</Box>
);
}
DataGridViewWithHeaderForm.propTypes = {
label: PropTypes.string,
value: PropTypes.array,
viewHelperProps: PropTypes.object,
formErr: PropTypes.object,
headerSchema: CustomPropTypes.schemaUI.isRequired,
headerVisible: PropTypes.func,
schema: CustomPropTypes.schemaUI,
accessPath: PropTypes.array.isRequired,
dataDispatch: PropTypes.func.isRequired,
containerClassName: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
};