diff --git a/jsx/FilterForm.js b/jsx/FilterForm.js index 523cb56df5d..8217c5daa41 100644 --- a/jsx/FilterForm.js +++ b/jsx/FilterForm.js @@ -107,7 +107,7 @@ class FilterForm extends Component { value: filterValue ? filterValue : '', key: key, })); - // Initialize filter for StaticDataTable + // Initialize filter for DataTable this.setFilter(elementName, child.props.name, filterValue); } else { formChildren.push(React.cloneElement(child, {key: key})); @@ -122,7 +122,7 @@ class FilterForm extends Component { * empty. * * Sets exactMatch to true for all SelectElements (i.e dropdowns) - * in order to force StaticDataTable to do exact comparaison + * in order to force DataTable to do exact comparaison * * @param {string} type - form element type (i.e component name) * @param {string} key - the name of the form element diff --git a/jsx/StaticDataTable.d.ts b/jsx/StaticDataTable.d.ts deleted file mode 100644 index 5203cc84b54..00000000000 --- a/jsx/StaticDataTable.d.ts +++ /dev/null @@ -1,60 +0,0 @@ -import {ReactNode} from 'react'; -/** - * This file contains a sufficient typescript definition of the StaticDataTable - * to allow it to be imported into TypeScript with "strict" mode and not trigger - * errors. - */ - -type TableRow = (string|null)[]; - -type StaticDataTableProps = { - Headers: string[], - Data: TableRow[], - RowNumLabel: string?, - getFormattedCell: ( - columnname: string, - celldata: string, - row: string[], - headers: string[], - fieldNo: number - ) => ReactNode, - onSort?: () => void -}; - -/** - * StaticDataTable class. See StaticDataTable.js - */ -class StaticDataTable { - props: StaticDataTableProps - state: any - context: object - refs: {[key: string]: ReactInstance} - - /** - * Create a StaticDataTable - * - * @param {StaticDataTableProps} props - React props - */ - constructor(props: StaticDataTableProps) - - /** - * React Lifecycle Method - * - * @returns {ReactNode} - the StaticDataTable - */ - render(): ReactNode - - /** - * React Lifecycle Method - * - * @param {object} newstate - the state to overwrite - */ - setState(newstate: object): void - - /** - * React Lifecycle Method - */ - forceUpdate(): void -} - -export default StaticDataTable; diff --git a/jsx/StaticDataTable.js b/jsx/StaticDataTable.js deleted file mode 100644 index 9b6e33ce4f0..00000000000 --- a/jsx/StaticDataTable.js +++ /dev/null @@ -1,774 +0,0 @@ -/** - * This file contains React component for Static Data Table - * - * @author Loris Team - * @version 1.0.0 - */ - -import React, {Component} from 'react'; -import PropTypes from 'prop-types'; -import PaginationLinks from 'jsx/PaginationLinks'; -import createFragment from 'react-addons-create-fragment'; -import {withTranslation} from 'react-i18next'; - -/** - * Static Data Table component - * Displays a set of data that is receives via props. - */ -class StaticDataTable extends Component { - /** - * @constructor - * @param {object} props - React Component properties - */ - constructor(props) { - super(props); - - this.state = { - PageNumber: 1, - SortColumn: -1, - SortOrder: 'ASC', - RowsPerPage: 20, - Hide: this.props.Hide, - }; - - this.changePage = this.changePage.bind(this); - this.setSortColumn = this.setSortColumn.bind(this); - this.changeRowsPerPage = this.changeRowsPerPage.bind(this); - this.downloadCSV = this.downloadCSV.bind(this); - this.countFilteredRows = this.countFilteredRows.bind(this); - this.toCamelCase = this.toCamelCase.bind(this); - this.getSortedRows = this.getSortedRows.bind(this); - this.hasFilterKeyword = this.hasFilterKeyword.bind(this); - } - - /** - * shouldComponentUpdate - * - * @param {object} nextProps - next props - * @param {object} nextState - next state - * @param {object} nextContext - next context - * @return {boolean} update component if true. - */ - shouldComponentUpdate(nextProps, nextState, nextContext) { - // prevents multiple reloads of the table in the DQT module. - return !(this.props.DisableFilter && nextProps.Data === this.props.Data - && nextState === this.state); - } - - /** - * Called by React when the component has been rendered on the page. - */ - componentDidMount() { - if (jQuery.fn.DynamicTable && !this.props.NoDynamicTable) { - if (this.props.freezeColumn) { - $('#dynamictable').DynamicTable({ - freezeColumn: this.props.freezeColumn, - }); - } else { - $('#dynamictable').DynamicTable(); - } - } - if (!this.props.DisableFilter) { - // Retrieve module preferences - let modulePrefs = JSON.parse(localStorage.getItem('modulePrefs')); - - // Init modulePrefs object - if (modulePrefs === null) { - modulePrefs = {}; - } - - // Init modulePrefs for current module - if (modulePrefs[loris.TestName] === undefined) { - modulePrefs[loris.TestName] = {}; - modulePrefs[loris.TestName].rowsPerPage = this.state.RowsPerPage; - } - - // Set rows per page - let rowsPerPage = modulePrefs[loris.TestName].rowsPerPage; - this.setState({ - RowsPerPage: rowsPerPage, - }); - - // Make prefs accesible within component - this.modulePrefs = modulePrefs; - } - } - - /** - * Called by React when the component is updated. - * - * @param {object} prevProps - Previous React Component properties - * @param {object} prevState - Previous React Component state - */ - componentDidUpdate(prevProps, prevState) { - if (jQuery.fn.DynamicTable && !this.props.NoDynamicTable) { - if (this.props.freezeColumn) { - $('#dynamictable').DynamicTable({ - freezeColumn: this.props.freezeColumn, - }); - } else { - $('#dynamictable').DynamicTable(); - } - } - if (!this.props.DisableFilter) { - if (this.props.onSort && - (this.state.SortColumn !== prevState.SortColumn || - this.state.SortOrder !== prevState.SortOrder) - ) { - let index = this.getSortedRows(); - this.props.onSort(index, this.props.Data, this.props.Headers); - } - } - } - - /** - * Set the component page variable - * to a new value - * - * @param {number} pageNo - Page index - */ - changePage(pageNo) { - this.setState({ - PageNumber: pageNo, - }); - } - - /** - * Update the sort column - * If component sortColumn is already set to colNumber - * Toggle SortOrder ASC/DESC - * - * @param {number} colNumber - The column index - * @return {Function} - onClick Event Handler - */ - setSortColumn(colNumber) { - return function(e) { - if (this.state.SortColumn === colNumber) { - this.setState({ - SortOrder: this.state.SortOrder === 'ASC' ? 'DESC' : 'ASC', - }); - } else { - this.setState({ - SortColumn: colNumber, - }); - } - }.bind(this); - } - - /** - * Update the number of rows per page - * - * @param {object} val - */ - changeRowsPerPage(val) { - if (!this.props.DisableFilter) { - let modulePrefs = this.modulePrefs; - - // Save current selection - modulePrefs[loris.TestName].rowsPerPage = val.target.value; - - // Update localstorage - localStorage.setItem('modulePrefs', JSON.stringify(modulePrefs)); - } - this.setState({ - RowsPerPage: val.target.value, - PageNumber: 1, - }); - } - - /** - * Export the filtered rows and columns into a csv - * - * @param {array} csvData - The csv data - */ - downloadCSV(csvData) { - let csvworker = new Worker(loris.BaseURL + '/js/workers/savecsv.js'); - - csvworker.addEventListener('message', function(e) { - let dataURL; - let dataDate; - let link; - if (e.data.cmd === 'SaveCSV') { - dataDate = new Date().toISOString(); - dataURL = window.URL.createObjectURL(e.data.message); - link = document.createElement('a'); - link.download = 'data-' + dataDate + '.csv'; - link.type = 'text/csv'; - link.href = dataURL; - document.body.appendChild(link); - $(link)[0].click(); - document.body.removeChild(link); - } - }); - - const correctReactLinks = (csvData) => { - for (const [index] of Object.entries(csvData)) { - for (const [indexChild] of Object.entries(csvData[index])) { - if (csvData[index][indexChild] == null) { - csvData[index][indexChild] = ['']; - } else if (csvData[index][indexChild].type === 'a') { - csvData[index][indexChild] = [ - csvData[index][indexChild].props['href'], - ]; - } - } - } - return csvData; - }; - - const csvDownload = correctReactLinks([...csvData]); - csvworker.postMessage({ - cmd: 'SaveFile', - data: csvDownload, - headers: this.props.Headers, - identifiers: this.props.RowNameMap, - }); - } - - /** - * Get the number of filtered rows - * - * @return {number} - */ - countFilteredRows() { - let useKeyword = false; - let filterMatchCount = 0; - let filterValuesCount = (this.props.Filter ? - Object.keys(this.props.Filter).length : - 0 - ); - let tableData = this.props.Data; - let headersData = this.props.Headers; - - if (this.props.Filter.keyword) { - useKeyword = true; - } - - if (useKeyword) { - filterValuesCount -= 1; - } - - for (let i = 0; i < tableData.length; i++) { - let headerCount = 0; - let keywordMatch = 0; - for (let j = 0; j < headersData.length; j++) { - let data = tableData[i] ? tableData[i][j] : null; - if (this.hasFilterKeyword(headersData[j], data)) { - headerCount++; - } - if (useKeyword) { - if (this.hasFilterKeyword('keyword', data)) { - keywordMatch++; - } - } - } - - if (headerCount === filterValuesCount && - ((useKeyword === true && keywordMatch > 0) || - (useKeyword === false && keywordMatch === 0))) { - filterMatchCount++; - } - } - - let hasFilters = (filterValuesCount !== 0); - if (filterMatchCount === 0 && hasFilters) { - return 0; - } - - return (filterMatchCount === 0) ? tableData.length : filterMatchCount; - } - - /** - * Convert a string to CamelCase - * - * @param {string} str - * @return {string} - */ - toCamelCase(str) { - return str.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function(match, index) { - if (Number(match) === 0) return ''; - return index === 0 ? match.toLowerCase() : match.toUpperCase(); - }); - } - - /** - * Get if the current sorting column has mixed types in its values. - * - * @return {boolean} true if mixed types, else false. - */ - hasMixedTypes() { - // TODO: data column type check should probably be done once at init, - // meaning when receiving data, to find which columns have mixed types. - let typeFound = null; - - // not the default column - if (this.state.SortColumn === -1) { - return false; - } - - // Only checks string or number types, others are considered undefined. - // Break out of this loop once we encounter two mixed types: - // number and string inside the sorted column. - for (const row of this.props.Data) { - // cell value - let val = row[this.state.SortColumn]; - - // if null or undefined, go to the next iteration - if (val == null) { - continue; - } - - // check number - if (!isNaN(val) && typeof val !== 'object') { - // if string is found, mix of types, break - if (typeFound === 'string') { - return true; - } - // register number only if not already in - if (typeFound == null) { - typeFound = 'number'; - } - - // avoid string section - continue; - } - - // check string - if (typeof val === 'string' || val instanceof String) { - // if number is found, mix of types, break - if (typeFound === 'number') { - return true; - } - // register string only if not already in - if (typeFound == null) { - typeFound = 'string'; - } - } - } - return false; - } - - /** - * Sort the rows according to the sort configuration - * - * @return {object[]} - */ - getSortedRows() { - const index = []; - - // is the current sorted column with mixed type? - const isMixedType = this.hasMixedTypes(); - - // - for (let i = 0; i < this.props.Data.length; i += 1) { - let val = this.props.Data[i][this.state.SortColumn] || undefined; - // If SortColumn is equal to default No. column, set value to be - // index + 1 - if (this.state.SortColumn === -1) { - val = i + 1; - } - const isString = (typeof val === 'string' || val instanceof String); - const isNumber = !isNaN(val) && typeof val !== 'object'; - - if (val === '.') { - // hack to handle non-existent items in DQT - val = null; - } else if (isNumber && !isMixedType) { - // perform type conversion (from string to int/float) - val = Number(val); - } else if (isString || isMixedType) { - // in case of mixed types, force values to string. - val = String(val); - // if string with text convert to lowercase - val = val.toLowerCase(); - } else { - val = undefined; - } - - if (this.props.RowNameMap) { - index.push({RowIdx: i, Value: val, Content: this.props.RowNameMap[i]}); - } else { - index.push({RowIdx: i, Value: val, Content: i + 1}); - } - } - - index.sort(function(a, b) { - if (this.state.SortOrder === 'ASC') { - if (a.Value === b.Value) { - // If all values are equal, sort by rownum - if (a.RowIdx < b.RowIdx) return -1; - if (a.RowIdx > b.RowIdx) return 1; - } - // Check if null values - if (a.Value === null || typeof a.Value === 'undefined') return -1; - if (b.Value === null || typeof b.Value === 'undefined') return 1; - - // Sort by value - if (a.Value < b.Value) return -1; - if (a.Value > b.Value) return 1; - } else { - if (a.Value === b.Value) { - // If all values are equal, sort by rownum - if (a.RowIdx < b.RowIdx) return 1; - if (a.RowIdx > b.RowIdx) return -1; - } - // Check if null values - if (a.Value === null || typeof a.Value === 'undefined') return 1; - if (b.Value === null || typeof b.Value === 'undefined') return -1; - - // Sort by value - if (a.Value < b.Value) return 1; - if (a.Value > b.Value) return -1; - } - // They're equal.. - return 0; - }.bind(this)); - return index; - } - - /** - * Searches for the filter keyword in the column cell - * - * Note: Search is case-insensitive. - * - * @param {string} headerData column name - * @param {string} data search string - * @return {boolean} true, if filter value is found to be a substring - * of one of the column values, false otherwise. - */ - hasFilterKeyword(headerData, data) { - let header = this.toCamelCase(headerData); - let filterData = null; - let exactMatch = false; - let result = false; - let searchKey = null; - let searchString = null; - - if (this.props.Filter[header]) { - filterData = this.props.Filter[header].value; - exactMatch = this.props.Filter[header].exactMatch; - } - - // Handle null inputs - if (filterData === null || data === null) { - return false; - } - - // Handle numeric inputs - if (typeof filterData === 'number') { - let intData = Number.parseInt(data, 10); - result = (filterData === intData); - } - - // Handle string inputs - if (typeof filterData === 'string') { - searchKey = filterData.toLowerCase(); - searchString = data.toLowerCase(); - - if (exactMatch) { - result = (searchString === searchKey); - } else { - result = (searchString.indexOf(searchKey) > -1); - } - } - - // Handle array inputs for multiselects - if (typeof filterData === 'object') { - let match = false; - for (let i = 0; i < filterData.length; i += 1) { - searchKey = filterData[i].toLowerCase(); - searchString = data.toLowerCase(); - match = (searchString === searchKey); - if (match) { - result = true; - } - } - } - return result; - } - - /** - * Renders the React component. - * - * @return {JSX} - React markup for the component - */ - render() { - if (this.props.Data === null || this.props.Data.length === 0) { - return ( -