From 7c7a055e878c8d3649e2e158f4dbcd40921ba95d Mon Sep 17 00:00:00 2001 From: ValerieCodeurs Date: Fri, 3 Mar 2023 17:54:54 +0100 Subject: [PATCH 1/4] Added button to collapse and expand content tree --- src/dashboard/App.tsx | 72 +++++++++++++++---------- src/dashboard/hook/UseContentTree.ts | 32 ++++++++++- src/dashboard/view/ContentTree.tsx | 38 ++++++------- src/dashboard/view/entry/RootHeader.tsx | 18 +++++-- 4 files changed, 105 insertions(+), 55 deletions(-) diff --git a/src/dashboard/App.tsx b/src/dashboard/App.tsx index 09c1e4c0..3eae35ae 100644 --- a/src/dashboard/App.tsx +++ b/src/dashboard/App.tsx @@ -1,53 +1,55 @@ -import {renderLabel, Session} from 'alinea/core' +import {DashboardProvider, useDashboard} from './hook/UseDashboard.js' +import {DraftsProvider, DraftsStatus, useDrafts} from './hook/UseDrafts.js' import { ErrorBoundary, FavIcon, Loader, PreferencesProvider, Statusbar, - useObservable, - Viewport + Viewport, + useObservable } from 'alinea/ui' -import {IcRoundCheck} from 'alinea/ui/icons/IcRoundCheck' -import {IcRoundEdit} from 'alinea/ui/icons/IcRoundEdit' -import {IcRoundInsertDriveFile} from 'alinea/ui/icons/IcRoundInsertDriveFile' -import {IcRoundRotateLeft} from 'alinea/ui/icons/IcRoundRotateLeft' -import {MdiSourceBranch} from 'alinea/ui/icons/MdiSourceBranch' +import {Fragment, Suspense, useMemo, useState} from 'react' +import { + QueryClient, + QueryClientProvider as ReactQueryClientProvider +} from 'react-query' import { Routes, useLocation, useMatch, useParams } from 'alinea/ui/util/HashRouter' -import {Fragment, Suspense, useMemo, useState} from 'react' -import { - QueryClient, - QueryClientProvider as ReactQueryClientProvider -} from 'react-query' -import {DashboardOptions} from './Dashboard.js' -import {CurrentDraftProvider} from './hook/UseCurrentDraft.js' -import {DashboardProvider, useDashboard} from './hook/UseDashboard.js' -import {useDraft} from './hook/UseDraft.js' -import {DraftsProvider, DraftsStatus, useDrafts} from './hook/UseDrafts.js' -import {useDraftsList} from './hook/UseDraftsList.js' -import {useEntryLocation} from './hook/UseEntryLocation.js' -import {EntrySummaryProvider} from './hook/UseEntrySummary.js' -import {useLocale} from './hook/UseLocale.js' -import {useNav} from './hook/UseNav.js' -import {useRoot} from './hook/UseRoot.js' -import {SessionProvider} from './hook/UseSession.js' -import {useWorkspace} from './hook/UseWorkspace.js' -import {Head} from './util/Head.js' +import {Session, renderLabel} from 'alinea/core' + import {ContentTree} from './view/ContentTree.js' +import {CurrentDraftProvider} from './hook/UseCurrentDraft.js' +import {DashboardOptions} from './Dashboard.js' import {DraftsOverview} from './view/DraftsOverview.js' import {EditMode} from './view/entry/EditMode.js' +import {EntryEdit} from './view/EntryEdit.js' +import {EntrySummaryProvider} from './hook/UseEntrySummary.js' +import {Head} from './util/Head.js' +import {IcRoundCheck} from 'alinea/ui/icons/IcRoundCheck' +import {IcRoundEdit} from 'alinea/ui/icons/IcRoundEdit' +import {IcRoundInsertDriveFile} from 'alinea/ui/icons/IcRoundInsertDriveFile' +import {IcRoundRotateLeft} from 'alinea/ui/icons/IcRoundRotateLeft' +import {MdiSourceBranch} from 'alinea/ui/icons/MdiSourceBranch' import {NewEntry} from './view/entry/NewEntry.js' import {RootHeader} from './view/entry/RootHeader.js' -import {EntryEdit} from './view/EntryEdit.js' import {RootOverview} from './view/RootOverview.js' import {SearchBox} from './view/SearchBox.js' +import {SessionProvider} from './hook/UseSession.js' import {Sidebar} from './view/Sidebar.js' import {Toolbar} from './view/Toolbar.js' +import {useContentTree} from './hook/UseContentTree.js' +import {useDraft} from './hook/UseDraft.js' +import {useDraftsList} from './hook/UseDraftsList.js' +import {useEntryLocation} from './hook/UseEntryLocation.js' +import {useLocale} from './hook/UseLocale.js' +import {useNav} from './hook/UseNav.js' +import {useRoot} from './hook/UseRoot.js' +import {useWorkspace} from './hook/UseWorkspace.js' const Router = { Entry() { @@ -204,16 +206,28 @@ function EntryRoute({id}: EntryRouteProps) { .concat(draft?.alinea.parents) .concat(draft?.id) .filter(Boolean) as Array + const { + isTreeOpen, + toggleTreeOpen, + locale: currentLocale, + ...contentTree + } = useContentTree({ + locale, + workspace: workspace.name, + root: root.name, + select + }) return ( - + toggleTreeOpen(isTreeOpen)} /> {search === '?new' && ( diff --git a/src/dashboard/hook/UseContentTree.ts b/src/dashboard/hook/UseContentTree.ts index 21a579c6..1b7d9b0e 100644 --- a/src/dashboard/hook/UseContentTree.ts +++ b/src/dashboard/hook/UseContentTree.ts @@ -1,6 +1,7 @@ import {Entry, EntryMeta, Label, Outcome} from 'alinea/core' import {Cursor, Functions} from 'alinea/store' import {useCallback, useEffect, useMemo, useState} from 'react' + import {useQuery} from 'react-query' import {useRoot} from '../hook/UseRoot.js' import {useSession} from '../hook/UseSession.js' @@ -92,7 +93,7 @@ export function useContentTree({ const stored = window?.localStorage?.getItem(persistenceId) const opened = stored && JSON.parse(stored) return new Set([ - ...select, + // ...select, ...(Array.isArray(opened) ? opened : []) ]) }) @@ -151,9 +152,36 @@ export function useContentTree({ true ) }) + const parentEntryOpen = (entry: ContentTreeEntry) => + entry.childrenCount > 0 && isOpen(entry.id) + const isTreeOpen = entries.some(parentEntryOpen) + const toggleTreeOpen = useCallback( + (open: boolean) => { + if (open) { + window?.localStorage?.setItem(persistenceId, JSON.stringify([])) + setOpen(new Set([])) + } + if (!open) { + // Todo: figure out how to open always the same/all entries + const entryIds = entries.map(entry => entry.id) + window?.localStorage?.setItem(persistenceId, JSON.stringify(entryIds)) + setOpen(new Set(entryIds)) + } + }, + [setOpen] + ) useEffect(() => { setOpen(current => new Set([...current, ...select])) }, [select.join('.')]) - return {locale, entries, isOpen, toggleOpen, refetch, index} + return { + locale, + entries, + isOpen, + toggleOpen, + refetch, + index, + isTreeOpen, + toggleTreeOpen + } } diff --git a/src/dashboard/view/ContentTree.tsx b/src/dashboard/view/ContentTree.tsx index 727cb2b2..5727bf3c 100644 --- a/src/dashboard/view/ContentTree.tsx +++ b/src/dashboard/view/ContentTree.tsx @@ -16,20 +16,20 @@ import { sortableKeyboardCoordinates, verticalListSortingStrategy } from '@dnd-kit/sortable' +import {fromModule, usePreferences} from 'alinea/ui' +import {useMemo, useRef, useState} from 'react' +import {TreeNode, TreeNodeSortable} from './tree/TreeNode.js' + import useSize from '@react-hook/size' import {Entry} from 'alinea/core/Entry' import {generateKeyBetween} from 'alinea/core/util/FractionalIndexing' -import {fromModule, usePreferences} from 'alinea/ui' -import {useMemo, useRef, useState} from 'react' import VirtualList from 'react-tiny-virtual-list' -import {ContentTreeEntry, useContentTree} from '../hook/UseContentTree.js' +import {ContentTreeEntry} from '../hook/UseContentTree.js' import {useDashboard} from '../hook/UseDashboard.js' import {useDrafts} from '../hook/UseDrafts.js' import {useNav} from '../hook/UseNav.js' import {useRoot} from '../hook/UseRoot.js' -import {useWorkspace} from '../hook/UseWorkspace.js' import css from './ContentTree.module.scss' -import {TreeNode, TreeNodeSortable} from './tree/TreeNode.js' const styles = fromModule(css) @@ -83,29 +83,25 @@ export type ContentTreeProps = { locale: string | undefined select?: Array redirectToRoot?: boolean + entries: ContentTreeEntry[] + index: Map + toggleOpen: (id: string) => void + isOpen: (id: string) => boolean + refetch: () => void } export function ContentTree({ - locale: currentLocale, + locale, select = [], - redirectToRoot + redirectToRoot, + entries: treeEntries, + index, + toggleOpen, + isOpen, + refetch }: ContentTreeProps) { const {schema} = useDashboard().config - const {name: workspace} = useWorkspace() const root = useRoot() - const { - locale, - entries: treeEntries, - isOpen, - toggleOpen, - refetch, - index - } = useContentTree({ - locale: currentLocale, - workspace, - root: root.name, - select - }) const drafts = useDrafts() const [moves, setMoves] = useState>([]) const entries = sortByIndex(index, applyMoves(treeEntries, moves)) diff --git a/src/dashboard/view/entry/RootHeader.tsx b/src/dashboard/view/entry/RootHeader.tsx index b3078dcd..e23dde38 100644 --- a/src/dashboard/view/entry/RootHeader.tsx +++ b/src/dashboard/view/entry/RootHeader.tsx @@ -1,7 +1,9 @@ -import {Listbox} from '@headlessui/react' import {Create, fromModule, HStack, Icon, TextLabel} from 'alinea/ui' -import {IcRoundUnfoldMore} from 'alinea/ui/icons/IcRoundUnfoldMore' import {link, useNavigate} from 'alinea/ui/util/HashRouter' + +import {IcRoundUnfoldMore} from 'alinea/ui/icons/IcRoundUnfoldMore' + +import {Listbox} from '@headlessui/react' import {useState} from 'react' import {useCurrentDraft} from '../../hook/UseCurrentDraft.js' import {useLocale} from '../../hook/UseLocale.js' @@ -12,7 +14,11 @@ import css from './RootHeader.module.scss' const styles = fromModule(css) -export function RootHeader() { +export type RootHeaderProps = { + toggleTreeOpen: () => void +} + +export function RootHeader({toggleTreeOpen}: RootHeaderProps) { const nav = useNav() const root = useRoot() const {name: workspace} = useWorkspace() @@ -28,6 +34,12 @@ export function RootHeader() { {root.i18n && } + {/* Todo: change icon */} + ) From 00061700bc0ed5efa0bc835c79fb893bb08dc7cc Mon Sep 17 00:00:00 2001 From: ValerieCodeurs Date: Wed, 22 Mar 2023 15:46:32 +0100 Subject: [PATCH 2/4] Show toggle conditionally --- src/dashboard/App.tsx | 8 ++++++-- src/dashboard/hook/UseContentTree.ts | 26 +++++++++++++------------ src/dashboard/view/entry/RootHeader.tsx | 22 +++++++++++---------- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/dashboard/App.tsx b/src/dashboard/App.tsx index 3eae35ae..d400f632 100644 --- a/src/dashboard/App.tsx +++ b/src/dashboard/App.tsx @@ -208,7 +208,8 @@ function EntryRoute({id}: EntryRouteProps) { .filter(Boolean) as Array const { isTreeOpen, - toggleTreeOpen, + showToggle, + toggleTree, locale: currentLocale, ...contentTree } = useContentTree({ @@ -221,7 +222,10 @@ function EntryRoute({id}: EntryRouteProps) { - toggleTreeOpen(isTreeOpen)} /> + toggleTree(isTreeOpen)} + /> entry.childrenCount > 0 && isOpen(entry.id) const isTreeOpen = entries.some(parentEntryOpen) - const toggleTreeOpen = useCallback( + const parentEntry = (entry: ContentTreeEntry) => entry.childrenCount > 0 + const showToggle = entries.some(parentEntry) + const toggleTree = useCallback( (open: boolean) => { if (open) { window?.localStorage?.setItem(persistenceId, JSON.stringify([])) setOpen(new Set([])) } if (!open) { - // Todo: figure out how to open always the same/all entries - const entryIds = entries.map(entry => entry.id) + const entryIds = results.map(entry => entry.id) window?.localStorage?.setItem(persistenceId, JSON.stringify(entryIds)) setOpen(new Set(entryIds)) } @@ -182,6 +183,7 @@ export function useContentTree({ refetch, index, isTreeOpen, - toggleTreeOpen + showToggle, + toggleTree } } diff --git a/src/dashboard/view/entry/RootHeader.tsx b/src/dashboard/view/entry/RootHeader.tsx index e23dde38..a38653f7 100644 --- a/src/dashboard/view/entry/RootHeader.tsx +++ b/src/dashboard/view/entry/RootHeader.tsx @@ -1,9 +1,8 @@ import {Create, fromModule, HStack, Icon, TextLabel} from 'alinea/ui' import {link, useNavigate} from 'alinea/ui/util/HashRouter' -import {IcRoundUnfoldMore} from 'alinea/ui/icons/IcRoundUnfoldMore' - import {Listbox} from '@headlessui/react' +import {IcRoundUnfoldMore} from 'alinea/ui/icons/IcRoundUnfoldMore' import {useState} from 'react' import {useCurrentDraft} from '../../hook/UseCurrentDraft.js' import {useLocale} from '../../hook/UseLocale.js' @@ -15,10 +14,11 @@ import css from './RootHeader.module.scss' const styles = fromModule(css) export type RootHeaderProps = { - toggleTreeOpen: () => void + showToggle: boolean + toggleTree: () => void } -export function RootHeader({toggleTreeOpen}: RootHeaderProps) { +export function RootHeader({showToggle, toggleTree}: RootHeaderProps) { const nav = useNav() const root = useRoot() const {name: workspace} = useWorkspace() @@ -34,12 +34,14 @@ export function RootHeader({toggleTreeOpen}: RootHeaderProps) { {root.i18n && } - {/* Todo: change icon */} - + {showToggle && ( + + )} ) From 5dd94a199eb462dc413c8291b08afef3a39bdb47 Mon Sep 17 00:00:00 2001 From: ValerieCodeurs Date: Mon, 3 Apr 2023 13:05:06 +0200 Subject: [PATCH 3/4] Change icon depending if tree is open or not --- src/dashboard/App.tsx | 1 + src/dashboard/view/entry/RootHeader.tsx | 37 +++++++++++++++++-------- src/ui/IconButton.module.scss | 1 - 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/dashboard/App.tsx b/src/dashboard/App.tsx index d400f632..d86c56b8 100644 --- a/src/dashboard/App.tsx +++ b/src/dashboard/App.tsx @@ -225,6 +225,7 @@ function EntryRoute({id}: EntryRouteProps) { toggleTree(isTreeOpen)} + isTreeOpen={isTreeOpen} /> void + isTreeOpen: boolean } -export function RootHeader({showToggle, toggleTree}: RootHeaderProps) { +export function RootHeader({ + showToggle, + toggleTree, + isTreeOpen +}: RootHeaderProps) { const nav = useNav() const root = useRoot() const {name: workspace} = useWorkspace() @@ -35,12 +49,13 @@ export function RootHeader({showToggle, toggleTree}: RootHeaderProps) { {root.i18n && } {showToggle && ( - + )} diff --git a/src/ui/IconButton.module.scss b/src/ui/IconButton.module.scss index bf4695b6..690ee3e0 100644 --- a/src/ui/IconButton.module.scss +++ b/src/ui/IconButton.module.scss @@ -24,7 +24,6 @@ &:focus { outline: none; color: inherit; - background: var(--alinea-background); box-shadow: 0 0 0 2px var(--alinea-fields-focus); } From 7afefd849c8a9603c99bc8f7118ffbf0c5292948 Mon Sep 17 00:00:00 2001 From: ValerieCodeurs Date: Mon, 3 Apr 2023 14:26:18 +0200 Subject: [PATCH 4/4] Fixed childrenCount with locale --- src/dashboard/hook/UseContentTree.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/dashboard/hook/UseContentTree.ts b/src/dashboard/hook/UseContentTree.ts index e93998be..aaf78478 100644 --- a/src/dashboard/hook/UseContentTree.ts +++ b/src/dashboard/hook/UseContentTree.ts @@ -1,12 +1,12 @@ -import {Cursor, Functions} from 'alinea/store' import {Entry, EntryMeta, Label, Outcome} from 'alinea/core' +import {Cursor, Functions} from 'alinea/store' import {useCallback, useEffect, useMemo, useState} from 'react' -import {useDashboard} from './UseDashboard.js' import {useQuery} from 'react-query' import {useRoot} from '../hook/UseRoot.js' import {useSession} from '../hook/UseSession.js' import {useWorkspace} from '../hook/UseWorkspace.js' +import {useDashboard} from './UseDashboard.js' type QueryParams = { workspace: string @@ -26,10 +26,9 @@ export interface ContentTreeEntry { alinea: EntryMeta } -function query({workspace, root, locale, open, visible}: QueryParams) { +function query({workspace, root, locale, visible}: QueryParams) { const Parent = Entry.as('Parent') const id = locale ? Entry.i18n.id : Entry.id - const parent = locale ? Entry.i18n.parent : Entry.parent const summary = { id: Entry.id, type: Entry.type, @@ -41,9 +40,8 @@ function query({workspace, root, locale, open, visible}: QueryParams) { parent: Entry.parent, parents: Entry.parents }, - // Todo: fix childrenCount when locale childrenCount: Parent.where( - (locale ? Parent.alinea.i18n.parent : Parent.alinea.parent).is(Entry.id) + (locale ? Parent.alinea.i18n.parent : Parent.alinea.parent).is(id) ) .select(Functions.count()) .first() @@ -92,10 +90,7 @@ export function useContentTree({ const [open, setOpen] = useState(() => { const stored = window?.localStorage?.getItem(persistenceId) const opened = stored && JSON.parse(stored) - return new Set([ - // ...select, - ...(Array.isArray(opened) ? opened : []) - ]) + return new Set([...(Array.isArray(opened) ? opened : [])]) }) const isOpen = useCallback((id: string) => open.has(id), [open]) const toggleOpen = useCallback(