Skip to content

fix(products): polish the product list, empty state and create form - #1174

Merged
JamieRuderman merged 6 commits into
mainfrom
claude/slack-thread-request-6h4649
Aug 4, 2026
Merged

fix(products): polish the product list, empty state and create form#1174
JamieRuderman merged 6 commits into
mainfrom
claude/slack-thread-request-6h4649

Conversation

@JamieRuderman

Copy link
Copy Markdown
Member

Changes

Four small UI fixes on the Products pages. Follow-up to #1173, which is already merged — this is a separate PR on a branch restarted from main.

  • Alpha sort the platform dropdown (ProductAddPage.tsx) — the Create Product platform list rendered in the API's id order, which is roughly chronological by when each platform was added, so Teltonika landed at the bottom under Unknown. Now sorted through the shared byName comparator from refactor(sort): route alphabetical sorts through a shared comparator #1169, so it picks up the same locale-aware, numeric-aware collator as the rest of the app rather than a new ad-hoc sort.

  • Hide the select toggle when there are no products (ProductsHeaderButtons.tsx) — the check-square button in the header rendered unconditionally, offering a selection mode over an empty list. Now gated on getProducts().length.

  • Empty state icon (ProductsPage.tsx) — the "No products" screen used box-open; switched to conveyor-belt-boxes, matching the Products entry in the sidebar nav and the icon already used in the add-page products tile.

  • Create button spinner spacing (ProductAddPage.tsx) — the spinner used inline, which sets margin-left. On an icon that precedes its label that puts the gap on the wrong side: the spinner was pushed off the button's left edge and sat flush against "Creating...". Changed to inlineLeft, which sets margin-right — the convention used elsewhere for leading icons.

Verification

  • npm run typecheck passes. The only output is the pre-existing tsconfig TS5101 deprecations on baseUrl / downlevelIteration, which are present on main and unrelated.
  • Not verified visually in a running app — these are small enough to read off the diff, but the spinner alignment and the empty-state icon are worth a glance before merge.

One behavior note on the select toggle: products load asynchronously, so on a cold load of /products the button is absent for a beat and then appears. That matches the intent (no products, no toggle) but it is a visible change from the button always being there.


Generated by Claude Code

- Sort the platform dropdown on create product alphabetically, through the
  shared byName comparator rather than the API's id order.
- Only show the select toggle in the products header when the account has
  products, so the empty state isn't offering a selection mode for nothing.
- Use the conveyor-belt-boxes icon on the "No products" empty state, matching
  the products entry in the sidebar nav instead of an unrelated open box.
- Give the create button's spinner a right margin. It used `inline`, which
  sets margin-left, so the spinner sat flush against the "Creating..." label
  with a gap on the wrong side.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018YfRBMvuTVpXniu6MNi5jo
@aws-amplify-us-west-1

Copy link
Copy Markdown

This pull request is automatically being deployed by Amplify Hosting (learn more).

Access this pull request here: https://pr-1174.d20k671nqqv4kl.amplifyapp.com

claude added 5 commits August 4, 2026 21:22
Follow-up polish on the products UI, aligning it with how the equivalent
screens are already built.

Empty state now matches the devices and scripting empty states: the action
button comes first, followed by centered body2 help text, with no icon or
heading. Previously it led with an icon and an h2, which no other list
empty state does.

Header buttons now go through useTranslation like every other header
affordance. The select toggle reuses the existing header.showSelect and
header.hideSelect keys already translated for the devices equivalent, and
the strings on the products page pick up a new productsPage namespace.

Also from the review pass:
- Use IconButton's `hide` prop rather than a conditional wrapper, and its
  `to` prop rather than a hand-rolled history.push, which removes the second
  URLSearchParams construction in the toggle handler.
- Derive hasProducts through a memoized selector so the header re-renders
  when the boolean flips instead of on every product mutation.
- Filter and sort the platform types once when they are fetched rather than
  on every render of the create form.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018YfRBMvuTVpXniu6MNi5jo
… mode

Creating a product requires the admin permission on the API, but the UI
offered it to anyone. An org account manager could fill in the form and
submit, then get an API error. Gate the affordances on ADMIN the way the
scripting pages already do:

- Header create button is disabled with an "Admin permissions required"
  tooltip, matching ScriptsListHeader.
- The add page shows a notice and disables its inputs, so a direct link to
  /products/add explains itself rather than failing on submit.
- The empty state hides its create button and says why.
- The select toggle is hidden without admin, since it exists to reach bulk
  delete.

The create form is now a real form element, so enter submits it. The submit
button carries type="submit" and cancel is explicitly type="button".

Select mode moves from a ?select=true query param to a /products/select
route, matching /devices/select. The route passes `select` to the page as a
prop instead of the page parsing the URL, which is how DevicesPage receives
it. The header buttons and action bar navigate between the two routes, so
the three copies of the query-param logic are gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018YfRBMvuTVpXniu6MNi5jo
Deleting a product is at least as privileged as creating one, so gate it the
same way. Follows DeleteDevice, which disables the button and wraps it in a
tooltip naming the reason only when it is actually disabled.

The product option menu's delete is disabled without the admin permission,
with an "Admin permissions required" tooltip.

Also gate the bulk delete in the products action bar. The select toggle is
already hidden without admin, but /products/select can still be reached by
typing the URL, which would otherwise leave a delete button that fails at
the API.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018YfRBMvuTVpXniu6MNi5jo
Make Product creates a product, so it needs the same admin gate the products
pages now use. It was still enabled for non-admins, who would get an API
error on click.

Disabled with an "Admin permissions required" tooltip, matching how the
adjacent Delete Device explains why it is unavailable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018YfRBMvuTVpXniu6MNi5jo
Three follow-ups from review of the admin gating:

The Make Product tooltip wrapper was applied even for admins, where the
title is empty and no tooltip renders anyway. MenuList arrow-key traversal
skips a span-wrapped child, so that cost admins keyboard access to the item
for nothing. Wrap only in the disabled branch, as the product option menu
and DeleteDevice already do.

The "Admin permissions required" title on the bulk delete never appeared:
IconButton drops the tooltip entirely when disabled unless forceTitle is
set, so a non-admin saw a greyed trash icon with no explanation.

The create form kept `required` on the platform Select, whose native input
is visually hidden. Browser validation blocked submit before handleCreate
ran, making the platformRequired notice unreachable and anchoring the
native bubble to an invisible control. Mark the form noValidate so the
in-code checks report through the Notice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018YfRBMvuTVpXniu6MNi5jo
@JamieRuderman
JamieRuderman merged commit 648c920 into main Aug 4, 2026
8 checks passed
@JamieRuderman
JamieRuderman deleted the claude/slack-thread-request-6h4649 branch August 4, 2026 23:11
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