Skip to content

feat: поддержка множественных тегов для хостов#341

Open
StylerHub wants to merge 35 commits into
remnawave:devfrom
StylerHub:feat/host-tags-array
Open

feat: поддержка множественных тегов для хостов#341
StylerHub wants to merge 35 commits into
remnawave:devfrom
StylerHub:feat/host-tags-array

Conversation

@StylerHub
Copy link
Copy Markdown
Contributor

Описание

Обновление фронтенда для поддержки множественных тегов у хостов — в связи с миграцией бэкенда с tag: string | null на tags: string[].

Что изменено

  • HostTagsInputWidget: заменён Combobox (одиночный автокомплит) на TagsInput (чипсы, как в нодах). Ввод через запятую/пробел/точку с запятой, автодополнение из существующих тегов, максимум 10, кнопка очистки. Сохранён description с пояснением о видимости тегов.
  • base-host-form: обновлён ключ формы с tag на tags, добавлена агрегация ошибок валидации элементов массива (как в нодах).
  • edit-host-modal: обновлена инициализация формы, обработка submit и клонирование для tags: string[].
  • host-card: отображаются максимум 2 тега + кликабельный бейдж "+N", который раскрывает все теги в отдельной строке под карточкой. Повторный клик сворачивает.
  • setup-template-monaco: обновлено отображение тегов через tags.join(', ').

Зависимости

Бэкенд PR: remnawave/backend#159

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 8, 2026

Greptile Summary

This PR migrates the host tag model from tag: string | null (single) to tags: string[] (multiple) across the frontend, replacing the Combobox-based input with Mantine's TagsInput, updating form keys/validation, filter logic, card display, and the Monaco template tooltip. All data paths (init → submit → clone) are updated consistently with safe ?? [] fallbacks.

Confidence Score: 5/5

Safe to merge; all remaining findings are P2 style/i18n suggestions that do not affect runtime correctness.

The migration from single tag to tags[] is thorough and consistent across all touched paths. No P0/P1 issues found. The three P2 comments cover missing i18n keys, a fragile-but-safe ReactNode cast, and a minor UX design choice — none block merge.

host-tags-input.tsx (missing i18n for label/placeholder), host-card.widget.tsx (expand UX behaviour worth confirming)

Vulnerabilities

No security concerns identified.

Important Files Changed

Filename Overview
src/widgets/dashboard/hosts/host-tags-input/host-tags-input.tsx Replaces Combobox/InputBase with Mantine TagsInput; autocomplete data wired correctly but label and placeholder strings are not passed through i18n unlike the rest of the widget.
src/shared/ui/forms/hosts/base-host-form/base-host-form.tsx Form key updated from tag to tags; error aggregation mirrors the nodes pattern but casts ReactNode values via .join() which is safe only when errors are strings.
src/widgets/dashboard/hosts/edit-host-modal/edit-host-modal.widget.tsx Init, submit, and clone paths all updated consistently from `tag: string
src/widgets/dashboard/hosts/host-card/host-card.widget.tsx Filter updated to Array.includes; expand/collapse badge logic for 2+ tags works correctly, though expanded state removes inline tags from the header row entirely rather than keeping them alongside the overflow row.
src/widgets/dashboard/hosts/host-tags-input/interfaces/props.interface.tsx Interface simplified to a direct re-export of TagsInputProps; the extra wrapper adds no value and could be a plain type alias, but not a bug.
src/widgets/dashboard/templates/subscription-template-editor/utils/setup-template-monaco.tsx Tags row in Monaco markdown updated cleanly: guard on tags?.length, values joined with , . No issues.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[HostTagsInputWidget\nTagsInput - string array] -->|form.getInputProps tags| B[BaseHostForm\nkey: tags]
    B --> C{Submit or Clone}
    C -->|updateHost| D[handleSubmit\ntags from values]
    C -->|createHost clone| E[handleCloneHost\ntags from host]
    F[Host API Response\ntags: string array] -->|init form| G[form.setValues\ntags]
    G --> B
    F -->|host-card| H[HostCardWidget]
    H --> I{tags length > 2}
    I -- No --> J[Show all tags inline]
    I -- Yes --> K[Show first 2 plus N badge]
    K -->|click badge| L[tagsExpanded true\nShow all tags in expanded row]
Loading

Reviews (1): Last reviewed commit: "fix: вернуть description в HostTagsInput..." | Re-trigger Greptile

Comment on lines +24 to +27
leftSection={<PiTagDuotone size="16px" />}
maxTags={10}
placeholder="Enter tags (comma, space, semicolon)"
splitChars={[',', ' ', ';']}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Hardcoded strings not passed through i18n

label and placeholder are plain English strings while description uses t(...). In a locale other than English these two strings will always appear in English. For consistency with the rest of the form, both should go through useTranslation.

Suggested change
leftSection={<PiTagDuotone size="16px" />}
maxTags={10}
placeholder="Enter tags (comma, space, semicolon)"
splitChars={[',', ' ', ';']}
label={t('host-tags-input.label')}
leftSection={<PiTagDuotone size="16px" />}
maxTags={10}
placeholder={t('host-tags-input.placeholder')}

Comment on lines +466 to +472
error={
Object.keys(form.errors)
.filter((key) => key.startsWith('tags.'))
.map((key) => form.errors[key])
.join(', ') ||
form.getInputProps('tags').error
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Array-error join may produce [object Object] for non-string ReactNode errors

form.errors[key] is typed as ReactNode, so .map(...).join(', ') will stringify any React element to [object Object]. In practice Zod validation only produces string messages, but the cast is fragile. Casting explicitly keeps this safe:

Suggested change
error={
Object.keys(form.errors)
.filter((key) => key.startsWith('tags.'))
.map((key) => form.errors[key])
.join(', ') ||
form.getInputProps('tags').error
}
error={
Object.keys(form.errors)
.filter((key) => key.startsWith('tags.'))
.map((key) => form.errors[key] as string)
.join(', ') ||
form.getInputProps('tags').error
}

Comment on lines +369 to +381
{!tagsExpanded &&
item.tags?.slice(0, 2).map((tag) => (
<Badge
autoContrast
color={ch.hex(tag)}
key={tag}
leftSection={<TbTagStarred size={12} />}
size="md"
variant="outline"
>
{tag}
</Badge>
))}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Expanded state hides inline tags entirely

When tagsExpanded is true, the {!tagsExpanded && ...} guard removes the first-two tags from the header row entirely; only the "−" collapse badge stays. The expanded row below shows all tags, so users can see them, but it creates a visual jump (tags disappear from the header, re-appear below). A more common pattern is to keep showing the first N tags inline and add/remove the overflow row on expand. Consider whether this is the intended UX.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants