Skip to content

Commit 3b1ecf2

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

10 files changed

+1094
-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",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
import React, { useEffect, useState } from "react";
2+
import { Link } from "react-router-dom";
3+
import { DICTIONARY_VERSION_CONTAINER, SOURCE_CONTAINER } from "../constants";
4+
import { getContainerIdFromUrl } from "../utils";
5+
import {
6+
Button,
7+
FormControlLabel,
8+
Grid,
9+
IconButton,
10+
Input,
11+
InputAdornment,
12+
Menu,
13+
MenuItem,
14+
Switch,
15+
Theme
16+
} from "@mui/material";
17+
import { PREFERRED_SOURCES_VIEW_ONLY, useAnchor } from "../../../utils";
18+
import { APISource } from "../../sources";
19+
import {
20+
AccountTreeOutlined,
21+
FolderOpen,
22+
Search as SearchIcon
23+
} from "@mui/icons-material";
24+
import { APIDictionary } from "../../dictionaries/types";
25+
import { createStyles, makeStyles } from "@mui/styles";
26+
import InfiniteScroll from "react-infinite-scroll-component";
27+
import { partialRight, pick } from "lodash";
28+
import dictionaryApi from "../../dictionaries/api";
29+
import sourceApi from "../../sources/api";
30+
31+
interface Props {
32+
containerType: string;
33+
containerUrl?: string;
34+
gimmeAUrl: Function;
35+
addConceptToDictionary?: string;
36+
sources: APISource[];
37+
dictionaries: APIDictionary[];
38+
showOnlyVerified: boolean;
39+
toggleShowVerified: React.ChangeEventHandler<HTMLInputElement>;
40+
goTo: Function;
41+
initialSearch: string;
42+
pathUrl: Function;
43+
dictionaryMeta?: {
44+
num_found?: number;
45+
page_number?: number;
46+
pages?: number;
47+
num_returned?: number;
48+
};
49+
sourcesMeta?: {
50+
num_found?: number;
51+
page_number?: number;
52+
pages?: number;
53+
num_returned?: number;
54+
};
55+
}
56+
57+
const useStyles = makeStyles((theme: Theme) =>
58+
createStyles({
59+
lightColour: {
60+
color: "white !important"
61+
},
62+
textField: {
63+
padding: "0.2rem 1rem",
64+
cursor: "none"
65+
},
66+
sourcesDropdownHeader: {
67+
padding: "0.5rem 1rem"
68+
},
69+
input: {
70+
cursor: "pointer",
71+
borderBottom: "1px dotted black",
72+
paddingBottom: "0.25rem"
73+
},
74+
underline: {
75+
"&&&:before": {
76+
borderBottom: "none"
77+
},
78+
"&&:after": {
79+
borderBottom: "none"
80+
}
81+
},
82+
sourceIcon: {
83+
marginRight: "0.2rem",
84+
fill: "#8080809c"
85+
},
86+
searchInput: {
87+
textAlign: "center",
88+
fontSize: "larger"
89+
}
90+
})
91+
);
92+
const ConceptSourceSwitcher: React.FC<Props> = ({
93+
containerType,
94+
containerUrl,
95+
gimmeAUrl,
96+
addConceptToDictionary,
97+
sources,
98+
dictionaries,
99+
goTo,
100+
initialSearch,
101+
pathUrl
102+
}) => {
103+
const [queryString, setQueryString] = useState(initialSearch);
104+
const [currentSources, setCurrentSources] = useState<
105+
{ name: string; url: string }[]
106+
>();
107+
const [useSources, setUseSources] = useState(true);
108+
const [currentPage, setCurrentPage] = useState(1);
109+
const [apiMethod, setApiMethod] = useState<
110+
| typeof sourceApi.sources.retrieve.private
111+
| typeof dictionaryApi.dictionaries.retrieve.private
112+
>(sourceApi.sources.retrieve.private);
113+
const [totalResultCount, setTotalResultCount] = useState(0);
114+
const [currentResultCount, setCurrentResultCount] = useState(0);
115+
const [resultsLoadedCount, setResultsLoadedCount] = useState(0);
116+
117+
useEffect(() => {
118+
const defaultSources = Object.entries(
119+
PREFERRED_SOURCES_VIEW_ONLY
120+
).map(([key, value]) => ({ name: key, url: value }));
121+
const allSources = defaultSources
122+
.concat(sources.map((s) => ({ name: s.name, url: s.url })))
123+
.concat(dictionaries.map((d) => ({ name: d.name, url: d.url })));
124+
setCurrentSources(allSources);
125+
}, [sources, dictionaries, initialSearch]);
126+
127+
useEffect(() => setCurrentPage(0), [useSources]);
128+
useEffect(
129+
() =>
130+
useSources
131+
? setApiMethod(sourceApi.sources.retrieve.private)
132+
: setApiMethod(dictionaryApi.dictionaries.retrieve.private),
133+
[useSources]
134+
);
135+
136+
useEffect(() => {
137+
const url = useSources ? "/sources/" : "/collections/";
138+
139+
apiMethod(url, undefined, 25, currentPage).then((results) => {
140+
const sources = results.data.map(partialRight(pick, "name", "url")) as {
141+
name: string;
142+
url: string;
143+
}[];
144+
145+
if (currentPage === 1) {
146+
setTotalResultCount(results.headers.num_found);
147+
}
148+
149+
setCurrentResultCount(results.headers.num_returned);
150+
setResultsLoadedCount(
151+
results.headers.offset + results.headers.num_returned
152+
);
153+
154+
const existingSources = currentSources ? currentSources : [];
155+
setCurrentSources([...existingSources, ...sources]);
156+
});
157+
}, [apiMethod, currentPage, currentSources, useSources]);
158+
159+
const loadData = () => {
160+
setCurrentPage(currentPage + 1);
161+
};
162+
163+
const handleSearch = (q: string) => goTo(pathUrl({ q }));
164+
165+
const classes = useStyles();
166+
const isSourceContainer = containerType === SOURCE_CONTAINER;
167+
const isAddToDictionary = isSourceContainer && !!addConceptToDictionary;
168+
const [
169+
switchSourceAnchor,
170+
handleSwitchSourceClick,
171+
handleSwitchSourceClose
172+
] = useAnchor();
173+
174+
return !isAddToDictionary ? null : (
175+
<>
176+
<Button
177+
data-testid="switch-source"
178+
className={classes.lightColour}
179+
variant="text"
180+
size="large"
181+
aria-haspopup="true"
182+
onClick={handleSwitchSourceClick}
183+
>
184+
Switch source (Currently {getContainerIdFromUrl(containerUrl)})
185+
</Button>
186+
<Menu
187+
PaperProps={{
188+
style: {
189+
marginTop: "30px",
190+
marginLeft: "10px"
191+
}
192+
}}
193+
anchorEl={switchSourceAnchor}
194+
keepMounted
195+
open={!!switchSourceAnchor}
196+
onClose={handleSwitchSourceClose}
197+
>
198+
<Grid
199+
container
200+
direction="column"
201+
className={classes.sourcesDropdownHeader}
202+
>
203+
<FormControlLabel
204+
control={
205+
<Switch
206+
checked={useSources}
207+
onChange={() => setUseSources(!useSources)}
208+
color="primary"
209+
name="displayVerified"
210+
/>
211+
}
212+
label={useSources ? `Showing all Sources` : `Show all Dictionaries`}
213+
/>
214+
<form
215+
onSubmit={(e: React.SyntheticEvent) => {
216+
e.preventDefault();
217+
handleSearch(queryString);
218+
}}
219+
>
220+
<Input
221+
color="primary"
222+
type="search"
223+
fullWidth
224+
placeholder={"Select an alternative source"}
225+
value={queryString}
226+
onChange={(e) => setQueryString(e.target.value)}
227+
endAdornment={
228+
<InputAdornment position="end">
229+
<IconButton onClick={() => handleSearch(queryString)}>
230+
<SearchIcon />
231+
</IconButton>
232+
</InputAdornment>
233+
}
234+
/>
235+
</form>
236+
</Grid>
237+
<InfiniteScroll
238+
dataLength={currentResultCount}
239+
next={loadData}
240+
hasMore={resultsLoadedCount < totalResultCount}
241+
loader={<h4>Loading...</h4>}
242+
endMessage={<h4>end</h4>}
243+
scrollableTarget="scrollableDiv"
244+
>
245+
{currentSources?.map(({ name, url }) => (
246+
<MenuItem
247+
// replace because we want to keep the back button useful
248+
replace
249+
to={gimmeAUrl({}, `${url}concepts/`)}
250+
key={name}
251+
component={Link}
252+
onClick={handleSwitchSourceClose}
253+
>
254+
{url?.includes("/collection") ? (
255+
<FolderOpen className={classes.sourceIcon} />
256+
) : (
257+
<AccountTreeOutlined className={classes.sourceIcon} />
258+
)}
259+
{name}
260+
</MenuItem>
261+
))}
262+
</InfiniteScroll>
263+
</Menu>
264+
</>
265+
);
266+
};
267+
268+
export default ConceptSourceSwitcher;

0 commit comments

Comments
 (0)