Skip to content

Commit

Permalink
WIP: all category levels select
Browse files Browse the repository at this point in the history
  • Loading branch information
dmijatovic committed Nov 21, 2024
1 parent 56376ac commit fed0913
Show file tree
Hide file tree
Showing 12 changed files with 324 additions and 87 deletions.
24 changes: 14 additions & 10 deletions data-generation/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand All @@ -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 = {
Expand Down
88 changes: 16 additions & 72 deletions frontend/components/category/CategoriesDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,19 @@
//
// 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'
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,
Expand All @@ -37,84 +36,22 @@ export default function CategoriesDialog({
const smallScreen = useMediaQuery('(max-width:600px)')
const [selectedCategoryIds, setSelectedCategoryIds] = useState<Set<string>>(new Set())

// console.group('CategoriesDialog')
// console.log('state...', state)
// console.log('selected...', selected)
// console.log('selectedCategoryIds...',selectedCategoryIds)
// console.groupEnd()
console.group('CategoriesDialog')
console.log('state...', state)
console.log('selected...', selected)
console.log('selectedCategoryIds...',selectedCategoryIds)
console.groupEnd()

useEffect(()=>{
if (state==='ready'){
setSelectedCategoryIds(selected)
}
},[selected,state])

function isSelected(node: TreeNode<CategoryEntry>) {
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<CategoryEntry>) {
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 (
<div className="flex-1 flex justify-center items-center">
<ContentLoader/>
</div>
)

case 'error':
return (
<Alert severity="error" sx={{marginTop: '0.5rem'}}>
{errorMsg ?? '500 - Unexpected error'}
</Alert>
)

case 'ready':
return (
<>
{(categories === null || categories.length === 0)
?
<Alert severity="info" sx={{'padding': '2rem'}}>
{noItemsMsg}
</Alert>
:
<RecursivelyGenerateItems
nodes={categories}
isSelected={isSelected}
keyExtractor={keyExtractor}
onSelect={onSelect}
textExtractor={textExtractor}
/>
}
</>
)
}
}

return (
<Dialog open fullScreen={smallScreen}>
<DialogTitle sx={{
Expand All @@ -131,7 +68,14 @@ export default function CategoriesDialog({
padding: '1rem 1.5rem 2.5rem !important',
}}>

{renderDialogContent()}
<CategoriesDialogBody
categories={categories}
state={state}
errorMsg={errorMsg}
noItemsMsg={noItemsMsg}
selectedCategoryIds={selectedCategoryIds}
setSelectedCategoryIds={setSelectedCategoryIds}
/>

</DialogContent>
<DialogActions sx={{
Expand Down
102 changes: 102 additions & 0 deletions frontend/components/category/CategoriesDialogBody.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center)
// SPDX-FileCopyrightText: 2024 Netherlands eScience Center
//
// SPDX-License-Identifier: Apache-2.0

import Alert from '@mui/material/Alert'
import {CategoryEntry} from '~/types/Category'
import {TreeNode} from '~/types/TreeNode'
import ContentLoader from '../layout/ContentLoader'
import {CategoryList} from './CategoryList'
import List from '@mui/material/List'

type CategoriesDialogBodyProps={
categories: TreeNode<CategoryEntry>[],
state: 'loading' | 'error' | 'ready' | 'saving',
errorMsg: string | null
noItemsMsg: string
selectedCategoryIds: Set<string>,
setSelectedCategoryIds: (ids:Set<string>)=>void
}

export default function CategoriesDialogBody({
categories,state,errorMsg,noItemsMsg,
selectedCategoryIds, setSelectedCategoryIds
}:CategoriesDialogBodyProps) {


function isSelected(node: TreeNode<CategoryEntry>) {
const val = node.getValue()
// default approach
// return selectedCategoryIds.has(val.id)

// ALTERNATIVE APPROACH
// directly selected
if (selectedCategoryIds.has(val.id)) return true

// any of children selected?
const found = node.children().find(item=>{
return isSelected(item)
})
if (found) {
// add parent to list of selected items
// if not already in the list
if (selectedCategoryIds.has(val.id)===false){
// debugger
selectedCategoryIds.add(val.id)
setSelectedCategoryIds(new Set(selectedCategoryIds))
}
return true
}
// none of children selected either
return false
}

function onSelect(node: TreeNode<CategoryEntry>) {
const val = node.getValue()
if (selectedCategoryIds.has(val.id)) {
selectedCategoryIds.delete(val.id)
} else {
selectedCategoryIds.add(val.id)
}
setSelectedCategoryIds(new Set(selectedCategoryIds))
}

switch (state) {
case 'loading':
case 'saving':
return (
<div className="flex-1 flex justify-center items-center">
<ContentLoader/>
</div>
)

case 'error':
return (
<Alert severity="error" sx={{marginTop: '0.5rem'}}>
{errorMsg ?? '500 - Unexpected error'}
</Alert>
)

case 'ready':
return (
<>
{(categories === null || categories.length === 0)
?
<Alert severity="info" sx={{'padding': '2rem'}}>
{noItemsMsg}
</Alert>
:
<List>
<CategoryList
categories={categories}
isSelected={isSelected}
onSelect={onSelect}
/>
</List>
}
</>
)
}

}
127 changes: 127 additions & 0 deletions frontend/components/category/CategoryList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center)
// SPDX-FileCopyrightText: 2024 Netherlands eScience Center
//
// SPDX-License-Identifier: Apache-2.0

import {useState} from 'react'
import ExpandLess from '@mui/icons-material/ExpandLess'
import ExpandMore from '@mui/icons-material/ExpandMore'
import Checkbox from '@mui/material/Checkbox'
import Collapse from '@mui/material/Collapse'
import IconButton from '@mui/material/IconButton'
import List from '@mui/material/List'
import ListItem from '@mui/material/ListItem'
import ListItemButton from '@mui/material/ListItemButton'
import ListItemText from '@mui/material/ListItemText'

import {TreeNode} from '~/types/TreeNode'
import {CategoryEntry} from '~/types/Category'

type NodeWithChildrenProps = {
node: TreeNode<CategoryEntry>
onSelect: (node: TreeNode<CategoryEntry>) => void
isSelected: (node: TreeNode<CategoryEntry>) => boolean
}

function NodeWithChildren({
node,
isSelected,
onSelect
}:NodeWithChildrenProps){
// open/close children panel
const [open,setOpen] = useState(true)
// get category
const cat = node.getValue()

return (
<>
<ListItem
key={cat.id}
title={cat.short_name}
secondaryAction={
<IconButton
edge="end"
aria-label="expand"
onClick={()=>setOpen(!open)}
>
{open ? <ExpandLess /> : <ExpandMore />}
</IconButton>
}
disablePadding
dense
>
<ListItemButton onClick={() => onSelect(node)}>
<Checkbox disableRipple checked={isSelected(node)} />
<ListItemText
primary={cat.name}
// secondary={cat.name}
/>
</ListItemButton>
</ListItem>
{/* Children block */}
<Collapse
in={open}
timeout="auto"
unmountOnExit={false}
>
<List sx={{pl:'1rem'}}>
<CategoryList
categories={node.children()}
onSelect={onSelect}
isSelected={isSelected}
/>
</List>
</Collapse>
</>
)
}

export type CategoryListProps = {
onSelect: (node: TreeNode<CategoryEntry>) => void
isSelected: (node: TreeNode<CategoryEntry>) => boolean
categories: TreeNode<CategoryEntry>[]
}

export function CategoryList({
categories,
isSelected,
onSelect
}: CategoryListProps) {
// loop all categories
return categories.map(node => {
const cat = node.getValue()

// single cat element without children
if (node.childrenCount() === 0) {
return (
<ListItem
key={cat.id}
// title={cat.name}
title={cat.short_name}
disablePadding
dense
>
<ListItemButton onClick={() => onSelect(node)}>
<Checkbox
disableRipple
checked={isSelected(node)}
/>
<ListItemText
primary={cat.name}
// secondary={cat.name}
/>
</ListItemButton>
</ListItem>
)
}

return (
<NodeWithChildren
key={cat.id}
node={node}
isSelected={isSelected}
onSelect={onSelect}
/>
)
})
}
Loading

0 comments on commit fed0913

Please sign in to comment.