Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[98] add query for curator comments #154

Merged
merged 16 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion data-serving/scripts/setup-db/schemas/day0cases.indexes.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"location.admin2": "text",
"location.admin3": "text",
"caseReference.sourceUrl": "text",
"caseStatus": "text"
"caseStatus": "text",
"comment": "text"
}
},
{
Expand Down Expand Up @@ -145,6 +146,16 @@
"strength": 2
}
},
{
"name": "commentIdx",
"key": {
"comment": -1
},
"collation": {
"locale": "en_US",
"strength": 2
}
},
{
"name": "countryAndDate",
"key": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ describe('Linelist table', function () {
sourceUrl: 'www.example.com',
caseStatus: CaseStatus.Confirmed,
occupation: 'Actor',
comment: 'note',
});
cy.addCase({
country: 'Germany',
Expand All @@ -524,6 +525,7 @@ describe('Linelist table', function () {
cy.intercept('GET', getDefaultQuery({ limit: 50 })).as('getCases');
cy.intercept('GET', getDefaultQuery({ limit: 50, query: 'Argentina' })).as('getCasesWithSearch1');
cy.intercept('GET', getDefaultQuery({ limit: 50, query: 'Doctor' })).as('getCasesWithSearch2');
cy.intercept('GET', getDefaultQuery({ limit: 50, query: 'note' })).as('getCasesWithSearch3');

cy.visit('/cases');
cy.wait('@getCases');
Expand All @@ -549,5 +551,30 @@ describe('Linelist table', function () {
cy.contains('Argentina').should('not.exist');
cy.contains('France').should('not.exist');
cy.contains('Germany').should('exist');

cy.get('#clear-search').click();
cy.wait('@getCases');
cy.contains('Argentina').should('exist');
cy.contains('France').should('exist');
cy.contains('Germany').should('exist');

cy.get('#search-field').type('note');
cy.wait('@getCasesWithSearch3');
cy.contains('Argentina').should('not.exist');
cy.contains('France').should('exist');
cy.contains('Germany').should('not.exist');
});

it('Informs user when uneven number of quotes is present in free-text search', () => {
cy.intercept('GET', getDefaultQuery({ limit: 50, query: '"Bus driver"' })).as('getCasesWithSearch');
cy.visit('/cases');
cy.contains('Please make sure you have an even number of quotes.').should('not.exist');

cy.get('#search-field').type('"Bus driver');
cy.contains('Please make sure you have an even number of quotes.').should('exist');

cy.get('#search-field').type('"');
cy.wait('@getCasesWithSearch');
cy.contains('Please make sure you have an even number of quotes.').should('not.exist');
});
});
2 changes: 2 additions & 0 deletions verification/curator-service/ui/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ interface AddCaseProps {
gender?: Gender;
outcome?: Outcome;
uploadIds?: string[];
comment?: string;
}

declare global {
Expand Down Expand Up @@ -111,6 +112,7 @@ export function addCase(opts: AddCaseProps): void {
travelHistory: {},
genomeSequences: {},
vaccination: {},
comment: opts.comment || '',
},
});
}
Expand Down
6 changes: 4 additions & 2 deletions verification/curator-service/ui/src/components/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -327,13 +327,14 @@ export default function App(): JSX.Element {
};

const onModalClose = (): void => {
const searchQueryObject = new URLSearchParams(searchQuery);
navigate(
{
pathname:
location.state && location.state.lastLocation
? location.state.lastLocation
: '/cases',
search: searchQuery,
search: searchQueryObject.toString(),
},
{ state: { lastLocation: '/case/view' } },
);
Expand Down Expand Up @@ -371,7 +372,8 @@ export default function App(): JSX.Element {
)
return;

dispatch(setSearchQuery(location.search));
const searchParams = new URLSearchParams(location.search);
dispatch(setSearchQuery(searchParams.toString()));

// Save searchQuery to local storage not to lost it when user goes through auth process
localStorage.setItem('searchQuery', location.search);
Expand Down
55 changes: 44 additions & 11 deletions verification/curator-service/ui/src/components/DataGuideDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import { Box, Portal, Theme, Typography } from '@mui/material';
import { withStyles } from 'tss-react/mui';
import CloseIcon from '@mui/icons-material/Close';
import { Close as CloseIcon } from '@mui/icons-material';
import Draggable, { ControlPosition } from 'react-draggable';

// As per this issue from react-draggable library: https://github.com/react-grid-layout/react-draggable/pull/648
Expand Down Expand Up @@ -96,16 +96,49 @@ const SearchGuideDialog = ({
the left and choose ascending or descending.
</Typography>
<Typography className={classes?.textSection}>
<strong>For full-text search</strong>, enter any combination of search terms.
<br/>
Full-text search covers: occupation, admin0, admin1, admin2, admin3, sourceUrl and caseStatus.
<br/>
Search terms must be exact (example: "German" will not match "Germany").
<br/>
Full-text search matches cases that contain any ot the search terms, not a combination.
<br/>
No special characters apart from "." are allowed and if the "." is used in a search term
given search term must be contained within quotation marks.
<strong>For full-text search</strong>, enter any
combination of search terms. Rules for full-text
search:
<br />
<ul>
<li>
Full-text search covers: occupation, admin0,
admin1, admin2, admin3, sourceUrl, comment
and caseStatus.
</li>
<li>
Search terms must be exact (example:{' '}
<b>
<i>German</i>
</b>{' '}
will not match{' '}
<b>
<i>Germany</i>
</b>
).
</li>
<li>
Full-text search matches cases that contain
any of the search terms, not a combination.
</li>
<li>
To search for a combination of terms, wrap
the combination in quotation marks (example:{' '}
<b>
<i>"Bus driver"</i>
</b>
).
</li>
<li>
No special characters apart from dot are
allowed. Search terms with dot must be
contained within quotation marks (example:{' '}
<b>
<i>"global.health"</i>
</b>
).
</li>
</ul>
</Typography>
<Typography>
You can use the icons on the right to navigate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
useMediaQuery,
} from '@mui/material';
import { Close as CloseIcon } from '@mui/icons-material';
import { filtersToURL, URLToFilters } from '../util/searchQuery';
import { URLToFilters } from '../util/searchQuery';
import { hasAnyRole } from '../util/helperFunctions';
import { useAppSelector, useAppDispatch } from '../../hooks/redux';
import { fetchCountries } from '../../redux/filters/thunk';
Expand Down Expand Up @@ -153,13 +153,19 @@ export default function FiltersDialog({
handleSetModalAlert();
dispatch(setModalOpen(false));

const searchQuery = filtersToURL(values);
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(values)) {
if (value) searchParams.set(key, value);
}
const q = (new URLSearchParams(location.search)).get('q')
if (q) searchParams.set('q', q);
const searchParamsString = searchParams.toString();

sendCustomGtmEvent('filters_applied', { query: searchQuery });
sendCustomGtmEvent('filters_applied', { query: searchParamsString });

navigate({
pathname: '/cases',
search: searchQuery,
search: searchParamsString,
});
},
});
Expand Down
57 changes: 31 additions & 26 deletions verification/curator-service/ui/src/components/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import clsx from 'clsx';
import DataGuideDialog from './DataGuideDialog';
import { useDebounce } from '../hooks/useDebounce';
import FiltersDialog from './FiltersDialog';
import { searchQueryToURL, URLToSearchQuery } from './util/searchQuery';
import { useLocation, useNavigate } from 'react-router-dom';
import { KeyboardEvent, ChangeEvent } from 'react';
import { useAppSelector, useAppDispatch } from '../hooks/redux';
Expand Down Expand Up @@ -85,9 +84,7 @@ export default function SearchBar({
const [isUserTyping, setIsUserTyping] = useState<boolean>(false);
const [isDataGuideOpen, setIsDataGuideOpen] = useState<boolean>(false);
const [searchInput, setSearchInput] = useState<string>(
location.search.includes('?q=')
? URLToSearchQuery(location.search)
: '',
new URLSearchParams(location.search).get('q') || '',
);
const [modalAlert, setModalAlert] = useState<boolean>(false);
const guideButtonRef = React.useRef<HTMLButtonElement>(null);
Expand All @@ -103,28 +100,30 @@ export default function SearchBar({
}
}, [filtersBreadcrumb]);

useEffect(() => {
const q = new URLSearchParams(location.search).get('q') || '';
if (q !== searchInput) setSearchInput(q);
}, [location.search]);

// Set search query debounce to 1000ms
const debouncedSearch = useDebounce(searchInput, 2000);

// Update search input based on search query
useEffect(() => {
if (!location.search.includes('?q=')) {
setSearchInput('');
return;
}
const handleNavigating = (q: string) => {
const searchParams = new URLSearchParams(location.search);
q !== '' ? searchParams.set('q', q) : searchParams.delete('q');

setSearchInput(URLToSearchQuery(location.search));
}, [location.search]);
navigate({
pathname: '/cases',
search: searchParams.toString(),
});
};

// Apply filter parameters after delay
useEffect(() => {
if (!isUserTyping) return;

setIsUserTyping(false);
navigate({
pathname: '/cases',
search: searchQueryToURL(debouncedSearch),
});

handleNavigating(debouncedSearch);
//eslint-disable-next-line
}, [debouncedSearch]);

Expand All @@ -136,10 +135,8 @@ export default function SearchBar({
if (ev.key === 'Enter') {
ev.preventDefault();
setIsUserTyping(false);
navigate({
pathname: '/cases',
search: searchQueryToURL(searchInput),
});

handleNavigating(searchInput);
}
};

Expand Down Expand Up @@ -174,16 +171,24 @@ export default function SearchBar({
return searchStringStrippedOutColon;
}

const renderSearchErrorMessage = () => {
if (searchError) {
return 'Incorrect entry. ":" characters have been removed. Please use filters instead.';
} else {
const quoteCount = searchInput.split('"').length - 1;
if (quoteCount % 2 !== 0) {
return 'Incorrect entry. Please make sure you have an even number of quotes.';
}
}
};

return (
<>
<div className={classes.searchRoot}>
<StyledSearchTextField
size="small"
error={searchError}
helperText={
searchError &&
'Incorrect entry. ":" characters have been removed. Please use filters instead.'
}
helperText={renderSearchErrorMessage()}
id="search-field"
data-testid="searchbar"
name="searchbar"
Expand All @@ -203,7 +208,7 @@ export default function SearchBar({
}
}}
placeholder="Fulltext search"
value={decodeURI(searchInput)}
value={searchInput}
variant="outlined"
fullWidth
InputProps={{
Expand Down
Loading
Loading