Skip to content

Commit f64d756

Browse files
committed
OCLOMRS-1044:Bug Fix: Pick Concepts from Source and Dictionaries
1 parent 72e7409 commit f64d756

File tree

9 files changed

+826
-777
lines changed

9 files changed

+826
-777
lines changed

package-lock.json

+556-716
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"react-csv": "^2.0.3",
4545
"react-dom": "^17.0.2",
4646
"react-ga": "^3.3.0",
47+
"react-infinite-scroll-component": "^6.1.0",
4748
"react-redux": "^7.2.2",
4849
"react-router-dom": "^5.1.2",
4950
"react-scripts": "^4.0.3",
@@ -102,7 +103,7 @@
102103
"@types/jest": "^26.0.23",
103104
"@types/node": "^12.7.12",
104105
"cypress": "^7.5.0",
105-
"cypress-cucumber-preprocessor": "^4.1.2",
106+
"cypress-cucumber-preprocessor": "^4.1.0",
106107
"cypress-wait-until": "^1.7.1",
107108
"eslint-plugin-cypress": "^2.11.2",
108109
"husky": "^6.0.0",

src/apps/concepts/components/ViewConceptsHeader.tsx

+203-51
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,31 @@ import {
77
SOURCE_CONTAINER
88
} from "../constants";
99
import { getContainerIdFromUrl } from "../utils";
10-
import { Button, Menu, MenuItem, TextField, Theme } from "@mui/material";
10+
import {
11+
Button,
12+
FormControlLabel,
13+
Grid,
14+
IconButton,
15+
Input,
16+
InputAdornment,
17+
Menu,
18+
MenuItem,
19+
Switch,
20+
Theme
21+
} from "@mui/material";
1122
import { PREFERRED_SOURCES_VIEW_ONLY, useAnchor } from "../../../utils";
1223
import { APISource } from "../../sources";
13-
import { AccountTreeOutlined, FolderOpen } from "@mui/icons-material";
24+
import {
25+
AccountTreeOutlined,
26+
FolderOpen,
27+
Search as SearchIcon
28+
} from "@mui/icons-material";
1429
import { APIDictionary } from "../../dictionaries/types";
1530
import { createStyles, makeStyles } from "@mui/styles";
31+
import InfiniteScroll from "react-infinite-scroll-component";
32+
import { partialRight, pick } from "lodash";
33+
import dictionaryApi from "../../dictionaries/api";
34+
import sourceApi from "../../sources/api";
1635

