Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
ensembl-web
ensembl-client
Commits
7214c8c0
Unverified
Commit
7214c8c0
authored
Jul 09, 2021
by
Andrey Azov
Committed by
GitHub
Jul 09, 2021
Browse files
Add in-app search (#521)
parent
cc56383d
Pipeline
#173763
passed with stages
in 4 minutes and 42 seconds
Changes
19
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
1101 additions
and
64 deletions
+1101
-64
src/ensembl/src/content/app/browser/interstitial/BrowserInterstitial.scss
...content/app/browser/interstitial/BrowserInterstitial.scss
+2
-7
src/ensembl/src/content/app/browser/interstitial/BrowserInterstitial.tsx
.../content/app/browser/interstitial/BrowserInterstitial.tsx
+5
-1
src/ensembl/src/content/app/browser/track-panel/track-panel-bar/TrackPanelBar.tsx
...app/browser/track-panel/track-panel-bar/TrackPanelBar.tsx
+16
-3
src/ensembl/src/content/app/browser/track-panel/track-panel-modal/TrackPanelModal.scss
...rowser/track-panel/track-panel-modal/TrackPanelModal.scss
+1
-1
src/ensembl/src/content/app/browser/track-panel/track-panel-modal/modal-views/TrackPanelSearch.tsx
...-panel/track-panel-modal/modal-views/TrackPanelSearch.tsx
+16
-2
src/ensembl/src/content/app/entity-viewer/interstitial/EntityViewerInterstitial.scss
.../entity-viewer/interstitial/EntityViewerInterstitial.scss
+2
-7
src/ensembl/src/content/app/entity-viewer/interstitial/EntityViewerInterstitial.tsx
...p/entity-viewer/interstitial/EntityViewerInterstitial.tsx
+5
-1
src/ensembl/src/content/app/entity-viewer/shared/components/entity-viewer-sidebar/entity-viewer-sidebar-modal/EntityViewerSidebarModal.scss
...entity-viewer-sidebar-modal/EntityViewerSidebarModal.scss
+1
-0
src/ensembl/src/content/app/entity-viewer/shared/components/entity-viewer-sidebar/entity-viewer-sidebar-modal/modal-views/EntityViewerSearch.tsx
...y-viewer-sidebar-modal/modal-views/EntityViewerSearch.tsx
+23
-7
src/ensembl/src/content/app/entity-viewer/shared/components/entity-viewer-sidebar/entity-viewer-sidebar-toolstrip/EntityViewerSidebarToolstrip.tsx
...viewer-sidebar-toolstrip/EntityViewerSidebarToolstrip.tsx
+2
-2
src/ensembl/src/root/rootReducer.ts
src/ensembl/src/root/rootReducer.ts
+2
-0
src/ensembl/src/shared/components/button/Button.tsx
src/ensembl/src/shared/components/button/Button.tsx
+1
-0
src/ensembl/src/shared/components/in-app-search/InAppSearch.scss
...embl/src/shared/components/in-app-search/InAppSearch.scss
+117
-6
src/ensembl/src/shared/components/in-app-search/InAppSearch.test.tsx
.../src/shared/components/in-app-search/InAppSearch.test.tsx
+182
-0
src/ensembl/src/shared/components/in-app-search/InAppSearch.tsx
...sembl/src/shared/components/in-app-search/InAppSearch.tsx
+103
-27
src/ensembl/src/shared/components/in-app-search/InAppSearchMatches.tsx
...rc/shared/components/in-app-search/InAppSearchMatches.tsx
+170
-0
src/ensembl/src/shared/components/in-app-search/test/response-fixture.ts
.../shared/components/in-app-search/test/response-fixture.ts
+258
-0
src/ensembl/src/shared/state/in-app-search/inAppSearchSelectors.ts
...bl/src/shared/state/in-app-search/inAppSearchSelectors.ts
+34
-0
src/ensembl/src/shared/state/in-app-search/inAppSearchSlice.ts
...nsembl/src/shared/state/in-app-search/inAppSearchSlice.ts
+161
-0
No files found.
src/ensembl/src/content/app/browser/interstitial/BrowserInterstitial.scss
View file @
7214c8c0
...
...
@@ -4,17 +4,12 @@
// We should extract it in a component (or at least reuse the same CSS classes)
.topPanel
{
background-color
:
$light-grey
;
height
:
235px
;
padding
:
65px
40px
;
min-
height
:
235px
;
padding
:
40px
;
padding-left
:
$global-padding-left
;
box-shadow
:
0
3px
5px
$global-box-shadow
;
}
.searchField
{
max-width
:
485px
;
height
:
30px
;
}
.exampleLinks
{
display
:
flex
;
flex-direction
:
column
;
...
...
src/ensembl/src/content/app/browser/interstitial/BrowserInterstitial.tsx
View file @
7214c8c0
...
...
@@ -42,7 +42,11 @@ const BrowserInterstitial = () => {
return
(
<
div
>
<
div
className
=
{
styles
.
topPanel
}
>
<
InAppSearch
className
=
{
styles
.
searchField
}
/>
<
InAppSearch
app
=
"genomeBrowser"
genomeId
=
{
activeGenomeId
}
mode
=
"interstitial"
/>
</
div
>
<
ExampleLinks
/>
</
div
>
...
...
src/ensembl/src/content/app/browser/track-panel/track-panel-bar/TrackPanelBar.tsx
View file @
7214c8c0
...
...
@@ -21,13 +21,16 @@ import {
getIsTrackPanelOpened
,
getTrackPanelModalView
}
from
'
../trackPanelSelectors
'
;
import
{
getIsDrawerOpened
}
from
'
src/content/app/browser/drawer/drawerSelectors
'
;
import
{
getBrowserActiveGenomeId
}
from
'
src/content/app/browser/browserSelectors
'
;
import
{
toggleTrackPanel
,
closeTrackPanelModal
,
openTrackPanelModal
}
from
'
../trackPanelActions
'
;
import
{
toggleDrawer
}
from
'
src/content/app/browser/drawer/drawerActions
'
;
import
{
getIsDrawerOpened
}
from
'
src/content/app/browser/drawer/drawerSelectors
'
;
import
{
clearSearch
}
from
'
src/shared/state/in-app-search/inAppSearchSlice
'
;
import
ImageButton
from
'
src/shared/components/image-button/ImageButton
'
;
...
...
@@ -43,6 +46,7 @@ import { Status } from 'src/shared/types/status';
import
styles
from
'
src/shared/components/layout/StandardAppLayout.scss
'
;
export
const
TrackPanelBar
=
()
=>
{
const
activeGenomeId
=
useSelector
(
getBrowserActiveGenomeId
);
const
isTrackPanelOpened
=
useSelector
(
getIsTrackPanelOpened
);
const
trackPanelModalView
=
useSelector
(
getTrackPanelModalView
);
const
isDrawerOpened
=
useSelector
(
getIsDrawerOpened
);
...
...
@@ -57,6 +61,15 @@ export const TrackPanelBar = () => {
dispatch
(
toggleDrawer
(
false
));
}
if
(
selectedItem
===
'
search
'
)
{
dispatch
(
clearSearch
({
app
:
'
genomeBrowser
'
,
genomeId
:
activeGenomeId
as
string
})
);
}
if
(
selectedItem
===
trackPanelModalView
)
{
dispatch
(
closeTrackPanelModal
());
}
else
{
...
...
@@ -74,8 +87,8 @@ export const TrackPanelBar = () => {
<>
<
div
className
=
{
styles
.
sidebarIcon
}
key
=
"search"
>
<
ImageButton
status
=
{
Status
.
DISABLED
}
description
=
"
Track s
earch"
status
=
{
getViewIconStatus
(
'
search
'
)
}
description
=
"
S
earch"
onClick
=
{
()
=>
toggleModalView
(
'
search
'
)
}
image
=
{
searchIcon
}
/>
...
...
src/ensembl/src/content/app/browser/track-panel/track-panel-modal/TrackPanelModal.scss
View file @
7214c8c0
...
...
@@ -2,9 +2,9 @@
.trackPanelModal
{
background
:
$white
;
font-weight
:
$light
;
position
:
relative
;
overflow
:
auto
;
height
:
100%
;
h3
{
font-size
:
14px
;
...
...
src/ensembl/src/content/app/browser/track-panel/track-panel-modal/modal-views/TrackPanelSearch.tsx
View file @
7214c8c0
...
...
@@ -15,13 +15,27 @@
*/
import
React
from
'
react
'
;
import
{
useSelector
}
from
'
react-redux
'
;
import
{
getBrowserActiveGenomeId
}
from
'
src/content/app/browser/browserSelectors
'
;
import
InAppSearch
from
'
src/shared/components/in-app-search/InAppSearch
'
;
const
TrackPanelSearch
=
()
=>
{
const
activeGenomeId
=
useSelector
(
getBrowserActiveGenomeId
);
return
(
<
section
className
=
"trackPanelSearch"
>
<
h3
>
Search
</
h3
>
<
p
>
Quickly find the tracks you want to show or hide in the browser
</
p
>
<
p
>
Not ready yet
…
</
p
>
<
div
>
{
activeGenomeId
&&
(
<
InAppSearch
app
=
"genomeBrowser"
genomeId
=
{
activeGenomeId
}
mode
=
"sidebar"
/>
)
}
</
div
>
</
section
>
);
};
...
...
src/ensembl/src/content/app/entity-viewer/interstitial/EntityViewerInterstitial.scss
View file @
7214c8c0
...
...
@@ -4,13 +4,8 @@
// We should extract it in a component (or at least reuse the same CSS classes)
.topPanel
{
background-color
:
$light-grey
;
height
:
235px
;
padding
:
65px
40px
;
min-
height
:
235px
;
padding
:
40px
;
padding-left
:
$global-padding-left
;
box-shadow
:
0
3px
5px
$global-box-shadow
;
}
.searchField
{
max-width
:
485px
;
height
:
30px
;
}
src/ensembl/src/content/app/entity-viewer/interstitial/EntityViewerInterstitial.tsx
View file @
7214c8c0
...
...
@@ -35,7 +35,11 @@ const EntityViewerInterstitial = () => {
return
(
<
div
>
<
div
className
=
{
styles
.
topPanel
}
>
<
InAppSearch
className
=
{
styles
.
searchField
}
/>
<
InAppSearch
app
=
"entityViewer"
mode
=
"interstitial"
genomeId
=
{
activeGenomeId
}
/>
</
div
>
<
ExampleLinks
/>
</
div
>
...
...
src/ensembl/src/content/app/entity-viewer/shared/components/entity-viewer-sidebar/entity-viewer-sidebar-modal/EntityViewerSidebarModal.scss
View file @
7214c8c0
...
...
@@ -2,6 +2,7 @@
.entityViewerSidebarModal
{
position
:
relative
;
height
:
100%
;
overflow
:
auto
;
h3
{
...
...
src/ensembl/src/content/app/entity-viewer/shared/components/entity-viewer-sidebar/entity-viewer-sidebar-modal/modal-views/EntityViewerSearch.tsx
View file @
7214c8c0
...
...
@@ -15,13 +15,29 @@
*/
import
React
from
'
react
'
;
import
{
useSelector
}
from
'
react-redux
'
;
const
EntityViewerSidebarSearch
=
()
=>
(
<
section
>
<
h3
>
Search
</
h3
>
<
p
>
Quickly search in entity viewer
</
p
>
<
p
>
Not ready yet
…
</
p
>
</
section
>
);
import
{
getEntityViewerActiveGenomeId
}
from
'
src/content/app/entity-viewer/state/general/entityViewerGeneralSelectors
'
;
import
InAppSearch
from
'
src/shared/components/in-app-search/InAppSearch
'
;
const
EntityViewerSidebarSearch
=
()
=>
{
const
activeGenomeId
=
useSelector
(
getEntityViewerActiveGenomeId
);
return
(
<
section
>
<
h3
>
Search
</
h3
>
<
div
>
{
activeGenomeId
&&
(
<
InAppSearch
app
=
"entityViewer"
genomeId
=
{
activeGenomeId
}
mode
=
"sidebar"
/>
)
}
</
div
>
</
section
>
);
};
export
default
EntityViewerSidebarSearch
;
src/ensembl/src/content/app/entity-viewer/shared/components/entity-viewer-sidebar/entity-viewer-sidebar-toolstrip/EntityViewerSidebarToolstrip.tsx
View file @
7214c8c0
...
...
@@ -66,10 +66,10 @@ export const EntityViewerSidebarToolstrip = () => {
return
(
<>
<
ImageButton
status
=
{
Status
.
DISABLED
}
status
=
{
getViewIconStatus
(
SidebarModalView
.
SEARCH
)
}
description
=
"Search"
className
=
{
styles
.
sidebarIcon
}
onClick
=
{
noop
}
onClick
=
{
()
=>
toggleModalView
(
SidebarModalView
.
SEARCH
)
}
image
=
{
searchIcon
}
/>
<
ImageButton
...
...
src/ensembl/src/root/rootReducer.ts
View file @
7214c8c0
...
...
@@ -24,6 +24,7 @@ import customDownload from '../content/app/custom-download/state/customDownloadR
import
global
from
'
../global/globalReducer
'
;
import
header
from
'
../header/headerReducer
'
;
import
ensObjects
from
'
../shared/state/ens-object/ensObjectReducer
'
;
import
inAppSearch
from
'
../shared/state/in-app-search/inAppSearchSlice
'
;
import
speciesSelector
from
'
../content/app/species-selector/state/speciesSelectorReducer
'
;
import
entityViewer
from
'
src/content/app/entity-viewer/state/entityViewerReducer
'
;
import
speciesPage
from
'
src/content/app/species/state/index
'
;
...
...
@@ -34,6 +35,7 @@ const createRootReducer = (history: any) =>
drawer
,
customDownload
,
ensObjects
,
inAppSearch
,
genome
,
global
,
header
,
...
...
src/ensembl/src/shared/components/button/Button.tsx
View file @
7214c8c0
...
...
@@ -53,6 +53,7 @@ const Button = (props: Props) => {
<
button
className
=
{
classNames
(
styles
.
button
,
props
.
className
)
}
onClick
=
{
handleClick
}
disabled
=
{
props
.
isDisabled
}
>
{
props
.
children
}
</
button
>
...
...
src/ensembl/src/shared/components/in-app-search/InAppSearch.scss
View file @
7214c8c0
@import
'src/styles/common'
;
.inAppSearchTopInterstitial
{
display
:
inline-grid
;
grid-template-areas
:
'label label'
'search-field search-button'
'hits-count .'
;
grid-template-columns
:
485px
auto
;
column-gap
:
48px
;
}
.inAppSearchTopSidebar
{
display
:
grid
;
grid-template-areas
:
'label label'
'search-field search-field'
'hits-count search-button'
;
grid-template-columns
:
1fr
min-content
;
align-items
:
top
;
}
.searchFieldWrapper
{
grid-area
:
search-field
;
padding
:
4px
;
box-shadow
:
inset
2px
2px
4px
-2px
$dark-grey
;
background
:
white
;
&
Interstitial
{
height
:
30px
;
}
// nesting to increase specificity of the selector
.searchField
{
border
:
none
;
...
...
@@ -13,11 +38,97 @@
}
}
// TODO: remove this temporary class when in-app search becomes functional
.fauxSearchField
{
color
:
$grey
;
height
:
36px
;
box-shadow
:
inset
1px
1px
3px
0
$dark-grey
;
.label
{
grid-area
:
label
;
color
:
$dark-grey
;
margin-bottom
:
15px
;
}
.searchButton
{
grid-area
:
search-button
;
}
.inAppSearchTopSidebar
.searchButton
{
margin-top
:
18px
;
}
.hitsCount
{
grid-area
:
hits-count
;
padding
:
20px
0
0
20px
;
.hitsNumber
{
font-weight
:
$bold
;
}
}
.searchMatches
{
display
:
flex
;
padding-left
:
20px
;
flex-direction
:
column
;
align-items
:
flex-start
;
margin-top
:
30px
;
}
.searchMatch
{
position
:
relative
;
display
:
inline-block
;
color
:
$blue
;
cursor
:
pointer
;
line-height
:
1
;
&
:not
(
:last-child
)
{
margin-bottom
:
1rem
;
}
&
>
span
:nth-child
(
2
)
{
margin-left
:
0
.6rem
;
}
.searchMatchAnchor
{
position
:
absolute
;
height
:
100%
;
&
Interstitial
{
right
:
-1
.5ch
;
}
&
Sidebar
{
left
:
3ch
;
}
}
}
.tooltip
{
background
:
$black
;
padding
:
12px
20px
;
width
:
485px
;
filter
:
drop-shadow
(
2px
2px
3px
$shadow-color
);
color
:
$white
;
}
.tooltipTip
{
fill
:
$black
;
}
.tooltipContent
{
font-weight
:
$light
;
&
>
div
:first-child
span
:first-child
{
margin-right
:
28px
;
}
&
>
div
:nth-child
(
3
)
span
:first-child
{
font-weight
:
$bold
;
}
&
>
div
:not
(
:last-child
)
{
margin-bottom
:
6px
;
}
&
>
div
:last-child
{
margin-top
:
28px
;
}
.transcriptsCount
{
margin-right
:
1ch
;
}
}
src/ensembl/src/shared/components/in-app-search/InAppSearch.test.tsx
0 → 100644
View file @
7214c8c0
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import
React
from
'
react
'
;
import
{
configureStore
}
from
'
@reduxjs/toolkit
'
;
import
{
Provider
}
from
'
react-redux
'
;
import
{
render
,
waitFor
}
from
'
@testing-library/react
'
;
import
userEvent
from
'
@testing-library/user-event
'
;
import
apiService
from
'
src/services/api-service
'
;
import
*
as
inAppSearchSlice
from
'
src/shared/state/in-app-search/inAppSearchSlice
'
;
import
inAppSearchReducer
from
'
src/shared/state/in-app-search/inAppSearchSlice
'
;
import
InAppSearch
,
{
Props
as
InAppSearchProps
}
from
'
./InAppSearch
'
;
import
{
brca2SearchResults
}
from
'
./test/response-fixture
'
;
const
rootReducer
=
{
inAppSearch
:
inAppSearchReducer
};
const
getStore
=
(
initialState
=
{})
=>
{
return
configureStore
({
reducer
:
rootReducer
,
devTools
:
false
,
preloadedState
:
initialState
});
};
const
defaultProps
:
InAppSearchProps
=
{
app
:
'
genomeBrowser
'
,
genomeId
:
'
human
'
,
mode
:
'
interstitial
'
};
describe
(
'
<InAppSearch />
'
,
()
=>
{
afterEach
(()
=>
{
jest
.
clearAllMocks
();
});
describe
(
'
initial rendering
'
,
()
=>
{
it
(
'
renders correctly before the request
'
,
()
=>
{
const
{
container
,
queryByText
}
=
render
(
<
Provider
store
=
{
getStore
()
}
>
<
InAppSearch
{
...
defaultProps
}
/>
</
Provider
>
);
const
searchField
=
container
.
querySelector
(
'
.searchField
'
);
expect
(
searchField
).
toBeTruthy
();
const
label
=
queryByText
(
'
Find a gene in this species
'
);
expect
(
label
).
toBeTruthy
();
const
button
=
container
.
querySelector
(
'
button
'
);
expect
(
button
).
toBeTruthy
();
expect
(
button
?.
getAttribute
(
'
disabled
'
)).
not
.
toBe
(
null
);
});
it
(
'
uses the mode property correctly
'
,
()
=>
{
const
{
rerender
,
queryByTestId
}
=
render
(
<
Provider
store
=
{
getStore
()
}
>
<
InAppSearch
{
...
defaultProps
}
/>
</
Provider
>
);
let
inAppSearchTop
=
queryByTestId
(
'
in-app search top
'
)
as
HTMLElement
;
expect
(
inAppSearchTop
.
classList
.
contains
(
'
inAppSearchTopInterstitial
'
)
).
toBe
(
true
);
const
sidebarProps
=
{
...
defaultProps
,
mode
:
'
sidebar
'
as
const
};
rerender
(
<
Provider
store
=
{
getStore
()
}
>
<
InAppSearch
{
...
sidebarProps
}
/>
</
Provider
>
);
inAppSearchTop
=
queryByTestId
(
'
in-app search top
'
)
as
HTMLElement
;
expect
(
inAppSearchTop
.
classList
.
contains
(
'
inAppSearchTopSidebar
'
)).
toBe
(
true
);
});
});
describe
(
'
search
'
,
()
=>
{
beforeAll
(()
=>
{
jest
.
spyOn
(
apiService
,
'
fetch
'
)
.
mockImplementation
(()
=>
Promise
.
resolve
(
brca2SearchResults
));
});
it
(
'
handles query submission
'
,
()
=>
{
// check that correct arguments are passed to the search function
jest
.
spyOn
(
inAppSearchSlice
,
'
search
'
)
.
mockImplementation
(()
=>
({
type
:
'
action
'
}
as
any
));
const
{
container
,
rerender
}
=
render
(
<
Provider
store
=
{
getStore
()
}
>
<
InAppSearch
{
...
defaultProps
}
/>
</
Provider
>
);
const
searchField
=
container
.
querySelector
(
'
.searchField input
'
)
as
HTMLInputElement
;
userEvent
.
type
(
searchField
,
'
BRCA2{enter}
'
);
const
[
search1Args
]
=
(
inAppSearchSlice
.
search
as
any
).
mock
.
calls
[
0
];
expect
(
search1Args
).
toEqual
({
app
:
defaultProps
.
app
,
genome_id
:
defaultProps
.
genomeId
,
query
:
'
BRCA2
'
,
page
:
1
,
per_page
:
50
});
// let's try passing a different app name and a different genome id in props
rerender
(
<
Provider
store
=
{
getStore
()
}
>
<
InAppSearch
{
...
defaultProps
}
app
=
"entityViewer"
genomeId
=
"wheat"
/>
</
Provider
>
);
userEvent
.
clear
(
searchField
);
userEvent
.
type
(
searchField
,
'
Traes
'
);
// also, let's try to submit the search by pressing on the button
const
submitButton
=
container
.
querySelector
(
'
button
'
)
as
HTMLElement
;
userEvent
.
click
(
submitButton
);
const
[
search2Args
]
=
(
inAppSearchSlice
.
search
as
any
).
mock
.
calls
[
1
];
expect
(
search2Args
).
toEqual
({
app
:
'
entityViewer
'
,
genome_id
:
'
wheat
'
,
query
:
'
Traes
'
,
page
:
1
,
per_page
:
50
});
(
inAppSearchSlice
.
search
as
any
).
mockRestore
();
});
it
(
'
displays search results
'
,
async
()
=>
{
const
{
container
}
=
render
(
<
Provider
store
=
{
getStore
()
}
>
<
InAppSearch
{
...
defaultProps
}
/>
</
Provider
>
);
const
searchField
=
container
.
querySelector
(
'
.searchField input
'
)
as
HTMLInputElement
;
userEvent
.
type
(
searchField
,
'
BRCA2{enter}
'
);
await
waitFor
(()
=>
{
const
hitsCount
=
container
.
querySelector
(
'
.hitsCount
'
);
expect
(
hitsCount
).
toBeTruthy
();
});
// now we can test the results in the DOM
expect
(
container
.
querySelector
(
'
.hitsCount
'
)?.
textContent
).
toBe
(
'
12 genes
'
);
// as defined in the fixture
expect
(
container
.
querySelectorAll
(
'
.searchMatch
'
).
length
).
toBe
(
10
);
// as defined in the fixture; 10 matches per page
});
});
});
src/ensembl/src/shared/components/in-app-search/InAppSearch.tsx
View file @
7214c8c0
...
...
@@ -14,54 +14,130 @@
* limitations under the License.
*/