mirror of
https://github.com/oceanprotocol/commons.git
synced 2023-03-15 18:03:00 +01:00
live filtering of search results
This commit is contained in:
parent
bab254e106
commit
e9dfd37e3d
2740
client/__fixtures__/search.json
Normal file
2740
client/__fixtures__/search.json
Normal file
File diff suppressed because one or more lines are too long
@ -1,16 +1,12 @@
|
||||
import searchMock from '../__fixtures__/search.json'
|
||||
|
||||
const oceanMock = {
|
||||
ocean: {
|
||||
accounts: {
|
||||
list: () => ['xxx', 'xxx']
|
||||
},
|
||||
aquarius: {
|
||||
queryMetadata: () => {
|
||||
return {
|
||||
results: [],
|
||||
totalResults: 1,
|
||||
totalPages: 1
|
||||
}
|
||||
}
|
||||
queryMetadata: () => searchMock
|
||||
},
|
||||
assets: {
|
||||
query: () => {
|
||||
|
@ -17,6 +17,8 @@ const AssetTeaser = ({
|
||||
list?: boolean
|
||||
minimal?: boolean
|
||||
}) => {
|
||||
if (!asset.findServiceByType) return null
|
||||
|
||||
const { attributes } = asset.findServiceByType('metadata')
|
||||
const { main, additionalInformation } = attributes
|
||||
|
||||
|
39
client/src/routes/Search/FilterItem.module.scss
Normal file
39
client/src/routes/Search/FilterItem.module.scss
Normal file
@ -0,0 +1,39 @@
|
||||
@import '../../styles/variables';
|
||||
|
||||
.item {
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.option {
|
||||
padding-top: $spacer / 10;
|
||||
padding-bottom: $spacer / 10;
|
||||
padding-right: $spacer / 6;
|
||||
text-align: left;
|
||||
color: $brand-grey-light;
|
||||
font-size: $font-size-small;
|
||||
transition: .1s ease-out;
|
||||
}
|
||||
|
||||
.active {
|
||||
composes: item;
|
||||
|
||||
.option {
|
||||
color: $brand-grey-dark;
|
||||
font-weight: $font-weight-bold;
|
||||
border-left: .3rem solid $brand-black;
|
||||
padding-left: $spacer / 4;
|
||||
}
|
||||
}
|
||||
|
||||
.cancel {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: $spacer / 10;
|
||||
font-size: $font-size-h3;
|
||||
color: inherit;
|
||||
line-height: 1;
|
||||
}
|
51
client/src/routes/Search/FilterItem.tsx
Normal file
51
client/src/routes/Search/FilterItem.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import React from 'react'
|
||||
import shortid from 'shortid'
|
||||
import Button from '../../components/atoms/Button'
|
||||
import styles from './FilterItem.module.scss'
|
||||
|
||||
export default function FilterItem({
|
||||
isActive,
|
||||
filter,
|
||||
filterByCategory,
|
||||
filterByLicense,
|
||||
option
|
||||
}: {
|
||||
isActive: boolean
|
||||
filter: any
|
||||
option: string
|
||||
filterByCategory(category: string): void
|
||||
filterByLicense(license: string): void
|
||||
}) {
|
||||
return (
|
||||
<li
|
||||
key={shortid.generate()}
|
||||
className={isActive ? styles.active : styles.item}
|
||||
>
|
||||
<Button
|
||||
link
|
||||
className={styles.option}
|
||||
onClick={() =>
|
||||
filter.label === 'Category'
|
||||
? filterByCategory(option)
|
||||
: filterByLicense(option)
|
||||
}
|
||||
>
|
||||
{option}{' '}
|
||||
</Button>
|
||||
{isActive && (
|
||||
<Button
|
||||
link
|
||||
className={styles.cancel}
|
||||
title="Clear"
|
||||
onClick={() =>
|
||||
filter.label === 'Category'
|
||||
? filterByCategory('')
|
||||
: filterByLicense('')
|
||||
}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
@ -3,43 +3,9 @@
|
||||
.filter {
|
||||
ul {
|
||||
padding-left: 0;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.option {
|
||||
padding-top: $spacer / 10;
|
||||
padding-bottom: $spacer / 10;
|
||||
padding-right: $spacer / 6;
|
||||
text-align: left;
|
||||
color: $brand-grey-light;
|
||||
font-size: $font-size-small;
|
||||
transition: .1s ease-out;
|
||||
|
||||
.active & {
|
||||
color: $brand-grey-dark;
|
||||
font-weight: $font-weight-bold;
|
||||
border-left: .3rem solid $brand-black;
|
||||
padding-left: $spacer / 4;
|
||||
}
|
||||
}
|
||||
|
||||
.cancel {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: $spacer / 10;
|
||||
font-size: $font-size-h3;
|
||||
color: inherit;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.filterTitle {
|
||||
font-size: $font-size-base;
|
||||
color: $brand-grey-light;
|
||||
|
22
client/src/routes/Search/Filters.test.tsx
Normal file
22
client/src/routes/Search/Filters.test.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import { User } from '../../context'
|
||||
import { userMockConnected } from '../../../__mocks__/user-mock'
|
||||
import Filters from './Filters'
|
||||
|
||||
describe('Filters', () => {
|
||||
it('renders without crashing', () => {
|
||||
const { debug, container } = render(
|
||||
<User.Provider value={userMockConnected}>
|
||||
<Filters
|
||||
category="Architecture"
|
||||
license="Public"
|
||||
results={[]}
|
||||
filterByCategory={() => null}
|
||||
filterByLicense={() => null}
|
||||
/>
|
||||
</User.Provider>
|
||||
)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
})
|
@ -1,8 +1,9 @@
|
||||
import React from 'react'
|
||||
import shortid from 'shortid'
|
||||
import Button from '../../components/atoms/Button'
|
||||
import styles from './Filters.module.scss'
|
||||
import data from '../../data/form-publish.json'
|
||||
import FilterItem from './FilterItem'
|
||||
import { DDO } from '@oceanprotocol/squid'
|
||||
|
||||
const { steps } = data
|
||||
|
||||
@ -19,10 +20,11 @@ const labelLicense =
|
||||
steps[2].fields.license.label
|
||||
|
||||
function getFilterMetadata(results: any[]) {
|
||||
const filterCategories: string[] = []
|
||||
const filterLicenses: string[] = []
|
||||
let filterCategories: string[] = []
|
||||
let filterLicenses: string[] = []
|
||||
|
||||
results.map(asset => {
|
||||
results.map((asset: DDO) => {
|
||||
if (!asset.findServiceByType) return null
|
||||
const { metadata } = asset.findServiceByType('Metadata')
|
||||
const { categories, license } = metadata.base
|
||||
categories && filterCategories.push(categories[0])
|
||||
@ -30,6 +32,10 @@ function getFilterMetadata(results: any[]) {
|
||||
return null
|
||||
})
|
||||
|
||||
// remove duplicates
|
||||
filterCategories = Array.from(new Set(filterCategories))
|
||||
filterLicenses = Array.from(new Set(filterLicenses))
|
||||
|
||||
return { filterCategories, filterLicenses }
|
||||
}
|
||||
|
||||
@ -37,14 +43,14 @@ export default function Filters({
|
||||
category,
|
||||
license,
|
||||
results,
|
||||
setCategory,
|
||||
setLicense
|
||||
filterByCategory,
|
||||
filterByLicense
|
||||
}: {
|
||||
category: string
|
||||
license: string
|
||||
results: any[]
|
||||
setCategory(category: string): void
|
||||
setLicense(license: string): void
|
||||
filterByCategory(category: string): void
|
||||
filterByLicense(license: string): void
|
||||
}) {
|
||||
const { filterCategories, filterLicenses } = getFilterMetadata(results)
|
||||
|
||||
@ -70,41 +76,13 @@ export default function Filters({
|
||||
license === option
|
||||
|
||||
return (
|
||||
<li
|
||||
key={shortid.generate()}
|
||||
className={
|
||||
isActive
|
||||
? styles.active
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Button
|
||||
link
|
||||
className={styles.option}
|
||||
onClick={() =>
|
||||
filter.label === 'Category'
|
||||
? setCategory(option)
|
||||
: setLicense(option)
|
||||
}
|
||||
>
|
||||
{option}{' '}
|
||||
</Button>
|
||||
{isActive && (
|
||||
<Button
|
||||
link
|
||||
className={styles.cancel}
|
||||
title="Clear"
|
||||
onClick={() =>
|
||||
filter.label ===
|
||||
'Category'
|
||||
? setCategory('')
|
||||
: setLicense('')
|
||||
}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
)}
|
||||
</li>
|
||||
<FilterItem
|
||||
isActive={isActive}
|
||||
filter={filter}
|
||||
filterByCategory={filterByCategory}
|
||||
filterByLicense={filterByLicense}
|
||||
option={option}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
|
32
client/src/routes/Search/Results.module.scss
Normal file
32
client/src/routes/Search/Results.module.scss
Normal file
@ -0,0 +1,32 @@
|
||||
@import '../../styles/variables';
|
||||
|
||||
.resultsTitle {
|
||||
color: $brand-grey-light;
|
||||
font-size: $font-size-h3;
|
||||
margin-top: 0;
|
||||
margin-bottom: $spacer;
|
||||
|
||||
span {
|
||||
color: $brand-grey-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.results {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-gap: $spacer;
|
||||
|
||||
@media (min-width: $break-point--medium) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media (min-width: $break-point--large) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
margin-top: $spacer * 4;
|
||||
color: $brand-grey-light;
|
||||
}
|
46
client/src/routes/Search/Results.tsx
Normal file
46
client/src/routes/Search/Results.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import shortid from 'shortid'
|
||||
import AssetTeaser from '../../components/molecules/AssetTeaser'
|
||||
import Pagination from '../../components/molecules/Pagination'
|
||||
import styles from './Results.module.scss'
|
||||
|
||||
export default function Results({
|
||||
title,
|
||||
results,
|
||||
totalResults,
|
||||
totalPages,
|
||||
currentPage,
|
||||
handlePageClick
|
||||
}: {
|
||||
title: string
|
||||
results: any[]
|
||||
totalResults: number
|
||||
totalPages: number
|
||||
currentPage: number
|
||||
handlePageClick(data: { selected: number }): Promise<any>
|
||||
}) {
|
||||
return results && results.length ? (
|
||||
<>
|
||||
<h2 className={styles.resultsTitle}>
|
||||
{totalResults} results for <span>{title}</span>
|
||||
</h2>
|
||||
<div className={styles.results}>
|
||||
{results.map((asset: any) => (
|
||||
<AssetTeaser key={shortid.generate()} asset={asset} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
handlePageClick={handlePageClick}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.empty}>
|
||||
<p>No Data Sets Found.</p>
|
||||
<Link to="/publish">+ Publish A Data Set</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -9,16 +9,16 @@ export default function Sidebar({
|
||||
category,
|
||||
license,
|
||||
results,
|
||||
setCategory,
|
||||
setLicense
|
||||
filterByCategory,
|
||||
filterByLicense
|
||||
}: {
|
||||
search: string
|
||||
inputChange: any
|
||||
category: string
|
||||
license: string
|
||||
results: any[]
|
||||
setCategory(category: string): void
|
||||
setLicense(license: string): void
|
||||
filterByCategory(category: string): void
|
||||
filterByLicense(license: string): void
|
||||
}) {
|
||||
return (
|
||||
<aside className={styles.sidebar}>
|
||||
@ -40,8 +40,8 @@ export default function Sidebar({
|
||||
category={category}
|
||||
license={license}
|
||||
results={results}
|
||||
setCategory={setCategory}
|
||||
setLicense={setLicense}
|
||||
filterByCategory={filterByCategory}
|
||||
filterByLicense={filterByLicense}
|
||||
/>
|
||||
</aside>
|
||||
)
|
||||
|
@ -9,34 +9,3 @@
|
||||
grid-template-columns: 1fr 3fr;
|
||||
}
|
||||
}
|
||||
|
||||
.resultsTitle {
|
||||
color: $brand-grey-light;
|
||||
font-size: $font-size-h3;
|
||||
margin-top: 0;
|
||||
margin-bottom: $spacer;
|
||||
|
||||
span {
|
||||
color: $brand-grey-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.results {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-gap: $spacer;
|
||||
|
||||
@media (min-width: $break-point--medium) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media (min-width: $break-point--large) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
margin-top: $spacer * 4;
|
||||
color: $brand-grey-light;
|
||||
}
|
||||
|
@ -1,18 +1,15 @@
|
||||
import React, { PureComponent, ChangeEvent } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import queryString from 'query-string'
|
||||
import { History, Location } from 'history'
|
||||
import shortid from 'shortid'
|
||||
import { Logger } from '@oceanprotocol/squid'
|
||||
import { Logger, DDO } from '@oceanprotocol/squid'
|
||||
import Spinner from '../../components/atoms/Spinner'
|
||||
import Route from '../../components/templates/Route'
|
||||
import { User } from '../../context'
|
||||
import AssetTeaser from '../../components/molecules/AssetTeaser'
|
||||
import Pagination from '../../components/molecules/Pagination'
|
||||
import styles from './index.module.scss'
|
||||
import Content from '../../components/atoms/Content'
|
||||
import withTracker from '../../hoc/withTracker'
|
||||
import Sidebar from './Sidebar'
|
||||
import Results from './Results'
|
||||
import styles from './index.module.scss'
|
||||
|
||||
interface SearchProps {
|
||||
location: Location
|
||||
@ -21,6 +18,7 @@ interface SearchProps {
|
||||
|
||||
interface SearchState {
|
||||
results: any[]
|
||||
resultsFiltered: any[]
|
||||
totalResults: number
|
||||
offset: number
|
||||
totalPages: number
|
||||
@ -38,6 +36,7 @@ class Search extends PureComponent<SearchProps, SearchState> {
|
||||
|
||||
public state = {
|
||||
results: [],
|
||||
resultsFiltered: [],
|
||||
totalResults: 0,
|
||||
offset: 25,
|
||||
totalPages: 1,
|
||||
@ -112,6 +111,7 @@ class Search extends PureComponent<SearchProps, SearchState> {
|
||||
const search = await ocean.assets.query(searchQuery)
|
||||
this.setState({
|
||||
results: search.results,
|
||||
resultsFiltered: search.results,
|
||||
totalResults: search.totalResults,
|
||||
totalPages: search.totalPages,
|
||||
isLoading: false
|
||||
@ -161,32 +161,37 @@ class Search extends PureComponent<SearchProps, SearchState> {
|
||||
this.setState({ category, isLoading: true }, () => this.searchAssets())
|
||||
}
|
||||
|
||||
public filterByCategory = (category: string) => {
|
||||
const resultsFiltered: any[] = this.state.results.filter(
|
||||
(asset: DDO) => {
|
||||
const { metadata } = asset.findServiceByType('Metadata')
|
||||
const { categories } = metadata.base
|
||||
if (!categories) return true
|
||||
|
||||
return category === '' ? true : categories.includes(category)
|
||||
}
|
||||
)
|
||||
|
||||
this.setState({ resultsFiltered, category })
|
||||
}
|
||||
|
||||
public filterByLicense = (name: string) => {
|
||||
const resultsFiltered: any[] = this.state.results.filter(
|
||||
(asset: any) => {
|
||||
const { metadata } = asset.findServiceByType('Metadata')
|
||||
const { license } = metadata.base
|
||||
|
||||
return name === '' ? true : license === name
|
||||
}
|
||||
)
|
||||
|
||||
this.setState({ resultsFiltered, license: name })
|
||||
}
|
||||
|
||||
public setLicense = (license: string) => {
|
||||
this.setState({ license, isLoading: true }, () => this.searchAssets())
|
||||
}
|
||||
|
||||
public renderResults = () =>
|
||||
this.state.results && this.state.results.length ? (
|
||||
<>
|
||||
<div className={styles.results}>
|
||||
{this.state.results.map((asset: any) => (
|
||||
<AssetTeaser key={shortid.generate()} asset={asset} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
totalPages={this.state.totalPages}
|
||||
currentPage={this.state.currentPage}
|
||||
handlePageClick={this.handlePageClick}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.empty}>
|
||||
<p>No Data Sets Found.</p>
|
||||
<Link to="/publish">+ Publish A Data Set</Link>
|
||||
</div>
|
||||
)
|
||||
|
||||
public render() {
|
||||
const {
|
||||
isLoading,
|
||||
@ -194,7 +199,10 @@ class Search extends PureComponent<SearchProps, SearchState> {
|
||||
totalResults,
|
||||
search,
|
||||
category,
|
||||
license
|
||||
license,
|
||||
resultsFiltered,
|
||||
totalPages,
|
||||
currentPage
|
||||
} = this.state
|
||||
|
||||
return (
|
||||
@ -207,26 +215,24 @@ class Search extends PureComponent<SearchProps, SearchState> {
|
||||
category={category}
|
||||
results={results}
|
||||
license={license}
|
||||
setCategory={this.setCategory}
|
||||
setLicense={this.setLicense}
|
||||
filterByCategory={this.filterByCategory}
|
||||
filterByLicense={this.filterByLicense}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{isLoading ? (
|
||||
<Spinner message="Searching..." />
|
||||
) : (
|
||||
<>
|
||||
<h2 className={styles.resultsTitle}>
|
||||
{totalResults} results for{' '}
|
||||
<span>
|
||||
{decodeURIComponent(
|
||||
search || category
|
||||
)}
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
{this.renderResults()}
|
||||
</>
|
||||
<Results
|
||||
title={decodeURIComponent(
|
||||
search || category
|
||||
)}
|
||||
results={resultsFiltered}
|
||||
totalResults={totalResults}
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
handlePageClick={this.handlePageClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user