Skip to content

Commit f1ed531

Browse files
committed
feat: add Table component & columns types
1 parent d123f6d commit f1ed531

File tree

7 files changed

+429
-0
lines changed

7 files changed

+429
-0
lines changed

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export {
1414
BasicEnumInput,
1515
UploadFile,
1616
UPLOAD_FILE_STATUS_KEY,
17+
Table,
18+
TABLE_DATA_STATUS,
1719
} from './inputs';
1820
export { UserInfo, HelpMenu } from './menus';
1921
export { PrivateRoute, PublicRoute, LoadingLine } from './misc';

src/inputs/Table/ColumnTypes.js

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// Copyright (c) Cosmo Tech.
2+
// Licensed under the MIT license.
3+
4+
import { DateUtils } from '@cosmotech/core';
5+
6+
const _editModeSetter = (params) => {
7+
let newValue = params.newValue;
8+
if (!params.context.editMode) {
9+
newValue = params.oldValue;
10+
}
11+
params.data[params.colDef.field] = newValue;
12+
return true;
13+
};
14+
15+
const _boolSetter = (params) => {
16+
let newValue = params.newValue.toLowerCase();
17+
if (!params.context.editMode) {
18+
newValue = params.oldValue;
19+
} else if (['0', 'false', 'no'].indexOf(newValue) !== -1) {
20+
newValue = 'false';
21+
} else if (['1', 'true', 'yes'].indexOf(newValue) !== -1) {
22+
newValue = 'true';
23+
} else {
24+
newValue = params.oldValue;
25+
}
26+
params.data[params.colDef.field] = newValue;
27+
return true;
28+
};
29+
30+
const _dateSetter = (params) => {
31+
const dateFormat = params.context.dateFormat;
32+
let newValue = params.newValue;
33+
if (!params.context.editMode || !DateUtils.isValid(newValue, dateFormat)) {
34+
newValue = params.oldValue;
35+
} else {
36+
const minValue = params.column.userProvidedColDef?.cellEditorParams?.minValue;
37+
const maxValue = params.column.userProvidedColDef?.cellEditorParams?.maxValue;
38+
if (minValue !== undefined) {
39+
newValue = DateUtils.strMax(newValue, minValue, dateFormat) || params.oldValue;
40+
}
41+
if (maxValue !== undefined) {
42+
newValue = DateUtils.strMin(newValue, maxValue, dateFormat) || params.oldValue;
43+
}
44+
}
45+
// Force date format before setting it
46+
params.data[params.colDef.field] = DateUtils.format(DateUtils.parse(newValue, dateFormat), dateFormat);
47+
return true;
48+
};
49+
50+
const _intSetter = (params) => {
51+
let newValue = parseInt(params.newValue);
52+
53+
if (!params.context.editMode || !Number.isSafeInteger(newValue)) {
54+
newValue = params.oldValue;
55+
} else {
56+
// Min & max values are currently limited by the default cellEditor behavior
57+
const DEFAULT_MIN_INT = -1e21 + 1;
58+
const DEFAULT_MAX_INT = 1e21 - 1;
59+
const configMinValue = params.column.userProvidedColDef?.cellEditorParams?.minValue;
60+
const configMaxValue = params.column.userProvidedColDef?.cellEditorParams?.maxValue;
61+
const minValue = configMinValue !== undefined ? configMinValue : DEFAULT_MIN_INT;
62+
const maxValue = configMaxValue !== undefined ? configMaxValue : DEFAULT_MAX_INT;
63+
newValue = Math.max(newValue, minValue);
64+
newValue = Math.min(newValue, maxValue);
65+
newValue = newValue.toString();
66+
}
67+
params.data[params.colDef.field] = newValue;
68+
return true;
69+
};
70+
71+
const _numberSetter = (params) => {
72+
let newValue = parseFloat(params.newValue);
73+
if (!params.context.editMode || isNaN(newValue)) {
74+
newValue = params.oldValue;
75+
} else {
76+
const minValue = params.column.userProvidedColDef?.cellEditorParams?.minValue;
77+
const maxValue = params.column.userProvidedColDef?.cellEditorParams?.maxValue;
78+
if (minValue !== undefined) {
79+
newValue = Math.max(newValue, minValue);
80+
}
81+
if (maxValue !== undefined) {
82+
newValue = Math.min(newValue, maxValue);
83+
}
84+
newValue = newValue.toString();
85+
}
86+
params.data[params.colDef.field] = newValue;
87+
return true;
88+
};
89+
90+
const _enumSetter = (params) => {
91+
const enumValues = params.column.userProvidedColDef?.cellEditorParams?.enumValues || [];
92+
if (enumValues.length === 0) {
93+
console.warn(`Missing enum values for table column "${params.column.colId}"`);
94+
}
95+
96+
let newValue = params.newValue;
97+
if (!params.context.editMode || enumValues.indexOf(newValue) === -1) {
98+
newValue = params.oldValue;
99+
}
100+
params.data[params.colDef.field] = newValue;
101+
return true;
102+
};
103+
104+
const _intFilterValueGetter = (params) => {
105+
return parseInt(params.data?.[params.column.colId]);
106+
};
107+
108+
const _numberFilterValueGetter = (params) => {
109+
return parseFloat(params.data?.[params.column.colId]);
110+
};
111+
112+
const _dateFilterValueGetter = (params) => {
113+
const dateFormat = params.context.dateFormat;
114+
const strValue = params.data?.[params.column.colId];
115+
return DateUtils.parse(strValue, dateFormat);
116+
};
117+
118+
export const getDefaultColumnsProperties = (onCellChange) => {
119+
return {
120+
editable: (params) => params.context.editMode,
121+
resizable: true,
122+
sortable: true,
123+
filter: 'agTextColumnFilter',
124+
valueSetter: _editModeSetter,
125+
onCellValueChanged: (event) => {
126+
onCellChange(event);
127+
},
128+
};
129+
};
130+
131+
export const getColumnTypes = (dateFormat) => {
132+
const _dateComparator = (valueA, valueB, nodeA, nodeB, isInverted) => {
133+
return DateUtils.parse(valueA, dateFormat) > DateUtils.parse(valueB, dateFormat) ? 1 : -1;
134+
};
135+
136+
const _numberComparator = (valueA, valueB, nodeA, nodeB, isInverted) => {
137+
return Number(valueA) > Number(valueB) ? 1 : -1;
138+
};
139+
140+
return {
141+
nonEditable: { editable: false },
142+
nonResizable: { resizable: false },
143+
nonSortable: { sortable: false },
144+
bool: {
145+
valueSetter: _boolSetter,
146+
},
147+
date: {
148+
comparator: _dateComparator,
149+
filter: 'agDateColumnFilter',
150+
valueSetter: _dateSetter,
151+
filterValueGetter: _dateFilterValueGetter,
152+
},
153+
enum: {
154+
valueSetter: _enumSetter,
155+
},
156+
int: {
157+
comparator: _numberComparator,
158+
filter: 'agNumberColumnFilter',
159+
valueSetter: _intSetter,
160+
filterValueGetter: _intFilterValueGetter,
161+
},
162+
number: {
163+
comparator: _numberComparator,
164+
filter: 'agNumberColumnFilter',
165+
valueSetter: _numberSetter,
166+
filterValueGetter: _numberFilterValueGetter,
167+
},
168+
};
169+
};

