Skip to content

Commit

Permalink
feat: organisation admin can create custom categories for the organis…
Browse files Browse the repository at this point in the history
…ation
  • Loading branch information
ewan-escience authored and dmijatovic committed Oct 15, 2024
1 parent d25b5c7 commit 5b54971
Show file tree
Hide file tree
Showing 24 changed files with 196 additions and 64 deletions.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,17 @@ CREATE TABLE category (
id UUID PRIMARY KEY,
parent UUID REFERENCES category DEFAULT NULL,
community UUID REFERENCES community(id) DEFAULT NULL,
organisation UUID REFERENCES organisation(id) DEFAULT NULL,
allow_software BOOLEAN NOT NULL DEFAULT FALSE,
allow_projects BOOLEAN NOT NULL DEFAULT FALSE,
short_name VARCHAR(100) NOT NULL,
name VARCHAR(250) NOT NULL,
properties JSONB NOT NULL DEFAULT '{}'::jsonb,
provenance_iri VARCHAR(250) DEFAULT NULL, -- e.g. https://www.w3.org/TR/skos-reference/#mapping

CONSTRAINT unique_short_name UNIQUE NULLS NOT DISTINCT (parent, short_name, community),
CONSTRAINT unique_name UNIQUE NULLS NOT DISTINCT (parent, name, community),
CONSTRAINT only_one_entity CHECK (community IS NULL OR organisation IS NULL),
CONSTRAINT unique_short_name UNIQUE NULLS NOT DISTINCT (parent, short_name, community, organisation),
CONSTRAINT unique_name UNIQUE NULLS NOT DISTINCT (parent, name, community, organisation),
CONSTRAINT invalid_value_for_properties CHECK (properties - '{icon, is_highlight, description, subtitle, tree_level_labels}'::text[] = '{}'::jsonb),
CONSTRAINT highlight_must_be_top_level_category CHECK (NOT ((properties->>'is_highlight')::boolean AND parent IS NOT NULL))
);
Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions frontend/components/admin/categories/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default function AdminCategories() {
<CategoryEditTree
roots={roots}
community={null}
organisation={null}
onMutation={onMutation}
/>
}
Expand Down
58 changes: 49 additions & 9 deletions frontend/components/category/CategoryEditForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,40 @@ import {CategoryEntry} from '~/types/Category'
import {createJsonHeaders} from '~/utils/fetchHelpers'
import useSnackbar from '~/components/snackbar/useSnackbar'
import TextFieldWithCounter from '~/components/form/TextFieldWithCounter'
import ControlledSwitch from '../form/ControlledSwitch'

type CategoryEditFormProps=Readonly<{
createNew: boolean
data: CategoryEntry | null
community: string | null
organisation: string | null
onSuccess: (category:CategoryEntry)=>void
onCancel: ()=>void
}>