1736
interface Props {
1837
containerType: string;
@@ -22,6 +41,23 @@ interface Props {
2241
children?: React.ReactNode[];
2342
sources: APISource[];
2443
dictionaries: APIDictionary[];
44+
showOnlyVerified: boolean;
45+
toggleShowVerified: React.ChangeEventHandler<HTMLInputElement>;
46+
goTo: Function;
47+
initialSearch: string;
48+
pathUrl: Function;
49+
dictionaryMeta?: {
50+
num_found?: number;
51+
page_number?: number;
52+
pages?: number;
53+
num_returned?: number;
54+
};
55+
sourcesMeta?: {
56+
num_found?: number;
57+
page_number?: number;
58+
pages?: number;
59+
num_returned?: number;
60+
};
2561
}
2662

2763
const useStyles = makeStyles((theme: Theme) =>
@@ -33,6 +69,9 @@ const useStyles = makeStyles((theme: Theme) =>
3369
padding: "0.2rem 1rem",
3470
cursor: "none"
3571
},
72+
sourcesDropdownHeader: {
73+
padding: "0.5rem 1rem"
74+
},
3675
input: {
3776
cursor: "pointer",
3877
borderBottom: "1px dotted black",
@@ -48,37 +87,97 @@ const useStyles = makeStyles((theme: Theme) =>
4887
},
4988
sourceIcon: {
5089
marginRight: "0.2rem",
51-
fill: "#8080809c",
52-
color:"#000000ad"
90+
fill: "#8080809c"
91+
},
92+
searchInput: {
93+
textAlign: "center",
94+
fontSize: "larger"
5395
}
5496
})
5597
);
56-
5798
const ViewConceptsHeader: React.FC<Props> = ({
5899
containerType,
59100
containerUrl,
60101
gimmeAUrl,
61102
addConceptToDictionary,
62103
children,
63104
sources,
64-
dictionaries
105+
dictionaries,
106+
goTo,
107+
initialSearch,
108+
pathUrl
65109
}) => {
66-
const [showSources, setShowSources] = useState(false);
67-
const [preferredSources, setPreferredSources] = useState<
110+
const [showAllSources, setShowAllSources] = useState(false);
111+
const [queryString, setQueryString] = useState(initialSearch);
112+
const [currentSources, setCurrentSources] = useState<
68113
{ name: string; url: string }[]
69114
>();
115+
const [useSources, setUseSources] = useState(true);
116+
const [currentPage, setCurrentPage] = useState(1);
117+
const [apiMethod, setApiMethod] = useState<
118+
| typeof sourceApi.sources.retrieve.private
119+
| typeof dictionaryApi.dictionaries.retrieve.private
120+
>(sourceApi.sources.retrieve.private);
121+
const [totalResultCount, setTotalResultCount] = useState(0);
122+
const [currentResultCount, setCurrentResultCount] = useState(0);
123+
const [resultsLoadedCount, setResultsLoadedCount] = useState(0);
124+
70125
useEffect(() => {
71126
const defaultSources = Object.entries(
72127
PREFERRED_SOURCES_VIEW_ONLY
73128
).map(([key, value]) => ({ name: key, url: value }));
74-
if (showSources) {
129+
if (showAllSources) {
75130
const allSources = defaultSources
76-
.concat(sources.map(s => ({ name: s.name, url: s.url })))
77-
.concat(dictionaries.map(d => ({ name: d.name, url: d.url })));
78-
setPreferredSources(allSources);
79-
} else setPreferredSources(defaultSources);
80-
}, [showSources, sources, dictionaries]);
131+
.concat(sources.map((s) => ({ name: s.name, url: s.url })))
132+
.concat(dictionaries.map((d) => ({ name: d.name, url: d.url })));
133+
setCurrentSources(allSources);
134+
} else setCurrentSources(defaultSources);
135+
}, [showAllSources, sources, dictionaries, initialSearch]);
136+
137+
useEffect(() => setCurrentPage(0), [useSources]);
138+
useEffect(
139+
() =>
140+
useSources
141+
? setApiMethod(sourceApi.sources.retrieve.private)
142+
: setApiMethod(dictionaryApi.dictionaries.retrieve.private),
143+
[useSources]
144+
);
145+
146+
const fetchMoreData = (page: number) => {
147+
setCurrentPage(page);
148+
apiMethod(
149+
useSources ? "/sources/" : "/collections/",
150+
"",
151+
25,
152+
currentPage
153+
).then((results) => {
154+
const sources = results.data.map(partialRight(pick, "name", "url")) as {
155+
name: string;
156+
url: string;
157+
}[];
81158

159+
if (currentPage === 1) {
160+
setTotalResultCount(results.headers.num_found);
161+
}
162+
163+
setCurrentResultCount(results.headers.num_returned);
164+
setResultsLoadedCount(
165+
results.headers.offset + results.headers.num_returned
166+
);
167+
168+
const existingSources = currentSources ? currentSources : [];
169+
setCurrentSources([...existingSources, ...sources]);
170+
});
171+
};
172+
173+
const loadData = () => {
174+
fetchMoreData(currentPage + 1);
175+
};
176+
177+
const handleSearch = (q: string) => goTo(pathUrl({ q }));
178+
const handleShowSources = (event: React.ChangeEvent<HTMLInputElement>) => {
179+
setShowAllSources(event.target.checked);
180+
};
82181
const classes = useStyles();
83182
const isSourceContainer = containerType === SOURCE_CONTAINER;
84183
const isAddToDictionary = isSourceContainer && !!addConceptToDictionary;
@@ -87,7 +186,6 @@ const ViewConceptsHeader: React.FC<Props> = ({
87186
handleSwitchSourceClick,
88187
handleSwitchSourceClose
89188
] = useAnchor();
90-
91189
const getTitleBasedOnContainerType = () => {
92190
return isAddToDictionary
93191
? `Import existing concept from ${getContainerIdFromUrl(containerUrl)}`
@@ -118,48 +216,103 @@ const ViewConceptsHeader: React.FC<Props> = ({
118216
}}
119217
anchorEl={switchSourceAnchor}
120218
keepMounted
121-
open={Boolean(switchSourceAnchor)}
219+
open={!!switchSourceAnchor}
122220
onClose={handleSwitchSourceClose}
123221
>
124-
<TextField
125-
multiline
126-
className={classes.textField}
127-
InputProps={{
128-
className: classes.underline
129-
}}
130-
inputProps={{
131-
className: classes.input
132-
}}
133-
value={
134-
showSources
135-
? "Choose a source/dictionary"
136-
: "Select a different source/dictionary"
137-
}
138-
onClick={() => setShowSources(!showSources)}
139-
/>
140-
{preferredSources?.map(({ name, url }) => (
141-
<MenuItem
142-
// replace because we want to keep the back button useful
143-
replace
144-
to={gimmeAUrl({}, `${url}concepts/`)}
145-
key={name}
146-
component={Link}
147-
onClick={handleSwitchSourceClose}
148-
data-testid={name}
222+
<Grid
223+
container
224+
direction="column"
225+
className={classes.sourcesDropdownHeader}
226+
>
227+
<FormControlLabel
228+
control={
229+
<Switch
230+
checked={showAllSources}
231+
onChange={handleShowSources}
232+
color="primary"
233+
name="displayVerified"
234+
/>
235+
}
236+
label={
237+
showAllSources ? `Showing all Sources` : `Show all Sources`
238+
}
239+
/>
240+
{showAllSources && (
241+
<form
242+
onSubmit={(e: React.SyntheticEvent) => {
243+
e.preventDefault();
244+
handleSearch(queryString);
245+
}}
246+
>
247+
<Input
248+
color="primary"
249+
type="search"
250+
fullWidth
251+
placeholder={"Select an alternative source"}
252+
value={queryString}
253+
onChange={(e) => setQueryString(e.target.value)}
254+
endAdornment={
255+
<InputAdornment position="end">
256+
<IconButton onClick={() => handleSearch(queryString)}>
257+
<SearchIcon />
258+
</IconButton>
259+
</InputAdornment>
260+
}
261+
/>
262+
</form>
263+
)}
264+
</Grid>
265+
{showAllSources ? (
266+
<InfiniteScroll
267+
dataLength={currentResultCount}
268+
next={loadData}
269+
hasMore={resultsLoadedCount < totalResultCount}
270+
loader={<h4>Loading...</h4>}
271+
endMessage={<h4>end</h4>}
272+
scrollableTarget="scrollableDiv"
149273
>
150-
{url?.includes("/collection") ? (
151-
<FolderOpen className={classes.sourceIcon} />
152-
) : (
153-
<AccountTreeOutlined className={classes.sourceIcon} />
154-
)}
155-
{name}
156-
</MenuItem>
157-
))}
274+
{currentSources?.map(({ name, url }) => (
275+
<MenuItem
276+
// replace because we want to keep the back button useful
277+
replace
278+
to={gimmeAUrl({}, `${url}concepts/`)}
279+
key={name}
280+
component={Link}
281+
onClick={handleSwitchSourceClose}
282+
>
283+
{url?.includes("/collection") ? (
284+
<FolderOpen className={classes.sourceIcon} />
285+
) : (
286+
<AccountTreeOutlined className={classes.sourceIcon} />
287+
)}
288+
{name}
289+
</MenuItem>
290+
))}
291+
</InfiniteScroll>
292+
) : (
293+
currentSources?.map(({ name, url }) => (
294+
<MenuItem
295+
// replace because we want to keep the back button useful
296+
replace
297+
to={gimmeAUrl({}, `${url}concepts/`)}
298+
key={name}
299+
component={Link}
300+
onClick={handleSwitchSourceClose}
301+
data-testid={name}
302+
>
303+
{url?.includes("/collection") ? (
304+
<FolderOpen className={classes.sourceIcon} />
305+
) : (
306+
<AccountTreeOutlined className={classes.sourceIcon} />
307+
)}
308+
{name}
309+
</MenuItem>
310+
))
311+
)}
158312
</Menu>
159313
</>
160314
);
161315
};
162-
163316
return (
164317
<Header
165318
title={getTitleBasedOnContainerType()}
@@ -177,5 +330,4 @@ const ViewConceptsHeader: React.FC<Props> = ({
177330
</Header>
178331
);
179332
};
180-
181333
export default ViewConceptsHeader;

0 commit comments

Comments
 (0)