diff --git a/src/components/Export/components/DownloadCurlCommand/downloadCurlCommand.tsx b/src/components/Export/components/DownloadCurlCommand/downloadCurlCommand.tsx index e8ec6d5d..a5289998 100644 --- a/src/components/Export/components/DownloadCurlCommand/downloadCurlCommand.tsx +++ b/src/components/Export/components/DownloadCurlCommand/downloadCurlCommand.tsx @@ -4,6 +4,7 @@ import { Filters } from "../../../../common/entities"; import { useExploreState } from "../../../../hooks/useExploreState"; import { FileManifestType } from "../../../../hooks/useFileManifest/common/entities"; import { useFileManifest } from "../../../../hooks/useFileManifest/useFileManifest"; +import { useFileManifestFileCount } from "../../../../hooks/useFileManifest/useFileManifestFileCount"; import { FileLocation, useRequestFileLocation, @@ -30,6 +31,7 @@ interface DownloadCurlCommandProps { filters: Filters; // Initializes bulk download filters. formFacet: FormFacet; manifestDownloadFormat?: ManifestDownloadFormat; + speciesFacetName: string; } export const DownloadCurlCommand = ({ @@ -41,8 +43,10 @@ export const DownloadCurlCommand = ({ filters, formFacet, manifestDownloadFormat = MANIFEST_DOWNLOAD_FORMAT.CURL, + speciesFacetName, }: DownloadCurlCommandProps): JSX.Element => { useFileManifest(filters, fileSummaryFacetName); + useFileManifestFileCount(filters, speciesFacetName, fileSummaryFacetName); const [executionEnvironment, setExecutionEnvironment] = useState(BULK_DOWNLOAD_EXECUTION_ENVIRONMENT.BASH); const { diff --git a/src/components/Export/components/ExportToTerra/exportToTerra.tsx b/src/components/Export/components/ExportToTerra/exportToTerra.tsx index 96b670fb..bec99a85 100644 --- a/src/components/Export/components/ExportToTerra/exportToTerra.tsx +++ b/src/components/Export/components/ExportToTerra/exportToTerra.tsx @@ -4,6 +4,7 @@ import { useExploreState } from "../../../../hooks/useExploreState"; import { useExportToTerraResponseURL } from "../../../../hooks/useExportToTerraResponseURL"; import { FileManifestType } from "../../../../hooks/useFileManifest/common/entities"; import { useFileManifest } from "../../../../hooks/useFileManifest/useFileManifest"; +import { useFileManifestFileCount } from "../../../../hooks/useFileManifest/useFileManifestFileCount"; import { useFileManifestFormat } from "../../../../hooks/useFileManifest/useFileManifestFormat"; import { useRequestFileLocation } from "../../../../hooks/useRequestFileLocation"; import { useRequestManifest } from "../../../../hooks/useRequestManifest/useRequestManifest"; @@ -24,6 +25,7 @@ export interface ExportToTerraProps { formFacet: FormFacet; manifestDownloadFormat?: ManifestDownloadFormat; manifestDownloadFormats: ManifestDownloadFormat[]; + speciesFacetName: string; } export const ExportToTerra = ({ @@ -36,11 +38,13 @@ export const ExportToTerra = ({ formFacet, manifestDownloadFormat, manifestDownloadFormats, + speciesFacetName, }: ExportToTerraProps): JSX.Element => { const { exploreState: { tabValue: entityList }, } = useExploreState(); useFileManifest(filters, fileSummaryFacetName); + useFileManifestFileCount(filters, speciesFacetName, fileSummaryFacetName); const fileManifestFormatState = useFileManifestFormat(manifestDownloadFormat); const { requestMethod, requestParams, requestUrl } = useRequestManifest( fileManifestFormatState.fileManifestFormat, diff --git a/src/components/Export/components/ManifestDownload/manifestDownload.tsx b/src/components/Export/components/ManifestDownload/manifestDownload.tsx index 08550ef1..de218f28 100644 --- a/src/components/Export/components/ManifestDownload/manifestDownload.tsx +++ b/src/components/Export/components/ManifestDownload/manifestDownload.tsx @@ -4,6 +4,7 @@ import { Filters } from "../../../../common/entities"; import { useExploreState } from "../../../../hooks/useExploreState"; import { FileManifestType } from "../../../../hooks/useFileManifest/common/entities"; import { useFileManifest } from "../../../../hooks/useFileManifest/useFileManifest"; +import { useFileManifestFileCount } from "../../../../hooks/useFileManifest/useFileManifestFileCount"; import { FileLocation, useRequestFileLocation, @@ -25,6 +26,7 @@ export interface ManifestDownloadProps { manifestDownloadFormat?: ManifestDownloadFormat; ManifestDownloadStart: ElementType; ManifestDownloadSuccess: ElementType; + speciesFacetName: string; } export const ManifestDownload = ({ @@ -36,8 +38,10 @@ export const ManifestDownload = ({ manifestDownloadFormat = MANIFEST_DOWNLOAD_FORMAT.COMPACT, ManifestDownloadStart, ManifestDownloadSuccess, + speciesFacetName, }: ManifestDownloadProps): JSX.Element => { useFileManifest(filters, fileSummaryFacetName); + useFileManifestFileCount(filters, speciesFacetName, fileSummaryFacetName); const { exploreState: { tabValue: entityList }, } = useExploreState(); diff --git a/src/hooks/useFileManifest/useFileManifestFileCount.ts b/src/hooks/useFileManifest/useFileManifestFileCount.ts new file mode 100644 index 00000000..d1a7b345 --- /dev/null +++ b/src/hooks/useFileManifest/useFileManifestFileCount.ts @@ -0,0 +1,65 @@ +import { useEffect, useState } from "react"; +import { Filters } from "../../common/entities"; +import { FileManifestActionKind } from "../../providers/fileManifestState"; +import { useCatalog } from "../useCatalog"; +import { useFileManifestState } from "../useFileManifestState"; +import { useFetchSummary } from "./useFetchSummary"; + +/** + * Fetches the latest file count from the summary endpoint, excluding filters + * that match the provided species and file facet names, and updates the file manifest state. + * + * @param initialFilters - The initial set of filters to apply. + * @param speciesFacetName - The facet name representing species to exclude from the summary request. + * @param fileFacetName - The facet name representing file type to exclude from the summary request. + */ +export const useFileManifestFileCount = ( + initialFilters: Filters | undefined, + speciesFacetName: string, + fileFacetName: string +): void => { + // Initial file manifest filter. + const [initFilters] = useState(() => initialFilters); + + // File manifest dispatch. + const { fileManifestDispatch } = useFileManifestState(); + + // Determine catalog. + const catalog = useCatalog() as string; // catalog should be defined. + + // Get filters required for fetching the summary. + const filters = excludeFacetsFromFilters(initFilters, [ + speciesFacetName, + fileFacetName, + ]); + + // Fetch file count from summary. + const { summary: { fileCount } = {} } = useFetchSummary( + filters, + catalog, + true + ); + + useEffect(() => { + fileManifestDispatch({ + payload: { fileCount }, + type: FileManifestActionKind.UpdateFileCount, + }); + }, [fileCount, fileManifestDispatch]); +}; + +/** + * Returns a new filters array with the specified facet names excluded. + * + * @param filters - The list of filters to process. + * @param facetNames - The facet names to exclude from the filters. + * @returns The filters array excluding any filters matching the provided facet names. + */ +function excludeFacetsFromFilters( + filters: Filters | undefined, + facetNames: string[] +): Filters { + return (filters || []).filter( + ({ categoryKey }) => !facetNames.includes(categoryKey) + ); +} diff --git a/src/hooks/useRequestManifest/utils.ts b/src/hooks/useRequestManifest/utils.ts index ee9af3e5..a07fbc59 100644 --- a/src/hooks/useRequestManifest/utils.ts +++ b/src/hooks/useRequestManifest/utils.ts @@ -12,36 +12,41 @@ import { REQUEST_MANIFEST } from "./constants"; import { UseRequestManifest } from "./types"; /** - * Returns true if all form facets have all their terms selected. - * @param formFacet - Form related file facets. - * @returns true if all form facets have all their terms selected. + * Determines whether all files are selected by comparing the file count to the summary file count. + * + * @param state - The file manifest state object. + * @returns True if all files are selected; otherwise, false. */ -export function areAllFormFilterTermsSelected(formFacet: FormFacet): boolean { - return Object.values(formFacet) - .filter(Boolean) - .every( - ({ selectedTermCount, termCount }: FileFacet) => - selectedTermCount === termCount - ); +export function areAllFilesSelected(state: FileManifestState): boolean { + const { fileCount, summary } = state; + + // Return false if file count or summary file count is undefined. + if (fileCount === undefined || summary?.fileCount === undefined) return false; + + // Return true if file count equals summary file count. + return fileCount === summary.fileCount; } /** - * Generates the filters for a request URL based on the file manifest state. - * - **all form facets have all their terms selected** - returns filters from state without form filters. - * - **at least one form facet has an unselected term** - returns filters from state. - * @param state - File manifest state. - * @param formFacet - Form related file facets. - * @returns filters for the request URL. + * Builds the filters object for a request URL based on the file manifest state and form facets. + * + * - If all files are selected, returns filters from state excluding fully selected form filters. + * - If only some files are selected, returns the current filters from state. + * + * @param state - The file manifest state object. + * @param formFacet - The form-related file facets. + * @returns The filters to use for the request URL. */ export function buildRequestFilters( state: FileManifestState, formFacet: FormFacet ): Filters { - // Form terms are fully selected; return filters excluding form filters. - if (areAllFormFilterTermsSelected(formFacet)) { + // Return filters from state excluding form filters if all files are selected. + if (areAllFilesSelected(state)) { return excludeFullySelectedFormFilters(state, formFacet); } - // Form terms are partially selected; return filters. + + // Return current filters from state. return state.filters; } @@ -115,16 +120,24 @@ export function isCatalogReady(catalog: string | undefined): boolean { } /** - * Returns true if the file manifest state is ready for a request. - * A file manifest state is considered ready if it is both enabled (`isEnabled` is `true`) - * and not currently loading (`isLoading` is `false`). + * Determines if the file manifest state is ready to be used in a request. + * + * The state is considered ready when: + * - isEnabled is true + * - fileCount is defined + * - isLoading is false + * * @param fileManifestState - File manifest state. - * @returns true if the file manifest state is ready for a request. + * @returns true if the file manifest state is ready. */ export function isFileManifestStateReady( fileManifestState: FileManifestState ): boolean { - return fileManifestState.isEnabled && !fileManifestState.isLoading; + return ( + fileManifestState.isEnabled && + fileManifestState.fileCount !== undefined && + !fileManifestState.isLoading + ); } /** diff --git a/src/mocks/useRequestFileManifest.mocks.ts b/src/mocks/useRequestFileManifest.mocks.ts index 40d796ce..340b1041 100644 --- a/src/mocks/useRequestFileManifest.mocks.ts +++ b/src/mocks/useRequestFileManifest.mocks.ts @@ -79,9 +79,11 @@ export const FILTERS: Record = { }; export const FILE_MANIFEST_STATE = { + fileCount: 10, filters: FILTERS.FORM_INITIAL_SET, isEnabled: true, isLoading: false, + summary: { fileCount: 10 }, } as FileManifestState; export const FORM_FACET: Record = { diff --git a/src/providers/fileManifestState.tsx b/src/providers/fileManifestState.tsx index 6fb6fbc8..b255b9f5 100644 --- a/src/providers/fileManifestState.tsx +++ b/src/providers/fileManifestState.tsx @@ -24,6 +24,7 @@ import { FILE_MANIFEST_STATE } from "./fileManifestState/constants"; * File manifest state. */ export type FileManifestState = { + fileCount: number | undefined; filesFacets: FileFacet[]; fileSummary?: AzulSummaryResponse; fileSummaryFacetName?: string; @@ -133,6 +134,7 @@ export function FileManifestStateProvider({ export enum FileManifestActionKind { ClearFileManifest = "CLEAR_FILE_MANIFEST", FetchFileManifest = "FETCH_FILE_MANIFEST", + UpdateFileCount = "UPDATE_FILE_COUNT", UpdateFileManifest = "UPDATE_FILE_MANIFEST", UpdateFilter = "UPDATE_FILTER", UpdateFiltersCategory = "UPDATE_FILTERS_CATEGORY", @@ -144,6 +146,7 @@ export enum FileManifestActionKind { export type FileManifestAction = | ClearFileManifestAction | FetchFileManifestAction + | UpdateFileCountAction | UpdateFileManifestAction | UpdateFilterAction | UpdateFiltersCategoryAction; @@ -172,6 +175,14 @@ type UpdateFileManifestAction = { type: FileManifestActionKind.UpdateFileManifest; }; +/** + * Update file count action. + */ +type UpdateFileCountAction = { + payload: UpdateFileCountPayload; + type: FileManifestActionKind.UpdateFileCount; +}; + /** * Update filter action. */ @@ -196,6 +207,13 @@ type FetchFileManifestPayload = { filters: Filters; }; +/** + * Update file count payload. + */ +export type UpdateFileCountPayload = { + fileCount: number | undefined; +}; + /** * Update file manifest payload. */ @@ -235,6 +253,7 @@ function fileManifestReducer( case FileManifestActionKind.ClearFileManifest: { return { ...state, + fileCount: undefined, isEnabled: false, }; } @@ -252,6 +271,10 @@ function fileManifestReducer( isEnabled: true, }; } + // Updates file count. + case FileManifestActionKind.UpdateFileCount: { + return { ...state, ...payload }; + } // Updates file manifest. case FileManifestActionKind.UpdateFileManifest: { return { ...state, ...payload }; diff --git a/src/providers/fileManifestState/constants.ts b/src/providers/fileManifestState/constants.ts index d23c2afb..f1ef724a 100644 --- a/src/providers/fileManifestState/constants.ts +++ b/src/providers/fileManifestState/constants.ts @@ -1,6 +1,7 @@ import { FileManifestState } from "../fileManifestState"; export const FILE_MANIFEST_STATE: FileManifestState = { + fileCount: undefined, fileSummary: undefined, fileSummaryFacetName: undefined, fileSummaryFilters: [], diff --git a/tests/buildRequestFilters.test.ts b/tests/buildRequestFilters.test.ts index 04c6dabe..2d4f0b69 100644 --- a/tests/buildRequestFilters.test.ts +++ b/tests/buildRequestFilters.test.ts @@ -24,26 +24,18 @@ describe("buildRequestFilters", () => { expect(result).toEqual(fileManifestState.filters); }); - test("when at least one form facet has no terms selected", () => { - const fileManifestState = getFileManifestState( - FILTERS.FORM_INCOMPLETE_SET - ); + test("when summary file count is not equal to initial file count", () => { + const fileManifestState = getFileManifestState(FILTERS.FORM_COMPLETE_SET); const result = buildRequestFilters( - fileManifestState, - FORM_FACET.INCOMPLETE_SET + { ...fileManifestState, summary: { fileCount: 9 } }, + FORM_FACET.COMPLETE_SET ); expect(result).toEqual(fileManifestState.filters); }); - - test("when at least one form facet has an unselected term", () => { - const fileManifestState = getFileManifestState(FILTERS.FORM_SUBSET); - const result = buildRequestFilters(fileManifestState, FORM_FACET.SUBSET); - expect(result).toEqual(fileManifestState.filters); - }); }); describe("should return filters excluding form related filters", () => { - test("when all form facets have all their terms selected", () => { + test("when summary file count is equal to initial file count", () => { const fileManifestState = getFileManifestState(FILTERS.FORM_COMPLETE_SET); const result = buildRequestFilters( fileManifestState, diff --git a/tests/useRequestManifest.test.ts b/tests/useRequestManifest.test.ts index ff6897d1..dfa86fc9 100644 --- a/tests/useRequestManifest.test.ts +++ b/tests/useRequestManifest.test.ts @@ -109,6 +109,16 @@ describe("useRequestManifest", () => { }); }); + test("when fileManifestState fileCount is undefined", () => { + MOCK_USE_FILE_MANIFEST_STATE.mockReturnValue({ + fileManifestDispatch: jest.fn(), + fileManifestState: { ...FILE_MANIFEST_STATE, fileCount: undefined }, + }); + testRequestManifest({ + fileManifestFormat: MANIFEST_DOWNLOAD_FORMAT.VERBATIM_PFB, + }); + }); + describe("form selection is not ready", () => { test("when a form facet is undefined", () => { testRequestManifest({