@bigfishtv/cockpit
Version:
378 lines (350 loc) • 12.6 kB
JavaScript
/**
* Table Utilities
* @module Utilities/tableUtils
*/
import _get from 'lodash/get'
import moment from 'moment'
import * as SortTypes from '../constants/SortTypes'
import * as Conditions from '../constants/Conditions'
import { isNumeric, isString, isFunction } from './typeUtils'
import { titleCase } from './stringUtils'
import { getSchemaWithAssociations } from './formUtils'
import FixedDataTableCheckboxCell from '../components/table/cell/FixedDataTableCheckboxCell'
import FixedDataTableDateCell from '../components/table/cell/FixedDataTableDateCell'
import FixedDataTableHtmlCell from '../components/table/cell/FixedDataTableHtmlCell'
import FixedDataTableAssetCell from '../components/table/cell/FixedDataTableAssetCell'
import FixedDataTableDecimalCell from '../components/table/cell/FixedDataTableDecimalCell'
/**
* Returns string swapping between ASC and DESC
* @param {String} sortDir
* @return {String}
*/
export function reverseSortDirection(sortDir) {
return sortDir === SortTypes.DESC ? SortTypes.ASC : SortTypes.DESC
}
/**
* Returns a sort function that takes into account sort direction and type
* @param {String} columnKey - key that will be in both objects to compare
* @param {String} sortDirection - direction of sort either ASC or DESC
* @param {String} [sortType=string] - sortType: string, numeric, boolean
* @return {Function} - returns sort function
*/
export function sortByObjectKey(columnKey, sortDirection = SortTypes.ASC, sortType = 'string') {
return (a, b) => {
let valA = _get(a, columnKey)
let valB = _get(b, columnKey)
let sortVal = 0
if (valA === null || valA === undefined) valA = sortType == 'string' ? '' : 0
if (valB === null || valB === undefined) valB = sortType == 'string' ? '' : 0
if (sortType == 'string') {
valA = valA && typeof valA.toLowerCase == 'function' ? valA.toLowerCase() : valA
valB = valB && typeof valB.toLowerCase == 'function' ? valB.toLowerCase() : valB
sortVal = valA > valB ? 1 : valA < valB ? -1 : 0
} else if (sortType == 'numeric' || sortType == 'boolean') {
sortVal = valB - valA > 0 ? -1 : valB - valA < 0 ? 1 : 0
}
if (sortVal !== 0 && sortDirection === SortTypes.DESC) sortVal = sortVal * -1
return sortVal
}
}
/**
* Takes schema column, looks at type and resolves to javascript sort type
* @param {Object} column
* @param {String} column.type - e.g. integer, float, decimal, boolean
* @return {string}
*/
export function getSortTypeFromColumn(column) {
switch (column.type) {
case 'integer':
return 'numeric'
break
case 'float':
return 'numeric'
break
case 'decimal':
return 'numeric'
break
case 'boolean':
return 'boolean'
break
}
return 'string'
}
/**
* Takes schema column, looks at type and resolves to corresponding table cell component
* @param {Object} column
* @param {String} column.type - e.g. datetime, timestamp, boolean, text
* @return {React.Component} - Returns react component, else null
*/
export function getCellComponentFromColumn(column) {
switch (column.type) {
case 'datetime':
return FixedDataTableDateCell
break
case 'timestamp':
return FixedDataTableDateCell
break
case 'boolean':
return FixedDataTableCheckboxCell
break
case 'text':
return FixedDataTableHtmlCell
break
case 'decimal':
case 'float':
return FixedDataTableDecimalCell
break
}
switch (column.className) {
case 'Tank.Assets':
return FixedDataTableAssetCell
break
}
return null
}
/**
* Generates default field attributes from a schema column
* @param {Object} column
* @param {String} column.type
* @param {String} column.property
* @return {Object}
*/
export function getFieldAttributesFromColumn(column) {
let attributes = {}
switch (column.property) {
case 'id':
attributes = { ...attributes, width: 40, fixed: true }
break
}
switch (column.type) {
case 'json':
attributes = { ...attributes, sortable: false }
break
}
switch (column.className) {
case 'Tank.Assets':
attributes = { ...attributes, width: 80 }
break
}
return attributes
}
/**
* Generates default table fields from schema
* @param {Array} schema
* @param {Function} componentResolver - precursor function to resolve component based on scema column
* @param {Function} attributeModifier - precursor function to resolve additional field attributes based on scema column
* @return {Array} - returns array of table fields
*/
export function getFieldsFromSchema(schema = [], componentResolver = () => null, attributeModifier = () => ({})) {
return schema.map(column => {
const sortType = getSortTypeFromColumn(column)
const Cell = componentResolver(column) || getCellComponentFromColumn(column)
const additionalAttributes = { ...attributeModifier(column), ...getFieldAttributesFromColumn(column) }
return {
key: column.property,
value: column.title || titleCase(column.property),
resizable: true,
sortable: true,
schema: column,
sortType,
Cell,
...additionalAttributes,
}
})
}
/**
* Takes schema and assocations and adds 'belongsToMany' assocations to schema before returning result from 'getFieldsFromSchema'
* @param {Array} schema
* @param {Object[]} assocations
* @param {String} assocations[].type - e.g. belongsTo, belongsToMany
* @param {Function} componentResolver - precursor function to resolve component based on scema column
* @param {Function} attributeModifier - precursor function to resolve additional field attributes based on scema column
* @return {Array} - returns array of table fields
*/
export function getFieldsFromSchemaAndAssociations(
schema = [],
associations = [],
componentResolver = () => null,
attributeModifier = () => ({})
) {
const newSchema = getSchemaWithAssociations(schema, associations)
return normalizeFields(getFieldsFromSchema(newSchema, componentResolver, attributeModifier))
}
/**
* Takes a table fields and fixes any undefined variables in each one
* @param {Object[]} fields
* @param {String} fields[].sortType
* @param {Boolean} fields[].resizable
* @param {Boolean} fields[].sortable
* @return {Array}
*/
export function normalizeFields(fields) {
let order = 0
return fields
.map(field => {
if (typeof field.sortType == 'undefined') field.sortType = 'string'
if (typeof field.resizable == 'undefined') field.resizable = true
if (typeof field.sortable == 'undefined') field.sortable = true
if (typeof field.order == 'undefined') field.order = ++order
return field
})
.sort((a, b) => (a.order < b.order ? -1 : a.order > b.order ? 1 : 0))
}
/**
* Filters a data set by a query string, takes schemaTypes object to smartly skip columns of certain type
* @param {Array} data
* @param {String} queryString
* @param {Object} schemaTypes - Keyed object containing schema column types, e.g. {id: 'numeric', title: 'string'}
* @return {Array} - returns filtered data array
*/
export function filterDataByQuery(data = [], queryString = '', schemaTypes = {}) {
// first check if anything is being searched
if (!queryString || queryString === '') return data
// filter all rows
return data.filter(row => {
let rowPassed = false
// map over field names for every row
Object.keys(schemaTypes).map(columnName => {
const rowValue = _get(row, columnName)
// if a number has been searched for then allow for searching in numeric fields
if (
isNumeric(queryString) &&
schemaTypes[columnName] == 'numeric' &&
isNumeric(rowValue) &&
rowValue.toString().indexOf(queryString.toString()) >= 0
) {
rowPassed = true
// otherwise search for query string in all string-type columns
} else if (
schemaTypes[columnName] == 'string' &&
isString(rowValue) &&
rowValue.toLowerCase().indexOf(queryString.toLowerCase()) >= 0
) {
rowPassed = true
}
})
return rowPassed
})
}
/**
* Takes data array, schema array and query string and smarly filters data using schema
* @param {Array} data
* @param {Array} schema
* @param {String} queryString
* @return {Array} - returns filtered data
*/
export function filterDataByQueryWithSchema(data = [], schema = [], queryString = '') {
// create an associative object for column name => content type
const schemaTypes = {}
for (let column of schema) {
schemaTypes[column.property] = getSortTypeFromColumn(column)
}
return filterDataByQuery(data, queryString, schemaTypes)
}
/**
* Takes data array, table fields and query string and smarly filters data using table fields
* @param {Array} data
* @param {Array} fields
* @param {String} queryString
* @return {Array} - returns filtered data
*/
export function filterDataByQueryWithFields(data = [], fields = [], queryString = '') {
// create an associative object for column name => content type
const schemaTypes = {}
for (let column of fields) {
schemaTypes[column.key] = column.sortType
}
return filterDataByQuery(data, queryString, schemaTypes)
}
/**
* Takes data array and a keyed object of filters that correspond to keys in data array objects
* @param {Array} data
* @param {Object} filterset
* @return {Array} - returns filtered data
*/
export function filterDataByFilterset(data = [], filterset = {}, condition = Conditions.OR) {
const properties = Object.keys(filterset)
if (!properties.length) return data
return data.filter(row => {
let passes = 0
for (let property of properties) {
if (filterset[property] === null) {
passes++
} else {
const rowValue = _get(row, property, undefined)
if (rowValue !== undefined) {
if (isFunction(filterset[property]) && filterset[property](rowValue, row)) passes++
else if (rowValue === filterset[property]) passes++
}
}
}
if ((condition == Conditions.OR && passes > 0) || (condition == Conditions.AND && passes === properties.length))
return true
return false
})
}
/**
* Filters out data not within the date range.
*
* @param {Array} data
* @param {String} property
* @param {String|Date} fromDate
* @param {String|Date} toDate
* @return {Array} filtered data
*/
export function filterDataByDateRange(data, property, fromDate, toDate) {
if (fromDate) {
fromDate = moment(fromDate)
data = data.filter(row => {
const value = _get(row, property, undefined)
return value && moment(value) >= fromDate
})
}
if (toDate) {
toDate = moment(toDate)
data = data.filter(row => {
const value = _get(row, property, undefined)
return value && moment(value) <= toDate
})
}
return data
}
/**
* Used for creating compact filterset rules
* Returns a function that takes override values and calls createFilterset the default values, override values, and callback function
* @param {Object} defaultValues Default filterset values
* @param {Function} callback Typically 'handleFilterChange' passed in from AutoTableIndex
* @return {Function} Returns function that takes override values
*/
export function createFiltersetGenerator(defaultValues = {}, callback = () => {}) {
return (values = {}) => createFilterset(defaultValues, values, callback)
}
/**
* Takes default values, override values and a callback function
* Returns an empty function that calls the callback function with the combined values
* @param {Object} defaultValues
* @param {Object} values
* @param {Function} callback Typically 'handleFilterChange' passed in from AutoTableIndex
* @return {Function} Returns function that can be directly called in on onClick prop
*/
export function createFilterset(defaultValues = {}, values = {}, callback = () => {}) {
return () => callback({ ...defaultValues, ...values })
}
/**
* Used for getting a currently selected filterset label based off rules provided
* Note that the order in which the rules are provided is important as the loop short circuits as soon as a checker returns true
* @param {Object} filterset Current filterset, typically provided by AutoTableIndex
* @param {Object} rules Rules object where key is label and value is a checker function that should return a boolean
* @param {String} defaultLabel Default label to return if no rules are matched
* @return {String}
*/
export function getFiltersetLabel(filterset = {}, rules = {}, defaultLabel = 'All') {
let filtersetLabel = defaultLabel
for (const [label, checker] of Object.entries(rules)) {
if (checker && checker(filterset)) {
filtersetLabel = label
break
}
}
return filtersetLabel
}