mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Refactor the SetLabels components to allow them to take a highlight or article
This commit is contained in:
parent
300969cc47
commit
16654bafef
7 changed files with 442 additions and 306 deletions
|
|
@ -29,7 +29,7 @@ const Modal = styled(Content, {
|
|||
boxShadow: theme.shadows.cardBoxShadow.toString(),
|
||||
position: 'fixed',
|
||||
'&:focus': { outline: 'none' },
|
||||
zIndex:'1',
|
||||
zIndex: '1',
|
||||
})
|
||||
|
||||
export const ModalContent = styled(Modal, {
|
||||
|
|
@ -43,6 +43,7 @@ export const ModalContent = styled(Modal, {
|
|||
maxWidth: '95%',
|
||||
width: '95%',
|
||||
},
|
||||
zIndex: '10',
|
||||
})
|
||||
|
||||
export type ModalTitleBarProps = {
|
||||
|
|
@ -57,9 +58,7 @@ export const ModalTitleBar = (props: ModalTitleBarProps) => {
|
|||
alignment="center"
|
||||
css={{ height: '68px', width: '100%' }}
|
||||
>
|
||||
<StyledText style="modalHeadline" css={{ }}>
|
||||
{props.title}
|
||||
</StyledText>
|
||||
<StyledText style="modalHeadline">{props.title}</StyledText>
|
||||
<Button
|
||||
css={{ ml: 'auto' }}
|
||||
style="ghost"
|
||||
|
|
@ -67,10 +66,7 @@ export const ModalTitleBar = (props: ModalTitleBarProps) => {
|
|||
props.onOpenChange(false)
|
||||
}}
|
||||
>
|
||||
<X
|
||||
size={24}
|
||||
color={theme.colors.textNonessential.toString()}
|
||||
/>
|
||||
<X size={24} color={theme.colors.textNonessential.toString()} />
|
||||
</Button>
|
||||
</HStack>
|
||||
)
|
||||
|
|
@ -98,7 +94,10 @@ export const ModalButtonBar = (props: ModalButtonBarProps) => {
|
|||
},
|
||||
}}
|
||||
>
|
||||
<Button style={'ctaOutlineYellow'} type="button" onClick={(event) => {
|
||||
<Button
|
||||
style={'ctaOutlineYellow'}
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
props.onOpenChange(false)
|
||||
}}
|
||||
|
|
@ -110,4 +109,4 @@ export const ModalButtonBar = (props: ModalButtonBarProps) => {
|
|||
</Button>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,25 @@
|
|||
import { Separator } from "@radix-ui/react-separator"
|
||||
import { ArchiveBox, DotsThree, HighlighterCircle, TagSimple, TextAa, Trash, Tray } from "phosphor-react"
|
||||
import { ArticleAttributes } from "../../../lib/networking/queries/useGetArticleQuery"
|
||||
import { Button } from "../../elements/Button"
|
||||
import { Dropdown } from "../../elements/DropdownElements"
|
||||
import { Box, SpanBox } from "../../elements/LayoutPrimitives"
|
||||
import { TooltipWrapped } from "../../elements/Tooltip"
|
||||
import { styled, theme } from "../../tokens/stitches.config"
|
||||
import { SetLabelsControl } from "./SetLabelsControl"
|
||||
import { DisplaySettingsModal } from "./DisplaySettingsModal"
|
||||
import { useReaderSettings } from "../../../lib/hooks/useReaderSettings"
|
||||
import { useRef } from "react"
|
||||
import { Separator } from '@radix-ui/react-separator'
|
||||
import {
|
||||
ArchiveBox,
|
||||
DotsThree,
|
||||
HighlighterCircle,
|
||||
TagSimple,
|
||||
TextAa,
|
||||
Trash,
|
||||
Tray,
|
||||
} from 'phosphor-react'
|
||||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { Dropdown } from '../../elements/DropdownElements'
|
||||
import { Box, SpanBox } from '../../elements/LayoutPrimitives'
|
||||
import { TooltipWrapped } from '../../elements/Tooltip'
|
||||
import { styled, theme } from '../../tokens/stitches.config'
|
||||
import { SetLabelsControl } from './SetLabelsControl'
|
||||
import { DisplaySettingsModal } from './DisplaySettingsModal'
|
||||
import { useReaderSettings } from '../../../lib/hooks/useReaderSettings'
|
||||
import { useRef } from 'react'
|
||||
import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
|
||||
export type ArticleActionsMenuLayout = 'top' | 'side'
|
||||
|
||||
|
|
@ -31,7 +41,7 @@ const MenuSeparator = (props: MenuSeparatorProps): JSX.Element => {
|
|||
borderBottom: `1px solid ${theme.colors.grayLine.toString()}`,
|
||||
my: '8px',
|
||||
})
|
||||
return (props.layout == 'side' ? <LineSeparator /> : <></>)
|
||||
return props.layout == 'side' ? <LineSeparator /> : <></>
|
||||
}
|
||||
|
||||
type ActionDropdownProps = {
|
||||
|
|
@ -41,126 +51,166 @@ type ActionDropdownProps = {
|
|||
}
|
||||
|
||||
const ActionDropdown = (props: ActionDropdownProps): JSX.Element => {
|
||||
return <Dropdown
|
||||
css={{ m: '0px', p: '0px', overflow: 'hidden', width: '265px', maxWidth: '265px', '@smDown': { width: '230px' } }}
|
||||
side={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
sideOffset={props.layout == 'side' ? 8 : 0}
|
||||
align={props.layout == 'side' ? 'start' : 'center'}
|
||||
alignOffset={props.layout == 'side' ? -18 : undefined}
|
||||
triggerElement={props.triggerElement}
|
||||
>
|
||||
{props.children}
|
||||
</Dropdown>
|
||||
return (
|
||||
<Dropdown
|
||||
css={{
|
||||
m: '0px',
|
||||
p: '0px',
|
||||
overflow: 'hidden',
|
||||
width: '265px',
|
||||
maxWidth: '265px',
|
||||
'@smDown': { width: '230px' },
|
||||
}}
|
||||
side={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
sideOffset={props.layout == 'side' ? 8 : 0}
|
||||
align={props.layout == 'side' ? 'start' : 'center'}
|
||||
alignOffset={props.layout == 'side' ? -18 : undefined}
|
||||
triggerElement={props.triggerElement}
|
||||
>
|
||||
{props.children}
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element {
|
||||
export function ArticleActionsMenu(
|
||||
props: ArticleActionsMenuProps
|
||||
): JSX.Element {
|
||||
const readerSettings = useReaderSettings()
|
||||
const displaySettingsButtonRef = useRef<HTMLElement | null>(null)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: props.layout == 'side' ? 'column' : 'row',
|
||||
justifyContent: props.layout == 'side' ? 'center' : 'flex-end',
|
||||
gap: props.layout == 'side' ? '8px' : '24px',
|
||||
paddingTop: '6px',
|
||||
}}
|
||||
>
|
||||
{props.showReaderDisplaySettings && (
|
||||
<>
|
||||
<Button style='articleActionIcon' onClick={() => readerSettings.setShowEditDisplaySettingsModal(true)}>
|
||||
<TooltipWrapped
|
||||
tooltipContent="Adjust Display Settings"
|
||||
tooltipSide={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
>
|
||||
<SpanBox ref={displaySettingsButtonRef}>
|
||||
<TextAa size={24} color={theme.colors.readerFont.toString()} />
|
||||
</SpanBox>
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
<MenuSeparator layout={props.layout} />
|
||||
</>
|
||||
)}
|
||||
<SpanBox css={{
|
||||
'display': 'flex',
|
||||
'@smDown': {
|
||||
display: 'none',
|
||||
}}}
|
||||
<Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: props.layout == 'side' ? 'column' : 'row',
|
||||
justifyContent: props.layout == 'side' ? 'center' : 'flex-end',
|
||||
gap: props.layout == 'side' ? '8px' : '24px',
|
||||
paddingTop: '6px',
|
||||
}}
|
||||
>
|
||||
{props.article ? (
|
||||
<ActionDropdown
|
||||
layout={props.layout}
|
||||
triggerElement={
|
||||
{props.showReaderDisplaySettings && (
|
||||
<>
|
||||
<Button
|
||||
style="articleActionIcon"
|
||||
onClick={() =>
|
||||
readerSettings.setShowEditDisplaySettingsModal(true)
|
||||
}
|
||||
>
|
||||
<TooltipWrapped
|
||||
tooltipContent="Edit labels"
|
||||
tooltipContent="Adjust Display Settings"
|
||||
tooltipSide={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
>
|
||||
<SpanBox ref={displaySettingsButtonRef}>
|
||||
<TextAa
|
||||
size={24}
|
||||
color={theme.colors.readerFont.toString()}
|
||||
/>
|
||||
</SpanBox>
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
<MenuSeparator layout={props.layout} />
|
||||
</>
|
||||
)}
|
||||
<SpanBox
|
||||
css={{
|
||||
display: 'flex',
|
||||
'@smDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.article ? (
|
||||
<ActionDropdown
|
||||
layout={props.layout}
|
||||
triggerElement={
|
||||
<TooltipWrapped
|
||||
tooltipContent="Edit labels"
|
||||
tooltipSide={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
>
|
||||
<TagSimple
|
||||
size={24}
|
||||
color={theme.colors.readerFont.toString()}
|
||||
/>
|
||||
</TooltipWrapped>
|
||||
}
|
||||
>
|
||||
<SetLabelsControl
|
||||
provider={props.article}
|
||||
save={(labels: Label[]) => {
|
||||
if (props.article?.id) {
|
||||
return setLabelsMutation(
|
||||
props.article?.id,
|
||||
labels.map((label) => label.id)
|
||||
)
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}}
|
||||
onSave={(labels) => {
|
||||
props.articleActionHandler('refreshLabels', labels)
|
||||
}}
|
||||
/>
|
||||
</ActionDropdown>
|
||||
) : (
|
||||
<Button
|
||||
style="articleActionIcon"
|
||||
css={{
|
||||
'@smDown': {
|
||||
display: 'flex',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TagSimple size={24} color={theme.colors.readerFont.toString()} />
|
||||
</TooltipWrapped>
|
||||
}
|
||||
>
|
||||
<SetLabelsControl
|
||||
article={props.article}
|
||||
linkId={props.article.linkId}
|
||||
labels={props.article.labels}
|
||||
articleActionHandler={props.articleActionHandler}
|
||||
/>
|
||||
</ActionDropdown>
|
||||
) : (
|
||||
<Button style='articleActionIcon'
|
||||
</Button>
|
||||
)}
|
||||
</SpanBox>
|
||||
|
||||
<Button
|
||||
style="articleActionIcon"
|
||||
onClick={() => props.articleActionHandler('setLabels')}
|
||||
css={{
|
||||
display: 'none',
|
||||
'@smDown': {
|
||||
display: 'flex',
|
||||
},
|
||||
}}>
|
||||
<TagSimple size={24} color={theme.colors.readerFont.toString()} />
|
||||
</Button>
|
||||
)}
|
||||
</SpanBox>
|
||||
|
||||
<Button style='articleActionIcon'
|
||||
onClick={() => props.articleActionHandler('setLabels')}
|
||||
css={{
|
||||
'display': 'none',
|
||||
'@smDown': {
|
||||
display: 'flex',
|
||||
},
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<TagSimple size={24} color={theme.colors.readerFont.toString()} />
|
||||
</Button>
|
||||
</Button>
|
||||
|
||||
<Button style='articleActionIcon' onClick={() => props.articleActionHandler('showHighlights')}>
|
||||
<TooltipWrapped
|
||||
tooltipContent="View Highlights"
|
||||
tooltipSide={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
<Button
|
||||
style="articleActionIcon"
|
||||
onClick={() => props.articleActionHandler('showHighlights')}
|
||||
>
|
||||
<HighlighterCircle size={24} color={theme.colors.readerFont.toString()} />
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
<TooltipWrapped
|
||||
tooltipContent="View Highlights"
|
||||
tooltipSide={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
>
|
||||
<HighlighterCircle
|
||||
size={24}
|
||||
color={theme.colors.readerFont.toString()}
|
||||
/>
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
|
||||
<MenuSeparator layout={props.layout} />
|
||||
<MenuSeparator layout={props.layout} />
|
||||
|
||||
<Button
|
||||
style="articleActionIcon"
|
||||
onClick={() => {
|
||||
props.articleActionHandler('delete')
|
||||
}}
|
||||
>
|
||||
<TooltipWrapped
|
||||
tooltipContent="Delete"
|
||||
tooltipSide={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
<Button
|
||||
style="articleActionIcon"
|
||||
onClick={() => {
|
||||
props.articleActionHandler('delete')
|
||||
}}
|
||||
>
|
||||
<Trash
|
||||
size={24}
|
||||
color={theme.colors.readerFont.toString()}
|
||||
/>
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
<TooltipWrapped
|
||||
tooltipContent="Delete"
|
||||
tooltipSide={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
>
|
||||
<Trash size={24} color={theme.colors.readerFont.toString()} />
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
|
||||
{!props.article?.isArchived ? (
|
||||
{!props.article?.isArchived ? (
|
||||
<Button
|
||||
style="articleActionIcon"
|
||||
onClick={() => props.articleActionHandler('archive')}
|
||||
|
|
@ -184,27 +234,26 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element
|
|||
tooltipContent="Unarchive"
|
||||
tooltipSide={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
>
|
||||
<Tray
|
||||
size={24}
|
||||
color={theme.colors.readerFont.toString()}
|
||||
/>
|
||||
<Tray size={24} color={theme.colors.readerFont.toString()} />
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* <MenuSeparator layout={props.layout} />
|
||||
{/* <MenuSeparator layout={props.layout} />
|
||||
<Button style='articleActionIcon'>
|
||||
<DotsThree size={24} color={theme.colors.readerFont.toString()} />
|
||||
</Button> */}
|
||||
</Box>
|
||||
{readerSettings.showEditDisplaySettingsModal && (
|
||||
<DisplaySettingsModal
|
||||
centerX={props.layout != 'side'}
|
||||
triggerElementRef={displaySettingsButtonRef}
|
||||
articleActionHandler={props.articleActionHandler}
|
||||
onOpenChange={() => readerSettings.setShowEditDisplaySettingsModal(false)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{readerSettings.showEditDisplaySettingsModal && (
|
||||
<DisplaySettingsModal
|
||||
centerX={props.layout != 'side'}
|
||||
triggerElementRef={displaySettingsButtonRef}
|
||||
articleActionHandler={props.articleActionHandler}
|
||||
onOpenChange={() =>
|
||||
readerSettings.setShowEditDisplaySettingsModal(false)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export function HighlightsModal(props: HighlightsModalProps): JSX.Element {
|
|||
event.preventDefault()
|
||||
props.onOpenChange(false)
|
||||
}}
|
||||
css={{ overflow: 'auto', px: '24px', zIndex: '10' }}
|
||||
css={{ overflow: 'auto', px: '24px' }}
|
||||
>
|
||||
<VStack distribution="start" css={{ height: '100%' }}>
|
||||
<ModalTitleBar title="Notebook" onOpenChange={props.onOpenChange} />
|
||||
|
|
|
|||
|
|
@ -9,18 +9,19 @@ import { Label } from '../../../lib/networking/fragments/labelFragment'
|
|||
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { Check, Circle, PencilSimple, Plus } from 'phosphor-react'
|
||||
import { isTouchScreenDevice } from '../../../lib/deviceType'
|
||||
import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation'
|
||||
import { createLabelMutation } from '../../../lib/networking/mutations/createLabelMutation'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
|
||||
export interface LabelsProvider {
|
||||
labels?: Label[]
|
||||
}
|
||||
|
||||
type SetLabelsControlProps = {
|
||||
linkId: string
|
||||
labels: Label[] | undefined
|
||||
article?: ArticleAttributes
|
||||
articleActionHandler: (action: string, arg?: unknown) => void
|
||||
provider?: LabelsProvider
|
||||
onSave: (labels: Label[] | undefined) => void
|
||||
save: (labels: Label[]) => Promise<Label[] | undefined>
|
||||
}
|
||||
|
||||
type HeaderProps = {
|
||||
|
|
@ -57,12 +58,16 @@ function Header(props: HeaderProps): JSX.Element {
|
|||
}, [props.focused])
|
||||
|
||||
return (
|
||||
<VStack css={{ width: '100%', my: '0px', borderBottom: '1px solid $grayBorder'}}>
|
||||
<Box css={{
|
||||
width: '100%',
|
||||
my: '14px',
|
||||
px: '14px',
|
||||
}}>
|
||||
<VStack
|
||||
css={{ width: '100%', my: '0px', borderBottom: '1px solid $grayBorder' }}
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
width: '100%',
|
||||
my: '14px',
|
||||
px: '14px',
|
||||
}}
|
||||
>
|
||||
<FormInput
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
|
|
@ -91,8 +96,9 @@ function Header(props: HeaderProps): JSX.Element {
|
|||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</VStack>)
|
||||
</Box>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
type LabelListItemProps = {
|
||||
|
|
@ -128,21 +134,61 @@ function LabelListItem(props: LabelListItemProps): JSX.Element {
|
|||
ref.current?.blur()
|
||||
}}
|
||||
>
|
||||
<input autoFocus={focused} hidden={true} type="checkbox" checked={selected} readOnly />
|
||||
<Box css={{ pl: '10px', width: '32px', display: 'flex', alignItems: 'center' }}>
|
||||
{selected && <Check size={15} color={theme.colors.grayText.toString()} weight='bold' />}
|
||||
<input
|
||||
autoFocus={focused}
|
||||
hidden={true}
|
||||
type="checkbox"
|
||||
checked={selected}
|
||||
readOnly
|
||||
/>
|
||||
<Box
|
||||
css={{
|
||||
pl: '10px',
|
||||
width: '32px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{selected && (
|
||||
<Check
|
||||
size={15}
|
||||
color={theme.colors.grayText.toString()}
|
||||
weight="bold"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box css={{ width: '30px', height: '100%', display: 'flex', alignItems: 'center' }}>
|
||||
<Circle width={22} height={22} color={label.color} weight='fill' />
|
||||
<Box
|
||||
css={{
|
||||
width: '30px',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Circle width={22} height={22} color={label.color} weight="fill" />
|
||||
</Box>
|
||||
<Box css={{ overflow: 'clip', height: '100%', display: 'flex', alignItems: 'center' }}>
|
||||
<Box
|
||||
css={{
|
||||
overflow: 'clip',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<StyledText style="caption">{label.name}</StyledText>
|
||||
</Box>
|
||||
<Box css={{ pl: '10px', width: '40px', marginLeft: 'auto', display: 'flex', alignItems: 'center' }}>
|
||||
{selected && <CrossIcon
|
||||
size={14}
|
||||
strokeColor={theme.colors.grayText.toString()}
|
||||
/>}
|
||||
<Box
|
||||
css={{
|
||||
pl: '10px',
|
||||
width: '40px',
|
||||
marginLeft: 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{selected && (
|
||||
<CrossIcon size={14} strokeColor={theme.colors.grayText.toString()} />
|
||||
)}
|
||||
</Box>
|
||||
</StyledLabel>
|
||||
)
|
||||
|
|
@ -164,9 +210,11 @@ function Footer(props: FooterProps): JSX.Element {
|
|||
return (
|
||||
<HStack
|
||||
ref={ref}
|
||||
distribution="start" alignment="center"
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{
|
||||
width: '100%', height: '42px',
|
||||
width: '100%',
|
||||
height: '42px',
|
||||
bg: props.focused ? '$grayBgActive' : 'unset',
|
||||
color: theme.colors.grayText.toString(),
|
||||
'a:link': {
|
||||
|
|
@ -176,12 +224,14 @@ function Footer(props: FooterProps): JSX.Element {
|
|||
color: theme.colors.grayText.toString(),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SpanBox css={{ display: 'flex', fontSize: '12px', padding: '33px', gap: '8px' }}>
|
||||
<PencilSimple size={18} color={theme.colors.grayText.toString()} />
|
||||
<Link href="/settings/labels">Edit labels</Link>
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
>
|
||||
<SpanBox
|
||||
css={{ display: 'flex', fontSize: '12px', padding: '33px', gap: '8px' }}
|
||||
>
|
||||
<PencilSimple size={18} color={theme.colors.grayText.toString()} />
|
||||
<Link href="/settings/labels">Edit labels</Link>
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -189,41 +239,47 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
const router = useRouter()
|
||||
const [filterText, setFilterText] = useState('')
|
||||
const { labels, revalidate } = useGetLabelsQuery()
|
||||
const [selectedLabels, setSelectedLabels] = useState<Label[]>(props.labels || [])
|
||||
const [selectedLabels, setSelectedLabels] = useState<Label[]>(
|
||||
props.provider?.labels || []
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setFocusedIndex(undefined)
|
||||
}, [filterText])
|
||||
|
||||
const isSelected = useCallback((label: Label): boolean => {
|
||||
return selectedLabels.some((other) => {
|
||||
return other.id === label.id
|
||||
})
|
||||
}, [selectedLabels])
|
||||
|
||||
const toggleLabel = useCallback(async (label: Label) => {
|
||||
let newSelectedLabels = [...selectedLabels]
|
||||
if (isSelected(label)) {
|
||||
newSelectedLabels = selectedLabels.filter((other) => {
|
||||
return other.id !== label.id
|
||||
const isSelected = useCallback(
|
||||
(label: Label): boolean => {
|
||||
return selectedLabels.some((other) => {
|
||||
return other.id === label.id
|
||||
})
|
||||
} else {
|
||||
newSelectedLabels = [...selectedLabels, label]
|
||||
}
|
||||
setSelectedLabels(newSelectedLabels)
|
||||
},
|
||||
[selectedLabels]
|
||||
)
|
||||
|
||||
const result = await setLabelsMutation(
|
||||
props.linkId,
|
||||
newSelectedLabels.map((label) => label.id)
|
||||
)
|
||||
const toggleLabel = useCallback(
|
||||
async (label: Label) => {
|
||||
let newSelectedLabels = [...selectedLabels]
|
||||
if (isSelected(label)) {
|
||||
newSelectedLabels = selectedLabels.filter((other) => {
|
||||
return other.id !== label.id
|
||||
})
|
||||
} else {
|
||||
newSelectedLabels = [...selectedLabels, label]
|
||||
}
|
||||
setSelectedLabels(newSelectedLabels)
|
||||
|
||||
if (props.article) {
|
||||
props.article.labels = result
|
||||
}
|
||||
props.articleActionHandler('refreshLabels', result)
|
||||
const result = await props.save(newSelectedLabels)
|
||||
if (props.provider) {
|
||||
props.provider.labels = result
|
||||
}
|
||||
if (props.onSave) {
|
||||
props.onSave(result)
|
||||
}
|
||||
|
||||
revalidate()
|
||||
}, [isSelected, selectedLabels, setSelectedLabels])
|
||||
revalidate()
|
||||
},
|
||||
[isSelected, selectedLabels, setSelectedLabels]
|
||||
)
|
||||
|
||||
const filteredLabels = useMemo(() => {
|
||||
if (!labels) {
|
||||
|
|
@ -235,62 +291,80 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
}, [labels, filterText])
|
||||
|
||||
// Move focus through the labels list on tab or arrow up/down keys
|
||||
const [focusedIndex, setFocusedIndex] = useState<number | undefined>(undefined)
|
||||
const handleKeyDown = useCallback(async (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const maxIndex = filteredLabels.length + 1
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
let newIndex = focusedIndex
|
||||
if (focusedIndex) {
|
||||
newIndex = Math.max(0, focusedIndex - 1)
|
||||
} else {
|
||||
newIndex = undefined
|
||||
const [focusedIndex, setFocusedIndex] = useState<number | undefined>(
|
||||
undefined
|
||||
)
|
||||
const handleKeyDown = useCallback(
|
||||
async (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const maxIndex = filteredLabels.length + 1
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
let newIndex = focusedIndex
|
||||
if (focusedIndex) {
|
||||
newIndex = Math.max(0, focusedIndex - 1)
|
||||
} else {
|
||||
newIndex = undefined
|
||||
}
|
||||
// If the `Create New label` button isn't visible we skip it
|
||||
// when navigating with the arrow keys
|
||||
if (focusedIndex === maxIndex && !filterText) {
|
||||
newIndex = maxIndex - 2
|
||||
}
|
||||
setFocusedIndex(newIndex)
|
||||
}
|
||||
// If the `Create New label` button isn't visible we skip it
|
||||
// when navigating with the arrow keys
|
||||
if (focusedIndex === maxIndex && !filterText) {
|
||||
newIndex = maxIndex - 2
|
||||
if (event.key === 'ArrowDown' || event.key === 'Tab') {
|
||||
event.preventDefault()
|
||||
let newIndex = focusedIndex
|
||||
if (focusedIndex === undefined) {
|
||||
newIndex = 0
|
||||
} else {
|
||||
newIndex = Math.min(maxIndex, focusedIndex + 1)
|
||||
}
|
||||
// If the `Create New label` button isn't visible we skip it
|
||||
// when navigating with the arrow keys
|
||||
if (focusedIndex === maxIndex - 2 && !filterText) {
|
||||
newIndex = maxIndex
|
||||
}
|
||||
setFocusedIndex(newIndex)
|
||||
}
|
||||
setFocusedIndex(newIndex)
|
||||
}
|
||||
if (event.key === 'ArrowDown' || event.key === 'Tab') {
|
||||
event.preventDefault()
|
||||
let newIndex = focusedIndex
|
||||
if (focusedIndex === undefined) {
|
||||
newIndex = 0
|
||||
} else {
|
||||
newIndex = Math.min(maxIndex, focusedIndex + 1)
|
||||
}
|
||||
// If the `Create New label` button isn't visible we skip it
|
||||
// when navigating with the arrow keys
|
||||
if (focusedIndex === maxIndex - 2 && !filterText) {
|
||||
newIndex = maxIndex
|
||||
}
|
||||
setFocusedIndex(newIndex)
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
if (focusedIndex === maxIndex) {
|
||||
router.push('/settings/labels')
|
||||
return
|
||||
}
|
||||
if (focusedIndex === maxIndex - 1) {
|
||||
await createLabelFromFilterText()
|
||||
return
|
||||
}
|
||||
if (focusedIndex !== undefined) {
|
||||
const label = filteredLabels[focusedIndex]
|
||||
if (label) {
|
||||
toggleLabel(label)
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
if (focusedIndex === maxIndex) {
|
||||
router.push('/settings/labels')
|
||||
return
|
||||
}
|
||||
if (focusedIndex === maxIndex - 1) {
|
||||
await createLabelFromFilterText()
|
||||
return
|
||||
}
|
||||
if (focusedIndex !== undefined) {
|
||||
const label = filteredLabels[focusedIndex]
|
||||
if (label) {
|
||||
toggleLabel(label)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [filterText, filteredLabels, focusedIndex, isSelected, selectedLabels, setSelectedLabels])
|
||||
},
|
||||
[
|
||||
filterText,
|
||||
filteredLabels,
|
||||
focusedIndex,
|
||||
isSelected,
|
||||
selectedLabels,
|
||||
setSelectedLabels,
|
||||
]
|
||||
)
|
||||
|
||||
const createLabelFromFilterText = useCallback(async () => {
|
||||
const label = await createLabelMutation(filterText, randomLabelColorHex(), '')
|
||||
const label = await createLabelMutation(
|
||||
filterText,
|
||||
randomLabelColorHex(),
|
||||
''
|
||||
)
|
||||
if (label) {
|
||||
showSuccessToast(`Created label ${label.name}`, { position: 'bottom-right' })
|
||||
showSuccessToast(`Created label ${label.name}`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
toggleLabel(label)
|
||||
} else {
|
||||
showErrorToast('Failed to create label', { position: 'bottom-right' })
|
||||
|
|
@ -301,20 +375,28 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
<VStack
|
||||
distribution="start"
|
||||
onKeyDown={handleKeyDown}
|
||||
css={{
|
||||
css={{
|
||||
p: '0',
|
||||
width: '100%',
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<Header
|
||||
focused={focusedIndex === undefined}
|
||||
resetFocusedIndex={() => setFocusedIndex(undefined)}
|
||||
setFilterText={setFilterText} filterText={filterText}
|
||||
setFilterText={setFilterText}
|
||||
filterText={filterText}
|
||||
/>
|
||||
<VStack
|
||||
distribution="start"
|
||||
alignment="start"
|
||||
css={{ flexGrow: '1', overflow: 'scroll', width: '100%', height: '100%', maxHeight: '294px'
|
||||
}}>
|
||||
css={{
|
||||
flexGrow: '1',
|
||||
overflow: 'scroll',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
maxHeight: '294px',
|
||||
}}
|
||||
>
|
||||
{filteredLabels &&
|
||||
filteredLabels.map((label, idx) => (
|
||||
<LabelListItem
|
||||
|
|
@ -327,23 +409,30 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
))}
|
||||
</VStack>
|
||||
{filterText && (
|
||||
<Button style='modalOption' css={{
|
||||
pl: '26px',
|
||||
color: theme.colors.grayText.toString(),
|
||||
height: '42px',
|
||||
borderBottom: '1px solid $grayBorder',
|
||||
bg: focusedIndex === filteredLabels.length ? '$grayBgActive' : 'unset',
|
||||
}}
|
||||
<Button
|
||||
style="modalOption"
|
||||
css={{
|
||||
pl: '26px',
|
||||
color: theme.colors.grayText.toString(),
|
||||
height: '42px',
|
||||
borderBottom: '1px solid $grayBorder',
|
||||
bg:
|
||||
focusedIndex === filteredLabels.length
|
||||
? '$grayBgActive'
|
||||
: 'unset',
|
||||
}}
|
||||
onClick={createLabelFromFilterText}
|
||||
>
|
||||
<HStack alignment='center' distribution='start' css={{ gap: '8px' }}>
|
||||
<HStack alignment="center" distribution="start" css={{ gap: '8px' }}>
|
||||
<Plus size={18} color={theme.colors.grayText.toString()} />
|
||||
<SpanBox css={{ fontSize: '12px' }}>{`Create new label "${filterText}"`}</SpanBox>
|
||||
<SpanBox
|
||||
css={{ fontSize: '12px' }}
|
||||
>{`Create new label "${filterText}"`}</SpanBox>
|
||||
</HStack>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Footer focused={focusedIndex === (filteredLabels.length + 1)} />
|
||||
<Footer focused={focusedIndex === filteredLabels.length + 1} />
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,22 +2,22 @@ import { Label } from '../../../lib/networking/fragments/labelFragment'
|
|||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { CrossIcon } from '../../elements/images/CrossIcon'
|
||||
import { HStack, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import {
|
||||
ModalRoot,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalTitleBar,
|
||||
} from '../../elements/ModalPrimitives'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { SetLabelsControl } from './SetLabelsControl'
|
||||
import { LabelsProvider, SetLabelsControl } from './SetLabelsControl'
|
||||
|
||||
type SetLabelsModalProps = {
|
||||
linkId: string
|
||||
labels: Label[] | undefined
|
||||
article?: ArticleAttributes
|
||||
provider?: LabelsProvider
|
||||
onOpenChange: (open: boolean) => void
|
||||
articleActionHandler: (action: string, arg?: unknown) => void
|
||||
onSave: (labels: Label[] | undefined) => void
|
||||
save: (labels: Label[]) => Promise<Label[] | undefined>
|
||||
}
|
||||
|
||||
export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
|
||||
|
|
@ -31,27 +31,10 @@ export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
|
|||
props.onOpenChange(false)
|
||||
}}
|
||||
>
|
||||
<VStack css={{ width: '100%' }}>
|
||||
<HStack
|
||||
distribution="between"
|
||||
alignment="center"
|
||||
css={{ width: '100%' }}
|
||||
>
|
||||
<StyledText style="modalHeadline" css={{ pl: '16px' }}>Labels</StyledText>
|
||||
<Button
|
||||
css={{ pt: '16px', pr: '16px' }}
|
||||
style="ghost"
|
||||
onClick={() => {
|
||||
props.onOpenChange(false)
|
||||
}}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<CrossIcon
|
||||
size={14}
|
||||
strokeColor={theme.colors.grayText.toString()}
|
||||
/>
|
||||
</Button>
|
||||
</HStack>
|
||||
<VStack distribution="start" css={{ height: '100%' }}>
|
||||
<SpanBox css={{ p: '16px', width: '100%' }}>
|
||||
<ModalTitleBar title="Labels" onOpenChange={props.onOpenChange} />
|
||||
</SpanBox>
|
||||
<SetLabelsControl {...props} />
|
||||
</VStack>
|
||||
</ModalContent>
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ import {
|
|||
} from '../../../lib/networking/queries/typeaheadSearch'
|
||||
import axios from 'axios'
|
||||
import { uploadFileRequestMutation } from '../../../lib/networking/mutations/uploadFileMutation'
|
||||
import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation'
|
||||
|
||||
export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT'
|
||||
|
||||
|
|
@ -100,14 +101,17 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
|
||||
const gridContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [shareTarget, setShareTarget] =
|
||||
useState<LibraryItem | undefined>(undefined)
|
||||
const [shareTarget, setShareTarget] = useState<LibraryItem | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const [snoozeTarget, setSnoozeTarget] =
|
||||
useState<LibraryItem | undefined>(undefined)
|
||||
const [snoozeTarget, setSnoozeTarget] = useState<LibraryItem | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const [labelsTarget, setLabelsTarget] =
|
||||
useState<LibraryItem | undefined>(undefined)
|
||||
const [labelsTarget, setLabelsTarget] = useState<LibraryItem | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const [showAddLinkModal, setShowAddLinkModal] = useState(false)
|
||||
const [showEditTitleModal, setShowEditTitleModal] = useState(false)
|
||||
|
|
@ -1200,18 +1204,22 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
|||
)}
|
||||
{props.labelsTarget?.node.id && (
|
||||
<SetLabelsModal
|
||||
linkId={props.labelsTarget.node.id}
|
||||
labels={props.labelsTarget.node.labels}
|
||||
articleActionHandler={(action, value) => {
|
||||
switch (action) {
|
||||
case 'refreshLabels':
|
||||
if (props.labelsTarget) {
|
||||
props.labelsTarget.node.labels = value as Label[] | undefined
|
||||
updateState({})
|
||||
}
|
||||
break
|
||||
provider={props.labelsTarget.node}
|
||||
onSave={(labels: Label[] | undefined) => {
|
||||
if (props.labelsTarget) {
|
||||
props.labelsTarget.node.labels = labels
|
||||
updateState({})
|
||||
}
|
||||
}}
|
||||
save={(labels: Label[]) => {
|
||||
if (props.labelsTarget?.node.id) {
|
||||
return setLabelsMutation(
|
||||
props.labelsTarget.node.id,
|
||||
labels.map((label) => label.id)
|
||||
)
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}}
|
||||
onOpenChange={() => {
|
||||
if (props.labelsTarget) {
|
||||
const activate = props.labelsTarget
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import { SkeletonArticleContainer } from '../../../components/templates/article/
|
|||
import { useRegisterActions } from 'kbar'
|
||||
import { deleteLinkMutation } from '../../../lib/networking/mutations/deleteLinkMutation'
|
||||
import { ConfirmationModal } from '../../../components/patterns/ConfirmationModal'
|
||||
import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation'
|
||||
|
||||
const PdfArticleContainerNoSSR = dynamic<PdfArticleContainerProps>(
|
||||
() => import('./../../../components/templates/article/PdfArticleContainer'),
|
||||
|
|
@ -365,10 +366,17 @@ export default function Home(): JSX.Element {
|
|||
|
||||
{article && readerSettings.showSetLabelsModal && (
|
||||
<SetLabelsModal
|
||||
article={article}
|
||||
linkId={article.id}
|
||||
labels={article.labels}
|
||||
articleActionHandler={actionHandler}
|
||||
provider={article}
|
||||
// labels={article.labels}
|
||||
onSave={(labels: Label[] | undefined) => {
|
||||
actionHandler('refreshLabels', labels)
|
||||
}}
|
||||
save={(labels: Label[]) => {
|
||||
return setLabelsMutation(
|
||||
article.linkId,
|
||||
labels.map((label) => label.id)
|
||||
)
|
||||
}}
|
||||
onOpenChange={() => readerSettings.setShowSetLabelsModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in a new issue