Merge pull request #2370 from omnivore-app/feat/web-labels-picker
Improved labels picker for the web
This commit is contained in:
62
packages/web/components/elements/EditLabelChip.tsx
Normal file
62
packages/web/components/elements/EditLabelChip.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
205
packages/web/components/elements/LabelsPicker.tsx
Normal file
205
packages/web/components/elements/LabelsPicker.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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)
|
||||
}}
|
||||
|
||||
@ -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={{
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -22,7 +22,7 @@ export const labelFragment = gql`
|
||||
export type Label = {
|
||||
id: string
|
||||
name: string
|
||||
color: LabelColor
|
||||
color: string
|
||||
description?: string
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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",
|
||||
|
||||
4
packages/web/pages/.well-known/security.txt
Normal file
4
packages/web/pages/.well-known/security.txt
Normal 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
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
94
yarn.lock
94
yarn.lock
@ -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"
|
||||
|
||||
Reference in New Issue
Block a user