src/inputs/Table/Table.js

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// Copyright (c) Cosmo Tech.
2+
// Licensed under the MIT license.
3+
4+
import React, { useMemo } from 'react';
5+
import PropTypes from 'prop-types';
6+
import { CircularProgress, makeStyles, Typography } from '@material-ui/core';
7+
import { AgGridReact } from 'ag-grid-react';
8+
import 'ag-grid-community/dist/styles/ag-grid.css';
9+
import 'ag-grid-community/dist/styles/ag-theme-balham-dark.css';
10+
import { DateUtils } from '@cosmotech/core';
11+
import { getColumnTypes, getDefaultColumnsProperties } from './ColumnTypes.js';
12+
import { TABLE_DATA_STATUS } from './TableDataStatus';
13+
14+
const useStyles = makeStyles((theme) => ({
15+
toolBar: {
16+
height: '40px',
17+
display: 'flex',
18+
flexDirection: 'row',
19+
flexFlow: 'flex-start',
20+
alignItems: 'stretch',
21+
marginTop: '10px',
22+
marginBottom: '6px;',
23+
},
24+
errorsContainer: {
25+
backgroundColor: theme.palette.black,
26+
color: theme.palette.text.error,
27+
fontSize: '15px',
28+
weight: 600,
29+
marginTop: '10px',
30+
padding: '4px',
31+
whiteSpace: 'pre-line',
32+
},
33+
}));
34+
35+
const LOADING_STATUS_MAPPING = {
36+
[TABLE_DATA_STATUS.EMPTY]: false,
37+
[TABLE_DATA_STATUS.UPLOADING]: true,
38+
[TABLE_DATA_STATUS.DOWNLOADING]: true,
39+
[TABLE_DATA_STATUS.PARSING]: true,
40+
[TABLE_DATA_STATUS.READY]: false,
41+
[TABLE_DATA_STATUS.ERROR]: false,
42+
};
43+
44+
const _formatMinMaxDatesInColumns = (col, dateFormat) => {
45+
if (col.type && col.type.indexOf('date') !== -1) {
46+
if (col.minValue) {
47+
col.minValue = DateUtils.format(new Date(col.minValue), dateFormat) || col.minValue;
48+
}
49+
if (col.maxValue) {
50+
col.maxValue = DateUtils.format(new Date(col.maxValue), dateFormat) || col.maxValue;
51+
}
52+
}
53+
};
54+
55+
const _moveKeyToCellEditorParams = (col, key) => {
56+
if (Object.keys(col).indexOf(key) !== -1) {
57+
col.cellEditorParams = {
58+
...col.cellEditorParams,
59+
[key]: JSON.parse(JSON.stringify(col[key])),
60+
};
61+
delete col[key];
62+
}
63+
};
64+
65+
const _moveExtraPropertiesToCellEditorParams = (col) => {
66+
const keys = ['enumValues', 'minValue', 'maxValue'];
67+
keys.forEach((key) => _moveKeyToCellEditorParams(col, key));
68+
};
69+
70+
const _formatColumnsData = (columns, dateFormat) =>
71+
columns.map((col, index) => {
72+
_formatMinMaxDatesInColumns(col, dateFormat);
73+
_moveExtraPropertiesToCellEditorParams(col);
74+
return col;
75+
});
76+
77+
export const Table = (props) => {
78+
const {
79+
dateFormat,
80+
editMode,
81+
dataStatus,
82+
errors,
83+
height,
84+
width,
85+
columns,
86+
rows,
87+
labels,
88+
extraToolbarActions,
89+
onCellChange,
90+
...otherProps
91+
} = props;
92+
const dimensions = { height: height, width: width };
93+
const classes = useStyles();
94+
95+
const context = {
96+
dateFormat: dateFormat,
97+
editMode: editMode,
98+
};
99+
100+
const defaultColDef = getDefaultColumnsProperties(onCellChange);
101+
const columnTypes = getColumnTypes(dateFormat);
102+
const formattedColumns = useMemo(() => _formatColumnsData(columns, dateFormat), [columns, dateFormat]);
103+
const hasErrors = errors && errors.length > 0;
104+
const isLoading = LOADING_STATUS_MAPPING[dataStatus];
105+
const isReady = dataStatus === TABLE_DATA_STATUS.READY;
106+
107+
return (
108+
<div id="table-container" {...otherProps}>
109+
<Typography data-cy="label">{labels.label}</Typography>
110+
<div className={classes.toolBar}>
111+
{extraToolbarActions}
112+
{isLoading && (
113+
<div>
114+
{labels.loading}
115+
<CircularProgress />
116+
</div>
117+
)}
118+
</div>
119+
{hasErrors && <div className={classes.errorsContainer}>{errors.join('\n')}</div>}
120+
<div data-cy="grid" id="grid-container" style={dimensions} className="ag-theme-balham-dark">
121+
{isReady && (
122+
<AgGridReact
123+
undoRedoCellEditing={true}
124+
rowDragManaged={true}
125+
suppressDragLeaveHidesColumns={true}
126+
allowDragFromColumnsToolPanel={true}
127+
columnDefs={formattedColumns}
128+
defaultColDef={defaultColDef}
129+
columnTypes={columnTypes}
130+
rowData={rows}
131+
context={context}
132+
/>
133+
)}
134+
</div>
135+
</div>
136+
);
137+
};
138+
139+
Table.propTypes = {
140+
/**
141+
* Custom date format for columns of type "date". Default value: 'dd/MM/yyyy'
142+
*/
143+
dateFormat: PropTypes.string,
144+
/**
145+
* Define whether or not the table can be edited
146+
*/
147+
editMode: PropTypes.bool.isRequired,
148+
/**
149+
* Define the current status of the table data (c.f. TableDataStatus.js)
150+
*/
151+
dataStatus: PropTypes.string,
152+
/**
153+
* List of errors to display instead of the table
154+
*/
155+
errors: PropTypes.array,
156+
/**
157+
* Table height
158+
*/
159+
height: PropTypes.string,
160+
/**
161+
* Table width
162+
*/
163+
width: PropTypes.string,
164+
columns: PropTypes.array.isRequired,
165+
rows: PropTypes.array.isRequired,
166+
/**
167+
* Component's labels:
168+
* Structure:
169+
* <pre>
170+
{
171+
label: 'string'
172+
}
173+
</pre>
174+
*/
175+
labels: PropTypes.shape({
176+
label: PropTypes.string,
177+
loading: PropTypes.string,
178+
}),
179+
/**
180+
* List of extra React elements to add in the Table toolbar
181+
*/
182+
extraToolbarActions: PropTypes.arrayOf(PropTypes.node),
183+
/**
184+
* Callback function that will be called when a cell is edited
185+
* Function parameters:
186+
* event: object containing the ag grid veent data
187+
*/
188+
onCellChange: PropTypes.func,
189+
};
190+
191+
Table.defaultProps = {
192+
dateFormat: 'dd/MM/yyyy',
193+
dataStatus: TABLE_DATA_STATUS.EMPTY,
194+
height: '200px',
195+
width: '100%',
196+
labels: {
197+
loading: 'Loading...',
198+
},
199+
onCellChange: () => {},
200+
};

0 commit comments

Comments
 (0)