From a8acbc81f3bac2f256935d85fdd01b84cb8ae0e1 Mon Sep 17 00:00:00 2001
From: "Dusan Mijatovic (PC2020)"
Date: Tue, 19 Nov 2024 12:56:00 +0100
Subject: [PATCH 1/4] chore: adapt data generation category labels to be able
to see origin: global, organisation, community
---
data-generation/main.js | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/data-generation/main.js b/data-generation/main.js
index e5481d06b..5f13e232b 100644
--- a/data-generation/main.js
+++ b/data-generation/main.js
@@ -336,7 +336,7 @@ async function generateCategories(idsCommunities, idsOrganisations, maxDepth = 3
async function generateAndSaveCategoriesForEntity(idCommunity, idOrganisation, maxDepth) {
return new Promise(async res => {
let parentIdsAndFlags = [
- {id: null, forSoftware: faker.datatype.boolean(), forProjects: faker.datatype.boolean()},
+ {id: null, forSoftware: faker.datatype.boolean(), forProjects: idCommunity ? false : faker.datatype.boolean()},
];
const idsAndFlags = [];
for (let level = 1; level <= maxDepth; level++) {
@@ -347,8 +347,17 @@ async function generateAndSaveCategoriesForEntity(idCommunity, idOrganisation, m
toGenerateCount += 1;
}
for (let i = 0; i < toGenerateCount; i++) {
- const name = `Parent ${parent.id}, level ${level}, item ${i + 1}`;
- const shortName = `Level ${level}, item ${i + 1}`;
+ let name = `Global, level ${level}, item ${i + 1}${parent.id ? `, parent${parent.id.substring(0,5)}` : ""}`;
+ let shortName = `G-${level}-${i + 1}${parent.id ? `, P-${parent.id.substring(0,5)}` : ""}`;
+
+ if (idCommunity){
+ name = `Level ${level}, item ${i + 1}, community-${idCommunity.substring(0,5)}${parent.id ? `, parent-${parent.id.substring(0,5)}` : ""}`;
+ shortName = `L-${level}-${i + 1}, C-${idCommunity.substring(0,5)}${parent.id ? `, P-${parent.id.substring(0,5)}` : ""}`;
+ }else if (idOrganisation){
+ name = `Level ${level}, item ${i + 1}, organisation-${idOrganisation.substring(0,5)}${parent.id ? `, parent-${parent.id.substring(0,5)}` : ""}`;
+ shortName = `L-${level}-${i + 1}, O-${idOrganisation.substring(0,5)}${parent.id ? `, P-${parent.id.substring(0,5)}` : ""}`;
+ }
+
const body = {
community: idCommunity,
organisation: idOrganisation,
From 83cecc47a00679f65604c92224e43beb4056dc96 Mon Sep 17 00:00:00 2001
From: "Dusan Mijatovic (PC2020)"
Date: Tue, 19 Nov 2024 13:36:53 +0100
Subject: [PATCH 2/4] feat: remove categories when organisation removed from
software or projects
---
frontend/components/projects/edit/organisations/index.tsx | 4 ++++
frontend/components/software/edit/organisations/index.tsx | 8 ++++++--
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/frontend/components/projects/edit/organisations/index.tsx b/frontend/components/projects/edit/organisations/index.tsx
index 91ab12746..4002ebca8 100644
--- a/frontend/components/projects/edit/organisations/index.tsx
+++ b/frontend/components/projects/edit/organisations/index.tsx
@@ -34,6 +34,7 @@ import useProjectContext from '../useProjectContext'
import useParticipatingOrganisations from './useParticipatingOrganisations'
import {cfgOrganisations as config} from './config'
import ProjectCategoriesDialog from './ProjectCategoriesDialog'
+import {removeOrganisationCategoriesFromProject} from './apiProjectOrganisations'
export default function ProjectOrganisations() {
const {token,user} = useSession()
@@ -190,6 +191,9 @@ export default function ProjectOrganisations() {
const organisation = organisations[pos]
// if it has id
if (organisation?.id) {
+ // remove categories from project - do not wait for result
+ removeOrganisationCategoriesFromProject(project.id,organisation.id,token)
+ // remove organisation from project
const resp = await deleteOrganisationFromProject({
project: project.id,
organisation: organisation.id,
diff --git a/frontend/components/software/edit/organisations/index.tsx b/frontend/components/software/edit/organisations/index.tsx
index 7ce5db665..6d35f93e2 100644
--- a/frontend/components/software/edit/organisations/index.tsx
+++ b/frontend/components/software/edit/organisations/index.tsx
@@ -40,6 +40,7 @@ import {
deleteOrganisationFromSoftware, patchOrganisationPositions
} from './organisationForSoftware'
import SoftwareCategoriesDialog from './SoftwareCategoriesDialog'
+import {removeOrganisationCategoriesFromSoftware} from './apiSoftwareOrganisations'
export type OrganisationModalStates = ModalStates & {
categories: T
@@ -211,9 +212,12 @@ export default function SoftwareOrganisations() {
// get organisation
const organisation = organisations[pos]
// if it has id
- if (organisation?.id) {
+ if (organisation?.id && software?.id) {
+ // remove categories from software - do not wait for result
+ removeOrganisationCategoriesFromSoftware(software?.id, organisation.id, token)
+ // remove organisation from software
const resp = await deleteOrganisationFromSoftware({
- software: software?.id ?? undefined,
+ software: software?.id,
organisation: organisation.id,
token
})
From 1ac3e64c113d572b36eeea5158c6a8a01fc80729 Mon Sep 17 00:00:00 2001
From: "Dusan Mijatovic (PC2020)"
Date: Tue, 19 Nov 2024 18:14:00 +0100
Subject: [PATCH 3/4] feat: add all category levels select to organisation
categories feat: add categories selection on all levels to community
categories
---
data-generation/main.js | 24 ++--
.../components/category/CategoriesDialog.tsx | 78 ++---------
.../category/CategoriesDialogBody.tsx | 113 +++++++++++++++
.../category/CategoriesWithHeadlines.tsx | 2 +-
.../components/category/CategoryEditForm.tsx | 3 +-
frontend/components/category/CategoryList.tsx | 127 +++++++++++++++++
.../components/category/CategoryTable.tsx | 10 +-
.../{software => category}/TreeSelect.tsx | 1 +
.../components/category/useCategoryTree.tsx | 39 ++++++
.../category/useReorderedCategories.tsx} | 51 ++-----
.../components/projects/ProjectCategories.tsx | 2 +-
.../EditProjectOrganisationsIndex.test.tsx | 2 +
.../__mocks__/apiProjectOrganisations.ts | 18 +++
.../organisations/useProjectCategories.tsx | 4 +-
.../components/software/CategoriesSection.tsx | 5 +-
.../CommunityAddCategoriesDialog.tsx | 2 +-
.../communities/CommunityCategoriesDialog.tsx | 70 ++++++++++
.../software/edit/communities/index.tsx | 130 +++++++++++-------
.../communities/useCommunityCategories.tsx | 118 ++++++++++++++++
.../communities/useSoftwareCommunities.tsx | 3 +
.../edit/links/AutosaveSoftwareCategories.tsx | 9 +-
.../edit/links/EditSoftwareMetadataInputs.tsx | 2 +-
.../software/edit/links/SoftwareLinksInfo.tsx | 2 +-
.../EditSoftwareOrganisationsIndex.test.tsx | 2 +
.../__mocks__/apiSoftwareOrganisations.ts | 40 ++++++
.../organisations/apiSoftwareOrganisations.ts | 26 +++-
.../organisations/useSoftwareCategories.tsx | 28 ++--
27 files changed, 706 insertions(+), 205 deletions(-)
create mode 100644 frontend/components/category/CategoriesDialogBody.tsx
create mode 100644 frontend/components/category/CategoryList.tsx
rename frontend/components/{software => category}/TreeSelect.tsx (96%)
create mode 100644 frontend/components/category/useCategoryTree.tsx
rename frontend/{utils/categories.ts => components/category/useReorderedCategories.tsx} (60%)
create mode 100644 frontend/components/projects/edit/organisations/__mocks__/apiProjectOrganisations.ts
create mode 100644 frontend/components/software/edit/communities/CommunityCategoriesDialog.tsx
create mode 100644 frontend/components/software/edit/communities/useCommunityCategories.tsx
create mode 100644 frontend/components/software/edit/organisations/__mocks__/apiSoftwareOrganisations.ts
diff --git a/data-generation/main.js b/data-generation/main.js
index 5f13e232b..ca6a94eb0 100644
--- a/data-generation/main.js
+++ b/data-generation/main.js
@@ -336,7 +336,11 @@ async function generateCategories(idsCommunities, idsOrganisations, maxDepth = 3
async function generateAndSaveCategoriesForEntity(idCommunity, idOrganisation, maxDepth) {
return new Promise(async res => {
let parentIdsAndFlags = [
- {id: null, forSoftware: faker.datatype.boolean(), forProjects: idCommunity ? false : faker.datatype.boolean()},
+ {
+ id: null,
+ forSoftware: faker.datatype.boolean(),
+ forProjects: idCommunity ? false : faker.datatype.boolean(),
+ },
];
const idsAndFlags = [];
for (let level = 1; level <= maxDepth; level++) {
@@ -347,15 +351,15 @@ async function generateAndSaveCategoriesForEntity(idCommunity, idOrganisation, m
toGenerateCount += 1;
}
for (let i = 0; i < toGenerateCount; i++) {
- let name = `Global, level ${level}, item ${i + 1}${parent.id ? `, parent${parent.id.substring(0,5)}` : ""}`;
- let shortName = `G-${level}-${i + 1}${parent.id ? `, P-${parent.id.substring(0,5)}` : ""}`;
-
- if (idCommunity){
- name = `Level ${level}, item ${i + 1}, community-${idCommunity.substring(0,5)}${parent.id ? `, parent-${parent.id.substring(0,5)}` : ""}`;
- shortName = `L-${level}-${i + 1}, C-${idCommunity.substring(0,5)}${parent.id ? `, P-${parent.id.substring(0,5)}` : ""}`;
- }else if (idOrganisation){
- name = `Level ${level}, item ${i + 1}, organisation-${idOrganisation.substring(0,5)}${parent.id ? `, parent-${parent.id.substring(0,5)}` : ""}`;
- shortName = `L-${level}-${i + 1}, O-${idOrganisation.substring(0,5)}${parent.id ? `, P-${parent.id.substring(0,5)}` : ""}`;
+ let name = `Global, level ${level}, item ${i + 1}${parent.id ? `, parent${parent.id.substring(0, 5)}` : ''}`;
+ let shortName = `G-${level}-${i + 1}${parent.id ? `, P-${parent.id.substring(0, 5)}` : ''}`;
+
+ if (idCommunity) {
+ name = `Level ${level}, item ${i + 1}, community-${idCommunity.substring(0, 5)}${parent.id ? `, parent-${parent.id.substring(0, 5)}` : ''}`;
+ shortName = `L-${level}-${i + 1}, C-${idCommunity.substring(0, 5)}${parent.id ? `, P-${parent.id.substring(0, 5)}` : ''}`;
+ } else if (idOrganisation) {
+ name = `Level ${level}, item ${i + 1}, organisation-${idOrganisation.substring(0, 5)}${parent.id ? `, parent-${parent.id.substring(0, 5)}` : ''}`;
+ shortName = `L-${level}-${i + 1}, O-${idOrganisation.substring(0, 5)}${parent.id ? `, P-${parent.id.substring(0, 5)}` : ''}`;
}
const body = {
diff --git a/frontend/components/category/CategoriesDialog.tsx b/frontend/components/category/CategoriesDialog.tsx
index 40bac32ad..d43b2d5f4 100644
--- a/frontend/components/category/CategoriesDialog.tsx
+++ b/frontend/components/category/CategoriesDialog.tsx
@@ -3,10 +3,11 @@
//
// SPDX-License-Identifier: Apache-2.0
+import {useEffect, useState} from 'react'
+
import Dialog from '@mui/material/Dialog'
import DialogContent from '@mui/material/DialogContent'
import DialogTitle from '@mui/material/DialogTitle'
-import Alert from '@mui/material/Alert'
import DialogActions from '@mui/material/DialogActions'
import Button from '@mui/material/Button'
import useMediaQuery from '@mui/material/useMediaQuery'
@@ -14,9 +15,7 @@ import SaveIcon from '@mui/icons-material/Save'
import {TreeNode} from '~/types/TreeNode'
import {CategoryEntry} from '~/types/Category'
-import ContentLoader from '../layout/ContentLoader'
-import {RecursivelyGenerateItems} from '~/components/software/TreeSelect'
-import {useEffect, useState} from 'react'
+import CategoriesDialogBody from './CategoriesDialogBody'
type CategoriesDialogProps={
title: string,
@@ -49,72 +48,10 @@ export default function CategoriesDialog({
}
},[selected,state])
- function isSelected(node: TreeNode) {
- const val = node.getValue()
- return selectedCategoryIds.has(val.id)
- }
-
- function textExtractor(value: CategoryEntry) {
- return value.name
- }
-
- function keyExtractor(value: CategoryEntry) {
- return value.id
- }
-
- function onSelect(node: TreeNode) {
- const val = node.getValue()
- if (selectedCategoryIds.has(val.id)) {
- selectedCategoryIds.delete(val.id)
- } else {
- selectedCategoryIds.add(val.id)
- }
- setSelectedCategoryIds(new Set(selectedCategoryIds))
- }
-
function isSaveDisabled(){
return categories === null || categories.length === 0 || state !== 'ready'
}
- function renderDialogContent(): JSX.Element {
- switch (state) {
- case 'loading':
- case 'saving':
- return (
-
-
-
- )
-
- case 'error':
- return (
-
- {errorMsg ?? '500 - Unexpected error'}
-
- )
-
- case 'ready':
- return (
- <>
- {(categories === null || categories.length === 0)
- ?
-
- {noItemsMsg}
-
- :
-
- }
- >
- )
- }
- }
-
return (
+ Are you sure you want to remove {modal.delete.name ?? ''}? This will also delete all related (if any) categories.
}
- onCancel={()=>setModal({open:false,id:null,name:null})}
+ onCancel={closeModals}
onDelete={()=>{
// only if id present
- if(modal.id) {
- deleteCommunity(modal.id)
+ if(modal.delete.id) {
+ leaveCommunity({
+ software: software.id,
+ community: modal.delete.id
+ })
}
// we close modal anyway
- setModal({open:false,id:null,name:null})
+ closeModals()
}}
/>
+ : null
}
- {openCategoryModalProps!== null &&
- {setOpenCategoryModalProps(null); setSelectedCommunity(null)}}
- onConfirm={openCategoryModalProps.onSave}
- autoConfirm={openCategoryModalProps.autoConfirm ?? false}
- />
+ {modal.categories.open && modal.categories.community ?
+ {
+ // if new community we also need to join
+ if (modal.categories.community && modal.categories.edit===false){
+ joinCommunity({
+ software: software.id,
+ community: modal.categories.community
+ })
+ }
+ closeModals()
+ }}
+ />
+ :null
}
>
)
diff --git a/frontend/components/software/edit/communities/useCommunityCategories.tsx b/frontend/components/software/edit/communities/useCommunityCategories.tsx
new file mode 100644
index 000000000..8663d2903
--- /dev/null
+++ b/frontend/components/software/edit/communities/useCommunityCategories.tsx
@@ -0,0 +1,118 @@
+// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center)
+// SPDX-FileCopyrightText: 2024 Netherlands eScience Center
+//
+// SPDX-License-Identifier: Apache-2.0
+
+import {useEffect, useState} from 'react'
+import {useSession} from '~/auth'
+import {loadCategoryRoots} from '~/components/category/apiCategories'
+import {CategoryEntry} from '~/types/Category'
+import {TreeNode} from '~/types/TreeNode'
+import {getCategoryForSoftwareIds} from '~/utils/getSoftware'
+import {saveSoftwareCategories, SoftwareCategories} from '../organisations/apiSoftwareOrganisations'
+import {removeCommunityCategoriesFromSoftware} from './apiSoftwareCommunities'
+
+type UseSoftwareCommunityCategoriesProps={
+ communityId:string|null,
+ softwareId:string
+}
+
+export default function useCommunityCategories({
+ communityId,softwareId
+}:UseSoftwareCommunityCategoriesProps){
+ const {token} = useSession()
+ const [categories, setCategories] = useState[] | null>(null)
+ const [error, setError] = useState(null)
+ const [state, setState] = useState<'loading' | 'error' | 'ready' | 'saving'>('loading')
+ const [selectedCategoryIds, setSelectedCategoryIds] = useState>(new Set())
+ const [availableCategoryIds, setAvailableCategoryIds] = useState>(new Set())
+
+ // console.group('useCommunityCategories')
+ // console.log('state...',state)
+ // console.log('categories...', categories)
+ // console.groupEnd()
+
+ useEffect(() => {
+ let abort = false
+ if (communityId && softwareId && token){
+ Promise.all([
+ loadCategoryRoots({community:communityId}),
+ getCategoryForSoftwareIds(softwareId, token)
+ ])
+ .then(([roots,selected]) => {
+ // filter top level categories for software (only top level items have this flag)
+ // collect tree leaves ids (end nodes)
+ const availableIds = new Set()
+ roots.forEach(root=>{
+ root.forEach(node=>{
+ availableIds.add(node.getValue().id)
+ })
+ })
+ if (abort) return
+ // save values
+ setAvailableCategoryIds(availableIds)
+ setCategories(roots)
+ setSelectedCategoryIds(selected)
+ })
+ .catch(e => {
+ if (abort) return
+ setError(`Couldn't load categories: ${e}`)
+ setState('error')
+ })
+ .finally(()=>{
+ if (abort) return
+ setState('ready')
+ })
+ }
+ return ()=>{abort=true}
+ }, [communityId, softwareId, token])
+
+
+ async function saveCommunityCategories(selected:Set,onComplete:()=>void) {
+ // delete old selection
+ if (communityId){
+ const deleteErrorMessage = await removeCommunityCategoriesFromSoftware(softwareId, communityId, token)
+ if (deleteErrorMessage !== null) {
+ setError(`Failed to save categories: ${deleteErrorMessage}`)
+ setState('error')
+ return
+ }
+ }
+
+ if (selectedCategoryIds.size === 0) {
+ onComplete()
+ return
+ }
+
+ // generate new collection
+ const categoriesArrayToSave: SoftwareCategories[] = []
+ selected.forEach(id => {
+ if (availableCategoryIds.has(id)) {
+ categoriesArrayToSave.push({software_id: softwareId, category_id: id})
+ }
+ })
+
+ // save community categories for software (if any)
+ if (categoriesArrayToSave.length > 0){
+ const resp = await saveSoftwareCategories(categoriesArrayToSave,token)
+ // debugger
+ if (resp.status===200) {
+ // signal we are done
+ onComplete()
+ } else {
+ setError(`Failed to save categories: ${resp.message}`)
+ setState('error')
+ }
+ }else{
+ onComplete()
+ }
+ }
+
+ return {
+ categories,
+ selectedCategoryIds,
+ error,
+ state,
+ saveCommunityCategories
+ }
+}
diff --git a/frontend/components/software/edit/communities/useSoftwareCommunities.tsx b/frontend/components/software/edit/communities/useSoftwareCommunities.tsx
index c610aed50..a7655a187 100644
--- a/frontend/components/software/edit/communities/useSoftwareCommunities.tsx
+++ b/frontend/components/software/edit/communities/useSoftwareCommunities.tsx
@@ -65,6 +65,9 @@ export function useSoftwareCommunities(software:string){
return
}
+ // remove all community categories without waiting
+ void removeCommunityCategoriesFromSoftware(software, community, token)
+
const resp = await removeSoftwareFromCommunity({
software,
community,
diff --git a/frontend/components/software/edit/links/AutosaveSoftwareCategories.tsx b/frontend/components/software/edit/links/AutosaveSoftwareCategories.tsx
index e74d489e2..72f86438c 100644
--- a/frontend/components/software/edit/links/AutosaveSoftwareCategories.tsx
+++ b/frontend/components/software/edit/links/AutosaveSoftwareCategories.tsx
@@ -7,15 +7,16 @@
// SPDX-License-Identifier: Apache-2.0
import {Fragment, useMemo, useState} from 'react'
+import {useSession} from '~/auth'
import {CategoryEntry} from '~/types/Category'
-import {categoryTreeNodesSort, ReorderedCategories} from '~/utils/categories'
-import TreeSelect from '~/components/software/TreeSelect'
import {TreeNode} from '~/types/TreeNode'
import {addCategoryToSoftware, deleteCategoryToSoftware} from '~/utils/getSoftware'
-import {useSession} from '~/auth'
+import EditSectionTitle from '~/components/layout/EditSectionTitle'
+import {categoryTreeNodesSort} from '~/components/category/useCategoryTree'
+import TreeSelect from '~/components/category/TreeSelect'
import {CategoryTreeLevel} from '~/components/category/CategoryTree'
+import {ReorderedCategories} from '~/components/category/useReorderedCategories'
import {config} from '~/components/software/edit/links/config'
-import EditSectionTitle from '~/components/layout/EditSectionTitle'
export type SoftwareCategoriesProps = {
softwareId: string
diff --git a/frontend/components/software/edit/links/EditSoftwareMetadataInputs.tsx b/frontend/components/software/edit/links/EditSoftwareMetadataInputs.tsx
index 53b321e91..d370b45fd 100644
--- a/frontend/components/software/edit/links/EditSoftwareMetadataInputs.tsx
+++ b/frontend/components/software/edit/links/EditSoftwareMetadataInputs.tsx
@@ -22,7 +22,7 @@ import AutosaveSoftwareCategories from './AutosaveSoftwareCategories'
import AutosaveSoftwareKeywords from './AutosaveSoftwareKeywords'
import AutosaveSoftwareLicenses from './AutosaveSoftwareLicenses'
import SoftwareLinksInfo from './SoftwareLinksInfo'
-import {ReorderedCategories, useReorderedCategories} from '~/utils/categories'
+import {ReorderedCategories, useReorderedCategories} from '~/components/category/useReorderedCategories'
export default function EditSoftwareMetadataInputs() {
// use form context to interact with form data
diff --git a/frontend/components/software/edit/links/SoftwareLinksInfo.tsx b/frontend/components/software/edit/links/SoftwareLinksInfo.tsx
index a0d029430..672942538 100644
--- a/frontend/components/software/edit/links/SoftwareLinksInfo.tsx
+++ b/frontend/components/software/edit/links/SoftwareLinksInfo.tsx
@@ -11,7 +11,7 @@
import {Fragment} from 'react'
import Alert from '@mui/material/Alert'
-import {ReorderedCategories} from '~/utils/categories'
+import {ReorderedCategories} from '~/components/category/useReorderedCategories'
import {config} from '~/components/software/edit/links/config'
import {CategoryEntry} from '~/types/Category'
diff --git a/frontend/components/software/edit/organisations/EditSoftwareOrganisationsIndex.test.tsx b/frontend/components/software/edit/organisations/EditSoftwareOrganisationsIndex.test.tsx
index 20f146cf5..8dcb81bd7 100644
--- a/frontend/components/software/edit/organisations/EditSoftwareOrganisationsIndex.test.tsx
+++ b/frontend/components/software/edit/organisations/EditSoftwareOrganisationsIndex.test.tsx
@@ -66,6 +66,8 @@ jest.mock('./organisationForSoftware', () => ({
// by default we return no categories
jest.mock('~/components/category/apiCategories')
jest.mock('~/utils/getSoftware')
+// MOCK removeOrganisationCategoriesFromSoftware
+jest.mock('./apiSoftwareOrganisations')
describe('frontend/components/software/edit/organisations/index.tsx', () => {
beforeEach(() => {
diff --git a/frontend/components/software/edit/organisations/__mocks__/apiSoftwareOrganisations.ts b/frontend/components/software/edit/organisations/__mocks__/apiSoftwareOrganisations.ts
new file mode 100644
index 000000000..2004c5cca
--- /dev/null
+++ b/frontend/components/software/edit/organisations/__mocks__/apiSoftwareOrganisations.ts
@@ -0,0 +1,40 @@
+// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center)
+// SPDX-FileCopyrightText: 2024 Netherlands eScience Center
+//
+// SPDX-License-Identifier: Apache-2.0
+
+/* eslint-disable @typescript-eslint/no-unused-vars */
+
+import {canEditOrganisations} from '~/auth/permissions/isMaintainerOfOrganisation'
+import {getOrganisationsForSoftware} from '~/utils/editOrganisation'
+
+
+export type UseParticipatingOrganisationsProps = {
+ software: string,
+ token: string,
+ account: string
+}
+
+export async function getParticipatingOrganisationsForSoftware({software, token, account}: UseParticipatingOrganisationsProps) {
+ const resp = await getOrganisationsForSoftware({
+ software,
+ token
+ })
+ // convert to EditOrganisation type and add canEdit flag
+ const organisations = await canEditOrganisations({
+ organisations: resp,
+ account,
+ token
+ })
+ // debugger
+ return organisations
+}
+
+
+export async function removeOrganisationCategoriesFromSoftware(
+ softwareId: string,
+ organisationId: string,
+ token: string
+){
+ return {status:200}
+}
diff --git a/frontend/components/software/edit/organisations/apiSoftwareOrganisations.ts b/frontend/components/software/edit/organisations/apiSoftwareOrganisations.ts
index ef43f763f..fe58ba4d5 100644
--- a/frontend/components/software/edit/organisations/apiSoftwareOrganisations.ts
+++ b/frontend/components/software/edit/organisations/apiSoftwareOrganisations.ts
@@ -4,7 +4,7 @@
// SPDX-License-Identifier: Apache-2.0
import {getOrganisationsForSoftware} from '~/utils/editOrganisation'
-import {createJsonHeaders, getBaseUrl} from '~/utils/fetchHelpers'
+import {createJsonHeaders, extractReturnMessage, getBaseUrl} from '~/utils/fetchHelpers'
import {canEditOrganisations} from '~/auth/permissions/isMaintainerOfOrganisation'
export type UseParticipatingOrganisationsProps = {
@@ -47,3 +47,27 @@ export async function removeOrganisationCategoriesFromSoftware(
return resp.ok ? null : resp.text()
}
+
+export type SoftwareCategories={
+ software_id: string,
+ category_id: string
+}
+
+export async function saveSoftwareCategories(categories:SoftwareCategories[],token:string){
+ try{
+ const categoryUrl = `${getBaseUrl()}/category_for_software`
+ const resp = await fetch(categoryUrl, {
+ method: 'POST',
+ body: JSON.stringify(categories),
+ headers: {
+ ...createJsonHeaders(token)
+ }
+ })
+ return extractReturnMessage(resp)
+ }catch(e:any){
+ return {
+ status:500,
+ message: e.message
+ }
+ }
+}
diff --git a/frontend/components/software/edit/organisations/useSoftwareCategories.tsx b/frontend/components/software/edit/organisations/useSoftwareCategories.tsx
index e484c1f11..827d59089 100644
--- a/frontend/components/software/edit/organisations/useSoftwareCategories.tsx
+++ b/frontend/components/software/edit/organisations/useSoftwareCategories.tsx
@@ -9,9 +9,12 @@ import {useSession} from '~/auth'
import {CategoryEntry} from '~/types/Category'
import {TreeNode} from '~/types/TreeNode'
import {getCategoryForSoftwareIds} from '~/utils/getSoftware'
-import {createJsonHeaders, getBaseUrl} from '~/utils/fetchHelpers'
import {loadCategoryRoots} from '~/components/category/apiCategories'
-import {removeOrganisationCategoriesFromSoftware} from './apiSoftwareOrganisations'
+import {
+ removeOrganisationCategoriesFromSoftware,
+ saveSoftwareCategories,
+ SoftwareCategories
+} from './apiSoftwareOrganisations'
type UseSoftwareOrganisationCategoriesProps={
organisationId:string|null,
@@ -47,9 +50,7 @@ export default function useSoftwareCategories({
const availableIds = new Set()
categories.forEach(root=>{
root.forEach(node=>{
- if (node.children().length === 0) {
- availableIds.add(node.getValue().id)
- }
+ availableIds.add(node.getValue().id)
})
})
if (abort) return
@@ -90,29 +91,22 @@ export default function useSoftwareCategories({
}
// generate new collection
- const categoriesArrayToSave: {software_id: string, category_id: string}[] = []
+ const categoriesArrayToSave: SoftwareCategories[] = []
selected.forEach(id => {
if (availableCategoryIds.has(id)) {
categoriesArrayToSave.push({software_id: softwareId, category_id: id})
}
})
- // save organisation categories (if any)
+ // save organisation categories for software (if any)
if (categoriesArrayToSave.length > 0){
- const categoryUrl = `${getBaseUrl()}/category_for_software`
- const resp = await fetch(categoryUrl, {
- method: 'POST',
- body: JSON.stringify(categoriesArrayToSave),
- headers: {
- ...createJsonHeaders(token)
- }
- })
+ const resp = await saveSoftwareCategories(categoriesArrayToSave,token)
// debugger
- if (resp.ok) {
+ if (resp.status===200) {
// signal we are done
onComplete()
} else {
- setError(`Failed to save categories: ${await resp.text()}`)
+ setError(`Failed to save categories: ${resp.message()}`)
setState('error')
}
}else{
From 82e62526aa398f061bd80dd6222dd10d537cc2eb Mon Sep 17 00:00:00 2001
From: "Dusan Mijatovic (PC2020)"
Date: Fri, 22 Nov 2024 18:01:44 +0100
Subject: [PATCH 4/4] feat: order organisation and community categories
alphabetically fix: software organisation categories select/decelet/save
---
frontend/components/category/useCategories.ts | 7 +++++++
.../components/category/useCategoryTree.tsx | 19 ++++++++++++++++++-
.../components/projects/ProjectCategories.tsx | 5 +++++
.../organisations/useProjectCategories.tsx | 3 +++
.../communities/useCommunityCategories.tsx | 8 +++++---
.../organisations/useSoftwareCategories.tsx | 7 +++++--
6 files changed, 43 insertions(+), 6 deletions(-)
diff --git a/frontend/components/category/useCategories.ts b/frontend/components/category/useCategories.ts
index eb8fbd345..c94f845f4 100644
--- a/frontend/components/category/useCategories.ts
+++ b/frontend/components/category/useCategories.ts
@@ -10,6 +10,7 @@ import logger from '~/utils/logger'
import {TreeNode} from '~/types/TreeNode'
import {CategoryEntry} from '~/types/Category'
import {loadCategoryRoots} from '~/components/category/apiCategories'
+import {sortCategoriesByName} from './useCategoryTree'
type UseCategoriesProps={
community?:string|null,
@@ -27,6 +28,9 @@ export default function useCategories({community,organisation}:UseCategoriesProp
loadCategoryRoots({community,organisation})
.then(roots => {
if (abort) return
+ // sort categories
+ sortCategoriesByName(roots)
+ // set state
setRoots(roots)
setError(null)
})
@@ -46,6 +50,9 @@ export default function useCategories({community,organisation}:UseCategoriesProp
function onMutation() {
if (roots !== null) {
+ // sort categories
+ sortCategoriesByName(roots)
+ // update state
setRoots([...roots])
}
}
diff --git a/frontend/components/category/useCategoryTree.tsx b/frontend/components/category/useCategoryTree.tsx
index dbab5d6d3..1a0d73f82 100644
--- a/frontend/components/category/useCategoryTree.tsx
+++ b/frontend/components/category/useCategoryTree.tsx
@@ -18,6 +18,22 @@ export const categoryTreeNodesSort = (trees: TreeNode[]) => {
}
}
+/**
+ * Sort (ascending) the complete category tree, at all levels, on name property .
+ * @param trees TreeNode[]
+ */
+export function sortCategoriesByName(trees: TreeNode[]){
+ trees.sort(compareCategoryTreeNode)
+ for (const root of trees) {
+ // sort children first
+ if (root.childrenCount()>0){
+ sortCategoriesByName(root.children())
+ }
+ // sort roots
+ root.sortRecursively(compareCategoryEntry)
+ }
+}
+
export const genCategoryTreeNodes = (categories: CategoryPath[]=[]) : TreeNode[] => {
const allEntries: CategoryEntry[] = []
@@ -29,7 +45,7 @@ export const genCategoryTreeNodes = (categories: CategoryPath[]=[]) : TreeNode[]{
return useMemo(() => genCategoryTreeNodes(categories), [categories])
}
+
diff --git a/frontend/components/projects/ProjectCategories.tsx b/frontend/components/projects/ProjectCategories.tsx
index 25ca62069..6e40d4e09 100644
--- a/frontend/components/projects/ProjectCategories.tsx
+++ b/frontend/components/projects/ProjectCategories.tsx
@@ -12,6 +12,11 @@ import {CategoryChipFilter} from '../category/CategoryChipFilter'
export default function ProjectCategories({categories}:{categories:CategoryPath[]}) {
const tree = useCategoryTree(categories)
+ // console.group('ProjectCategories')
+ // console.log('categories...', categories)
+ // console.log('tree...', tree)
+ // console.groupEnd()
+
// each root category is separate sidebar section
return tree.map(node => {
const category = node.getValue()
diff --git a/frontend/components/projects/edit/organisations/useProjectCategories.tsx b/frontend/components/projects/edit/organisations/useProjectCategories.tsx
index 45c6c128d..81b08039d 100644
--- a/frontend/components/projects/edit/organisations/useProjectCategories.tsx
+++ b/frontend/components/projects/edit/organisations/useProjectCategories.tsx
@@ -9,6 +9,7 @@ import {createJsonHeaders, getBaseUrl} from '~/utils/fetchHelpers'
import {CategoryEntry} from '~/types/Category'
import {TreeNode} from '~/types/TreeNode'
import {loadCategoryRoots} from '~/components/category/apiCategories'
+import {sortCategoriesByName} from '~/components/category/useCategoryTree'
import {getCategoryListForProject, removeOrganisationCategoriesFromProject} from './apiProjectOrganisations'
type UseProjectOrganisationCategoriesProps={
@@ -46,6 +47,8 @@ export default function useProjectCategories({
.then(([roots,selected]) => {
// filter top level categories for projects (only top level items have this flag)
const categories = roots.filter(item=>item.getValue().allow_projects)
+ // sort categories
+ sortCategoriesByName(categories)
// collect tree leaves ids (end nodes)
const availableIds = new Set()
categories.forEach(root=>{
diff --git a/frontend/components/software/edit/communities/useCommunityCategories.tsx b/frontend/components/software/edit/communities/useCommunityCategories.tsx
index 8663d2903..c4be37211 100644
--- a/frontend/components/software/edit/communities/useCommunityCategories.tsx
+++ b/frontend/components/software/edit/communities/useCommunityCategories.tsx
@@ -5,10 +5,11 @@
import {useEffect, useState} from 'react'
import {useSession} from '~/auth'
-import {loadCategoryRoots} from '~/components/category/apiCategories'
import {CategoryEntry} from '~/types/Category'
import {TreeNode} from '~/types/TreeNode'
import {getCategoryForSoftwareIds} from '~/utils/getSoftware'
+import {loadCategoryRoots} from '~/components/category/apiCategories'
+import {sortCategoriesByName} from '~/components/category/useCategoryTree'
import {saveSoftwareCategories, SoftwareCategories} from '../organisations/apiSoftwareOrganisations'
import {removeCommunityCategoriesFromSoftware} from './apiSoftwareCommunities'
@@ -40,8 +41,9 @@ export default function useCommunityCategories({
getCategoryForSoftwareIds(softwareId, token)
])
.then(([roots,selected]) => {
- // filter top level categories for software (only top level items have this flag)
- // collect tree leaves ids (end nodes)
+ // sort categories
+ sortCategoriesByName(roots)
+ // collect ids
const availableIds = new Set()
roots.forEach(root=>{
root.forEach(node=>{
diff --git a/frontend/components/software/edit/organisations/useSoftwareCategories.tsx b/frontend/components/software/edit/organisations/useSoftwareCategories.tsx
index 827d59089..6cc9f99f6 100644
--- a/frontend/components/software/edit/organisations/useSoftwareCategories.tsx
+++ b/frontend/components/software/edit/organisations/useSoftwareCategories.tsx
@@ -10,6 +10,7 @@ import {CategoryEntry} from '~/types/Category'
import {TreeNode} from '~/types/TreeNode'
import {getCategoryForSoftwareIds} from '~/utils/getSoftware'
import {loadCategoryRoots} from '~/components/category/apiCategories'
+import {sortCategoriesByName} from '~/components/category/useCategoryTree'
import {
removeOrganisationCategoriesFromSoftware,
saveSoftwareCategories,
@@ -46,7 +47,9 @@ export default function useSoftwareCategories({
.then(([roots,selected]) => {
// filter top level categories for software (only top level items have this flag)
const categories = roots.filter(item=>item.getValue().allow_software)
- // collect tree leaves ids (end nodes)
+ // sort categories
+ sortCategoriesByName(categories)
+ // collect ids
const availableIds = new Set()
categories.forEach(root=>{
root.forEach(node=>{
@@ -85,7 +88,7 @@ export default function useSoftwareCategories({
}
}
- if (selectedCategoryIds.size === 0) {
+ if (selected.size === 0) {
onComplete()
return
}