diff --git a/src/actions/measurements.js b/src/actions/measurements.js index 63dfc372b..6aa078843 100644 --- a/src/actions/measurements.js +++ b/src/actions/measurements.js @@ -25,7 +25,7 @@ import { * @param {string} defaultKey * @returns {Object} */ -export const getCollectionToDisplay = (collections, collectionKey, defaultKey) => { +const getCollectionToDisplay = (collections, collectionKey, defaultKey) => { const defaultCollection = collections.filter((collection) => collection.key === defaultKey)[0]; if (!collectionKey) return defaultCollection; const potentialCollections = collections.filter((collection) => collection.key === collectionKey); @@ -109,7 +109,7 @@ function getCollectionDefaultControl(controlKey, collection) { * @param {Object} collection * @returns {MeasurementsControlState} */ -export function getCollectionDefaultControls(collection) { +function getCollectionDefaultControls(collection) { const defaultControls = {...defaultMeasurementsControlState}; if (Object.keys(collection).length) { for (const [key, value] of Object.entries(defaultControls)) { @@ -447,7 +447,7 @@ export function removeInvalidMeasurementsFilterQuery(query, newQueryParams) { return newQuery } -export function createMeasurementsQueryFromControls(measurementControls, collection, defaultCollectionKey) { +function createMeasurementsQueryFromControls(measurementControls, collection, defaultCollectionKey) { const newQuery = { m_collection: collection.key === defaultCollectionKey ? "" : collection.key }; @@ -497,49 +497,100 @@ export function createMeasurementsQueryFromControls(measurementControls, collect return newQuery; } -export function createMeasurementsControlsFromQuery(query){ - const newState = {}; +/** + * Parses the current collection's controls from measurements and updates them + * with valid query parameters. + * + * In cases where the query param is invalid, the query param is removed from the + * returned query object. + * @param {Object} measurements + * @param {Object} query + * @returns {Object} + */ +export const combineMeasurementsControlsAndQuery = (measurements, query) => { + const updatedQuery = cloneDeep(query); + const collectionKeys = measurements.collections.map((collection) => collection.key); + // Remove m_collection query if it's invalid or the default collection key + if (!collectionKeys.includes(updatedQuery.m_collection) || + updatedQuery.m_collection === measurements.defaultCollectionKey) { + delete updatedQuery.m_collection; + } + // Parse collection's default controls + const collectionKey = updatedQuery.m_collection || measurements.defaultCollectionKey; + const collectionToDisplay = getCollectionToDisplay(measurements.collections, collectionKey, measurements.defaultCollectionKey) + const collectionControls = getCollectionDefaultControls(collectionToDisplay); + const collectionGroupings = Array.from(collectionToDisplay.groupings.keys()); + // Modify controls via query for (const [controlKey, queryKey] of Object.entries(controlToQueryParamMap)) { - const queryValue = query[queryKey]; + const queryValue = updatedQuery[queryKey]; if (queryValue === undefined) continue; - let expectedValues = []; - let conversionFn = () => null; + let newControlState = undefined; switch(queryKey) { case "m_display": - expectedValues = ["mean", "raw"]; - conversionFn = () => queryValue; + if (queryValue === "mean" || queryValue === "raw") { + newControlState = queryValue; + } break; - case "m_collection": // fallthrough case "m_groupBy": - // Accept any value here because we cannot validate the query before - // the measurements JSON is loaded - expectedValues = [queryValue]; - conversionFn = () => queryValue; + // Verify value is a valid grouping of collection + if (collectionGroupings.includes(queryValue)) { + newControlState = queryValue; + } + break; + case "m_overallMean": + if (queryValue === "show" || queryValue === "hide") { + newControlState = queryValue === "show"; + } break; - case "m_overallMean": // fallthrough case "m_threshold": - expectedValues = ["show", "hide"]; - conversionFn = () => queryValue === "show"; + if (collectionToDisplay.thresholds && + (queryValue === "show" || queryValue === "hide")) { + newControlState = queryValue === "show"; + } break; + default: + console.error(`Ignoring unsupported query ${queryKey}`); } - if(expectedValues.includes(queryValue)) { - newState[controlKey] = conversionFn(); - } else { - console.error(`Ignoring invalid query param ${queryKey}=${queryValue}, value should be one of ${expectedValues}`); + // Remove query if it's invalid or the same as the collection's default controls + if (newControlState === undefined || newControlState === collectionControls[controlKey]) { + delete updatedQuery[queryKey]; + continue; } + collectionControls[controlKey] = newControlState } - // Accept any value here because we cannot validate the query before the measurements JSON is loaded - for (const filterKey of Object.keys(query).filter((c) => c.startsWith(filterQueryPrefix))) { + // Special handling of the filter query since these can be arbitrary query keys `mf_*` + for (const filterKey of Object.keys(updatedQuery).filter((c) => c.startsWith(filterQueryPrefix))) { + // Remove and ignore query for invalid fields const field = filterKey.replace(filterQueryPrefix, ''); - const filterValues = Array.isArray(query[filterKey]) ? query[filterKey] : [query[filterKey]]; - const measurementsFilters = {...newState.measurementsFilters}; + if (!collectionToDisplay.filters.has(field)) { + delete updatedQuery[filterKey]; + continue; + } + + // Remove and ignore query for invalid field values + const collectionFieldValues = collectionToDisplay.filters.get(field).values; + const filterValues = Array.isArray(updatedQuery[filterKey]) ? updatedQuery[filterKey] : [updatedQuery[filterKey]]; + const validFilterValues = filterValues.filter((value) => collectionFieldValues.has(value)); + if (!validFilterValues.length) { + delete updatedQuery[filterKey]; + continue; + } + + // Set field filter controls and query to the valid filter values + updatedQuery[filterKey] = validFilterValues; + const measurementsFilters = {...collectionControls.measurementsFilters}; measurementsFilters[field] = new Map(measurementsFilters[field]); - for (const value of filterValues) { + for (const value of validFilterValues) { measurementsFilters[field].set(value, {active: true}); } - newState.measurementsFilters = measurementsFilters; + collectionControls.measurementsFilters = measurementsFilters; + + } + return { + collectionToDisplay, + collectionControls, + updatedQuery } - return newState; } diff --git a/src/actions/recomputeReduxState.js b/src/actions/recomputeReduxState.js index d886d5436..6dcc52aa6 100644 --- a/src/actions/recomputeReduxState.js +++ b/src/actions/recomputeReduxState.js @@ -22,7 +22,7 @@ import { getTraitFromNode, getDivFromNode, collectGenotypeStates } from "../util import { collectAvailableTipLabelOptions } from "../components/controls/choose-tip-label"; import { hasMultipleGridPanels } from "./panelDisplay"; import { strainSymbolUrlString } from "../middleware/changeURL"; -import { createMeasurementsControlsFromQuery, getCollectionDefaultControls, getCollectionToDisplay, loadMeasurements } from "./measurements"; +import { combineMeasurementsControlsAndQuery, loadMeasurements } from "./measurements"; export const doesColorByHaveConfidence = (controlsState, colorBy) => controlsState.coloringsPresentOnTreeWithConfidence.has(colorBy); @@ -207,9 +207,6 @@ const modifyStateViaURLQuery = (state, query) => { if (query.scatterX) state.scatterVariables.x = query.scatterX; if (query.scatterY) state.scatterVariables.y = query.scatterY; - /* Process query params for measurements panel. These all start with `m_` or `mf_` prefix to avoid conflicts */ - state = {...state, ...createMeasurementsControlsFromQuery(query)} - return state; function _validDate(dateNum, absoluteDateMinNumeric, absoluteDateMaxNumeric) { return !(dateNum===undefined || dateNum > absoluteDateMaxNumeric || dateNum < absoluteDateMinNumeric); @@ -891,9 +888,6 @@ export const createStateFromQueryOrJSONs = ({ controls = getDefaultControlsState(); controls = modifyControlsStateViaTree(controls, tree, treeToo, metadata.colorings); controls = modifyStateViaMetadata(controls, metadata, entropy.genomeMap); - if (measurements.loaded) { - controls = {...controls, ...getCollectionDefaultControls(measurements.collectionToDisplay)}; - } } else if (oldState) { /* creating deep copies avoids references to (nested) objects remaining the same which can affect props comparisons. Due to the size of some of the state, we only do this selectively */ @@ -906,12 +900,6 @@ export const createStateFromQueryOrJSONs = ({ measurements = {...oldState.measurements}; controls = restoreQueryableStateToDefaults(controls); controls = modifyStateViaMetadata(controls, metadata, entropy.genomeMap); - /* If available, reset to the default collection and the collection's default controls - so that narrative queries are respected between slides */ - if (measurements.loaded) { - measurements.collectionToDisplay = getCollectionToDisplay(measurements.collections, "", measurements.defaultCollectionKey) - controls = {...controls, ...getCollectionDefaultControls(measurements.collectionToDisplay)}; - } } /* For the creation of state, we want to parse out URL query parameters @@ -925,22 +913,18 @@ export const createStateFromQueryOrJSONs = ({ narrativeSlideIdx = getNarrativePageFromQuery(query, narrative); /* replace the query with the information which can guide the view */ query = queryString.parse(narrative[narrativeSlideIdx].query); - /** - * Special case where narrative includes query param for new measurements collection `m_collection` - * We need to reset the measurements and controls to the new collection's defaults before - * processing the remaining query params - */ - if (query.m_collection && measurements.loaded) { - const newCollectionToDisplay = getCollectionToDisplay(measurements.collections, query.m_collection, measurements.defaultCollectionKey); - measurements.collectionToDisplay = newCollectionToDisplay; - controls = {...controls, ...getCollectionDefaultControls(measurements.collectionToDisplay)}; - // Delete `m_collection` so there's no chance of things getting mixed up when processing remaining query params - delete query.m_collection; - } } controls = modifyStateViaURLQuery(controls, query); + /* Special handling of measurements controls and query params */ + if (measurements.loaded) { + const { collectionToDisplay, collectionControls, updatedQuery} = combineMeasurementsControlsAndQuery(measurements, query); + measurements.collectionToDisplay = collectionToDisplay; + controls = {...controls, ...collectionControls}; + query = updatedQuery; + } + /* certain narrative slides prescribe the main panel to simply render narrative-provided markdown content */ if (narrativeBlocks && narrative[narrativeSlideIdx].mainDisplayMarkdown) { controls.panelsToDisplay = ["MainDisplayMarkdown"];