diff --git a/.env.example b/.env.example index 1b223454..ad992691 100644 --- a/.env.example +++ b/.env.example @@ -58,4 +58,7 @@ VITE_AUTH_PROVIDER_MODULE_GOOGLE_ALT=false # VITE_VARIANT_EXPLORER_EXCLUDE_COLUMNS=["AC","AN"] VITE_GOOGLE_ANALYTICS_ID=someid -VITE_GOOGLE_TAG_MANAGER_ID=someid \ No newline at end of file +VITE_GOOGLE_TAG_MANAGER_ID=someid + +VITE_AUTH_TOUR_NAME=NHANES-Auth +VITE_OPEN_TOUR_NAME=BDC-Open \ No newline at end of file diff --git a/.env.test b/.env.test index 92147064..f21e6526 100644 --- a/.env.test +++ b/.env.test @@ -66,4 +66,7 @@ VITE_VARIANT_EXPLORER_MAX_COUNT=20 VITE_VARIANT_EXPLORER_EXCLUDE_COLUMNS='[]' VITE_GOOGLE_ANALYTICS_ID=someid -VITE_GOOGLE_TAG_MANAGER_ID=someid \ No newline at end of file +VITE_GOOGLE_TAG_MANAGER_ID=someid + +VITE_AUTH_TOUR_NAME=NHANES-Auth +VITE_OPEN_TOUR_NAME=BDC-Open \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e6d75a92..f3bfda1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "@sveltejs/vite-plugin-svelte": "^3.1.0", "@tailwindcss/forms": "0.5.7", "@tailwindcss/typography": "0.5.15", - "@types/node": "20.12.11", + "@types/node": "22.9.0", "@types/plotly.js": "^2.33.3", "@types/plotly.js-dist-min": "^2.3.4", "@types/uuid": "^10.0.0", @@ -1262,12 +1262,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.12.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz", - "integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==", + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", + "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", "dev": true, "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.8" } }, "node_modules/@types/plotly.js": { @@ -5486,9 +5486,9 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true }, "node_modules/unfetch": { diff --git a/package.json b/package.json index 91555a18..5adac007 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@sveltejs/vite-plugin-svelte": "^3.1.0", "@tailwindcss/forms": "0.5.7", "@tailwindcss/typography": "0.5.15", - "@types/node": "20.12.11", + "@types/node": "22.9.0", "@types/plotly.js": "^2.33.3", "@types/plotly.js-dist-min": "^2.3.4", "@types/uuid": "^10.0.0", diff --git a/src/lib/assets/TourConfiguration.json b/src/lib/assets/TourConfiguration.json new file mode 100644 index 00000000..9fb70926 --- /dev/null +++ b/src/lib/assets/TourConfiguration.json @@ -0,0 +1,253 @@ +{ + "NHANES-Auth": { + "searchTerm": "Age", + "title": "Welcome to PIC-SURE", + "description": "PIC-SURE allows you to explore variables, apply filters, and prepare data for analysis. When applying filters, participants are selected that meet those criteria.

This tour demonstrates how to search, apply filters, and interpret results using PIC-SURE.

Once the tour starts, you can click anywhere to go to the next step. You can press the escape key to stop the tour at any point. You may also use the arrow keys, enter key, or the spacebar to navigate the tour.", + "steps": [ + { + "element": "#explorer-search-box", + "popover": { + "title": "Search", + "description": "Search clinical variables of interest. Here, the term \"{{searchTerm}}\" was used to get all demographics-related results. Click anywhere to continue the tour.", + "onPrevClick": "disablePrevious" + } + }, + { + "element": "#facet-side-bar", + "popover": { + "title": "Refine Search", + "description": "Use the options displayed here to further refine your search results using facets.", + "onPrevClick": "disablePrevious" + } + }, + { + "element": "#ExplorerTable-table", + "popover": { + "title": "Search Results", + "description": "The search results are displayed in this area. Each row describes a different variable that matches your search term and/or facets.", + "onPrevClick": "disablePrevious" + } + }, + { + "element": "#ExplorerTable-table #row-0", + "onHighlightStarted": "clickElement", + "popover": { + "title": "Variable Details", + "description": "To learn more about a variable, click on the row or click the information icon.", + "onPrevClick": "disablePrevious" + } + }, + { + "element": "#row-0 button[title=\"Filter\"]", + "onHighlightStarted": "clickElement", + "popover": { + "title": "Apply Filters", + "description": "Click the filter icon to apply inclusion criteria to build your cohort.", + "onPrevClick": "disablePrevious" + } + }, + { + "element": "#row-0 button[title=\"Add for Analysis\"]", + "popover": { + "title": "Export Variable", + "description": "Click the export icon to add a variable to your export without applying a filter.", + "onPrevClick": "disablePrevious" + } + }, + { + "element": "#sidebar-right", + "popover": { + "title": "Cohort Summary", + "description": "You can view the summary of the filtered cohort based on the applied variable filters. Additionally, you can also view variable distributions and prepare your cohort for analysis.", + "onPrevClick": "disablePrevious" + } + }, + { + "element": "#nav-link-help", + "onDeselected": "removeHighlightClass", + "popover": { + "title": "Help Resources", + "description": "To learn more about PIC-SURE, please consult the user guide and video demonstrations. You can also find these resources on the help page.", + "onPrevClick": "disablePrevious" + } + } + ] + }, + "BDC-Open": { + "searchTerm": "RECOVER", + "title": "Welcome to BioData Catalyst Powered by PIC-SURE", + "description": "BioData Catalyst Powered by PIC-SURE allows you to explore variables, apply filters, and prepare data for analysis. When applying filters, participants are selected that meet those criteria.

