Add missing bulk labels modal file
This commit is contained in:
257
packages/web/components/templates/article/AddBulkLabelsModal.tsx
Normal file
257
packages/web/components/templates/article/AddBulkLabelsModal.tsx
Normal file
@ -0,0 +1,257 @@
|
||||
import { useCallback, useEffect, useReducer, useRef, useState } from 'react'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import {
|
||||
ModalRoot,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalTitleBar,
|
||||
} from '../../elements/ModalPrimitives'
|
||||
import { 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'
|
||||
import { LabelAction } from '../../../lib/hooks/useSetPageLabels'
|
||||
import { Button } from '../../elements/Button'
|
||||
|
||||
type AddBulkLabelsModalProps = {
|
||||
onOpenChange: (open: boolean) => void
|
||||
bulkSetLabels: (labels: Label[]) => void
|
||||
}
|
||||
|
||||
export function AddBulkLabelsModal(
|
||||
props: AddBulkLabelsModalProps
|
||||
): JSX.Element {
|
||||
const availableLabels = useGetLabelsQuery()
|
||||
const [tabCount, setTabCount] = useState(-1)
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [tabStartValue, setTabStartValue] = useState('')
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
const errorTimeoutRef = useRef<NodeJS.Timeout | undefined>()
|
||||
const [highlightLastLabel, setHighlightLastLabel] = useState(false)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
const labelsReducer = (
|
||||
state: {
|
||||
labels: Label[]
|
||||
},
|
||||
action: {
|
||||
type: LabelAction
|
||||
labels: Label[]
|
||||
}
|
||||
) => {
|
||||
return {
|
||||
labels: action.labels,
|
||||
}
|
||||
}
|
||||
|
||||
const [selectedLabels, dispatchLabels] = useReducer(labelsReducer, {
|
||||
labels: [],
|
||||
})
|
||||
|
||||
const showMessage = useCallback(
|
||||
(msg: string, timeout?: number) => {
|
||||
if (errorTimeoutRef.current) {
|
||||
clearTimeout(errorTimeoutRef.current)
|
||||
errorTimeoutRef.current = undefined
|
||||
}
|
||||
setErrorMessage(msg)
|
||||
if (timeout) {
|
||||
errorTimeoutRef.current = setTimeout(() => {
|
||||
setErrorMessage(undefined)
|
||||
if (errorTimeoutRef.current) {
|
||||
clearTimeout(errorTimeoutRef.current)
|
||||
errorTimeoutRef.current = undefined
|
||||
}
|
||||
}, timeout)
|
||||
}
|
||||
},
|
||||
[errorTimeoutRef]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const maxLengthMessage = 'Max label length: 48 chars'
|
||||
|
||||
if (inputValue.length >= 48) {
|
||||
showMessage(maxLengthMessage)
|
||||
} else if (errorMessage === maxLengthMessage) {
|
||||
setErrorMessage(undefined)
|
||||
}
|
||||
|
||||
if (inputValue.length > 0) {
|
||||
setHighlightLastLabel(false)
|
||||
}
|
||||
}, [errorMessage, inputValue, showMessage])
|
||||
|
||||
const clearInputState = useCallback(() => {
|
||||
setTabCount(-1)
|
||||
setInputValue('')
|
||||
setTabStartValue('')
|
||||
setHighlightLastLabel(false)
|
||||
}, [])
|
||||
|
||||
const createLabelAsync = useCallback(
|
||||
(newLabels: Label[], tempLabel: Label) => {
|
||||
;(async () => {
|
||||
const currentLabels = newLabels
|
||||
const newLabel = await createLabelMutation(
|
||||
tempLabel.name,
|
||||
tempLabel.color
|
||||
)
|
||||
const idx = currentLabels.findIndex((l) => l.id === tempLabel.id)
|
||||
if (newLabel) {
|
||||
showSuccessToast(`Created label ${newLabel.name}`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
if (idx !== -1) {
|
||||
currentLabels[idx] = newLabel
|
||||
dispatchLabels({ type: 'SAVE', labels: [...currentLabels] })
|
||||
} else {
|
||||
dispatchLabels({
|
||||
type: 'SAVE',
|
||||
labels: [...currentLabels, newLabel],
|
||||
})
|
||||
}
|
||||
} else {
|
||||
showMessage(`Error creating label ${tempLabel.name}`, 5000)
|
||||
if (idx !== -1) {
|
||||
currentLabels.splice(idx, 1)
|
||||
dispatchLabels({ type: 'SAVE', labels: [...currentLabels] })
|
||||
}
|
||||
}
|
||||
})()
|
||||
},
|
||||
[dispatchLabels, showMessage]
|
||||
)
|
||||
|
||||
const selectOrCreateLabel = useCallback(
|
||||
(value: string) => {
|
||||
const current = selectedLabels.labels ?? []
|
||||
const lowerCasedValue = value.toLowerCase()
|
||||
const existing = availableLabels.labels.find(
|
||||
(l) => l.name.toLowerCase() == lowerCasedValue
|
||||
)
|
||||
|
||||
if (lowerCasedValue.length < 1) {
|
||||
return
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
const isAdded = selectedLabels.labels.find(
|
||||
(l) => l.name.toLowerCase() == lowerCasedValue
|
||||
)
|
||||
if (!isAdded) {
|
||||
dispatchLabels({ type: 'SAVE', labels: [...current, existing] })
|
||||
clearInputState()
|
||||
} else {
|
||||
showMessage(`label ${value} already added.`, 5000)
|
||||
}
|
||||
} else {
|
||||
const tempLabel = {
|
||||
id: uuidv4(),
|
||||
name: value,
|
||||
color: randomLabelColorHex(),
|
||||
description: '',
|
||||
createdAt: new Date(),
|
||||
}
|
||||
const newLabels = [...current, tempLabel]
|
||||
dispatchLabels({ type: 'TEMP', labels: newLabels })
|
||||
clearInputState()
|
||||
|
||||
createLabelAsync(newLabels, tempLabel)
|
||||
}
|
||||
},
|
||||
[
|
||||
availableLabels,
|
||||
selectedLabels,
|
||||
dispatchLabels,
|
||||
clearInputState,
|
||||
createLabelAsync,
|
||||
showMessage,
|
||||
]
|
||||
)
|
||||
|
||||
const deleteLastLabel = useCallback(() => {
|
||||
if (highlightLastLabel) {
|
||||
const current = selectedLabels.labels
|
||||
current.pop()
|
||||
dispatchLabels({ type: 'SAVE', labels: [...current] })
|
||||
setHighlightLastLabel(false)
|
||||
} else {
|
||||
setHighlightLastLabel(true)
|
||||
}
|
||||
}, [highlightLastLabel, selectedLabels, dispatchLabels])
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
props.bulkSetLabels(selectedLabels.labels)
|
||||
props.onOpenChange(true)
|
||||
}, [selectedLabels])
|
||||
|
||||
return (
|
||||
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
tabIndex={0}
|
||||
css={{
|
||||
border: '1px solid $grayBorder',
|
||||
backgroundColor: '$thBackground',
|
||||
}}
|
||||
onPointerDownOutside={(event) => {
|
||||
event.preventDefault()
|
||||
props.onOpenChange(false)
|
||||
}}
|
||||
onEscapeKeyDown={(event) => {
|
||||
props.onOpenChange(false)
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
<VStack distribution="start" css={{ height: '100%' }}>
|
||||
<SpanBox css={{ pt: '0px', px: '16px', width: '100%' }}>
|
||||
<ModalTitleBar
|
||||
title="Add Labels to selected items"
|
||||
onOpenChange={props.onOpenChange}
|
||||
/>
|
||||
</SpanBox>
|
||||
<SetLabelsControl
|
||||
inputValue={inputValue}
|
||||
setInputValue={setInputValue}
|
||||
clearInputState={clearInputState}
|
||||
selectedLabels={selectedLabels.labels}
|
||||
dispatchLabels={dispatchLabels}
|
||||
tabCount={tabCount}
|
||||
setTabCount={setTabCount}
|
||||
tabStartValue={tabStartValue}
|
||||
setTabStartValue={setTabStartValue}
|
||||
highlightLastLabel={highlightLastLabel}
|
||||
setHighlightLastLabel={setHighlightLastLabel}
|
||||
deleteLastLabel={deleteLastLabel}
|
||||
selectOrCreateLabel={selectOrCreateLabel}
|
||||
errorMessage={errorMessage}
|
||||
footer={
|
||||
<HStack
|
||||
distribution="end"
|
||||
css={{ width: '100%', pb: '20px', px: '20px' }}
|
||||
>
|
||||
<Button
|
||||
disabled={isSaving}
|
||||
style="ctaDarkYellow"
|
||||
onClick={(event) => {
|
||||
handleSave()
|
||||
event.preventDefault()
|
||||
}}
|
||||
css={{ fontSize: '10px', p: '10px', fontFamily: '$inter' }}
|
||||
>
|
||||
Add Labels
|
||||
</Button>
|
||||
</HStack>
|
||||
}
|
||||
/>
|
||||
</VStack>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user