diff --git a/moped-editor/src/components/GridTable/GridTable.js b/moped-editor/src/components/GridTable/GridTable.js deleted file mode 100644 index d8825ea8b7..0000000000 --- a/moped-editor/src/components/GridTable/GridTable.js +++ /dev/null @@ -1,517 +0,0 @@ -import React, { useState } from "react"; -import { NavLink as RouterLink, useLocation } from "react-router-dom"; - -/** - * Material UI - */ -import { - Box, - Card, - CircularProgress, - Container, - Icon, - Paper, - Table, - TableBody, - TableCell, - TableContainer, - TableRow, - Typography, -} from "@mui/material"; - -import makeStyles from '@mui/styles/makeStyles'; - -// Abstract & GridTable -import { useQuery } from "@apollo/client"; -import GridTableToolbar from "./GridTableToolbar"; -import GridTableListHeader from "./GridTableListHeader"; -import GridTablePagination from "./GridTablePagination"; -import GridTableSearch from "./GridTableSearch"; -import ApolloErrorHandler from "../ApolloErrorHandler"; -import { getSearchValue } from "../../utils/gridTableHelpers"; - -/** - * GridTable Style - */ -const useStyles = makeStyles((theme) => ({ - root: { - width: "100%", - }, - paper: { - width: "100%", - marginBottom: theme.spacing(1), - }, - title: { - position: "relative", - top: "1.2rem", - left: "0.3rem", - "text-shadow": "1px 1px 0px white", - }, - table: { - minWidth: 750, - }, - tableCell: { - "text-transform": "capitalize", - "white-space": "pre-wrap", - }, - noResults: { - paddingTop: "25px", - paddingBottom: "16px", - }, -})); - -/** - * GridTable Component for Material UI - * @param {string} title - The title header of the component - * @param {Object} query - The GraphQL query configuration - * @param {String} searchTerm - The initial term - * @param {Object} referenceData - optional, static data used in presentation - * @param {Object} customComponents - An object containing custom components - * @return {JSX.Element} - * @constructor - */ -const GridTable = ({ - title, - query, - searchTerm, - referenceData, - customComponents, -}) => { - const classes = useStyles(); - - /** - * State Management for pagination - * @type {Object} pagination - * @property {integer} limit - The limit of records to be shown in a single page (default: query.limit) - * @property {integer} offset - The number of records to be skipped in GraphQL (default: query.limit) - * @property {integer} page - Current page being shown (0 to N) where 0 is the first page (default: 0) - * @function setPagination - Sets the state of pagination - * @default {{limit: query.limit, offset: query.offset, page: 0}} - */ - const [pagination, setPagination] = useState({ - limit: query.limit, - offset: query.offset, - page: 0, - }); - - /** - * Stores the column name and the order to order by - * @type {Object} sort - * @property {string} column - The column name in graphql to sort by - * @property {string} order - Either "asc" or "desc" or "" (default: "") - * @function setSort - Sets the state of sort - * @default {{value: "", column: ""}} - */ - const [sort, setSort] = useState({ - column: "", - order: "", - }); - - /** - * Stores the string to search for and the column to search against - * @type {Object} search - * @property {string} value - The string to be searched for - * @property {string} column - The name of the column to search against - * @function setSearch - Sets the state of search - * @default {{value: "", column: ""}} - */ - const [search, setSearch] = useState({ - value: searchTerm ?? "", - column: "", - }); - - // anchor element for advanced search popper in GridTableSearch to "attach" to - // State is handled here so we could listen for changes in a useeffect in this component - const [advancedSearchAnchor, setAdvancedSearchAnchor] = useState(null); - - // create URLSearchParams from url - const filterQuery = new URLSearchParams(useLocation().search); - - /** - * if filter exists in url, decodes base64 string and returns as object - * Used to initialize filter state - * @return Object if valid JSON otherwise false - */ - const getFilterQuery = () => { - if (Array.from(filterQuery).length > 0) { - try { - return JSON.parse(atob(filterQuery.get("filter"))); - } catch { - return false; - } - } - return false; - }; - - /** - * Stores objects storing a random id, column, operator, and value. - * @type {Object} filters - * @function setFilter - Sets the state of filters - * @default {if filter in url, use those params, otherwise {}} - */ - const [filters, setFilter] = useState(getFilterQuery() || {}); - - /** - * Query Management - */ - // Manage the ORDER BY clause of our query - if (sort.column !== "" && sort.order !== "") { - query.setOrder(sort.column, sort.order); - } - - // Set limit, offset and clear any 'Where' filters - if (query.config.showPagination) { - query.limit = pagination.limit; - query.offset = pagination.offset; - } else { - query.limit = 0; - } - - query.cleanWhere(); - - // If we have a search value in state, initiate search - // GridTableSearchBar in GridTableSearch updates search value - if (search.value && search.value !== "") { - /** - * Iterate through all column keys, if they are searchable - * add the to the Or list. - */ - Object.keys(query.config.columns) - .filter((column) => query.config.columns[column]?.searchable) - .forEach((column) => { - const { operator, quoted, envelope } = - query.config.columns[column].search; - const searchValue = getSearchValue(query, column, search.value); - const graphqlSearchValue = quoted - ? `"${envelope.replace("{VALUE}", searchValue)}"` - : searchValue; - - query.setOr(column, `${operator}: ${graphqlSearchValue}`); - }); - } - - // For each filter added to state, add a where clause in GraphQL - // Advanced Search - Object.keys(filters).forEach((filter) => { - let { envelope, field, gqlOperator, value, type, specialNullValue } = - filters[filter]; - - // If we have no operator, then there is nothing we can do. - if (field === null || gqlOperator === null) { - return; - } - - // If the operator includes "is_null", we check for empty strings - if (gqlOperator.includes("is_null")) { - gqlOperator = envelope === "true" ? "_eq" : "_neq"; - value = specialNullValue ? specialNullValue : '""'; - } else { - if (value !== null) { - // If there is an envelope, insert value in envelope. - value = envelope ? envelope.replace("{VALUE}", value) : value; - - // If it is a number or boolean, it does not need quotation marks - // Otherwise, add quotation marks for the query to identify as string - value = type in ["number", "boolean"] ? value : `"${value}"`; - } else { - // We don't have a value - return; - } - } - query.setWhere(field, `${gqlOperator}: ${value}`); - }); - - /** - * Handles the header click for sorting asc/desc. - * @param {string} columnName - The name of the column - **/ - const handleTableHeaderClick = (columnName) => { - // Before anything, let's clear all current conditions - query.clearOrderBy(); - - // If both column and order are empty... - if (sort.order === "" && sort.column === "") { - // First time sort is applied - setSort({ - order: "asc", - column: columnName, - }); - } else if (sort.column === columnName) { - // Else if the current sortColumn is the same as the new - // then invert values and repeat sort on column - setSort({ - order: sort.order === "desc" ? "asc" : "desc", - column: columnName, - }); - } else if (sort.column !== columnName) { - // Sort different column after initial sort, then reset - setSort({ - order: "desc", - column: columnName, - }); - } - }; - - /** - * Returns true if the input string is a valid alphanumeric object key - * @param {string} input - The string to be tested - * @returns {boolean} - */ - const isAlphanumeric = (input) => input.match(/^[0-9a-zA-Z\-_]+$/) !== null; - - /** - * Extracts a list of keys in a graphql expression - * @param {string} exp - The expression - * @returns {Array} - */ - const listKeys = (exp) => - exp.split(/[{} ]+/).filter((n) => isAlphanumeric(n) && n !== ""); - - /** - * Returns the value of a data structure based on the list of keys provided - * @param {object} obj - the item from the row section - * @param {Array} keys - the list of keys - * @returns {*} - */ - const responseValue = (obj, keys) => { - for (let k = 1; k < keys.length; k++) { - try { - obj = obj[keys[k]]; - } catch { - obj = null; - } - } - return obj; - }; - - /** - * Extracts the value (or summary of values) for nested field names - * @param {object} obj - The dataset current object (the table row) - * @param {string} exp - The graphql expression (from the column name) - * @returns {string} - */ - const getSummary = (obj, exp) => { - let result = []; - let map = new Map(); - const keys = listKeys(exp); - - // First we need to get to the specific section of the dataset object - // The first key is the outermost nested part of the graphql query - const section = obj[keys[0]]; - - // Bypass value extraction if column value should be "stringified" - if (query.config.columns[exp]?.stringify) { - return JSON.stringify(section); - } - - // If not an array, resolve its value - if (!Array.isArray(section)) { - // Return direct value - return responseValue(section, keys); - } - - // If it is an array, resolve each and aggregate - for (let item of section) { - let val = responseValue(item, keys); - - if (val !== null) { - map.set(val, true); - result.push(val); - } - } - // Merge all into a string - return result.join(", "); - }; - - /** - * Returns a stringified object with information to format link. - * @param {string} row - row from data - * @param {Object} column - column with link attribute - * @return {string} - */ - const buildLinkData = (row, column) => - JSON.stringify({ - singleItem: query.singleItem, - data: row[column], - link: row[query.config.columns[column].link], - state: { - filters: Object.keys(filters).length - ? btoa(JSON.stringify(filters)) - : false, - }, - }); - - /** - * Data Management - */ - const { data, loading, error } = useQuery( - query.gql, - query.config.options.useQuery - ); - - return ( - - - {/*Title*/} - - {title} - - {/*Toolbar Space*/} - - {customComponents?.toolbar?.before} - - {customComponents?.toolbar?.after} - - {/*Main Table Body*/} - {customComponents?.table?.before} - - - {loading ? ( - - ) : data ? ( - - - - - - {data[query.table].length < 1 ? ( - - - - {query.config.noResultsMessage ?? - "No results found"} - - - - ) : ( - data[query.table].map((row, rowIndex) => { - return ( - - {query.columns.map( - (column, columnIndex) => - // If column is hidden, don't render
- !query.isHidden(column) && ( - - {query.isPK(column) ? ( - // If there is custom JSX for the PK single item button, render it - (query.config.customSingleItemButton && - query.config.customSingleItemButton( - row[column] - )) || ( - - {query.config.columns[ - column - ].hasOwnProperty("icon") ? ( - - { - query.config.columns[column] - .icon.name - } - - ) : ( - row[column] - )} - - ) - ) : query.config.columns[column]?.link ? ( - query.getFormattedValue( - column, - buildLinkData(row, column) - ) - ) : isAlphanumeric(column) ? ( - <> - {query.getFormattedValue( - column, - row[column] - )} - - ) : ( - // if column is not alphanumeric - // it is formatted like a nested query - query.getFormattedValue( - column, - getSummary(row, column.trim()) - ) - )} - - ) - )} - - ); - }) - )} - -
-
- - {/*Pagination Management*/} - {query.config.showPagination && ( - - )} -
- ) : ( - {error ? error : "Could not fetch data"} - )} -
-
- {customComponents?.table?.after} -
-
- ); -}; - -export default GridTable; diff --git a/moped-editor/src/components/GridTable/GridTableListHeader.js b/moped-editor/src/components/GridTable/GridTableListHeader.js deleted file mode 100644 index 1ee5b7985c..0000000000 --- a/moped-editor/src/components/GridTable/GridTableListHeader.js +++ /dev/null @@ -1,105 +0,0 @@ -import React from "react"; - -import { TableHead, TableRow, TableCell, Icon, Grid } from "@mui/material"; -import makeStyles from '@mui/styles/makeStyles'; - -/** - * GridTableListHeader Styles - */ -const useStyles = makeStyles(theme => ({ - columnTitle: { - "font-weight": "bold", - }, - columnTitleSortable: { - paddingTop: "8px", - "font-weight": "bold", - }, - columnCell: { - "user-select": "none", - backgroundColor: theme.palette.background.paper, - }, - columnCellCursor: { - cursor: "pointer", - "user-select": "none", - backgroundColor: theme.palette.background.paper, - }, -})); - -/** - * GridTableListHeader Component - * @param {GQLAbstract} query - The GQLAbstract class passed down for reference - * @param {function} handleTableHeaderClick - A handler for table header clicks - * @param {string} sortColumn - The name of the column to sort by - * @param {string} sortOrder - The order in which the sorting is made: asc, desc - * @return {JSX.Element} - * @constructor - */ -const GridTableListHeader = ({ - query, - handleTableHeaderClick, - sortColumn, - sortOrder, -}) => { - const classes = useStyles(); - - /** - * Renders a label with sorting icons going up or down - * @param {string} col - The name of the column (label) - * @param {boolean} sortable - True if the column is sortable - * @param {boolean} ascending - True if ordering in ascending mode - * @returns {object} jsx component - */ - const renderLabel = (col, sortable = false, ascending = false) => { - return ( - - {col}  - {sortable && ( - - arrow_{ascending ? "up" : "down"}ward - - )} - - ); - }; - - return ( - - - {query.columns.map( - (column, index) => - // If column is hidden, don't render - !query.isHidden(column) && ( - handleTableHeaderClick(column) - : null - } - key={`th-${index}`} - size="small" - > - {renderLabel( - // Get a human-readable label string - query.config.columns[column].label, - // If it is sortable, render as such - query.isSortable(column), - // If sort column is defined, use sort order, or false as default - sortColumn === column ? sortOrder === "asc" : false - )} - - ) - )} - - - ); -}; - -export default GridTableListHeader; diff --git a/moped-editor/src/components/GridTable/GridTableNewItem.js b/moped-editor/src/components/GridTable/GridTableNewItem.js deleted file mode 100644 index 81880e42d4..0000000000 --- a/moped-editor/src/components/GridTable/GridTableNewItem.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from "react"; -import PropTypes from "prop-types"; - -import { Box, Button, Icon } from "@mui/material"; -import { NavLink as RouterLink } from "react-router-dom"; - -/** - * Based on GQLAbstract configuration, renders add new Item button - * @param {GQLAbstract} query - The GQLAbstract query object that provides the configuration - * @return {JSX.Element} - * @constructor - */ -const GridTableNewItem = ({ query }) => { - return ( - - {query.config.customNewItemButton || ( - - )} - - ); -}; - -GridTableNewItem.propTypes = { - className: PropTypes.string, -}; - -export default GridTableNewItem; diff --git a/moped-editor/src/components/GridTable/GridTableSearch.js b/moped-editor/src/components/GridTable/GridTableSearch.js index 9aea10cb07..8a9e14c156 100644 --- a/moped-editor/src/components/GridTable/GridTableSearch.js +++ b/moped-editor/src/components/GridTable/GridTableSearch.js @@ -19,7 +19,6 @@ import { import SaveAltIcon from "@mui/icons-material/SaveAlt"; import GridTableFilters from "./GridTableFilters"; import GridTableSearchBar from "./GridTableSearchBar"; -import GridTableNewItem from "./GridTableNewItem"; import makeStyles from '@mui/styles/makeStyles'; import { useLazyQuery } from "@apollo/client"; import { format } from "date-fns"; @@ -85,7 +84,6 @@ const GridTableSearch = ({ query, searchState, filterState, - children, filterQuery, parentData = null, advancedSearchAnchor, @@ -160,7 +158,6 @@ const GridTableSearch = ({ // If there is a filter, use it. Assign the value to the new column name. entry[newColumnName] = filter ? filter(record[column]) : record[column]; }); - // Return new object return entry; }; @@ -256,8 +253,6 @@ const GridTableSearch = ({ return (
- {query.config.showNewItemButton && } - {children} diff --git a/moped-editor/src/components/GridTable/GridTableToolbar.js b/moped-editor/src/components/GridTable/GridTableToolbar.js deleted file mode 100644 index 63223f0915..0000000000 --- a/moped-editor/src/components/GridTable/GridTableToolbar.js +++ /dev/null @@ -1,27 +0,0 @@ -import React from "react"; -import clsx from "clsx"; - -import makeStyles from '@mui/styles/makeStyles'; - - -const useStyles = makeStyles(theme => ({ - root: {}, - importButton: { - marginRight: theme.spacing(1), - }, - exportButton: { - marginRight: theme.spacing(1), - }, -})); - -const GridTableToolbar = ({ children, change, className, ...rest }) => { - const classes = useStyles(); - - return ( -
- {children} -
- ); -}; - -export default GridTableToolbar; diff --git a/moped-editor/src/libs/GQLAbstract.js b/moped-editor/src/libs/GQLAbstract.js index a5e0e38e20..31c701c559 100644 --- a/moped-editor/src/libs/GQLAbstract.js +++ b/moped-editor/src/libs/GQLAbstract.js @@ -122,18 +122,6 @@ class GQLAbstract { return this.config.offset; } - /** - * Returns an array of searchable columns - * @returns {Array} - */ - get searchableFields() { - const columns = []; - for (const [key, value] of this.getEntries("columns")) { - if (value.searchable) columns.push(key); - } - return columns; - } - /** * Resets the value of where and or to empty */ @@ -226,139 +214,8 @@ class GQLAbstract { * @param {string} syntax - either 'asc' or 'desc' */ setOrder(key, syntax) { - if (this.config && this.config.order_by) { - // First, RESET the order_by value, with the assumption - // that there should only by 1 order_by at a time. - // This assumption is a self-imposed subset of the GraphQL syntax - // which happens to make the removal of implicit ordering - // of order directives as implemented by Hasura in graphql-engine - // 2.0+ a non-issue for this app. this.config.order_by = {}; - // Now, set new key, syntax pair for order_by this.config.order_by[key] = syntax; - } else { - this.config.order_by = {}; - this.config.order_by[key] = syntax; - } - } - - /** - * Returns true if a column is defined as sortable in the config, assumes false if not found. - * @param {string} columnName - The name of the column in the config - * @returns {boolean} - */ - isSortable(columnName) { - return this.config.columns[columnName].sortable || false; - } - - /** - * Returns true if a column is defined as hidden in the config, assumes false if not found. - * @param {string} columnName - The name of the column in the config - * @returns {boolean} - */ - isHidden(columnName) { - return this.config.columns[columnName].hidden || false; - } - - /** - * Returns true if a column is defined as searchable in the config, assumes false if not found. - * @param {string} columnName - The name of the column in the config - * @returns {boolean} - */ - isSearchable(columnName) { - return this.config.columns[columnName].searchable || false; - } - - /** - * Returns true if a column is defined as primary key in the config, assumes false if not found. - * @param {string} columnName - The name of the column in the config - * @returns {boolean} - */ - isPK(columnName) { - return this.config.columns[columnName].primary_key || false; - } - - /** - * Returns the type of a column as defined in the config, assumes string if not found. - * @param {string} columnName - The name of the column in the config - * @returns {string} - */ - getType(columnName) { - return (this.config.columns[columnName].type || "string").toLowerCase(); - } - - /** - * Returns true if the column contains a filter - * @param {string} columnName - The name of the column in the config - * @return {boolean} - */ - hasFilter(columnName) { - return !!this.config.columns[columnName].filter; - } - - /** - * Returns the default value when value is null - * @param {string} columnName - The name of the column in the config - * @returns {string} - */ - getDefault(columnName) { - return this.config.columns[columnName].default; - } - - /** - * Attempts to format value based on configuration specification `format` - * @param {string} columnName - The column to read the configuration from - * @param {object} value - The actual value to be presented to the component - */ - getFormattedValue(columnName, value) { - const type = this.getType(columnName); - - if (value === null) { - return "-"; - } else { - value = String(value); - } - - if (this.hasFilter(columnName)) { - return this.config.columns[columnName].filter(value); - } - - switch (type) { - case "string": { - if (typeof value === "object") return JSON.stringify(value); - else return `${value}`; - } - case "date_iso": { - let dateValue = ""; - try { - dateValue = new Date(Date.parse(value)).toLocaleString(); - } catch { - dateValue = "n/a"; - } - return `${dateValue}`; - } - case "currency": { - return `$${value.toLocaleString()}`; - } - case "boolean": { - return value ? "True" : "False"; - } - // Integers, Decimals - default: { - return `${value}`; - } - } - } - - /** - * Returns the label for a column as specified in the config, either a 'table' label or 'search' label. - * Returns null if the label is not found. Assumes type as 'table'. - * @param {string} columnName - The name of the column. - * @param {string} type - Type type: 'table' or 'search' - * @returns {string|null} - */ - getLabel(columnName, type = "table") { - return this.config.columns[columnName]["label_" + type] || null; } /** @@ -378,21 +235,6 @@ class GQLAbstract { return this.getEntries("columns").map((k) => k[0]); } - /** - * Returns the url path for a single item, or null if ot does not exist. - * @returns {string|null} - */ - get singleItem() { - return this.config.single_item || null; - } - - /** - * Returns the showDateRange configuration value as a boolean. - * @return {boolean} - */ - get showDateRange() { - return this.config.showDateRange || false; - } /** * Generates the filters section and injects the abstract with finished GraphQL syntax. @@ -613,35 +455,6 @@ class GQLAbstract { }`; } - /** - * Sets the options for Apollo query methods - * @param {string} optionType - The method in question: useQuery, useMutation, etc. - * @param {object} optionsObject - A key value pair with Apollo config stipulations. - */ - setOption(optionType, optionsObject) { - this.config.options[optionType] = optionsObject; - } - - /** - * Returns an apollo query option by type - * @param {string} optionType - The option type name being retrieved: useQuery, useMutation, etc. - */ - getOption(optionType) { - try { - return this.config.options[optionType]; - } catch { - return {}; - } - } - - /** - * Returns a key-value object with options for the Apollo useQuery method - * @returns {object} - The options object - */ - get useQueryOptions() { - return this.getOption("useQuery") || {}; - } - /** * Returns a GQL object based on the current state of the configuration. * @returns {Object} gql object diff --git a/moped-editor/src/views/projects/projectsListView/ProjectsListViewQueryConf.js b/moped-editor/src/views/projects/projectsListView/ProjectsListViewQueryConf.js index 4e442a3cd7..06082119c9 100644 --- a/moped-editor/src/views/projects/projectsListView/ProjectsListViewQueryConf.js +++ b/moped-editor/src/views/projects/projectsListView/ProjectsListViewQueryConf.js @@ -2,10 +2,8 @@ import React from "react"; import { ProjectsListViewFiltersConf } from "./ProjectsListViewFiltersConf"; import { ProjectsListViewExportConf } from "./ProjectsListViewExportConf"; import ExternalLink from "../../../components/ExternalLink"; -import { NavLink as RouterLink } from "react-router-dom"; import { filterProjectTeamMembers } from "./helpers.js"; import { formatTimeStampTZType } from "src/utils/dateAndTime"; -import theme from "src/theme/index"; /** * The Query configuration (now also including filters) @@ -24,7 +22,6 @@ export const ProjectsListViewQueryConf = { single_item: "/moped/projects", new_item: "/moped/projects/new", new_item_label: "New Project", - showDateRange: false, showSearchBar: true, showFilters: false, showExport: true, @@ -75,18 +72,6 @@ export const ProjectsListViewQueryConf = { }, width: "*", type: "String", - filter: (values) => { - const jsonValues = JSON.parse(values); - return ( - - {jsonValues.data} - - ); - }, }, project_description: { hidden: true, diff --git a/moped-editor/src/views/projects/projectsListView/ProjectsListViewTable.js b/moped-editor/src/views/projects/projectsListView/ProjectsListViewTable.js index 9be09e24a9..d79b1f656d 100644 --- a/moped-editor/src/views/projects/projectsListView/ProjectsListViewTable.js +++ b/moped-editor/src/views/projects/projectsListView/ProjectsListViewTable.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useMemo } from "react"; import { NavLink as RouterLink, useLocation } from "react-router-dom"; import { Box, Card, CircularProgress, Container, Paper } from "@mui/material"; @@ -7,7 +7,6 @@ import makeStyles from "@mui/styles/makeStyles"; import typography from "../../../theme/typography"; import { useQuery } from "@apollo/client"; -import GridTableToolbar from "../../../components/GridTable/GridTableToolbar"; import GridTableSearch from "../../../components/GridTable/GridTableSearch"; import GridTablePagination from "../../../components/GridTable/GridTablePagination"; import ApolloErrorHandler from "../../../components/ApolloErrorHandler"; @@ -15,7 +14,7 @@ import ProjectStatusBadge from "./../projectView/ProjectStatusBadge"; import ExternalLink from "../../../components/ExternalLink"; import RenderSignalLink from "../signalProjectTable/RenderSignalLink"; -import MaterialTable, { MTableBody, MTableHeader } from "@material-table/core"; +import MaterialTable, { MTableHeader } from "@material-table/core"; import { filterProjectTeamMembers as renderProjectTeamMembers } from "./helpers.js"; import { getSearchValue } from "../../../utils/gridTableHelpers"; import { formatDateType, formatTimeStampTZType } from "src/utils/dateAndTime"; @@ -97,6 +96,28 @@ const handleColumnChange = ({ field }, hidden) => { localStorage.setItem("mopedColumnConfig", JSON.stringify(storedConfig)); }; +const useFilterQuery = (locationSearch) => + useMemo(() => { + return new URLSearchParams(locationSearch); + }, [locationSearch]); + +/** + * if filter exists in url, decodes base64 string and returns as object + * Used to initialize filter state + * @return Object + */ +const useMakeFilterState = (filterQuery) => + useMemo(() => { + if (Array.from(filterQuery).length > 0) { + try { + return JSON.parse(atob(filterQuery.get("filter"))); + } catch { + return {}; + } + } + return {}; + }, [filterQuery]); + /** * GridTable Search Capability plus Material Table * @param {Object} query - The GraphQL query configuration @@ -162,23 +183,8 @@ const ProjectsListViewTable = ({ query, searchTerm }) => { const [advancedSearchAnchor, setAdvancedSearchAnchor] = useState(null); // create URLSearchParams from url - const filterQuery = new URLSearchParams(useLocation().search); - - /** - * if filter exists in url, decodes base64 string and returns as object - * Used to initialize filter state - * @return Object if valid JSON otherwise false - */ - const getFilterQuery = () => { - if (Array.from(filterQuery).length > 0) { - try { - return JSON.parse(atob(filterQuery.get("filter"))); - } catch { - return false; - } - } - return false; - }; + const filterQuery = useFilterQuery(useLocation().search); + const initialFilterState = useMakeFilterState(filterQuery); /** * Stores objects storing a random id, column, operator, and value. @@ -186,17 +192,16 @@ const ProjectsListViewTable = ({ query, searchTerm }) => { * @function setFilter - Sets the state of filters * @default {if filter in url, use those params, otherwise {}} */ - const [filters, setFilter] = useState(getFilterQuery() || {}); + const [filters, setFilter] = useState(initialFilterState); const [hiddenColumns, setHiddenColumns] = useState( JSON.parse(localStorage.getItem("mopedColumnConfig")) ?? DEFAULT_HIDDEN_COLS ); - /** - * Query Management - */ // Manage the ORDER BY clause of our query - query.setOrder(sort.column, sort.order); + useEffect(() => { + query.setOrder(sort.column, sort.order); + }, [sort.column, sort.order, query]); // Set limit, offset based on pagination state if (query.config.showPagination) { @@ -274,7 +279,7 @@ const ProjectsListViewTable = ({ query, searchTerm }) => { const buildStatusBadge = ({ phaseName, phaseKey }) => ( ); - // Data Management + const { data, loading, error } = useQuery( query.gql, query.config.options.useQuery @@ -618,24 +623,21 @@ const ProjectsListViewTable = ({ query, searchTerm }) => { return ( - {/*Toolbar Space*/} - - - + {/*Main Table Body*/} @@ -681,24 +683,6 @@ const ProjectsListViewTable = ({ query, searchTerm }) => { orderDirection={sort.order} /> ), - Body: (props) => { - // see PR #639 https://github.com/cityofaustin/atd-moped/pull/639 for context - // we have configured MT to use local data but are technically using remote data - // this results in inconsistencies with how MT displays filtered data - const indexedData = data["project_list_view"].map( - (row, index) => ({ - tableData: { id: index, uuid: row.project_id }, - ...row, - }) - ); - return ( - - ); - }, }} />