This tour demonstrates how to search, apply filters, and interpret results using PIC-SURE. This tour uses the Researching COVID to Enhance Recovery (RECOVER) dataset as an example. The RECOVER initiative is focused on understanding, diagnosing, treating, and preventing Long COVID.

Once the tour starts, you can click “Next” or use the arrow keys to navigate the tour. You can press any other key or click anywhere on the screen to escape the tour.", + "steps": [ + { + "element": "#explorer-search-box", + "popover": { + "title": "Search", + "description": "Search clinical variables of interest. Here, the term \"{{searchTerm}}\" was used to get all {{searchTerm}}-related results.", + "onPrevClick": "disablePrevious" + } + }, + { + "element": "#facet-side-bar", + "popover": { + "title": "Refine Search", + "description": "Use the options displayed here to further refine your search results using facets.", + "onPrevClick": "disablePrevious" + } + }, + { + "element": "#ExplorerTable-table", + "popover": { + "title": "Search Results", + "description": "The search results are displayed in this area. Each row describes a different variable that matches your search term and/or facets.", + "onPrevClick": "disablePrevious", + "onNextClick": "findAndSetFirstNonStigmatizedAvailableFilterThenNext" + } + }, + { + "element": "#ExplorerTable-table .non-stigmatized-row", + "onHighlightStarted": "clickElement", + "popover": { + "title": "Variable Details", + "description": "To learn more about a variable, click on the row or click the information icon.", + "onPrevClick": "disablePrevious" + } + }, + { + "element": "#ExplorerTable-table .expandable-row", + "popover": { + "title": "Variable Details", + "description": "This displays information about the variable, including dataset information and study information.", + "onPrevClick": "disablePrevious" + } + }, + { + "element": "#ExplorerTable-table .non-stigmatized-row button[title=\"Filter\"]", + "onHighlightStarted": "clickElement", + "popover": { + "title": "Apply Filters", + "description": "Click the filter icon to apply inclusion criteria to build your cohort.", + "onPrevClick": "disablePrevious" + } + }, + { + "element": "#ExplorerTable-table .expandable-row", + "popover": { + "title": "Apply Filters", + "description": "You can add filters by selecting different values or specifying a numeric range here, then clicking the “+” button.", + "onPrevClick": "disablePrevious", + "onNextClick": "applyFilterThenNext" + } + }, + { + "element": "#sidebar-right", + "popover": { + "title": "Cohort Summary", + "description": "You can view the summary of the filtered cohort based on the applied variable filters. Additionally, you can also view variable distributions and counts per study. Note that this count is obfuscated to protect participant-level data.", + "onPrevClick": "disablePrevious" + } + }, + { + "element": "#nav-link-help", + "onDeselected": "removeHighlightClass", + "popover": { + "title": "Help Resources", + "description": "To learn more about PIC-SURE, please consult the user guide and video demonstrations. You can also find these resources on the help page.", + "onPrevClick": "disablePrevious" + } + } + ] + }, + "BDC-Auth": { + "searchTerm": "cardiac surgery", + "title": "Welcome to BioData Catalyst Powered by PIC-SURE", + "description": "BioData Catalyst Powered by PIC-SURE allows you to explore variables, apply filters, and prepare data for analysis. When applying filters, participants are selected that meet those criteria.

