diff --git a/app/scripts/components/common/browse-controls/index.tsx b/app/scripts/components/common/browse-controls/index.tsx index 5bc8d4e91..245fef34d 100644 --- a/app/scripts/components/common/browse-controls/index.tsx +++ b/app/scripts/components/common/browse-controls/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import styled from 'styled-components'; +import { Taxonomy } from 'veda'; import { Overline } from '@devseed-ui/typography'; import { Button, ButtonProps } from '@devseed-ui/button'; import { @@ -62,7 +63,7 @@ const ButtonPrefix = styled(Overline).attrs({ as: 'small' })` `; interface BrowseControlsProps extends ReturnType { - taxonomiesOptions: Record; + taxonomiesOptions: Taxonomy[]; sortOptions: FilterOption[]; } @@ -142,11 +143,11 @@ function BrowseControls(props: BrowseControlsProps) { - {Object.entries(taxonomiesOptions).map(([name, options]) => ( + {taxonomiesOptions.map(({ name, values }) => ( { onAction(Actions.TAXONOMY, { key: name, value: v }); diff --git a/app/scripts/components/common/browse-controls/use-browse-controls.ts b/app/scripts/components/common/browse-controls/use-browse-controls.ts index 23e51d5b4..51c0664b7 100644 --- a/app/scripts/components/common/browse-controls/use-browse-controls.ts +++ b/app/scripts/components/common/browse-controls/use-browse-controls.ts @@ -99,7 +99,7 @@ export function useBrowserControls({ sortOptions }: BrowseControlsHookParams) { if (val === optionAll.id) { setTaxonomies(omit(taxonomies, key)); } else { - setTaxonomies(set({...taxonomies}, key, val)); + setTaxonomies(set({ ...taxonomies }, key, val)); } } break; diff --git a/app/scripts/components/common/content-taxonomy.tsx b/app/scripts/components/common/content-taxonomy.tsx index e6c7d84af..f135084b8 100644 --- a/app/scripts/components/common/content-taxonomy.tsx +++ b/app/scripts/components/common/content-taxonomy.tsx @@ -1,6 +1,6 @@ import React from 'react'; import styled from 'styled-components'; -import { TaxonomyItem } from 'veda'; +import { Taxonomy } from 'veda'; import { Link } from 'react-router-dom'; import { glsp, themeVal } from '@devseed-ui/theme-provider'; import { Heading, Overline } from '@devseed-ui/typography'; @@ -43,15 +43,13 @@ const TaxonomyList = styled.dl` `; interface ContentTaxonomyProps { - taxonomy: Record; + taxonomy: Taxonomy[]; } export function ContentTaxonomy(props: ContentTaxonomyProps) { const { taxonomy } = props; - const taxonomies = Object.entries(taxonomy) as [string, TaxonomyItem[]][]; - - if (!taxonomies.length) return null; + if (!taxonomy.length) return null; return ( @@ -60,10 +58,10 @@ export function ContentTaxonomy(props: ContentTaxonomyProps) { Taxonomy - {taxonomies.map(([key, values]) => ( - + {taxonomy.map(({ name, values }) => ( +
- {key} + {name}
{values.map((t) => ( @@ -72,7 +70,7 @@ export function ContentTaxonomy(props: ContentTaxonomyProps) { as={Link} to={`${DATASETS_PATH}?${ Actions.TAXONOMY - }=${encodeURIComponent(JSON.stringify({ [key]: t.id }))}`} + }=${encodeURIComponent(JSON.stringify({ [name]: t.id }))}`} > {t.name} diff --git a/app/scripts/components/common/featured-slider-section.tsx b/app/scripts/components/common/featured-slider-section.tsx index 58a233761..9d618ef3b 100644 --- a/app/scripts/components/common/featured-slider-section.tsx +++ b/app/scripts/components/common/featured-slider-section.tsx @@ -19,6 +19,11 @@ import { ContinuumScrollIndicator } from '$styles/continuum/continuum-scroll-ind import { getDatasetPath, getDiscoveryPath } from '$utils/routes'; import { Pill } from '$styles/pill'; import DatasetMenu from '$components/data-catalog/dataset-menu'; +import { + getTaxonomy, + TAXONOMY_SOURCE, + TAXONOMY_TOPICS +} from '$utils/veda-data'; const allFeaturedDiscoveries = Object.values(discoveries) .map((d) => d!.data) @@ -76,6 +81,7 @@ function FeaturedSliderSection(props: FeaturedSliderSectionProps) { render={(bag) => { return featuredItems.map((d) => { const date = new Date(d[dateProperty ?? '']); + const topics = getTaxonomy(d, TAXONOMY_TOPICS)?.values; return ( @@ -94,7 +100,9 @@ function FeaturedSliderSection(props: FeaturedSliderSectionProps) { title={d.name} overline={ - + {!isNaN(date.getTime()) && ( @@ -106,10 +114,10 @@ function FeaturedSliderSection(props: FeaturedSliderSectionProps) { imgAlt={d.media?.alt} footerContent={ <> - {d.taxonomy.Topics?.length ? ( + {topics?.length ? (
Topics
- {d.taxonomy.Topics.map((t) => ( + {topics.map((t) => (
{t.name}
diff --git a/app/scripts/components/data-catalog/index.tsx b/app/scripts/components/data-catalog/index.tsx index c3250c427..489a7b86b 100644 --- a/app/scripts/components/data-catalog/index.tsx +++ b/app/scripts/components/data-catalog/index.tsx @@ -41,6 +41,11 @@ import Pluralize from '$utils/pluralize'; import { Pill } from '$styles/pill'; import { FeaturedDatasets } from '$components/common/featured-slider-section'; import { CardSourcesList } from '$components/common/card-sources'; +import { + getTaxonomy, + TAXONOMY_SOURCE, + TAXONOMY_TOPICS +} from '$utils/veda-data'; const allDatasets = Object.values(datasets).map((d) => d!.data); @@ -57,40 +62,54 @@ const DatasetCount = styled(Subtitle)` const sortOptions = [{ id: 'name', name: 'Name' }]; -const prepareDatasets = (data: DatasetData[], options) => { +const prepareDatasets = ( + data: DatasetData[], + options: { + search: string; + taxonomies: Record | null; + sortField: string | null; + sortDir: string | null; + } +) => { const { sortField, sortDir, search, taxonomies } = options; let filtered = [...data]; // Does the free text search appear in specific fields? if (search.length >= 3) { + const topicsTaxonomy = datasetTaxonomies.find( + (t) => t.name === TAXONOMY_TOPICS + ); const searchLower = search.toLowerCase(); filtered = filtered.filter( (d) => d.name.toLowerCase().includes(searchLower) || d.description.toLowerCase().includes(searchLower) || d.layers.some((l) => l.stacCol.toLowerCase().includes(searchLower)) || - d.taxonomy.Topics?.some((t) => + topicsTaxonomy?.values.some((t) => t.name.toLowerCase().includes(searchLower) ) ); } - Object.entries(taxonomies).forEach(([name, value]) => { - if (value !== optionAll.id) { - const txId = datasetTaxonomies[name].find((t) => t.id === value)?.id; - filtered = filtered.filter( - (d) => txId && d.taxonomy[name]?.find((t) => t.id === txId) - ); - } - }); + taxonomies && + Object.entries(taxonomies).forEach(([name, value]) => { + if (value !== optionAll.id) { + filtered = filtered.filter((d) => + d.taxonomy.some( + (t) => t.name === name && t.values.some((v) => v.id === value) + ) + ); + } + }); - /* eslint-disable-next-line fp/no-mutating-methods */ - filtered.sort((a, b) => { - if (!a[sortField]) return Infinity; + sortField && + /* eslint-disable-next-line fp/no-mutating-methods */ + filtered.sort((a, b) => { + if (!a[sortField]) return Infinity; - return a[sortField]?.localeCompare(b[sortField]); - }); + return a[sortField]?.localeCompare(b[sortField]); + }); if (sortDir === 'desc') { /* eslint-disable-next-line fp/no-mutating-methods */ @@ -177,23 +196,28 @@ function DataCatalog() { {displayDatasets.length ? ( - {displayDatasets.map((d) => ( -
  • - - { - onAction(Actions.TAXONOMY, { key: 'Source', id }); - browseControlsHeaderRef.current?.scrollIntoView(); - }} - /> - - {/* TODO: Implement modified date: https://github.com/NASA-IMPACT/veda-ui/issues/514 */} - {/* + {displayDatasets.map((d) => { + const topics = getTaxonomy(d, TAXONOMY_TOPICS)?.values; + return ( +
  • + + { + onAction(Actions.TAXONOMY, { + key: TAXONOMY_SOURCE, + id + }); + browseControlsHeaderRef.current?.scrollIntoView(); + }} + /> + + {/* TODO: Implement modified date: https://github.com/NASA-IMPACT/veda-ui/issues/514 */} + {/* { @@ -203,63 +227,70 @@ function DataCatalog() { > Updated */} - - } - linkLabel='View more' - linkTo={getDatasetPath(d)} - title={ - - {d.name} - - } - description={ - - {d.description} - - } - imgSrc={d.media?.src} - imgAlt={d.media?.alt} - footerContent={ - <> - {d.taxonomy.Topics?.length ? ( - -
    Topics
    - {d.taxonomy.Topics.map((t) => ( -
    - { - e.preventDefault(); - onAction(Actions.TAXONOMY, { - key: 'Topics', - value: t.id - }); - browseControlsHeaderRef.current?.scrollIntoView(); - }} - > - + } + linkLabel='View more' + linkTo={getDatasetPath(d)} + title={ + + {d.name} + + } + description={ + + {d.description} + + } + imgSrc={d.media?.src} + imgAlt={d.media?.alt} + footerContent={ + <> + {topics?.length ? ( + +
    Topics
    + {topics.map((t) => ( +
    + { + e.preventDefault(); + onAction(Actions.TAXONOMY, { + key: TAXONOMY_TOPICS, + value: t.id + }); + browseControlsHeaderRef.current?.scrollIntoView(); + }} > - {t.name} - - -
    - ))} -
    - ) : null} - - - } - /> -
  • - ))} + + {t.name} + + + + ))} + + ) : null} + + + } + /> + + ); + })}
    ) : ( diff --git a/app/scripts/components/discoveries/hub/index.tsx b/app/scripts/components/discoveries/hub/index.tsx index 9ac3ca4ee..b5e2168d0 100644 --- a/app/scripts/components/discoveries/hub/index.tsx +++ b/app/scripts/components/discoveries/hub/index.tsx @@ -40,6 +40,11 @@ import Pluralize from '$utils/pluralize'; import { Pill } from '$styles/pill'; import { FeaturedDiscoveries } from '$components/common/featured-slider-section'; import { CardSourcesList } from '$components/common/card-sources'; +import { + getTaxonomy, + TAXONOMY_SOURCE, + TAXONOMY_TOPICS +} from '$utils/veda-data'; const allDiscoveries = Object.values(discoveries).map((d) => d!.data); @@ -59,39 +64,53 @@ const sortOptions = [ { id: 'pubDate', name: 'Date' } ]; -const prepareDiscoveries = (data: DiscoveryData[], options) => { +const prepareDiscoveries = ( + data: DiscoveryData[], + options: { + search: string; + taxonomies: Record | null; + sortField: string | null; + sortDir: string | null; + } +) => { const { sortField, sortDir, search, taxonomies } = options; let filtered = [...data]; // Does the free text search appear in specific fields? if (search.length >= 3) { + const topicsTaxonomy = discoveryTaxonomies.find( + (t) => t.name === TAXONOMY_TOPICS + ); const searchLower = search.toLowerCase(); filtered = filtered.filter( (d) => d.name.toLowerCase().includes(searchLower) || d.description.toLowerCase().includes(searchLower) || - d.taxonomy.Topics?.some((t) => + topicsTaxonomy?.values.some((t) => t.name.toLowerCase().includes(searchLower) ) ); } - Object.entries(taxonomies).forEach(([name, value]) => { - if (value !== optionAll.id) { - const txId = discoveryTaxonomies[name].find((t) => t.id === value)?.id; - filtered = filtered.filter( - (d) => txId && d.taxonomy[name]?.find((t) => t.id === txId) - ); - } - }); + taxonomies && + Object.entries(taxonomies).forEach(([name, value]) => { + if (value !== optionAll.id) { + filtered = filtered.filter((d) => + d.taxonomy.some( + (t) => t.name === name && t.values.some((v) => v.id === value) + ) + ); + } + }); - /* eslint-disable-next-line fp/no-mutating-methods */ - filtered.sort((a, b) => { - if (!a[sortField]) return Infinity; + sortField && + /* eslint-disable-next-line fp/no-mutating-methods */ + filtered.sort((a, b) => { + if (!a[sortField]) return Infinity; - return a[sortField]?.localeCompare(b[sortField]); - }); + return a[sortField]?.localeCompare(b[sortField]); + }); // In the case of the date, ordering is reversed. if (sortField === 'pubDate') { @@ -186,6 +205,7 @@ function DiscoveriesHub() { {displayDiscoveries.map((d) => { const pubDate = new Date(d.pubDate); + const topics = getTaxonomy(d, TAXONOMY_TOPICS)?.values; return (
  • { onAction(Actions.TAXONOMY, { - key: 'Source', + key: TAXONOMY_SOURCE, value: id }); browseControlsHeaderRef.current?.scrollIntoView(); @@ -240,10 +260,10 @@ function DiscoveriesHub() { imgAlt={d.media?.alt} footerContent={ <> - {d.taxonomy.Topics?.length ? ( + {topics?.length ? (
    Topics
    - {d.taxonomy.Topics.map((t) => ( + {topics.map((t) => (
    Promise) { return pageMdx; } + +// Taxonomies with special meaning as they're used in the app, like in the cards +// for example. +export const TAXONOMY_TOPICS = 'Topics'; +export const TAXONOMY_SOURCE = 'Source'; + +export function getTaxonomy( + data: DatasetData | DiscoveryData | Taxonomy[], + taxonomyName: string +) { + const list = Array.isArray(data) ? data : data.taxonomy; + + return list.find((t) => t.name === taxonomyName); +} diff --git a/docs/content/CONTENT.md b/docs/content/CONTENT.md index fea6c9744..2bec4d600 100644 --- a/docs/content/CONTENT.md +++ b/docs/content/CONTENT.md @@ -95,11 +95,13 @@ List of taxonomies and their values. See [taxonomy.md](./TAXONOMY.md). Example: ```yaml taxonomy: - Topics: - - Covid 19 - - Agriculture - Source: - - Development Seed + - name: Topics + values: + - Covid 19 + - Agriculture + - name: Source + values: + - Development Seed ``` **featured** @@ -225,11 +227,13 @@ List of taxonomies and their values. See [taxonomy.md](./TAXONOMY.md). Example: ```yaml taxonomy: - Topics: - - Covid 19 - - Agriculture - Source: - - Development Seed + - name: Topics + values: + - Covid 19 + - Agriculture + - name: Source + values: + - Development Seed ``` **featured** diff --git a/docs/content/TAXONOMY.md b/docs/content/TAXONOMY.md index 2c148d8fb..7f1b7b589 100644 --- a/docs/content/TAXONOMY.md +++ b/docs/content/TAXONOMY.md @@ -9,18 +9,19 @@ VEDA content types, like discoveries and datasets, have taxonomies that can be u ## Adding taxonomies to content A content type can have an arbitrary number of taxonomies which are defined under the `taxonomies` key in the content's frontmatter. -The key used to define a taxonomy is the taxonomy's name, and the value is an array of strings that are the taxonomy's values. Because of this it is important the the taxonomy name is written in a human readable way, and that it is consistently used across all content. It is also recommended that taxonomy names are singular. +Each taxonomy is defined by a name, and a list of values. It is important that the taxonomy name and its values are written in a human readable way, and that they are consistently used across all content. It is also recommended that taxonomy names are singular. For example, the following frontmatter defines two taxonomies, `Topics` and `Source`, with the values `Covid 19`, `Agriculture`, and `Development Seed`: ```yaml name: Dataset Name -taxonomy: - Topics: - - Covid 19 - - Agriculture - Source: - - Development Seed + - name: Topics + values: + - Covid 19 + - Agriculture + - name: Source + values: + - Development Seed ``` Note how the values are used: starting with a capital letter, and using spaces instead of dashes or underscores. This is because the values are displayed to the user, and should be as readable as possible. diff --git a/mock/datasets/fire.data.mdx b/mock/datasets/fire.data.mdx index 1a2eed915..b7fe01504 100644 --- a/mock/datasets/fire.data.mdx +++ b/mock/datasets/fire.data.mdx @@ -9,15 +9,19 @@ media: name: Mick Truyts url: https://unsplash.com/photos/x6WQeNYJC1w taxonomy: - Topics: - - Covid 19 - Sector: - - Agriculture, Forestry and Land Use - - Energy - Producer: - - NIST - Gas Emission: - - COx + - name: Topics + values: + - Covid 19 + - name: Sector + values: + - Agriculture, Forestry and Land Use + - Energy + - name: Producer + values: + - NIST + - name: Gas Emission + values: + - COx layers: - id: eis_fire_fireline stacCol: eis_fire_fireline diff --git a/mock/datasets/nighttime-lights.data.mdx b/mock/datasets/nighttime-lights.data.mdx index bfefafff2..1d816deef 100644 --- a/mock/datasets/nighttime-lights.data.mdx +++ b/mock/datasets/nighttime-lights.data.mdx @@ -10,15 +10,19 @@ media: name: NASA Earth Observatory url: https://earthobservatory.nasa.gov/images/90008/night-light-maps-open-up-new-applications taxonomy: - Topics: - - Covid 19 - - Agriculture - Sector: - - Electricity - Producer: - - NASA - Gas Emission: - - DOS + - name: Topics + values: + - Covid 19 + - Agriculture + - name: Sector + values: + - Electricity + - name: Producer + values: + - NASA + - name: Gas Emission + values: + - DOS layers: - id: nightlights-hd-monthly stacCol: nightlights-hd-monthly diff --git a/mock/datasets/no2.data.mdx b/mock/datasets/no2.data.mdx index ad18dbb9d..b64ea8519 100644 --- a/mock/datasets/no2.data.mdx +++ b/mock/datasets/no2.data.mdx @@ -20,16 +20,20 @@ media: name: Mick Truyts url: https://unsplash.com/photos/x6WQeNYJC1w taxonomy: - Topics: - - Covid 19 - - Agriculture - - Air Quality - Sector: - - Electricity - Producer: - - NASA - Gas Emission: - - DOS + - name: Topics + values: + - Covid 19 + - Agriculture + - Air Quality + - name: Sector + values: + - Electricity + - name: Producer + values: + - NASA + - name: Gas Emission + values: + - DOS layers: - id: no2-monthly stacCol: no2-monthly diff --git a/mock/datasets/oco2-geos-l3-daily.data.mdx b/mock/datasets/oco2-geos-l3-daily.data.mdx index bd179e7ed..dc5dd4227 100644 --- a/mock/datasets/oco2-geos-l3-daily.data.mdx +++ b/mock/datasets/oco2-geos-l3-daily.data.mdx @@ -10,8 +10,9 @@ media: name: Mick Truyts url: https://unsplash.com/photos/x6WQeNYJC1w taxonomy: - Topics: - - Air Quality + - name: Topics + values: + - Air Quality layers: - id: oco2-geos-l3-daily stacCol: oco2-geos-l3-daily diff --git a/mock/datasets/sandbox.data.mdx b/mock/datasets/sandbox.data.mdx index cd4ed4835..29d7bb3dd 100644 --- a/mock/datasets/sandbox.data.mdx +++ b/mock/datasets/sandbox.data.mdx @@ -10,12 +10,13 @@ media: name: Unsplash url: https://unsplash.com/ taxonomy: - Topics: - - Covid 19 - - Agriculture - - Our Planet - - Experimental - - Untested + - name: Topics + values: + - Covid 19 + - Agriculture + - Our Planet + - Experimental + - Untested layers: - id: blue-tarp-planetscope stacCol: blue-tarp-planetscope diff --git a/mock/discoveries/air-quality-and-covid-19.discoveries.mdx b/mock/discoveries/air-quality-and-covid-19.discoveries.mdx index 565d47965..4589f2c18 100644 --- a/mock/discoveries/air-quality-and-covid-19.discoveries.mdx +++ b/mock/discoveries/air-quality-and-covid-19.discoveries.mdx @@ -11,9 +11,10 @@ media: url: https://unsplash.com/photos/U-Kty6HxcQc pubDate: 2020-12-01 taxonomy: - Topics: - - Air Quality - - Ccovid 19 + - name: Topics + values: + - Air Quality + - Covid 19 --- diff --git a/mock/discoveries/life-of-water.discoveries.mdx b/mock/discoveries/life-of-water.discoveries.mdx index 7a99f758c..ac5288b37 100644 --- a/mock/discoveries/life-of-water.discoveries.mdx +++ b/mock/discoveries/life-of-water.discoveries.mdx @@ -11,10 +11,12 @@ media: url: https://unsplash.com/ pubDate: 2022-02-09 taxonomy: - Topics: - - Agriculture - Source: - - Development Seed + - name: Topics + values: + - Agriculture + - name: Source + values: + - Development Seed related: - type: dataset id: no2 diff --git a/parcel-resolver-veda/index.d.ts b/parcel-resolver-veda/index.d.ts index 2f2d5c46f..b36d9669e 100644 --- a/parcel-resolver-veda/index.d.ts +++ b/parcel-resolver-veda/index.d.ts @@ -143,7 +143,7 @@ declare module 'veda' { featured?: boolean; id: string; name: string; - taxonomy: Record + taxonomy: Taxonomy[]; description: string; usage?: DatasetUsage[]; media?: Media; @@ -165,7 +165,7 @@ declare module 'veda' { description: string; pubDate: string; media?: Media; - taxonomy: Record + taxonomy: Taxonomy[]; related?: RelatedContentData[]; } @@ -200,6 +200,11 @@ declare module 'veda' { content: () => Promise; } + export interface Taxonomy { + name: string; + values: TaxonomyItem[]; + } + interface TaxonomyItem { id: string; name: string; @@ -219,15 +224,15 @@ declare module 'veda' { /** * Named exports: datasetTaxonomies. - * Object with all the veda datasets taxonomies. + * Array with all the veda datasets taxonomies. */ - export const datasetTaxonomies: Record; + export const datasetTaxonomies: Taxonomy[]; /** * Named exports: discoveryTaxonomies. - * Object with all the veda discovery taxonomies. + * Array with all the veda discovery taxonomies. */ - export const discoveryTaxonomies: Record; + export const discoveryTaxonomies: Taxonomy[]; export type PageOverrides = | 'aboutContent' diff --git a/parcel-resolver-veda/taxonomies.js b/parcel-resolver-veda/taxonomies.js index 1c0abf77f..8b0213ac6 100644 --- a/parcel-resolver-veda/taxonomies.js +++ b/parcel-resolver-veda/taxonomies.js @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-var-requires */ -const { mapValues, kebabCase, uniqBy } = require('lodash'); +const { kebabCase, uniqBy } = require('lodash'); /** * Converts the taxonomies to an array of objects @@ -11,9 +11,16 @@ function processTaxonomies(contentType) { ...contentType, data: contentType.data.map((o) => ({ ...o, - taxonomy: mapValues(o.taxonomy || {}, (val) => - val.map((v) => ({ id: kebabCase(v), name: v })) - ) + taxonomy: (o.taxonomy || []) + .map((tx) => { + if (!tx.name || !tx.values?.length) return null; + + return { + name: tx.name, + values: tx.values.map((v) => ({ id: kebabCase(v), name: v })) + }; + }) + .filter(Boolean) })) }; } @@ -24,14 +31,17 @@ function generateTaxonomiesModuleOutput(data) { let taxonomyData = {}; // for loops are faster than reduces. for (const { taxonomy } of data) { - for (const [key, value] of Object.entries(taxonomy || {})) { - taxonomyData[key] = concat(taxonomyData[key], value); + for (const { name, values } of taxonomy) { + if (!name || !values?.length) continue; + + taxonomyData[name] = concat(taxonomyData[name], values); } } - const taxonomiesUnique = mapValues(taxonomyData, (val) => - uniqBy(val, (t) => t.id) - ); + const taxonomiesUnique = Object.entries(taxonomyData).map(([key, tx]) => ({ + name: key, + values: uniqBy(tx, (t) => t.id) + })); return JSON.stringify(taxonomiesUnique); }