Skip to content

Commit

Permalink
Merge pull request #96 from bento-platform/fix/query-params-navigate
Browse files Browse the repository at this point in the history
fix: error when selecting query option + bad selectors
  • Loading branch information
davidlougheed authored Jul 3, 2023
2 parents ac67de2 + 135e9e8 commit 6144511
Show file tree
Hide file tree
Showing 10 changed files with 62 additions and 38 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ go 1.19
require (
github.com/kelseyhightower/envconfig v1.4.0
github.com/labstack/echo v3.3.10+incompatible
github.com/patrickmn/go-cache v2.1.0+incompatible
)

require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/stretchr/testify v1.8.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bento_public",
"version": "0.13.0",
"version": "0.13.1",
"description": "A publicly accessible portal for clinical datasets, where users are able to see high-level statistics of the data available through predefined variables of interest and search the data using limited variables at a time. This portal allows users to gain a generic understanding of the data available (secure and firewalled) without the need to access it directly. Initially, this portal facilitates the search in English language only, but the French language will be added at a later time.",
"main": "index.js",
"scripts": {
Expand Down
18 changes: 4 additions & 14 deletions src/js/components/Search/MakeQueryOption.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React from 'react';
import { Row, Col, Checkbox } from 'antd';
import { useTranslation } from 'react-i18next';

Expand All @@ -9,26 +9,16 @@ import SelectOption from './SelectOption';
import { DEFAULT_TRANSLATION, NON_DEFAULT_TRANSLATION } from '@/constants/configConstants';
import { useAppDispatch, useAppSelector } from '@/hooks';
import { Field } from '@/types/search';
import { useLocation, useNavigate } from 'react-router-dom';

const MakeQueryOption = ({ queryField }: MakeQueryOptionProps) => {
const { t } = useTranslation(NON_DEFAULT_TRANSLATION);
const { t: td } = useTranslation(DEFAULT_TRANSLATION);
const dispatch = useAppDispatch();
const navigate = useNavigate();
const location = useLocation();

const { title, id, description, config, options } = queryField;

const [checkedCount, queryParams, maxCount] = useAppSelector((state) => [
state.query.queryParamCount,
state.query.queryParams,
state.config.maxQueryParameters,
]);

useEffect(() => {
navigate(`${location.pathname}?${new URLSearchParams(queryParams).toString()}`, { replace: true });
}, [queryParams]);
const maxQueryParameters = useAppSelector((state) => state.config.maxQueryParameters);
const { queryParamCount, queryParams } = useAppSelector((state) => state.query);

const isChecked = Object.prototype.hasOwnProperty.call(queryParams, id);

Expand All @@ -41,7 +31,7 @@ const MakeQueryOption = ({ queryField }: MakeQueryOptionProps) => {
dispatch(makeGetKatsuPublic());
};

const disabled = isChecked ? false : checkedCount >= maxCount;
const disabled = isChecked ? false : queryParamCount >= maxQueryParameters;

return (
<>
Expand Down
50 changes: 34 additions & 16 deletions src/js/components/Search/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { Row, Typography, Space, FloatButton } from 'antd';
import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router-dom';

import SearchFieldsStack from './SearchFieldsStack';
import SearchResults from './SearchResults';

import { makeGetKatsuPublic, setQueryParams } from '@/features/search/query.store';
import { NON_DEFAULT_TRANSLATION } from '@/constants/configConstants';
import { makeGetKatsuPublic, setQueryParams } from '@/features/search/query.store';
import { useAppDispatch, useAppSelector } from '@/hooks';
import { useLocation, useNavigate } from 'react-router-dom';
import { buildQueryParamsUrl } from '@/utils/search';

import type { QueryParams } from '@/types/search';

type QueryParams = { [key: string]: string };
const checkQueryParamsEqual = (qp1: QueryParams, qp2: QueryParams): boolean =>
[...new Set(...Object.keys(qp1), ...Object.keys(qp2))].reduce((acc, v) => acc && qp1[v] === qp2[v], true);

const RoutedSearch: React.FC = () => {
const dispatch = useAppDispatch();
const location = useLocation();
const navigate = useNavigate();

const searchSections = useAppSelector((state) => state.query.querySections);
const maxQueryParameters = useAppSelector((state) => state.config.maxQueryParameters);
const isFetchingSearchFields = useAppSelector((state) => state.query.isFetchingFields);

const searchFields = searchSections.flatMap(({ fields }) =>
fields.map((field) => ({ id: field.id, options: field.options }))
const {
querySections: searchSections,
queryParams,
isFetchingFields: isFetchingSearchFields,
attemptedFetch,
} = useAppSelector((state) => state.query);

const searchFields = useMemo(
() => searchSections.flatMap(({ fields }) => fields.map((field) => ({ id: field.id, options: field.options }))),
[searchSections]
);

const validateQuery = (query: URLSearchParams): { valid: boolean; validQueryParamsObject: QueryParams } => {
Expand All @@ -45,22 +54,31 @@ const RoutedSearch: React.FC = () => {
return { valid: JSON.stringify(validQueryParamArray) === JSON.stringify(queryParamArray), validQueryParamsObject };
};

// Synchronize Redux query params state from URL
useEffect(() => {
if (isFetchingSearchFields) return;
if (!location.pathname.endsWith('/search')) return;
const queryParam = new URLSearchParams(location.search);
const { valid, validQueryParamsObject } = validateQuery(queryParam);
if (valid) {
dispatch(setQueryParams(validQueryParamsObject));
dispatch(makeGetKatsuPublic());
if (!attemptedFetch || !checkQueryParamsEqual(validQueryParamsObject, queryParams)) {
// Only update the state & refresh if we have a new set of query params from the URL.
dispatch(setQueryParams(validQueryParamsObject));
dispatch(makeGetKatsuPublic());
}
} else {
console.debug(
'Redirecting to : ',
`${location.pathname}?${new URLSearchParams(validQueryParamsObject).toString()}`
);
navigate(`${location.pathname}?${new URLSearchParams(validQueryParamsObject).toString()}`);
console.debug('Redirecting to : ', buildQueryParamsUrl(location.pathname, validQueryParamsObject));
navigate(buildQueryParamsUrl(location.pathname, validQueryParamsObject));
}
}, [location.search]);

// Synchronize URL from Redux query params state
useEffect(() => {
if (!location.pathname.endsWith('/search')) return;
if (!attemptedFetch) return;
navigate(buildQueryParamsUrl(location.pathname, queryParams), { replace: true });
}, [queryParams]);

return <Search />;
};

Expand Down
9 changes: 7 additions & 2 deletions src/js/components/TabbedDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import ProvenanceTab from './Provenance/ProvenanceTab';
import BeaconQueryUi from './Beacon/BeaconQueryUi';
import { DEFAULT_TRANSLATION } from '@/constants/configConstants';
import { useAppDispatch, useAppSelector } from '@/hooks';
import { buildQueryParamsUrl } from '@/utils/search';

const TabbedDashboard = () => {
const dispatch = useAppDispatch();
Expand All @@ -37,6 +38,7 @@ const TabbedDashboard = () => {

const isFetchingOverviewData = useAppSelector((state) => state.data.isFetchingData);
const isFetchingSearchFields = useAppSelector((state) => state.query.isFetchingFields);
const queryParams = useAppSelector((state) => state.query.queryParams);
const isFetchingBeaconConfig = useAppSelector((state) => state.beaconConfig?.isFetchingBeaconConfig);
const renderBeaconUi = useAppSelector((state) => state.config?.beaconUiEnabled);

Expand All @@ -46,9 +48,12 @@ const TabbedDashboard = () => {
const currentPathParts = currentPath.split('/');
const currentLang = currentPathParts[1];
const newPath = `/${currentLang}/${key === 'overview' ? '' : key}`;
navigate(newPath);
// If we're going to the search page, insert query params into the URL pulled from the Redux state.
// This is important to keep the URL updated if we've searched something, navigated away, and now
// are returning to the search page.
navigate(key === 'search' ? buildQueryParamsUrl(newPath, queryParams) : newPath);
},
[location, navigate]
[location, navigate, queryParams]
);

const TabTitle = ({ title }: { title: string }) => (
Expand Down
8 changes: 6 additions & 2 deletions src/js/features/search/query.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { serializeChartData } from '@/utils/chart';
import { KatsuSearchResponse, SearchFieldResponse } from '@/types/search';
import { ChartData } from '@/types/data';

type queryState = {
type QueryState = {
isFetchingFields: boolean;
isFetchingData: boolean;
attemptedFetch: boolean;
querySections: SearchFieldResponse['sections'];
queryParams: { [key: string]: string };
queryParamCount: number;
Expand All @@ -20,9 +21,10 @@ type queryState = {
individualCount: number;
};

const initialState: queryState = {
const initialState: QueryState = {
isFetchingFields: true,
isFetchingData: false,
attemptedFetch: false,
message: '',
querySections: [],
queryParams: {},
Expand Down Expand Up @@ -63,6 +65,7 @@ const query = createSlice({
});
builder.addCase(makeGetKatsuPublic.fulfilled, (state, { payload }: PayloadAction<KatsuSearchResponse>) => {
state.isFetchingData = false;
state.attemptedFetch = true;
if ('message' in payload) {
state.message = payload.message;
return;
Expand All @@ -76,6 +79,7 @@ const query = createSlice({
});
builder.addCase(makeGetKatsuPublic.rejected, (state) => {
state.isFetchingData = false;
state.attemptedFetch = true;
});
builder.addCase(makeGetSearchFields.pending, (state) => {
state.isFetchingFields = true;
Expand Down
2 changes: 2 additions & 0 deletions src/js/types/search.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Datum } from '@/types/overviewResponse';

export type QueryParams = { [key: string]: string };

export interface SearchFieldResponse {
sections: Section[];
}
Expand Down
4 changes: 4 additions & 0 deletions src/js/utils/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { QueryParams } from '@/types/search';

export const buildQueryParamsUrl = (pathName: string, qp: QueryParams): string =>
`${pathName}?${new URLSearchParams(qp).toString()}`;
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"strictNullChecks": true,
"module": "es6",
"target": "es5",
"downlevelIteration": true,
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
Expand Down

0 comments on commit 6144511

Please sign in to comment.