Skip to content

Commit

Permalink
feat: add Table component & columns types
Browse files Browse the repository at this point in the history
  • Loading branch information
csm-thu committed Dec 1, 2021
1 parent d123f6d commit f1ed531
Show file tree
Hide file tree
Showing 7 changed files with 429 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export {
BasicEnumInput,
UploadFile,
UPLOAD_FILE_STATUS_KEY,
Table,
TABLE_DATA_STATUS,
} from './inputs';
export { UserInfo, HelpMenu } from './menus';
export { PrivateRoute, PublicRoute, LoadingLine } from './misc';
169 changes: 169 additions & 0 deletions src/inputs/Table/ColumnTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Copyright (c) Cosmo Tech.
// Licensed under the MIT license.

import { DateUtils } from '@cosmotech/core';

const _editModeSetter = (params) => {
let newValue = params.newValue;
if (!params.context.editMode) {
newValue = params.oldValue;
}
params.data[params.colDef.field] = newValue;
return true;
};

const _boolSetter = (params) => {
let newValue = params.newValue.toLowerCase();
if (!params.context.editMode) {
newValue = params.oldValue;
} else if (['0', 'false', 'no'].indexOf(newValue) !== -1) {
newValue = 'false';
} else if (['1', 'true', 'yes'].indexOf(newValue) !== -1) {
newValue = 'true';
} else {
newValue = params.oldValue;
}
params.data[params.colDef.field] = newValue;
return true;
};

const _dateSetter = (params) => {
const dateFormat = params.context.dateFormat;
let newValue = params.newValue;
if (!params.context.editMode || !DateUtils.isValid(newValue, dateFormat)) {
newValue = params.oldValue;
} else {
const minValue = params.column.userProvidedColDef?.cellEditorParams?.minValue;
const maxValue = params.column.userProvidedColDef?.cellEditorParams?.maxValue;
if (minValue !== undefined) {
newValue = DateUtils.strMax(newValue, minValue, dateFormat) || params.oldValue;
}
if (maxValue !== undefined) {
newValue = DateUtils.strMin(newValue, maxValue, dateFormat) || params.oldValue;
}
}
// Force date format before setting it
params.data[params.colDef.field] = DateUtils.format(DateUtils.parse(newValue, dateFormat), dateFormat);
return true;
};

const _intSetter = (params) => {
let newValue = parseInt(params.newValue);

if (!params.context.editMode || !Number.isSafeInteger(newValue)) {
newValue = params.oldValue;
} else {
// Min & max values are currently limited by the default cellEditor behavior
const DEFAULT_MIN_INT = -1e21 + 1;
const DEFAULT_MAX_INT = 1e21 - 1;
const configMinValue = params.column.userProvidedColDef?.cellEditorParams?.minValue;
const configMaxValue = params.column.userProvidedColDef?.cellEditorParams?.maxValue;
const minValue = configMinValue !== undefined ? configMinValue : DEFAULT_MIN_INT;
const maxValue = configMaxValue !== undefined ? configMaxValue : DEFAULT_MAX_INT;
newValue = Math.max(newValue, minValue);
newValue = Math.min(newValue, maxValue);
newValue = newValue.toString();
}
params.data[params.colDef.field] = newValue;
return true;
};

const _numberSetter = (params) => {
let newValue = parseFloat(params.newValue);
if (!params.context.editMode || isNaN(newValue)) {
newValue = params.oldValue;
} else {
const minValue = params.column.userProvidedColDef?.cellEditorParams?.minValue;
const maxValue = params.column.userProvidedColDef?.cellEditorParams?.maxValue;
if (minValue !== undefined) {
newValue = Math.max(newValue, minValue);
}
if (maxValue !== undefined) {
newValue = Math.min(newValue, maxValue);
}
newValue = newValue.toString();
}
params.data[params.colDef.field] = newValue;
return true;
};