This tour demonstrates how to search, apply filters, and interpret results using PIC-SURE.

Once the tour starts, you can click “Next” or use the arrow keys to navigate the tour. You can press any other key or click anywhere on the screen to escape the tour.", + "steps": [ + { + "element": "#explorer-search-box", + "popover": { + "title": "Search", + "description": "Search clinical variables of interest. Here, the term \"{{searchTerm}}\" was used to get all results related to {{searchTerm}}.", + "onPrevClick": "disablePrevious" + } + }, + { + "element": "#facet-side-bar", + "popover": { + "title": "Refine Search", + "description": "Use the options displayed here to further refine your search results using facets.", + "onPrevClick": "disablePrevious" + } + }, + { + "element": "#ExplorerTable-table", + "popover": { + "title": "Search Results", + "description": "The search results are displayed in this area. Each row describes a different variable that matches your search term and/or facets.", + "onPrevClick": "disablePrevious" + } + }, + { + "element": "#ExplorerTable-table #row-0", + "onHighlightStarted": "clickElement", + "popover": { + "title": "Variable Details", + "description": "To learn more about a variable, click on the row or click the information icon.", + "onPrevClick": "disablePrevious" + } + }, + { + "element": "#ExplorerTable-table #active-row-0", + "popover": { + "title": "Variable Details", + "description": "This displays information about the variable, including dataset information and study information.", + "onPrevClick": "disablePrevious", + "onNextClick": "clickElementThenNext" + } + }, + { + "element": "#row-0 button[title=\"Filter\"]", + "onHighlightStarted": "clickElement", + "popover": { + "title": "Apply Filters", + "description": "Click the filter icon to apply inclusion criteria to build your cohort.", + "onPrevClick": "disablePrevious" + } + }, + { + "element": "#active-row-0", + "popover": { + "title": "Apply Filters", + "description": "You can add filters by selecting different values or specifying a numeric range here, then clicking the “+” button.", + "onPrevClick": "disablePrevious", + "onNextClick": "applyFilterThenNext" + } + }, + { + "element": "#row-0 button[title=\"Add for Analysis\"]", + "popover": { + "title": "Export Variable", + "description": "Click the export icon to add a variable to your export without applying a filter.", + "onPrevClick": "disablePrevious", + "onNextClick": "clickElementThenNext" + } + }, + { + "element": "#sidebar-right", + "popover": { + "title": "Cohort Summary", + "description": "You can view the summary of the filtered cohort based on the applied variable filters. Additionally, you can also view variable distributions and prepare your cohort for analysis.", + "onPrevClick": "disablePrevious" + } + }, + { + "element": "#nav-link-help", + "onDeselected": "removeHighlightClass", + "popover": { + "title": "Help Resources", + "description": "To learn more about PIC-SURE, please consult the user guide and video demonstrations. You can also find these resources on the help page.", + "onPrevClick": "disablePrevious" + } + } + ] + } +} diff --git a/src/lib/assets/authTourConfiguration.json b/src/lib/assets/authTourConfiguration.json deleted file mode 100644 index 4f255d2d..00000000 --- a/src/lib/assets/authTourConfiguration.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "searchTerm": "cardiac surgery", - "title": "Welcome to BioData Catalyst Powered by PIC-SURE", - "description": "BioData Catalyst Powered by PIC-SURE allows you to explore variables, apply filters, and prepare data for analysis. When applying filters, participants are selected that meet those criteria.

This tour demonstrates how to search, apply filters, and interpret results using PIC-SURE.

