diff --git a/packages/web/components/elements/EditLabelChip.tsx b/packages/web/components/elements/EditLabelChip.tsx
new file mode 100644
index 000000000..bf6fefcfc
--- /dev/null
+++ b/packages/web/components/elements/EditLabelChip.tsx
@@ -0,0 +1,62 @@
+import { Button } from './Button'
+import { SpanBox, HStack } from './LayoutPrimitives'
+import { Circle, X } from 'phosphor-react'
+import { isDarkTheme } from '../../lib/themeUpdater'
+import { theme } from '../tokens/stitches.config'
+
+type EditLabelChipProps = {
+ text: string
+ color: string
+ isSelected?: boolean
+ xAction: () => void
+}
+
+export function EditLabelLabelChip(props: EditLabelChipProps): JSX.Element {
+ const isDark = isDarkTheme()
+
+ const selectedBorder = '#FFEA9F'
+ const unSelectedBorder = 'transparent'
+ return (
+
+
+
+ {props.text}
+
+
+
+ )
+}
diff --git a/packages/web/components/elements/LabelChip.tsx b/packages/web/components/elements/LabelChip.tsx
index 96b76e9cf..29c29e0cb 100644
--- a/packages/web/components/elements/LabelChip.tsx
+++ b/packages/web/components/elements/LabelChip.tsx
@@ -2,38 +2,26 @@ 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 {
const router = useRouter()
const isDark = isDarkTheme()
- const hexToRgb = (hex: string) => {
- const bigint = parseInt(hex.substring(1), 16)
- const r = (bigint >> 16) & 255
- const g = (bigint >> 8) & 255
- const b = bigint & 255
-
- return [r, g, b]
- }
-
- function f(x: number) {
- const channel = x / 255
- return channel <= 0.03928
- ? channel / 12.92
- : Math.pow((channel + 0.055) / 1.055, 2.4)
- }
-
const luminance = getLuminance(props.color)
- const backgroundColor = hexToRgb(props.color)
const textColor = luminance > 0.5 ? '#000000' : '#ffffff'
+ const selectedBorder = isDark ? '#FFEA9F' : 'black'
+ const unSelectedBorder = isDark ? '#6A6968' : '#D9D9D9'
if (props.useAppAppearance) {
return (
@@ -52,13 +40,34 @@ export function LabelChip(props: LabelChipProps): JSX.Element {
borderWidth: '1px',
borderStyle: 'solid',
color: isDark ? '#EBEBEB' : '#2A2A2A',
- borderColor: isDark ? '#6A6968' : '#D9D9D9',
+ borderColor: props.isSelected ? selectedBorder : unSelectedBorder,
backgroundColor: isDark ? '#2A2A2A' : '#F5F5F5',
}}
>
-
+
{props.text}
+ {props.xAction && (
+
+ )}
)
diff --git a/packages/web/components/elements/LabelColorDropdown.tsx b/packages/web/components/elements/LabelColorDropdown.tsx
index 763ee7289..28beae07f 100644
--- a/packages/web/components/elements/LabelColorDropdown.tsx
+++ b/packages/web/components/elements/LabelColorDropdown.tsx
@@ -1,9 +1,6 @@
-import React, { useState } from 'react'
+import React, { useMemo, useRef, useState } from 'react'
import { styled } from '../tokens/stitches.config'
-import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
-import { HexColorPicker } from 'react-colorful'
-import { HStack, SpanBox } from './LayoutPrimitives'
-import { CaretDown } from 'phosphor-react'
+import { Box, HStack, SpanBox } from './LayoutPrimitives'
import { StyledText } from './StyledText'
import {
LabelColorDropdownProps,
@@ -11,41 +8,14 @@ import {
LabelOptionProps,
} from '../../utils/settings-page/labels/types'
import { labelColorObjects } from '../../utils/settings-page/labels/labelColorObjects'
-import { DropdownOption } from './DropdownElements'
-import { isDarkTheme } from '../../lib/themeUpdater'
import { LabelColor } from '../../lib/networking/fragments/labelFragment'
+import { TwitterPicker } from 'react-color'
+import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
const DropdownMenuContent = styled(DropdownMenuPrimitive.Content, {
borderRadius: 6,
backgroundColor: '$grayBg',
padding: 5,
- boxShadow:
- '0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)',
-})
-
-const itemStyles = {
- all: 'unset',
- fontSize: '$3',
- lineHeight: 1,
- borderRadius: 3,
- display: 'flex',
- alignItems: 'center',
- height: 25,
- position: 'relative',
- userSelect: 'none',
-}
-
-const DropdownMenuTriggerItem = styled(DropdownMenuPrimitive.TriggerItem, {
- '&[data-state="open"]': {
- outline: 'none',
- backgroundColor: '$grayBgHover',
- },
- ...itemStyles,
- padding: '$2 0',
- '&:focus': {
- outline: 'none',
- backgroundColor: '$grayBgHover',
- },
})
const DropdownMenu = DropdownMenuPrimitive.Root
@@ -56,67 +26,26 @@ const DropdownMenuTrigger = styled(DropdownMenuPrimitive.Trigger, {
padding: 0,
marginRight: '$2',
})
-const Box = styled('div', {})
-
-const MainContainer = styled(Box, {
- fontFamily: 'inter',
- fontSize: '$2',
- lineHeight: '1.25',
- color: '$grayText',
- display: 'flex',
- justifyContent: 'space-between',
- alignItems: 'center',
- backgroundColor: '$grayBg',
- border: '1px solid $grayBorder',
- width: '180px',
- borderRadius: '$3',
- px: '$3',
- py: '0px',
- cursor: 'pointer',
- '&:hover': {
- border: '1px solid $grayBorderHover',
- },
- '@mdDown': {
- width: '100%',
- },
-})
-
-const CustomLabelWrapper = styled(Box, {
- fontSize: 13,
- padding: 24,
- borderRadius: 3,
- cursor: 'default',
- color: '$grayText',
-
- '&:focus': {
- outline: 'none',
- backgroundColor: '$grayBgHover',
- },
-})
export const LabelColorDropdown = (props: LabelColorDropdownProps) => {
- const {
- isCreateMode,
- canEdit,
- labelColorHexRowId,
- labelId,
- labelColor,
- labelColorHexValue,
- setLabelColorHex,
- } = props
-
- const isDarkMode = isDarkTheme()
- const iconColor = isDarkMode ? '#FFFFFF' : '#0A0806'
+ const pickerRef = useRef(null)
+ const { isCreateMode, canEdit } = props
+ const [triangle, setTriangle] = useState<
+ 'top-left' | 'hide' | 'top-right' | undefined
+ >('top-left')
const [open, setOpen] = useState(false)
- const handleCustomColorChange = (color: string) => {
- setLabelColorHex({
- rowId: labelId,
- value: color.toUpperCase() as LabelColor,
- })
- }
-
const handleOpen = (open: boolean) => {
+ if (
+ pickerRef.current &&
+ window.innerHeight - pickerRef.current?.getBoundingClientRect().bottom <
+ 116
+ ) {
+ setTriangle('hide')
+ } else {
+ setTriangle('top-left')
+ }
+
if (canEdit && open) setOpen(true)
else if (isCreateMode && !canEdit && open) setOpen(true)
else setOpen(false)
@@ -125,6 +54,7 @@ export const LabelColorDropdown = (props: LabelColorDropdownProps) => {
return (
{
minWidth: '170px',
width: 'auto',
},
+ borderRadius: '6px',
+ outlineStyle: 'solid',
+ outlineColor: open ? '$omnivoreYellow' : 'transparent',
}}
>
-
-
- {labelId !== '' && labelId === labelColorHexRowId ? (
-
- ) : (
- <>
- {labelId ? (
-
- ) : (
-
- )}
- >
- )}
-
-
-
-
+
-
- {Object.keys(labelColorObjects)
- .filter((labelColor) => labelColor !== 'custom color')
- .map((labelColor) => (
-
- setLabelColorHex({
- rowId: labelId,
- value: labelColor as LabelColor,
- })
- }
- >
-
-
- ))}
-
-
- null}>
-
-
-
-
-
-
-
+ {
+ switch (event.key) {
+ case 'Escape':
+ setOpen(false)
+ event.preventDefault()
+ break
+ case 'Enter':
+ setOpen(false)
+ event.preventDefault()
+ break
+ }
+ }}
+ >
+ {
+ props.setLabelColor(color.hex.toUpperCase())
+ event.preventDefault()
+ }}
+ onChangeComplete={(color, event) => {
+ props.setLabelColor(color.hex.toUpperCase())
+ event.preventDefault()
+ }}
+ styles={{
+ default: {
+ input: {
+ color: '$grayText',
+ },
+ },
+ }}
+ />
)
@@ -218,64 +117,31 @@ export const LabelColorDropdown = (props: LabelColorDropdownProps) => {
function LabelOption(props: LabelOptionProps): JSX.Element {
const { color, isDropdownOption, isCreateMode, labelId } = props
- // const colorDetails = getColorDetails(
- // color as LabelColor,
- // labelId,
- // Boolean(isCreateMode)
- // )
- const isCreating = isCreateMode && !labelId
- const { text, border, colorName, background } = getLabelColorObject(
- color as LabelColor
- )
-
- let colorNameText = colorName
- if (!labelId && isCreateMode) {
- colorNameText = 'Select Color'
- colorNameText = isDropdownOption ? colorName : colorNameText
- }
-
- colorNameText = color === 'custom color' ? colorNameText : colorName
-
- let colorHex = !labelId && isCreateMode && !isDropdownOption ? '' : text
-
- colorHex =
- !labelId && isCreateMode && !isDropdownOption && color !== 'custom color'
- ? text
- : colorHex
-
return (
-
+
- {colorNameText}
-
-
- {colorNameText === 'custom color' ? '' : colorHex}
+ {props.color}
- {isDropdownOption ? : null}
)
}
@@ -304,10 +169,7 @@ function getLabelColorObject(color: LabelColor) {
return colorObject
}
-function LabelColorIcon(props: {
- fillColor: string
- strokeColor: string
-}): JSX.Element {
+function LabelColorIcon(props: { color: string }): JSX.Element {
return (
)
diff --git a/packages/web/components/elements/LabelsPicker.tsx b/packages/web/components/elements/LabelsPicker.tsx
new file mode 100644
index 000000000..c76924e1c
--- /dev/null
+++ b/packages/web/components/elements/LabelsPicker.tsx
@@ -0,0 +1,205 @@
+import AutosizeInput from 'react-input-autosize'
+import { Box, SpanBox } from './LayoutPrimitives'
+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 { isTouchScreenDevice } from '../../lib/deviceType'
+import { EditLabelLabelChip } from './EditLabelChip'
+
+type LabelsPickerProps = {
+ selectedLabels: Label[]
+ focused: boolean
+
+ 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()
+ const availableLabels = useGetLabelsQuery()
+
+ useEffect(() => {
+ if (!isTouchScreenDevice() && props.focused && inputRef.current) {
+ inputRef.current.focus()
+ }
+ }, [props.focused])
+
+ const autoComplete = useCallback(() => {
+ const lowerCasedValue = props.inputValue.toLowerCase()
+
+ if (lowerCasedValue.length < 1) {
+ return
+ }
+
+ let _tabCount = props.tabCount
+ let _tabStartValue = props.tabStartValue.toLowerCase()
+
+ if (_tabCount === -1) {
+ _tabCount = 0
+ _tabStartValue = lowerCasedValue
+
+ props.setTabCount(0)
+ props.setTabStartValue(lowerCasedValue)
+ } else {
+ _tabCount = props.tabCount + 1
+ props.setTabCount(_tabCount)
+ }
+
+ const matches = availableLabels.labels.filter((l) =>
+ l.name.toLowerCase().startsWith(_tabStartValue)
+ )
+
+ if (_tabCount < matches.length) {
+ props.setInputValue(matches[_tabCount].name)
+ } else if (matches.length > 0) {
+ props.setTabCount(0)
+ props.setInputValue(matches[0].name)
+ }
+ }, [props.inputValue, availableLabels, props.tabCount, props.tabStartValue])
+
+ const clearTabState = useCallback(() => {
+ props.setTabCount(-1)
+ props.setTabStartValue('')
+ }, [])
+
+ const isEmpty = useMemo(() => {
+ return props.selectedLabels.length === 0 && props.inputValue.length === 0
+ }, [props.inputValue, props.selectedLabels])
+
+ return (
+ span': {
+ marginTop: '0px',
+ marginBottom: '0px',
+ },
+ }}
+ onMouseDown={(event) => {
+ inputRef.current?.focus()
+ inputRef.current?.setSelectionRange(
+ inputRef.current?.value.length,
+ inputRef.current?.value.length
+ )
+ event.preventDefault()
+ }}
+ onDoubleClick={(event) => {
+ inputRef.current?.focus()
+ inputRef.current?.setSelectionRange(0, inputRef.current?.value.length)
+ }}
+ >
+ {props.selectedLabels.map((label, idx) => (
+ {
+ const idx = props.selectedLabels.findIndex((l) => l.id == label.id)
+ if (idx !== -1) {
+ const _selectedLabels = props.selectedLabels
+ _selectedLabels.splice(idx, 1)
+ props.setSelectedLabels([..._selectedLabels])
+ }
+ }}
+ />
+ ))}
+
+ {
+ inputRef.current = ref
+ }}
+ onFocus={() => {
+ if (props.onFocus) {
+ props.onFocus()
+ }
+ }}
+ minWidth="2px"
+ maxLength={48}
+ value={props.inputValue}
+ onClick={(event) => {
+ event.stopPropagation()
+ }}
+ onKeyUp={(event) => {
+ switch (event.key) {
+ case 'Escape':
+ props.clearInputState()
+ break
+ case 'Enter':
+ props.selectOrCreateLabel(props.inputValue)
+ event.preventDefault()
+ break
+ }
+ }}
+ onKeyDown={(event) => {
+ switch (event.key) {
+ case 'Tab':
+ autoComplete()
+ event.preventDefault()
+ break
+ case 'Delete':
+ case 'Backspace':
+ clearTabState()
+ if (props.inputValue.length === 0) {
+ props.deleteLastLabel()
+ event.preventDefault()
+ }
+ break
+ }
+ }}
+ onChange={function (event) {
+ props.setInputValue(event.target.value)
+ }}
+ />
+
+
+ )
+}
diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx
index f399b9c51..402cb7d62 100644
--- a/packages/web/components/templates/article/ArticleActionsMenu.tsx
+++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx
@@ -6,6 +6,7 @@ import {
TagSimple,
Trash,
Tray,
+ Tag,
} from 'phosphor-react'
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
import { Button } from '../../elements/Button'
@@ -79,7 +80,7 @@ export function ArticleActionsMenu(
tooltipSide={props.layout == 'side' ? 'right' : 'bottom'}
>
-
@@ -97,10 +98,7 @@ export function ArticleActionsMenu(
},
}}
>
-
+
)}
@@ -116,7 +114,7 @@ export function ArticleActionsMenu(
},
}}
>
-
+
)
}
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 => {
@@ -273,6 +286,7 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
props.onLabelsUpdated(newSelectedLabels)
}
+ props.clearInputState()
revalidate()
},
[isSelected, props, revalidate]
@@ -284,33 +298,32 @@ 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(
undefined
)
- const createLabelFromFilterText = useCallback(async () => {
- const label = await createLabelMutation(
- filterText,
- randomLabelColorHex(),
- ''
- )
- if (label) {
- showSuccessToast(`Created label ${label.name}`, {
- position: 'bottom-right',
- })
- toggleLabel(label)
- } else {
- showErrorToast('Failed to create label', { position: 'bottom-right' })
- }
- }, [filterText, toggleLabel])
+ const createLabelFromFilterText = useCallback(
+ async (text: string) => {
+ const label = await createLabelMutation(text, randomLabelColorHex(), '')
+ if (label) {
+ showSuccessToast(`Created label ${label.name}`, {
+ position: 'bottom-right',
+ })
+ toggleLabel(label)
+ } else {
+ showErrorToast('Failed to create label', { position: 'bottom-right' })
+ }
+ },
+ [props.inputValue, toggleLabel]
+ )
const handleKeyDown = useCallback(
async (event: React.KeyboardEvent) => {
@@ -325,12 +338,12 @@ 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)
}
- if (event.key === 'ArrowDown' || event.key === 'Tab') {
+ if (event.key === 'ArrowDown') {
event.preventDefault()
let newIndex = focusedIndex
if (focusedIndex === undefined) {
@@ -340,7 +353,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)
@@ -352,7 +365,9 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
return
}
if (focusedIndex === maxIndex - 1) {
- await createLabelFromFilterText()
+ const _filterText = props.inputValue
+ props.setInputValue('')
+ await createLabelFromFilterText(_filterText)
return
}
if (focusedIndex !== undefined) {
@@ -364,7 +379,7 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
}
},
[
- filterText,
+ props.inputValue,
filteredLabels,
focusedIndex,
createLabelFromFilterText,
@@ -385,55 +400,45 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
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}
/>
- {filteredLabels &&
- filteredLabels.map((label, idx) => (
-
- ))}
+ {filteredLabels.map((label, idx) => (
+
+ ))}
- {filterText && (
-
-
-
- {`Create new label "${filterText}"`}
-
-
- )}
-
-
+
)
}
diff --git a/packages/web/components/templates/article/SetLabelsModal.tsx b/packages/web/components/templates/article/SetLabelsModal.tsx
index 2b0828aa4..87e14009f 100644
--- a/packages/web/components/templates/article/SetLabelsModal.tsx
+++ b/packages/web/components/templates/article/SetLabelsModal.tsx
@@ -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,80 +23,166 @@ type SetLabelsModalProps = {
}
export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
- const [previousSelectedLabels, setPreviousSelectedLabels] = useState(
- props.provider.labels ?? []
- )
+ 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 ?? []
)
- const labelsEqual = (left: Label[], right: Label[]) => {
- if (left.length !== right.length) {
- return false
- }
-
- for (const label of left) {
- if (!right.find((r) => label.id == r.id)) {
- return false
- }
- }
-
- return true
+ const containsTemporaryLabel = (labels: Label[]) => {
+ return !!labels.find((l) => '_temporary' in l)
}
const onOpenChange = useCallback(
- async (open: boolean) => {
- // Only make API call if the labels have been modified
- if (!labelsEqual(selectedLabels, previousSelectedLabels)) {
- const result = await props.save(selectedLabels)
- if (props.onLabelsUpdated) {
- props.onLabelsUpdated(selectedLabels)
- }
-
- if (!result) {
- showErrorToast('Error updating labels')
- }
- }
-
- props.onOpenChange(open)
+ (open: boolean) => {
+ ;(async () => {
+ await props.save(selectedLabels)
+ props.onOpenChange(open)
+ })()
},
- [props, selectedLabels, previousSelectedLabels]
+ [props, selectedLabels]
)
- useEffect(() => {
- if (labelsEqual(selectedLabels, previousSelectedLabels)) {
- return
- }
+ const showMessage = useCallback((msg: string) => {
+ console.log('showMessage: ', msg)
+ }, [])
- props
- .save(selectedLabels)
- .then((result) => {
- setPreviousSelectedLabels(result ?? [])
- })
- .catch((err) => {
- console.log('error saving labels: ', err)
- })
- }, [props, selectedLabels, previousSelectedLabels, setPreviousSelectedLabels])
+ 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 () => {
+ await props.save(selectedLabels)
+ })()
+ }
+ }, [props, selectedLabels])
return (
{
event.preventDefault()
onOpenChange(false)
}}
>
-
+
diff --git a/packages/web/components/templates/article/VerticalArticleActions.tsx b/packages/web/components/templates/article/VerticalArticleActions.tsx
index cfdfcdd1e..e9726644f 100644
--- a/packages/web/components/templates/article/VerticalArticleActions.tsx
+++ b/packages/web/components/templates/article/VerticalArticleActions.tsx
@@ -7,6 +7,7 @@ import {
Trash,
Tray,
Notebook,
+ Tag,
} from 'phosphor-react'
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
import { Button } from '../../elements/Button'
@@ -45,7 +46,7 @@ export function VerticalArticleActionsMenu(
alignItems: 'center',
}}
>
-
+
{
const mutation = gql`
- mutation {
- createLabel(
- input: {
- color: "${color}"
- name: "${name}"
- description: "${description}"
- }
- ) {
+ mutation CreateLabel($input: CreateLabelInput!) {
+ createLabel(input: $input) {
... on CreateLabelSuccess {
label {
id
@@ -42,8 +36,13 @@ export async function createLabelMutation(
`
try {
- const data = await gqlFetcher(mutation) as CreateLabelResult
- console.log('created label', data)
+ const data = (await gqlFetcher(mutation, {
+ input: {
+ name,
+ color,
+ description,
+ },
+ })) as CreateLabelResult
return data.errorCodes ? undefined : data.createLabel.label
} catch (error) {
console.log('createLabelMutation error', error)
diff --git a/packages/web/lib/networking/mutations/setLabelsMutation.ts b/packages/web/lib/networking/mutations/setLabelsMutation.ts
index dabdb4ec2..46d842e64 100644
--- a/packages/web/lib/networking/mutations/setLabelsMutation.ts
+++ b/packages/web/lib/networking/mutations/setLabelsMutation.ts
@@ -35,7 +35,6 @@ export async function setLabelsMutation(
const data = (await gqlFetcher(mutation, {
input: { pageId, labelIds },
})) as SetLabelsResult
- console.log(' -- errorCodes', data.errorCodes)
return data.errorCodes ? undefined : data.setLabels.labels
} catch (error) {
console.log(' -- SetLabelsOutput error', error)
diff --git a/packages/web/package.json b/packages/web/package.json
index 23545c72b..5e7657691 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -32,28 +32,31 @@
"@segment/analytics-next": "^1.33.5",
"@sentry/nextjs": "^7.42.0",
"@stitches/react": "^1.2.5",
+ "@types/react-input-autosize": "^2.2.1",
"antd": "4.24.3",
"axios": "^1.2.0",
"color2k": "^2.0.0",
"cookie": "^0.5.0",
"dayjs": "^1.11.7",
"diff-match-patch": "^1.0.5",
- "downshift": "^6.1.9",
"epubjs": "^0.3.93",
"graphql-request": "^3.6.1",
"kbar": "^0.1.0-beta.35",
"loadjs": "^4.3.0-rc1",
"markdown-it": "^13.0.1",
+ "match-sorter": "^6.3.1",
"nanoid": "^3.1.29",
"next": "^12.1.0",
"node-html-markdown": "^1.3.0",
"phosphor-react": "^1.4.0",
"pspdfkit": "^2022.2.3",
"react": "^17.0.2",
+ "react-color": "^2.19.3",
"react-colorful": "^5.5.1",
"react-dom": "^17.0.2",
"react-dropzone": "^14.2.3",
"react-hot-toast": "^2.1.1",
+ "react-input-autosize": "^3.0.0",
"react-markdown": "^8.0.6",
"react-markdown-editor-lite": "^1.3.4",
"react-masonry-css": "^1.0.16",
@@ -86,7 +89,9 @@
"@types/lodash.debounce": "^4.0.6",
"@types/markdown-it": "^12.2.3",
"@types/react": "17.0.2",
+ "@types/react-color": "^3.0.6",
"@types/react-dom": "^17.0.2",
+ "@types/react-input-autosize": "^2.2.1",
"@types/segment-analytics": "^0.0.34",
"@types/uuid": "^8.3.1",
"babel-jest": "^27.4.5",
diff --git a/packages/web/pages/.well-known/security.txt b/packages/web/pages/.well-known/security.txt
new file mode 100644
index 000000000..80a8d9147
--- /dev/null
+++ b/packages/web/pages/.well-known/security.txt
@@ -0,0 +1,4 @@
+Contact: mailto:feedback@omnivore.app
+Expires: 2024-06-01T04:00:00.000Z
+Canonical: https://omnivore.app/.well-known/security.txt
+Policy: https://github.com/omnivore-app/omnivore/blob/main/SECURITY.md
diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx
index 7e4e02dfa..f76a80e48 100644
--- a/packages/web/pages/[username]/[slug]/index.tsx
+++ b/packages/web/pages/[username]/[slug]/index.tsx
@@ -150,7 +150,6 @@ export default function Home(): JSX.Element {
}
break
case 'refreshLabels':
- console.log('refreshing labels: ', arg)
setLabels(arg as Label[])
break
case 'showHighlights':
@@ -252,6 +251,14 @@ export default function Home(): JSX.Element {
name: 'Back to library',
shortcut: ['escape'],
perform: () => {
+ if (
+ readerSettings.showSetLabelsModal ||
+ readerSettings.showDeleteConfirmation ||
+ readerSettings.showDeleteConfirmation ||
+ readerSettings.showEditDisplaySettingsModal
+ ) {
+ return
+ }
const query = window.sessionStorage.getItem('q')
if (query) {
router.push(`/home?${query}`)
@@ -354,7 +361,7 @@ export default function Home(): JSX.Element {
perform: () => setShowEditModal(true),
},
],
- []
+ [readerSettings]
)
if (articleFetchError && articleFetchError.indexOf('NOT_FOUND') > -1) {
diff --git a/packages/web/pages/settings/integrations.tsx b/packages/web/pages/settings/integrations.tsx
index 747233b3e..cf5dd805b 100644
--- a/packages/web/pages/settings/integrations.tsx
+++ b/packages/web/pages/settings/integrations.tsx
@@ -217,91 +217,97 @@ export default function Integrations(): JSX.Element {
top: '5rem',
}}
/>
-
-
- Connect with other applications can help enhance and streamline your
- experience with Omnivore, below are some useful apps to connect your
- Omnivore account to.
-
-
-
- {integrationsArray.map((item) => {
- return (
-
-
-
+
+
+ Connect with other applications can help enhance and streamline your
+ experience with Omnivore, below are some useful apps to connect your
+ Omnivore account to.
+
+
+
+
+ {integrationsArray.map((item) => {
+ return (
+
- {item.title}
- {item.subText}
-
-
-
+
- {item.button.icon}
- {item.title}
+ {item.subText}
+
+
+
- {item.button.text}
-
-
+ {item.button.icon}
+
+ {item.button.text}
+
+
+
-
- )
- })}
+ )
+ })}
+
+
)
diff --git a/packages/web/pages/settings/labels.tsx b/packages/web/pages/settings/labels.tsx
index 85e7649d0..cd82f8488 100644
--- a/packages/web/pages/settings/labels.tsx
+++ b/packages/web/pages/settings/labels.tsx
@@ -24,10 +24,7 @@ import {
Trash,
Plus,
} from 'phosphor-react'
-import {
- GenericTableCardProps,
- LabelColorHex,
-} from '../../utils/settings-page/labels/types'
+import { GenericTableCardProps } from '../../utils/settings-page/labels/types'
import { labelColorObjects } from '../../utils/settings-page/labels/labelColorObjects'
import { TooltipWrapped } from '../../components/elements/Tooltip'
import { LabelColorDropdown } from '../../components/elements/LabelColorDropdown'
@@ -82,6 +79,7 @@ const TableCardBox = styled(Box, {
})
const inputStyles = {
+ height: '35px',
backgroundColor: 'transparent',
color: '$grayTextContrast',
padding: '6px 6px',
@@ -146,10 +144,7 @@ const TextArea = styled('textarea', { ...inputStyles })
export default function LabelsPage(): JSX.Element {
const { labels, revalidate } = useGetLabelsQuery()
- const [labelColorHex, setLabelColorHex] = useState({
- rowId: '',
- value: '#000000',
- })
+ const [labelColorHex, setLabelColorHex] = useState('#000000')
const [editingLabelId, setEditingLabelId] = useState(null)
const [nameInputText, setNameInputText] = useState('')
const [descriptionInputText, setDescriptionInputText] = useState('')
@@ -184,13 +179,13 @@ export default function LabelsPage(): JSX.Element {
setEditingLabelId('')
setNameInputText('')
setDescriptionInputText('')
- setLabelColorHex({ rowId: '', value: '#000000' })
+ setLabelColorHex('#000000')
}
async function createLabel(): Promise {
const res = await createLabelMutation(
nameInputText.trim(),
- labelColorHex.value,
+ labelColorHex,
descriptionInputText
)
if (res) {
@@ -206,7 +201,7 @@ export default function LabelsPage(): JSX.Element {
await updateLabelMutation({
labelId: id,
name: nameInputText,
- color: labelColorHex.value,
+ color: labelColorHex,
description: descriptionInputText,
})
revalidate()
@@ -217,7 +212,7 @@ export default function LabelsPage(): JSX.Element {
setEditingLabelId(label.id)
setNameInputText(label.name)
setDescriptionInputText(label.description || '')
- setLabelColorHex({ rowId: '', value: label.color })
+ setLabelColorHex(label.color)
} else {
resetLabelState()
}
@@ -244,11 +239,7 @@ export default function LabelsPage(): JSX.Element {
) as LabelColor[]
const randomColorHex =
colorHexes[Math.floor(Math.random() * colorHexes.length)]
- setLabelColorHex((prevState) => ({
- ...prevState,
- rowId: rowId || '',
- value: randomColorHex,
- }))
+ setLabelColorHex(randomColorHex)
}
return (
@@ -459,8 +450,7 @@ function GenericTableCard(
resetState,
} = props
const showInput = editingLabelId === label?.id || (isCreateMode && !label)
- const labelColor =
- editingLabelId === label?.id ? labelColorHex.value : label?.color
+ const labelColor = editingLabelId === label?.id ? labelColorHex : label?.color
const iconColor = isDarkTheme() ? '#D8D7D5' : '#5F5E58'
const handleEdit = () => {
@@ -649,11 +639,9 @@ function GenericTableCard(
)}
{showInput && (
@@ -809,7 +797,7 @@ function MobileEditCard(props: any) {
{nameInputText && (
-
+
)}