Skip to content

refactor(ui): extract shared TagsSection for devices and containers#6644

Merged
otavio merged 1 commit into
masterfrom
refactor/ui/device-tags-section
Jul 14, 2026
Merged

refactor(ui): extract shared TagsSection for devices and containers#6644
otavio merged 1 commit into
masterfrom
refactor/ui/device-tags-section

Conversation

@luizhf42

Copy link
Copy Markdown
Member

What

Unified the duplicated TagsSection implementations — the extracted component in pages/devices/TagsSection.tsx and the inline copy in ContainerDetails.tsx — into a single shared component at components/common/TagsSection.tsx that accepts addTag/removeTag mutation functions as props.

Why

Both detail pages had near-identical tag management UI with the same validation rules, styling, and mutation call signature ({ path: { uid, name } }). The device version had a search/autocomplete dropdown the container version lacked; the container version had isSdkError-based error messages the device version lacked. Maintaining both meant divergent features and duplicated bug surface.

Changes

  • components/common/TagsSection.tsx: new shared component merging the best of both versions — the device version's autocomplete dropdown (powered by the namespace-wide useTags() hook) and the container version's status-specific error handling (isSdkError for 403/400). Added WAI-ARIA combobox keyboard navigation (ArrowDown/ArrowUp to cycle options, Enter to select, aria-activedescendant for screen readers), a double-submit guard in handleAdd, scrollIntoView on the active option, and closeDropdown helper that resets both open and activeIndex state. Dropdown items use role="listbox"/role="option" with onMouseDown + preventDefault to avoid input blur race.
  • pages/DeviceDetails.tsx: imports shared TagsSection, lifts useAddDeviceTag/useRemoveDeviceTag to page level, passes mutateAsync as props.
  • pages/ContainerDetails.tsx: deleted the ~120-line inline TagsSection, imports shared version with container mutations. Removed unused TagIcon, PlusIcon, useHasPermission imports. Replaced local LABEL constant with shared LABEL_BASE from @/utils/styles.
  • pages/devices/TagsSection.tsx: deleted — replaced by the shared component.
  • components/common/__tests__/TagsSection.test.tsx: 22 tests covering rendering, permission gating, add/remove with validation, API error differentiation (403/400), autocomplete suggestions, keyboard navigation (ArrowDown + Enter, wrap-around), and accessibility (role="alert", descriptive aria-labels).

Testing

  • npm run test — 22/22 TagsSection tests pass
  • Open a device detail page: verify tags display, add a tag via autocomplete (type partial name, arrow-key to suggestion, Enter), remove a tag
  • Open a container detail page: same — containers now have the autocomplete dropdown they previously lacked
  • Keyboard-only: verify ArrowDown/ArrowUp cycle through suggestions, Enter selects, Escape closes

@luizhf42 luizhf42 requested a review from a team as a code owner July 13, 2026 20:21
@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown

Claude finished @luizhf42's task in 5m 44s —— View job


Code Review Complete

Reviewed 5 files across code quality, security, testing, language patterns, and architecture — no new issues found. The code looks good as-is.

A few things I specifically verified during the review:

  • Mutation contract paritymutateAsync from useAddDeviceTag/useRemoveDeviceTag and useAddContainerTag/useRemoveContainerTag all structurally satisfy the new addTag/removeTag prop signature (opts: { path: { uid, name } }) => Promise<unknown>.
  • No dangling importersgrep for devices/TagsSection returns zero matches across both ui/ and cloud/; the deleted file is fully replaced.
  • showDropdown = open && trimmed && … — the "" short-circuit is safe here (empty string renders as nothing in JSX, and aria-expanded/aria-controls explicitly coerce it), so there's no leaked-text render bug.
  • Keyboard navactiveIndex is reset to -1 on every change/close and always bounds-checked against options.length, so no stale-index crash; the wrap-around test correctly accounts for the create-option being suppressed for short inputs.
  • XSS surface — tags and suggestions render as escaped JSX text; no dangerouslySetInnerHTML or attribute/URL sinks.

Behavioral changes (containers gaining autocomplete, devices gaining visible remove-error messages, ARIA combobox navigation) are intentional merges of the two prior implementations, not regressions.

If you push additional changes and want a new review, tag @shellhub-io/admin and a team member can trigger it.

Unify the duplicated `TagsSection` implementations — the extracted
component in `pages/devices/TagsSection.tsx` and the inline copy in
`ContainerDetails.tsx` — into a single shared component at
`components/common/TagsSection.tsx`.

The shared component accepts `addTag`/`removeTag` mutation functions
as props, keeping it entity-agnostic. It merges the device version's
search/autocomplete dropdown with the container version's
`isSdkError`-based error handling. Containers gain the autocomplete
feature and devices gain visible remove-error messages.

Also adds WAI-ARIA combobox keyboard navigation (ArrowDown/ArrowUp
to cycle options, Enter to select, `aria-activedescendant` for
screen readers), a double-submit guard, and `scrollIntoView` on
the active option. Replaces local `LABEL` constants with the
shared `LABEL_BASE` from `@/utils/styles`.
@otavio otavio force-pushed the refactor/ui/device-tags-section branch from 30f74c1 to 0886852 Compare July 14, 2026 13:54
@otavio otavio merged commit 42e6763 into master Jul 14, 2026
13 checks passed
@otavio otavio deleted the refactor/ui/device-tags-section branch July 14, 2026 14:01
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