feat: поддержка множественных тегов для хостов#341
Conversation
Merge pull request remnawave#212 from remnawave/dev
Merge pull request remnawave#214 from remnawave/dev
Merge pull request remnawave#248 from remnawave/dev
Merge pull request remnawave#267 from remnawave/dev
Merge pull request remnawave#272 from remnawave/dev
Merge pull request remnawave#276 from remnawave/dev
Merge pull request remnawave#278 from remnawave/dev
Merge pull request remnawave#335 from remnawave/dev
TagsInput вместо Combobox, отображение max 2 + раскрытие по клику.
Greptile SummaryThis PR migrates the host tag model from Confidence Score: 5/5Safe to merge; all remaining findings are P2 style/i18n suggestions that do not affect runtime correctness. The migration from single host-tags-input.tsx (missing i18n for label/placeholder), host-card.widget.tsx (expand UX behaviour worth confirming)
|
| 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]
Reviews (1): Last reviewed commit: "fix: вернуть description в HostTagsInput..." | Re-trigger Greptile
| leftSection={<PiTagDuotone size="16px" />} | ||
| maxTags={10} | ||
| placeholder="Enter tags (comma, space, semicolon)" | ||
| splitChars={[',', ' ', ';']} |
There was a problem hiding this comment.
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.
| 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')} |
| error={ | ||
| Object.keys(form.errors) | ||
| .filter((key) => key.startsWith('tags.')) | ||
| .map((key) => form.errors[key]) | ||
| .join(', ') || | ||
| form.getInputProps('tags').error | ||
| } |
There was a problem hiding this comment.
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:
| 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 | |
| } |
| {!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> | ||
| ))} |
There was a problem hiding this comment.
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!
Описание
Обновление фронтенда для поддержки множественных тегов у хостов — в связи с миграцией бэкенда с
tag: string | nullнаtags: string[].Что изменено
Combobox(одиночный автокомплит) наTagsInput(чипсы, как в нодах). Ввод через запятую/пробел/точку с запятой, автодополнение из существующих тегов, максимум 10, кнопка очистки. Сохранён description с пояснением о видимости тегов.tagнаtags, добавлена агрегация ошибок валидации элементов массива (как в нодах).tags: string[].tags.join(', ').Зависимости
Бэкенд PR: remnawave/backend#159