fix: unify status dropdown and make color swatch visible (#128)
Bug 1: Replace plain-text Radix Select in AddStatusInput with the same StatusDropdown component used for existing status properties. Both now show colored pills, search, and custom status creation. Bug 2: Add color swatch dot next to each status option in the dropdown. Clicking it opens an inline palette of 8 accent colors that persist via localStorage (leveraging existing setStatusColor infrastructure). Refactor StatusDropdown to extract useStatusFiltering, useStatusKeyboard hooks and VaultSection/SuggestedSection/CreateSection sub-components to keep Code Health above 9.2 threshold. Co-authored-by: Test <test@test.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,6 @@ import {
|
||||
detectPropertyType,
|
||||
} from '../utils/propertyTypes'
|
||||
import { StatusPill, StatusDropdown } from './StatusDropdown'
|
||||
import { SUGGESTED_STATUSES } from '../utils/statusStyles'
|
||||
|
||||
// Keys that are relationships (contain wikilinks)
|
||||
export const RELATIONSHIP_KEYS = new Set([
|
||||
@@ -286,23 +285,25 @@ function AddDateInput({ value, onChange }: { value: string; onChange: (v: string
|
||||
}
|
||||
|
||||
function AddStatusInput({ value, onChange, vaultStatuses }: { value: string; onChange: (v: string) => void; vaultStatuses: string[] }) {
|
||||
const allStatuses = [...new Set([...vaultStatuses, ...SUGGESTED_STATUSES])]
|
||||
const [showDropdown, setShowDropdown] = useState(false)
|
||||
return (
|
||||
<Select value={value || undefined} onValueChange={onChange}>
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className="h-[26px] min-w-0 flex-1 gap-1 border-border bg-muted px-1.5 py-0 shadow-none"
|
||||
style={{ fontSize: 12, borderRadius: 4 }}
|
||||
<span className="relative inline-flex min-w-0 flex-1 items-center">
|
||||
<button
|
||||
className="inline-flex h-[26px] min-w-0 flex-1 cursor-pointer items-center gap-1 rounded border border-border bg-muted px-1.5 text-[12px] transition-colors hover:bg-accent"
|
||||
onClick={() => setShowDropdown(true)}
|
||||
data-testid="add-property-status-trigger"
|
||||
>
|
||||
<SelectValue placeholder="Status\u2026" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{allStatuses.map(s => (
|
||||
<SelectItem key={s} value={s}>{s}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{value ? <StatusPill status={value} /> : <span className="text-muted-foreground">Status{'\u2026'}</span>}
|
||||
</button>
|
||||
{showDropdown && (
|
||||
<StatusDropdown
|
||||
value={value}
|
||||
vaultStatuses={vaultStatuses}
|
||||
onSave={(v) => { onChange(v); setShowDropdown(false) }}
|
||||
onCancel={() => setShowDropdown(false)}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
|
||||
import { getStatusStyle, SUGGESTED_STATUSES } from '../utils/statusStyles'
|
||||
import { getStatusStyle, SUGGESTED_STATUSES, setStatusColor, getStatusColorKey } from '../utils/statusStyles'
|
||||
import { ACCENT_COLORS } from '../utils/typeColors'
|
||||
|
||||
export function StatusPill({ status, className }: { status: string; className?: string }) {
|
||||
const style = getStatusStyle(status)
|
||||
@@ -25,33 +26,193 @@ export function StatusPill({ status, className }: { status: string; className?:
|
||||
)
|
||||
}
|
||||
|
||||
function ColorPickerRow({ status, onColorChange }: { status: string; onColorChange: (status: string, colorKey: string) => void }) {
|
||||
const currentKey = getStatusColorKey(status)
|
||||
return (
|
||||
<div className="flex items-center gap-1 px-3 py-1.5" data-testid={`color-picker-${status}`}>
|
||||
{ACCENT_COLORS.map(c => (
|
||||
<button
|
||||
key={c.key}
|
||||
className="flex size-4 shrink-0 items-center justify-center rounded-full border-none p-0 transition-transform hover:scale-125"
|
||||
style={{ backgroundColor: c.css }}
|
||||
onClick={(e) => { e.stopPropagation(); onColorChange(status, c.key) }}
|
||||
title={c.label}
|
||||
data-testid={`color-option-${c.key}`}
|
||||
>
|
||||
{currentKey === c.key && (
|
||||
<span style={{ color: 'white', fontSize: 8, lineHeight: 1 }}>{'\u2713'}</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StatusOption({
|
||||
status,
|
||||
highlighted,
|
||||
onSelect,
|
||||
onMouseEnter,
|
||||
colorEditing,
|
||||
onToggleColor,
|
||||
onColorChange,
|
||||
}: {
|
||||
status: string
|
||||
highlighted: boolean
|
||||
onSelect: (status: string) => void
|
||||
onMouseEnter: () => void
|
||||
colorEditing: boolean
|
||||
onToggleColor: (status: string) => void
|
||||
onColorChange: (status: string, colorKey: string) => void
|
||||
}) {
|
||||
const style = getStatusStyle(status)
|
||||
return (
|
||||
<button
|
||||
className="flex w-full items-center border-none bg-transparent px-2 py-1 text-left transition-colors"
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
backgroundColor: highlighted ? 'var(--muted)' : 'transparent',
|
||||
}}
|
||||
onClick={() => onSelect(status)}
|
||||
onMouseEnter={onMouseEnter}
|
||||
data-testid={`status-option-${status}`}
|
||||
>
|
||||
<StatusPill status={status} />
|
||||
</button>
|
||||
<>
|
||||
<div
|
||||
className="flex w-full items-center gap-1 px-2 py-1 transition-colors"
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
backgroundColor: highlighted ? 'var(--muted)' : 'transparent',
|
||||
}}
|
||||
onMouseEnter={onMouseEnter}
|
||||
>
|
||||
<button
|
||||
className="flex min-w-0 flex-1 items-center border-none bg-transparent p-0 text-left"
|
||||
onClick={() => onSelect(status)}
|
||||
data-testid={`status-option-${status}`}
|
||||
>
|
||||
<StatusPill status={status} />
|
||||
</button>
|
||||
<button
|
||||
className="flex size-4 shrink-0 items-center justify-center rounded-full border-none p-0"
|
||||
style={{ backgroundColor: style.color }}
|
||||
onClick={() => onToggleColor(status)}
|
||||
title="Change color"
|
||||
data-testid={`status-color-swatch-${status}`}
|
||||
/>
|
||||
</div>
|
||||
{colorEditing && <ColorPickerRow status={status} onColorChange={onColorChange} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const SECTION_LABEL_STYLE = {
|
||||
fontFamily: "'IBM Plex Mono', monospace",
|
||||
fontSize: 9,
|
||||
fontWeight: 500,
|
||||
letterSpacing: '1.2px',
|
||||
textTransform: 'uppercase' as const,
|
||||
}
|
||||
|
||||
function SectionLabel({ children }: { children: string }) {
|
||||
return (
|
||||
<div className="px-2 py-1">
|
||||
<span className="text-muted-foreground" style={SECTION_LABEL_STYLE}>{children}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface StatusOptionProps {
|
||||
highlightOffset: number
|
||||
highlightIndex: number
|
||||
colorEditingStatus: string | null
|
||||
onSelect: (status: string) => void
|
||||
onHighlight: (index: number) => void
|
||||
onToggleColor: (status: string) => void
|
||||
onColorChange: (status: string, colorKey: string) => void
|
||||
}
|
||||
|
||||
function StatusOptionList({ statuses, ...props }: StatusOptionProps & { statuses: string[] }) {
|
||||
return (
|
||||
<>
|
||||
{statuses.map((status, i) => (
|
||||
<StatusOption
|
||||
key={status}
|
||||
status={status}
|
||||
highlighted={props.highlightIndex === props.highlightOffset + i}
|
||||
onSelect={props.onSelect}
|
||||
onMouseEnter={() => props.onHighlight(props.highlightOffset + i)}
|
||||
colorEditing={props.colorEditingStatus === status}
|
||||
onToggleColor={props.onToggleColor}
|
||||
onColorChange={props.onColorChange}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function useStatusFiltering(query: string, vaultStatuses: string[]) {
|
||||
return useMemo(() => {
|
||||
const lowerQuery = query.toLowerCase()
|
||||
const vaultSet = new Set(vaultStatuses.map(s => s.toLowerCase()))
|
||||
const suggested = SUGGESTED_STATUSES.filter(
|
||||
s => s.toLowerCase().includes(lowerQuery) && !vaultSet.has(s.toLowerCase()),
|
||||
)
|
||||
const vault = vaultStatuses.filter(s => s.toLowerCase().includes(lowerQuery))
|
||||
return { suggestedFiltered: suggested, vaultFiltered: vault, allFiltered: [...vault, ...suggested] }
|
||||
}, [query, vaultStatuses])
|
||||
}
|
||||
|
||||
interface KeyboardNavOptions {
|
||||
allFiltered: string[]
|
||||
totalOptions: number
|
||||
showCreateOption: boolean
|
||||
query: string
|
||||
onSave: (v: string) => void
|
||||
onCancel: () => void
|
||||
listRef: React.RefObject<HTMLDivElement | null>
|
||||
}
|
||||
|
||||
function useStatusKeyboard(opts: KeyboardNavOptions) {
|
||||
const { allFiltered, totalOptions, showCreateOption, query, onSave, onCancel, listRef } = opts
|
||||
const [highlightIndex, setHighlightIndex] = useState(-1)
|
||||
|
||||
const scrollIntoView = useCallback((index: number) => {
|
||||
const list = listRef.current
|
||||
if (!list) return
|
||||
const items = list.querySelectorAll('[data-testid^="status-option-"], [data-testid="status-create-option"]')
|
||||
items[index]?.scrollIntoView({ block: 'nearest' })
|
||||
}, [listRef])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case 'ArrowDown': {
|
||||
e.preventDefault()
|
||||
const next = highlightIndex < totalOptions - 1 ? highlightIndex + 1 : 0
|
||||
setHighlightIndex(next)
|
||||
scrollIntoView(next)
|
||||
break
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
e.preventDefault()
|
||||
const prev = highlightIndex > 0 ? highlightIndex - 1 : totalOptions - 1
|
||||
setHighlightIndex(prev)
|
||||
scrollIntoView(prev)
|
||||
break
|
||||
}
|
||||
case 'Enter': {
|
||||
e.preventDefault()
|
||||
const trimmed = query.trim()
|
||||
if (highlightIndex >= 0 && highlightIndex < allFiltered.length) onSave(allFiltered[highlightIndex])
|
||||
else if (showCreateOption && highlightIndex === allFiltered.length) onSave(trimmed)
|
||||
else if (trimmed) onSave(trimmed)
|
||||
break
|
||||
}
|
||||
case 'Escape':
|
||||
e.preventDefault()
|
||||
onCancel()
|
||||
break
|
||||
}
|
||||
},
|
||||
[highlightIndex, totalOptions, allFiltered, showCreateOption, query, onSave, onCancel, scrollIntoView],
|
||||
)
|
||||
|
||||
const resetHighlight = useCallback(() => setHighlightIndex(-1), [])
|
||||
|
||||
return { highlightIndex, setHighlightIndex, handleKeyDown, resetHighlight }
|
||||
}
|
||||
|
||||
export function StatusDropdown({
|
||||
vaultStatuses,
|
||||
onSave,
|
||||
@@ -63,181 +224,83 @@ export function StatusDropdown({
|
||||
onCancel: () => void
|
||||
}) {
|
||||
const [query, setQuery] = useState('')
|
||||
const [highlightIndex, setHighlightIndex] = useState(-1)
|
||||
const [colorEditingStatus, setColorEditingStatus] = useState<string | null>(null)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const listRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus()
|
||||
}, [])
|
||||
useEffect(() => { inputRef.current?.focus() }, [])
|
||||
|
||||
const { suggestedFiltered, vaultFiltered, allFiltered } = useMemo(() => {
|
||||
const lowerQuery = query.toLowerCase()
|
||||
const vaultSet = new Set(vaultStatuses.map(s => s.toLowerCase()))
|
||||
|
||||
const suggested = SUGGESTED_STATUSES.filter(
|
||||
s => s.toLowerCase().includes(lowerQuery) && !vaultSet.has(s.toLowerCase()),
|
||||
)
|
||||
|
||||
const vault = vaultStatuses.filter(s => s.toLowerCase().includes(lowerQuery))
|
||||
|
||||
return {
|
||||
suggestedFiltered: suggested,
|
||||
vaultFiltered: vault,
|
||||
allFiltered: [...vault, ...suggested],
|
||||
}
|
||||
}, [query, vaultStatuses])
|
||||
const { suggestedFiltered, vaultFiltered, allFiltered } = useStatusFiltering(query, vaultStatuses)
|
||||
|
||||
const showCreateOption = useMemo(() => {
|
||||
if (!query.trim()) return false
|
||||
const lowerQuery = query.trim().toLowerCase()
|
||||
return !allFiltered.some(s => s.toLowerCase() === lowerQuery)
|
||||
return !allFiltered.some(s => s.toLowerCase() === query.trim().toLowerCase())
|
||||
}, [query, allFiltered])
|
||||
|
||||
const totalOptions = allFiltered.length + (showCreateOption ? 1 : 0)
|
||||
|
||||
const scrollHighlightedIntoView = useCallback((index: number) => {
|
||||
const list = listRef.current
|
||||
if (!list) return
|
||||
const items = list.querySelectorAll('[data-testid^="status-option-"], [data-testid="status-create-option"]')
|
||||
items[index]?.scrollIntoView({ block: 'nearest' })
|
||||
const { highlightIndex, setHighlightIndex, handleKeyDown, resetHighlight } =
|
||||
useStatusKeyboard({ allFiltered, totalOptions, showCreateOption, query, onSave, onCancel, listRef })
|
||||
|
||||
const handleToggleColor = useCallback((status: string) => {
|
||||
setColorEditingStatus(prev => prev === status ? null : status)
|
||||
}, [])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
const next = highlightIndex < totalOptions - 1 ? highlightIndex + 1 : 0
|
||||
setHighlightIndex(next)
|
||||
scrollHighlightedIntoView(next)
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
const prev = highlightIndex > 0 ? highlightIndex - 1 : totalOptions - 1
|
||||
setHighlightIndex(prev)
|
||||
scrollHighlightedIntoView(prev)
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
if (highlightIndex >= 0 && highlightIndex < allFiltered.length) {
|
||||
onSave(allFiltered[highlightIndex])
|
||||
} else if (showCreateOption && highlightIndex === allFiltered.length) {
|
||||
onSave(query.trim())
|
||||
} else if (query.trim()) {
|
||||
onSave(query.trim())
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
onCancel()
|
||||
}
|
||||
},
|
||||
[highlightIndex, totalOptions, allFiltered, showCreateOption, query, onSave, onCancel, scrollHighlightedIntoView],
|
||||
)
|
||||
const handleColorChange = useCallback((status: string, colorKey: string) => {
|
||||
const currentKey = getStatusColorKey(status)
|
||||
setStatusColor(status, currentKey === colorKey ? null : colorKey)
|
||||
setColorEditingStatus(null)
|
||||
}, [])
|
||||
|
||||
const handleQueryChange = useCallback((value: string) => {
|
||||
setQuery(value)
|
||||
resetHighlight()
|
||||
}, [resetHighlight])
|
||||
|
||||
const optionProps: StatusOptionProps = {
|
||||
highlightOffset: 0,
|
||||
highlightIndex,
|
||||
colorEditingStatus,
|
||||
onSelect: onSave,
|
||||
onHighlight: setHighlightIndex,
|
||||
onToggleColor: handleToggleColor,
|
||||
onColorChange: handleColorChange,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative" data-testid="status-dropdown">
|
||||
{/* Backdrop to close on outside click */}
|
||||
<div className="fixed inset-0 z-[12000]" onClick={onCancel} data-testid="status-dropdown-backdrop" />
|
||||
|
||||
<div
|
||||
className="absolute right-0 top-full z-[12001] mt-1 w-52 overflow-hidden rounded-lg border border-border bg-background shadow-lg"
|
||||
data-testid="status-dropdown-popover"
|
||||
>
|
||||
{/* Search input */}
|
||||
<div className="border-b border-border px-2 py-1.5">
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="w-full border-none bg-transparent text-[12px] text-foreground outline-none placeholder:text-muted-foreground"
|
||||
placeholder="Type a status..."
|
||||
value={query}
|
||||
onChange={e => {
|
||||
setQuery(e.target.value)
|
||||
setHighlightIndex(-1)
|
||||
}}
|
||||
onChange={e => handleQueryChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
data-testid="status-search-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Options list */}
|
||||
<div ref={listRef} className="max-h-52 overflow-y-auto py-1">
|
||||
{vaultFiltered.length > 0 && (
|
||||
<div>
|
||||
<div className="px-2 py-1">
|
||||
<span
|
||||
className="text-muted-foreground"
|
||||
style={{
|
||||
fontFamily: "'IBM Plex Mono', monospace",
|
||||
fontSize: 9,
|
||||
fontWeight: 500,
|
||||
letterSpacing: '1.2px',
|
||||
textTransform: 'uppercase' as const,
|
||||
}}
|
||||
>
|
||||
From vault
|
||||
</span>
|
||||
</div>
|
||||
{vaultFiltered.map((status, i) => (
|
||||
<StatusOption
|
||||
key={status}
|
||||
status={status}
|
||||
highlighted={highlightIndex === i}
|
||||
onSelect={onSave}
|
||||
onMouseEnter={() => setHighlightIndex(i)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{suggestedFiltered.length > 0 && (
|
||||
<div>
|
||||
{vaultFiltered.length > 0 && (
|
||||
<div className="my-1 h-px bg-border" />
|
||||
)}
|
||||
<div className="px-2 py-1">
|
||||
<span
|
||||
className="text-muted-foreground"
|
||||
style={{
|
||||
fontFamily: "'IBM Plex Mono', monospace",
|
||||
fontSize: 9,
|
||||
fontWeight: 500,
|
||||
letterSpacing: '1.2px',
|
||||
textTransform: 'uppercase' as const,
|
||||
}}
|
||||
>
|
||||
Suggested
|
||||
</span>
|
||||
</div>
|
||||
{suggestedFiltered.map((status, i) => (
|
||||
<StatusOption
|
||||
key={status}
|
||||
status={status}
|
||||
highlighted={highlightIndex === vaultFiltered.length + i}
|
||||
onSelect={onSave}
|
||||
onMouseEnter={() => setHighlightIndex(vaultFiltered.length + i)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showCreateOption && (
|
||||
<>
|
||||
{allFiltered.length > 0 && <div className="my-1 h-px bg-border" />}
|
||||
<button
|
||||
className="flex w-full items-center gap-1.5 border-none bg-transparent px-2 py-1 text-left text-[11px] transition-colors"
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
backgroundColor:
|
||||
highlightIndex === allFiltered.length ? 'var(--muted)' : 'transparent',
|
||||
color: 'var(--muted-foreground)',
|
||||
}}
|
||||
onClick={() => onSave(query.trim())}
|
||||
onMouseEnter={() => setHighlightIndex(allFiltered.length)}
|
||||
data-testid="status-create-option"
|
||||
>
|
||||
Create <StatusPill status={query.trim()} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<VaultSection statuses={vaultFiltered} {...optionProps} />
|
||||
<SuggestedSection
|
||||
statuses={suggestedFiltered}
|
||||
showDivider={vaultFiltered.length > 0}
|
||||
{...optionProps}
|
||||
highlightOffset={vaultFiltered.length}
|
||||
/>
|
||||
<CreateSection
|
||||
show={showCreateOption}
|
||||
query={query}
|
||||
showDivider={allFiltered.length > 0}
|
||||
highlighted={highlightIndex === allFiltered.length}
|
||||
onSave={onSave}
|
||||
onMouseEnter={() => setHighlightIndex(allFiltered.length)}
|
||||
/>
|
||||
{allFiltered.length === 0 && !showCreateOption && (
|
||||
<div className="px-2 py-2 text-center text-[11px] text-muted-foreground">
|
||||
No matching statuses
|
||||
@@ -248,3 +311,50 @@ export function StatusDropdown({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function VaultSection({ statuses, ...props }: StatusOptionProps & { statuses: string[] }) {
|
||||
if (statuses.length === 0) return null
|
||||
return (
|
||||
<div>
|
||||
<SectionLabel>From vault</SectionLabel>
|
||||
<StatusOptionList statuses={statuses} {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SuggestedSection({ statuses, showDivider, ...props }: StatusOptionProps & { statuses: string[]; showDivider: boolean }) {
|
||||
if (statuses.length === 0) return null
|
||||
return (
|
||||
<div>
|
||||
{showDivider && <div className="my-1 h-px bg-border" />}
|
||||
<SectionLabel>Suggested</SectionLabel>
|
||||
<StatusOptionList statuses={statuses} {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CreateSection({ show, query, showDivider, highlighted, onSave, onMouseEnter }: {
|
||||
show: boolean; query: string; showDivider: boolean; highlighted: boolean
|
||||
onSave: (v: string) => void; onMouseEnter: () => void
|
||||
}) {
|
||||
if (!show) return null
|
||||
const trimmed = query.trim()
|
||||
return (
|
||||
<>
|
||||
{showDivider && <div className="my-1 h-px bg-border" />}
|
||||
<button
|
||||
className="flex w-full items-center gap-1.5 border-none bg-transparent px-2 py-1 text-left text-[11px] transition-colors"
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
backgroundColor: highlighted ? 'var(--muted)' : 'transparent',
|
||||
color: 'var(--muted-foreground)',
|
||||
}}
|
||||
onClick={() => onSave(trimmed)}
|
||||
onMouseEnter={onMouseEnter}
|
||||
data-testid="status-create-option"
|
||||
>
|
||||
Create <StatusPill status={trimmed} />
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user