Once the tour starts, you can click “Next” or use the arrow keys to navigate the tour. You can press any other key or click anywhere on the screen to escape the tour.", - "steps": [ - { - "element": "#explorer-search-box", - "popover": { - "title": "Search", - "description": "Search clinical variables of interest. Here, the term \"{{searchTerm}}\" was used to get all results related to {{searchTerm}}.", - "onPrevClick": "disablePrevious" - } - }, - { - "element": "#facet-side-bar", - "popover": { - "title": "Refine Search", - "description": "Use the options displayed here to further refine your search results using facets.", - "onPrevClick": "disablePrevious" - } - }, - { - "element": "#ExplorerTable-table", - "popover": { - "title": "Search Results", - "description": "The search results are displayed in this area. Each row describes a different variable that matches your search term and/or facets.", - "onPrevClick": "disablePrevious" - } - }, - { - "element": "#ExplorerTable-table #row-0", - "onHighlightStarted": "clickElement", - "popover": { - "title": "Variable Details", - "description": "To learn more about a variable, click on the row or click the information icon.", - "onPrevClick": "disablePrevious" - } - }, - { - "element": "#ExplorerTable-table #active-row-0", - "popover": { - "title": "Variable Details", - "description": "This displays information about the variable, including dataset information and study information.", - "onPrevClick": "disablePrevious", - "onNextClick": "clickElementThenNext" - } - }, - { - "element": "#row-0 button[title=\"Filter\"]", - "onHighlightStarted": "clickElement", - "popover": { - "title": "Apply Filters", - "description": "Click the filter icon to apply inclusion criteria to build your cohort.", - "onPrevClick": "disablePrevious" - } - }, - { - "element": "#active-row-0", - "popover": { - "title": "Apply Filters", - "description": "You can add filters by selecting different values or specifying a numeric range here, then clicking the “+” button.", - "onPrevClick": "disablePrevious", - "onNextClick": "applyFilterThenNext" - } - }, - { - "element": "#row-0 button[title=\"Add for Analysis\"]", - "popover": { - "title": "Export Variable", - "description": "Click the export icon to add a variable to your export without applying a filter.", - "onPrevClick": "disablePrevious", - "onNextClick": "clickElementThenNext" - } - }, - { - "element": "#sidebar-right", - "popover": { - "title": "Cohort Summary", - "description": "You can view the summary of the filtered cohort based on the applied variable filters. Additionally, you can also view variable distributions and prepare your cohort for analysis.", - "onPrevClick": "disablePrevious" - } - }, - { - "element": "#nav-link-help", - "onDeselected": "removeHighlightClass", - "popover": { - "title": "Help Resources", - "description": "To learn more about PIC-SURE, please consult the user guide and video demonstrations. You can also find these resources on the help page.", - "onPrevClick": "disablePrevious" - } - } - ] -} diff --git a/src/lib/assets/dash.svg b/src/lib/assets/dash.svg new file mode 100644 index 00000000..c571a11b --- /dev/null +++ b/src/lib/assets/dash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/lib/assets/openTourConfiguration.json b/src/lib/assets/openTourConfiguration.json deleted file mode 100644 index 96723faa..00000000 --- a/src/lib/assets/openTourConfiguration.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "searchTerm": "RECOVER", - "title": "Welcome to BioData Catalyst Powered by PIC-SURE", - "description": "BioData Catalyst Powered by PIC-SURE allows you to explore variables, apply filters, and prepare data for analysis. When applying filters, participants are selected that meet those criteria.

This tour demonstrates how to search, apply filters, and interpret results using PIC-SURE. This tour uses the Researching COVID to Enhance Recovery (RECOVER) dataset as an example. The RECOVER initiative is focused on understanding, diagnosing, treating, and preventing Long COVID.

