Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic collections #559

Closed
wants to merge 13 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@ import PanelsWrapper from './components/PanelsWrapper'
import { FC } from 'react'
import { configStore } from '@/store/ConfigStore.tsx'

import TopBar from '@/components/TopBar'

interface AppProps {
customConfig: Config
}
@@ -12,6 +14,7 @@ const App: FC<AppProps> = ({ customConfig }) => {

return (
<div className="tido t-flex t-flex-col">
<TopBar />
<PanelsWrapper />
</div>
)
113 changes: 113 additions & 0 deletions src/components/LocalTreeModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { FC, ReactNode, useRef, useState } from 'react'

import { dataStore } from '@/store/DataStore.tsx'
import { configStore } from '@/store/ConfigStore'

import { Popover, PopoverContent, PopoverTrigger, ClosePopover } from '@/components/ui/popover'
import TreeView from '@/components/TreeView'

import { getItemIndices } from '@/utils/tree'


interface LocalTreeProps {
TriggerButton: ReactNode
}

const LocalTreeModal: FC <LocalTreeProps> = ({ TriggerButton }) => {

// TODO: add a [loading, setLoading] => which shows the pop over when the tree has been loaded -> TreeView Component updates the loading of its parent

const clickedItemUrl = dataStore((state) => state.clickedItemUrl)
const setClickedItemUrl = dataStore(state => state.setClickedItemUrl)
const treeNodes = dataStore((state) => state.treeNodes)
const addNewPanel = configStore((state) => state.addNewPanel)
const [inputGiven, setInputGiven] = useState(false)

const [clickedButton, setClickedButton] = useState(false)

const inputCollectionRef = useRef(null);

Check failure on line 28 in src/components/LocalTreeModal.tsx

GitHub Actions / build (18)

Extra semicolon

Check failure on line 28 in src/components/LocalTreeModal.tsx

GitHub Actions / build (20)

Extra semicolon


function handleSelectClick(e) {

let manifestIndex: number | undefined, itemIndex: number | undefined, collectionUrl: string | undefined

Check failure on line 33 in src/components/LocalTreeModal.tsx

GitHub Actions / build (18)

Irregular whitespace not allowed

Check failure on line 33 in src/components/LocalTreeModal.tsx

GitHub Actions / build (20)

Irregular whitespace not allowed

if (!clickedItemUrl && inputCollectionRef.current.value === '') {
setClickedButton(true)
e.preventDefault();

Check failure on line 37 in src/components/LocalTreeModal.tsx

GitHub Actions / build (18)

Extra semicolon

Check failure on line 37 in src/components/LocalTreeModal.tsx

GitHub Actions / build (20)

Extra semicolon
return
}

setInputGiven(true)

if (clickedItemUrl) {
const data = getItemIndices(clickedItemUrl, treeNodes)
if (!data) {
console.error('Indices of clicked item could not be found')
return
}

collectionUrl = data?.collectionUrl
manifestIndex = data?.manifestIndex
itemIndex = data?.itemIndex

addNewPanel({
entrypoint: {
url: collectionUrl,
type: "collection",

Check failure on line 57 in src/components/LocalTreeModal.tsx

GitHub Actions / build (18)

Strings must use singlequote

Check failure on line 57 in src/components/LocalTreeModal.tsx

GitHub Actions / build (20)

Strings must use singlequote
},
manifestIndex: manifestIndex,
itemIndex: itemIndex
}
)
}

if (inputCollectionRef.current.value !== '') {
collectionUrl = inputCollectionRef.current?.value

addNewPanel({
entrypoint: {
url: collectionUrl,
type: "collection",

Check failure on line 71 in src/components/LocalTreeModal.tsx

GitHub Actions / build (18)

Strings must use singlequote

Check failure on line 71 in src/components/LocalTreeModal.tsx

GitHub Actions / build (20)

Strings must use singlequote
}
}
)
}

// lines below serve mainly for showing the error message. Error message appears when a user does not provide input for opening a new a collection/panel
setClickedItemUrl('')
setInputGiven(false)
setClickedButton(false)

return

}

return <div className="local-tree-modal">
<Popover>
<PopoverTrigger className="open-tree-button t-h-8 t-w-10 t-relative">
{ TriggerButton }
</PopoverTrigger>
<PopoverContent className="t-bg-white t-absolute t-z-10">
<div className="t-flex t-flex-col t-pt-4 t-pl-3 t-w-[500px] t-shadow-md t-border-[1px] t-border-solid t-border-gray-300 t-rounded-md">
<div className="t-text-red-400" style={{display: !inputGiven && clickedButton && !clickedItemUrl ? 'block': 'none'}}> Please do provide a way to open a new collection</div>

Check failure on line 93 in src/components/LocalTreeModal.tsx

GitHub Actions / build (18)

A space is required after '{'

Check failure on line 93 in src/components/LocalTreeModal.tsx

GitHub Actions / build (18)

A space is required before '}'

Check failure on line 93 in src/components/LocalTreeModal.tsx

GitHub Actions / build (20)

A space is required after '{'

Check failure on line 93 in src/components/LocalTreeModal.tsx

GitHub Actions / build (20)

A space is required before '}'
<span className="t-font-bold">Enter a collection/manifest Url</span>
<input ref={inputCollectionRef} className="t-border-solid t-border-[1.5px] t-w-[200px] t-h-[30px] t-mb-[10px]" />
<span>Or choose:</span>

<TreeView />

<div className="t-pb-4">
<ClosePopover className='t-bg-blue-500 t-text-white t-rounded t-flex t-text-center t-pl-2 t-ml-[80%] t-mt-10 t-items-center t-justify-items-center t-w-16 t-h-10'
onClick = {(e) => handleSelectClick(e)}>

Check failure on line 102 in src/components/LocalTreeModal.tsx

GitHub Actions / build (18)

There should be no space before '='

Check failure on line 102 in src/components/LocalTreeModal.tsx

GitHub Actions / build (18)

There should be no space after '='

Check failure on line 102 in src/components/LocalTreeModal.tsx

GitHub Actions / build (20)

There should be no space before '='

Check failure on line 102 in src/components/LocalTreeModal.tsx

GitHub Actions / build (20)

There should be no space after '='
Select
</ClosePopover>
</div>

</div>
</PopoverContent>
</Popover>
</div>
}

export default LocalTreeModal
20 changes: 20 additions & 0 deletions src/components/TopBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { FC } from 'react'


import LocalTreeModal from '@/components/LocalTreeModal'

const TopBar: FC = () => {

const addButton =
<span className="t-bg-blue-500 t-text-white t-rounded t-flex t-pl-4 t-items-center t-justify-items-center t-w-16 t-h-10">
New
</span>


return <div className="t-flex t-flex-row t-ml-[6%] t-mt-10">
<LocalTreeModal TriggerButton={addButton} />
</div>

}

export default TopBar
51 changes: 51 additions & 0 deletions src/components/TreeView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { FC, useEffect, useState } from 'react'
import { configStore } from '@/store/ConfigStore.tsx'
import { dataStore } from '@/store/DataStore.tsx'


import { createTree } from '@/utils/tree'
import CollectionSubtree from '@/components/tree/CollectionSubtree'

const Tree: FC = () => {

const config = configStore(state => state.config)
const initTreeNodes = dataStore(state => state.initTreeNodes)


const [treeNodes, setTreeNodes] = useState<CollectionNode[]>([])

const [loadingTree, setLoadingTreee] = useState(true)

useEffect(() => {
async function initTree(panels?: PanelConfig[]) {
if (!panels) return
const nodes = await createTree(panels)

setTreeNodes(nodes)
setLoadingTreee(false)
initTreeNodes(nodes)
}
initTree(config.panels)
}, [])

Check warning on line 29 in src/components/TreeView.tsx

GitHub Actions / build (18)

React Hook useEffect has missing dependencies: 'config.panels' and 'initTreeNodes'. Either include them or remove the dependency array

Check warning on line 29 in src/components/TreeView.tsx

GitHub Actions / build (20)

React Hook useEffect has missing dependencies: 'config.panels' and 'initTreeNodes'. Either include them or remove the dependency array

if (loadingTree) return <></>

const tree =
treeNodes.length > 0 &&
treeNodes.map((collection, i) => (
<div
key={i}
className=""
>
<CollectionSubtree collectionData={collection} />
</div>
))



return <div className="tree t-h-96 t-overflow-hidden t-overflow-y-auto">
{tree}
</div>
}

export default Tree
5 changes: 4 additions & 1 deletion src/components/panel/Panel.tsx
Original file line number Diff line number Diff line change
@@ -16,7 +16,8 @@
}

