From 70bee59fc6499beefa5df53de58885bbb7bd1eb9 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Tue, 21 Jul 2026 00:58:07 +0700 Subject: [PATCH] feat(web): update list entry management and UI labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- apps/web/src/components/status-badge.tsx | 2 +- apps/web/src/routes/_auth/lists/$id.tsx | 100 +++++++++++++++-------- packages/shared/src/list-entries.ts | 2 +- 3 files changed, 67 insertions(+), 37 deletions(-) diff --git a/apps/web/src/components/status-badge.tsx b/apps/web/src/components/status-badge.tsx index f0fc7c2..a18b671 100644 --- a/apps/web/src/components/status-badge.tsx +++ b/apps/web/src/components/status-badge.tsx @@ -76,7 +76,7 @@ const STATUS_LABELS: Record = { ip: 'IP', cidr: 'CIDR', hostname: 'Домен', - list: 'Список', + list: 'Вложенный список', unknown: 'Неизвестно', } diff --git a/apps/web/src/routes/_auth/lists/$id.tsx b/apps/web/src/routes/_auth/lists/$id.tsx index 9bacfff..8cc0ec9 100644 --- a/apps/web/src/routes/_auth/lists/$id.tsx +++ b/apps/web/src/routes/_auth/lists/$id.tsx @@ -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('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([]) @@ -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>() + 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} /> - + { + setAddOpen(open) + if (!open) { + setAddValue('') + setAddListRef('') + setAddMode('plaintext') + } + }} + > Добавить запись - Выберите вид и значение. Для IP/CIDR/доменов можно вставить - несколько строк сразу. + IP, CIDR и домены определяются автоматически. Можно вставить + несколько строк сразу. Либо вложите другой список целиком.
- Вид + Режим - {addKind === 'list' ? ( + {addMode === 'nested' ? ( Список +

+ Содержимое выбранного списка подтянется в эту политику + (ссылка, не копия). +

) : ( @@ -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 ? ( +
+ + Определено: + + {detectedKinds.map((kind) => ( + + ))} +
+ ) : addValue.trim() ? ( +

+ Не распознано — проверьте формат перед добавлением. +

+ ) : null}
)}
diff --git a/packages/shared/src/list-entries.ts b/packages/shared/src/list-entries.ts index b555633..6fde168 100644 --- a/packages/shared/src/list-entries.ts +++ b/packages/shared/src/list-entries.ts @@ -35,7 +35,7 @@ export const listEntryKindLabels: Record = { ip: 'IP', cidr: 'CIDR', hostname: 'Домен', - list: 'Список', + list: 'Вложенный список', } const IPV4 =