Merge pull request #2370 from omnivore-app/feat/web-labels-picker

Improved labels picker for the web
This commit is contained in:
Jackson Harper
2023-06-19 22:26:20 +08:00
committed by GitHub
20 changed files with 922 additions and 621 deletions

View File

@ -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 (
<SpanBox
css={{
display: 'inline-table',
margin: '2px',
fontSize: '11px',
fontWeight: '500',
fontFamily: '$inter',
padding: '1px 7px',
whiteSpace: 'nowrap',
cursor: 'pointer',
backgroundClip: 'padding-box',
borderRadius: '5px',
borderWidth: '1px',
borderStyle: 'solid',
color: isDark ? '#EBEBEB' : '#2A2A2A',
borderColor: props.isSelected ? selectedBorder : unSelectedBorder,
backgroundColor: isDark ? '#2A2A2A' : '#F5F5F5',
}}
>
<HStack alignment="center" css={{ gap: '7px' }}>
<Circle size={14} color={props.color} weight="fill" />
<SpanBox css={{ pt: '1px' }}>{props.text}</SpanBox>
<Button
style="ghost"
css={{ display: 'flex', pt: '1px' }}
onClick={(event) => {
props.xAction()
event.preventDefault()
}}
>
<X
size={14}
color={
props.isSelected
? '#FFEA9F'
: theme.colors.thBorderSubtle.toString()
}
/>
</Button>
</HStack>
</SpanBox>
)
}

View File

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

View File