const Panel: FC<Props> = ({ config }) => {
const initCollection = dataStore(state => state.initCollection)

Check failure on line 19 in src/components/panel/Panel.tsx

GitHub Actions / build (18)

'initCollection' is assigned a value but never used

Check failure on line 19 in src/components/panel/Panel.tsx

GitHub Actions / build (20)

'initCollection' is assigned a value but never used
const getCollection = dataStore(state => state.getCollection)
const addPanelContent = contentStore((state) => state.addPanelContent)

const [error, setError] = useState<boolean | string>(false)
@@ -28,8 +29,10 @@
const collectionUrl = config.entrypoint.url
const init = async () => {
try {

setLoading(true)
const collection = await initCollection(collectionUrl)
const collection = await getCollection(collectionUrl)

const manifest = await apiRequest<Manifest>(collection.sequence[config.manifestIndex ?? 0].id)
const item = await apiRequest<Item>(manifest.sequence[config.itemIndex ?? 0].id)
const contentTypes: string[] = getContentTypes(item.content)
@@ -49,7 +52,7 @@
}
}
init()
}, [config])

Check warning on line 55 in src/components/panel/Panel.tsx

GitHub Actions / build (18)

React Hook useEffect has missing dependencies: 'addPanelContent' and 'getCollection'. Either include them or remove the dependency array

