UNPKG

@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
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 &nbsp; <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); }} /> &nbsp; of &nbsp; {pages} &nbsp;&nbsp; <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> ); } }