@ -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<HTMLButtonElement | null>(null)
const { isCreateMode, canEdit } = props
const [triangle, setTriangle] = useState<
'top-left' | 'hide' | 'top-right' | undefined
>('top-left')
const [open, setOpen] = useState<boolean | undefined>(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 (
<DropdownMenu onOpenChange={handleOpen} open={open}>
<DropdownMenuTrigger
ref={pickerRef}
css={{
minWidth: '64px',
width: '100%',
@ -132,85 +62,54 @@ export const LabelColorDropdown = (props: LabelColorDropdownProps) => {
minWidth: '170px',
width: 'auto',
},
borderRadius: '6px',
outlineStyle: 'solid',
outlineColor: open ? '$omnivoreYellow' : 'transparent',
}}
>
<MainContainer>
<SpanBox css={{ paddingRight: '$3' }}>
{labelId !== '' && labelId === labelColorHexRowId ? (
<LabelOption
isCreateMode={isCreateMode}
labelId={labelId}
color={labelColorHexValue}
isDropdownOption={false}
/>
) : (
<>
{labelId ? (
<LabelOption
isCreateMode={isCreateMode}
labelId={labelId}
color={labelColor}
isDropdownOption={false}
/>
) : (
<LabelOption
isCreateMode={isCreateMode}
labelId={''}
color={labelColorHexValue}
isDropdownOption={false}
/>
)}
</>
)}
</SpanBox>
<CaretDown size={16} color={iconColor} weight="bold" />
</MainContainer>
<LabelOption
isCreateMode={isCreateMode}
labelId={''}
color={props.labelColor}
isDropdownOption={false}
/>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={5}>
{Object.keys(labelColorObjects)
.filter((labelColor) => labelColor !== 'custom color')
.map((labelColor) => (
<DropdownOption
key={labelColor}
onSelect={() =>
setLabelColorHex({
rowId: labelId,
value: labelColor as LabelColor,
})
}
>
<LabelOption
isCreateMode={isCreateMode}
labelId={labelId}
color={labelColor}
isDropdownOption
/>
</DropdownOption>
))}
<DropdownMenu>
<DropdownMenuTriggerItem>
<CustomLabelWrapper onSelect={() => null}>
<LabelOption
isCreateMode={isCreateMode}
labelId={labelId}
color="custom color"
isDropdownOption
/>
</CustomLabelWrapper>
</DropdownMenuTriggerItem>
<DropdownMenuContent
sideOffset={-25}
alignOffset={-5}
css={{ minWidth: 'unset' }}
>
<HexColorPicker
color={labelColor}
onChange={handleCustomColorChange}
/>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenuContent
align="start"
sideOffset={10}
onKeyUp={(event) => {
switch (event.key) {
case 'Escape':
setOpen(false)
event.preventDefault()
break
case 'Enter':
setOpen(false)
event.preventDefault()
break
}
}}
>
<TwitterPicker
triangle={triangle}
color={props.labelColor}
onChange={(color, event) => {
props.setLabelColor(color.hex.toUpperCase())
event.preventDefault()
}}
onChangeComplete={(color, event) => {
props.setLabelColor(color.hex.toUpperCase())
event.preventDefault()
}}
styles={{
default: {
input: {
color: '$grayText',
},
},
}}
/>
</DropdownMenuContent>
</DropdownMenu>
)
@ -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 (
<HStack
alignment="center"
distribution="start"
css={{ width: '100%', padding: isDropdownOption ? '' : '$2 0' }}
css={{
width: '100%',
height: '35px',
padding: '$2 0',
border: '1px solid $grayBorder',
borderRadius: '6px',
}}
>
<Box
css={{
m: '$1',
m: '10px',
textTransform: 'capitalize',
fontSize: '$3',
whiteSpace: 'nowrap',
}}
>
<LabelColorIcon fillColor={text} strokeColor={border} />
<LabelColorIcon color={props.color} />
</Box>
<StyledText
css={{
m: '$1',
color: '$grayText',
fontSize: '$3',
whiteSpace: 'nowrap',
textTransform: 'capitalize',
'@md': {
display: 'unset',
},
}}
>
{colorNameText}
</StyledText>
<StyledText
css={{
m: '$1',
m: '0px',
color: '$grayText',
fontSize: '$3',
whiteSpace: 'nowrap',
@ -284,9 +150,8 @@ function LabelOption(props: LabelOptionProps): JSX.Element {
},
}}
>
{colorNameText === 'custom color' ? '' : colorHex}
{props.color}
</StyledText>
{isDropdownOption ? <Box css={{ pr: '$2' }} /> : null}
</HStack>
)
}
@ -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 (
<Box
css={{
@ -315,8 +177,8 @@ function LabelColorIcon(props: {
height: '14px',
borderRadius: '50%',
border: '2px solid',
borderColor: props.strokeColor,
backgroundColor: props.fillColor,
borderColor: props.color,
backgroundColor: props.color,
}}
/>
)

View File

@ -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<HTMLInputElement | null>()
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 (
<Box
css={{
display: 'inline-block',
bg: '#3D3D3D',
border: '1px transparent solid',
borderRadius: '6px',
verticalAlign: 'center',
padding: '5px',
lineHeight: '2',
width: '100%',
cursor: 'text',
color: '#EBEBEB',
fontSize: '12px',
fontFamily: '$inter',
input: {
all: 'unset',
left: '0px',
outline: 'none',
borderStyle: 'none',
marginLeft: '2px',
},
'&:focus-within': {
outline: 'none',
border: '1px solid $thLibraryMenuUnselected',
},
'>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) => (
<EditLabelLabelChip
key={label.id}
text={label.name}
color={label.color}
isSelected={
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])
}
}}
/>
))}
<SpanBox
css={{
display: 'inline-flex',
height: '24px',
paddingBottom: '20px',
transform: `translateY(-2px)`,
}}
>
<AutosizeInput
placeholder={isEmpty ? 'Filter for label' : undefined}
inputRef={(ref) => {
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)
}}
/>
</SpanBox>
</Box>
)
}

View File