const _enumSetter = (params) => {
const enumValues = params.column.userProvidedColDef?.cellEditorParams?.enumValues || [];
if (enumValues.length === 0) {
console.warn(`Missing enum values for table column "${params.column.colId}"`);
}

let newValue = params.newValue;
if (!params.context.editMode || enumValues.indexOf(newValue) === -1) {
newValue = params.oldValue;
}
params.data[params.colDef.field] = newValue;
return true;
};

const _intFilterValueGetter = (params) => {
return parseInt(params.data?.[params.column.colId]);
};

const _numberFilterValueGetter = (params) => {
return parseFloat(params.data?.[params.column.colId]);
};

const _dateFilterValueGetter = (params) => {
const dateFormat = params.context.dateFormat;
const strValue = params.data?.[params.column.colId];
return DateUtils.parse(strValue, dateFormat);
};

export const getDefaultColumnsProperties = (onCellChange) => {
return {
editable: (params) => params.context.editMode,
resizable: true,
sortable: true,
filter: 'agTextColumnFilter',
valueSetter: _editModeSetter,
onCellValueChanged: (event) => {
onCellChange(event);
},
};
};

export const getColumnTypes = (dateFormat) => {
const _dateComparator = (valueA, valueB, nodeA, nodeB, isInverted) => {
return DateUtils.parse(valueA, dateFormat) > DateUtils.parse(valueB, dateFormat) ? 1 : -1;
};

const _numberComparator = (valueA, valueB, nodeA, nodeB, isInverted) => {
return Number(valueA) > Number(valueB) ? 1 : -1;
};

return {
nonEditable: { editable: false },
nonResizable: { resizable: false },
nonSortable: { sortable: false },
bool: {
valueSetter: _boolSetter,
},
date: {
comparator: _dateComparator,
filter: 'agDateColumnFilter',
valueSetter: _dateSetter,
filterValueGetter: _dateFilterValueGetter,
},
enum: {
valueSetter: _enumSetter,
},
int: {
comparator: _numberComparator,
filter: 'agNumberColumnFilter',
valueSetter: _intSetter,
filterValueGetter: _intFilterValueGetter,
},
number: {
comparator: _numberComparator,
filter: 'agNumberColumnFilter',
valueSetter: _numberSetter,
filterValueGetter: _numberFilterValueGetter,
},
};
};
200 changes: 200 additions & 0 deletions src/inputs/Table/Table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// Copyright (c) Cosmo Tech.
// Licensed under the MIT license.

import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { CircularProgress, makeStyles, Typography } from '@material-ui/core';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-balham-dark.css';
import { DateUtils } from '@cosmotech/core';
import { getColumnTypes, getDefaultColumnsProperties } from './ColumnTypes.js';
import { TABLE_DATA_STATUS } from './TableDataStatus';

const useStyles = makeStyles((theme) => ({
toolBar: {
height: '40px',
display: 'flex',
flexDirection: 'row',
flexFlow: 'flex-start',
alignItems: 'stretch',
marginTop: '10px',
marginBottom: '6px;',
},
errorsContainer: {
backgroundColor: theme.palette.black,
color: theme.palette.text.error,
fontSize: '15px',
weight: 600,
marginTop: '10px',
padding: '4px',
whiteSpace: 'pre-line',
},
}));

const LOADING_STATUS_MAPPING = {
[TABLE_DATA_STATUS.EMPTY]: false,
[TABLE_DATA_STATUS.UPLOADING]: true,
[TABLE_DATA_STATUS.DOWNLOADING]: true,
[TABLE_DATA_STATUS.PARSING]: true,
[TABLE_DATA_STATUS.READY]: false,
[TABLE_DATA_STATUS.ERROR]: false,
};

const _formatMinMaxDatesInColumns = (col, dateFormat) => {
if (col.type && col.type.indexOf('date') !== -1) {
if (col.minValue) {
col.minValue = DateUtils.format(new Date(col.minValue), dateFormat) || col.minValue;
}
if (col.maxValue) {
col.maxValue = DateUtils.format(new Date(col.maxValue), dateFormat) || col.maxValue;
}
}
};