Check warning on line 55 in src/components/panel/Panel.tsx

GitHub Actions / build (20)

React Hook useEffect has missing dependencies: 'addPanelContent' and 'getCollection'. Either include them or remove the dependency array

if (error) {
return <ErrorComponent message={error} />
28 changes: 28 additions & 0 deletions src/components/tree/CollectionSubtree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@


import { FC } from 'react'
import ManifestSubtree from '@/components/tree/ManifestSubtree'


interface CollectionSubtreeProps {
collectionData: any
}

const CollectionSubtree: FC<CollectionSubtreeProps> = ({ collectionData }) => {

const collectionSubTree = collectionData.children.length > 0
&&
collectionData.children.map((manifest: ManifestNode, i: number) => (
<ManifestSubtree key={i} manifestData={manifest} />
))

return <div className="collection-subtree">

{collectionData.title}
<div className="t-ml-[5px]">
{ collectionSubTree}
</div>
</div>
}

export default CollectionSubtree
35 changes: 35 additions & 0 deletions src/components/tree/Item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@


import { FC, MouseEvent, useState } from 'react'

import { dataStore } from '@/store/DataStore.tsx'

interface ItemProps {
label: string,
url: string
}

const ItemTree: FC<ItemProps> = ({ label, url }) => {

const [active, setActive] = useState(false)
const [itemUrl] = useState(url)

const setClickedItemUrl = dataStore(state => state.setClickedItemUrl)
const clickedItemUrl = dataStore(state => state.clickedItemUrl)

function handleClick(e: MouseEvent<HTMLButtonElement, MouseEvent>) {
e.preventDefault()
if (!active) setClickedItemUrl(itemUrl)

setActive(prevState => !prevState)
}

return <div>
<button className="t-w-full t-text-left hover:t-bg-gray-200 t-cursor-pointer" style={{backgroundColor: url === clickedItemUrl ? '#0284c7': 'white'}}
onClick={(e) => handleClick(e)}>
{ label }
</button>
</div>
}

export default ItemTree
27 changes: 27 additions & 0 deletions src/components/tree/ManifestSubtree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@


import { FC } from 'react'
import ItemTree from '@/components/tree/Item'

interface ManifestSubtreeProps {
manifestData: any
}

const ManifestSubtree: FC<ManifestSubtreeProps> = ({ manifestData }) => {

const itemsLabels = manifestData.children.length > 0
&&
manifestData.children.map((item: ItemNode, i: number) => (
<ItemTree label={item.label} url = {item.url} key={i}/>
))

return <div className="manifest-subtree">

{manifestData.label}
<div className="t-ml-[5px] t-flex t-flex-col">
{ itemsLabels}
</div>
</div>
}

export default ManifestSubtree
3 changes: 2 additions & 1 deletion src/components/ui/popover.tsx
Original file line number Diff line number Diff line change
@@ -23,5 +23,6 @@ const PopoverContent = React.forwardRef<
/>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
const ClosePopover = PopoverPrimitive.Close

export { Popover, PopoverTrigger, PopoverContent }
export { Popover, PopoverTrigger, PopoverContent, ClosePopover }
11 changes: 9 additions & 2 deletions src/store/ConfigStore.tsx
Original file line number Diff line number Diff line change
@@ -2,12 +2,19 @@ import { create } from 'zustand'

interface ConfigStoreType {
config: Config,
addCustomConfig: (customConfig: Config) => void
addCustomConfig: (customConfig: Config) => void,
addNewPanel: (newPanel: PanelConfig) => void
}

export const configStore = create<ConfigStoreType>((set) => ({
export const configStore = create<ConfigStoreType>((set, get) => ({
config: {},
addCustomConfig: (customConfig: Config) => {
set({ config: customConfig })
},
addNewPanel: (newPanel: PanelConfig) => {
let newConfig = {...get().config}
newConfig.panels?.push(newPanel)

set({config: newConfig})
}
}))
24 changes: 23 additions & 1 deletion src/store/DataStore.tsx
Original file line number Diff line number Diff line change
@@ -6,17 +6,39 @@ interface CollectionMap {
}

interface DataStoreType {
collections: CollectionMap
collections: CollectionMap,
treeNodes: CollectionNode[],
clickedItemUrl: string,
initCollection: (url: string) => Promise<Collection>
initTreeNodes: (newTreeNodes: CollectionNode[]) => void,
setClickedItemUrl: (newUrl: string) => void,
getCollection: (collectionUrl: string) => Promise<Collection>

}

export const dataStore = create<DataStoreType>((set, get) => ({
collections: {},
treeNodes: [],
clickedItemUrl: '',
initCollection: async (url: string) => {
const collection = await apiRequest<Collection>(url)
const collections: CollectionMap = { ...get().collections }
collections[collection.id] = collection
set({ collections })
return collection
},
initTreeNodes: (newTreeNodes: CollectionNode[]) => {
set({ treeNodes: newTreeNodes})
},

setClickedItemUrl: (newUrl: string) => {
set({clickedItemUrl: newUrl})
},

async getCollection(collectionUrl: string): Promise<Collection> {
if (collectionUrl in get().collections) return get().collections[collectionUrl]

const collection = await get().initCollection(collectionUrl)
return collection
}
}))
31 changes: 28 additions & 3 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -210,9 +210,9 @@ declare global {
}
interface PanelConfig {
entrypoint: Entrypoint
colors: Colors
manifestIndex: number
itemIndex: number
color?: Colors
manifestIndex?: number
itemIndex?: number
}

type RangeSelector = {
@@ -265,6 +265,31 @@ declare global {
}

type HttpResponse<T> = SuccessResponse<T> | ErrorResponse


// tree types

interface ItemNode {
key: string,
label: string,
url: string
}

interface ManifestNode {
key: string,
label: string,
children: ItemNode[]
}

interface CollectionNode {
key: number,
title: string,
url: string,
children: ManifestNode
}

}



export {}
103 changes: 103 additions & 0 deletions src/utils/tree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@

import { request } from '@/utils/http'

export async function createTree(panels: PanelConfig[]) {
if (!panels || panels.length === 0) return []

const nodes: CollectionNode[] = []

for (let i = 0; i< panels.length; i++) {
await createCollectionNode(panels[i].entrypoint.url, i).then((node) => {
nodes.push(node)
})
}

return nodes
}



async function createCollectionNode(url: string, key: number) {
const node: CollectionNode = {}
const response = await request<Collection>(url)
if (!response.success) return node
const collectionTitle = response.data.title[0].title

node['url'] = url
node['key'] = key
node['title'] = collectionTitle
node['children'] = await getManifestNodes(node, response.data.sequence)

return node
}

async function getManifestNodes(parentNode, manifests) {
// items: 'sequence items' of collection
if (!manifests || manifests.length === 0) return []

const collectionNode = { ...parentNode }
collectionNode['children'] = []

for (let i = 0; i < manifests.length; i++) {
// here node refers to manifestNode
const node: ManifestNode = {}
node['key'] = collectionNode.key + '-' + i.toString()
node['label'] = manifests[i].label

// getItemsTitles
const response = await request<Manifest>(manifests[i].id)
if (!response.success) continue

const data = response.data
node['children'] = getItemsNodes(node['key'], data.sequence)

collectionNode['children'].push(node)
}
return collectionNode['children']
}

function getItemsNodes(parentKey: string, items: Sequence[]) {
const nodes = []
for (let i = 0; i < items.length ; i++) {
nodes.push({ label: items[i].label, key: parentKey + '-' + i, url: items[i].id })
}
return nodes
}

interface ItemIndices {
collectionUrl: string,
manifestIndex: number,
itemIndex: number
}

export function getItemIndices(itemUrl: string, treeNodes: CollectionNode[]): ItemIndices | null{

for (let i = 0; i < treeNodes.length ; i++) {
const collectionNode = treeNodes[i]

if (!collectionNode.children || collectionNode.children.length === 0) return null

for (let j = 0; j < collectionNode.children.length; j++) {

const manifest = collectionNode.children[j]

if (!manifest.children || manifest.children.length === 0) continue

const itemIndex = getItemIndex(manifest, itemUrl)
if (itemIndex !== -1) {
return {
collectionUrl: collectionNode.url,
manifestIndex: j,
itemIndex: itemIndex
}
}
}
}

return null
}


function getItemIndex(manifest: ManifestNode, itemUrl: string): number {
return manifest.children.findIndex((item: ItemNode) => item.url === itemUrl)
}

Unchanged files with check annotations Beta

if (icon?.type === 'icon') {
const iconEls = ref.current.children
if (iconEls && iconEls.length > 0) {
iconEl = iconEls[0]

Check warning on line 26 in src/components/CustomHTML.tsx

GitHub Actions / build (18)

Assignments to the 'iconEl' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect

Check warning on line 26 in src/components/CustomHTML.tsx

GitHub Actions / build (20)

Assignments to the 'iconEl' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect
iconEl.classList.add('t-w-' + icon.width, 't-h-' + icon.height)
}
}
return () => {
viewerRef.current?.destroy()
}
}, [])

Check warning on line 38 in src/components/OpenSeaDragonViewer.tsx

GitHub Actions / build (18)

React Hook useEffect has missing dependencies: 'imageUrl', 'panelId', and 'viewerId'. Either include them or remove the dependency array

Check warning on line 38 in src/components/OpenSeaDragonViewer.tsx

GitHub Actions / build (20)

React Hook useEffect has missing dependencies: 'imageUrl', 'panelId', and 'viewerId'. Either include them or remove the dependency array
return (
<div>
<ImageActionButtons />
}
updateText(contentUrl)
}, [activeContentTypeIndex])

Check warning on line 47 in src/components/panel/views/PanelCentralContent.tsx

GitHub Actions / build (18)

React Hook useEffect has a missing dependency: 'content'. Either include it or remove the dependency array

Check warning on line 47 in src/components/panel/views/PanelCentralContent.tsx

GitHub Actions / build (20)

React Hook useEffect has a missing dependency: 'content'. Either include it or remove the dependency array
if (error) {
return <ErrorComponent message={error} />