Refactor shortcuts tree out of navmenu so we can use it in the shortcuts editor

This commit is contained in:
Jackson Harper
2024-07-31 16:20:57 +08:00
parent d44f5cc6ff
commit 332583d5c1
5 changed files with 478 additions and 563 deletions

View File

@ -0,0 +1,443 @@
import { useRouter } from 'next/router'
import { NodeApi, SimpleTree, Tree, TreeApi } from 'react-arborist'
import useResizeObserver from 'use-resize-observer'
import {
Shortcut,
useGetShortcuts,
useSetShortcuts,
} from '../../lib/networking/shortcuts/useShortcuts'
import { usePersistedState } from '../../lib/hooks/usePersistedState'
import { CSSProperties, useCallback, useMemo, useState } from 'react'
import { v4 as uuidv4 } from 'uuid'
import { Box, HStack, SpanBox } from '../elements/LayoutPrimitives'
import { Dropdown, DropdownOption } from '../elements/DropdownElements'
import { DotsThree, ListMagnifyingGlass, Tag } from '@phosphor-icons/react'
import { ShortcutFolderClosed } from '../elements/icons/ShortcutFolderClosed'
import { theme } from '../tokens/stitches.config'
import { ShortcutFolderOpen } from '../elements/icons/ShortcutFolderOpen'
import { CoverImage } from '../elements/CoverImage'
import { NewsletterIcon } from '../elements/icons/NewsletterIcon'
import { FollowingIcon } from '../elements/icons/FollowingIcon'
import { StyledText } from '../elements/StyledText'
import { OpenMap } from 'react-arborist/dist/module/state/open-slice'
type ShortcutsTreeProps = {
treeRef: React.MutableRefObject<TreeApi<Shortcut> | undefined>
}
export const ShortcutsTree = (props: ShortcutsTreeProps): JSX.Element => {
const router = useRouter()
const { ref, width, height } = useResizeObserver()
const { data, isLoading } = useGetShortcuts()
const setShorcuts = useSetShortcuts()
const [folderOpenState, setFolderOpenState] = usePersistedState<
Record<string, boolean>
>({
key: 'nav-menu-open-state',
isSessionStorage: false,
initialValue: {},
})
const tree = useMemo(() => {
const result = new SimpleTree<Shortcut>((data ?? []) as Shortcut[])
return result
}, [data])
const syncTreeData = async (data: Shortcut[]) => {
await setShorcuts.mutateAsync({ shortcuts: data })
}
const onMove = useCallback(
async (args: {
dragIds: string[]
parentId: null | string
index: number
}) => {
for (const id of args.dragIds) {
tree?.move({ id, parentId: args.parentId, index: args.index })
}
await syncTreeData(tree.data)
},
[tree, data]
)
const onCreate = useCallback(
async (args: { parentId: string | null; index: number; type: string }) => {
const data = { id: uuidv4(), name: '', type: 'folder' } as any
if (args.type === 'internal') {
data.children = []
}
tree.create({ parentId: args.parentId, index: args.index, data })
await syncTreeData(tree.data)
return data
},
[tree, data]
)
const onDelete = useCallback(
async (args: { ids: string[] }) => {
args.ids.forEach((id) => tree.drop({ id }))
await syncTreeData(tree.data)
},
[tree, data]
)
const onRename = useCallback(
async (args: { name: string; id: string }) => {
tree.update({ id: args.id, changes: { name: args.name } as any })
await syncTreeData(tree.data)
},
[tree, data]
)
const onToggle = useCallback(
(id: string) => {
if (id && props.treeRef.current) {
const isOpen = props.treeRef.current?.isOpen(id)
const newItem: OpenMap = {}
newItem[id] = isOpen
setFolderOpenState({ ...folderOpenState, ...newItem })
}
},
[props, folderOpenState, setFolderOpenState]
)
const onActivate = useCallback(
(node: NodeApi<Shortcut>) => {
if (node.data.type == 'folder') {
const join = node.data.join
if (join == 'or') {
const query = node.children
?.map((child) => {
return `(${child.data.filter})`
})
.join(' OR ')
}
} else if (node.data.section != null && node.data.filter != null) {
router.push(`/${node.data.section}?q=${node.data.filter}`)
}
},
[tree, router]
)
function countTotalShortcuts(shortcuts: Shortcut[]): number {
let total = 0
for (const shortcut of shortcuts) {
// Count the current shortcut
total++
// If the shortcut has children, recursively count them
if (shortcut.children && shortcut.children.length > 0) {
total += countTotalShortcuts(shortcut.children)
}
}
return total
}
const maximumHeight = useMemo(() => {
if (!data) {
return 320
}
return countTotalShortcuts(data as Shortcut[]) * 36
}, [data])
return (
<Box
ref={ref}
css={{
height: maximumHeight,
flexGrow: 1,
minBlockSize: 0,
}}
>
{!isLoading && (
<Tree
ref={props.treeRef}
data={data as Shortcut[]}
onCreate={onCreate}
onMove={onMove}
onDelete={onDelete}
onRename={onRename}
onToggle={onToggle}
onActivate={onActivate}
rowHeight={36}
initialOpenState={folderOpenState}
width={width}
height={maximumHeight}
>
{NodeRenderer}
</Tree>
)}
</Box>
)
}
function NodeRenderer(args: {
style: CSSProperties
node: NodeApi<Shortcut>
tree: TreeApi<Shortcut>
dragHandle?: (el: HTMLDivElement | null) => void
preview?: boolean
}) {
const isSelected = false
const [menuVisible, setMenuVisible] = useState(false)
const [menuOpened, setMenuOpened] = useState(false)
return (
<HStack
ref={args.dragHandle}
alignment="center"
distribution="start"
css={{
pl: `${20 + args.node.level * 15}px`,
mb: '2px',
gap: '10px',
display: 'flex',
width: '100%',
maxWidth: '100%',
height: '34px',
backgroundColor: isSelected ? '$thLibrarySelectionColor' : 'unset',
fontSize: '15px',
fontWeight: 'regular',
fontFamily: '$display',
color: isSelected
? '$thLibraryMenuSecondary'
: '$thLibraryMenuUnselected',
verticalAlign: 'middle',
borderRadius: '3px',
cursor: 'pointer',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
'&:hover': {
backgroundColor: isSelected
? '$thLibrarySelectionColor'
: '$thBackground4',
},
'&:active': {
outline: 'unset',
backgroundColor: isSelected
? '$thLibrarySelectionColor'
: '$thBackground4',
},
'&:hover [role="hover-menu"]': {
opacity: '1',
},
}}
onMouseEnter={() => {
setMenuVisible(true)
}}
onMouseLeave={() => {
setMenuVisible(false)
}}
title={args.node.data.name}
onClick={(e) => {
// router.push(`/` + props.section)
}}
>
<HStack
css={{
width: '100%',
height: '100%',
}}
distribution="start"
alignment="center"
>
<NodeItemContents node={args.node} />
<SpanBox
role="hover-menu"
css={{
display: 'flex',
ml: 'auto',
mr: '15px',
opacity: menuVisible || menuOpened ? '1' : '0',
}}
>
<Dropdown
side="bottom"
triggerElement={<DotsThree size={20} />}
css={{ ml: 'auto' }}
onOpenChange={(open) => {
setMenuOpened(open)
}}
>
<DropdownOption
onSelect={() => {
args.tree.delete(args.node)
}}
title="Remove"
/>
{/* {args.node.data.type == 'folder' && (
<DropdownOption
onSelect={() => {
args.node.data.join = 'or'
}}
title="Folder query: OR"
/>
)} */}
</Dropdown>
</SpanBox>
</HStack>
</HStack>
)
}
type NodeItemContentsProps = {
node: NodeApi<Shortcut>
}
const NodeItemContents = (props: NodeItemContentsProps): JSX.Element => {
if (props.node.isEditing) {
return (
<input
autoFocus
type="text"
defaultValue={props.node.data.name}
onFocus={(e) => e.currentTarget.select()}
onBlur={() => props.node.reset()}
onKeyDown={(e) => {
if (e.key === 'Escape') {
props.node.reset()
}
if (e.key === 'Enter') {
// props.node.data = {
// id: 'new-folder',
// type: 'folder',
// name: e.currentTarget.value,
// }
props.node.submit(e.currentTarget.value)
props.node.activate()
}
}}
/>
)
}
if (props.node.isLeaf) {
const shortcut = props.node.data
if (shortcut) {
switch (shortcut.type) {
case 'feed':
case 'newsletter':
return (
<SpanBox>
<FeedOrNewsletterShortcut shortcut={shortcut} />
</SpanBox>
)
case 'label':
return (
<Box>
<LabelShortcut shortcut={shortcut} />
</Box>
)
case 'search':
return (
<Box>
<SearchShortcut shortcut={shortcut} />
</Box>
)
}
}
} else {
return (
<HStack
distribution="start"
alignment="center"
css={{ gap: '10px', width: '100%' }}
onClick={(event) => {
props.node.toggle()
event.preventDefault()
}}
>
{props.node.isClosed ? (
<ShortcutFolderClosed
color={theme.colors.thLibraryMenuPrimary.toString()}
/>
) : (
<ShortcutFolderOpen
color={theme.colors.thLibraryMenuPrimary.toString()}
/>
)}
{props.node.data.name}
</HStack>
)
}
return <></>
}
type ShortcutItemProps = {
shortcut: Shortcut
}
const FeedOrNewsletterShortcut = (props: ShortcutItemProps): JSX.Element => {
return (
<HStack
alignment="center"
distribution="start"
css={{ pl: '10px', width: '100%', gap: '10px' }}
key={`search-${props.shortcut.id}`}
>
<HStack
distribution="start"
alignment="center"
css={{ minWidth: '20px' }}
>
{props.shortcut.icon ? (
<CoverImage
src={props.shortcut.icon}
width={20}
height={20}
css={{ borderRadius: '20px' }}
/>
) : props.shortcut.type == 'newsletter' ? (
<NewsletterIcon color="#F59932" size={18} />
) : (
<FollowingIcon color="#F59932" size={21} />
)}
</HStack>
<StyledText style="settingsItem">{props.shortcut.name}</StyledText>
</HStack>
)
}
const SearchShortcut = (props: ShortcutItemProps): JSX.Element => {
return (
<HStack
alignment="center"
distribution="start"
css={{ pl: '10px', width: '100%', gap: '7px' }}
key={`search-${props.shortcut.id}`}
>
<HStack
distribution="start"
alignment="center"
css={{ minWidth: '20px' }}
>
<ListMagnifyingGlass size={17} />
</HStack>
<StyledText style="settingsItem">{props.shortcut.name}</StyledText>
</HStack>
)
}
const LabelShortcut = (props: ShortcutItemProps): JSX.Element => {
return (
<HStack
alignment="center"
distribution="start"
css={{ width: '100%', gap: '7px' }}
key={`search-${props.shortcut.id}`}
>
<Tag
size={15}
color={props.shortcut.label?.color ?? 'gray'}
weight="fill"
/>
<StyledText style="settingsItem" css={{ pb: '1px' }}>
{props.shortcut.name}
</StyledText>
</HStack>
)
}