@ -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'}
>
<SpanBox ref={displaySettingsButtonRef}>
<TagSimple
<Tag
size={24}
color={theme.colors.thHighContrast.toString()}
/>
@ -97,10 +98,7 @@ export function ArticleActionsMenu(
},
}}
>
<TagSimple
size={24}
color={theme.colors.thHighContrast.toString()}
/>
<Tag size={24} color={theme.colors.thHighContrast.toString()} />
</Button>
)}
</SpanBox>
@ -116,7 +114,7 @@ export function ArticleActionsMenu(
},
}}
>
<TagSimple size={24} color={theme.colors.thHighContrast.toString()} />
<Tag size={24} color={theme.colors.thHighContrast.toString()} />
</Button>
<Button
@ -228,14 +226,15 @@ export function ArticleActionsMenu(
onLabelsUpdated={(labels: Label[]) => {
props.articleActionHandler('refreshLabels', labels)
}}
save={(labels: Label[]) => {
save={async (labels: Label[]) => {
if (props.article?.id) {
return (
setLabelsMutation(
const result =
(await setLabelsMutation(
props.article?.id,
labels.map((l) => l.id)
) ?? []
)
)) ?? []
props.article.labels = result
return Promise.resolve(result)
}
return Promise.resolve(labels)
}}

View File

@ -23,6 +23,8 @@ import { Label } from '../../../lib/networking/fragments/labelFragment'
import { Recommendation } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { Avatar } from '../../elements/Avatar'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import Downshift from 'downshift'
import { LabelsPicker } from '../../elements/LabelsPicker'
type ArticleContainerProps = {
viewer: UserBasicData
@ -421,6 +423,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
author={props.article.author}
href={props.article.url}
/>
{labels ? (
<SpanBox
css={{

View File

@ -3,16 +3,15 @@ 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'
import { useRouter } from 'next/router'
import { LabelsPicker } from '../../elements/LabelsPicker'
export interface LabelsProvider {
labels?: Label[]
@ -21,30 +20,49 @@ 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
}
const FormInput = styled('input', {
width: '100%',
fontSize: '16px',
fontFamily: 'inter',
fontWeight: 'normal',
lineHeight: '1.8',
color: '$grayTextContrast',
'&:focus': {
outline: 'none',
},
})
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', {
display: 'flex',
@ -52,18 +70,8 @@ const StyledLabel = styled('label', {
})
function Header(props: HeaderProps): JSX.Element {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (!isTouchScreenDevice() && props.focused && inputRef.current) {
inputRef.current.focus()
}
}, [props.focused])
return (
<VStack
css={{ width: '100%', my: '0px', borderBottom: '1px solid $grayBorder' }}
>
<VStack css={{ width: '100%', my: '0px' }}>
<Box
css={{
width: '100%',
@ -71,33 +79,24 @@ function Header(props: HeaderProps): JSX.Element {
px: '14px',
}}
>
<FormInput
ref={inputRef}
type="text"
tabIndex={props.focused && !isTouchScreenDevice() ? 0 : -1}
autoFocus={!isTouchScreenDevice()}
value={props.filterText}
placeholder="Filter for label"
onChange={(event) => {
props.setFilterText(event.target.value)
}}
<LabelsPicker
focused={props.focused}
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}
onFocus={() => {
props.resetFocusedIndex()
}}
css={{
border: '1px solid $grayBorder',
borderRadius: '8px',
width: '100%',
bg: 'transparent',
fontSize: '16px',
textIndent: '8px',
marginBottom: '2px',
color: '$grayTextContrast',
'&:focus': {
outline: 'none',
boxShadow: '0px 0px 2px 2px rgba(255, 234, 159, 0.56)',
},
}}
clearInputState={props.clearInputState}
deleteLastLabel={props.deleteLastLabel}
selectOrCreateLabel={props.selectOrCreateLabel}
/>
</Box>
</VStack>
@ -127,8 +126,11 @@ function LabelListItem(props: LabelListItemProps): JSX.Element {
css={{
width: '100%',
height: '42px',
borderBottom: '1px solid $grayBorder',
p: '15px',
bg: props.focused ? '$grayBgActive' : 'unset',
'&:focus-visible': {
outline: 'none',
},
}}
tabIndex={props.focused ? 0 : -1}
onClick={(event) => {
@ -144,22 +146,6 @@ function LabelListItem(props: LabelListItemProps): JSX.Element {
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',
@ -183,14 +169,17 @@ function LabelListItem(props: LabelListItemProps): JSX.Element {
<Box
css={{
pl: '10px',
width: '40px',
marginLeft: 'auto',
display: 'flex',
alignItems: 'center',
}}
>
{selected && (
<CrossIcon size={14} strokeColor={theme.colors.grayText.toString()} />
<Check
size={15}
color={theme.colors.grayText.toString()}
weight="bold"
/>
)}
</Box>
</StyledLabel>
@ -199,6 +188,7 @@ function LabelListItem(props: LabelListItemProps): JSX.Element {
type FooterProps = {
focused: boolean
filterText: string
}
function Footer(props: FooterProps): JSX.Element {
@ -228,24 +218,47 @@ function Footer(props: FooterProps): JSX.Element {
},
}}
>
<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>
{props.filterText.length > 0 ? (
<Button
style="modalOption"
css={{
pl: '26px',
position: 'relative',
color: theme.colors.grayText.toString(),
height: '42px',
borderBottom: '1px solid $grayBorder',
bg: props.focused ? '$grayBgActive' : 'unset',
}}
// onClick={createLabelFromFilterText}
>
<HStack alignment="center" distribution="start" css={{ gap: '8px' }}>
<Plus size={18} color={theme.colors.grayText.toString()} />
<SpanBox
css={{ fontSize: '12px' }}
>{`Use Enter to create new label "${props.filterText}"`}</SpanBox>
</HStack>
</Button>
) : (
<SpanBox
css={{
display: 'flex',
fontSize: '12px',
padding: '33px',
gap: '8px',
}}
></SpanBox>
)}
</HStack>
)
}
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<number | undefined>(
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<HTMLInputElement>) => {
@ -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 {
<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"
alignment="start"
css={{
mt: '10px',
flexGrow: '1',
width: '100%',
height: '294px',
height: '200px',
overflowY: 'scroll',
}}
>
{filteredLabels &&
filteredLabels.map((label, idx) => (
<LabelListItem
key={label.id}
label={label}
focused={idx === focusedIndex}
selected={isSelected(label)}
toggleLabel={toggleLabel}
/>
))}
{filteredLabels.map((label, idx) => (
<LabelListItem
key={label.id}
label={label}
focused={idx === focusedIndex}
selected={isSelected(label)}
toggleLabel={toggleLabel}
/>
))}
</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',
}}
onClick={createLabelFromFilterText}
>
<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 focused={focusedIndex === filteredLabels.length + 1} />
<Footer
filterText={props.inputValue}
focused={focusedIndex === filteredLabels.length + 1}
/>
</VStack>
)
}

View File

@ -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 (
<ModalRoot defaultOpen onOpenChange={onOpenChange}>
<ModalOverlay />
<ModalContent
css={{ border: '1px solid $grayBorder' }}
css={{
border: '1px solid $grayBorder',
backgroundColor: '$thBackground',
}}
onPointerDownOutside={(event) => {
event.preventDefault()
onOpenChange(false)
}}
>
<VStack distribution="start" css={{ height: '100%' }}>
<SpanBox css={{ p: '16px', width: '100%' }}>
<SpanBox css={{ pt: '0px', px: '16px', width: '100%' }}>
<ModalTitleBar title="Labels" onOpenChange={onOpenChange} />
</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>

View File

@ -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',
}}
>
<TagSimple size={24} color={theme.colors.thHighContrast.toString()} />
<Tag size={24} color={theme.colors.thHighContrast.toString()} />
</Button>
<Button

View File

@ -22,7 +22,7 @@ export const labelFragment = gql`
export type Label = {
id: string
name: string
color: LabelColor
color: string
description?: string
createdAt: Date
}

View File

@ -17,14 +17,8 @@ export async function createLabelMutation(
description?: string
): Promise<any | undefined> {
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)

View File

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

View File

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

View File

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

View File

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

View File

@ -217,91 +217,97 @@ export default function Integrations(): JSX.Element {
top: '5rem',
}}
/>
<Header css={{ textAlign: 'center', width: '100%' }}>Integrations</Header>
<Subheader>
Connect with other applications can help enhance and streamline your
experience with Omnivore, below are some useful apps to connect your
Omnivore account to.
</Subheader>
<VStack
distribution={'start'}
css={{
width: '80%',
margin: '0 auto',
height: '800px',
'@smDown': {
width: '100%',
},
}}
>
<Header>Applications</Header>
{integrationsArray.map((item) => {
return (
<HStack
key={item.title}
css={{
width: '100%',
borderRadius: '5px',
backgroundColor: '$grayBg',
margin: '10px 0',
padding: '20px',
display: 'flex',
alignItems: 'center',
'@smDown': {
flexWrap: 'wrap',
borderRadius: 'unset',
},
}}
>
<Image
src={item.icon}
alt="integration Image"
width={75}
height={75}
/>
<Box
<VStack css={{ width: '100%', height: '100%' }}>
<Header css={{ textAlign: 'center', width: '100%' }}>
Integrations
</Header>
<Subheader>
Connect with other applications can help enhance and streamline your
experience with Omnivore, below are some useful apps to connect your
Omnivore account to.
</Subheader>
<VStack
distribution={'start'}
css={{
width: '80%',
margin: '0 auto',
height: '800px',
'@smDown': {
width: '100%',
},
}}
>
<Header>Applications</Header>
{integrationsArray.map((item) => {
return (
<HStack
key={item.title}
css={{
'@sm': {
width: '60%',
},
padding: '8px',
color: '$utilityTextDefault',
m: '10px',
'h3, p': {
margin: '0',
width: '100%',
borderRadius: '5px',
backgroundColor: '$grayBg',
margin: '10px 0',
padding: '20px',
display: 'flex',
alignItems: 'center',
'@smDown': {
flexWrap: 'wrap',
borderRadius: 'unset',
},
}}
>
<h3>{item.title}</h3>
<p>{item.subText}</p>
</Box>
<HStack css={{ '@smDown': { width: '100%' } }}>
<Button
style={
item.button.style === 'ctaDarkYellow'
? 'ctaDarkYellow'
: 'ctaWhite'
}
<Image
src={item.icon}
alt="integration Image"
width={75}
height={75}
/>
<Box
css={{
py: '10px',
px: '14px',
minWidth: '230px',
width: '100%',
'@sm': {
width: '60%',
},
padding: '8px',
color: '$utilityTextDefault',
m: '10px',
'h3, p': {
margin: '0',
},
}}
onClick={item.button.action}
>
{item.button.icon}
<SpanBox
css={{ pl: '10px', fontWeight: '600', fontSize: '16px' }}
<h3>{item.title}</h3>
<p>{item.subText}</p>
</Box>
<HStack css={{ '@smDown': { width: '100%' } }}>
<Button
style={
item.button.style === 'ctaDarkYellow'
? 'ctaDarkYellow'
: 'ctaWhite'
}
css={{
py: '10px',
px: '14px',
minWidth: '230px',
width: '100%',
}}
onClick={item.button.action}
>
{item.button.text}
</SpanBox>
</Button>
{item.button.icon}
<SpanBox
css={{ pl: '10px', fontWeight: '600', fontSize: '16px' }}
>
{item.button.text}
</SpanBox>
</Button>
</HStack>
</HStack>
</HStack>
)
})}
)
})}
</VStack>
<Box css={{ height: '400px', width: '100% ' }} />
</VStack>
</SettingsLayout>
)

View File

@ -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<LabelColorHex>({
rowId: '',
value: '#000000',
})
const [labelColorHex, setLabelColorHex] = useState('#000000')
const [editingLabelId, setEditingLabelId] = useState<string | null>(null)
const [nameInputText, setNameInputText] = useState<string>('')
const [descriptionInputText, setDescriptionInputText] = useState<string>('')
@ -184,13 +179,13 @@ export default function LabelsPage(): JSX.Element {
setEditingLabelId('')
setNameInputText('')
setDescriptionInputText('')
setLabelColorHex({ rowId: '', value: '#000000' })
setLabelColorHex('#000000')
}
async function createLabel(): Promise<void> {
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(
<LabelColorDropdown
isCreateMode={isCreateMode && !label}
canEdit={editingLabelId === label?.id}
labelColorHexRowId={labelColorHex.rowId}
labelColorHexValue={labelColorHex.value}
labelColor={labelColorHex}
setLabelColor={setLabelColorHex}
labelId={label?.id || ''}
labelColor={label?.color || '#000000'}
setLabelColorHex={setLabelColorHex}
/>
)}
{showInput && (
@ -809,7 +797,7 @@ function MobileEditCard(props: any) {
<VStack distribution="center" css={{ width: '100%', margin: '8px' }}>
{nameInputText && (
<SpanBox css={{ ml: '-2px', mt: '0px' }}>
<LabelChip color={labelColorHex.value} text={nameInputText} />
<LabelChip color={labelColorHex} text={nameInputText} />
</SpanBox>
)}
<Input
@ -821,11 +809,9 @@ function MobileEditCard(props: any) {
<LabelColorDropdown
isCreateMode={isCreateMode && !label}
canEdit={editingLabelId === label?.id}
labelColorHexRowId={labelColorHex.rowId}
labelColorHexValue={labelColorHex.value}
labelId={label?.id || ''}
labelColor={label?.color || '#000000'}
setLabelColorHex={setLabelColorHex}
setLabelColor={setLabelColorHex}
/>
<TextArea
placeholder="Description (optional)"
@ -900,7 +886,7 @@ function DesktopEditCard(props: any) {
>
{nameInputText && (
<SpanBox css={{ px: '11px', mt: '3px' }}>
<LabelChip color={labelColorHex.value} text={nameInputText} />
<LabelChip color={labelColorHex} text={nameInputText} />
</SpanBox>
)}
<HStack
@ -917,11 +903,9 @@ function DesktopEditCard(props: any) {
<LabelColorDropdown
isCreateMode={isCreateMode && !label}
canEdit={editingLabelId === label?.id}
labelColorHexRowId={labelColorHex.rowId}
labelColorHexValue={labelColorHex.value}
labelId={label?.id || ''}
labelColor={label?.color || '#000000'}
setLabelColorHex={setLabelColorHex}
labelColor={labelColorHex}
setLabelColor={setLabelColorHex}
/>
<Input
type="text"

View File

@ -123,8 +123,9 @@ const CreateActionModal = (props: CreateActionModalProps): JSX.Element => {
}
}
const [actionType, setActionType] =
useState<RuleActionType | undefined>(undefined)
const [actionType, setActionType] = useState<RuleActionType | undefined>(
undefined
)
return (
<Modal
@ -191,8 +192,9 @@ export default function Rules(): JSX.Element {
const { rules, revalidate } = useGetRulesQuery()
const { labels } = useGetLabelsQuery()
const [isCreateRuleModalOpen, setIsCreateRuleModalOpen] = useState(false)
const [createActionRule, setCreateActionRule] =
useState<Rule | undefined>(undefined)
const [createActionRule, setCreateActionRule] = useState<Rule | undefined>(
undefined
)
const dataSource = useMemo(() => {
return rules.map((rule: Rule) => {

View File

@ -1,61 +1,54 @@
import { Label, LabelColor } from "../../../lib/networking/fragments/labelFragment";
import { Label } from '../../../lib/networking/fragments/labelFragment'
export type LabelOptionProps = {
color: string;
isDropdownOption?: boolean;
isCreateMode: boolean | undefined;
labelId: string;
};
color: string
isDropdownOption?: boolean
isCreateMode: boolean | undefined
labelId: string
}
export type ColorDetailsProps = {
colorName: string;
color: string;
icon: JSX.Element;
};
colorName: string
color: string
icon: JSX.Element
}
export type LabelColorObject = {
colorName: string;
text: string;
border: string;
background: string;
};
colorName: string
text: string
border: string
background: string
}
export type LabelColorObjects = {
[key: string]: LabelColorObject;
};
export type LabelColorHex = {
rowId: string;
value: LabelColor;
};
[key: string]: LabelColorObject
}
export type GenericTableCardProps = {
label: Label | null;
editingLabelId: string | null;
labelColorHex: LabelColorHex;
isCreateMode: boolean;
nameInputText: string,
descriptionInputText: string,
isMobileView?: boolean;
handleGenerateRandomColor: (rowId?: string) => void;
setEditingLabelId: (id: string | null) => void;
setLabelColorHex: (color: LabelColorHex) => void;
deleteLabel: (id: string) => void;
setNameInputText: (text: string) => void,
setDescriptionInputText: (text: string) => void,
resetState: () => void,
createLabel: () => void,
updateLabel: (id: string) => void;
setIsCreateMode: (isCreateMode: boolean) => void,
onEditPress: (label: Label | null) => void,
};
label: Label | null
editingLabelId: string | null
labelColorHex: string
isCreateMode: boolean
nameInputText: string
descriptionInputText: string
isMobileView?: boolean
handleGenerateRandomColor: (rowId?: string) => void
setEditingLabelId: (id: string | null) => void
setLabelColorHex: (color: string) => void
deleteLabel: (id: string) => void
setNameInputText: (text: string) => void
setDescriptionInputText: (text: string) => void
resetState: () => void
createLabel: () => void
updateLabel: (id: string) => void
setIsCreateMode: (isCreateMode: boolean) => void
onEditPress: (label: Label | null) => void
}
export type LabelColorDropdownProps = {
isCreateMode: boolean;
canEdit: boolean;
labelColorHexRowId: string;
labelColorHexValue: string;
labelId: string;
labelColor: LabelColor;
setLabelColorHex: (color: LabelColorHex) => void;
};
isCreateMode: boolean
canEdit: boolean
labelId: string
labelColor: string
setLabelColor: (color: string) => void
}

View File

@ -3245,6 +3245,11 @@
resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c"
integrity sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==
"@icons/material@^0.2.4":
version "0.2.4"
resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8"
integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@ -8703,6 +8708,14 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/react-color@^3.0.6":
version "3.0.6"
resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.6.tgz#602fed023802b2424e7cd6ff3594ccd3d5055f9a"
integrity sha512-OzPIO5AyRmLA7PlOyISlgabpYUa3En74LP8mTMa0veCA719SvYQov4WLMsHvCgXP+L+KI9yGhYnqZafVGG0P4w==
dependencies:
"@types/react" "*"
"@types/reactcss" "*"
"@types/react-dom@^17.0.2":
version "17.0.11"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.11.tgz#e1eadc3c5e86bdb5f7684e00274ae228e7bcc466"
@ -8710,6 +8723,13 @@
dependencies:
"@types/react" "*"
"@types/react-input-autosize@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@types/react-input-autosize/-/react-input-autosize-2.2.1.tgz#6a335212e7fce1e1a4da56ae2095c8c5c35fbfe6"
integrity sha512-RxzEjd4gbLAAdLQ92Q68/AC+TfsAKTc4evsArUH1aIShIMqQMIMjsxoSnwyjtbFTO/AGIW/RQI94XSdvOxCz/w==
dependencies:
"@types/react" "*"
"@types/react-syntax-highlighter@11.0.5":
version "11.0.5"
resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.5.tgz#0d546261b4021e1f9d85b50401c0a42acb106087"
@ -8734,6 +8754,13 @@
"@types/prop-types" "*"
csstype "^3.0.2"
"@types/reactcss@*":
version "1.2.6"
resolved "https://registry.yarnpkg.com/@types/reactcss/-/reactcss-1.2.6.tgz#133c1e7e896f2726370d1d5a26bf06a30a038bcc"
integrity sha512-qaIzpCuXNWomGR1Xq8SCFTtF4v8V27Y6f+b9+bzHiv087MylI/nTCqqdChNeWS7tslgROmYB7yeiruWX7WnqNg==
dependencies:
"@types/react" "*"
"@types/request@*":
version "2.48.7"
resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.7.tgz#a962d11a26e0d71d9a9913d96bb806dc4d4c2f19"
@ -13477,17 +13504,6 @@ downshift@^6.0.15:
react-is "^17.0.2"
tslib "^2.3.0"
downshift@^6.1.9:
version "6.1.12"
resolved "https://registry.yarnpkg.com/downshift/-/downshift-6.1.12.tgz#f14476b41a6f6fd080c340bad1ddf449f7143f6f"
integrity sha512-7XB/iaSJVS4T8wGFT3WRXmSF1UlBHAA40DshZtkrIscIN+VC+Lh363skLxFTvJwtNgHxAMDGEHT4xsyQFWL+UA==
dependencies:
"@babel/runtime" "^7.14.8"
compute-scroll-into-view "^1.0.17"
prop-types "^15.7.2"
react-is "^17.0.2"
tslib "^2.3.0"
dset@^3.1.0, dset@^3.1.1, dset@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.2.tgz#89c436ca6450398396dc6538ea00abc0c54cd45a"
@ -19033,7 +19049,7 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lodash-es@^4.17.11:
lodash-es@^4.17.11, lodash-es@^4.17.15:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
@ -19178,7 +19194,7 @@ lodash.uniq@4.5.0, lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0, lodash@~4.17.0:
lodash@^4.0.1, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0, lodash@~4.17.0:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -19572,6 +19588,19 @@ marks-pane@^1.0.9:
resolved "https://registry.yarnpkg.com/marks-pane/-/marks-pane-1.0.9.tgz#c0b5ab813384d8cd81faaeb3bbf3397dc809c1b3"
integrity sha512-Ahs4oeG90tbdPWwAJkAAoHg2lRR8lAs9mZXETNPO9hYg3AkjUJBKi1NQ4aaIQZVGrig7c/3NUV1jANl8rFTeMg==
match-sorter@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.1.tgz#98cc37fda756093424ddf3cbc62bfe9c75b92bda"
integrity sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==
dependencies:
"@babel/runtime" "^7.12.5"
remove-accents "0.4.2"
material-colors@^1.2.1:
version "1.2.6"
resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
@ -22772,7 +22801,7 @@ promzard@^0.3.0:
dependencies:
read "1"
prop-types@^15.0.0, prop-types@^15.6.0, prop-types@^15.8.1:
prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -23649,6 +23678,19 @@ rc@^1.2.8:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-color@^2.19.3:
version "2.19.3"
resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.19.3.tgz#ec6c6b4568312a3c6a18420ab0472e146aa5683d"
integrity sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==
dependencies:
"@icons/material" "^0.2.4"
lodash "^4.17.15"
lodash-es "^4.17.15"
material-colors "^1.2.1"
prop-types "^15.5.10"
reactcss "^1.2.0"
tinycolor2 "^1.4.1"
react-colorful@^5.1.2, react-colorful@^5.5.1:
version "5.5.1"
resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.5.1.tgz#29d9c4e496f2ca784dd2bb5053a3a4340cfaf784"
@ -23733,6 +23775,13 @@ react-hot-toast@^2.1.1:
dependencies:
goober "^2.1.1"
react-input-autosize@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-3.0.0.tgz#6b5898c790d4478d69420b55441fcc31d5c50a85"
integrity sha512-nL9uS7jEs/zu8sqwFE5MAPx6pPkNAriACQ2rGLlqmKr2sPGtN7TXTyDdQt4lbNXVx7Uzadb40x8qotIuru6Rhg==
dependencies:
prop-types "^15.5.8"
react-inspector@^5.1.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-5.1.1.tgz#58476c78fde05d5055646ed8ec02030af42953c8"
@ -23937,6 +23986,13 @@ react@^17.0.2:
loose-envify "^1.1.0"
object-assign "^4.1.1"
reactcss@^1.2.0:
version "1.2.3"
resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd"
integrity sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==
dependencies:
lodash "^4.0.1"
read-cmd-shim@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-2.0.0.tgz#4a50a71d6f0965364938e9038476f7eede3928d9"
@ -24403,6 +24459,11 @@ remedial@^1.0.7:
resolved "https://registry.yarnpkg.com/remedial/-/remedial-1.0.8.tgz#a5e4fd52a0e4956adbaf62da63a5a46a78c578a0"
integrity sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==
remove-accents@0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5"
integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==
remove-trailing-separator@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@ -26444,6 +26505,11 @@ tiny-invariant@^1.2.0:
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9"
integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==
tinycolor2@^1.4.1:
version "1.6.0"
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e"
integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==
title-case@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/title-case/-/title-case-2.1.1.tgz#3e127216da58d2bc5becf137ab91dae3a7cd8faa"