From df7f5526d7b4b7a65b1cf813e5ac398eb805a201 Mon Sep 17 00:00:00 2001 From: orlinmalkja Date: Mon, 10 Feb 2025 13:47:29 +0100 Subject: [PATCH] feat: add update/create a new panel through the Global Tree Modal and make the Tree Component generic --- src/App.tsx | 31 ++++++- src/components/Modal.tsx | 42 ++++++++++ src/components/PanelsWrapper.tsx | 2 +- src/components/TopBar.tsx | 41 +++++++-- src/components/Tree.tsx | 24 +++--- src/components/TreeSelectionModal.tsx | 25 ------ src/components/base/IconRenderer.tsx | 30 +++++++ src/components/base/InputField.tsx | 7 +- .../GlobalTreeSelectionModalContent.tsx | 53 ++++++++++++ .../tree-modal/TreeSelectionModalContent.tsx | 83 ++----------------- src/components/tree/GlobalTree.tsx | 46 ++++++++++ src/components/tree/TreeNode.tsx | 35 +++++--- src/contexts/TreeContext.tsx | 23 ++--- src/store/ConfigStore.tsx | 9 +- src/store/DataStore.tsx | 7 +- src/types.d.ts | 7 +- src/utils/icons.ts | 1 + src/utils/tree.ts | 17 ++-- 18 files changed, 319 insertions(+), 164 deletions(-) create mode 100644 src/components/Modal.tsx delete mode 100644 src/components/TreeSelectionModal.tsx create mode 100644 src/components/base/IconRenderer.tsx create mode 100644 src/components/tree-modal/GlobalTreeSelectionModalContent.tsx create mode 100644 src/components/tree/GlobalTree.tsx diff --git a/src/App.tsx b/src/App.tsx index ace024b9..bbe797ab 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,13 @@ -import PanelsWrapper from './components/PanelsWrapper' -import { FC } from 'react' +import { FC, useEffect } from 'react' + import { configStore } from '@/store/ConfigStore.tsx' +import { dataStore } from '@/store/DataStore.tsx' import TopBar from '@/components/TopBar' +import GlobalTree from '@/components/tree/GlobalTree.tsx' + +import { createCollectionNodes } from '@/utils/tree.ts' +import PanelsWrapper from '@/components/PanelsWrapper.tsx' interface AppProps { customConfig: Config @@ -12,10 +17,28 @@ const App: FC = ({ customConfig }) => { const addCustomConfig = configStore((state) => state.addCustomConfig) addCustomConfig(customConfig) + const collections = dataStore(state => state.collections) + const setTreeNodes = dataStore(state => state.setTreeNodes) + + + useEffect(() => { + async function initTree(collections: CollectionMap) { + const nodes = await createCollectionNodes(collections) + if (!nodes) return + + setTreeNodes(nodes) + } + + initTree(collections) + }, [collections]) + return (
- - + +
+ + +
) } diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx new file mode 100644 index 00000000..7371cb84 --- /dev/null +++ b/src/components/Modal.tsx @@ -0,0 +1,42 @@ +import { FC, ReactNode, useEffect, useState } from 'react' + +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' + +interface ModalProps { + children: ReactNode, + TriggerButton?: ReactNode, + showPopover?: boolean, + position?: Position +} + +const Modal: FC = ({ + children, TriggerButton, showPopover, position +}) => { + + const [isOpen, setIsOpen] = useState(false) + + const handleOpenChange = (open: boolean) => { + setIsOpen(open) + } + + useEffect(() => { + if (showPopover) setIsOpen(true) + }, [position]) + + return
+ + + {TriggerButton} + + + {children} + + +
+} + +export default Modal diff --git a/src/components/PanelsWrapper.tsx b/src/components/PanelsWrapper.tsx index 0cc1d2db..37b2181a 100644 --- a/src/components/PanelsWrapper.tsx +++ b/src/components/PanelsWrapper.tsx @@ -6,7 +6,7 @@ import { configStore } from '@/store/ConfigStore.tsx' const PanelsWrapper: FC = () => { const config = configStore(state => state.config) const panels = config?.panels - return
{ + return
{ panels?.map((panelConfig, i: number) => ()) }
} diff --git a/src/components/TopBar.tsx b/src/components/TopBar.tsx index 1ed26097..a9233e9b 100644 --- a/src/components/TopBar.tsx +++ b/src/components/TopBar.tsx @@ -1,21 +1,52 @@ -import { FC } from 'react' +import { FC, useState } from 'react' +import { configStore } from '@/store/ConfigStore.tsx' + +import Modal from '@/components/Modal.tsx' +import TreeSelectionModalContent from '@/components/tree-modal/TreeSelectionModalContent.tsx' +import IconRenderer from '@/components/base/IconRenderer.tsx' + +import { tree } from '@/utils/icons' +import { cross } from '@/utils/icons' +import { dataStore } from '@/store/DataStore.tsx' -import TreeSelectionModal from '@/components/TreeSelectionModal' const TopBar: FC = () => { + const [iconHtmlString, setIconHtmlString] = useState(tree) + const globalTree = configStore(state => state.config.globalTree) + + const setShowGlobalTree = dataStore(state => state.setShowGlobalTree) + + function toggleIcon() { + if (iconHtmlString === tree) { + // we click the tree icon - now we show the global tree (set the value to true) + setShowGlobalTree(true) + setIconHtmlString(cross) + } else if (iconHtmlString === cross) { + setShowGlobalTree(false) + setIconHtmlString(tree) + } + } + const addButton = - New + New return
- + + + +
- } export default TopBar diff --git a/src/components/Tree.tsx b/src/components/Tree.tsx index b02a9965..5eaff7e1 100644 --- a/src/components/Tree.tsx +++ b/src/components/Tree.tsx @@ -8,27 +8,25 @@ import TreeNode from '@/components/tree/TreeNode' interface TreeProps { nodes: TreeNode[], - onSelect(node: TreeNode): void, + onSelect(node: TreeNode, target): void, - onExpand(node: TreeNode): void, - - onCollapse(node: TreeNode): void, + getChildren(node: TreeNode): Promise } -const Tree: FC = ({ nodes, onSelect, onExpand, onCollapse }) => { +const Tree: FC = ({ nodes, onSelect, getChildren }) => { const tree = - nodes.length > 0 && - nodes.map((collection, i) => ( -
- -
- )) + nodes?.length > 0 && + nodes.map((collection, i) => ( +
+ +
+ )) - return
- + return
+ {tree}
diff --git a/src/components/TreeSelectionModal.tsx b/src/components/TreeSelectionModal.tsx deleted file mode 100644 index f38cbcab..00000000 --- a/src/components/TreeSelectionModal.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { FC, ReactNode } from 'react' - -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' -import TreeSelectionModalContent from '@/components/tree-modal/TreeSelectionModalContent.tsx' - -interface LocalTreeProps { - TriggerButton: ReactNode -} - -const TreeSelectionModal: FC = ({ TriggerButton }) => { - - - return
- - - {TriggerButton} - - - - - -
-} - -export default TreeSelectionModal diff --git a/src/components/base/IconRenderer.tsx b/src/components/base/IconRenderer.tsx new file mode 100644 index 00000000..77e4b78b --- /dev/null +++ b/src/components/base/IconRenderer.tsx @@ -0,0 +1,30 @@ +import { useEffect, useRef, FC, useState } from 'react' + +interface IconRendererProps { + htmlString: string + width?: number, + height?: number +} + +const IconRenderer: FC = ({ htmlString, width, height }) => { + const ref = useRef(null) + + const [iconWidth, setIconWidth] = useState(4) + const [iconHeight, setIconHeight] = useState(4) + + useEffect(() => { + if (width) setIconWidth(width) + if (height) setIconHeight(height) + + if (ref?.current) { + (ref.current as HTMLElement).innerHTML = htmlString + const iconEls = ref.current.children + + const iconEl = iconEls[0] + iconEl.classList.add('t-w-' + iconWidth, 't-h-' + iconHeight) + } + }, [htmlString, iconWidth, iconHeight]) + + return
+} +export default IconRenderer diff --git a/src/components/base/InputField.tsx b/src/components/base/InputField.tsx index b4310bda..4f858d59 100644 --- a/src/components/base/InputField.tsx +++ b/src/components/base/InputField.tsx @@ -1,13 +1,14 @@ import { FC } from 'react' interface InputFieldProps { - updateInputValue: (newValue: string) => void + updateInputValue: (newValue: string) => void, + width: number } -const InputField: FC = ({ updateInputValue }) => { +const InputField: FC = ({ updateInputValue, width }) => { return <> - updateInputValue(e.target.value)}/> } diff --git a/src/components/tree-modal/GlobalTreeSelectionModalContent.tsx b/src/components/tree-modal/GlobalTreeSelectionModalContent.tsx new file mode 100644 index 00000000..272b1ffc --- /dev/null +++ b/src/components/tree-modal/GlobalTreeSelectionModalContent.tsx @@ -0,0 +1,53 @@ +import { FC } from 'react' +import { configStore } from '@/store/ConfigStore.tsx' + + +interface SelectedItemIndicesType { + collectionUrl: string, + manifestIndex: number, + itemIndex: number, +} + +interface GlobalTreeSelectionModalContentProps { + selectedItemIndices: SelectedItemIndicesType, +} + +const GlobalTreeSelectionModalContent: FC = ({ selectedItemIndices }) => { + + const panels = configStore(state => state.config.panels) + const addNewPanel = configStore(state => state.addNewPanel) + const updatePanel = configStore(state => state.updatePanel) + + const newPanelConfig = { + entrypoint: { + url: selectedItemIndices.collectionUrl, + type: 'collection', + }, + manifestIndex: selectedItemIndices.manifestIndex, + itemIndex: selectedItemIndices.itemIndex + } + + let buttonsUpdatePanel + if (panels && panels?.length > 0) { + buttonsUpdatePanel = panels?.map((_, i) => ) + } + + + return ( +
+
+ {buttonsUpdatePanel} +
+ +
) +} + +export default GlobalTreeSelectionModalContent diff --git a/src/components/tree-modal/TreeSelectionModalContent.tsx b/src/components/tree-modal/TreeSelectionModalContent.tsx index a7cf5b75..b4393428 100644 --- a/src/components/tree-modal/TreeSelectionModalContent.tsx +++ b/src/components/tree-modal/TreeSelectionModalContent.tsx @@ -1,4 +1,4 @@ -import { FC, useEffect, useRef } from 'react' +import { FC, useRef } from 'react' import { configStore } from '@/store/ConfigStore' import { dataStore } from '@/store/DataStore' @@ -7,18 +7,17 @@ import { dataStore } from '@/store/DataStore' import Tree from '@/components/Tree.tsx' import InputField from '@/components/base/InputField.tsx' import { ClosePopover } from '@/components/ui/popover' -import { createTree, getChildren, getNodeIndices } from '@/utils/tree' + +import { getChildren, getNodeIndices } from '@/utils/tree.ts' const TreeSelectionModalContent: FC = () => { const addNewPanel = configStore(state => state.addNewPanel) - const setTreeNodes = dataStore(state => state.setTreeNodes) const initCollection = dataStore(state => state.initCollection) - const collections = dataStore(state => state.collections) - const nodes = dataStore(state => state.treeNodes) + const treeNodes = dataStore(state => state.treeNodes) const inputValue = useRef('') const clickedItemUrl = useRef('') @@ -30,20 +29,6 @@ const TreeSelectionModalContent: FC = () => { }) - useEffect(() => { - async function initTree(collections: CollectionMap) { - - const collectionsUrls = Object.keys(collections) - if (collectionsUrls.length === 0) return - - const nodes = await createTree(collectionsUrls) - setTreeNodes(nodes) - } - - initTree(collections) - }, [collections]) - - function updateInputValue(newValue: string) { inputValue.current = newValue } @@ -84,63 +69,11 @@ const TreeSelectionModalContent: FC = () => { } - async function onExpand(node: TreeNode) { - const { type } = node - const updatedTree = [...nodes] - - if (type === 'collection') { - const [collectionIndex] = getNodeIndices(node.key) - if (!('children' in updatedTree[collectionIndex])) { - const childrenNodes = await getChildren(node) - if (childrenNodes.length === 0) return - - updatedTree[collectionIndex].children = childrenNodes - } - - updatedTree[collectionIndex].expanded = true - - } else if (type === 'manifest') { - const [collectionIndex, manifestIndex] = getNodeIndices(node.key) - const manifests = updatedTree[collectionIndex].children - if (!manifests) return - if (manifests.length === 0) return - - const manifestChildren = await getChildren(node) - if (manifestChildren.length === 0) return - manifests[manifestIndex].children = manifestChildren - manifests[manifestIndex].expanded = true - - updatedTree[collectionIndex].children = [...manifests] - } - - setTreeNodes(updatedTree) - } - - async function onCollapse(node: TreeNode) { - const { type } = node - const updatedTree = [...nodes] - - if (type === 'collection') { - const [collectionIndex] = getNodeIndices(node.key) - updatedTree[collectionIndex].expanded = false - } else if (type === 'manifest') { - const [collectionIndex, manifestIndex] = getNodeIndices(node.key) - - const manifests = updatedTree[collectionIndex].children - if (!manifests) return - manifests[manifestIndex].expanded = false - - updatedTree[collectionIndex].children = [...manifests] - } - - setTreeNodes(updatedTree) - } - function onSelect(node: TreeNode) { const { id } = node clickedItemUrl.current = id const [collectionIndex, manifestIndex, itemIndex] = getNodeIndices(node.key) - const collectionUrl = nodes[collectionIndex].id + const collectionUrl = treeNodes[collectionIndex].id selectedItemIndices.current = { collectionUrl: collectionUrl, manifestIndex: manifestIndex, itemIndex: itemIndex } } @@ -148,11 +81,11 @@ const TreeSelectionModalContent: FC = () => { return
- Enter a Collection Url - + Enter a collection URL + Or choose: - +
{ + + const showGlobalTree = dataStore(state => state.showGlobalTree) + + const selectedItemIndices = useRef({ + collectionUrl: '', + manifestIndex: -1, + itemIndex: -1 + }) + + const treeNodes = dataStore(state => state.treeNodes) + + const [showSelectionModal, setShowSelectionModal] = useState(false) + const [positionSelectedItem, setPositionSelectedItem] = useState({ x: 0, y: 0 }) + + + function onSelectNode(node: TreeNode, target) { + const [collectionIndex, manifestIndex, itemIndex] = getNodeIndices(node.key) + const collectionUrl = treeNodes[collectionIndex].id + selectedItemIndices.current = { collectionUrl: collectionUrl, manifestIndex: manifestIndex, itemIndex: itemIndex } + + // when we click at another item, show the modal + setShowSelectionModal(true) + setPositionSelectedItem(target.getBoundingClientRect()) + } + + + return showGlobalTree &&
+ + + + +
+} + +export default GlobalTree diff --git a/src/components/tree/TreeNode.tsx b/src/components/tree/TreeNode.tsx index fc07765a..e078a0dc 100644 --- a/src/components/tree/TreeNode.tsx +++ b/src/components/tree/TreeNode.tsx @@ -1,4 +1,4 @@ -import { FC, useRef } from 'react' +import { FC, MouseEvent, useState } from 'react' import { useTree } from '@/contexts/TreeContext' @@ -9,19 +9,35 @@ interface TreeNodeProps { const TreeNode: FC = ({ node }) => { - const { onClick } = useTree() + const [hasChildren, setHasChildren] = useState(false) + const [isExpanded, setIsExpanded] = useState(false) + const { onSelect, getChildren } = useTree() - const itemRef = useRef(null) + async function handleNodeClick(e: MouseEvent) { + e.preventDefault() - function handleNodeClick() { - onClick(node) + if (!hasChildren) { + node.children = [...await getChildren(node)] + if (node.children.length > 0) setHasChildren(true) + } + + if (node.children.length > 0) { + toggleExpand() + return + } + + onSelect(node, e.target) + } + + const toggleExpand = () => { + setIsExpanded(!isExpanded) } - if ('children' in node && node.expanded) + if (hasChildren && isExpanded) return
handleNodeClick()}> {node.label}
+ onClick={(e) => handleNodeClick(e)}> {node.label}
{node.children?.map((item: TreeNode, i) => (
    @@ -29,9 +45,8 @@ const TreeNode: FC = ({ node }) => { ))}
- return
handleNodeClick()}>{node.label}
+ return
handleNodeClick(e)}>{node.label}
} export default TreeNode diff --git a/src/contexts/TreeContext.tsx b/src/contexts/TreeContext.tsx index 89596552..39899708 100644 --- a/src/contexts/TreeContext.tsx +++ b/src/contexts/TreeContext.tsx @@ -3,35 +3,24 @@ import { ReactNode, createContext, useContext, FC } from 'react' const TreeContext = createContext(undefined) interface TreeType { - onClick(node: TreeNode): void + onSelect(node: TreeNode, target): void - onSelect(node: TreeNode): void - - onExpand(node: TreeNode): void - - onCollapse(node: TreeNode): void + getChildren(node: TreeNode): Promise } interface TreeProviderProps { children?: ReactNode - onSelect(node: TreeNode): void + onSelect(node: TreeNode, target): void - onExpand(node: TreeNode): void - - onCollapse(node: TreeNode): void + getChildren(node: TreeNode): Promise } -const TreeProvider: FC = ({ children, onSelect, onExpand, onCollapse }) => { +const TreeProvider: FC = ({ children, onSelect, getChildren }) => { - function onClick(node: TreeNode) { - if ('leaf' in node) onSelect(node) - else if (!node.expanded) onExpand(node) - else if (node.expanded) onCollapse(node) - } return ( - + {children} ) diff --git a/src/store/ConfigStore.tsx b/src/store/ConfigStore.tsx index 7fa5aedf..14d55411 100644 --- a/src/store/ConfigStore.tsx +++ b/src/store/ConfigStore.tsx @@ -4,6 +4,7 @@ interface ConfigStoreType { config: Config, addCustomConfig: (customConfig: Config) => void, addNewPanel: (newPanelConfig: PanelConfig) => void, + updatePanel: (newPanelConfig: PanelConfig, newIndex: number) => void, } export const configStore = create((set, get) => ({ @@ -13,10 +14,16 @@ export const configStore = create((set, get) => ({ set({ config: customConfig }) }, addNewPanel: (newPanelConfig: PanelConfig) => { - const newConfig = { ...get().config } newConfig.panels?.push(newPanelConfig) set({ config: newConfig }) }, + + updatePanel: (newPanelConfig: PanelConfig, index: number) => { + const newConfig = { ...get().config } + if (newConfig.panels) newConfig.panels[index] = newPanelConfig + + set({ config: newConfig }) + } })) diff --git a/src/store/DataStore.tsx b/src/store/DataStore.tsx index 41125f17..39aaeea3 100644 --- a/src/store/DataStore.tsx +++ b/src/store/DataStore.tsx @@ -8,11 +8,14 @@ interface DataStoreType { initCollection: (url: string) => Promise setTreeNodes: (newTreeNodes: TreeNode[]) => void, getCollection: (collectionUrl: string) => Promise, + showGlobalTree: boolean, + setShowGlobalTree: (newValue: boolean) => void, } export const dataStore = create((set, get) => ({ collections: {}, treeNodes: [], + showGlobalTree: false, initCollection: async (url: string) => { const collection = await apiRequest(url) const collections: CollectionMap = { ...get().collections } @@ -30,5 +33,7 @@ export const dataStore = create((set, get) => ({ const collection = await get().initCollection(collectionUrl) return collection }, - + setShowGlobalTree: (newValue: boolean) => { + set({ showGlobalTree: newValue }) + } })) diff --git a/src/types.d.ts b/src/types.d.ts index 39125088..df694e6a 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -278,11 +278,16 @@ declare global { type: string, leaf?: boolean, expanded?: boolean, - children?: TreeNode[] + children: TreeNode[] } interface CollectionMap { [key: string]: Collection } + + interface Position { + x: number, + y: number + } } export {} diff --git a/src/utils/icons.ts b/src/utils/icons.ts index 40dd7470..80999851 100644 --- a/src/utils/icons.ts +++ b/src/utils/icons.ts @@ -7,3 +7,4 @@ export const zoomOut = ' { + const collectionsUrls = Object.keys(collections) + if (collectionsUrls.length === 0) return [] + const nodes: TreeNode[] = [] for (let i = 0; i < collectionsUrls.length; i++) { - await createNode(collectionsUrls[i], i).then((node) => { + await createCollectionNode(collectionsUrls[i], i).then((node) => { nodes.push(node) }) } @@ -12,9 +15,9 @@ export async function createTree(collectionsUrls: string[]) { return nodes } -async function createNode(url: string, key: number) { - const node: TreeNode = { key: '', id: '', type: '', label: '' } - +async function createCollectionNode(url: string, key: number) { + const node: TreeNode = { key: '', id: '', type: '', label: '', children: [] } + const response = await request(url) if (!response.success) return node @@ -50,11 +53,9 @@ export async function getChildren(node: TreeNode): Promise { id: items[i].id, label: items[i].label ?? 'label not found', type: items[i].type, - expanded: false + children: [] } - if (childNode.type === 'item') childNode.leaf = true - childrenNodes.push(childNode) }