1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-12-02 05:57:29 +01:00

add sort filter features to search (#322)

* sort and filter working added ui element for filter

* added UI for filter and sort

* made sort and filter work with the new UI

* filter and sort inline style

* updated styling and linters

* added sort direction

* apply sort and filter when starting a new search and sort order style update

* fixed sort by price

* change sort order selector
change direction when click on a sort option instead of clicking on the arrow that shows actual sort direction

* added default sort to owner and tag search

* refactor getSearchQuery method

* refactor sort methods

* fixed lint errors

* updated styling for sort and filter components

* fixed lint

* apply dynamic filter when sorting by liquidity

* interaction & alignment styling

* style tweaks, legacy search layout styles removal

* refactor sort component and fixed browsing all datasets
also aded default sort params when browsing all data sets

* fixed lint issues

Co-authored-by: Matthias Kretschmann <m@kretschmann.io>
This commit is contained in:
Bogdan Fazakas 2021-01-21 11:42:05 +02:00 committed by GitHub
parent 0cad79ae97
commit 83dca873f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 394 additions and 33 deletions

View File

@ -63,7 +63,7 @@ export default function Publisher({
) : ( ) : (
<> <>
<Link <Link
to={`/search/?owner=${account}`} to={`/search/?owner=${account}&sort=created&sortOrder=desc`}
title="Show all data sets created by this account." title="Show all data sets created by this account."
> >
{name} {name}

View File

@ -15,7 +15,11 @@ const Tag = ({ tag, noLinks }: { tag: string; noLinks?: boolean }) => {
return noLinks ? ( return noLinks ? (
<span className={styles.tag}>{tag}</span> <span className={styles.tag}>{tag}</span>
) : ( ) : (
<Link to={`/search?tags=${tag}`} className={styles.tag} title={tag}> <Link
to={`/search?tags=${tag}&sort=created&sortOrder=desc`}
className={styles.tag}
title={tag}
>
{tag} {tag}
</Link> </Link>
) )

View File

@ -4,6 +4,7 @@ import styles from './SearchBar.module.css'
import Button from '../atoms/Button' import Button from '../atoms/Button'
import Input from '../atoms/Input' import Input from '../atoms/Input'
import InputGroup from '../atoms/Input/InputGroup' import InputGroup from '../atoms/Input/InputGroup'
import { addExistingParamsToUrl } from '../templates/Search/utils'
export default function SearchBar({ export default function SearchBar({
placeholder, placeholder,
@ -23,10 +24,11 @@ export default function SearchBar({
setValue(e.target.value) setValue(e.target.value)
} }
function startSearch(e: FormEvent<HTMLButtonElement>) { async function startSearch(e: FormEvent<HTMLButtonElement>) {
e.preventDefault() e.preventDefault()
if (value === '') return if (value === '') return
navigate(`/search?text=${value}`) const url = await addExistingParamsToUrl(location, 'text')
navigate(`${url}&text=${value}`)
} }
return ( return (
@ -41,7 +43,11 @@ export default function SearchBar({
required required
size={size} size={size}
/> />
<Button onClick={(e: FormEvent<HTMLButtonElement>) => startSearch(e)}> <Button
onClick={async (e: FormEvent<HTMLButtonElement>) =>
await startSearch(e)
}
>
Search Search
</Button> </Button>
</InputGroup> </InputGroup>

View File

@ -112,7 +112,7 @@ export default function HomePage(): ReactElement {
title="New Data Sets" title="New Data Sets"
query={queryLatest} query={queryLatest}
action={ action={
<Button style="text" to="/search"> <Button style="text" to="/search?sort=created&sortOrder=desc">
All data sets All data sets
</Button> </Button>
} }

View File

@ -0,0 +1,33 @@
/* .filterList {
display: inline-flex;
float: left;
} */
.filter,
button.filter,
.filter:hover,
.filter:active,
.filter:focus {
border: 1px solid var(--border-color);
text-transform: uppercase;
border-radius: var(--border-radius);
margin-right: calc(var(--spacer) / 6);
color: var(--color-secondary);
background: var(--background-body);
/* the only thing not possible to overwrite button style="text" with more specifity of selectors, so sledgehammer */
padding: calc(var(--spacer) / 5) !important;
}
.filter:hover,
.filter:focus {
color: var(--font-color-text);
background: inherit;
transform: none;
}
.filter.selected {
color: var(--background-body);
background: var(--font-color-text);
border-color: var(--background-body);
}

View File

@ -0,0 +1,57 @@
import React, { ReactElement } from 'react'
import { useNavigate } from '@reach/router'
import styles from './filterPrice.module.css'
import classNames from 'classnames/bind'
import { addExistingParamsToUrl, FilterByPriceOptions } from './utils'
import Button from '../../atoms/Button'
const cx = classNames.bind(styles)
const filterItems = [
{ display: 'all', value: undefined },
{ display: 'fixed price', value: FilterByPriceOptions.Fixed },
{ display: 'dynamic price', value: FilterByPriceOptions.Dynamic }
]
export default function FilterPrice({
priceType,
setPriceType
}: {
priceType: string
setPriceType: React.Dispatch<React.SetStateAction<string>>
}): ReactElement {
const navigate = useNavigate()
async function applyFilter(filterBy: string) {
let urlLocation = await addExistingParamsToUrl(location, 'priceType')
if (filterBy) {
urlLocation = `${urlLocation}&priceType=${filterBy}`
}
setPriceType(filterBy)
navigate(urlLocation)
}
return (
<div>
{filterItems.map((e, index) => {
const filter = cx({
[styles.selected]: e.value === priceType,
[styles.filter]: true
})
return (
<Button
size="small"
style="text"
key={index}
className={filter}
onClick={async () => {
await applyFilter(e.value)
}}
>
{e.display}
</Button>
)
})}
</div>
)
}

View File

@ -1,21 +1,19 @@
.grid { .row {
display: grid; display: inline-flex;
flex-direction: column;
align-items: stretch;
justify-content: space-between;
width: 100%;
white-space: nowrap;
margin-bottom: calc(var(--spacer) / 2);
} }
@media (min-width: 55rem) { .row > div {
.grid { margin-bottom: calc(var(--spacer) / 2);
grid-column-gap: calc(var(--spacer) * 3); }
/* grid-template-columns: minmax(0, 3fr) 1fr; */
grid-template-areas:
'search'
'results';
}
.search { @media (min-width: 40rem) {
grid-area: search; .row {
} flex-direction: row;
.results {
grid-area: results;
} }
} }

View File

@ -4,7 +4,9 @@ import SearchBar from '../../molecules/SearchBar'
import AssetQueryList from '../../organisms/AssetQueryList' import AssetQueryList from '../../organisms/AssetQueryList'
import styles from './index.module.css' import styles from './index.module.css'
import queryString from 'query-string' import queryString from 'query-string'
import { getResults } from './utils' import PriceFilter from './filterPrice'
import Sort from './sort'
import { getResults, addExistingParamsToUrl } from './utils'
import Loader from '../../atoms/Loader' import Loader from '../../atoms/Loader'
import { useOcean } from '@oceanprotocol/react' import { useOcean } from '@oceanprotocol/react'
@ -17,9 +19,14 @@ export default function SearchPage({
}): ReactElement { }): ReactElement {
const { config } = useOcean() const { config } = useOcean()
const parsed = queryString.parse(location.search) const parsed = queryString.parse(location.search)
const { text, owner, tags, page } = parsed const { text, owner, tags, page, sort, sortOrder, priceType } = parsed
const [queryResult, setQueryResult] = useState<QueryResult>() const [queryResult, setQueryResult] = useState<QueryResult>()
const [loading, setLoading] = useState<boolean>() const [loading, setLoading] = useState<boolean>()
const [price, setPriceType] = useState<string>(priceType as string)
const [sortType, setSortType] = useState<string>(sort as string)
const [sortDirection, setSortDirection] = useState<string>(
sortOrder as string
)
useEffect(() => { useEffect(() => {
if (!config?.metadataCacheUri) return if (!config?.metadataCacheUri) return
@ -33,18 +40,37 @@ export default function SearchPage({
setLoading(false) setLoading(false)
} }
initSearch() initSearch()
}, [text, owner, tags, page, config.metadataCacheUri]) }, [
text,
owner,
tags,
page,
sort,
priceType,
sortOrder,
config.metadataCacheUri
])
return ( return (
<section className={styles.grid}> <>
<div className={styles.search}> <div className={styles.search}>
{(text || owner) && ( {(text || owner) && (
<SearchBar initialValue={(text || owner) as string} /> <SearchBar initialValue={(text || owner) as string} />
)} )}
<div className={styles.row}>
<PriceFilter priceType={price} setPriceType={setPriceType} />
<Sort
sortType={sortType}
sortDirection={sortDirection}
setSortType={setSortType}
setSortDirection={setSortDirection}
setPriceType={setPriceType}
/>
</div>
</div> </div>
<div className={styles.results}> <div className={styles.results}>
{loading ? <Loader /> : <AssetQueryList queryResult={queryResult} />} {loading ? <Loader /> : <AssetQueryList queryResult={queryResult} />}
</div> </div>
</section> </>
) )
} }

View File

@ -0,0 +1,48 @@
.sortList {
display: flex;
align-items: center;
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
background: var(--background-body);
}
.sortLabel {
composes: label from '../../atoms/Input/Label.module.css';
margin-bottom: 0;
margin-left: calc(var(--spacer) / 2);
margin-right: calc(var(--spacer) / 1.5);
text-transform: uppercase;
color: var(--color-secondary);
}
.sorted {
display: flex;
padding: calc(var(--spacer) / 6) calc(var(--spacer) / 2);
margin-right: calc(var(--spacer) / 4);
color: var(--color-secondary);
text-transform: capitalize;
border-radius: 0;
font-weight: var(--font-weight-base);
background: var(--background-body);
box-shadow: none;
}
.sorted:hover,
.sorted:focus,
.sorted.selected {
color: var(--font-color-text);
background: inherit;
transform: none;
box-shadow: none;
}
.direction {
display: flex;
background: transparent;
border: none;
color: inherit;
font-size: 0.75em;
outline: none;
margin-left: calc(var(--spacer) / 8);
margin-top: calc(var(--spacer) / 14);
}

View File

@ -0,0 +1,94 @@
import React, { ReactElement } from 'react'
import { useNavigate } from '@reach/router'
import {
addExistingParamsToUrl,
SortTermOptions,
SortValueOptions,
FilterByPriceOptions
} from './utils'
import Button from '../../atoms/Button'
import styles from './sort.module.css'
import classNames from 'classnames/bind'
const cx = classNames.bind(styles)
const sortItems = [
{ display: 'Published', value: SortTermOptions.Created },
{ display: 'Liquidity', value: SortTermOptions.Liquidity },
{ display: 'Price', value: SortTermOptions.Price }
]
export default function Sort({
sortType,
setSortType,
sortDirection,
setSortDirection,
setPriceType
}: {
sortType: string
setSortType: React.Dispatch<React.SetStateAction<string>>
sortDirection: string
setSortDirection: React.Dispatch<React.SetStateAction<string>>
setPriceType: React.Dispatch<React.SetStateAction<string>>
}): ReactElement {
const navigate = useNavigate()
const directionArrow = String.fromCharCode(
sortDirection === SortValueOptions.Ascending ? 9650 : 9660
)
async function sortResults(sortBy?: string, direction?: string) {
let urlLocation: string
if (sortBy) {
urlLocation = await addExistingParamsToUrl(location, 'sort', 'priceType')
urlLocation = `${urlLocation}&sort=${sortBy}`
if (sortBy === SortTermOptions.Liquidity) {
urlLocation = `${urlLocation}&priceType=${FilterByPriceOptions.Dynamic}`
setPriceType(FilterByPriceOptions.Dynamic)
} else {
setPriceType(undefined)
}
setSortType(sortBy)
} else if (direction) {
urlLocation = await addExistingParamsToUrl(location, 'sortOrder')
urlLocation = `${urlLocation}&sortOrder=${direction}`
setSortDirection(direction)
}
navigate(urlLocation)
}
function handleSortButtonClick(value: string) {
if (value === sortType) {
if (sortDirection === SortValueOptions.Descending) {
sortResults(null, SortValueOptions.Ascending)
} else {
sortResults(null, SortValueOptions.Descending)
}
} else {
sortResults(value, null)
}
}
return (
<div className={styles.sortList}>
<label className={styles.sortLabel}>Sort</label>
{sortItems.map((e, index) => {
const sorted = cx({
[styles.selected]: e.value === sortType,
[styles.sorted]: true
})
return (
<Button
key={index}
className={sorted}
size="small"
onClick={() => {
handleSortButtonClick(e.value)
}}
>
{e.display}
{e.value === sortType ? (
<span className={styles.direction}>{directionArrow}</span>
) : null}
</Button>
)
})}
</div>
)
}

View File

@ -3,6 +3,52 @@ import {
QueryResult QueryResult
} from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache' } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache'
import { MetadataCache, Logger } from '@oceanprotocol/lib' import { MetadataCache, Logger } from '@oceanprotocol/lib'
import queryString from 'query-string'
export const SortTermOptions = {
Liquidity: 'liquidity',
Price: 'price',
Created: 'created'
} as const
type SortTermOptions = typeof SortTermOptions[keyof typeof SortTermOptions]
export const SortElasticTerm = {
Liquidity: 'price.ocean',
Price: 'price.value',
Created: 'created'
} as const
type SortElasticTerm = typeof SortElasticTerm[keyof typeof SortElasticTerm]
export const SortValueOptions = {
Ascending: 'asc',
Descending: 'desc'
} as const
type SortValueOptions = typeof SortValueOptions[keyof typeof SortValueOptions]
export const FilterByPriceOptions = {
Fixed: 'exchange',
Dynamic: 'pool'
} as const
type FilterByPriceOptions = typeof FilterByPriceOptions[keyof typeof FilterByPriceOptions]
function addPriceFilterToQuerry(sortTerm: string, priceFilter: string): string {
sortTerm = priceFilter
? sortTerm === ''
? `price.type:${priceFilter}`
: `${sortTerm} AND price.type:${priceFilter}`
: sortTerm
return sortTerm
}
function getSortType(sortParam: string): string {
const sortTerm =
sortParam === SortTermOptions.Liquidity
? SortElasticTerm.Liquidity
: sortParam === SortTermOptions.Price
? SortElasticTerm.Price
: SortTermOptions.Created
return sortTerm
}
export function getSearchQuery( export function getSearchQuery(
text?: string, text?: string,
@ -10,9 +56,14 @@ export function getSearchQuery(
tags?: string, tags?: string,
categories?: string, categories?: string,
page?: string, page?: string,
offset?: string offset?: string,
sort?: string,
sortOrder?: string,
priceType?: string
): SearchQuery { ): SearchQuery {
const searchTerm = owner const sortTerm = getSortType(sort)
const sortValue = sortOrder === SortValueOptions.Ascending ? 1 : -1
let searchTerm = owner
? `(publicKey.owner:${owner})` ? `(publicKey.owner:${owner})`
: tags : tags
? // eslint-disable-next-line no-useless-escape ? // eslint-disable-next-line no-useless-escape
@ -21,6 +72,7 @@ export function getSearchQuery(
? // eslint-disable-next-line no-useless-escape ? // eslint-disable-next-line no-useless-escape
`(service.attributes.additionalInformation.categories:\"${categories}\")` `(service.attributes.additionalInformation.categories:\"${categories}\")`
: text || '' : text || ''
searchTerm = addPriceFilterToQuerry(searchTerm, priceType)
return { return {
page: Number(page) || 1, page: Number(page) || 1,
@ -35,7 +87,7 @@ export function getSearchQuery(
// ...(categories && { categories: [categories] }) // ...(categories && { categories: [categories] })
}, },
sort: { sort: {
created: -1 [sortTerm]: sortValue
} }
// Something in ocean.js is weird when using 'tags: [tag]' // Something in ocean.js is weird when using 'tags: [tag]'
@ -57,11 +109,23 @@ export async function getResults(
categories?: string categories?: string
page?: string page?: string
offset?: string offset?: string
sort?: string
sortOrder?: string
priceType?: string
}, },
metadataCacheUri: string metadataCacheUri: string
): Promise<QueryResult> { ): Promise<QueryResult> {
const { text, owner, tags, page, offset, categories } = params const {
text,
owner,
tags,
page,
offset,
categories,
sort,
sortOrder,
priceType
} = params
const metadataCache = new MetadataCache(metadataCacheUri, Logger) const metadataCache = new MetadataCache(metadataCacheUri, Logger)
const searchQuery = getSearchQuery( const searchQuery = getSearchQuery(
text, text,
@ -69,9 +133,40 @@ export async function getResults(
tags, tags,
categories, categories,
page, page,
offset offset,
sort,
sortOrder,
priceType
) )
const queryResult = await metadataCache.queryMetadata(searchQuery) const queryResult = await metadataCache.queryMetadata(searchQuery)
return queryResult return queryResult
} }
export async function addExistingParamsToUrl(
location: Location,
excludedParam: string,
secondExcludedParam?: string
): Promise<string> {
const parsed = queryString.parse(location.search)
let urlLocation = '/search?'
if (Object.keys(parsed).length > 0) {
for (const querryParam in parsed) {
if (
querryParam !== excludedParam &&
querryParam !== secondExcludedParam
) {
if (querryParam === 'page' && excludedParam === 'text') {
Logger.log('remove page when starting a new search')
} else {
const value = parsed[querryParam]
urlLocation = `${urlLocation}${querryParam}=${value}&`
}
}
}
} else {
urlLocation = `${urlLocation}sort=${SortTermOptions.Created}&sortOrder=${SortValueOptions.Descending}&`
}
urlLocation = urlLocation.slice(0, -1)
return urlLocation
}