mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Push props down so we can manipulate at the top level
This commit is contained in:
parent
3c7dbc995c
commit
a730a4cb85
4 changed files with 251 additions and 383 deletions
|
|
@ -2,14 +2,16 @@ import { getLuminance, lighten, parseToRgba, toHsla } from 'color2k'
|
|||
import { useRouter } from 'next/router'
|
||||
import { Button } from './Button'
|
||||
import { SpanBox, HStack } from './LayoutPrimitives'
|
||||
import { Circle } from 'phosphor-react'
|
||||
import { Circle, X } from 'phosphor-react'
|
||||
import { isDarkTheme } from '../../lib/themeUpdater'
|
||||
import { theme } from '../tokens/stitches.config'
|
||||
|
||||
type LabelChipProps = {
|
||||
text: string
|
||||
color: string // expected to be a RGB hex color string
|
||||
isSelected?: boolean
|
||||
useAppAppearance?: boolean
|
||||
xAction?: () => void
|
||||
}
|
||||
|
||||
export function LabelChip(props: LabelChipProps): JSX.Element {
|
||||
|
|
@ -18,7 +20,7 @@ export function LabelChip(props: LabelChipProps): JSX.Element {
|
|||
|
||||
const luminance = getLuminance(props.color)
|
||||
const textColor = luminance > 0.5 ? '#000000' : '#ffffff'
|
||||
const selectedBorder = isDark ? 'white' : 'black'
|
||||
const selectedBorder = isDark ? '#FFEA9F' : 'black'
|
||||
const unSelectedBorder = isDark ? '#6A6968' : '#D9D9D9'
|
||||
|
||||
if (props.useAppAppearance) {
|
||||
|
|
@ -42,9 +44,30 @@ export function LabelChip(props: LabelChipProps): JSX.Element {
|
|||
backgroundColor: isDark ? '#2A2A2A' : '#F5F5F5',
|
||||
}}
|
||||
>
|
||||
<HStack alignment="center" css={{ gap: '5px' }}>
|
||||
<HStack alignment="center" css={{ gap: '10px' }}>
|
||||
<Circle size={14} color={props.color} weight="fill" />
|
||||
<SpanBox css={{ pt: '1px' }}>{props.text}</SpanBox>
|
||||
{props.xAction && (
|
||||
<Button
|
||||
style="ghost"
|
||||
css={{ display: 'flex', pt: '1px' }}
|
||||
onClick={(event) => {
|
||||
if (props.xAction) {
|
||||
props.xAction()
|
||||
event.preventDefault()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<X
|
||||
size={14}
|
||||
color={
|
||||
props.isSelected
|
||||
? '#FFEA9F'
|
||||
: theme.colors.thBorderSubtle.toString()
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
</SpanBox>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,29 +4,34 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|||
import { Label } from '../../lib/networking/fragments/labelFragment'
|
||||
import { useGetLabelsQuery } from '../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { LabelChip } from './LabelChip'
|
||||
import { randomLabelColorHex } from '../../utils/settings-page/labels/labelColorObjects'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { createLabelMutation } from '../../lib/networking/mutations/createLabelMutation'
|
||||
import { showSuccessToast } from '../../lib/toastHelpers'
|
||||
import { isTouchScreenDevice } from '../../lib/deviceType'
|
||||
|
||||
type LabelsPickerProps = {
|
||||
selectedLabels: Label[]
|
||||
focused: boolean
|
||||
|
||||
onFocus?: () => void
|
||||
onFilterTextChange?: (filterText: string) => void
|
||||
inputValue: string
|
||||
setInputValue: (value: string) => void
|
||||
clearInputState: () => void
|
||||
|
||||
onFocus?: () => void
|
||||
setSelectedLabels: (labels: Label[]) => void
|
||||
|
||||
deleteLastLabel: () => void
|
||||
selectOrCreateLabel: (value: string) => void
|
||||
|
||||
tabCount: number
|
||||
setTabCount: (count: number) => void
|
||||
tabStartValue: string
|
||||
setTabStartValue: (value: string) => void
|
||||
|
||||
highlightLastLabel: boolean
|
||||
setHighlightLastLabel: (set: boolean) => void
|
||||
}
|
||||
|
||||
export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => {
|
||||
const inputRef = useRef<HTMLInputElement | null>()
|
||||
const availableLabels = useGetLabelsQuery()
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [tabCount, setTabCount] = useState(-1)
|
||||
const [tabStartValue, setTabStartValue] = useState('')
|
||||
const [highlightLastLabel, setHighlightLastLabel] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTouchScreenDevice() && props.focused && inputRef.current) {
|
||||
|
|
@ -34,115 +39,25 @@ export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => {
|
|||
}
|
||||
}, [props.focused])
|
||||
|
||||
useEffect(() => {
|
||||
if (props.onFilterTextChange) {
|
||||
props.onFilterTextChange(inputValue)
|
||||
}
|
||||
}, [inputValue])
|
||||
|
||||
const showMessage = useCallback((msg: string) => {
|
||||
console.log('showMessage: ', msg)
|
||||
}, [])
|
||||
|
||||
const clearInputState = useCallback(() => {
|
||||
setTabCount(-1)
|
||||
setInputValue('')
|
||||
setTabStartValue('')
|
||||
setHighlightLastLabel(false)
|
||||
}, [tabCount, inputValue, tabStartValue])
|
||||
|
||||
const createLabelAsync = useCallback(
|
||||
(tempLabel: Label) => {
|
||||
;(async () => {
|
||||
const currentLabels = props.selectedLabels
|
||||
const newLabel = await createLabelMutation(
|
||||
tempLabel.name,
|
||||
tempLabel.color
|
||||
)
|
||||
if (newLabel) {
|
||||
const idx = currentLabels.findIndex((l) => l.id === tempLabel.id)
|
||||
showSuccessToast(`Created label ${newLabel.name}`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
if (idx !== -1) {
|
||||
currentLabels[idx] = newLabel
|
||||
props.setSelectedLabels([...currentLabels])
|
||||
} else {
|
||||
props.setSelectedLabels([...currentLabels, newLabel])
|
||||
}
|
||||
} else {
|
||||
showMessage(`Error creating label ${tempLabel.name}`)
|
||||
}
|
||||
})()
|
||||
},
|
||||
[props.selectedLabels]
|
||||
)
|
||||
|
||||
const selectOrCreateLabel = useCallback(
|
||||
(value: string) => {
|
||||
const current = props.selectedLabels ?? []
|
||||
const lowerCasedValue = value.toLowerCase()
|
||||
const existing = availableLabels.labels.find(
|
||||
(l) => l.name.toLowerCase() == lowerCasedValue
|
||||
)
|
||||
|
||||
if (lowerCasedValue.length < 1) {
|
||||
return
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
const isAdded = props.selectedLabels.find(
|
||||
(l) => l.name.toLowerCase() == lowerCasedValue
|
||||
)
|
||||
if (!isAdded) {
|
||||
props.setSelectedLabels([...current, existing])
|
||||
clearInputState()
|
||||
} else {
|
||||
showMessage(`label ${value} already added.`)
|
||||
}
|
||||
} else {
|
||||
const tempLabel = {
|
||||
id: uuidv4(),
|
||||
name: value,
|
||||
color: randomLabelColorHex(),
|
||||
description: '',
|
||||
createdAt: new Date(),
|
||||
_temporary: true,
|
||||
}
|
||||
props.setSelectedLabels([...current, tempLabel])
|
||||
clearInputState()
|
||||
|
||||
createLabelAsync(tempLabel)
|
||||
}
|
||||
},
|
||||
[
|
||||
availableLabels,
|
||||
props.selectedLabels,
|
||||
clearInputState,
|
||||
createLabelAsync,
|
||||
showMessage,
|
||||
]
|
||||
)
|
||||
|
||||
const autoComplete = useCallback(() => {
|
||||
const lowerCasedValue = inputValue.toLowerCase()
|
||||
const lowerCasedValue = props.inputValue.toLowerCase()
|
||||
|
||||
if (lowerCasedValue.length < 1) {
|
||||
return
|
||||
}
|
||||
|
||||
let _tabCount = tabCount
|
||||
let _tabStartValue = tabStartValue.toLowerCase()
|
||||
let _tabCount = props.tabCount
|
||||
let _tabStartValue = props.tabStartValue.toLowerCase()
|
||||
|
||||
if (_tabCount === -1) {
|
||||
_tabCount = 0
|
||||
_tabStartValue = lowerCasedValue
|
||||
|
||||
setTabCount(0)
|
||||
setTabStartValue(lowerCasedValue)
|
||||
props.setTabCount(0)
|
||||
props.setTabStartValue(lowerCasedValue)
|
||||
} else {
|
||||
_tabCount = tabCount + 1
|
||||
setTabCount(_tabCount)
|
||||
_tabCount = props.tabCount + 1
|
||||
props.setTabCount(_tabCount)
|
||||
}
|
||||
|
||||
const matches = availableLabels.labels.filter((l) =>
|
||||
|
|
@ -150,32 +65,21 @@ export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => {
|
|||
)
|
||||
|
||||
if (_tabCount < matches.length) {
|
||||
setInputValue(matches[_tabCount].name)
|
||||
props.setInputValue(matches[_tabCount].name)
|
||||
} else if (matches.length > 0) {
|
||||
setTabCount(0)
|
||||
setInputValue(matches[0].name)
|
||||
props.setTabCount(0)
|
||||
props.setInputValue(matches[0].name)
|
||||
}
|
||||
}, [inputValue, availableLabels, tabCount, tabStartValue])
|
||||
|
||||
const deleteLastLabel = useCallback(() => {
|
||||
if (highlightLastLabel) {
|
||||
const current = props.selectedLabels
|
||||
current.pop()
|
||||
props.setSelectedLabels([...current])
|
||||
setHighlightLastLabel(false)
|
||||
} else {
|
||||
setHighlightLastLabel(true)
|
||||
}
|
||||
}, [highlightLastLabel, props.selectedLabels])
|
||||
}, [props.inputValue, availableLabels, props.tabCount, props.tabStartValue])
|
||||
|
||||
const clearTabState = useCallback(() => {
|
||||
setTabCount(-1)
|
||||
setTabStartValue('')
|
||||
props.setTabCount(-1)
|
||||
props.setTabStartValue('')
|
||||
}, [])
|
||||
|
||||
const isEmpty = useMemo(() => {
|
||||
return props.selectedLabels.length === 0 && inputValue.length === 0
|
||||
}, [inputValue, props.selectedLabels])
|
||||
return props.selectedLabels.length === 0 && props.inputValue.length === 0
|
||||
}, [props.inputValue, props.selectedLabels])
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
|
@ -206,7 +110,6 @@ export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => {
|
|||
'>span': {
|
||||
marginTop: '0px',
|
||||
marginBottom: '0px',
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
onMouseDown={(event) => {
|
||||
|
|
@ -228,8 +131,16 @@ export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => {
|
|||
text={label.name}
|
||||
color={label.color}
|
||||
isSelected={
|
||||
highlightLastLabel && idx == props.selectedLabels.length - 1
|
||||
props.highlightLastLabel && idx == props.selectedLabels.length - 1
|
||||
}
|
||||
xAction={() => {
|
||||
const idx = props.selectedLabels.findIndex((l) => l.id == label.id)
|
||||
if (idx !== -1) {
|
||||
const _selectedLabels = props.selectedLabels
|
||||
_selectedLabels.splice(idx, 1)
|
||||
props.setSelectedLabels([..._selectedLabels])
|
||||
}
|
||||
}}
|
||||
useAppAppearance={true}
|
||||
/>
|
||||
))}
|
||||
|
|
@ -253,17 +164,17 @@ export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => {
|
|||
}}
|
||||
minWidth="2px"
|
||||
maxLength={48}
|
||||
value={inputValue}
|
||||
value={props.inputValue}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onKeyUp={(event) => {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
clearInputState()
|
||||
props.clearInputState()
|
||||
break
|
||||
case 'Enter':
|
||||
selectOrCreateLabel(inputValue)
|
||||
props.selectOrCreateLabel(props.inputValue)
|
||||
event.preventDefault()
|
||||
break
|
||||
}
|
||||
|
|
@ -277,243 +188,18 @@ export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => {
|
|||
case 'Delete':
|
||||
case 'Backspace':
|
||||
clearTabState()
|
||||
if (inputValue.length === 0) {
|
||||
deleteLastLabel()
|
||||
if (props.inputValue.length === 0) {
|
||||
props.deleteLastLabel()
|
||||
event.preventDefault()
|
||||
}
|
||||
break
|
||||
}
|
||||
}}
|
||||
onChange={function (event) {
|
||||
setInputValue(event.target.value)
|
||||
props.setInputValue(event.target.value)
|
||||
}}
|
||||
/>
|
||||
</SpanBox>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// import { styled } from '@stitches/react'
|
||||
// import { Label } from '../../lib/networking/fragments/labelFragment'
|
||||
// import { LabelChip } from './LabelChip'
|
||||
// import { Box } from './LayoutPrimitives'
|
||||
// import { useCallback, useMemo, useState } from 'react'
|
||||
// import { useCombobox, useMultipleSelection } from 'downshift'
|
||||
|
||||
// type LabelsPickerProps = {
|
||||
// // selectedLabels: Label[]
|
||||
// }
|
||||
|
||||
// const InputLabel = styled('input', {
|
||||
// outline: 'none',
|
||||
// boxSizing: 'content-box',
|
||||
// maxWidth: '237px',
|
||||
// background: 'red',
|
||||
// borderStyle: 'none',
|
||||
// width: '100%',
|
||||
// })
|
||||
|
||||
// const StyledInput = styled('input', {
|
||||
// outline: 'none',
|
||||
// boxSizing: 'content-box',
|
||||
// maxWidth: '237px',
|
||||
// background: 'red',
|
||||
// borderStyle: 'none',
|
||||
// width: '100%',
|
||||
// })
|
||||
|
||||
// type Suggestion = {
|
||||
// name: string
|
||||
// year: number
|
||||
// }
|
||||
|
||||
// const languages = [
|
||||
// {
|
||||
// name: 'C',
|
||||
// year: 1972,
|
||||
// },
|
||||
// {
|
||||
// name: 'Elm',
|
||||
// year: 2012,
|
||||
// },
|
||||
// ]
|
||||
|
||||
// export function LabelsPicker(props: LabelsPickerProps): JSX.Element {
|
||||
// const labels: Label[] = [
|
||||
// { id: '123', name: 'Label 01', color: '#000000', createdAt: new Date(0) },
|
||||
// { id: '124', name: 'Label 02', color: '#000000', createdAt: new Date(0) },
|
||||
// { id: '125', name: 'Label 03', color: '#000000', createdAt: new Date(0) },
|
||||
// ]
|
||||
// const initialSelectedItems: Label[] = []
|
||||
|
||||
// function getFilteredBooks(selectedItems: Label[], inputValue: string) {
|
||||
// const lowerCasedInputValue = inputValue.toLowerCase()
|
||||
|
||||
// return labels.filter(function filterBook(book) {
|
||||
// return (
|
||||
// !selectedItems.includes(book) &&
|
||||
// book.name.toLowerCase().includes(lowerCasedInputValue)
|
||||
// )
|
||||
// })
|
||||
// }
|
||||
|
||||
// function MultipleComboBox() {
|
||||
// const [inputValue, setInputValue] = useState('')
|
||||
// const [selectedItems, setSelectedItems] = useState(initialSelectedItems)
|
||||
// const items = useMemo(
|
||||
// () => getFilteredBooks(selectedItems, inputValue),
|
||||
// [selectedItems, inputValue]
|
||||
// )
|
||||
// const { getSelectedItemProps, getDropdownProps, removeSelectedItem } =
|
||||
// useMultipleSelection({
|
||||
// selectedItems,
|
||||
// onStateChange({ selectedItems: newSelectedItems, type }) {
|
||||
// switch (type) {
|
||||
// case useMultipleSelection.stateChangeTypes
|
||||
// .SelectedItemKeyDownBackspace:
|
||||
// case useMultipleSelection.stateChangeTypes
|
||||
// .SelectedItemKeyDownDelete:
|
||||
// case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
|
||||
// case useMultipleSelection.stateChangeTypes
|
||||
// .FunctionRemoveSelectedItem:
|
||||
// setSelectedItems(newSelectedItems as Label[])
|
||||
// break
|
||||
// default:
|
||||
// break
|
||||
// }
|
||||
// },
|
||||
// })
|
||||
// const {
|
||||
// isOpen,
|
||||
// getToggleButtonProps,
|
||||
// getLabelProps,
|
||||
// getMenuProps,
|
||||
// getInputProps,
|
||||
// highlightedIndex,
|
||||
// getItemProps,
|
||||
// selectedItem,
|
||||
// } = useCombobox({
|
||||
// items,
|
||||
// itemToString(item: Label | null) {
|
||||
// return item ? item.name : ''
|
||||
// },
|
||||
// defaultHighlightedIndex: 0, // after selection, highlight the first item.
|
||||
// selectedItem: null,
|
||||
// stateReducer(state, actionAndChanges) {
|
||||
// const { changes, type } = actionAndChanges
|
||||
|
||||
// switch (type) {
|
||||
// case useCombobox.stateChangeTypes.InputKeyDownEnter:
|
||||
// case useCombobox.stateChangeTypes.ItemClick:
|
||||
// return {
|
||||
// ...changes,
|
||||
// isOpen: true, // keep the menu open after selection.
|
||||
// highlightedIndex: 0, // with the first option highlighted.
|
||||
// }
|
||||
// default:
|
||||
// return changes
|
||||
// }
|
||||
// },
|
||||
// onStateChange({
|
||||
// inputValue: newInputValue,
|
||||
// type,
|
||||
// selectedItem: newSelectedItem,
|
||||
// }) {
|
||||
// switch (type) {
|
||||
// case useCombobox.stateChangeTypes.InputKeyDownEnter:
|
||||
// case useCombobox.stateChangeTypes.ItemClick:
|
||||
// case useCombobox.stateChangeTypes.InputBlur:
|
||||
// if (newSelectedItem) {
|
||||
// setSelectedItems([...selectedItems, newSelectedItem])
|
||||
// }
|
||||
// break
|
||||
|
||||
// case useCombobox.stateChangeTypes.InputChange:
|
||||
// setInputValue(newInputValue || '')
|
||||
|
||||
// break
|
||||
// default:
|
||||
// break
|
||||
// }
|
||||
// },
|
||||
// })
|
||||
|
||||
// return (
|
||||
// <div className="w-[592px]">
|
||||
// <div className="flex flex-col gap-1">
|
||||
// <label className="w-fit" {...getLabelProps()}>
|
||||
// Pick some books:
|
||||
// </label>
|
||||
// <div className="shadow-sm bg-white inline-flex gap-2 items-center flex-wrap p-1.5">
|
||||
// {selectedItems.map(function renderSelectedItem(
|
||||
// selectedItemForRender,
|
||||
// index
|
||||
// ) {
|
||||
// return (
|
||||
// <span
|
||||
// className="bg-gray-100 rounded-md px-1 focus:bg-red-400"
|
||||
// key={`selected-item-${index}`}
|
||||
// {...getSelectedItemProps({
|
||||
// selectedItem: selectedItemForRender,
|
||||
// index,
|
||||
// })}
|
||||
// >
|
||||
// {selectedItemForRender.name}
|
||||
// <span
|
||||
// className="px-1 cursor-pointer"
|
||||
// onClick={(e) => {
|
||||
// e.stopPropagation()
|
||||
// removeSelectedItem(selectedItemForRender)
|
||||
// }}
|
||||
// >
|
||||
// ✕
|
||||
// </span>
|
||||
// </span>
|
||||
// )
|
||||
// })}
|
||||
// <div className="flex gap-0.5 grow">
|
||||
// <input
|
||||
// placeholder="Best book ever"
|
||||
// className="w-full"
|
||||
// {...getInputProps(
|
||||
// getDropdownProps({ preventKeyAction: isOpen })
|
||||
// )}
|
||||
// />
|
||||
// <button
|
||||
// aria-label="toggle menu"
|
||||
// className="px-2"
|
||||
// type="button"
|
||||
// {...getToggleButtonProps()}
|
||||
// >
|
||||
// ↓
|
||||
// </button>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// <ul
|
||||
// className={`absolute w-inherit bg-white mt-1 shadow-md max-h-80 overflow-scroll p-0 ${
|
||||
// !(isOpen && items.length) && 'hidden'
|
||||
// }`}
|
||||
// {...getMenuProps()}
|
||||
// >
|
||||
// {isOpen &&
|
||||
// items.map((item, index) => (
|
||||
// <li
|
||||
// // className={
|
||||
// // (highlightedIndex === index && 'bg-blue-300',
|
||||
// // selectedItem === item && 'font-bold',
|
||||
// // 'py-2 px-3 shadow-sm flex flex-col')
|
||||
// // }
|
||||
// key={`${item.id}${index}`}
|
||||
// {...getItemProps({ item, index })}
|
||||
// >
|
||||
// <span>{item.name}</span>
|
||||
// <span className="text-sm text-gray-700">{item.name}</span>
|
||||
// </li>
|
||||
// ))}
|
||||
// </ul>
|
||||
// </div>
|
||||
// )
|
||||
// }
|
||||
// return <MultipleComboBox />
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -3,12 +3,10 @@ import Link from 'next/link'
|
|||
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { CrossIcon } from '../../elements/images/CrossIcon'
|
||||
import { styled, theme } from '../../tokens/stitches.config'
|
||||
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 { createLabelMutation } from '../../../lib/networking/mutations/createLabelMutation'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects'
|
||||
|
|
@ -22,20 +20,48 @@ export interface LabelsProvider {
|
|||
type SetLabelsControlProps = {
|
||||
provider: LabelsProvider
|
||||
|
||||
inputValue: string
|
||||
setInputValue: (value: string) => void
|
||||
clearInputState: () => void
|
||||
|
||||
selectedLabels: Label[]
|
||||
setSelectedLabels: (labels: Label[]) => void
|
||||
|
||||
onLabelsUpdated?: (labels: Label[]) => void
|
||||
|
||||
tabCount: number
|
||||
setTabCount: (count: number) => void
|
||||
tabStartValue: string
|
||||
setTabStartValue: (value: string) => void
|
||||
|
||||
highlightLastLabel: boolean
|
||||
setHighlightLastLabel: (set: boolean) => void
|
||||
|
||||
deleteLastLabel: () => void
|
||||
selectOrCreateLabel: (value: string) => void
|
||||
}
|
||||
|
||||
type HeaderProps = {
|
||||
filterText: string
|
||||
focused: boolean
|
||||
resetFocusedIndex: () => void
|
||||
setFilterText: (text: string) => void
|
||||
|
||||
inputValue: string
|
||||
setInputValue: (value: string) => void
|
||||
clearInputState: () => void
|
||||
|
||||
selectedLabels: Label[]
|
||||
setSelectedLabels: (labels: Label[]) => void
|
||||
|
||||
tabCount: number
|
||||
setTabCount: (count: number) => void
|
||||
tabStartValue: string
|
||||
setTabStartValue: (value: string) => void
|
||||
|
||||
highlightLastLabel: boolean
|
||||
setHighlightLastLabel: (set: boolean) => void
|
||||
|
||||
deleteLastLabel: () => void
|
||||
selectOrCreateLabel: (value: string) => void
|
||||
}
|
||||
|
||||
const StyledLabel = styled('label', {
|
||||
|
|
@ -55,14 +81,22 @@ function Header(props: HeaderProps): JSX.Element {
|
|||
>
|
||||
<LabelsPicker
|
||||
focused={props.focused}
|
||||
inputValue={props.inputValue}
|
||||
setInputValue={props.setInputValue}
|
||||
selectedLabels={props.selectedLabels}
|
||||
setSelectedLabels={props.setSelectedLabels}
|
||||
onFilterTextChange={(filterText) => {
|
||||
props.setFilterText(filterText)
|
||||
}}
|
||||
tabCount={props.tabCount}
|
||||
setTabCount={props.setTabCount}
|
||||
tabStartValue={props.tabStartValue}
|
||||
setTabStartValue={props.setTabStartValue}
|
||||
highlightLastLabel={props.highlightLastLabel}
|
||||
setHighlightLastLabel={props.setHighlightLastLabel}
|
||||
onFocus={() => {
|
||||
props.resetFocusedIndex()
|
||||
}}
|
||||
clearInputState={props.clearInputState}
|
||||
deleteLastLabel={props.deleteLastLabel}
|
||||
selectOrCreateLabel={props.selectOrCreateLabel}
|
||||
/>
|
||||
</Box>
|
||||
</VStack>
|
||||
|
|
@ -223,12 +257,11 @@ function Footer(props: FooterProps): JSX.Element {
|
|||
|
||||
export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
||||
const router = useRouter()
|
||||
const [filterText, setFilterText] = useState('')
|
||||
const { labels, revalidate } = useGetLabelsQuery()
|
||||
|
||||
useEffect(() => {
|
||||
setFocusedIndex(undefined)
|
||||
}, [filterText])
|
||||
}, [props.inputValue])
|
||||
|
||||
const isSelected = useCallback(
|
||||
(label: Label): boolean => {
|
||||
|
|
@ -256,6 +289,7 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
props.onLabelsUpdated(newSelectedLabels)
|
||||
}
|
||||
|
||||
props.clearInputState()
|
||||
revalidate()
|
||||
},
|
||||
[isSelected, props, revalidate]
|
||||
|
|
@ -267,12 +301,12 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
}
|
||||
return labels
|
||||
.filter((label) => {
|
||||
return label.name.toLowerCase().includes(filterText.toLowerCase())
|
||||
return label.name.toLowerCase().includes(props.inputValue.toLowerCase())
|
||||
})
|
||||
.sort((left: Label, right: Label) => {
|
||||
return left.name.localeCompare(right.name)
|
||||
})
|
||||
}, [labels, filterText])
|
||||
}, [labels, props.inputValue])
|
||||
|
||||
// Move focus through the labels list on tab or arrow up/down keys
|
||||
const [focusedIndex, setFocusedIndex] = useState<number | undefined>(
|
||||
|
|
@ -291,7 +325,7 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
showErrorToast('Failed to create label', { position: 'bottom-right' })
|
||||
}
|
||||
},
|
||||
[filterText, toggleLabel]
|
||||
[props.inputValue, toggleLabel]
|
||||
)
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
|
|
@ -307,7 +341,7 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
}
|
||||
// If the `Create New label` button isn't visible we skip it
|
||||
// when navigating with the arrow keys
|
||||
if (focusedIndex === maxIndex && !filterText) {
|
||||
if (focusedIndex === maxIndex && !props.inputValue) {
|
||||
newIndex = maxIndex - 2
|
||||
}
|
||||
setFocusedIndex(newIndex)
|
||||
|
|
@ -322,7 +356,7 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
}
|
||||
// If the `Create New label` button isn't visible we skip it
|
||||
// when navigating with the arrow keys
|
||||
if (focusedIndex === maxIndex - 2 && !filterText) {
|
||||
if (focusedIndex === maxIndex - 2 && !props.inputValue) {
|
||||
newIndex = maxIndex
|
||||
}
|
||||
setFocusedIndex(newIndex)
|
||||
|
|
@ -334,8 +368,8 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
return
|
||||
}
|
||||
if (focusedIndex === maxIndex - 1) {
|
||||
const _filterText = filterText
|
||||
setFilterText('')
|
||||
const _filterText = props.inputValue
|
||||
props.setInputValue('')
|
||||
await createLabelFromFilterText(_filterText)
|
||||
return
|
||||
}
|
||||
|
|
@ -348,7 +382,7 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
}
|
||||
},
|
||||
[
|
||||
filterText,
|
||||
props.inputValue,
|
||||
filteredLabels,
|
||||
focusedIndex,
|
||||
createLabelFromFilterText,
|
||||
|
|
@ -369,10 +403,19 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
<Header
|
||||
focused={focusedIndex === undefined}
|
||||
resetFocusedIndex={() => setFocusedIndex(undefined)}
|
||||
setFilterText={setFilterText}
|
||||
filterText={filterText}
|
||||
inputValue={props.inputValue}
|
||||
setInputValue={props.setInputValue}
|
||||
selectedLabels={props.selectedLabels}
|
||||
setSelectedLabels={props.setSelectedLabels}
|
||||
tabCount={props.tabCount}
|
||||
setTabCount={props.setTabCount}
|
||||
tabStartValue={props.tabStartValue}
|
||||
setTabStartValue={props.setTabStartValue}
|
||||
highlightLastLabel={props.highlightLastLabel}
|
||||
setHighlightLastLabel={props.setHighlightLastLabel}
|
||||
deleteLastLabel={props.deleteLastLabel}
|
||||
selectOrCreateLabel={props.selectOrCreateLabel}
|
||||
clearInputState={props.clearInputState}
|
||||
/>
|
||||
<VStack
|
||||
distribution="start"
|
||||
|
|
@ -396,7 +439,7 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
))}
|
||||
</VStack>
|
||||
<Footer
|
||||
filterText={filterText}
|
||||
filterText={props.inputValue}
|
||||
focused={focusedIndex === filteredLabels.length + 1}
|
||||
/>
|
||||
</VStack>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { showErrorToast } from '../../../lib/toastHelpers'
|
||||
import { SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import {
|
||||
ModalRoot,
|
||||
|
|
@ -9,6 +8,11 @@ import {
|
|||
ModalTitleBar,
|
||||
} from '../../elements/ModalPrimitives'
|
||||
import { LabelsProvider, SetLabelsControl } from './SetLabelsControl'
|
||||
import { createLabelMutation } from '../../../lib/networking/mutations/createLabelMutation'
|
||||
import { showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects'
|
||||
|
||||
type SetLabelsModalProps = {
|
||||
provider: LabelsProvider
|
||||
|
|
@ -19,6 +23,12 @@ type SetLabelsModalProps = {
|
|||
}
|
||||
|
||||
export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const availableLabels = useGetLabelsQuery()
|
||||
const [tabCount, setTabCount] = useState(-1)
|
||||
const [tabStartValue, setTabStartValue] = useState('')
|
||||
const [highlightLastLabel, setHighlightLastLabel] = useState(false)
|
||||
|
||||
const [selectedLabels, setSelectedLabels] = useState(
|
||||
props.provider.labels ?? []
|
||||
)
|
||||
|
|
@ -37,6 +47,101 @@ export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
|
|||
[props, selectedLabels]
|
||||
)
|
||||
|
||||
const showMessage = useCallback((msg: string) => {
|
||||
console.log('showMessage: ', msg)
|
||||
}, [])
|
||||
|
||||
const clearInputState = useCallback(() => {
|
||||
setTabCount(-1)
|
||||
setInputValue('')
|
||||
setTabStartValue('')
|
||||
setHighlightLastLabel(false)
|
||||
}, [tabCount, tabStartValue, highlightLastLabel])
|
||||
|
||||
const createLabelAsync = useCallback(
|
||||
(tempLabel: Label) => {
|
||||
;(async () => {
|
||||
const currentLabels = selectedLabels
|
||||
const newLabel = await createLabelMutation(
|
||||
tempLabel.name,
|
||||
tempLabel.color
|
||||
)
|
||||
if (newLabel) {
|
||||
const idx = currentLabels.findIndex((l) => l.id === tempLabel.id)
|
||||
showSuccessToast(`Created label ${newLabel.name}`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
if (idx !== -1) {
|
||||
currentLabels[idx] = newLabel
|
||||
setSelectedLabels([...currentLabels])
|
||||
} else {
|
||||
setSelectedLabels([...currentLabels, newLabel])
|
||||
}
|
||||
} else {
|
||||
showMessage(`Error creating label ${tempLabel.name}`)
|
||||
}
|
||||
})()
|
||||
},
|
||||
[selectedLabels]
|
||||
)
|
||||
|
||||
const selectOrCreateLabel = useCallback(
|
||||
(value: string) => {
|
||||
const current = selectedLabels ?? []
|
||||
const lowerCasedValue = value.toLowerCase()
|
||||
const existing = availableLabels.labels.find(
|
||||
(l) => l.name.toLowerCase() == lowerCasedValue
|
||||
)
|
||||
|
||||
if (lowerCasedValue.length < 1) {
|
||||
return
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
const isAdded = selectedLabels.find(
|
||||
(l) => l.name.toLowerCase() == lowerCasedValue
|
||||
)
|
||||
if (!isAdded) {
|
||||
setSelectedLabels([...current, existing])
|
||||
clearInputState()
|
||||
} else {
|
||||
showMessage(`label ${value} already added.`)
|
||||
}
|
||||
} else {
|
||||
const tempLabel = {
|
||||
id: uuidv4(),
|
||||
name: value,
|
||||
color: randomLabelColorHex(),
|
||||
description: '',
|
||||
createdAt: new Date(),
|
||||
_temporary: true,
|
||||
}
|
||||
setSelectedLabels([...current, tempLabel])
|
||||
clearInputState()
|
||||
|
||||
createLabelAsync(tempLabel)
|
||||
}
|
||||
},
|
||||
[
|
||||
availableLabels,
|
||||
selectedLabels,
|
||||
clearInputState,
|
||||
createLabelAsync,
|
||||
showMessage,
|
||||
]
|
||||
)
|
||||
|
||||
const deleteLastLabel = useCallback(() => {
|
||||
if (highlightLastLabel) {
|
||||
const current = selectedLabels
|
||||
current.pop()
|
||||
setSelectedLabels([...current])
|
||||
setHighlightLastLabel(false)
|
||||
} else {
|
||||
setHighlightLastLabel(true)
|
||||
}
|
||||
}, [highlightLastLabel, selectedLabels])
|
||||
|
||||
useEffect(() => {
|
||||
if (!containsTemporaryLabel(selectedLabels)) {
|
||||
;(async () => {
|
||||
|
|
@ -64,9 +169,20 @@ export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
|
|||
</SpanBox>
|
||||
<SetLabelsControl
|
||||
provider={props.provider}
|
||||
inputValue={inputValue}
|
||||
setInputValue={setInputValue}
|
||||
clearInputState={clearInputState}
|
||||
selectedLabels={selectedLabels}
|
||||
setSelectedLabels={setSelectedLabels}
|
||||
onLabelsUpdated={props.onLabelsUpdated}
|
||||
tabCount={tabCount}
|
||||
setTabCount={setTabCount}
|
||||
tabStartValue={tabStartValue}
|
||||
setTabStartValue={setTabStartValue}
|
||||
highlightLastLabel={highlightLastLabel}
|
||||
setHighlightLastLabel={setHighlightLastLabel}
|
||||
deleteLastLabel={deleteLastLabel}
|
||||
selectOrCreateLabel={selectOrCreateLabel}
|
||||
/>
|
||||
</VStack>
|
||||
</ModalContent>
|
||||
|
|
|
|||
Loading…
Reference in a new issue