feat(web): update list entry management and UI labels
- Changed label for 'list' to 'Вложенный список' in status badge and list entry management. - Refactored list entry handling to support new 'plaintext' and 'nested' modes for adding entries. - Enhanced user experience by auto-detecting entry types and improving placeholder text. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -76,7 +76,7 @@ const STATUS_LABELS: Record<string, string> = {
|
||||
ip: 'IP',
|
||||
cidr: 'CIDR',
|
||||
hostname: 'Домен',
|
||||
list: 'Список',
|
||||
list: 'Вложенный список',
|
||||
unknown: 'Неизвестно',
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,8 @@ import {
|
||||
import { Skeleton } from '@evofw/ui/components/skeleton'
|
||||
import {
|
||||
isManualListType,
|
||||
parseListEntry,
|
||||
splitListPlaintext,
|
||||
type ListEntryKind,
|
||||
} from '@evofw/shared'
|
||||
|
||||
@@ -91,7 +93,8 @@ function ListDetailPage() {
|
||||
const listQ = useQuery(listQueryOptions(id))
|
||||
|
||||
const [addOpen, setAddOpen] = useState(false)
|
||||
const [addKind, setAddKind] = useState<ListEntryKind>('ip')
|
||||
/** plaintext = auto-detect IP/CIDR/hostname; nested = link another list */
|
||||
const [addMode, setAddMode] = useState<'plaintext' | 'nested'>('plaintext')
|
||||
const [addValue, setAddValue] = useState('')
|
||||
const [addListRef, setAddListRef] = useState('')
|
||||
const [entryFilters, setEntryFilters] = useState<Filter[]>([])
|
||||
@@ -121,7 +124,7 @@ function ListDetailPage() {
|
||||
|
||||
const addEntries = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (addKind === 'list') {
|
||||
if (addMode === 'nested') {
|
||||
if (!addListRef) throw new Error('Выберите список')
|
||||
return apiFetch(`/api/v1/lists/${id}/entries`, {
|
||||
method: 'POST',
|
||||
@@ -132,18 +135,7 @@ function ListDetailPage() {
|
||||
}
|
||||
const text = addValue.trim()
|
||||
if (!text) throw new Error('Введите значение')
|
||||
const lines = text
|
||||
.split(/[\n,;]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
if (lines.length === 1) {
|
||||
return apiFetch(`/api/v1/lists/${id}/entries`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
items: [{ kind: addKind, value: lines[0]! }],
|
||||
}),
|
||||
})
|
||||
}
|
||||
// Backend auto-classifies each token via parseListEntry (ip / cidr / hostname)
|
||||
return apiFetch(`/api/v1/lists/${id}/entries`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ values: [text] }),
|
||||
@@ -153,7 +145,7 @@ function ListDetailPage() {
|
||||
toast.success('Добавлено')
|
||||
setAddValue('')
|
||||
setAddListRef('')
|
||||
setAddKind('ip')
|
||||
setAddMode('plaintext')
|
||||
setAddOpen(false)
|
||||
void qc.invalidateQueries({ queryKey: ['lists'] })
|
||||
},
|
||||
@@ -196,7 +188,7 @@ function ListDetailPage() {
|
||||
{ value: 'ip', label: 'IP' },
|
||||
{ value: 'cidr', label: 'CIDR' },
|
||||
{ value: 'hostname', label: 'Домен' },
|
||||
{ value: 'list', label: 'Список' },
|
||||
{ value: 'list', label: 'Вложенный список' },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -281,8 +273,24 @@ function ListDetailPage() {
|
||||
[manual],
|
||||
)
|
||||
|
||||
const detectedKinds = useMemo(() => {
|
||||
if (addMode !== 'plaintext' || !addValue.trim()) return []
|
||||
const kinds = new Set<Exclude<ListEntryKind, 'list'>>()
|
||||
for (const token of splitListPlaintext(addValue)) {
|
||||
try {
|
||||
const parsed = parseListEntry(token)
|
||||
if (parsed.kind !== 'list') kinds.add(parsed.kind)
|
||||
} catch {
|
||||
/* preview only — backend will reject invalid on submit */
|
||||
}
|
||||
}
|
||||
return [...kinds]
|
||||
}, [addMode, addValue])
|
||||
|
||||
const canAdd =
|
||||
addKind === 'list' ? Boolean(addListRef) : Boolean(addValue.trim())
|
||||
addMode === 'nested'
|
||||
? Boolean(addListRef)
|
||||
: Boolean(addValue.trim())
|
||||
|
||||
if (listQ.isLoading) {
|
||||
return (
|
||||
@@ -481,37 +489,47 @@ function ListDetailPage() {
|
||||
disabled={removeEntry.isPending}
|
||||
/>
|
||||
|
||||
<Sheet open={addOpen} onOpenChange={setAddOpen}>
|
||||
<Sheet
|
||||
open={addOpen}
|
||||
onOpenChange={(open) => {
|
||||
setAddOpen(open)
|
||||
if (!open) {
|
||||
setAddValue('')
|
||||
setAddListRef('')
|
||||
setAddMode('plaintext')
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SheetContent className="flex flex-col gap-0 overflow-hidden sm:max-w-md">
|
||||
<SheetHeader className="shrink-0">
|
||||
<SheetTitle>Добавить запись</SheetTitle>
|
||||
<SheetDescription>
|
||||
Выберите вид и значение. Для IP/CIDR/доменов можно вставить
|
||||
несколько строк сразу.
|
||||
IP, CIDR и домены определяются автоматически. Можно вставить
|
||||
несколько строк сразу. Либо вложите другой список целиком.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<ScrollArea className="flex-1 px-4">
|
||||
<div className="grid auto-rows-min gap-4 py-2 pb-4">
|
||||
<Field>
|
||||
<FieldLabel>Вид</FieldLabel>
|
||||
<FieldLabel>Режим</FieldLabel>
|
||||
<Select
|
||||
value={addKind}
|
||||
value={addMode}
|
||||
onValueChange={(v) => {
|
||||
if (v) setAddKind(v as ListEntryKind)
|
||||
if (v === 'plaintext' || v === 'nested') setAddMode(v)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ip">IP</SelectItem>
|
||||
<SelectItem value="cidr">CIDR / диапазон</SelectItem>
|
||||
<SelectItem value="hostname">Домен</SelectItem>
|
||||
<SelectItem value="list">Список</SelectItem>
|
||||
<SelectItem value="plaintext">
|
||||
Значения (автоопределение)
|
||||
</SelectItem>
|
||||
<SelectItem value="nested">Вложенный список</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
{addKind === 'list' ? (
|
||||
{addMode === 'nested' ? (
|
||||
<Field>
|
||||
<FieldLabel>Список</FieldLabel>
|
||||
<Select
|
||||
@@ -531,6 +549,10 @@ function ListDetailPage() {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Содержимое выбранного списка подтянется в эту политику
|
||||
(ссылка, не копия).
|
||||
</p>
|
||||
</Field>
|
||||
) : (
|
||||
<Field>
|
||||
@@ -539,15 +561,23 @@ function ListDetailPage() {
|
||||
id="entry-value"
|
||||
rows={5}
|
||||
value={addValue}
|
||||
placeholder={
|
||||
addKind === 'ip'
|
||||
? '8.8.8.8'
|
||||
: addKind === 'cidr'
|
||||
? '10.0.0.0/8'
|
||||
: 'bad.example.com'
|
||||
}
|
||||
placeholder={'8.8.8.8\n10.0.0.0/8\nbad.example.com'}
|
||||
onChange={(e) => setAddValue(e.target.value)}
|
||||
/>
|
||||
{detectedKinds.length > 0 ? (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
Определено:
|
||||
</span>
|
||||
{detectedKinds.map((kind) => (
|
||||
<StatusBadge key={kind} status={kind} />
|
||||
))}
|
||||
</div>
|
||||
) : addValue.trim() ? (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Не распознано — проверьте формат перед добавлением.
|
||||
</p>
|
||||
) : null}
|
||||
</Field>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@ export const listEntryKindLabels: Record<ListEntryKind, string> = {
|
||||
ip: 'IP',
|
||||
cidr: 'CIDR',
|
||||
hostname: 'Домен',
|
||||
list: 'Список',
|
||||
list: 'Вложенный список',
|
||||
}
|
||||
|
||||
const IPV4 =
|
||||
|
||||
Reference in New Issue
Block a user