export default function CategoryEditForm({createNew, data, community, onSuccess, onCancel}:CategoryEditFormProps) {
export default function CategoryEditForm({
createNew, data, community=null,organisation=null,onSuccess, onCancel
}:CategoryEditFormProps) {
const {token} = useSession()
const {showErrorMessage} = useSnackbar()
const [disableSave, setDisableSave] = useState<boolean>(false)
const {register, control, handleSubmit, formState, watch} = useForm<CategoryEntry>({
mode: 'onChange'
})

// console.group('CategoryEditForm')
// console.log('createNew...',createNew)
const [parent] = watch(['parent'])

console.group('CategoryEditForm')
console.log('createNew...',createNew)
// console.log('data...',data)
// console.log('disableSave...',disableSave)
// console.log('community...',community)
// console.groupEnd()
// console.log('organisation...',organisation)
console.log('parent...',parent)
console.groupEnd()


const onSubmit = (formData: CategoryEntry) => {
function onSubmit(formData: CategoryEntry){
setDisableSave(true)
// debugger
if (createNew) {
Expand All @@ -56,7 +65,6 @@ export default function CategoryEditForm({createNew, data, community, onSuccess,
...createJsonHeaders(token),
Prefer: 'return=representation',
Accept: 'application/vnd.pgrst.object+json'

},
body: JSON.stringify(formData)
})
Expand Down Expand Up @@ -127,6 +135,7 @@ export default function CategoryEditForm({createNew, data, community, onSuccess,
</>
}
<input type="hidden" {...register('community', {value: community})} />
<input type="hidden" {...register('organisation', {value: organisation})} />

<h3 className="py-4 text-base-content-secondary">{getFormTitle()}</h3>

Expand Down Expand Up @@ -156,6 +165,7 @@ export default function CategoryEditForm({createNew, data, community, onSuccess,
error: formState.errors?.name?.message !== undefined
}}
/>

<TextFieldWithCounter
register={register('provenance_iri', {
maxLength: {value: 250, message: 'max length is 250'}
Expand All @@ -169,9 +179,39 @@ export default function CategoryEditForm({createNew, data, community, onSuccess,
}}
/>

{/* Highlight options are only for the top level items and for general categories */}
{((createNew && data === null && community===null) ||
(!createNew && data?.parent === null && community===null)) ?
{/*
Organisation categories can be used for software or project items
We show software/project switch only at top level
*/}
{
organisation && !parent ?
<div className="flex gap-8 pt-8">
<ControlledSwitch
label="For software"
name="allow_software"
defaultValue = {data?.allow_software}
control={control}
disabled={parent ? true : false}
/>
<ControlledSwitch
label="For projects"
name="allow_projects"
defaultValue = {data?.allow_projects ?? true}
control={control}
disabled={parent ? true : false}
/>
</div>
:
<>
{/* By default categories are used by software in communities and by project for organisations */}
<input type="hidden" {...register('allow_software', {value: data?.allow_software ?? community!==null})} />
<input type="hidden" {...register('allow_projects', {value: data?.allow_projects ?? organisation!==null})} />
</>
}

{/* Highlight options are only for the top level items of general categories */}
{((createNew && data === null && community===null && organisation===null) ||
(!createNew && data?.parent === null && community===null && organisation===null)) ?
<>
<div className="py-4"/>
<Controller
Expand Down
5 changes: 4 additions & 1 deletion frontend/components/category/CategoryEditTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ import EditSectionTitle from '../layout/EditSectionTitle'
type CategoryEditTreeProps=Readonly<{
roots: TreeNode<CategoryEntry>[],
community: string | null
organisation: string | null
onMutation: ()=>void
title?:string
}>

export default function CategoryEditTree({roots, community, title, onMutation}:CategoryEditTreeProps) {
export default function CategoryEditTree({roots, community, organisation, title, onMutation}:CategoryEditTreeProps) {

const [showAddChildForm, setShowAddChildForm] = useState<boolean>(false)

Expand Down Expand Up @@ -61,6 +62,7 @@ export default function CategoryEditTree({roots, community, title, onMutation}:C
<CategoryEditForm
createNew={true}
community={community}
organisation={organisation}
data={null}
onSuccess={onNewChildSuccess}
onCancel={()=>setShowAddChildForm(false)}
Expand All @@ -74,6 +76,7 @@ export default function CategoryEditTree({roots, community, title, onMutation}:C
key={node.getValue().id}
node={node}
community={community}
organisation={organisation}
onDelete={onDeleteChild}
onMutation={onMutation}
/>
Expand Down
6 changes: 5 additions & 1 deletion frontend/components/category/CategoryEditTreeNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ import CategoryEditForm from '~/components/category/CategoryEditForm'
import useSnackbar from '~/components/snackbar/useSnackbar'
import ConfirmDeleteModal from '~/components/layout/ConfirmDeleteModal'

export default function CategoryEditTreeNode({node, community, onDelete, onMutation}: Readonly<{
export default function CategoryEditTreeNode({node, community, organisation, onDelete, onMutation}: Readonly<{
node: TreeNode<CategoryEntry>
community: string | null
organisation: string | null
onDelete: (node: TreeNode<CategoryEntry>) => void
onMutation: ()=>void
}>) {
Expand Down Expand Up @@ -152,6 +153,7 @@ export default function CategoryEditTreeNode({node, community, onDelete, onMutat
<CategoryEditForm
createNew={false}
community={community}
organisation={organisation}
data={categoryData}
onSuccess={onEditSuccess}
onCancel={()=>setShowItem('none')}
Expand All @@ -162,6 +164,7 @@ export default function CategoryEditTreeNode({node, community, onDelete, onMutat
<CategoryEditForm
createNew={true}
community={community}
organisation={organisation}
data={categoryData}
onSuccess={onNewChildSuccess}
onCancel={()=>setShowItem('none')}
Expand All @@ -176,6 +179,7 @@ export default function CategoryEditTreeNode({node, community, onDelete, onMutat
key={child.getValue().id}
node={child}
community={community}
organisation={organisation}
onDelete={onDeleteChild}
onMutation={onMutation}
/>
Expand Down
4 changes: 2 additions & 2 deletions frontend/components/category/CategoryTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
import CancelIcon from '@mui/icons-material/Cancel'
import Tooltip from '@mui/material/Tooltip'
import IconButton from '@mui/material/IconButton'
import {CategoryEntry, CategoryID} from '~/types/Category'
import {CategoryEntry} from '~/types/Category'
import {TreeNode} from '~/types/TreeNode'

export type CategoryTreeLevelProps = {
items: TreeNode<CategoryEntry>[]
showLongNames?: boolean
onRemove?: (categoryId: CategoryID) => void
onRemove?: (categoryId: string) => void
}
export const CategoryTreeLevel = ({onRemove, ...props}: CategoryTreeLevelProps) => {

Expand Down
25 changes: 19 additions & 6 deletions frontend/components/category/apiCategories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,28 @@
//
// SPDX-License-Identifier: Apache-2.0

import {CategoryEntry, CategoryID} from '~/types/Category'
import {CategoryEntry} from '~/types/Category'
import {getBaseUrl} from '~/utils/fetchHelpers'
import {TreeNode} from '~/types/TreeNode'

export async function loadCategoryRoots(community: string | null): Promise<TreeNode<CategoryEntry>[]> {
type LoadCategoryProps={
community?: string | null,
organisation?: string | null
}

const communityFilter = community === null ? 'community=is.null' : `community=eq.${community}`
export async function loadCategoryRoots({community, organisation}:LoadCategoryProps){
// global categories is default
let categoryFilter = 'community=is.null&organisation=is.null'
// community filter
if (community){
categoryFilter = `community=eq.${community}`
}
// organisation filter
if (organisation){
categoryFilter = `organisation=eq.${organisation}`
}

const resp = await fetch(`${getBaseUrl()}/category?${communityFilter}`)
const resp = await fetch(`${getBaseUrl()}/category?${categoryFilter}`)

if (!resp.ok) {
throw new Error(`${await resp.text()}`)
Expand All @@ -25,8 +38,8 @@ export async function loadCategoryRoots(community: string | null): Promise<TreeN
}

export function categoryEntriesToRoots(categoriesArr: CategoryEntry[]): TreeNode<CategoryEntry>[] {
const idToNode: Map<CategoryID, TreeNode<CategoryEntry>> = new Map()
const idToChildren: Map<CategoryID, TreeNode<CategoryEntry>[]> = new Map()
const idToNode: Map<string, TreeNode<CategoryEntry>> = new Map()
const idToChildren: Map<string, TreeNode<CategoryEntry>[]> = new Map()

for (const cat of categoriesArr) {
const id = cat.id
Expand Down
13 changes: 9 additions & 4 deletions frontend/components/category/useCategories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,25 @@

import {useEffect, useState} from 'react'

import logger from '~/utils/logger'
import {TreeNode} from '~/types/TreeNode'
import {CategoryEntry} from '~/types/Category'
import {loadCategoryRoots} from '~/components/category/apiCategories'
import logger from '~/utils/logger'

export default function useCategories({community}:{community:string|null}){
type UseCategoriesProps={
community?:string|null,
organisation?:string|null
}

export default function useCategories({community,organisation}:UseCategoriesProps){
const [roots, setRoots] = useState<TreeNode<CategoryEntry>[] | null> (null)
const [error, setError] = useState<string | null> (null)
const [loading, setLoading] = useState<boolean> (true)

useEffect(() => {
let abort: boolean = false
// only if there is community value
loadCategoryRoots(community)
loadCategoryRoots({community,organisation})
.then(roots => {
if (abort) return
setRoots(roots)
Expand All @@ -37,7 +42,7 @@ export default function useCategories({community}:{community:string|null}){
})

return ()=>{abort=true}
}, [community])
}, [community,organisation])

function onMutation() {
if (roots !== null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default function CommunityCategories() {
title="Categories"
roots={roots}
community={community.id}
organisation={null}
onMutation={onMutation}
/>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import InfoIcon from '@mui/icons-material/Info'
import PersonIcon from '@mui/icons-material/Person'
import SettingsIcon from '@mui/icons-material/Settings'
import CategoryIcon from '@mui/icons-material/Category'

export type SettingsMenuProps = {
id: string,
Expand All @@ -24,6 +25,12 @@ export const settingsMenu: SettingsMenuProps[] = [
icon: <SettingsIcon />,
status: 'Organisation details'
},
{
id:'categories',
label:()=>'Categories',
icon: <CategoryIcon />,
status: 'Define categories',
},
{
id:'maintainers',
label:()=>'Maintainers',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center)
// SPDX-FileCopyrightText: 2023 Netherlands eScience Center
// SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center)
// SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center
//
// SPDX-License-Identifier: Apache-2.0

import {useRouter} from 'next/router'
import OrganisationSettingsAboutPage from './about-page'
import OrganisationMaintainers from './maintainers'
import OrganisationGeneralSettings from './general'
import {useRouter} from 'next/router'
import OrganisationCategories from './categories'


export default function SettingsPageContent() {
Expand All @@ -18,6 +19,8 @@ export default function SettingsPageContent() {
return <OrganisationSettingsAboutPage />
case 'maintainers':
return <OrganisationMaintainers />
case 'categories':
return <OrganisationCategories />
default:
return <OrganisationGeneralSettings/>
}
Expand Down
Loading

0 comments on commit 5b54971

Please sign in to comment.