-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add Table component & columns types
- Loading branch information
Showing
7 changed files
with
429 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: () => {}, | ||
}; |
Oops, something went wrong.