diff --git a/src/js/components/search/resultsView/ResultsView.jsx b/src/js/components/search/resultsView/ResultsView.jsx
index ec92447789..97f3ee677c 100644
--- a/src/js/components/search/resultsView/ResultsView.jsx
+++ b/src/js/components/search/resultsView/ResultsView.jsx
@@ -3,15 +3,12 @@
* Created by Andrea Blackwell
**/
-import React, { useEffect, useState } from "react";
+import React from "react";
import PropTypes from "prop-types";
-import { isCancel } from "axios";
import { useSelector } from "react-redux";
import TopFilterBarContainer from "containers/search/topFilterBar/TopFilterBarContainer";
-import SearchAwardsOperation from "models/v1/search/SearchAwardsOperation";
-import { performSpendingByAwardTabCountSearch, areFiltersEqual } from "helpers/searchHelper";
-import { performTabCountSearch } from "helpers/keywordHelper";
+import useResultsCount from "containers/search/resultsView/useResultsCount";
import NewSearchScreen from "./NewSearchScreen";
import NoDataScreen from "./NoDataScreen";
import SectionsContent from "./SectionsContent";
@@ -34,124 +31,57 @@ const ResultsView = React.memo(function ResultsView({
hash,
setFilterCount
}) {
- const [hasResults, setHasResults] = useState(false);
- const [resultContent, setResultContent] = useState(null);
- const [tabData, setTabData] = useState();
- const [inFlight, setInFlight] = useState(false);
- const [error, setError] = useState(false);
-
const filters = useSelector((state) => state.appliedFilters.filters);
const spendingLevel = useSelector((state) => state.searchView.spendingLevel);
- let countRequest;
-
- const checkForData = () => {
- if (countRequest) {
- countRequest.cancel();
- }
-
- const searchParamsTemp = new SearchAwardsOperation();
- searchParamsTemp.fromState(filters);
+ const { data, error } = useResultsCount(filters, spendingLevel, hash);
- setInFlight(true);
- setError(false);
+ let content = null;
- if (spendingLevel === 'transactions') {
- countRequest = performTabCountSearch({
- filters: searchParamsTemp.toParams(),
- spending_level: spendingLevel,
- auditTrail: 'Results View - Tab Counts'
- });
- }
- else {
- // if subawards is true, newAwardsOnly cannot be true, so we remove dateType
- if (spendingLevel === 'subawards') {
- delete searchParamsTemp.dateType;
- }
+ if (!error && data) {
+ /* eslint-disable camelcase */
+ const {
+ contracts, direct_payments, grants, idvs, loans, other, subgrants, subcontracts
+ } = data.data.results;
+ let resCount = contracts + direct_payments + grants + idvs + loans + other;
- countRequest = performSpendingByAwardTabCountSearch({
- filters: searchParamsTemp.toParams(),
- spending_level: spendingLevel,
- auditTrail: 'Results View - Tab Counts'
- });
+ if (spendingLevel === 'subawards') {
+ resCount = subgrants + subcontracts;
}
- countRequest.promise
- .then((res) => {
- /* eslint-disable camelcase */
- setTabData(res.data);
- const {
- contracts, direct_payments, grants, idvs, loans, other, subgrants, subcontracts
- } = res.data.results;
- let resCount = contracts + direct_payments + grants + idvs + loans + other;
+ const hasResults = resCount > 0;
+ /* eslint-enable camelcase */
- if (spendingLevel === 'subawards') {
- resCount = subgrants + subcontracts;
- }
- /* eslint-enable camelcase */
-
- if (resCount > 0) {
- setHasResults(true);
- }
- else {
- setHasResults(false);
- }
-
- setInFlight(false);
- setError(false);
- })
- .catch((err) => {
- if (!isCancel(err)) {
- setInFlight(false);
- setError(true);
- console.log(err);
- }
- });
- };
-
- useEffect(() => {
- if (!areFiltersEqual(filters) || !hash) {
- checkForData();
+ if (!hash && noFiltersApplied) {
+ content = ;
}
- return () => {
- countRequest?.cancel();
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [filters, spendingLevel]);
- useEffect(() => {
- let content = null;
-
- if (!inFlight && !error) {
- if (!hash && noFiltersApplied) {
- content = ;
+ if (!noFiltersApplied) {
+ if (hasResults) {
+ content = (
+
+ );
}
-
- if (!noFiltersApplied) {
- if (hasResults) {
- content = (
-
- );
- }
- else {
- content = ;
- }
+ else {
+ content = ;
}
}
-
- setResultContent(content);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [noFiltersApplied, hasResults, inFlight, error, hash]);
+ }
return (
-
-
- {resultContent}
+
+
+ {content}
diff --git a/src/js/components/search/resultsView/SectionsContent.jsx b/src/js/components/search/resultsView/SectionsContent.jsx
index b1e899a031..bbb8595f70 100644
--- a/src/js/components/search/resultsView/SectionsContent.jsx
+++ b/src/js/components/search/resultsView/SectionsContent.jsx
@@ -6,21 +6,34 @@
import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
+import Analytics from "helpers/analytics/Analytics";
import TableSection from "./table/TableSection";
import CategoriesSection from "./categories/CategoriesSection";
import TimeSection from "./time/TimeSection";
import MapSection from "./map/MapSection";
-import Analytics from "../../../helpers/analytics/Analytics";
require("pages/search/searchPage.scss");
+const logVisualizationViewEvent = (action, label) => window.setTimeout(
+ () => Analytics.event({
+ event: 'search_visualization_type',
+ category: 'Advanced Search - Visualization Type',
+ action,
+ gtm: true,
+ label
+ }), 15 * 1000);
+
const propTypes = {
tabData: PropTypes.object,
hash: PropTypes.string,
spendingLevel: PropTypes.string
};
-const SectionsContent = (props) => {
+const SectionsContent = ({
+ tabData,
+ hash,
+ spendingLevel
+}) => {
const [observerSupported, setObserverSupported] = useState(false);
const [timeHasLoaded, setTimeHasLoaded] = useState(false);
const [categoriesHasLoaded, setCategoriesHasLoaded] = useState(false);
@@ -30,36 +43,24 @@ const SectionsContent = (props) => {
threshold: 0.1
};
- const logVisualizationViewEvent = (activeLabel) => {
- window.setTimeout(() => {
- Analytics.event({
- event: 'search_visualization_type',
- category: 'Advanced Search - Visualization Type',
- action: activeLabel,
- gtm: true,
- label: props.hash
- });
- }, 15 * 1000);
- };
-
const callbackFunction = (entries) => {
entries.forEach((entry) => {
const section = entry.target.className;
if (entry.isIntersecting) {
if (section === 'awards') {
- logVisualizationViewEvent("awards");
+ logVisualizationViewEvent("awards", hash);
}
else if (section === 'time') {
setTimeHasLoaded(true);
- logVisualizationViewEvent("time");
+ logVisualizationViewEvent("time", hash);
}
else if (section === 'categories') {
setCategoriesHasLoaded(true);
- logVisualizationViewEvent("categories");
+ logVisualizationViewEvent("categories", hash);
}
else if (section === "map") {
setMapHasLoaded(true);
- logVisualizationViewEvent("map");
+ logVisualizationViewEvent("map", hash);
}
}
});
@@ -67,7 +68,6 @@ const SectionsContent = (props) => {
useEffect(() => {
setObserverSupported('IntersectionObserver' in window);
- // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// eslint-disable-next-line consistent-return
@@ -88,25 +88,28 @@ const SectionsContent = (props) => {
return () => observer.disconnect();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [observerSupported, props.hash]);
+ }, [observerSupported, hash]);
return (
<>
-
+
+ hash={hash} />
+ hash={hash}
+ spendingLevel={spendingLevel} />
+ hash={hash} />
>
);
};
diff --git a/src/js/components/search/resultsView/table/TableSection.jsx b/src/js/components/search/resultsView/table/TableSection.jsx
index 4fd499020c..50ae35ca14 100644
--- a/src/js/components/search/resultsView/table/TableSection.jsx
+++ b/src/js/components/search/resultsView/table/TableSection.jsx
@@ -2,9 +2,9 @@
* TableSection.jsx
*/
-import React from "react";
+import React, { useMemo } from "react";
import PropTypes from "prop-types";
-import ResultsTableContainer from "../../../../containers/search/resultsView/ResultsTableContainer";
+import ResultsTableContainer from "containers/search/resultsView/ResultsTableContainer";
import TableDsm from "./TableDsm";
@@ -17,27 +17,19 @@ const propTypes = {
const TableSection = ({
tabData, hash, spendingLevel
}) => {
- const sectionTitle = () => {
- switch (spendingLevel) {
- case 'awards': return 'Prime Award Results';
- case 'subawards': return 'Subaward Results';
- default: return 'Transaction Results';
- }
- };
+ const sectionTitle = spendingLevel === "awards" ? 'Prime Award Results' : 'Subaward Results';
- const wrapperProps = {
- sectionTitle: sectionTitle(),
- dsmContent:
,
- sectionName: 'table'
- };
+ const dsmContent = useMemo(() =>
, [spendingLevel]);
return (
+ spendingLevel={spendingLevel}
+ sectionTitle={sectionTitle}
+ dsmContent={dsmContent}
+ sectionName="table" />
);
};
diff --git a/src/js/containers/account/awards/AccountAwardsContainer.jsx b/src/js/containers/account/awards/AccountAwardsContainer.jsx
index 6d22899372..60d8d31456 100644
--- a/src/js/containers/account/awards/AccountAwardsContainer.jsx
+++ b/src/js/containers/account/awards/AccountAwardsContainer.jsx
@@ -15,7 +15,7 @@ import * as SearchHelper from 'helpers/searchHelper';
import { defaultColumns, defaultSort } from 'dataMapping/search/awardTableColumns';
import AccountAwardSearchOperation from 'models/v1/account/queries/AccountAwardSearchOperation';
import ResultsTableSection from 'components/search/resultsView/table/ResultsTableSection';
-import { tableTypes, subTypes } from 'containers/search/resultsView/ResultsTableContainer';
+import { tableTypes, subTypes } from 'dataMapping/search/resultsView/table';
import { SectionHeader } from "data-transparency-ui";
const propTypes = {
diff --git a/src/js/containers/search/resultsView/ResultsTableContainer.jsx b/src/js/containers/search/resultsView/ResultsTableContainer.jsx
index 2763a29517..7b646bc9ce 100644
--- a/src/js/containers/search/resultsView/ResultsTableContainer.jsx
+++ b/src/js/containers/search/resultsView/ResultsTableContainer.jsx
@@ -3,394 +3,122 @@
* Created by Kevin Li 11/8/16
**/
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, memo } from 'react';
import { useLocation } from 'react-router';
import PropTypes from 'prop-types';
-import { bindActionCreators } from 'redux';
-import { connect } from 'react-redux';
-import { isCancel } from 'axios';
-import { uniqueId, intersection, throttle } from 'lodash-es';
-import SearchAwardsOperation from 'models/v1/search/SearchAwardsOperation';
+import { useDispatch, useSelector } from 'react-redux';
+import { throttle } from 'lodash-es';
+
import { subAwardIdClicked } from 'redux/actions/search/searchSubAwardTableActions';
-import * as SearchHelper from 'helpers/searchHelper';
+import { measureTableHeader } from 'helpers/textMeasurement';
import Analytics from 'helpers/analytics/Analytics';
-import { awardTypeGroups, subawardTypeGroups, transactionTypeGroups } from 'dataMapping/search/awardType';
+import { tableTypes, subTypes, transactionTypes } from 'dataMapping/search/resultsView/table';
import {
defaultColumns,
- defaultSort,
- apiFieldByTableColumnName
+ defaultSort
} from 'dataMapping/search/awardTableColumns';
import { awardTableColumnTypes } from 'dataMapping/search/awardTableColumnTypes';
-import { measureTableHeader } from 'helpers/textMeasurement';
import ResultsTableSection from 'components/search/resultsView/table/ResultsTableSection';
-import searchActions from 'redux/actions/searchActions';
-import * as appliedFilterActions from 'redux/actions/search/appliedFilterActions';
-import SearchSectionWrapper from "../../../components/search/resultsView/SearchSectionWrapper/SearchSectionWrapper";
-import { performKeywordSearch } from "../../../helpers/keywordHelper";
+import SearchSectionWrapper from
+ "components/search/resultsView/SearchSectionWrapper/SearchSectionWrapper";
+import useResultsTableSearch from './useResultsTableSearch';
+
+const createColumn = (col) => {
+ // create an object that integrates with the expected column data structure used by
+ // the table component
+
+ // BODGE: Temporarily only allow descending columns
+ const direction = 'desc';
+ const width = col.customWidth || measureTableHeader(col.displayName || col.title);
+
+ return {
+ columnName: col.title,
+ displayName: col.displayName || col.title,
+ subtitle: col.subtitle || '',
+ width,
+ background: col.background || '',
+ defaultDirection: direction,
+ right: col.right || false
+ };
+};
+
+// in the future, this will be an API call, but for now, read the local data file
+// load every possible table column up front, so we don't need to deal with this when
+// switching tabs
+const columns = tableTypes
+ .concat(subTypes)
+ .concat(transactionTypes)
+ .reduce((cols, type) => {
+ const visibleColumns = defaultColumns(type.internal).map((data) => data.title);
+ const parsedColumns = defaultColumns(type.internal)
+ .reduce((parsedCols, data) => Object.assign({}, parsedCols, {
+ [data.title]: createColumn(data)
+ }), {});
+
+ return Object.assign(cols, {
+ [type.internal]: {
+ visibleOrder: visibleColumns,
+ data: parsedColumns
+ }
+ });
+ }, {});
const propTypes = {
- filters: PropTypes.object,
- setAppliedFilterCompletion: PropTypes.func,
- noApplied: PropTypes.bool,
- subAwardIdClicked: PropTypes.func,
- wrapperProps: PropTypes.object,
tabData: PropTypes.object,
+ spendingLevel: PropTypes.string,
hash: PropTypes.string,
- spendingLevel: PropTypes.string
+ sectionTitle: PropTypes.string,
+ dsmContent: PropTypes.element,
+ sectionName: PropTypes.string
};
-export const tableTypes = [
- {
- label: 'Contracts',
- internal: 'contracts'
- },
- {
- label: 'Contract IDVs',
- internal: 'idvs'
- },
- {
- label: 'Grants',
- internal: 'grants'
- },
- {
- label: 'Direct Payments',
- internal: 'direct_payments'
- },
- {
- label: 'Loans',
- internal: 'loans'
- },
- {
- label: 'Other',
- internal: 'other'
- }
-];
-
-export const subTypes = [
- {
- label: 'Sub-Contracts',
- internal: 'subcontracts'
- },
- {
- label: 'Sub-Grants',
- internal: 'subgrants'
- }
-];
-
-const transactionTypes = [
- {
- label: 'Contracts',
- internal: 'transaction_contracts'
- },
- {
- label: 'Contract IDVs',
- internal: 'transaction_idvs'
- },
- {
- label: 'Grants',
- internal: 'transaction_grants'
- },
- {
- label: 'Direct Payments',
- internal: 'transaction_direct_payments'
- },
- {
- label: 'Loans',
- internal: 'transaction_loans'
- },
- {
- label: 'Other',
- internal: 'transaction_other'
- }
-];
-const ResultsTableContainer = (props) => {
- let tabCountRequest = null;
- let searchRequest = null;
+// eslint-disable-next-line prefer-arrow-callback
+const ResultsTableContainer = memo(function ResultsTableContainer({
+ tabData,
+ spendingLevel,
+ hash,
+ sectionTitle,
+ dsmContent,
+ sectionName
+}) {
const location = useLocation();
- const [searchParams, setSearchParams] = useState(new SearchAwardsOperation());
+ const dispatch = useDispatch();
+ const filters = useSelector((state) => state.appliedFilters.filters);
const [page, setPage] = useState(1);
- const [lastPage, setLastPage] = useState(true);
- const [counts, setCounts] = useState({});
- const [tableType, setTableType] = useState();
- const [columns, setColumns] = useState({});
+ const [tableType, setTableType] = useState(
+ spendingLevel === 'subawards' ? 'subcontracts' : 'contracts'
+ );
const [sort, setSort] = useState({
- field: 'Award Amount',
+ field: spendingLevel === 'subawards' ? 'Sub-Award Amount' : 'Award Amount',
direction: 'desc'
});
- const [inFlight, setInFlight] = useState(false);
- const [error, setError] = useState(false);
- const [results, setResults] = useState([]);
- const [total, setTotal] = useState(0);
const [resultLimit, setResultLimit] = useState(100);
- const [tableInstance, setTableInstance] = useState(`${uniqueId()}`);
const [isLoadingNextPage, setLoadNextPage] = useState(false);
- const [isInitialLoad, setIsInitialLoad] = useState(true);
- const [spendingLevel, setSpendingLevel] = useState(props.spendingLevel);
- const [isSubaward, setIsSubaward] = useState(props.spendingLevel === 'subawards');
- const [isTransactions, setIsTransactions] = useState(props.spendingLevel === 'transactions');
- const [expandableData, setExpandableData] = useState([]);
- const { pathname } = useLocation();
- const isV2 = pathname === '/search';
- const showToggle = isV2 && (props.spendingLevel !== "awards");
const [isMobile, setIsMobile] = useState(false);
- const [columnType, setColumnType] = useState(props.spendingLevel);
-
- const performSearch = throttle((newSearch = false) => {
- if (searchRequest) {
- // a request is currently in-flight, cancel it
- searchRequest.cancel();
- }
-
- const tableTypeTemp = tableType;
-
- // get searchParams from state
- const searchParamsTemp = new SearchAwardsOperation();
- searchParamsTemp.fromState(props.filters);
-
- // if subawards is true, newAwardsOnly cannot be true, so we remove
- // dateType for this request; also has to be done for the tabCounts request
- if (isSubaward && searchParamsTemp.dateType) {
- delete searchParamsTemp.dateType;
- }
-
- // generate an array of award type codes representing the current table tab we're showing
- // and use a different mapping if we're showing a subaward table vs a prime award table
- let groupsFromTableType =
- isSubaward ? subawardTypeGroups[tableTypeTemp] : awardTypeGroups[tableTypeTemp];
-
- if (isTransactions) {
- groupsFromTableType = transactionTypeGroups[tableTypeTemp];
- }
-
- if (searchParams.awardType.length === 0) {
- searchParamsTemp.awardType = groupsFromTableType;
- }
- else {
- let intersectingTypes = intersection(groupsFromTableType,
- searchParams.awardType);
- if (!intersectingTypes || intersectingTypes.length === 0) {
- // the filtered types and the table type do not align
- // in this case, send an array of non-existent types because the endpoint requires
- // an award type parameter
- intersectingTypes = ['no intersection'];
- }
- searchParamsTemp.awardType = intersectingTypes;
- }
-
- // indicate the request is about to start
- setInFlight(true);
- setError(false);
-
- let pageNumber = page;
-
- if (newSearch) {
- // a new search (vs just getting more pages of an existing search) requires resetting
- // the page number
- pageNumber = 1;
- }
-
- const requestFields = [];
-
- // Request fields for visible columns only
- const columnVisibility = columns[tableTypeTemp]?.visibleOrder;
- if (!columnVisibility) {
- return null;
- }
-
- columnVisibility.forEach((field) => {
- if (!requestFields.includes(field) && field !== "Action Date") {
- // Prevent duplicates in the list of fields to request
- if (Object.keys(apiFieldByTableColumnName).includes(field)) {
- requestFields.push(apiFieldByTableColumnName[field]);
- }
- else {
- requestFields.push(field);
- }
- }
- else if (field === "Action Date" && props.spendingLevel !== 'transactions') {
- requestFields.push('Sub-Award Date');
- }
- });
-
- if (props.spendingLevel === 'transactions') {
- requestFields.push('awarding_agency_id');
- }
- else {
- requestFields.push('recipient_id', 'prime_award_recipient_id');
- }
-
- // parse the redux search order into the API-consumable format
- const searchOrder = sort;
- let sortDirection = searchOrder.direction;
-
- if (!sortDirection) {
- sortDirection = 'desc';
- }
-
- if (searchOrder?.field === 'Action Date' && props.spendingLevel !== 'transactions') {
- searchOrder.field = 'Sub-Award Date';
- }
-
- const loadExpandableData = (showToggle && spendingLevel === "awards" && !isMobile);
-
- let params = {
- filters: searchParamsTemp.toParams(),
- page: pageNumber,
- limit: resultLimit,
- sort: "award_id",
- order: sortDirection,
- auditTrail: 'Results Table - Spending by award search'
- };
-
- // Set the params needed for download API call
- if (!params.filters.award_type_codes) {
- return null;
- }
-
- if (loadExpandableData) {
- if (props.spendingLevel === 'transactions') {
- setColumnType('transactions');
- searchRequest = SearchHelper.performSpendingByTransactionsGrouped(params);
- }
- else {
- setColumnType('subawards');
- searchRequest = SearchHelper.performSpendingBySubawardGrouped(params);
- }
- }
- else {
- params = {
- ...params,
- fields: requestFields,
- spending_level: spendingLevel,
- sort: searchOrder.field,
- page
- };
-
- if (isTransactions) {
- params.fields = [
- "Award ID",
- "Mod",
- "Recipient Name",
- "Transaction Amount",
- "Action Date",
- "Transaction Description",
- "Action Type",
- "Award Type",
- "Recipient UEI",
- "Recipient Location",
- "Primary Place of Performance",
- "Awarding Agency",
- "awarding_agency_id",
- "recipient_id",
- "Awarding Sub Agency",
- "NAICS",
- "PSC",
- "Assistance Listing"
- ];
-
- searchRequest = performKeywordSearch(params);
- }
- else {
- searchRequest = SearchHelper.performSpendingByAwardSearch(params);
- }
- }
-
- return searchRequest.promise
- .then((res) => {
- const newState = {
- inFlight: false
- };
-
- const parsedResults = res.data.results.map((result) => ({
- ...result,
- generated_internal_id: encodeURIComponent(result.generated_internal_id)
- }));
-
- // don't clear records if we're appending (not the first page)
- newState.tableInstance = `${uniqueId()}`;
- newState.results = parsedResults;
-
- if (newSearch) {
- setTotal(newState.results.length);
- }
-
- // request is done
- searchRequest = null;
- newState.page = res.data.page_metadata.page;
- newState.lastPage = !res.data.page_metadata.hasNext;
- setInFlight(newState.inFlight);
- setTableInstance(newState.tableInstance);
- setPage(newState.page);
- setLastPage(newState.lastPage);
- if (loadExpandableData) {
- setExpandableData(newState.results);
- }
- else {
- setResults(newState.results);
- }
-
- props.setAppliedFilterCompletion(true);
- })
- .catch((err) => {
- if (!isCancel(err)) {
- setInFlight(false);
- setError(true);
- props.setAppliedFilterCompletion(true);
- console.log(err);
- }
- });
- }, 400);
-
- const createColumn = (col) => {
- // create an object that integrates with the expected column data structure used by
- // the table component
-
- // BODGE: Temporarily only allow descending columns
- const direction = 'desc';
- const width = col.customWidth || measureTableHeader(col.displayName || col.title);
-
- return {
- columnName: col.title,
- displayName: col.displayName || col.title,
- subtitle: col.subtitle || '',
- width,
- background: col.background || '',
- defaultDirection: direction,
- right: col.right || false
- };
- };
-
- const loadColumns = () => {
- // in the future, this will be an API call, but for now, read the local data file
- // load every possible table column up front, so we don't need to deal with this when
- // switching tabs
- const columnsTemp = tableTypes.concat(subTypes).concat(transactionTypes).reduce((cols, type) => {
- const visibleColumns = defaultColumns(type.internal).map((data) => data.title);
- const parsedColumns = defaultColumns(type.internal).reduce((parsedCols, data) => Object.assign({}, parsedCols, {
- [data.title]: createColumn(data)
- }), {});
-
- return Object.assign(cols, {
- [type.internal]: {
- visibleOrder: visibleColumns,
- data: parsedColumns
- }
- });
- }, {});
-
- setColumns(Object.assign(columns, columnsTemp));
- };
- const updateFilters = throttle(() => {
- // the searchParams state var is now only used in the
- // block using intersection in performSearch
- const newSearch = new SearchAwardsOperation();
- newSearch.fromState(props.filters);
- setSearchParams(newSearch);
+ const isSubaward = spendingLevel === "subawards";
+ const loadExpandableData = (isSubaward && spendingLevel === "awards" && !isMobile);
+ const counts = tabData.results;
+
+ const {
+ isLoading,
+ error,
+ results,
+ total,
+ tableInstance,
+ lastPage
+ } = useResultsTableSearch(
+ filters,
+ tableType,
+ spendingLevel,
+ resultLimit,
+ sort,
+ loadExpandableData,
+ page,
+ columns
+ );
- setPage(1);
- performSearch(true);
- }, 350);
+ const updateFilters = throttle(() => setPage(1), 350);
const switchTab = (tab) => {
const newState = {
@@ -426,8 +154,7 @@ const ResultsTableContainer = (props) => {
});
};
- const parseTabCounts = (data) => {
- let awardCounts = data.results;
+ const parseTabCounts = () => {
let firstAvailable = '';
let i = 0;
let availableTabs = tableTypes;
@@ -435,23 +162,12 @@ const ResultsTableContainer = (props) => {
if (isSubaward) {
availableTabs = subTypes;
}
- else if (isTransactions) {
- availableTabs = transactionTypes;
- awardCounts = {
- transaction_contracts: data.results.contracts,
- transaction_grants: data.results.grants,
- transaction_direct_payments: data.results.direct_payments,
- transaction_loans: data.results.loans,
- transaction_other: data.results.other,
- transaction_idvs: data.results.idvs
- };
- }
// Set the first available award type to the first non-zero entry in the
while (firstAvailable === '' && i < availableTabs.length) {
const tableTypeTemp = availableTabs[i].internal;
- if (awardCounts[tableTypeTemp] > 0) {
+ if (counts[tableTypeTemp] > 0) {
firstAvailable = tableTypeTemp;
}
@@ -464,56 +180,14 @@ const ResultsTableContainer = (props) => {
firstAvailable = availableTabs[0].internal;
}
- setCounts(Object.assign({}, counts, awardCounts));
switchTab(firstAvailable);
updateFilters();
};
- const pickDefaultTab = () => {
- // get the award counts for the current filter set
- if (tabCountRequest) {
- tabCountRequest.cancel();
- }
-
- if (props.tabData && props.spendingLevel === spendingLevel) {
- parseTabCounts(props.tabData);
- return;
- }
-
- setInFlight(true);
- setError(false);
-
- const searchParamsTemp = new SearchAwardsOperation();
- searchParamsTemp.fromState(props.filters);
-
- // if subawards is true, newAwardsOnly cannot be true, so we remove dateType for this request
- // also has to be done for the main request, in performSearch
- if (isSubaward && searchParamsTemp.dateType) {
- delete searchParamsTemp.dateType;
- }
-
- tabCountRequest = SearchHelper.performSpendingByAwardTabCountSearch({
- filters: searchParamsTemp.toParams(),
- spending_level: spendingLevel,
- auditTrail: 'Award Table - Tab Counts'
- });
-
- tabCountRequest.promise
- .then((res) => {
- parseTabCounts(res.data);
- })
- .catch((err) => {
- if (!isCancel(err)) {
- setInFlight(false);
- setError(true);
- console.log(err);
- }
- });
- };
const loadNextPage = () => {
// check if request is already in-flight
- if (inFlight) {
+ if (isLoading) {
// in-flight, ignore this request
return;
}
@@ -526,20 +200,7 @@ const ResultsTableContainer = (props) => {
}
};
- const updateSort = (field, direction) => {
- if (field === 'Action Date' && props.spendingLevel !== 'transactions') {
- setSort(Object.assign({
- field: 'Sub-Award Date',
- direction
- }));
- }
- else {
- setSort(Object.assign({
- field,
- direction
- }));
- }
- };
+ const updateSort = (field, direction) => setSort({ field, direction });
const awardIdClick = (id) => {
Analytics.event({
@@ -559,52 +220,17 @@ const ResultsTableContainer = (props) => {
label: id,
gtm: true
});
- props.subAwardIdClicked(true);
+ dispatch(subAwardIdClicked(true));
};
- let availableTypes = tableTypes;
-
- if (isSubaward) {
- availableTypes = subTypes;
- }
- else if (isTransactions) {
- availableTypes = transactionTypes;
- }
+ const availableTypes = isSubaward ? subTypes : tableTypes;
const tabsWithCounts = availableTypes.map((type) => ({
...type,
count: counts[type.internal],
- disabled: inFlight || counts[type.internal] === 0
+ disabled: counts[type.internal] === 0
}));
- const initialTableLoad = () => {
- loadColumns();
- if (SearchHelper.isSearchHashReady(location) && props?.tabData?.results?.length > 0) {
- parseTabCounts(props.tabData);
- }
- else if (SearchHelper.isSearchHashReady(location)) {
- pickDefaultTab();
- }
- else {
- pickDefaultTab();
- }
- };
-
- const toggleSpendingLevel = () => {
- if (spendingLevel === "awards") {
- // return back to original.
- setSpendingLevel(props.spendingLevel);
- setIsSubaward(props.spendingLevel === "subawards");
- setIsTransactions(props.spendingLevel === "transactions");
- setExpandableData([]);
- return;
- }
-
- setSpendingLevel("awards");
- setIsSubaward(false);
- setIsTransactions(false);
- };
-
const formattedSubSort = () => {
const formattedSort = sort;
if (formattedSort?.field === 'Sub-Award Date') {
@@ -614,50 +240,19 @@ const ResultsTableContainer = (props) => {
return formattedSort;
};
- useEffect(throttle(() => {
- if (isInitialLoad) {
- setIsInitialLoad(false);
- initialTableLoad();
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, 400), []);
-
- useEffect(throttle(() => {
- if (!isInitialLoad && tableType) {
- performSearch(props?.spendingLevel === "subawards");
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, 400), [tableType, sort, resultLimit, page]);
-
- useEffect(throttle(() => {
- if (!isInitialLoad) {
- if (isSubaward && !props.noApplied) {
- // subaward toggle changed, update the search object
- pickDefaultTab();
- }
- else if (SearchHelper.isSearchHashReady(location) && location.search) {
- // hash is (a) defined and (b) new
- pickDefaultTab();
- }
- else if (!isSubaward || !isTransactions) {
- pickDefaultTab();
- }
- }
+ const onToggleSpendingLevel = () => {
+ // TODO: fix this function for expandable tables
+ // IF switching to expandable table AND spendingLevel === "subawards"
+ // THEN dispatch(setSpendingLevel("awards"))
+ };
- return () => {
- if (searchRequest) {
- searchRequest.cancel();
- }
- if (tabCountRequest) {
- tabCountRequest.cancel();
- }
- };
+ useEffect(() => {
+ parseTabCounts(tabData);
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, 400), [isSubaward, props.noApplied, isTransactions]);
+ }, [tabData]);
useEffect(throttle(() => {
if (isLoadingNextPage) {
- performSearch();
setLoadNextPage(false);
}
}, 400), [isLoadingNextPage]);
@@ -669,23 +264,25 @@ const ResultsTableContainer = (props) => {
return (
{
resultsLimit={resultLimit}
setResultLimit={setResultLimit}
resultsCount={counts[tableType]}
- showToggle={showToggle}
- expandableData={expandableData}
- filters={props.filters}
+ showToggle
+ expandableData={loadExpandableData ? results : []}
+ filters={filters}
checkMobile={(isMobileState) => setIsMobile(isMobileState)}
- columnType={columnType}
+ columnType={spendingLevel}
subColumnOptions={columns} />
);
-};
+});
ResultsTableContainer.propTypes = propTypes;
-
-export default connect(
- (state) => ({
- filters: state.appliedFilters.filters,
- noApplied: state.appliedFilters._empty,
- spendingLevel: state.searchView.spendingLevel
- }),
- (dispatch) => bindActionCreators(
- // access multiple redux actions
- Object.assign(
- {},
- searchActions,
- appliedFilterActions,
- { subAwardIdClicked }
- ),
- dispatch
- )
-)(ResultsTableContainer);
+export default ResultsTableContainer;
diff --git a/src/js/containers/search/resultsView/useResultsCount.jsx b/src/js/containers/search/resultsView/useResultsCount.jsx
new file mode 100644
index 0000000000..355fbafd9d
--- /dev/null
+++ b/src/js/containers/search/resultsView/useResultsCount.jsx
@@ -0,0 +1,34 @@
+// eslint-disable-next-line no-unused-vars
+import React from "react";
+import { useQuery } from "@tanstack/react-query";
+import { areFiltersEqual, performSpendingByAwardTabCountSearch } from "helpers/searchHelper";
+import SearchAwardsOperation from 'models/v1/search/SearchAwardsOperation';
+
+const useResultsCount = (filters, spendingLevel, hash) => {
+ const filtersParamsTemp = new SearchAwardsOperation();
+
+ filtersParamsTemp.fromState(filters);
+
+ // if subawards is true, newAwardsOnly cannot be true, so we remove dateType
+ if (spendingLevel === 'subawards') {
+ delete filtersParamsTemp.dateType;
+ }
+
+ const filtersParams = filtersParamsTemp.toParams();
+
+ const { data, error } = useQuery({
+ queryKey: ['performSpendingByAwardTabCountSearch', filtersParams.toString(), spendingLevel],
+ queryFn: () => performSpendingByAwardTabCountSearch({
+ filters: filtersParams,
+ spending_level: spendingLevel,
+ auditTrail: 'Results View - Tab Counts'
+ }).promise,
+ staleTime: 60000,
+ refetchOnWindowFocus: false,
+ enabled: !areFiltersEqual(filters) || !hash
+ });
+
+ return { data, error };
+};
+
+export default useResultsCount;
diff --git a/src/js/containers/search/resultsView/useResultsTableSearch.jsx b/src/js/containers/search/resultsView/useResultsTableSearch.jsx
new file mode 100644
index 0000000000..97215d0bf1
--- /dev/null
+++ b/src/js/containers/search/resultsView/useResultsTableSearch.jsx
@@ -0,0 +1,160 @@
+// eslint-disable-next-line no-unused-vars
+import React from "react";
+import { useQuery } from "@tanstack/react-query";
+import { intersection, uniqueId } from 'lodash-es';
+
+import {
+ performSpendingByAwardSearch, performSpendingBySubawardGrouped
+} from "helpers/searchHelper";
+import SearchAwardsOperation from 'models/v1/search/SearchAwardsOperation';
+import {
+ awardTypeGroups, subawardTypeGroups
+} from 'dataMapping/search/awardType';
+import { apiFieldByTableColumnName } from 'dataMapping/search/awardTableColumns';
+
+const getAwardTypeGroup = (spendingLevel, tableType, awardType) => {
+ // generate an array of award type codes representing the current table tab we're showing
+ // and use a different mapping if we're showing a subaward table vs a prime award table
+ const awardTypeGroup = spendingLevel === "subawards" ?
+ subawardTypeGroups[tableType] :
+ awardTypeGroups[tableType];
+
+ if (awardType.size === 0) {
+ return awardTypeGroup;
+ }
+
+ let intersectingTypes = intersection(awardTypeGroup, awardType);
+ if (!intersectingTypes || intersectingTypes.length === 0) {
+ // the filtered types and the table type do not align
+ // in this case, send an array of non-existent types because the endpoint requires
+ // an award type parameter
+ intersectingTypes = ['no intersection'];
+ }
+ return intersectingTypes;
+};
+
+const getFields = (tableType, columns) => {
+ const fields = [];
+
+ // Request fields for visible columns only
+ const columnVisibility = columns[tableType]?.visibleOrder;
+ if (!columnVisibility) {
+ return null;
+ }
+
+ columnVisibility.forEach((field) => {
+ if (!fields.includes(field) && field !== "Action Date") {
+ // Prevent duplicates in the list of fields to request
+ if (Object.keys(apiFieldByTableColumnName).includes(field)) {
+ fields.push(apiFieldByTableColumnName[field]);
+ }
+ else {
+ fields.push(field);
+ }
+ }
+ else if (field === "Action Date") {
+ fields.push('Sub-Award Date');
+ }
+ });
+
+ fields.push('recipient_id', 'prime_award_recipient_id');
+
+ return fields;
+};
+
+const getSortOrder = (searchOrder, grouped) => {
+ // parse the redux search order into the API-consumable format
+ let sort = searchOrder?.field;
+ let order = searchOrder?.direction;
+
+ if (!order) order = 'desc';
+
+ if (sort === 'Action Date') sort = 'Sub-Award Date';
+
+ if (grouped) sort = 'award_id';
+
+ return { sort, order };
+};
+
+const useResultsTableSearch = (
+ searchFilters, tableType, spendingLevel, limit, searchOrder, grouped, page, columns
+) => {
+ const fields = getFields(tableType, columns);
+
+ const { sort, order } = getSortOrder(searchOrder, grouped);
+
+ const filtersTemp = new SearchAwardsOperation();
+
+ // get initial searchParams from state
+ filtersTemp.fromState(searchFilters);
+
+ // if subawards is true, newAwardsOnly cannot be true, so we remove
+ // dateType for this request; also has to be done for the tabCounts request
+ if (spendingLevel === "subaward" && filtersTemp.dateType) {
+ delete filtersTemp.dateType;
+ }
+
+ filtersTemp.awardType = getAwardTypeGroup(
+ spendingLevel, tableType, searchFilters.awardType
+ );
+
+ const filters = filtersTemp.toParams();
+
+ const params = {
+ auditTrail: 'Results Table - Spending by award search',
+ filters,
+ limit,
+ order,
+ page,
+ sort
+ };
+
+ const {
+ isPending, error, data
+ } = useQuery({
+ queryKey: [
+ grouped ? 'performSpendingBySubawardGrouped' : 'performSpendingByAwardSearch',
+ limit,
+ page,
+ sort,
+ order,
+ spendingLevel,
+ grouped,
+ tableType,
+ searchFilters
+ ],
+ queryFn: () => {
+ if (grouped) {
+ return performSpendingBySubawardGrouped(params).promise;
+ }
+
+ return performSpendingByAwardSearch({
+ ...params,
+ fields,
+ spending_level: spendingLevel
+ }).promise;
+ },
+ staleTime: 60000,
+ refetchOnWindowFocus: false,
+ enabled: !!filtersTemp.awardType
+ });
+
+ const results = data?.data ?
+ data?.data.results.map((result) => ({
+ ...result,
+ generated_internal_id: encodeURIComponent(result.generated_internal_id)
+ })) :
+ [];
+
+ return {
+ isLoading: isPending,
+ error,
+ results,
+ total: results?.length,
+ tableInstance: uniqueId(),
+ page: data?.data?.page_metadata.page,
+ lastPage: !data?.data?.page_metadata.hasNext
+ };
+};
+
+export default useResultsTableSearch;
diff --git a/src/js/dataMapping/search/resultsView/table.js b/src/js/dataMapping/search/resultsView/table.js
new file mode 100644
index 0000000000..faede6cb57
--- /dev/null
+++ b/src/js/dataMapping/search/resultsView/table.js
@@ -0,0 +1,64 @@
+export const tableTypes = [
+ {
+ label: 'Contracts',
+ internal: 'contracts'
+ },
+ {
+ label: 'Contract IDVs',
+ internal: 'idvs'
+ },
+ {
+ label: 'Grants',
+ internal: 'grants'
+ },
+ {
+ label: 'Direct Payments',
+ internal: 'direct_payments'
+ },
+ {
+ label: 'Loans',
+ internal: 'loans'
+ },
+ {
+ label: 'Other',
+ internal: 'other'
+ }
+];
+
+export const subTypes = [
+ {
+ label: 'Sub-Contracts',
+ internal: 'subcontracts'
+ },
+ {
+ label: 'Sub-Grants',
+ internal: 'subgrants'
+ }
+];
+
+export const transactionTypes = [
+ {
+ label: 'Contracts',
+ internal: 'transaction_contracts'
+ },
+ {
+ label: 'Contract IDVs',
+ internal: 'transaction_idvs'
+ },
+ {
+ label: 'Grants',
+ internal: 'transaction_grants'
+ },
+ {
+ label: 'Direct Payments',
+ internal: 'transaction_direct_payments'
+ },
+ {
+ label: 'Loans',
+ internal: 'transaction_loans'
+ },
+ {
+ label: 'Other',
+ internal: 'transaction_other'
+ }
+];