Once the tour starts, you can click “Next” or use the arrow keys to navigate the tour. You can press any other key or click anywhere on the screen to escape the tour.", - "steps": [ - { - "element": "#explorer-search-box", - "popover": { - "title": "Search", - "description": "Search clinical variables of interest. Here, the term \"{{searchTerm}}\" was used to get all {{searchTerm}}-related results.", - "onPrevClick": "disablePrevious" - } - }, - { - "element": "#facet-side-bar", - "popover": { - "title": "Refine Search", - "description": "Use the options displayed here to further refine your search results using facets.", - "onPrevClick": "disablePrevious" - } - }, - { - "element": "#ExplorerTable-table", - "popover": { - "title": "Search Results", - "description": "The search results are displayed in this area. Each row describes a different variable that matches your search term and/or facets.", - "onPrevClick": "disablePrevious", - "onNextClick": "findAndSetFirstNonStigmatizedAvailableFilterThenNext" - } - }, - { - "element": "#ExplorerTable-table .non-stigmatized-row", - "onHighlightStarted": "clickElement", - "popover": { - "title": "Variable Details", - "description": "To learn more about a variable, click on the row or click the information icon.", - "onPrevClick": "disablePrevious" - } - }, - { - "element": "#ExplorerTable-table .expandable-row", - "popover": { - "title": "Variable Details", - "description": "This displays information about the variable, including dataset information and study information.", - "onPrevClick": "disablePrevious" - } - }, - { - "element": "#ExplorerTable-table .non-stigmatized-row button[title=\"Filter\"]", - "onHighlightStarted": "clickElement", - "popover": { - "title": "Apply Filters", - "description": "Click the filter icon to apply inclusion criteria to build your cohort.", - "onPrevClick": "disablePrevious" - } - }, - { - "element": "#ExplorerTable-table .expandable-row", - "popover": { - "title": "Apply Filters", - "description": "You can add filters by selecting different values or specifying a numeric range here, then clicking the “+” button.", - "onPrevClick": "disablePrevious", - "onNextClick": "applyFilterThenNext" - } - }, - { - "element": "#sidebar-right", - "popover": { - "title": "Cohort Summary", - "description": "You can view the summary of the filtered cohort based on the applied variable filters. Additionally, you can also view variable distributions and counts per study. Note that this count is obfuscated to protect participant-level data.", - "onPrevClick": "disablePrevious" - } - }, - { - "element": "#nav-link-help", - "onDeselected": "removeHighlightClass", - "popover": { - "title": "Help Resources", - "description": "To learn more about PIC-SURE, please consult the user guide and video demonstrations. You can also find these resources on the help page.", - "onPrevClick": "disablePrevious" - } - } - ] -} diff --git a/src/lib/components/explorer/FacetCategory.svelte b/src/lib/components/explorer/FacetCategory.svelte index 45998625..0d59dd73 100644 --- a/src/lib/components/explorer/FacetCategory.svelte +++ b/src/lib/components/explorer/FacetCategory.svelte @@ -2,18 +2,19 @@ import type { DictionaryFacetResult } from '$lib/models/api/DictionaryResponses'; import { AccordionItem } from '@skeletonlabs/skeleton'; import SearchStore from '$lib/stores/Search'; - import type { Facet } from '$lib/models/Search'; import { hiddenFacets } from '$lib/services/dictionary'; - let { updateFacet, selectedFacets } = SearchStore; + import FacetItem from './FacetItem.svelte'; + import type { Facet } from '$lib/models/Search'; + let { updateFacets, selectedFacets } = SearchStore; export let facetCategory: DictionaryFacetResult; export let facets = facetCategory.facets; export let numFacetsToShow: number = 5; - export let shouldShowSearchBar: boolean = facets.length > numFacetsToShow; + export let shouldShowSearchBar: boolean = facets?.length > numFacetsToShow; - const anyFacetNot0 = facets.some((facet) => facet.count > 0); + const anyFacetNot0 = facets?.some((facet) => facet.count > 0); let textFilterValue: string; - let moreThanTenFacets = facets.length > numFacetsToShow; + let moreThanTenFacets = facets?.length > numFacetsToShow; $: facetsToDisplay = (facets || textFilterValue || moreThanTenFacets || $selectedFacets || facetCategory) && @@ -23,37 +24,100 @@ (facet) => facet?.categoryRef?.name === facetCategory?.name, ); - $: isChecked = (facetToCheck: string) => { - return $selectedFacets.some((facet: Facet) => { - return facet.name === facetToCheck; + function isIndeterminate(facet: Facet): boolean { + const atLeastOneChildSelected = + facet.children?.some((child) => $selectedFacets.some((f) => f.name === child.name)) ?? false; + const isEveryChildSelected = facet.children?.length + ? facet.children.every((child) => $selectedFacets.some((f) => f.name === child.name)) + : false; + return atLeastOneChildSelected && !isEveryChildSelected; + } + + function isParentFullySelected(facetName: string): boolean { + const result = facets.some((parent) => { + if (!parent.children || parent?.children?.length === 0) return false; + return parent.children.every( + (child) => child.name === facetName || $selectedFacets.some((f) => f.name === child.name), + ); }); - }; + return result; + } function getFacetsToDisplay() { - const hiddenFacetsForCategory = $hiddenFacets[facetCategory.name]; + const hiddenFacetsForCategory = $hiddenFacets[facetCategory.name] || []; let facetsToDisplay = facets.filter((f) => !hiddenFacetsForCategory.includes(f.name)); - //Put selected facets at the top const selectedFacetsMap = new Map($selectedFacets.map((facet) => [facet.name, facet])); - facetsToDisplay = facetsToDisplay.filter((f) => !selectedFacetsMap.has(f.name)); + const indeterminateFacets = facetsToDisplay.filter(isIndeterminate); + const indeterminateMap = new Map(indeterminateFacets.map((facet) => [facet.name, facet])); + const isChildOfIndeterminate = (facetName: string) => { + return indeterminateFacets.some((parent) => + parent.children?.some((child) => child.name === facetName), + ); + }; + + // Remove facets that will be added to the top or are children of fully selected parents + facetsToDisplay = facetsToDisplay.filter( + (f) => + !selectedFacetsMap.has(f.name) && + !isChildOfIndeterminate(f.name) && + !indeterminateMap.has(f.name) && + !isParentFullySelected(f.name), + ); + + // Add selected facets at the top (excluding children of indeterminate parents and fully selected parents) const selectedFacetsForCategory = $selectedFacets.filter( - (facet) => facet.category === facetCategory.name, + (facet) => + facet.category === facetCategory.name && + !isChildOfIndeterminate(facet.name) && + !isParentFullySelected(facet.name), ); selectedFacetsForCategory.forEach((facet) => { facet.count = facets.find((f) => f.name === facet.name)?.count || 0; }); - facetsToDisplay.unshift(...selectedFacetsForCategory); + //Add parents with all children selected + const parentsWithAllChildrenSelected = facets.filter( + (f) => + f.children?.length && + f.children.every((child) => $selectedFacets.some((f) => f.name === child.name)), + ); + parentsWithAllChildrenSelected.forEach((facet) => { + facet.count = facets.find((f) => f.name === facet.name)?.count || 0; + }); + + // Add indeterminate facets at the top + const indeterminateFacetsForCategory = indeterminateFacets.filter( + (facet) => facet.category === facetCategory.name, + ); + indeterminateFacetsForCategory.forEach((facet) => { + facet.count = facets.find((f) => f.name === facet.name)?.count || 0; + }); + + facetsToDisplay.unshift( + ...selectedFacetsForCategory, + ...parentsWithAllChildrenSelected, + ...indeterminateFacetsForCategory, + ); + if (textFilterValue) { - //Filter Facets by searched text const lowerFilterValue = textFilterValue.toLowerCase(); - facetsToDisplay = facetsToDisplay.filter( - (facet) => + facetsToDisplay = facetsToDisplay.filter((facet) => { + const facetMatches = facet.display.toLowerCase().includes(lowerFilterValue) || facet.name.toLowerCase().includes(lowerFilterValue) || - facet.description?.toLowerCase().includes(lowerFilterValue), - ); + facet.description?.toLowerCase().includes(lowerFilterValue); + + const childrenMatch = facet.children?.some( + (child) => + child.display.toLowerCase().includes(lowerFilterValue) || + child.name.toLowerCase().includes(lowerFilterValue) || + child.description?.toLowerCase().includes(lowerFilterValue), + ); + + return facetMatches || childrenMatch; + }); } else if (moreThanTenFacets) { // Only show the first n facets facetsToDisplay = facetsToDisplay.slice(0, numFacetsToShow); @@ -77,24 +141,9 @@ /> {/if} {#each facetsToDisplay as facet} - + {/each} - {#if facets.length > numFacetsToShow && !textFilterValue} + {#if facets?.length > numFacetsToShow && !textFilterValue} diff --git a/src/lib/components/explorer/FacetItem.svelte b/src/lib/components/explorer/FacetItem.svelte new file mode 100644 index 00000000..49326206 --- /dev/null +++ b/src/lib/components/explorer/FacetItem.svelte @@ -0,0 +1,144 @@ + + + +{#if open && facetsToDisplay !== undefined && facetsToDisplay?.length > 0} +
+ {#each facetsToDisplay as child} + + {/each} +
+{/if} + + diff --git a/src/lib/components/tour/ExplorerTour.svelte b/src/lib/components/tour/ExplorerTour.svelte index 388e64df..8d7947c0 100644 --- a/src/lib/components/tour/ExplorerTour.svelte +++ b/src/lib/components/tour/ExplorerTour.svelte @@ -55,12 +55,12 @@ const clickFilterOption = (activeRowSelector?: string) => { const allOptions = document.querySelector( - `#${activeRowSelector} #select-all`, + `${activeRowSelector} #select-all`, ) as HTMLInputElement; allOptions?.click(); const addFilter = document.querySelector( - `#${activeRowSelector} [data-testid="add-filter"]`, + `${activeRowSelector} [data-testid="add-filter"]`, ) as HTMLElement; addFilter.click(); diff --git a/src/lib/configuration.ts b/src/lib/configuration.ts index e5021e2d..a01f4a83 100644 --- a/src/lib/configuration.ts +++ b/src/lib/configuration.ts @@ -119,6 +119,7 @@ export const features: Indexable = { variantExplorer: import.meta.env?.VITE_VARIANT_EXPLORER === 'true', distributionExplorer: import.meta.env?.VITE_DIST_EXPLORER === 'true', enableTour: import.meta.env?.EXPLORER_TOUR ? import.meta.env?.EXPLORE_TOUR === 'true' : true, // default to true unless set otherwise + authTour: import.meta.env?.VITE_OPEN_TOUR_NAME ?? 'NHANES-Auth', enableHierarchy: import.meta.env?.VITE_ENABLE_HIERARCHY === 'true', enableTerraExport: import.meta.env?.VITE_ENABLE_TERRA_EXPORT === 'true', }, @@ -133,6 +134,7 @@ export const features: Indexable = { discover: import.meta.env?.VITE_DISCOVER === 'true', discoverFeautures: { enableTour: import.meta.env?.EXPLORER_TOUR !== 'false', + openTour: import.meta.env?.VITE_OPEN_TOUR_NAME ?? 'BDC-Open', distributionExplorer: import.meta.env?.VITE_DIST_EXPLORER === 'true', }, dashboard: import.meta.env?.VITE_DASHBOARD === 'true', diff --git a/src/lib/models/Search.ts b/src/lib/models/Search.ts index e234fa6e..bb8c7793 100644 --- a/src/lib/models/Search.ts +++ b/src/lib/models/Search.ts @@ -8,6 +8,7 @@ export type Facet = Indexable & { children?: Facet[]; category: string; categoryRef?: ShallowFacetCategory; + parentRef?: ShallowFacetCategory; }; export type ShallowFacetCategory = Pick; diff --git a/src/lib/stores/Search.ts b/src/lib/stores/Search.ts index 9f19dda5..1819f49a 100644 --- a/src/lib/stores/Search.ts +++ b/src/lib/stores/Search.ts @@ -1,9 +1,6 @@ import { get, writable, type Writable } from 'svelte/store'; import { type Facet, type SearchResult } from '$lib/models/Search'; -import type { - DictionaryConceptResult, - DictionaryFacetResult, -} from '$lib/models/api/DictionaryResponses'; +import type { DictionaryConceptResult } from '$lib/models/api/DictionaryResponses'; import type { State } from '@vincjo/datatables/remote'; import { searchDictionary } from '$lib/services/dictionary'; @@ -38,30 +35,18 @@ async function search(searchTerm: string, facets: Facet[], state?: State): Promi } } -async function updateFacet(newFacet: Facet, facetCategory: DictionaryFacetResult | undefined) { - if (facetCategory) { - newFacet.categoryRef = { - display: facetCategory.display, - name: facetCategory.name, - description: facetCategory.description, - }; - } - try { - selectedFacets.update((facets) => { - const index = facets.findIndex((facet) => facet.name === newFacet.name); - if (index === -1) { - facets.push(newFacet); - } else { - facets.splice(index, 1); - } - //For reactivity and sorting - selectedFacets.set(get(selectedFacets).sort((a, b) => b.count - a.count)); - return facets; - }); - } catch (e) { - console.error(e); - return; - } +async function updateFacets(facetsToUpdate: Facet[]) { + const currentFacets = get(selectedFacets); + facetsToUpdate.forEach((facet) => { + const facetIndex = currentFacets.findIndex((f) => f.name === facet.name); + if (facetIndex !== -1) { + currentFacets.splice(facetIndex, 1); + } else { + currentFacets.push(facet); + } + }); + + selectedFacets.set(currentFacets.sort((a, b) => b.count - a.count)); } export function resetSearch() { @@ -75,6 +60,6 @@ export default { searchTerm, error, search, - updateFacet, + updateFacets, resetSearch, }; diff --git a/src/routes/(picsure)/(authorized)/explorer/+page.svelte b/src/routes/(picsure)/(authorized)/explorer/+page.svelte index eddfb95e..2c520aef 100644 --- a/src/routes/(picsure)/(authorized)/explorer/+page.svelte +++ b/src/routes/(picsure)/(authorized)/explorer/+page.svelte @@ -1,8 +1,19 @@ diff --git a/src/routes/(picsure)/(public)/discover/+page.svelte b/src/routes/(picsure)/(public)/discover/+page.svelte index fbe9efef..c9309335 100644 --- a/src/routes/(picsure)/(public)/discover/+page.svelte +++ b/src/routes/(picsure)/(public)/discover/+page.svelte @@ -1,8 +1,18 @@ diff --git a/tests/mock-data.ts b/tests/mock-data.ts index bddaa840..b0851440 100644 --- a/tests/mock-data.ts +++ b/tests/mock-data.ts @@ -697,6 +697,96 @@ export const facetsResponse = [ }, ]; +export const nestedFacetsResponse = [ + ...facetsResponse, + { + name: 'nested_category', + display: 'Nested Category', + description: 'Nested Category Description', + facets: [ + { + name: 'nested_facet', + display: 'Nested Facet', + description: 'Nested Facet Description', + count: 8, + children: [ + { + name: 'nested_facet_child', + display: 'Nested Facet Child', + description: 'Nested Facet Child Description', + count: 2, + children: null, + }, + { + name: 'nested_facet_child_2', + display: 'Nested Facet Child 2', + description: 'Nested Facet Child 2 Description', + count: 5, + children: null, + }, + { + name: 'nested_facet_child_3', + display: 'Nested Facet Child 3', + description: 'Nested Facet Child 3 Description', + count: 1, + children: null, + }, + ], + }, + { + name: 'nested_facet_2', + display: 'Nested Facet 2', + description: 'Nested Facet 2 Description', + count: 1, + children: null, + }, + { + name: 'nested_facet_3', + display: 'Nested Facet 3', + description: 'Nested Facet 3 Description', + count: 10, + children: [ + { + name: 'nested_facet_child_4', + display: 'Nested Facet Child 4', + description: 'Nested Facet Child 4 Description', + count: 1, + children: null, + }, + { + name: 'nested_facet_child_5', + display: 'Nested Facet Child 5', + description: 'Nested Facet Child 5 Description', + count: 1, + children: null, + }, + { + name: 'nested_facet_child_6', + display: 'Nested Facet Child 6', + description: 'Nested Facet Child 6 Description', + count: 5, + children: null, + }, + { + name: 'nested_facet_child_7', + display: 'Nested Facet Child 7', + description: 'Nested Facet Child 7 Description', + count: 3, + children: null, + }, + ], + }, + { + name: 'nested_facet_4', + display: 'Nested Facet 4', + description: 'Nested Facet 4 Description', + count: 300, + children: null, + }, + ], + }, +]; + const _application = { app1: { uuid: 'a1234', diff --git a/tests/routes/explorer/facets/test.ts b/tests/routes/explorer/facets/test.ts index 88f516c8..e64c51b6 100644 --- a/tests/routes/explorer/facets/test.ts +++ b/tests/routes/explorer/facets/test.ts @@ -5,6 +5,7 @@ import { facetsResponse, searchResultPath, facetResultPath, + nestedFacetsResponse, } from '../../../mock-data'; const MAX_FACETS_TO_SHOW = 5; @@ -335,10 +336,10 @@ test.describe('Facet Categories', () => { await searchInput.fill('Study Display'); await expect(searchInput).toHaveValue('Study Display'); const facetItems = await facetList.locator('label').all(); - facetItems.forEach(async (facetItem) => { + for (const facetItem of facetItems) { const facetItemText = await facetItem.textContent(); expect(facetItemText).toContain('Study Display'); - }); + } } } }); @@ -496,3 +497,44 @@ test.describe('Facet & search', () => { await expect(spanInInput).toContainText(facetsResponse[0].facets[0].count.toString()); }); }); + +test.describe('Nested Facets', () => { + test('Nested facets are displayed', async ({ page }) => { + // Given + await page.route(searchResultPath, async (route: Route) => + route.fulfill({ json: searchResults }), + ); + await page.route(facetResultPath, async (route: Route) => + route.fulfill({ json: nestedFacetsResponse }), + ); + await page.goto('/explorer?search=age'); + + // When + const nestedCategory = page.getByText('Nested Category'); + const nestedFacetArrow = page.getByTestId('facet-nested_facet-arrow'); + + // Then + await expect(nestedCategory).toBeVisible(); + await expect(nestedFacetArrow).toBeVisible(); + }); + test('Nested facets are toggleable', async ({ page }) => { + // Given + await page.route(searchResultPath, async (route: Route) => + route.fulfill({ json: searchResults }), + ); + await page.route(facetResultPath, async (route: Route) => + route.fulfill({ json: nestedFacetsResponse }), + ); + await page.goto('/explorer?search=age'); + + // When + const nestedFacetArrow = page.getByTestId('facet-nested_facet-arrow'); + await nestedFacetArrow.click(); + + // Then + const nestedFacetChildren = page.getByTestId('facet-nested_facet-children'); + await expect(nestedFacetChildren).toBeVisible(); + await nestedFacetArrow.click(); + await expect(nestedFacetChildren).not.toBeVisible(); + }); +});