View File

@ -22,13 +22,13 @@ import { HomeIcon } from '../../elements/icons/HomeIcon'
import { LibraryIcon } from '../../elements/icons/LibraryIcon'
import { HighlightsIcon } from '../../elements/icons/HighlightsIcon'
import { CoverImage } from '../../elements/CoverImage'
import { Shortcut } from './NavigationMenu'
import { OutlinedLabelChip } from '../../elements/OutlinedLabelChip'
import { NewsletterIcon } from '../../elements/icons/NewsletterIcon'
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
import { useRouter } from 'next/router'
import { DiscoverIcon } from '../../elements/icons/DiscoverIcon'
import { escapeQuotes } from '../../../utils/helper'
import { Shortcut } from '../../../lib/networking/shortcuts/useShortcuts'
export const LIBRARY_LEFT_MENU_WIDTH = '275px'

View File

@ -1,16 +1,8 @@
import {
CSSProperties,
ReactNode,
useCallback,
useMemo,
useRef,
useState,
} from 'react'
import { ReactNode, useCallback, useRef, useState } from 'react'
import { StyledText } from '../../elements/StyledText'
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { Button } from '../../elements/Button'
import { DotsThree, List, X, Tag } from '@phosphor-icons/react'
import { Label } from '../../../lib/networking/fragments/labelFragment'
import { DotsThree } from '@phosphor-icons/react'
import { theme } from '../../tokens/stitches.config'
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
import { NavMenuFooter } from './Footer'
@ -18,52 +10,23 @@ import { FollowingIcon } from '../../elements/icons/FollowingIcon'
import { HomeIcon } from '../../elements/icons/HomeIcon'
import { LibraryIcon } from '../../elements/icons/LibraryIcon'
import { HighlightsIcon } from '../../elements/icons/HighlightsIcon'
import { CoverImage } from '../../elements/CoverImage'
import { NewsletterIcon } from '../../elements/icons/NewsletterIcon'
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
import { useRouter } from 'next/router'
import { NavigationSection } from '../NavigationLayout'
import { NodeApi, SimpleTree, Tree, TreeApi } from 'react-arborist'
import { ListMagnifyingGlass } from '@phosphor-icons/react'
import { TreeApi } from 'react-arborist'
import React from 'react'
import { fetchEndpoint } from '../../../lib/appConfig'
import { requestHeaders } from '../../../lib/networking/networkHelpers'
import { v4 as uuidv4 } from 'uuid'
import { showErrorToast } from '../../../lib/toastHelpers'
import { OpenMap } from 'react-arborist/dist/module/state/open-slice'
import { ArchiveSectionIcon } from '../../elements/icons/ArchiveSectionIcon'
import { NavMoreButtonDownIcon } from '../../elements/icons/NavMoreButtonDown'
import { NavMoreButtonUpIcon } from '../../elements/icons/NavMoreButtonUp'
import { ShortcutFolderClosed } from '../../elements/icons/ShortcutFolderClosed'
import { TrashSectionIcon } from '../../elements/icons/TrashSectionIcon'
import { ShortcutFolderOpen } from '../../elements/icons/ShortcutFolderOpen'
import useResizeObserver from 'use-resize-observer'
import {
useGetShortcuts,
Shortcut,
useResetShortcuts,
useSetShortcuts,
} from '../../../lib/networking/shortcuts/useShortcuts'
import { ShortcutsTree } from '../ShortcutsTree'
export const LIBRARY_LEFT_MENU_WIDTH = '275px'
export type ShortcutType = 'search' | 'label' | 'newsletter' | 'feed' | 'folder'
export type Shortcut = {
type: ShortcutType
id: string
name: string
section: string
filter: string
icon?: string
label?: Label
join?: string
children?: Shortcut[]
}
type NavigationMenuProps = {
section: NavigationSection
@ -270,6 +233,7 @@ const LibraryNav = (props: NavigationMenuProps): JSX.Element => {
}
const Shortcuts = (props: NavigationMenuProps): JSX.Element => {
const router = useRouter()
const treeRef = useRef<TreeApi<Shortcut> | undefined>(undefined)
const resetShortcuts = useResetShortcuts()
@ -321,6 +285,12 @@ const Shortcuts = (props: NavigationMenuProps): JSX.Element => {
triggerElement={<DotsThree size={20} />}
css={{ ml: 'auto' }}
>
<DropdownOption
onSelect={() => {
router.push(`/settings/shortcuts`)
}}
title="Edit shortcuts"
/>
<DropdownOption
onSelect={resetShortcutsToDefault}
title="Reset to default"
@ -350,514 +320,6 @@ const Shortcuts = (props: NavigationMenuProps): JSX.Element => {
)
}
type ShortcutsTreeProps = {
treeRef: React.MutableRefObject<TreeApi<Shortcut> | undefined>
}
async function getShortcuts(path: string): Promise<Shortcut[]> {
const url = new URL(path, fetchEndpoint)
try {
const response = await fetch(url.toString(), {
method: 'GET',
headers: requestHeaders(),
credentials: 'include',
mode: 'cors',
})
const payload = await response.json()
if ('shortcuts' in payload) {
return payload['shortcuts'] as Shortcut[]
}
return []
} catch (err) {
console.log('error getting shortcuts: ', err)
throw err
}
}
async function setShortcuts(
path: string,
{ arg }: { arg: { shortcuts: Shortcut[] } }
): Promise<Shortcut[]> {
const url = new URL(path, fetchEndpoint)
try {
const response = await fetch(url.toString(), {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...requestHeaders(),
},
credentials: 'include',
mode: 'cors',
body: JSON.stringify(arg),
})
const payload = await response.json()
if (!('shortcuts' in payload)) {
throw new Error('Error syncing shortcuts')
}
return payload['shortcuts'] as Shortcut[]
} catch (err) {
showErrorToast('Error syncing shortcut changes.')
}
return arg.shortcuts
}
async function resetShortcuts(path: string): Promise<Shortcut[]> {
const url = new URL(path, fetchEndpoint)
try {
const response = await fetch(url.toString(), {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
...requestHeaders(),
},
credentials: 'include',
mode: 'cors',
})
const payload = await response.json()
if (!('shortcuts' in payload)) {
throw new Error('Error syncing shortcuts')
}
return payload['shortcuts'] as Shortcut[]
} catch (err) {
showErrorToast('Error syncing shortcut changes.')
}
return []
}
const cachedShortcutsData = (): Shortcut[] | undefined => {
if (typeof localStorage !== 'undefined') {
const str = localStorage.getItem('/api/shortcuts')
if (str) {
return JSON.parse(str) as Shortcut[]
}
}
return undefined
}
const ShortcutsTree = (props: ShortcutsTreeProps): JSX.Element => {
const router = useRouter()
const { ref, width, height } = useResizeObserver()
const { data, isLoading } = useGetShortcuts()
const setShorcuts = useSetShortcuts()
const resetShortcuts = useResetShortcuts()
const [folderOpenState, setFolderOpenState] = usePersistedState<
Record<string, boolean>
>({
key: 'nav-menu-open-state',
isSessionStorage: false,
initialValue: {},
})
const tree = useMemo(() => {
const result = new SimpleTree<Shortcut>((data ?? []) as Shortcut[])
return result
}, [data])
const syncTreeData = async (data: Shortcut[]) => {
await setShorcuts.mutateAsync({ shortcuts: data })
}
const onMove = useCallback(
async (args: {
dragIds: string[]
parentId: null | string
index: number
}) => {
for (const id of args.dragIds) {
tree?.move({ id, parentId: args.parentId, index: args.index })
}
await syncTreeData(tree.data)
},
[tree, data]
)
const onCreate = useCallback(
async (args: { parentId: string | null; index: number; type: string }) => {
const data = { id: uuidv4(), name: '', type: 'folder' } as any
if (args.type === 'internal') {
data.children = []
}
tree.create({ parentId: args.parentId, index: args.index, data })
await syncTreeData(tree.data)
return data
},
[tree, data]
)
const onDelete = useCallback(
async (args: { ids: string[] }) => {
args.ids.forEach((id) => tree.drop({ id }))
await syncTreeData(tree.data)
},
[tree, data]
)
const onRename = useCallback(
async (args: { name: string; id: string }) => {
tree.update({ id: args.id, changes: { name: args.name } as any })
await syncTreeData(tree.data)
},
[tree, data]
)
const onToggle = useCallback(
(id: string) => {
if (id && props.treeRef.current) {
const isOpen = props.treeRef.current?.isOpen(id)
const newItem: OpenMap = {}
newItem[id] = isOpen
setFolderOpenState({ ...folderOpenState, ...newItem })
}
},
[props, folderOpenState, setFolderOpenState]
)
const onActivate = useCallback(
(node: NodeApi<Shortcut>) => {
if (node.data.type == 'folder') {
const join = node.data.join
if (join == 'or') {
const query = node.children
?.map((child) => {
return `(${child.data.filter})`
})
.join(' OR ')
}
} else if (node.data.section != null && node.data.filter != null) {
router.push(`/${node.data.section}?q=${node.data.filter}`)
}
},
[tree, router]
)
function countTotalShortcuts(shortcuts: Shortcut[]): number {
let total = 0
for (const shortcut of shortcuts) {
// Count the current shortcut
total++
// If the shortcut has children, recursively count them
if (shortcut.children && shortcut.children.length > 0) {
total += countTotalShortcuts(shortcut.children)
}
}
return total
}
const maximumHeight = useMemo(() => {
if (!data) {
return 320
}
return countTotalShortcuts(data as Shortcut[]) * 36
}, [data])
return (
<Box
ref={ref}
css={{
height: maximumHeight,
flexGrow: 1,
minBlockSize: 0,
}}
>
{!isLoading && (
<Tree
ref={props.treeRef}
data={data as Shortcut[]}
onCreate={onCreate}
onMove={onMove}
onDelete={onDelete}
onRename={onRename}
onToggle={onToggle}
onActivate={onActivate}
rowHeight={36}
initialOpenState={folderOpenState}
width={width}
height={maximumHeight}
>
{NodeRenderer}
</Tree>
)}
</Box>
)
}
function NodeRenderer(args: {
style: CSSProperties
node: NodeApi<Shortcut>
tree: TreeApi<Shortcut>
dragHandle?: (el: HTMLDivElement | null) => void
preview?: boolean
}) {
const isSelected = false
const [menuVisible, setMenuVisible] = useState(false)
const [menuOpened, setMenuOpened] = useState(false)
const router = useRouter()
return (
<HStack
ref={args.dragHandle}
alignment="center"
distribution="start"
css={{
pl: `${20 + args.node.level * 15}px`,
mb: '2px',
gap: '10px',
display: 'flex',
width: '100%',
maxWidth: '100%',
height: '34px',
backgroundColor: isSelected ? '$thLibrarySelectionColor' : 'unset',
fontSize: '15px',
fontWeight: 'regular',
fontFamily: '$display',
color: isSelected
? '$thLibraryMenuSecondary'
: '$thLibraryMenuUnselected',
verticalAlign: 'middle',
borderRadius: '3px',
cursor: 'pointer',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
'&:hover': {
backgroundColor: isSelected
? '$thLibrarySelectionColor'
: '$thBackground4',
},
'&:active': {
outline: 'unset',
backgroundColor: isSelected
? '$thLibrarySelectionColor'
: '$thBackground4',
},
'&:hover [role="hover-menu"]': {
opacity: '1',
},
}}
onMouseEnter={() => {
setMenuVisible(true)
}}
onMouseLeave={() => {
setMenuVisible(false)
}}
title={args.node.data.name}
onClick={(e) => {
// router.push(`/` + props.section)
}}
>
<HStack
css={{
width: '100%',
height: '100%',
}}
distribution="start"
alignment="center"
>
<NodeItemContents node={args.node} />
<SpanBox
role="hover-menu"
css={{
display: 'flex',
ml: 'auto',
mr: '15px',
opacity: menuVisible || menuOpened ? '1' : '0',
}}
>
<Dropdown
side="bottom"
triggerElement={<DotsThree size={20} />}
css={{ ml: 'auto' }}
onOpenChange={(open) => {
setMenuOpened(open)
}}
>
<DropdownOption
onSelect={() => {
args.tree.delete(args.node)
}}
title="Remove"
/>
{/* {args.node.data.type == 'folder' && (
<DropdownOption
onSelect={() => {
args.node.data.join = 'or'
}}
title="Folder query: OR"
/>
)} */}
</Dropdown>
</SpanBox>
</HStack>
</HStack>
)
}
type NodeItemContentsProps = {
node: NodeApi<Shortcut>
}
const NodeItemContents = (props: NodeItemContentsProps): JSX.Element => {
if (props.node.isEditing) {
return (
<input
autoFocus
type="text"
defaultValue={props.node.data.name}
onFocus={(e) => e.currentTarget.select()}
onBlur={() => props.node.reset()}
onKeyDown={(e) => {
if (e.key === 'Escape') {
props.node.reset()
}
if (e.key === 'Enter') {
// props.node.data = {
// id: 'new-folder',
// type: 'folder',
// name: e.currentTarget.value,
// }
props.node.submit(e.currentTarget.value)
props.node.activate()
}
}}
/>
)
}
if (props.node.isLeaf) {
const shortcut = props.node.data
if (shortcut) {
switch (shortcut.type) {
case 'feed':
case 'newsletter':
return (
<SpanBox>
<FeedOrNewsletterShortcut shortcut={shortcut} />
</SpanBox>
)
case 'label':
return (
<Box>
<LabelShortcut shortcut={shortcut} />
</Box>
)
case 'search':
return (
<Box>
<SearchShortcut shortcut={shortcut} />
</Box>
)
}
}
} else {
return (
<HStack
distribution="start"
alignment="center"
css={{ gap: '10px', width: '100%' }}
onClick={(event) => {
props.node.toggle()
event.preventDefault()
}}
>
{props.node.isClosed ? (
<ShortcutFolderClosed
color={theme.colors.thLibraryMenuPrimary.toString()}
/>
) : (
<ShortcutFolderOpen
color={theme.colors.thLibraryMenuPrimary.toString()}
/>
)}
{props.node.data.name}
</HStack>
)
}
return <></>
}
type ShortcutItemProps = {
shortcut: Shortcut
}
const FeedOrNewsletterShortcut = (props: ShortcutItemProps): JSX.Element => {
return (
<HStack
alignment="center"
distribution="start"
css={{ pl: '10px', width: '100%', gap: '10px' }}
key={`search-${props.shortcut.id}`}
>
<HStack
distribution="start"
alignment="center"
css={{ minWidth: '20px' }}
>
{props.shortcut.icon ? (
<CoverImage
src={props.shortcut.icon}
width={20}
height={20}
css={{ borderRadius: '20px' }}
/>
) : props.shortcut.type == 'newsletter' ? (
<NewsletterIcon color="#F59932" size={18} />
) : (
<FollowingIcon color="#F59932" size={21} />
)}
</HStack>
<StyledText style="settingsItem">{props.shortcut.name}</StyledText>
</HStack>
)
}
const SearchShortcut = (props: ShortcutItemProps): JSX.Element => {
return (
<HStack
alignment="center"
distribution="start"
css={{ pl: '10px', width: '100%', gap: '7px' }}
key={`search-${props.shortcut.id}`}
>
<HStack
distribution="start"
alignment="center"
css={{ minWidth: '20px' }}
>
<ListMagnifyingGlass size={17} />
</HStack>
<StyledText style="settingsItem">{props.shortcut.name}</StyledText>
</HStack>
)
}
const LabelShortcut = (props: ShortcutItemProps): JSX.Element => {
// <OutlinedLabelChip
// text={props.shortcut.name}
// color={props.shortcut.label?.color ?? 'gray'}
// />
return (
<HStack
alignment="center"
distribution="start"
css={{ width: '100%', gap: '7px' }}
key={`search-${props.shortcut.id}`}
>
<Tag
size={15}
color={props.shortcut.label?.color ?? 'gray'}
weight="fill"
/>
<StyledText style="settingsItem" css={{ pb: '1px' }}>
{props.shortcut.name}
</StyledText>
</HStack>
)
}
type NavButtonProps = {
text: string
icon: ReactNode

View File

@ -1,7 +1,25 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { requestHeaders } from '../networkHelpers'
import { fetchEndpoint } from '../../appConfig'
import { Shortcut } from '../../../components/templates/navMenu/NavigationMenu'
import { Label } from '../fragments/labelFragment'
export type ShortcutType = 'search' | 'label' | 'newsletter' | 'feed' | 'folder'
export type Shortcut = {
type: ShortcutType
id: string
name: string
section: string
filter: string
icon?: string
label?: Label
join?: string
children?: Shortcut[]
}
export function useGetShortcuts() {
return useQuery({

View File

@ -33,9 +33,10 @@ import { Button } from '../../components/elements/Button'
import { styled } from '@stitches/react'
import { SavedSearch } from '../../lib/networking/fragments/savedSearchFragment'
import { escapeQuotes } from '../../utils/helper'
import { Shortcut } from '../../components/templates/navMenu/NavigationMenu'
import { useGetLabels } from '../../lib/networking/labels/useLabels'
import { useGetSavedSearches } from '../../lib/networking/savedsearches/useSavedSearches'
import { Shortcut } from '../../lib/networking/shortcuts/useShortcuts'
type ListAction = 'RESET' | 'ADD_ITEM' | 'REMOVE_ITEM'
const SHORTCUTS_KEY = 'library-shortcuts'
@ -181,16 +182,7 @@ export default function Shortcuts(): JSX.Element {
)
event.preventDefault()
}}
>
{navMenuStyle === 'shortcuts' ? (
<CheckSquare size={20} weight="duotone" />
) : (
<Square size={20} weight="duotone" />
)}
<StyledText style="modalTitle" css={{}}>
Enable shortcuts
</StyledText>
</HStack>
></HStack>
<Box
css={{
py: '$3',