const _moveKeyToCellEditorParams = (col, key) => {
if (Object.keys(col).indexOf(key) !== -1) {
col.cellEditorParams = {
...col.cellEditorParams,
[key]: JSON.parse(JSON.stringify(col[key])),
};
delete col[key];
}
};

const _moveExtraPropertiesToCellEditorParams = (col) => {
const keys = ['enumValues', 'minValue', 'maxValue'];
keys.forEach((key) => _moveKeyToCellEditorParams(col, key));
};

const _formatColumnsData = (columns, dateFormat) =>
columns.map((col, index) => {
_formatMinMaxDatesInColumns(col, dateFormat);
_moveExtraPropertiesToCellEditorParams(col);
return col;
});

export const Table = (props) => {
const {
dateFormat,
editMode,
dataStatus,
errors,
height,
width,
columns,
rows,
labels,
extraToolbarActions,
onCellChange,
...otherProps
} = props;
const dimensions = { height: height, width: width };
const classes = useStyles();

const context = {
dateFormat: dateFormat,
editMode: editMode,
};

const defaultColDef = getDefaultColumnsProperties(onCellChange);
const columnTypes = getColumnTypes(dateFormat);
const formattedColumns = useMemo(() => _formatColumnsData(columns, dateFormat), [columns, dateFormat]);
const hasErrors = errors && errors.length > 0;
const isLoading = LOADING_STATUS_MAPPING[dataStatus];
const isReady = dataStatus === TABLE_DATA_STATUS.READY;

return (
<div id="table-container" {...otherProps}>
<Typography data-cy="label">{labels.label}</Typography>
<div className={classes.toolBar}>
{extraToolbarActions}
{isLoading && (
<div>
{labels.loading}
<CircularProgress />
</div>
)}
</div>
{hasErrors && <div className={classes.errorsContainer}>{errors.join('\n')}</div>}
<div data-cy="grid" id="grid-container" style={dimensions} className="ag-theme-balham-dark">
{isReady && (
<AgGridReact
undoRedoCellEditing={true}
rowDragManaged={true}
suppressDragLeaveHidesColumns={true}
allowDragFromColumnsToolPanel={true}
columnDefs={formattedColumns}
defaultColDef={defaultColDef}
columnTypes={columnTypes}
rowData={rows}
context={context}
/>
)}
</div>
</div>
);
};

Table.propTypes = {
/**
* Custom date format for columns of type "date". Default value: 'dd/MM/yyyy'
*/
dateFormat: PropTypes.string,
/**
* Define whether or not the table can be edited
*/
editMode: PropTypes.bool.isRequired,
/**
* Define the current status of the table data (c.f. TableDataStatus.js)
*/
dataStatus: PropTypes.string,
/**
* List of errors to display instead of the table
*/
errors: PropTypes.array,
/**
* Table height
*/
height: PropTypes.string,
/**
* Table width
*/
width: PropTypes.string,
columns: PropTypes.array.isRequired,
rows: PropTypes.array.isRequired,
/**
* Component's labels:
* Structure:
* <pre>
{
label: 'string'
}
</pre>
*/
labels: PropTypes.shape({
label: PropTypes.string,
loading: PropTypes.string,
}),
/**
* List of extra React elements to add in the Table toolbar
*/
extraToolbarActions: PropTypes.arrayOf(PropTypes.node),
/**
* Callback function that will be called when a cell is edited
* Function parameters:
* event: object containing the ag grid veent data
*/
onCellChange: PropTypes.func,
};

Table.defaultProps = {
dateFormat: 'dd/MM/yyyy',
dataStatus: TABLE_DATA_STATUS.EMPTY,
height: '200px',
width: '100%',
labels: {
loading: 'Loading...',
},
onCellChange: () => {},
};
Loading

0 comments on commit f1ed531

Please sign in to comment.