{getSoftwareLogo()}
+
diff --git a/frontend/components/software/edit/editSoftwareConfig.tsx b/frontend/components/software/edit/editSoftwareConfig.tsx
index fb5025c2a..ef5a0304d 100644
--- a/frontend/components/software/edit/editSoftwareConfig.tsx
+++ b/frontend/components/software/edit/editSoftwareConfig.tsx
@@ -1,10 +1,11 @@
// SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all)
+// SPDX-FileCopyrightText: 2022 - 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences
// SPDX-FileCopyrightText: 2022 - 2023 dv4all
// SPDX-FileCopyrightText: 2022 Christian Meeßen (GFZ)
// SPDX-FileCopyrightText: 2022 Ewan Cahen (Netherlands eScience Center)
-// SPDX-FileCopyrightText: 2022 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences
// SPDX-FileCopyrightText: 2022 Matthias Rüster (GFZ)
// SPDX-FileCopyrightText: 2022 Netherlands eScience Center
+// SPDX-FileCopyrightText: 2023 Felix Mühlbauer (GFZ)
//
// SPDX-License-Identifier: Apache-2.0
@@ -124,6 +125,10 @@ export const softwareInformation = {
is_published: {
label: 'Published',
},
+ categories: {
+ title: 'Categories',
+ subtitle: 'Tell us more about your software.',
+ },
keywords: {
title: 'Keywords',
subtitle: 'Find, add or import using concept DOI.',
diff --git a/frontend/components/software/edit/information/AutosaveSoftwareCategories.tsx b/frontend/components/software/edit/information/AutosaveSoftwareCategories.tsx
new file mode 100644
index 000000000..20c1319fb
--- /dev/null
+++ b/frontend/components/software/edit/information/AutosaveSoftwareCategories.tsx
@@ -0,0 +1,105 @@
+// SPDX-FileCopyrightText: 2023 Felix Mühlbauer (GFZ)
+// SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences
+//
+// SPDX-License-Identifier: Apache-2.0
+
+import {ChangeEventHandler, Fragment, useEffect, useMemo, useState} from 'react'
+import {useSession} from '~/auth'
+import {softwareInformation as config} from '../editSoftwareConfig'
+import useSnackbar from '~/components/snackbar/useSnackbar'
+import EditSectionTitle from '~/components/layout/EditSectionTitle'
+import {addCategoryToSoftware, deleteCategoryToSoftware, getAvailableCategories} from '~/utils/getSoftware'
+import {CategoryID, CategoryPath, CategoryTree} from '~/types/Category'
+import {CategoriesWithHeadlines} from '~/components/category/CategoriesWithHeadlines'
+import {genCategoryTree, leaf} from '~/utils/categories'
+
+export type SoftwareCategoriesProps = {
+ softwareId: string
+ categories: CategoryPath[]
+}
+export default function AutosaveSoftwareCategories({softwareId, categories: defaultCategoryPaths}: SoftwareCategoriesProps) {
+ const {token} = useSession()
+ const {showErrorMessage} = useSnackbar()
+
+ const [availableCategoryPaths, setAvailableCategoryPaths] = useState([])
+ const [availableCategories, setAvailableCategories] = useState([])
+
+ const [categoryPaths, setCategoryPaths] = useState(defaultCategoryPaths)
+ const selectedCategoryIDs = useMemo(() => {
+ const ids = new Set()
+ for (const category of categoryPaths ) {
+ ids.add(leaf(category).id)
+ }
+ return ids
+ },[categoryPaths])
+
+ useEffect(() => {
+ getAvailableCategories()
+ .then((categories) => {
+ setAvailableCategoryPaths(categories)
+ setAvailableCategories(genCategoryTree(categories))
+ })
+ }, [])
+
+ const onAdd: ChangeEventHandler = (event) => {
+ const categoryId = event.target.value
+ event.target.value = 'none'
+
+ const categoryPath = availableCategoryPaths.find(path => leaf(path).id === categoryId)
+ if (!categoryPath) return
+
+ const category = leaf(categoryPath)
+ addCategoryToSoftware(softwareId, category.id, token).then(() => {
+ // Should we trust that this is the current value or should we re-fetch the values from backend?
+ setCategoryPaths([...categoryPaths, categoryPath])
+ }).catch((error) => {
+ showErrorMessage(error.message)
+ })
+ }
+
+ const onRemove = (categoryId: CategoryID) => {
+ deleteCategoryToSoftware(softwareId, categoryId, token).then(() => {
+ // Should we trust that this is the current value or should we re-fetch the values from backend?
+ setCategoryPaths(categoryPaths.filter(path => leaf(path).id != categoryId))
+ }).catch((error) => {
+ showErrorMessage(error.message)
+ })
+ }
+
+ const OptionsTree = ({items, indent=0}: {items: CategoryTree, indent?: number}) => {
+ return items.map((item, index) => {
+ const isLeaf = item.children.length === 0
+ const isSelected = selectedCategoryIDs.has(item.category.id)
+ const title = ' '.repeat(indent)+item.category.name
+ return
+ {isLeaf ?
+
+ :
+
+ }
+
+
+ })
+ }
+
+ if (availableCategories.length === 0) return null
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/frontend/components/software/edit/information/SoftwareInformationForm.tsx b/frontend/components/software/edit/information/SoftwareInformationForm.tsx
index 1eabc57dd..d935818b4 100644
--- a/frontend/components/software/edit/information/SoftwareInformationForm.tsx
+++ b/frontend/components/software/edit/information/SoftwareInformationForm.tsx
@@ -1,4 +1,6 @@
// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all)
+// SPDX-FileCopyrightText: 2023 Felix Mühlbauer (GFZ)
+// SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences
// SPDX-FileCopyrightText: 2023 dv4all
//
// SPDX-License-Identifier: Apache-2.0
@@ -18,6 +20,7 @@ import AutosaveRepositoryUrl from './AutosaveRepositoryUrl'
import AutosaveSoftwareLicenses from './AutosaveSoftwareLicenses'
import AutosaveSoftwareMarkdown from './AutosaveSoftwareMarkdown'
import AutosaveSoftwareLogo from './AutosaveSoftwareLogo'
+import AutosaveSoftwareCategories from './AutosaveSoftwareCategories'
type SoftwareInformationFormProviderProps = {
editSoftware: EditSoftwareItem
@@ -140,6 +143,11 @@ export default function SoftwareInformationForm({editSoftware}: SoftwareInformat
+
+
({
// MOCK getKeywordsForSoftware, getLicenseForSoftware
const mockGetKeywordsForSoftware = jest.fn(props => Promise.resolve([] as any))
+const mockGetCategoriesForSoftware = jest.fn(props => Promise.resolve([] as CategoriesForSoftware))
const mockGetLicenseForSoftware = jest.fn(props => Promise.resolve([] as any))
jest.mock('~/utils/getSoftware', () => ({
getKeywordsForSoftware: jest.fn(props => mockGetKeywordsForSoftware(props)),
- getLicenseForSoftware: jest.fn(props => mockGetLicenseForSoftware(props))
+ getCategoriesForSoftware: jest.fn(props => mockGetCategoriesForSoftware(props)),
+ getLicenseForSoftware: jest.fn(props => mockGetLicenseForSoftware(props)),
}))
const copySoftware = {
@@ -31,6 +36,10 @@ const mockKeywords = [
...copySoftware.keywords
]
+const mockCategories = [
+ ...copySoftware.categories
+]
+
const mockLicenses = [
...copySoftware.licenses
]
@@ -78,6 +87,7 @@ it('renders software returned by api', async() => {
// mock api responses
mockGetSoftwareToEdit.mockResolvedValueOnce(copySoftware)
mockGetKeywordsForSoftware.mockResolvedValueOnce(mockKeywords)
+ mockGetCategoriesForSoftware.mockResolvedValueOnce(mockCategories as any)
mockGetLicenseForSoftware.mockResolvedValueOnce(mockLicenses)
render(
diff --git a/frontend/components/software/edit/information/useSoftwareToEdit.tsx b/frontend/components/software/edit/information/useSoftwareToEdit.tsx
index f809bbd54..f392e6137 100644
--- a/frontend/components/software/edit/information/useSoftwareToEdit.tsx
+++ b/frontend/components/software/edit/information/useSoftwareToEdit.tsx
@@ -1,8 +1,9 @@
// SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all)
+// SPDX-FileCopyrightText: 2022 - 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences
// SPDX-FileCopyrightText: 2022 - 2023 dv4all
// SPDX-FileCopyrightText: 2022 Christian Meeßen (GFZ)
-// SPDX-FileCopyrightText: 2022 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences
// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all)
+// SPDX-FileCopyrightText: 2023 Felix Mühlbauer (GFZ)
//
// SPDX-License-Identifier: Apache-2.0
@@ -10,7 +11,7 @@ import {useEffect, useState} from 'react'
import {AutocompleteOption} from '../../../../types/AutocompleteOptions'
import {EditSoftwareItem, KeywordForSoftware, License} from '../../../../types/SoftwareTypes'
import {getSoftwareToEdit} from '../../../../utils/editSoftware'
-import {getKeywordsForSoftware, getLicenseForSoftware} from '../../../../utils/getSoftware'
+import {getCategoriesForSoftware, getKeywordsForSoftware, getLicenseForSoftware} from '../../../../utils/getSoftware'
function prepareLicenses(rawLicense: License[]=[]) {
const license:AutocompleteOption[] = rawLicense?.map((item: any) => {
@@ -30,14 +31,16 @@ export async function getSoftwareInfoForEdit({slug, token}: { slug: string, toke
if (software) {
const requests = [
getKeywordsForSoftware(software.id, true, token),
- getLicenseForSoftware(software.id, true, token)
- ]
+ getCategoriesForSoftware(software.id, token),
+ getLicenseForSoftware(software.id, true, token),
+ ] as const
// other api requests
- const [keywords, respLicense,] = await Promise.all(requests)
+ const [keywords, categories, respLicense] = await Promise.all(requests)
const data:EditSoftwareItem = {
...software,
keywords: keywords as KeywordForSoftware[],
+ categories,
licenses: prepareLicenses(respLicense as License[]),
image_b64: null,
image_mime_type: null,
diff --git a/frontend/components/typography/Icon.tsx b/frontend/components/typography/Icon.tsx
new file mode 100644
index 000000000..28ddf78aa
--- /dev/null
+++ b/frontend/components/typography/Icon.tsx
@@ -0,0 +1,39 @@
+// SPDX-FileCopyrightText: 2023 Felix Mühlbauer (GFZ)
+// SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences
+//
+// SPDX-License-Identifier: Apache-2.0
+
+import QuestionMarkIcon from '@mui/icons-material/QuestionMark'
+import CategoryIcon from '@mui/icons-material/Category'
+import ScienceIcon from '@mui/icons-material/Science'
+
+import type SvgIcon from '@mui/material/SvgIcon'
+
+// => This is a workaround before we can load icons dynamically (see #975).
+
+// create a component map with lowercase icon names as index
+const iconMap = (() => {
+ const icons = {
+ QuestionMarkIcon,
+ CategoryIcon,
+ ScienceIcon,
+ // => extend the list above if necessary
+ } as Record
+ // magically generate icon names
+ return Object.keys(icons).reduce((map, key)=>{
+ const iconName = key.toLowerCase().slice(0, -4)
+ map[iconName] = icons[key]
+ return map
+ },{} as Record)
+})()
+
+type IconProps = {
+ name: string
+} & React.ComponentProps
+
+export const Icon = ({name, ...props} : IconProps) => {
+ const MuiIcon = (name && iconMap[name.toLowerCase()]) || QuestionMarkIcon
+ return
+}
+
+
diff --git a/frontend/components/typography/SidebarHeadline.tsx b/frontend/components/typography/SidebarHeadline.tsx
new file mode 100644
index 000000000..335cad340
--- /dev/null
+++ b/frontend/components/typography/SidebarHeadline.tsx
@@ -0,0 +1,17 @@
+// SPDX-FileCopyrightText: 2023 Felix Mühlbauer (GFZ)
+// SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences
+//
+// SPDX-License-Identifier: Apache-2.0
+
+import {Icon} from './Icon'
+
+type SidebarHeadlineProps = {
+ iconName?: string
+ title: string
+}
+export const SidebarHeadline = ({iconName, title}: SidebarHeadlineProps) => {
+ return
+ {iconName && }
+ {title}
+
+}
diff --git a/frontend/package.json b/frontend/package.json
index 947ecd38e..2aaff9ee2 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -5,7 +5,7 @@
"scripts": {
"dev": "next dev",
"dev:turbo": "next dev --turbo",
- "dev:docker": "NODE_ENV=docker NODE_OPTIONS='--inspect=0.0.0.0:9229' next dev",
+ "dev:docker": "NODE_ENV=docker next dev # backup: NODE_ENV=docker NODE_OPTIONS='--inspect=0.0.0.0:9229' next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
diff --git a/frontend/pages/software/[slug]/index.tsx b/frontend/pages/software/[slug]/index.tsx
index b06ec18b9..61b9ed326 100644
--- a/frontend/pages/software/[slug]/index.tsx
+++ b/frontend/pages/software/[slug]/index.tsx
@@ -1,8 +1,9 @@
// SPDX-FileCopyrightText: 2021 - 2023 Dusan Mijatovic (dv4all)
// SPDX-FileCopyrightText: 2021 - 2023 dv4all
+// SPDX-FileCopyrightText: 2022 - 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences
// SPDX-FileCopyrightText: 2022 Christian Meeßen (GFZ)
-// SPDX-FileCopyrightText: 2022 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences
// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center)
+// SPDX-FileCopyrightText: 2023 Felix Mühlbauer (GFZ)
// SPDX-FileCopyrightText: 2023 Netherlands eScience Center
//
// SPDX-License-Identifier: Apache-2.0
@@ -39,6 +40,7 @@ import {
getRemoteMarkdown,
ContributorMentionCount,
getKeywordsForSoftware,
+ getCategoriesForSoftware,
getRelatedProjectsForSoftware,
getReleasesForSoftware,
SoftwareVersion,
@@ -51,6 +53,7 @@ import {getRelatedSoftwareForSoftware} from '~/utils/editRelatedSoftware'
import {getMentionsForSoftware} from '~/utils/editMentions'
import {getParticipatingOrganisations} from '~/utils/editOrganisation'
import {
+ CategoriesForSoftware,
KeywordForSoftware, License, RepositoryInfo,
SoftwareItem, SoftwareOverviewItemProps
} from '~/types/SoftwareTypes'
@@ -66,6 +69,7 @@ interface SoftwareIndexData extends ScriptProps{
software: SoftwareItem
releases: SoftwareVersion[]
keywords: KeywordForSoftware[]
+ categories: CategoriesForSoftware
licenseInfo: License[]
repositoryInfo: RepositoryInfo
softwareIntroCounts: ContributorMentionCount
@@ -82,7 +86,7 @@ export default function SoftwareIndexPage(props:SoftwareIndexData) {
const [author, setAuthor] = useState('')
// extract data from props
const {
- software, releases, keywords,
+ software, releases, keywords, categories,
licenseInfo, repositoryInfo, softwareIntroCounts,
mentions, testimonials, contributors,
relatedSoftware, relatedProjects, isMaintainer,
@@ -149,6 +153,7 @@ export default function SoftwareIndexPage(props:SoftwareIndexData) {
description={software?.description ?? ''}
description_type={software?.description_type}
keywords={keywords}
+ categories={categories}
licenses={licenseInfo}
languages={repositoryInfo?.languages}
repository={repositoryInfo?.url}
@@ -215,6 +220,7 @@ export async function getServerSideProps(context:GetServerSidePropsContext) {
const [
releases,
keywords,
+ categories,
licenseInfo,
repositoryInfo,
softwareIntroCounts,
@@ -230,6 +236,8 @@ export async function getServerSideProps(context:GetServerSidePropsContext) {
getReleasesForSoftware(software.id,token),
// keywords
getKeywordsForSoftware(software.id,false,token),
+ // categories
+ getCategoriesForSoftware(software.id, token),
// licenseInfo
getLicenseForSoftware(software.id, false, token),
// repositoryInfo: url, languages and commits
@@ -257,6 +265,7 @@ export async function getServerSideProps(context:GetServerSidePropsContext) {
software,
releases,
keywords,
+ categories,
licenseInfo,
repositoryInfo,
softwareIntroCounts,
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index e4e4fda5c..baf9a7677 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -1,6 +1,6 @@
{
"compilerOptions": {
- "target": "es5",
+ "target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
diff --git a/frontend/types/Category.ts b/frontend/types/Category.ts
new file mode 100644
index 000000000..de2295785
--- /dev/null
+++ b/frontend/types/Category.ts
@@ -0,0 +1,23 @@
+// SPDX-FileCopyrightText: 2023 Felix Mühlbauer (GFZ)
+// SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences
+//
+// SPDX-License-Identifier: Apache-2.0
+
+export type CategoryID = string // NOSONAR ignore: typescript:S6564
+
+export type CategoryEntry = {
+ id: CategoryID
+ parent: CategoryID | null
+ short_name: string
+ name: string
+ icon?: string
+}
+
+
+export type CategoryPath = CategoryEntry[]
+
+export type CategoryTreeLevel = {
+ category: CategoryEntry
+ children: CategoryTreeLevel[]
+}
+export type CategoryTree = CategoryTreeLevel[]
diff --git a/frontend/types/SoftwareTypes.ts b/frontend/types/SoftwareTypes.ts
index a8844906d..a6dfa3a0d 100644
--- a/frontend/types/SoftwareTypes.ts
+++ b/frontend/types/SoftwareTypes.ts
@@ -5,6 +5,7 @@
// SPDX-FileCopyrightText: 2022 - 2023 Netherlands eScience Center
// SPDX-FileCopyrightText: 2022 - 2023 dv4all
// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center)
+// SPDX-FileCopyrightText: 2023 Felix Mühlbauer (GFZ)
//
// SPDX-License-Identifier: Apache-2.0
@@ -13,6 +14,7 @@
*/
import {AutocompleteOption} from './AutocompleteOptions'
+import {CategoryPath} from './Category'
import {Status} from './Organisation'
export type CodePlatform = 'github' | 'gitlab' | 'bitbucket' | 'other'
@@ -108,6 +110,7 @@ export const SoftwarePropsToSave = [
export type EditSoftwareItem = SoftwareItem & {
keywords: KeywordForSoftware[]
+ categories: CategoriesForSoftware
licenses: AutocompleteOption[]
image_b64: string | null
image_mime_type: string | null
@@ -127,6 +130,8 @@ export type KeywordForSoftware = {
pos?: number
}
+export type CategoriesForSoftware = CategoryPath[]
+
/**
* LiCENSES
*/
diff --git a/frontend/utils/__mocks__/getSoftware.ts b/frontend/utils/__mocks__/getSoftware.ts
new file mode 100644
index 000000000..98d8260f3
--- /dev/null
+++ b/frontend/utils/__mocks__/getSoftware.ts
@@ -0,0 +1,102 @@
+// SPDX-FileCopyrightText: 2023 Netherlands eScience Center
+// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center)
+//
+// SPDX-License-Identifier: Apache-2.0
+
+import {CategoriesForSoftware,} from '~/types/SoftwareTypes'
+import {CategoryID, CategoryPath} from '~/types/Category'
+
+export async function getSoftwareList({url,token}:{url:string,token?:string }){
+ return []
+}
+
+export async function getSoftwareItem(slug:string|undefined, token?:string){
+ return []
+}
+
+export async function getRepostoryInfoForSoftware(software: string | undefined, token?: string) {
+ return []
+}
+
+
+/**
+ * CITATIONS
+ * @param uuid as software_id
+ * @returns SoftwareVersion[] | null
+ */
+
+export type SoftwareVersion = {
+ doi: string,
+ version: string,
+ doi_registration_date: string
+}
+
+export async function getReleasesForSoftware(uuid:string,token?:string){
+ return []
+}
+
+export async function getKeywordsForSoftware(uuid:string,frontend?:boolean,token?:string){
+ return []
+}
+
+export async function getCategoriesForSoftware(software_id: string, token?: string): Promise {
+ return []
+}
+
+export async function getAvailableCategories(): Promise {
+ return []
+}
+
+export async function addCategoryToSoftware(softwareId: string, categoryId: CategoryID, token: string) {
+ return []
+}
+
+export async function deleteCategoryToSoftware(softwareId: string, categoryId: CategoryID, token: string) {
+ return null
+}
+
+
+/**
+ * LICENSE
+ */
+
+export type License = {
+ id:string
+ software:string
+ license: string
+}
+
+export async function getLicenseForSoftware(uuid:string,frontend?:boolean,token?:string){
+ return []
+}
+
+/**
+ * Contributors and mentions counts
+ */
+
+export type ContributorMentionCount = {
+ id: string
+ contributor_cnt: number | null
+ mention_cnt: number | null
+}
+
+export async function getContributorMentionCount(uuid: string,token?: string){
+ return []
+}
+
+/**
+ * REMOTE MARKDOWN FILE
+ */
+export async function getRemoteMarkdown(url: string) {
+ return []
+}
+
+export function getRemoteMarkdownTest(url: string) {
+ return []
+}
+
+// RELATED PROJECTS FOR SORFTWARE
+export async function getRelatedProjectsForSoftware({software, token, frontend, approved=true}:
+ { software: string, token?: string, frontend?: boolean, approved?:boolean }) {
+ return []
+}
diff --git a/frontend/utils/categories.ts b/frontend/utils/categories.ts
new file mode 100644
index 000000000..792d90b3a
--- /dev/null
+++ b/frontend/utils/categories.ts
@@ -0,0 +1,50 @@
+// SPDX-FileCopyrightText: 2023 Felix Mühlbauer (GFZ)
+// SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences
+//
+// SPDX-License-Identifier: Apache-2.0
+
+import {useMemo} from 'react'
+import {CategoryEntry, CategoryPath, CategoryTree, CategoryTreeLevel} from '~/types/Category'
+import logger from './logger'
+
+export const leaf = (list: T[]) => list[list.length - 1]
+
+const compareCategoryEntry = (p1: CategoryEntry, p2: CategoryEntry) => p1.short_name.localeCompare(p2.short_name)
+const compareCategoryTreeLevel = (p1: CategoryTreeLevel, p2: CategoryTreeLevel) => compareCategoryEntry(p1.category, p2.category)
+
+const categoryTreeSort = (tree: CategoryTree) => {
+ tree.sort(compareCategoryTreeLevel)
+ for (const item of tree) {
+ categoryTreeSort(item.children)
+ }
+}
+
+export const genCategoryTree = (categories: CategoryPath[]) : CategoryTree => {
+ const tree: CategoryTree = []
+ try {
+ for (const path of categories) {
+ let cursor = tree
+ for (const item of path) {
+ const found = cursor.find(el => el.category.id == item.id)
+ if (found) {
+ cursor = found.children
+ } else {
+ const sub: CategoryTreeLevel = {category: item, children: []}
+ cursor.push(sub)
+ cursor = sub.children
+ }
+ }
+ }
+
+ categoryTreeSort(tree)
+
+ return tree
+ } catch (e: any) {
+ logger(`genCategoryTree failed to process data: ${e.message}`, 'error')
+ return []
+ }
+}
+
+export const useCategoryTree = (categories: CategoryPath[]) : CategoryTree => {
+ return useMemo(() => genCategoryTree(categories), [categories])
+}
diff --git a/frontend/utils/getSoftware.ts b/frontend/utils/getSoftware.ts
index 3cb312f60..e583f179b 100644
--- a/frontend/utils/getSoftware.ts
+++ b/frontend/utils/getSoftware.ts
@@ -3,14 +3,17 @@
// SPDX-FileCopyrightText: 2022 - 2023 Ewan Cahen (Netherlands eScience Center)
// SPDX-FileCopyrightText: 2022 - 2023 Netherlands eScience Center
// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center)
+// SPDX-FileCopyrightText: 2023 Felix Mühlbauer (GFZ)
+// SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences
//
// SPDX-License-Identifier: Apache-2.0
import logger from './logger'
-import {KeywordForSoftware, RepositoryInfo, SoftwareItem, SoftwareOverviewItemProps} from '../types/SoftwareTypes'
+import {CategoriesForSoftware, KeywordForSoftware, RepositoryInfo, SoftwareItem, SoftwareOverviewItemProps} from '../types/SoftwareTypes'
import {extractCountFromHeader} from './extractCountFromHeader'
import {createJsonHeaders, getBaseUrl} from './fetchHelpers'
import {RelatedProjectForSoftware} from '~/types/Project'
+import {CategoryID, CategoryPath} from '~/types/Category'
/*
* Software list for the software overview page
@@ -33,7 +36,7 @@ export async function getSoftwareList({url,token}:{url:string,token?:string }){
count: extractCountFromHeader(resp.headers),
data: json
}
- } else{
+ } else {
logger(`getSoftwareList failed: ${resp.status} ${resp.statusText} ${url}`, 'warn')
return {
count:0,
@@ -151,7 +154,6 @@ export async function getReleasesForSoftware(uuid:string,token?:string){
}
}
-
export async function getKeywordsForSoftware(uuid:string,frontend?:boolean,token?:string){
try{
// this request is always perfomed from backend
@@ -179,6 +181,90 @@ export async function getKeywordsForSoftware(uuid:string,frontend?:boolean,token
}
}
+function prepareQueryURL(path: string, params?: Record) {
+ const baseURL = getBaseUrl()
+ logger(`prepareQueryURL baseURL:${baseURL}`)
+ let url = `${baseURL}${path}`
+ if (params) {
+ const paramStr = Object.keys(params).map((key) => `${key}=${encodeURIComponent(params[key])}`).join('&')
+ if (paramStr) url += '?' + paramStr
+ }
+ return url
+}
+
+export async function getCategoriesForSoftware(software_id: string, token?: string): Promise {
+ try {
+ const url = prepareQueryURL('/rpc/category_paths_by_software_expanded', {software_id})
+ const resp = await fetch(url, {
+ method: 'GET',
+ headers: createJsonHeaders(token)
+ })
+ if (resp.status === 200) {
+ const data = await resp.json()
+ logger(`getCategoriesForSoftware response: ${JSON.stringify(data)}`)
+ return data
+ } else if (resp.status === 404) {
+ logger(`getCategoriesForSoftware: 404 [${url}]`, 'error')
+ }
+ } catch (e: any) {
+ logger(`getCategoriesForSoftware: ${e?.message}`, 'error')
+ }
+ return []
+}
+
+export async function getAvailableCategories(): Promise {
+ try {
+ const url = prepareQueryURL('/rpc/available_categories_expanded')
+ const resp = await fetch(url, {
+ method: 'GET',
+ })
+ if (resp.status === 200) {
+ const data = await resp.json()
+ return data
+ } else if (resp.status === 404) {
+ logger(`getAvailableCategories: 404 [${url}]`, 'error')
+ }
+ } catch (e: any) {
+ logger(`getAvailableCategories: ${e?.message}`, 'error')
+ }
+ return []
+}
+
+export async function addCategoryToSoftware(softwareId: string, categoryId: CategoryID, token: string) {
+ const url = prepareQueryURL('/category_for_software')
+ const data = {software_id: softwareId, category_id: categoryId}
+
+ const resp = await fetch(url, {
+ method: 'POST',
+ headers: {
+ ...createJsonHeaders(token),
+ },
+ body: JSON.stringify(data),
+ })
+ logger(`addCategoryToSoftware: resp: ${resp}`)
+ if (resp.ok) {
+ return null
+ }
+ throw new Error(`API returned: ${resp.status} ${resp.statusText}`)
+}
+
+export async function deleteCategoryToSoftware(softwareId: string, categoryId: CategoryID, token: string) {
+ const url = prepareQueryURL(`/category_for_software?software_id=eq.${softwareId}&category_id=eq.${categoryId}`)
+
+ const resp = await fetch(url, {
+ method: 'DELETE',
+ headers: {
+ ...createJsonHeaders(token),
+ },
+ })
+ logger(`deleteCategoryToSoftware: resp: ${resp}`)
+ if (resp.ok) {
+ return null
+ }
+ throw new Error(`API returned: ${resp.status} ${resp.statusText}`)
+}
+
+
/**
* LICENSE
*/