@amxchange/grid-view-web-client
Version:
amxchange grid view framework web client in react ( a module extracted from existing jax )
856 lines (795 loc) • 34.6 kB
JSX
import React, { Component } from "react";
import Button from "@mui/material/Button";
import ReactTable, { ReactTableDefaults } from "react-table";
import "react-table/react-table.css";
import withFixedColumns from "react-table-hoc-fixed-columns";
import "react-table-hoc-fixed-columns/lib/styles.css"; // important: this line must be placed after react-table css import
import PropTypes from "prop-types";
import moment from "moment";
import isEqual from "react-fast-compare";
import "../../assets/modules/_table-view.scss";
import { Api } from "../../services";
import { commonUtils } from "../../utils";
import { FormField, StackedFAIcon } from "../../modules";
const TABLE_ACTIONS_LOCAL = "LOCAL";
const TABLE_ACTIONS_REMOTE = "REMOTE";
const TABLE_ACTION_ICON_META = {
ACTION_CANCEL: {
handler: "CANCEL_SINGLE_TRNX",
iconClass: "fa-times",
iconBackgroundColor: "#ff0000",
iconColor: "#fff"
}
};
const TABLE_ACTION_ICON_HANDLERS = {
// CANCEL_SINGLE_TRNX: CancelAndReissueSingle
};
const TABLE_UTIL = {
CELL: {
STRING: row => (
<span {...(row.value ? { title: row.value.toUpperCase() } : {})}>
{row.value ? row.value.toUpperCase() : "-"}
</span>
),
NUMBER: row => <span {...(row.value ? { title: row.value } : {})}>{row.value ? row.value : "-"}</span>,
DATE: row => (
<span {...(row.value ? { title: moment(row.value).format("DD/MM/YYYY hh:mm:ss") } : {})}>
{row.value ? moment(row.value).format("DD/MM/YYYY hh:mm:ss") : "-"}
</span>
)
},
FILTER: {
STRING: (filter, row) => {
if (filter.value === "") return true;
return (row[filter.id] || "").toUpperCase().indexOf(filter.value.toUpperCase()) !== -1;
},
NUMBER: (filter, row) => {
if (filter.value === "") return true;
return (
String(row[filter.id] || "")
.toUpperCase()
.indexOf(filter.value.toUpperCase()) !== -1
);
},
DATE: (filter, row) => {
if (filter.value === "") return true;
let convertedDate = moment(row[filter.id]).format("DD/MM/YYYY hh:mm:ss");
return convertedDate.toUpperCase().indexOf(filter.value.toUpperCase()) !== -1;
}
}
};
const API_URLS = {
gridMeta: `${window.CONST?.remoteServerUrl || ""}/emos/grid/api/{gridView}/meta`,
gridData: `${window.CONST?.remoteServerUrl || ""}/emos/grid/api/{gridView}/data`,
gridLock: `${window.CONST?.remoteServerUrl || ""}/emos/api/lock/check`
};
const ReactTableFixedColumns = withFixedColumns(ReactTable);
class DataGridTableView extends Component {
constructor(props) {
super(props);
this.state = {
loading: false,
meta: {},
metaColumns: [],
metaFilters: null,
results: []
};
}
async componentDidMount() {
// console.log("DataGridTableView Mounted!!");
this.props.onRef && this.props.onRef(this);
await this.fetchMeta();
if (this.props.tableActions !== TABLE_ACTIONS_REMOTE) {
this.fetchResults();
}
if (this.props.enableLockMechanism && this.props.tunnelClient) {
this.addListener();
}
}
componentWillUnmount() {
// console.log("DataGridTableView Unmounted!!");
this.props.onRef && this.props.onRef(null);
this.socketService && this.socketService.off();
}
componentDidUpdate(prevProps, prevState) {
// console.log("DataGridTableView Updated!!");
if (!isEqual(this.props.customFilters, prevProps.customFilters)) {
this.fetchResults();
}
}
fetchMeta = async () => {
try {
let { view, tableActions, onFetchData, refreshCache = false } = this.props;
let responseObject =
tableActions === TABLE_ACTIONS_LOCAL && onFetchData
? await onFetchData({ apiName: "gridMeta", view, refreshCache })
: await Api.root.post(API_URLS.gridMeta.replace("{gridView}", view), {});
let meta = responseObject.data.meta;
let metaColumns = this.getColumnsFromMeta(meta);
let metaFilters = this.getFiltersFromMeta(meta);
return new Promise((resolve, reject) => {
this.setState(
{
meta,
metaColumns,
metaFilters
},
() => {
resolve({
meta,
metaColumns,
metaFilters
});
}
);
});
} catch (error) {
console.error("func fetchTableMeta ", error);
throw error;
}
};
fetchResults = async (state, instance) => {
let {
onTableRefreshed,
customFilters = null,
extraConfig = {},
tableActions = TABLE_ACTIONS_LOCAL
} = this.props;
if (tableActions === TABLE_ACTIONS_REMOTE) {
state = state ? state : this.reactTableRef ? this.reactTableRef.resolvedState() : undefined;
}
// validations
if (this.state.metaFilters?.length && !this.filters?.isValid()) return;
// form request object
let requestObject = {
filter: this.filters?.val() || {},
pageSize: 1000,
...(state
? {
pageNo: state.page,
pageSize: state.pageSize,
paginated: true
}
: {}),
...extraConfig
};
if (customFilters) {
requestObject.filter = {
...requestObject.filter,
...customFilters
};
}
if (state && state.sorted) {
/* No support for REMOTE table actions yet. */
state.sorted.map(sortedColumn => {
let column = state.columns.find(column => sortedColumn.id === column.accessor);
// console.log("SORTED ", {sortedColumn, sortedColumns: state.sorted, column, columns: state.columns});
});
}
if (state && state.filtered) {
state.filtered.map(filteredColumn => {
let column = state.columns.find(column => filteredColumn.id === column.accessor);
// console.log("FILTERED ", {filteredColumn, filteredColumns: state.filtered, column, columns: state.columns});
requestObject.filter[filteredColumn.id] = filteredColumn.value;
});
}
try {
let {
view,
tableActions,
onFetchData,
refreshCache = false,
enableLockMechanism,
tunnelClient,
lockEntityIdName,
dRTnt = {}
} = this.props;
this.setState({ loading: true });
let responseObject =
tableActions === TABLE_ACTIONS_LOCAL && onFetchData
? await onFetchData({ apiName: "gridData", view, requestObject, refreshCache })
: await Api.root.post(API_URLS.gridData.replace("{gridView}", view), requestObject, {
headers: dRTnt
});
let meta = responseObject.data.meta;
let rawResults = responseObject.data.results;
let results =
enableLockMechanism && tunnelClient
? await this.addLockingInfo({ rawResults, lockName: enableLockMechanism, lockEntityIdName })
: rawResults;
this.setState(
{
meta,
results
},
() => {
if (onTableRefreshed) onTableRefreshed(meta, results);
return {
meta,
results
};
}
);
} catch (error) {
console.error("func fetchTableMeta ", error);
throw error;
} finally {
this.setState({ loading: false });
}
};
debouncedFetchResults = commonUtils.debounce(this.fetchResults, 300);
addLockingInfo = ({ rawResults, lockName, lockType = "WRITE", lockEntityIdName }) =>
new Promise(async (resolve, reject) => {
try {
let response = await Api.root.post(API_URLS.gridLock, {
lockName,
lockType
});
let lockedResults = response.data.data;
let lockedIds = lockedResults.map(result => result.entityId);
for (let index = 0; index < rawResults.length; index++) {
let indexOfLockedResult = lockedIds.indexOf(rawResults[index][lockEntityIdName]);
if (indexOfLockedResult > -1) {
rawResults[index].lockDto = {
lockName,
lockType,
lockEmployeeId: lockedResults[indexOfLockedResult].employeeId
};
} else {
rawResults[index].lockDto = null;
}
}
} catch (error) {
console.error("Failed to add locking info ", error);
}
resolve(rawResults);
});
addListener = () => {
this.socketService = this.props.tunnelClient.instance();
this.socketService.on(`/api/lock/event`, response => {
let { meta, enableLockMechanism, lockEntityIdName } = this.props;
let { lockName, lockType, lockEmployeeId, lockEventType, ids } = response; // {"ids":[181],"lockEventType":"ACQUIRE","lockEmployeeId":703,"lockType":"WRITE","lockName":"ADD_ASSIST_BENE"}
if (meta.userDetails.employeeId == lockEmployeeId || enableLockMechanism != lockName) return;
// console.log(`Component :: DataGridTableView : socket handler!!!`, response);
let results = this.state.results;
let updatedRecords = 0;
for (let index = 0; index < results.length; index++) {
if (ids.indexOf(results[index][lockEntityIdName]) > -1) {
results[index].lockDto =
lockEventType === "ACQUIRE"
? {
lockName,
lockType,
lockEmployeeId
}
: null;
updatedRecords++;
}
if (updatedRecords === ids.length) {
break;
}
}
if (updatedRecords) {
this.setState({ results });
}
});
};
getColumnsFromMeta = (meta = {}, results = []) => {
let metaColumns = [];
if (meta.descriptors && meta.descriptors.length) {
metaColumns = meta.descriptors
.filter(descriptor => descriptor.isActive === "Y")
.filter(descriptor => descriptor.fieldType !== "FILTER")
.map((descriptor, index, arr) => {
switch (descriptor.fieldType) {
case "ACTION":
return {
_id: descriptor.id,
Header: descriptor.fieldLabel || descriptor.dbColumnName,
accessor: descriptor.dbColumnName,
width: Math.max(
160,
this.getColumnWidth(
results,
descriptor.dbColumnName,
descriptor.fieldLabel || descriptor.dbColumnName
)
),
Cell: rowData => {
let row = rowData.original;
if (descriptor.condition && !eval(descriptor.condition)) return "-";
return (
<StackedFAIcon
title={descriptor.fieldLabel || descriptor.dbColumnName}
background={
TABLE_ACTION_ICON_META[descriptor.dbColumnName]?.iconBackgroundColor ||
"#45a655"
}
color={TABLE_ACTION_ICON_META[descriptor.dbColumnName]?.iconColor || "#fff"}
faIconClass={
TABLE_ACTION_ICON_META[descriptor.dbColumnName]?.iconClass ||
"fa-wrench"
}
onClick={() => {
let { dynamicActionIconHandlers = {} } = this.props;
let handlers = {
...TABLE_ACTION_ICON_HANDLERS,
...dynamicActionIconHandlers
};
handlers[TABLE_ACTION_ICON_META[descriptor.dbColumnName]?.handler]?.({
meta: descriptor,
row,
refreshTableData: this.fetchResults
});
}}
/>
);
},
filterable: false,
sortable: false
};
default:
return {
_id: descriptor.id,
Header: descriptor.fieldLabel || descriptor.dbColumnName,
accessor: descriptor.dbColumnName,
width: Math.max(
160,
this.getColumnWidth(
results,
descriptor.dbColumnName,
descriptor.fieldLabel || descriptor.dbColumnName
)
),
Cell: TABLE_UTIL.CELL[descriptor.fieldDataType],
filterMethod: TABLE_UTIL.FILTER[descriptor.fieldDataType]
};
}
});
}
return metaColumns;
};
getFiltersFromMeta = (meta = {}) => {
let metaFilters = [];
if (meta.descriptors && meta.descriptors.length) {
metaFilters = meta.descriptors
.filter(descriptor => descriptor.isActive === "Y")
.filter(descriptor => descriptor.fieldType === "FILTER");
}
return metaFilters.length ? metaFilters : null;
};
getColumnWidth = (rows, accessor, headerText) => {
const maxWidth = 250;
const magicSpacing = 9;
const rowValuesLength = rows.map(row => (`${row[accessor]}` || "").length);
const cellLength = Math.max(...rowValuesLength, headerText.length);
return Math.min(maxWidth, cellLength * magicSpacing);
};
getTableInstance = () => this.reactTableRef;
render() {
const { loading, meta, metaColumns, metaFilters, results } = this.state;
const pages = Number(meta?.recordsTotal) || 100;
const {
tableActions = TABLE_ACTIONS_LOCAL,
preColumns = [],
overrideColumns = [],
additionalColumns = [],
additionalRows = [],
filterable = true,
showPagination = true,
defaultPageSize = 5,
enableHeaderLabelPriority
} = this.props;
const overriddenColumns = overrideColumns.length
? metaColumns.map(column => {
let overrideColumn = overrideColumns.find(_column => column.accessor === _column.accessor);
return {
...column,
...(overrideColumn || {})
};
})
: metaColumns;
const columns = [...preColumns, ...overriddenColumns, ...additionalColumns].map(column => ({
...column,
//Adding Hover Text to Column Headers
Header: props =>
typeof column.Header === "function" ? (
<span title={props.column.id.replaceAll("_", " ")}>{column.Header()}</span>
) : (
<span title={column.Header}>{column.Header}</span>
)
}));
const finalResults = [...additionalRows, ...results];
return metaColumns.length ? (
<div className="grid-container">
<Filters
onRef={ref => (this.filters = ref)}
filters={metaFilters}
filterConfig={this.props.filterConfig}
onSearch={() => this.fetchResults()}
/>
<div className="grid-table">
<ReactTableFixedColumns
innerRef={ref => {
this.reactTableRef = ref;
}}
manual={tableActions === TABLE_ACTIONS_REMOTE ? true : false}
{...(tableActions === TABLE_ACTIONS_REMOTE ? { onFetchData: this.debouncedFetchResults } : {})}
loading={loading}
loadingText={<div className="loader-s" style={{ margin: "0 auto" }} />}
columns={columns}
column={{
...ReactTableDefaults.column,
style: {
justifyContent: "center",
lineHeight: "32px",
padding: "5px 5px",
"--data-noOfColumns": columns.length
},
headerStyle: {
lineHeight: "32px",
fontWeight: "bold",
...(enableHeaderLabelPriority
? {
whiteSpace: "normal",
display: "flex",
alignItems: "center",
justifyContent: "center"
}
: {}),
padding: "5px 5px",
"--data-noOfColumns": columns.length
}
}}
data={finalResults}
{...(this.props.cellStyle ? { getTdProps: this.props.cellStyle } : {})}
//check for row style as well
{...(this.props.rowStyle ? { getTrProps: this.props.rowStyle } : {})}
// className="-striped -highlight"
filterable={filterable}
showPagination={showPagination}
defaultPageSize={defaultPageSize}
PaginationComponent={Pagination}
{...(tableActions === TABLE_ACTIONS_REMOTE ? { pages } : {})}
/>
</div>
</div>
) : (
<div className="empty-response">
<div className="loading-inline"></div>
</div>
);
}
}
DataGridTableView.defualtProps = {
tableActions: TABLE_ACTIONS_LOCAL,
onTableRefreshed: () => {}
};
DataGridTableView.propTypes = {
view: PropTypes.string.isRequired,
tableActions: PropTypes.oneOf([TABLE_ACTIONS_LOCAL, TABLE_ACTIONS_REMOTE]),
overrideColumns: PropTypes.array,
preColumns: PropTypes.array,
additionalColumns: PropTypes.array,
additionalRows: PropTypes.array,
filterable: PropTypes.bool,
showPagination: PropTypes.bool,
defaultPageSize: PropTypes.number,
onTableRefreshed: PropTypes.func,
enableLockMechanism: PropTypes.string,
tunnelClient: PropTypes.object,
lockEntityIdName: PropTypes.string,
customFilters: PropTypes.object,
filterConfig: PropTypes.object,
meta: PropTypes.object
};
export default DataGridTableView;
class Filters extends Component {
constructor(props) {
super(props);
this.state = {
fields: [],
values: {},
selectOptsMap: {}
};
}
async componentDidMount() {
this.props.onRef && this.props.onRef(this);
const { filters } = this.props;
if (filters && Array.isArray(filters) && filters.length) {
const fields = filters.map(f => {
if (f.filterType === "SELECT" && f.filterOptionsView) {
this.fetchSelectOptions(f.dbColumnName, f.filterOptionsView);
}
return f;
});
this.setState({
fields
});
if (this.props.filterConfig) {
this.setDefaultFilterValues();
}
}
}
componentWillUnmount() {
this.props.onRef && this.props.onRef(null);
}
componentDidUpdate(prevProps, prevState) {}
setDefaultFilterValues = () => {
/* Seperating default Values and filterConfig and then setting directly in state */
let filterConfig = this.props.filterConfig;
Object.keys(filterConfig).map(filter => {
if (filterConfig[filter].defaultValue) this.setVal(filter, filterConfig[filter].defaultValue);
});
};
fetchSelectOptions = async (selectId, view, optionsFilter = null) => {
try {
let responseObject = await Api.root.post(API_URLS.gridData.replace("{gridView}", view), {
...(optionsFilter ? { filter: optionsFilter } : {}),
pageSize: 1000
});
let rawResults = responseObject.data.results;
const filterConfig = this.props.filterConfig || {};
const config = filterConfig[selectId] || {};
const {
optionsGenerator = r => ({
label: r.DESCRIPTION,
value: r.CODE
}),
optionsFilterer = () => true
} = config;
this.setState(prevState => ({
selectOptsMap: {
...prevState.selectOptsMap,
[selectId]: rawResults.map(optionsGenerator).filter(optionsFilterer)
},
values: {
...prevState.values,
...(optionsFilter ? {
[selectId]: rawResults.map(optionsGenerator).find((option) => option.value === prevState.values[selectId]?.value)
} : {})
}
}));
} catch (error) {
console.error("fetchSelectOptions ", error);
}
};
isValid = ({ showError = false } = {}) => {
const { fields, values } = this.state;
const filterGroupStrategy =
fields[0]?.filterGroupStrategy; /* assuming filterGroupStrategy will be same for all filters */
if (!filterGroupStrategy) return true;
const regExp = "#[_a-zA-Z0-9]*";
const filtersArray = filterGroupStrategy.matchAll(regExp);
let evalString = filterGroupStrategy;
let errorMsgsArray = [];
for (let filter of filtersArray) {
filter = filter[0]; /* since matchAll returns an array */
let filterValue = this.parseVal(values[`${filter.replace("#", "")}`]);
let isFilterValid = showError ? this[`${filter.replace("#", "")}`].isValid() : !!filterValue;
if (!isFilterValid) errorMsgsArray.push(filter);
let replaceValue = isFilterValid
? typeof filterValue === "string"
? `"${filterValue}"`
: filterValue
: false;
evalString = evalString.replace(filter, replaceValue);
}
// console.log({ evalString });
const result = !!eval(evalString);
// console.log({ result });
if (result) {
for (const filter of errorMsgsArray) {
this[`${filter.replace("#", "")}`].setError("");
}
}
return result;
};
parseVal = rawVal => {
let val = rawVal;
if (typeof val !== "object") {
val = val;
} else if (val.value !== undefined) {
val = val.value;
} else if (val.checked !== undefined) {
val = val.checked;
} else if (moment.isMoment(val)) {
val = val.valueOf();
} else {
val = "";
}
return val;
};
val = () => {
let { fields, values } = this.state;
let result = {};
fields.map(f => {
let rawVal = this[`${f.dbColumnName}`].val(); // values[`${f.dbColumnName}`]
let val = this.parseVal(rawVal);
if (val !== "") result[f.filterCondition] = val;
});
return result;
};
setVal = async (fieldId, newVal) => {
// const { values } = this.state;
this.updateFilterOptions(fieldId, newVal);
this.setState(
prev => {
return { values: { ...prev.values, [fieldId]: newVal } };
},
() => {
return 1;
}
);
};
onSearch = () => {
if (this.isValid({ showError: true })) this.props.onSearch();
};
updateFilterOptions = (updatedFieldId, newVal) => {
//function that changes the options of one filter field based on input of another field
//applicable only for SELECT Field
const { fields } = this.state;
const updatedFieldDependentFilters = fields.filter(
field => field.filterType === "SELECT" && field.filterDependsOn === updatedFieldId
);
Object.keys(updatedFieldDependentFilters).length > 0 &&
updatedFieldDependentFilters.map(dependentField => {
const { dbColumnName, filterOptionsView } = dependentField;
this.fetchSelectOptions(dbColumnName, filterOptionsView, { [updatedFieldId]: newVal.value });
});
};
render() {
const { fields, values, selectOptsMap } = this.state;
if (!fields.length) return null;
return (
<div className="grid-filters">
<div className="grid-filters-fields">
{fields
.sort((a, b) => parseInt(a.fieldOrder) - parseInt(b.fieldOrder))
.map(fieldObj => {
let { dbColumnName: id, filterType, filterRequired, fieldLabel } = fieldObj;
const filterConfig = this.props.filterConfig || {};
const { defaultValue, ...rest } = filterConfig[id] || {};
/* rest are other filterConfig keys that are to be set when formfield/s is rendered*/
const config = rest;
let fieldProps = {
type: filterType?.toLowerCase(),
label: fieldLabel,
...(filterType === "SELECT"
? {
options: selectOptsMap[id] || []
}
: {}),
required: filterRequired,
...config
};
return (
<FormField
key={`key-${id}`}
ref={ref => (this[`${id}`] = ref)}
controlled={true}
value={values[id]}
onChange={newVal => this.setVal(id, newVal)}
{...fieldProps}
/>
);
})}
</div>
<div className="grid-filters-btns">
<Button
variant="contained"
color="success"
onClick={() => this.onSearch()}
{...this.props.filterSearchBtn}
>
Search
</Button>
{/* //not needed as of now
<Button variant="contained" onClick={() => this.onSearch()}>
Refresh
</Button> */}
</div>
</div>
);
}
}
class Pagination extends Component {
constructor() {
super();
}
static propTypes = {
pages: PropTypes.number,
page: PropTypes.number,
pageSize: PropTypes.number,
onPageChange: PropTypes.func,
onPageSizeChange: PropTypes.func
};
changePage(page) {
this.props.onPageChange(page);
}
pageSizeChange(page) {
this.props.onPageSizeChange(page);
}
render() {
const { page, pages, pageSize } = this.props;
return (
<div style={{ display: "flex", margin: "0.4rem", height: "4rem" }}>
<button
onClick={() => {
this, this.changePage(page - 1);
}}
disabled={page === 0}
style={{
width: "33%",
border: "1px solid rgba(0,0,0,0.1)",
borderRadius: "0.2rem"
}}
>
Previous
</button>
<span
style={{
width: "33%",
display: "flex",
justifyContent: "center",
alignItems: "center"
}}
>
Page
<input
style={{
width: "4rem",
textAlign: "center",
border: "1px solid rgba(0,0,0,0.1)",
borderRadius: "0.2rem"
}}
type="number"
value={pages > 0 ? page + 1 : pages}
max={pages}
min={1}
onChange={event => {
this.changePage(parseInt(event.target.value) - 1);
}}
/>
of
{pages}
<div
style={{
display: "flex",
justifyContent: "end",
width: "40%"
}}
>
<select
value={pageSize}
onChange={event => {
this.pageSizeChange(parseInt(event.target.value));
}}
style={{
border: "1px solid rgba(0,0,0,0.1)",
borderRadius: "0.2rem"
}}
>
{this.props.pageSizeOptions.map((value, i) => {
return <option key={i} value={value}>{`${value} rows`}</option>;
})}
</select>
</div>
</span>
<button
onClick={() => {
this.changePage(page + 1);
}}
disabled={page === pages - 1 || pages === 0}
style={{
width: "33%",
border: "1px solid rgba(0,0,0,0.1)",
borderRadius: "0.2rem"
}}
>
Next
</button>
</div>
);
}
}