mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Handle toggling label state
This commit is contained in:
parent
6b84192543
commit
0a4215c57b
2 changed files with 194 additions and 165 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import { Separator } from "@radix-ui/react-separator"
|
||||
import { ArchiveBox, DotsThree, HighlighterCircle, TagSimple, TextAa } from "phosphor-react"
|
||||
import { useRef } from "react"
|
||||
import { useGetUserPreferences, UserPreferences } from "../../../lib/networking/queries/useGetUserPreferences"
|
||||
import { Button } from "../../elements/Button"
|
||||
import { Dropdown } from "../../elements/DropdownElements"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { StyledText } from '../../elements/StyledText'
|
|||
import { CrossIcon } from '../../elements/images/CrossIcon'
|
||||
import { styled, theme } from '../../tokens/stitches.config'
|
||||
import { Label, useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { ChangeEvent, useCallback, useRef, useState, useMemo } from 'react'
|
||||
import { ChangeEvent, useCallback, useRef, useState, useMemo, useEffect } from 'react'
|
||||
import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation'
|
||||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { LabelChip } from '../../elements/LabelChip'
|
||||
|
|
@ -26,6 +26,8 @@ type EditLabelsModalProps = {
|
|||
|
||||
type HeaderProps = {
|
||||
filterText: string
|
||||
focused: boolean
|
||||
parentRef: React.RefObject<HTMLDivElement>
|
||||
setFilterText: (text: string) => void
|
||||
}
|
||||
|
||||
|
|
@ -44,70 +46,68 @@ const FormInput = styled('input', {
|
|||
const StyledLabel = styled('label', {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
'&:focus-visible': {
|
||||
backgroundColor: 'red',
|
||||
'&:focus': {
|
||||
bg: '$grayBgActive',
|
||||
},
|
||||
'input:&:focus-within': {
|
||||
bg: '$grayBgActive',
|
||||
},
|
||||
})
|
||||
|
||||
const useToggleLabels = () => {
|
||||
const [selectedLabels, setSelectedLabels] = useState<Label[]>([])
|
||||
|
||||
const isSelected = useCallback((label: Label): boolean => {
|
||||
return selectedLabels.some((other) => {
|
||||
return other.id === label.id
|
||||
})
|
||||
}, [selectedLabels])
|
||||
|
||||
const toggleLabel = useCallback((label: Label) => {
|
||||
if (isSelected(label)) {
|
||||
setSelectedLabels(selectedLabels.filter((other) => {
|
||||
return other.id !== label.id
|
||||
}))
|
||||
} else {
|
||||
setSelectedLabels([...selectedLabels, label])
|
||||
}
|
||||
}, [isSelected, selectedLabels])
|
||||
|
||||
return [isSelected, toggleLabel]
|
||||
}
|
||||
|
||||
function Header(props: HeaderProps): JSX.Element {
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (props.focused && inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
}
|
||||
}, [props.focused])
|
||||
|
||||
return (
|
||||
<VStack css={{ width: '100%', my: '0px', borderBottom: '1px solid $grayBorder',
|
||||
}}>
|
||||
{/* <HStack
|
||||
distribution="between"
|
||||
alignment="center"
|
||||
css={{
|
||||
width: '100%',
|
||||
flex: '0 0 20px',
|
||||
mx: '0px',
|
||||
p: '0px',
|
||||
borderBottom: `1px solid ${theme.colors.grayLine.toString()}`,
|
||||
}}
|
||||
> */}
|
||||
{/* <StyledText style="modalHeadline" css={{ p: '16px', fontSize: '12px', }}>
|
||||
Apply labels to this page
|
||||
</StyledText> */}
|
||||
{/* <Button
|
||||
css={{ pt: '16px', pr: '16px' }}
|
||||
style="ghost"
|
||||
onClick={() => {
|
||||
|
||||
}}
|
||||
>
|
||||
<CrossIcon
|
||||
size={20}
|
||||
strokeColor={theme.colors.grayText.toString()}
|
||||
/>
|
||||
</Button> */}
|
||||
{/* </HStack> */}
|
||||
<Box css={{
|
||||
width: '100%',
|
||||
my: '14px',
|
||||
px: '14px',
|
||||
}}>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
// event.preventDefault()
|
||||
// props.applySearchQuery(searchTerm || '')
|
||||
inputRef.current?.blur()
|
||||
}}
|
||||
>
|
||||
<FormInput
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
tabIndex={0}
|
||||
tabIndex={props.focused ? 0 : -1}
|
||||
autoFocus={!isTouchScreenDevice()}
|
||||
value={props.filterText}
|
||||
placeholder="Filter for label"
|
||||
onFocus={(event) => {
|
||||
event.target.select()
|
||||
//setFocused(true)
|
||||
console.log('input::focused()')
|
||||
}}
|
||||
onBlur={() => {
|
||||
//setFocused(false)
|
||||
console.log('blurred')
|
||||
// setFocused(false)
|
||||
console.log('input::blurred')
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
console.log('keydown', event.key)
|
||||
|
|
@ -134,94 +134,82 @@ function Header(props: HeaderProps): JSX.Element {
|
|||
},
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
</Box>
|
||||
</VStack>)
|
||||
}
|
||||
|
||||
type LabelsListProps = {
|
||||
// pageId: string
|
||||
// selectedLabels: Label[]
|
||||
availableLabels: Label[]
|
||||
// setSelectedLabels: (labels: Label[]) => void
|
||||
type LabelListItemProps = {
|
||||
label: Label
|
||||
focused: boolean
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
function LabelsList(props: LabelsListProps): JSX.Element {
|
||||
const isSelected = useCallback((label: Label) => {
|
||||
// return props.selectedLabels.some((other) => {
|
||||
// return other.id === label.id
|
||||
// })
|
||||
return false
|
||||
}, [])
|
||||
function LabelListItem(props: LabelListItemProps): JSX.Element {
|
||||
const ref = useRef<HTMLLabelElement>(null)
|
||||
const { label, focused, selected } = props
|
||||
|
||||
useEffect(() => {
|
||||
if (props.focused && ref.current) {
|
||||
ref.current.focus()
|
||||
}
|
||||
}, [props.focused])
|
||||
|
||||
return (
|
||||
<VStack css={{ flexGrow: '1', overflow: 'scroll', width: '100%', height: '100%', maxHeight: '400px', }}>
|
||||
{props.availableLabels &&
|
||||
props.availableLabels.map((label, idx) => (
|
||||
<StyledLabel
|
||||
key={label.id}
|
||||
tabIndex={idx + 1}
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '42px',
|
||||
borderBottom: '1px solid $grayBorder',
|
||||
}}
|
||||
onClick={async () => {
|
||||
// console.log('selected label', label)
|
||||
// if (props.selectedLabels.includes(label)) {
|
||||
// props.setSelectedLabels(
|
||||
// props.selectedLabels.filter((id) => id !== label)
|
||||
// )
|
||||
// } else {
|
||||
// props.setSelectedLabels([...props.selectedLabels, label])
|
||||
// }
|
||||
// const result = await setLabelsMutation(props.pageId, props.selectedLabels.map((l) => l.id))
|
||||
// console.log('result', result)
|
||||
}}
|
||||
>
|
||||
<Box css={{ pl: '5px', width: '25px', height: '45px', display: 'flex', alignItems: 'center' }}>
|
||||
{isSelected(label) && <Check 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>
|
||||
<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', height: '100%', display: 'flex', alignItems: 'center' }}>
|
||||
{isSelected(label) && <CrossIcon
|
||||
size={14}
|
||||
strokeColor={theme.colors.grayText.toString()}
|
||||
/>}
|
||||
</Box>
|
||||
{/*
|
||||
|
||||
<CrossIcon
|
||||
size={14}
|
||||
strokeColor={theme.colors.grayText.toString()}
|
||||
/> */}
|
||||
{/* <input
|
||||
type="checkbox"
|
||||
value={label.id}
|
||||
onChange={handleChange}
|
||||
checked={selectedLabels.includes(label)}
|
||||
/> */}
|
||||
</StyledLabel>
|
||||
))}
|
||||
</VStack>)
|
||||
<StyledLabel
|
||||
ref={ref}
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '42px',
|
||||
borderBottom: '1px solid $grayBorder',
|
||||
bg: props.focused ? '$grayBgActive' : 'unset',
|
||||
}}
|
||||
tabIndex={props.focused ? 0 : -1}
|
||||
onClick={async () => {
|
||||
console.log('toggling label')
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
</StyledLabel>
|
||||
)
|
||||
}
|
||||
|
||||
export function EditLabelsModal(props: EditLabelsModalProps): JSX.Element {
|
||||
const parentRef = useRef<HTMLDivElement>(null)
|
||||
const [filterText, setFilterText] = useState('')
|
||||
const [selectedLabels, setSelectedLabels] = useState([])
|
||||
const { labels } = useGetLabelsQuery()
|
||||
|
||||
const saveAndExit = useCallback(async () => {
|
||||
// const result = await setLabelsMutation(props.article.id, selectedLabels.map((l) => l.id))
|
||||
// console.log('result of setting labels', result)
|
||||
// // props.onOpenChange(false)
|
||||
// // props.setLabels(selectedLabels)
|
||||
}, [props, selectedLabels])
|
||||
const [selectedLabels, setSelectedLabels] = useState<Label[]>([])
|
||||
|
||||
const isSelected = useCallback((label: Label): boolean => {
|
||||
return selectedLabels.some((other) => {
|
||||
return other.id === label.id
|
||||
})
|
||||
}, [selectedLabels])
|
||||
|
||||
const toggleLabel = useCallback((label: Label) => {
|
||||
if (isSelected(label)) {
|
||||
setSelectedLabels(selectedLabels.filter((other) => {
|
||||
return other.id !== label.id
|
||||
}))
|
||||
} else {
|
||||
setSelectedLabels([...selectedLabels, label])
|
||||
}
|
||||
}, [isSelected, selectedLabels])
|
||||
|
||||
const filteredLabels = useMemo(() => {
|
||||
if (!labels) {
|
||||
|
|
@ -232,58 +220,98 @@ export function EditLabelsModal(props: EditLabelsModalProps): JSX.Element {
|
|||
})
|
||||
}, [labels, filterText])
|
||||
|
||||
useEffect(() => {
|
||||
setFocusedIndex(undefined)
|
||||
}, [filterText])
|
||||
|
||||
// Move focus through the labels list on tab or arrow up/down keys
|
||||
const [focusedIndex, setFocusedIndex] = useState<number | undefined>(undefined)
|
||||
const handleKeyDown = useCallback((event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
if (focusedIndex) {
|
||||
setFocusedIndex(Math.max(0, focusedIndex - 1))
|
||||
} else {
|
||||
setFocusedIndex(undefined)
|
||||
}
|
||||
}
|
||||
if (event.key === 'ArrowDown' || event.key === 'Tab') {
|
||||
event.preventDefault()
|
||||
if (focusedIndex === undefined) {
|
||||
setFocusedIndex(0)
|
||||
} else {
|
||||
setFocusedIndex(Math.min(filteredLabels.length - 1, focusedIndex + 1))
|
||||
}
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
if (focusedIndex !== undefined) {
|
||||
const label = filteredLabels[focusedIndex]
|
||||
if (label) {
|
||||
toggleLabel(label)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [filteredLabels, focusedIndex])
|
||||
|
||||
return (
|
||||
// <ModalRoot defaultOpen onOpenChange={saveAndExit}>
|
||||
// <ModalOverlay />
|
||||
// <ModalContent
|
||||
// onPointerDownOutside={(event) => {
|
||||
// event.preventDefault()
|
||||
// }}
|
||||
// css={{ p: '0', width: '100%', maxWidth: '400px' }}
|
||||
// >
|
||||
<VStack distribution="start" css={{
|
||||
p: '0', maxHeight: '400px',
|
||||
maxWidth: '265px',
|
||||
'@mdDown': {
|
||||
maxWidth: '100%',
|
||||
maxHeight: '320px',
|
||||
<VStack
|
||||
ref={parentRef}
|
||||
distribution="start"
|
||||
onKeyDown={handleKeyDown}
|
||||
css={{
|
||||
p: '0',
|
||||
maxHeight: '92%',
|
||||
maxWidth: '265px',
|
||||
'@mdDown': {
|
||||
maxWidth: '100%',
|
||||
maxHeight: '92%',
|
||||
},
|
||||
}}>
|
||||
<Header
|
||||
parentRef={parentRef}
|
||||
focused={focusedIndex === undefined}
|
||||
setFilterText={setFilterText} filterText={filterText}
|
||||
/>
|
||||
<VStack css={{ flexGrow: '1', overflow: 'scroll', width: '100%', height: '100%', maxHeight: '400px', }}>
|
||||
{labels &&
|
||||
labels.map((label, idx) => (
|
||||
<LabelListItem
|
||||
key={label.id}
|
||||
label={label}
|
||||
focused={idx === focusedIndex}
|
||||
selected={isSelected(label)}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
{filterText && (
|
||||
<Button style='modalOption' css={{
|
||||
pl: '26px', color: theme.colors.grayText.toString(), height: '42px', borderBottom: '1px solid $grayBorder'
|
||||
}}
|
||||
onClick={() => {
|
||||
// props.createLabel(filterText)
|
||||
// setFilterText('')
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</HStack>
|
||||
</Button>)}
|
||||
{/* Footer */}
|
||||
<HStack distribution="start" alignment="center" css={{
|
||||
ml: '20px', gap: '8px', width: '100%', fontSize: '12px', p: '8px', height: '42px',
|
||||
'a:link': {
|
||||
textDecoration: 'none',
|
||||
},
|
||||
}}>
|
||||
<Header setFilterText={setFilterText} filterText={filterText} />
|
||||
<LabelsList
|
||||
availableLabels={filteredLabels}
|
||||
// selectedLabels={selectedLabels || []}
|
||||
/>
|
||||
{filterText && (
|
||||
<Button style='modalOption' css={{
|
||||
pl: '26px', color: theme.colors.grayText.toString(), height: '42px', borderBottom: '1px solid $grayBorder'
|
||||
}}
|
||||
onClick={() => {
|
||||
// props.createLabel(filterText)
|
||||
// setFilterText('')
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</HStack>
|
||||
</Button>)}
|
||||
{/* Footer */}
|
||||
<HStack distribution="start" alignment="center" css={{
|
||||
ml: '20px', gap: '8px', width: '100%', fontSize: '12px', p: '8px', height: '42px',
|
||||
'a:link': {
|
||||
textDecoration: 'none',
|
||||
},
|
||||
'a:visited': {
|
||||
color: theme.colors.grayText.toString(),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<PencilSimple size={18} color={theme.colors.grayText.toString()} />
|
||||
<Link href="/settings/labels">Edit labels</Link>
|
||||
</HStack>
|
||||
</VStack>
|
||||
// </ModalContent>
|
||||
// </ModalRoot>
|
||||
'a:visited': {
|
||||
color: theme.colors.grayText.toString(),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<PencilSimple size={18} color={theme.colors.grayText.toString()} />
|
||||
<Link href="/settings/labels">Edit labels</Link>
|
||||
</HStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue