Refactor the SetLabels components to allow them to take a highlight or article

This commit is contained in:
Jackson Harper 2022-12-02 15:30:27 +08:00
parent 300969cc47
commit 16654bafef
7 changed files with 442 additions and 306 deletions

View file

@ -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>
)
}
}

View file

@ -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)
}
/>
)}
</>
)
}

View file

@ -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} />

View file

@ -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>
)
}
}

View file

@ -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>

View file

@ -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

View file

@ -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)}
/>
)}