mirror of
https://github.com/oceanprotocol/market.git
synced 2024-12-02 05:57:29 +01:00
Merge branch 'main' into feature/enforce-docker-containers
This commit is contained in:
commit
b505bddb4b
10
README.md
10
README.md
@ -86,7 +86,7 @@ npm start
|
||||
|
||||
To use the app together with MetaMask, importing one of the accounts auto-generated by the Ganache container is the easiest way to have test ETH available. All of them have 100 ETH by default. Upon start, the `ocean_ganache_1` container will print out the private keys of multiple accounts in its logs. Pick one of them and import into MetaMask.
|
||||
|
||||
To fully test all [The Graph](https://thegraph.com) integrations, you have to run your own local Graph node with our [`ocean-subgraph`](https://github.com/oceanprotocol/ocean-subgraph) deployed to it. Barge does not include a local subgraph so by default, the `subgraphUri` is hardcoded to the Rinkeby subgraph in our [`getDevelopmentConfig` function](https://github.com/oceanprotocol/market/blob/d0b1534d105e5dcb3790c65d4bb04ff1d2dbc575/src/utils/ocean.ts#L31).
|
||||
To fully test all [The Graph](https://thegraph.com) integrations, you have to run your own local Graph node with our [`ocean-subgraph`](https://github.com/oceanprotocol/ocean-subgraph) deployed to it. Barge does not include a local subgraph so by default, the `subgraphUri` is hardcoded to the Goerli subgraph in our [`getDevelopmentConfig` function](https://github.com/oceanprotocol/market/blob/d0b1534d105e5dcb3790c65d4bb04ff1d2dbc575/src/utils/ocean.ts#L31).
|
||||
|
||||
> Cleaning all Docker images so they are fetched freshly is often a good idea to make sure no issues are caused by old or stale images: `docker system prune --all --volumes`
|
||||
|
||||
@ -97,7 +97,7 @@ The `app.config.js` file is setup to prioritize environment variables for settin
|
||||
For local development, you can use a `.env` file:
|
||||
|
||||
```bash
|
||||
# modify env variables, Rinkeby is enabled by default when using those files
|
||||
# modify env variables, Goerli is enabled by default when using those files
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
@ -316,7 +316,7 @@ npm run jest
|
||||
|
||||
A coverage report is automatically shown in console whenever `npm run jest` is called. Generated reports are sent to CodeClimate during CI runs.
|
||||
|
||||
During local development you can continously get coverage report feedback in your console by running Jest in watch mode:
|
||||
During local development you can continuously get coverage report feedback in your console by running Jest in watch mode:
|
||||
|
||||
```bash
|
||||
npm run jest:watch
|
||||
@ -379,7 +379,7 @@ We encourage you to fork this repository and create your own data marketplace. W
|
||||
- The Ocean Protocol logo is a trademark of the Ocean Protocol Foundation and must be removed from forked versions of the market.
|
||||
- The name "Ocean Market" is also copyright protected and should be changed to the name of your market.
|
||||
|
||||
Additionally, we would also advise that your retain the text saying "Powered by Ocean Protocol" on your forked version of the marketplace in order to give credit for the development work done by the Ocean Protocol team.
|
||||
Additionally, we would also advise that you retain the text saying "Powered by Ocean Protocol" on your forked version of the marketplace in order to give credit for the development work done by the Ocean Protocol team.
|
||||
|
||||
Everything else is made open according to the apache2 license. We look forward to seeing your data marketplace!
|
||||
|
||||
@ -411,7 +411,7 @@ Feel free to adopt our provided privacy policies to your needs. Per default we c
|
||||
|
||||
### Privacy Preference Center
|
||||
|
||||
Additionally, Ocean Market provides a privacy preference center for you to use. This feature is disabled per default since we do not use cookies requiring consent on our deployment of the market. However, if you need to add some functionality depending on cookies, you can simply enable this feature by changing the value of the `NEXT_PUBLIC_PRIVACY_PREFERENCE_CENTER` environmental variable to `"true"` in your `.env` file. This will enable a customizable cookie banner stating the use of your individual cookies. The content of this banner can be adjusted within the `content/gdpr.json` file. If no `optionalCookies` are provided, the privacy preference center will be set to a simpler version displaying only the `title`, `text` and `close`-button. This can be used to inform the user about the use of essential cookies, where no consent is needed. The privacy preference center supports two different styling options: `'small'` and `'default'`. Setting the style propertie to `'small'` will display a smaller cookie banner to the user at first, only showing the default styled privacy preference center upon the user's customization request.
|
||||
Additionally, Ocean Market provides a privacy preference center for you to use. This feature is disabled per default since we do not use cookies requiring consent on our deployment of the market. However, if you need to add some functionality depending on cookies, you can simply enable this feature by changing the value of the `NEXT_PUBLIC_PRIVACY_PREFERENCE_CENTER` environmental variable to `"true"` in your `.env` file. This will enable a customizable cookie banner stating the use of your individual cookies. The content of this banner can be adjusted within the `content/gdpr.json` file. If no `optionalCookies` are provided, the privacy preference center will be set to a simpler version displaying only the `title`, `text` and `close`-button. This can be used to inform the user about the use of essential cookies, where no consent is needed. The privacy preference center supports two different styling options: `'small'` and `'default'`. Setting the style property to `'small'` will display a smaller cookie banner to the user at first, only showing the default styled privacy preference center upon the user's customization request.
|
||||
|
||||
Now your market users will be provided with additional options to toggle the use of your configured cookie consent categories. You can always retrieve the current consent status per category with the provided `useConsent()` hook. See below, how you can set your own custom cookies depending on the market user's consent. Feel free to adjust the provided utility functions for cookie usage provided in the `src/utils/cookies.ts` file to your needs.
|
||||
|
||||
|
@ -9,20 +9,12 @@ module.exports = {
|
||||
process.env.NEXT_PUBLIC_METADATACACHE_URI ||
|
||||
'https://v4.aquarius.oceanprotocol.com',
|
||||
|
||||
v3MetadataCacheUri:
|
||||
process.env.NEXT_PUBLIC_V3_METADATACACHE_URI ||
|
||||
'https://aquarius.oceanprotocol.com',
|
||||
|
||||
v3MarketUri:
|
||||
process.env.NEXT_PUBLIC_V3_MARKET_URI ||
|
||||
'https://v3.market.oceanprotocol.com',
|
||||
|
||||
// List of chainIds which metadata cache queries will return by default.
|
||||
// This preselects the Chains user preferences.
|
||||
chainIds: [1, 137, 56, 246, 1285],
|
||||
|
||||
// List of all supported chainIds. Used to populate the Chains user preferences list.
|
||||
chainIdsSupported: [1, 137, 56, 246, 1285, 3, 4, 80001, 1287],
|
||||
chainIdsSupported: [1, 137, 56, 246, 1285, 5, 80001, 1287],
|
||||
|
||||
infuraProjectId: process.env.NEXT_PUBLIC_INFURA_PROJECT_ID || 'xxx',
|
||||
|
||||
|
@ -59,6 +59,13 @@
|
||||
"placeholder": "e.g. Mrs McJellyfish",
|
||||
"help": "Give proper attribution for your dataset.",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"label": "New Tags",
|
||||
"type": "tags",
|
||||
"placeholder": "e.g. logistics",
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -39,8 +39,8 @@
|
||||
{
|
||||
"name": "tags",
|
||||
"label": "Tags",
|
||||
"placeholder": "e.g. logistics, ai",
|
||||
"help": "Separate tags with comma."
|
||||
"type": "tags",
|
||||
"placeholder": "e.g. logistics"
|
||||
},
|
||||
{
|
||||
"name": "dockerImage",
|
||||
|
3152
package-lock.json
generated
3152
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@ -18,7 +18,7 @@
|
||||
"type-check": "tsc --noEmit",
|
||||
"deploy:s3": "bash scripts/deploy-s3.sh",
|
||||
"postinstall": "husky install && rm -r node_modules/apollo-language-server/node_modules/graphql",
|
||||
"codegen:apollo": "apollo client:codegen --endpoint=https://v4.subgraph.ropsten.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph --target typescript --tsFileExtension=d.ts --outputFlat src/@types/subgraph/",
|
||||
"codegen:apollo": "apollo client:codegen --endpoint=https://v4.subgraph.goerli.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph --target typescript --tsFileExtension=d.ts --outputFlat src/@types/subgraph/",
|
||||
"storybook": "cross-env NODE_ENV=test start-storybook -p 6006 --quiet",
|
||||
"storybook:build": "cross-env NODE_ENV=test build-storybook"
|
||||
},
|
||||
@ -26,7 +26,7 @@
|
||||
"@coingecko/cryptoformat": "^0.5.4",
|
||||
"@loadable/component": "^5.15.2",
|
||||
"@oceanprotocol/art": "^3.2.0",
|
||||
"@oceanprotocol/lib": "^2.0.2",
|
||||
"@oceanprotocol/lib": "^2.1.1",
|
||||
"@oceanprotocol/typographies": "^0.1.0",
|
||||
"@oceanprotocol/use-dark-mode": "^2.4.3",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
@ -38,13 +38,14 @@
|
||||
"decimal.js": "^10.3.1",
|
||||
"dom-confetti": "^0.2.2",
|
||||
"dotenv": "^16.0.1",
|
||||
"filesize": "^9.0.11",
|
||||
"filesize": "^10.0.5",
|
||||
"formik": "^2.2.9",
|
||||
"gray-matter": "^4.0.3",
|
||||
"is-url-superb": "^6.1.0",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.omit": "^4.5.0",
|
||||
"match-sorter": "^6.3.1",
|
||||
"myetherwallet-blockies": "^0.1.1",
|
||||
"next": "12.3.1",
|
||||
"query-string": "^7.1.1",
|
||||
@ -55,6 +56,7 @@
|
||||
"react-dotdotdot": "^1.3.1",
|
||||
"react-modal": "^3.15.1",
|
||||
"react-paginate": "^8.1.3",
|
||||
"react-select": "^5.4.0",
|
||||
"react-spring": "^9.5.2",
|
||||
"react-tabs": "^5.1.0",
|
||||
"react-toastify": "^9.0.4",
|
||||
@ -82,26 +84,26 @@
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/lodash.omit": "^4.5.7",
|
||||
"@types/node": "^18.7.18",
|
||||
"@types/react": "^18.0.14",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.5",
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"@types/react-paginate": "^7.1.1",
|
||||
"@types/remove-markdown": "^0.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.38.0",
|
||||
"@typescript-eslint/parser": "^5.38.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.38.1",
|
||||
"@typescript-eslint/parser": "^5.38.1",
|
||||
"apollo": "^2.34.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-config-oceanprotocol": "^2.0.3",
|
||||
"eslint-config-oceanprotocol": "^2.0.4",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-jest-dom": "^4.0.2",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.31.8",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-testing-library": "^5.6.4",
|
||||
"eslint-plugin-testing-library": "^5.7.0",
|
||||
"https-browserify": "^1.0.0",
|
||||
"husky": "^8.0.1",
|
||||
"jest": "^29.0.3",
|
||||
"jest": "^29.1.2",
|
||||
"jest-environment-jsdom": "^29.0.3",
|
||||
"prettier": "^2.7.1",
|
||||
"pretty-quick": "^3.1.3",
|
||||
|
@ -9,7 +9,7 @@ import React, {
|
||||
} from 'react'
|
||||
import { Config, LoggerInstance, Purgatory } from '@oceanprotocol/lib'
|
||||
import { CancelToken } from 'axios'
|
||||
import { checkV3Asset, retrieveAsset } from '@utils/aquarius'
|
||||
import { retrieveAsset } from '@utils/aquarius'
|
||||
import { useWeb3 } from './Web3'
|
||||
import { useCancelToken } from '@hooks/useCancelToken'
|
||||
import { getOceanConfig, getDevelopmentConfig } from '@utils/ocean'
|
||||
@ -25,7 +25,6 @@ export interface AssetProviderValue {
|
||||
owner: string
|
||||
error?: string
|
||||
isAssetNetwork: boolean
|
||||
isV3Asset: boolean
|
||||
isOwner: boolean
|
||||
oceanConfig: Config
|
||||
loading: boolean
|
||||
@ -53,7 +52,6 @@ function AssetProvider({
|
||||
const [error, setError] = useState<string>()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isAssetNetwork, setIsAssetNetwork] = useState<boolean>()
|
||||
const [isV3Asset, setIsV3Asset] = useState<boolean>()
|
||||
const [oceanConfig, setOceanConfig] = useState<Config>()
|
||||
|
||||
const newCancelToken = useCancelToken()
|
||||
@ -71,7 +69,6 @@ function AssetProvider({
|
||||
const asset = await retrieveAsset(did, token)
|
||||
|
||||
if (!asset) {
|
||||
setIsV3Asset(await checkV3Asset(did, token))
|
||||
setError(
|
||||
`\`${did}\`` +
|
||||
'\n\nWe could not find an asset for this DID in the cache. If you just published a new asset, wait some seconds and refresh this page.'
|
||||
@ -96,7 +93,6 @@ function AssetProvider({
|
||||
}
|
||||
|
||||
setTitle(`This asset has been flagged as "${state}" by the publisher`)
|
||||
setIsV3Asset(await checkV3Asset(did, token))
|
||||
setError(`\`${did}\`` + `\n\nPublisher Address: ${asset.nft.owner}`)
|
||||
LoggerInstance.error(`[asset] Failed getting asset for ${did}`, asset)
|
||||
return
|
||||
@ -208,7 +204,6 @@ function AssetProvider({
|
||||
loading,
|
||||
fetchAsset,
|
||||
isAssetNetwork,
|
||||
isV3Asset,
|
||||
isOwner,
|
||||
oceanConfig
|
||||
} as AssetProviderValue
|
||||
|
@ -26,8 +26,6 @@ export interface AppConfig {
|
||||
classNameLight: string
|
||||
storageKey: string
|
||||
}
|
||||
v3MetadataCacheUri: string
|
||||
v3MarketUri: string
|
||||
}
|
||||
export interface SiteContent {
|
||||
siteTitle: string
|
||||
|
@ -133,6 +133,14 @@ function UserPreferencesProvider({
|
||||
setBookmarks(newPinned)
|
||||
}, [bookmarks])
|
||||
|
||||
// chainIds old data migration
|
||||
// remove deprecated networks from user-saved chainIds
|
||||
useEffect(() => {
|
||||
if (!chainIds.includes(3) && !chainIds.includes(4)) return
|
||||
const newChainIds = chainIds.filter((id) => id !== 3 && id !== 4)
|
||||
setChainIds(newChainIds)
|
||||
}, [chainIds])
|
||||
|
||||
return (
|
||||
<UserPreferencesContext.Provider
|
||||
value={
|
||||
|
4
src/@types/aquarius/TagsList.d.ts
vendored
Normal file
4
src/@types/aquarius/TagsList.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
interface AggregatedTag {
|
||||
doc_count: number
|
||||
key: string
|
||||
}
|
@ -2,7 +2,7 @@ import { Asset, LoggerInstance } from '@oceanprotocol/lib'
|
||||
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
|
||||
import axios, { CancelToken, AxiosResponse } from 'axios'
|
||||
import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData'
|
||||
import { metadataCacheUri, v3MetadataCacheUri } from '../../app.config'
|
||||
import { metadataCacheUri } from '../../app.config'
|
||||
import {
|
||||
SortDirectionOptions,
|
||||
SortTermOptions
|
||||
@ -44,7 +44,10 @@ export function generateBaseQuery(
|
||||
): SearchQuery {
|
||||
const generatedQuery = {
|
||||
from: baseQueryParams.esPaginationOptions?.from || 0,
|
||||
size: baseQueryParams.esPaginationOptions?.size || 1000,
|
||||
size:
|
||||
baseQueryParams.esPaginationOptions?.size >= 0
|
||||
? baseQueryParams.esPaginationOptions?.size
|
||||
: 1000,
|
||||
query: {
|
||||
bool: {
|
||||
...baseQueryParams.nestedQuery,
|
||||
@ -145,28 +148,6 @@ export async function retrieveAsset(
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkV3Asset(
|
||||
did: string,
|
||||
cancelToken: CancelToken
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const response: AxiosResponse<Asset> = await axios.get(
|
||||
`${v3MetadataCacheUri}/api/v1/aquarius/assets/ddo/${did}`,
|
||||
{ cancelToken }
|
||||
)
|
||||
if (!response || response.status !== 200 || !response.data) return false
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
if (axios.isCancel(error)) {
|
||||
LoggerInstance.log(error.message)
|
||||
} else {
|
||||
LoggerInstance.error(error.message)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAssetsNames(
|
||||
didList: string[],
|
||||
cancelToken: CancelToken
|
||||
@ -478,3 +459,47 @@ export async function getDownloadAssets(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTagsList(
|
||||
chainIds: number[],
|
||||
cancelToken: CancelToken
|
||||
): Promise<string[]> {
|
||||
const baseQueryParams = {
|
||||
chainIds,
|
||||
esPaginationOptions: { from: 0, size: 0 }
|
||||
} as BaseQueryParams
|
||||
const query = {
|
||||
...generateBaseQuery(baseQueryParams),
|
||||
aggs: {
|
||||
tags: {
|
||||
terms: {
|
||||
field: 'metadata.tags.keyword',
|
||||
size: 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response: AxiosResponse<SearchResponse> = await axios.post(
|
||||
`${metadataCacheUri}/api/aquarius/assets/query`,
|
||||
{ ...query },
|
||||
{ cancelToken }
|
||||
)
|
||||
if (response?.status !== 200 || !response?.data) return
|
||||
const { buckets }: { buckets: AggregatedTag[] } =
|
||||
response.data.aggregations.tags
|
||||
|
||||
const tagsList = buckets
|
||||
.filter((tag) => tag.key !== '')
|
||||
.map((tag) => tag.key)
|
||||
|
||||
return tagsList.sort()
|
||||
} catch (error) {
|
||||
if (axios.isCancel(error)) {
|
||||
LoggerInstance.log(error.message)
|
||||
} else {
|
||||
LoggerInstance.error(error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ export function getDevelopmentConfig(): Config {
|
||||
// fixedRateExchangeAddress: contractAddresses.development?.FixedRateExchange,
|
||||
// metadataContractAddress: contractAddresses.development?.Metadata,
|
||||
// oceanTokenAddress: contractAddresses.development?.Ocean,
|
||||
// There is no subgraph in barge so we hardcode the Rinkeby one for now
|
||||
subgraphUri: 'https://v4.subgraph.rinkeby.oceanprotocol.com'
|
||||
// There is no subgraph in barge so we hardcode the Goerli one for now
|
||||
subgraphUri: 'https://v4.subgraph.goerli.oceanprotocol.com'
|
||||
} as Config
|
||||
}
|
||||
|
@ -1,13 +1,10 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import filesize from 'filesize'
|
||||
import classNames from 'classnames/bind'
|
||||
import { filesize } from 'filesize'
|
||||
import cleanupContentType from '@utils/cleanupContentType'
|
||||
import styles from './index.module.css'
|
||||
import Loader from '@shared/atoms/Loader'
|
||||
import { FileInfo } from '@oceanprotocol/lib'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
function LoaderArea() {
|
||||
return (
|
||||
<div className={styles.loaderWrap}>
|
||||
@ -27,11 +24,9 @@ export default function FileIcon({
|
||||
small?: boolean
|
||||
isLoading?: boolean
|
||||
}): ReactElement {
|
||||
const styleClasses = cx({
|
||||
file: true,
|
||||
small,
|
||||
[className]: className
|
||||
})
|
||||
const styleClasses = `${styles.file} ${small ? styles.small : ''} ${
|
||||
className || ''
|
||||
}`
|
||||
|
||||
return (
|
||||
<ul className={styleClasses}>
|
||||
@ -42,7 +37,7 @@ export default function FileIcon({
|
||||
<li>{cleanupContentType(file.contentType)}</li>
|
||||
<li>
|
||||
{file.contentLength && file.contentLength !== '0'
|
||||
? filesize(Number(file.contentLength))
|
||||
? filesize(Number(file.contentLength)).toString()
|
||||
: ''}
|
||||
</li>
|
||||
</>
|
||||
|
@ -12,6 +12,7 @@ import AssetSelection, {
|
||||
import Nft from '../FormFields/Nft'
|
||||
import InputRadio from './InputRadio'
|
||||
import ContainerInput from '@shared/FormFields/ContainerInput'
|
||||
import TagsAutoComplete from './TagsAutoComplete'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
@ -124,6 +125,8 @@ export default function InputElement({
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
case 'tags':
|
||||
return <TagsAutoComplete {...field} {...props} />
|
||||
default:
|
||||
return prefix || postfix ? (
|
||||
<div className={`${prefix ? styles.prefixGroup : styles.postfixGroup}`}>
|
||||
|
101
src/components/@shared/FormInput/TagsAutoComplete.module.css
Normal file
101
src/components/@shared/FormInput/TagsAutoComplete.module.css
Normal file
@ -0,0 +1,101 @@
|
||||
.select [class$='control'] {
|
||||
border-color: var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: none;
|
||||
background-color: var(--background-content);
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
cursor: text;
|
||||
min-height: 43px;
|
||||
}
|
||||
|
||||
.select [class$='control']:hover {
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.select [class$='control']:focus-within {
|
||||
border-color: var(--font-color-text);
|
||||
}
|
||||
|
||||
.select [class$='ValueContainer'] {
|
||||
padding: calc(var(--spacer) / 4) calc(var(--spacer) / 3);
|
||||
}
|
||||
|
||||
.select [class$='Input'] {
|
||||
margin: 0;
|
||||
padding-bottom: 0;
|
||||
padding-top: 0;
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.select input {
|
||||
color: var(--font-color-heading) !important;
|
||||
}
|
||||
|
||||
.select [class$='menu'] {
|
||||
background-color: var(--background-highlight);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.select [class$='option'] {
|
||||
color: var(--font-color-heading);
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.select [class$='option']:active {
|
||||
background-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.select [class$='multiValue'],
|
||||
.select [class$='multiValue'] > *,
|
||||
.select [class$='multiValue']:hover > * {
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--background-highlight);
|
||||
color: var(--font-color-text);
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.select [class$='multiValue'] > div[role$='button'],
|
||||
.select [class$='indicatorContainer'] svg {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.select [class$='placeholder'] {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
color: var(--color-secondary);
|
||||
font-weight: var(--font-weight-base);
|
||||
transition: 0.2s ease-out;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.select [class$='menu'] {
|
||||
background-color: var(--background-content);
|
||||
margin-top: -2px;
|
||||
border: 1px solid var(--font-color-text);
|
||||
border-top-color: var(--border-color);
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.select [class$='menu'] [class$='option']:hover,
|
||||
.select [class$='menu'] [class$='option']:focus-within {
|
||||
background-color: var(--font-color-heading);
|
||||
color: var(--background-content);
|
||||
}
|
||||
|
||||
.select [class$='NoOptionsMessage'] {
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--color-secondary);
|
||||
text-align: left;
|
||||
}
|
92
src/components/@shared/FormInput/TagsAutoComplete.tsx
Normal file
92
src/components/@shared/FormInput/TagsAutoComplete.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import CreatableSelect from 'react-select/creatable'
|
||||
import { OnChangeValue } from 'react-select'
|
||||
import { useField } from 'formik'
|
||||
import { InputProps } from '.'
|
||||
import { getTagsList } from '@utils/aquarius'
|
||||
import { chainIds } from 'app.config'
|
||||
import { useCancelToken } from '@hooks/useCancelToken'
|
||||
import styles from './TagsAutoComplete.module.css'
|
||||
import { matchSorter } from 'match-sorter'
|
||||
|
||||
interface AutoCompleteOption {
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
}
|
||||
|
||||
export default function TagsAutoComplete({
|
||||
...props
|
||||
}: InputProps): ReactElement {
|
||||
const { name, placeholder } = props
|
||||
const [tagsList, setTagsList] = useState<AutoCompleteOption[]>()
|
||||
const [matchedTagsList, setMatchedTagsList] = useState<AutoCompleteOption[]>(
|
||||
[]
|
||||
)
|
||||
const [field, meta, helpers] = useField(name)
|
||||
const [input, setInput] = useState<string>()
|
||||
|
||||
const newCancelToken = useCancelToken()
|
||||
|
||||
const generateAutocompleteOptions = (
|
||||
options: string[]
|
||||
): AutoCompleteOption[] => {
|
||||
return options?.map((tag) => ({
|
||||
value: tag,
|
||||
label: tag
|
||||
}))
|
||||
}
|
||||
|
||||
const defaultTags = !field.value
|
||||
? undefined
|
||||
: generateAutocompleteOptions(field.value)
|
||||
|
||||
useEffect(() => {
|
||||
const generateTagsList = async () => {
|
||||
const tags = await getTagsList(chainIds, newCancelToken())
|
||||
const autocompleteOptions = generateAutocompleteOptions(tags)
|
||||
setTagsList(autocompleteOptions)
|
||||
}
|
||||
generateTagsList()
|
||||
}, [newCancelToken])
|
||||
|
||||
const handleChange = (userInput: OnChangeValue<AutoCompleteOption, true>) => {
|
||||
const normalizedInput = userInput.map((input) => input.value)
|
||||
helpers.setValue(normalizedInput)
|
||||
helpers.setTouched(true)
|
||||
}
|
||||
|
||||
const handleOptionsFilter = (
|
||||
options: AutoCompleteOption[],
|
||||
input: string
|
||||
): void => {
|
||||
setInput(input)
|
||||
const matchedTagsList = matchSorter(options, input, { keys: ['value'] })
|
||||
setMatchedTagsList(matchedTagsList)
|
||||
}
|
||||
|
||||
return (
|
||||
<CreatableSelect
|
||||
components={{
|
||||
DropdownIndicator: () => null,
|
||||
IndicatorSeparator: () => null
|
||||
}}
|
||||
className={styles.select}
|
||||
defaultValue={defaultTags}
|
||||
hideSelectedOptions
|
||||
isMulti
|
||||
isClearable={false}
|
||||
noOptionsMessage={() =>
|
||||
'Start typing to get suggestions based on tags from all published assets.'
|
||||
}
|
||||
onChange={(value: AutoCompleteOption[]) => handleChange(value)}
|
||||
onInputChange={(value) => handleOptionsFilter(tagsList, value)}
|
||||
openMenuOnClick
|
||||
options={!input || input?.length < 1 ? [] : matchedTagsList}
|
||||
placeholder={placeholder}
|
||||
theme={(theme) => ({
|
||||
...theme,
|
||||
colors: { ...theme.colors, primary25: 'var(--border-color)' }
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
@ -73,7 +73,8 @@ export default function Edit({
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
links: linksTransformed,
|
||||
author: values.author
|
||||
author: values.author,
|
||||
tags: values.tags
|
||||
}
|
||||
|
||||
asset?.accessDetails?.type === 'fixed' &&
|
||||
|
@ -14,7 +14,8 @@ export function getInitialValues(
|
||||
links: metadata?.links as any,
|
||||
files: [{ url: '', type: '' }],
|
||||
timeout: secondsToString(timeout),
|
||||
author: metadata?.author
|
||||
author: metadata?.author,
|
||||
tags: metadata?.tags
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ export interface MetadataEditForm {
|
||||
files: FileInfo[]
|
||||
links?: FileInfo[]
|
||||
author?: string
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
export interface ComputeEditForm {
|
||||
|
@ -24,7 +24,8 @@ export const validationSchema = Yup.object().shape({
|
||||
)
|
||||
.nullable(),
|
||||
timeout: Yup.string().required('Required'),
|
||||
author: Yup.string().nullable()
|
||||
author: Yup.string().nullable(),
|
||||
tags: Yup.array<string[]>().nullable()
|
||||
})
|
||||
|
||||
export const computeSettingsValidationSchema = Yup.object().shape({
|
||||
|
@ -5,29 +5,25 @@ import Alert from '@shared/atoms/Alert'
|
||||
import Loader from '@shared/atoms/Loader'
|
||||
import { useAsset } from '@context/Asset'
|
||||
import AssetContent from './AssetContent'
|
||||
import { v3MarketUri } from 'app.config'
|
||||
|
||||
export default function AssetDetails({ uri }: { uri: string }): ReactElement {
|
||||
const router = useRouter()
|
||||
const { asset, title, error, isInPurgatory, loading, isV3Asset } = useAsset()
|
||||
const { asset, title, error, isInPurgatory, loading } = useAsset()
|
||||
const [pageTitle, setPageTitle] = useState<string>()
|
||||
|
||||
useEffect(() => {
|
||||
if (isV3Asset) {
|
||||
router.push(`${v3MarketUri}${uri}`)
|
||||
}
|
||||
if (!asset || error) {
|
||||
setPageTitle(title || 'Could not retrieve asset')
|
||||
return
|
||||
}
|
||||
setPageTitle(isInPurgatory ? '' : title)
|
||||
}, [asset, error, isInPurgatory, isV3Asset, router, title, uri])
|
||||
}, [asset, error, isInPurgatory, router, title, uri])
|
||||
|
||||
return asset && pageTitle !== undefined && !loading ? (
|
||||
<Page title={pageTitle} uri={uri}>
|
||||
<AssetContent asset={asset} />
|
||||
</Page>
|
||||
) : error && isV3Asset === false ? (
|
||||
) : error ? (
|
||||
<Page title={pageTitle} noPageHeader uri={uri}>
|
||||
<Alert title={pageTitle} text={error} state={'error'} />
|
||||
</Page>
|
||||
|
@ -63,7 +63,7 @@ export const initialValues: FormPublishData = {
|
||||
name: '',
|
||||
author: '',
|
||||
description: '',
|
||||
tags: '',
|
||||
tags: [],
|
||||
termsAndConditions: false,
|
||||
dockerImage: '',
|
||||
dockerImageCustom: '',
|
||||
|
@ -26,7 +26,7 @@ export interface FormPublishData {
|
||||
description: string
|
||||
author: string
|
||||
termsAndConditions: boolean
|
||||
tags?: string
|
||||
tags?: string[]
|
||||
dockerImage?: string
|
||||
dockerImageCustom?: string
|
||||
dockerImageCustomTag?: string
|
||||
|
@ -51,8 +51,7 @@ function dateToStringNoMS(date: Date): string {
|
||||
return date.toISOString().replace(/\.[0-9]{3}Z/, 'Z')
|
||||
}
|
||||
|
||||
function transformTags(value: string): string[] {
|
||||
const originalTags = value?.split(',')
|
||||
function transformTags(originalTags: string[]): string[] {
|
||||
const transformedTags = originalTags?.map((tag) => slugify(tag).toLowerCase())
|
||||
return transformedTags
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ const validationMetadata = {
|
||||
)
|
||||
.required('Required'),
|
||||
author: Yup.string().required('Required'),
|
||||
tags: Yup.string().nullable(),
|
||||
tags: Yup.array<string[]>().nullable(),
|
||||
termsAndConditions: Yup.boolean()
|
||||
.required('Required')
|
||||
.isTrue('Please agree to the Terms and Conditions.')
|
||||
|
Loading…
Reference in New Issue
Block a user