New release page: replace free-form tag input + caret button with a dropdown of existing tags #503

Closed
opened 2026-09-13 23:26:32 +00:00 by crueber · 3 comments
Owner

New release page: replace the free-form tag input + caret button with a dropdown of existing tags

What's requested

On the New release page (/:owner/:name/releases/new), the tag field should stop being a free-form text input with a caret-button accessory, and become a proper dropdown select of the repo's existing tags. The tag is required and must already exist (the backend's releases.put resolves the tag and errors otherwise), so a picker of real tags is the honest control — typing an arbitrary name is the one input the form can't accept.

Requested behavior:

  • Required selection: the tag is chosen from the dropdown of existing tags, not typed. No valid selection → no create (the submit/autodraft buttons already disable on empty tag; keep that but it should now reflect "nothing selected" rather than "typed string present").
  • Zero-tags state: when the repo has no tags, the control shows a clear empty state (no options) and the helper copy directs the user to how tags come into existence (create from a commit page or push with git). Do not offer a create-tag escape hatch inline — that's a separate surface's job.
  • Updated helper copy: the helper line under the field should describe picking from existing tags (e.g. "N tags available — pick one from the list"), and the no-tags variant should keep the current "No tags yet — create one from a commit page or push one with git." copy (or equivalent). Drop any copy implying free typing ("type the name").

Evidence

  • web/src/pages/ReleaseNew.jsx — current shape:
    • Lines 164–209: the combobox — a free-text <input id="release-tag" role="combobox"> plus an adjacent ▾ caret <button> (lines 191–208) that toggles the popover. onInput sets the tag from arbitrary typed text (line 179) and auto-opens the list.
    • Lines 210–246: the styled popover list (tag-drop card absolute inset-x-0 z-30 …) — this is the visual that a proper dropdown should retain.
    • Lines 248–257: current helper copy — "{n} tags available — pick from the list or type the name." (the "type the name" half goes away), no-tags fallback "No tags yet — create one from a commit page or push one with git."
    • Lines 74–75: tags come from the existing tags:{full} cache entry (ctx.repoClient.tags({ n: 100 })), names normalized by stripping refs/tags/. No new fetch should be introduced.
    • Lines 30–34, 50–55: both autodraft and create already hard-gate on a non-empty tag; the create path hits releases.put(tag) which the server resolves against existing tags.
  • web/src/lib/releases.js filterTagNames is the existing client-side filter used by the combobox — a plain dropdown (no typed filtering) may not need it; planner's call whether the select keeps type-to-filter behavior (see decision point below).
  • Header comment lines 1–6 documents the combobox intent (07 §8, issue #254) — update it when the control changes.

Architecture notes

  • Purely client-side: web/src/pages/ReleaseNew.jsx only. No backend change; the release service already rejects nonexistent tags.
  • Tags feed off the existing useData(tags:${ctx.full}) cache — keep it that way, no new network path.
  • If the implementation becomes a native <select>, note the selected-tag row and popover styling from the current combobox is lost; if it stays a custom popover triggered by a single button (rather than input + caret), preserve the accessibility contract now in place: role="listbox"/role="option", aria-expanded, aria-activedescendant, keyboard nav (arrows/Enter/Esc — RefPicker conventions), outside-click close via the document-level listener idiom (LabelPicker/MilestonePicker pattern; current implementation lines 98–112).

Decision points (planner's call)

  • Native <select> styled with the app's input class, vs. a single-button custom popover (input removed) reusing the current list markup. Either is acceptable; note the trade-off.
  • Whether any type-to-filter remains. If yes, it's the combobox pattern with a read-only-ish input; if no, plain dropdown.

Acceptance criteria

  • The free-form text input and the separate ▾ caret button are gone from the New release page tag field; one control opens the tag list.
  • With tags present: the user picks a tag from existing options only; create proceeds only with a selected existing tag.
  • With zero tags: the control renders an explicit empty state and the helper copy says how tags come to exist; create/autodraft stay disabled.
  • Helper copy updated: no "type the name" phrasing; the tags-available and no-tags variants are both correct.
  • Keyboard + screen-reader behavior retained (listbox semantics, arrows/Enter/Esc, outside-click close) if the custom popover path is chosen.
  • Header comment in ReleaseNew.jsx updated to reflect the new control.
# New release page: replace the free-form tag input + caret button with a dropdown of existing tags ## What's requested On the New release page (`/:owner/:name/releases/new`), the tag field should stop being a free-form text input with a caret-button accessory, and become a proper dropdown select of the repo's existing tags. The tag is required and must already exist (the backend's `releases.put` resolves the tag and errors otherwise), so a picker of real tags is the honest control — typing an arbitrary name is the one input the form can't accept. Requested behavior: - **Required selection**: the tag is chosen from the dropdown of existing tags, not typed. No valid selection → no create (the submit/autodraft buttons already disable on empty tag; keep that but it should now reflect "nothing selected" rather than "typed string present"). - **Zero-tags state**: when the repo has no tags, the control shows a clear empty state (no options) and the helper copy directs the user to how tags come into existence (create from a commit page or push with git). Do not offer a create-tag escape hatch inline — that's a separate surface's job. - **Updated helper copy**: the helper line under the field should describe picking from existing tags (e.g. "N tags available — pick one from the list"), and the no-tags variant should keep the current "No tags yet — create one from a commit page or push one with git." copy (or equivalent). Drop any copy implying free typing ("type the name"). ## Evidence - `web/src/pages/ReleaseNew.jsx` — current shape: - Lines 164–209: the combobox — a free-text `<input id="release-tag" role="combobox">` plus an adjacent `▾` caret `<button>` (lines 191–208) that toggles the popover. `onInput` sets the tag from arbitrary typed text (line 179) and auto-opens the list. - Lines 210–246: the styled popover list (`tag-drop card absolute inset-x-0 z-30 …`) — this is the visual that a proper dropdown should retain. - Lines 248–257: current helper copy — "{n} tags available — pick from the list or type the name." (the "type the name" half goes away), no-tags fallback "No tags yet — create one from a commit page or push one with git." - Lines 74–75: tags come from the existing `tags:{full}` cache entry (`ctx.repoClient.tags({ n: 100 })`), names normalized by stripping `refs/tags/`. No new fetch should be introduced. - Lines 30–34, 50–55: both autodraft and create already hard-gate on a non-empty tag; the create path hits `releases.put(tag)` which the server resolves against existing tags. - `web/src/lib/releases.js` `filterTagNames` is the existing client-side filter used by the combobox — a plain dropdown (no typed filtering) may not need it; planner's call whether the select keeps type-to-filter behavior (see decision point below). - Header comment lines 1–6 documents the combobox intent (07 §8, issue #254) — update it when the control changes. ## Architecture notes - Purely client-side: `web/src/pages/ReleaseNew.jsx` only. No backend change; the release service already rejects nonexistent tags. - Tags feed off the existing `useData(`tags:${ctx.full}`)` cache — keep it that way, no new network path. - If the implementation becomes a native `<select>`, note the selected-tag row and popover styling from the current combobox is lost; if it stays a custom popover triggered by a single button (rather than input + caret), preserve the accessibility contract now in place: `role="listbox"`/`role="option"`, `aria-expanded`, `aria-activedescendant`, keyboard nav (arrows/Enter/Esc — RefPicker conventions), outside-click close via the document-level listener idiom (LabelPicker/MilestonePicker pattern; current implementation lines 98–112). ## Decision points (planner's call) - Native `<select>` styled with the app's `input` class, vs. a single-button custom popover (input removed) reusing the current list markup. Either is acceptable; note the trade-off. - Whether any type-to-filter remains. If yes, it's the combobox pattern with a read-only-ish input; if no, plain dropdown. ## Acceptance criteria - [ ] The free-form text input and the separate `▾` caret button are gone from the New release page tag field; one control opens the tag list. - [ ] With tags present: the user picks a tag from existing options only; create proceeds only with a selected existing tag. - [ ] With zero tags: the control renders an explicit empty state and the helper copy says how tags come to exist; create/autodraft stay disabled. - [ ] Helper copy updated: no "type the name" phrasing; the tags-available and no-tags variants are both correct. - [ ] Keyboard + screen-reader behavior retained (listbox semantics, arrows/Enter/Esc, outside-click close) if the custom popover path is chosen. - [ ] Header comment in `ReleaseNew.jsx` updated to reflect the new control.
crueber added this to the v1 milestone 2026-09-13 23:26:50 +00:00
Author
Owner

Fixed by PR #508 (#508): tag field is now a native single-select dropdown of existing tags — required selection, zero-tags empty state, updated helper copy. No backend change, no new deps.

Fixed by PR #508 (https://git.packden.us/crueber/walhub/pulls/508): tag field is now a native single-select dropdown of existing tags — required selection, zero-tags empty state, updated helper copy. No backend change, no new deps.
Author
Owner

Review of PR #508 (fix/issue-503, commit 2ae035a) — verified in scratch worktree /tmp/pr508 (symlinked node_modules, since removed).

PASS — all 7 review points hold:

  1. Native select (ReleaseNew.jsx:144-163): one bound to getTag()/onChange-setTag. Disabled empty placeholder Pick a tag… (tag starts "" so it shows first). Zero-tags: select disabled (disabled={tagsLoading()||!hasTags()}, :148) with explicit No tags yet (:155); loading variant 'Loading tags…' same line. Helper copy (:164-173): '{n} tags available — pick one from the list.' + no-tags fallback 'No tags yet — create one from a commit page or push one with git.' (no inline create hatch). No 'type the name' phrasing anywhere.
  2. Type-to-filter loss acceptable: issue lists it as planner's call ('Either is acceptable'). Native select keeps browser prefix type-ahead; documented in code comment (:109-114) and 12_web_ui.md entry.
  3. Submit gates reflect nothing-selected: create (:249) + autodraft (:255) both disabled={getBusy()||!getTag()}, plus inline guards (:43 autodraft, :69-72 create with 'Choose a tag for this release.').
  4. Old shape fully deleted: combobox input/caret/popover/option-row ids/getTagOpen+getTagActive/doc listeners/onTagKey/filterTagNames import all gone (asserted by new test 'combobox shape is gone'). filterTagNames deleted from lib/releases.js + its 3 releases-panel.test.js cases removed. tag-drop guard retirement: popover-opaque LEGACY_PANELS drops tag-drop (9→8 legacy, comment updated); threshold >=10 still holds — scan counts per-file occurrences and multi-use hooks keep it above floor (suite green). popover-viewport.test.js still pins tag-drop in PANELS but that test asserts only the ui.css :204 bound covers it, which it still does — passes, harmless.
  5. Options from existing cache: useData(tags:${ctx.full}, () => ctx.repoClient.tags({ n: 100 })) unchanged (:30); exactly one repoClient.tags( call; refs/tags/ strip kept (:101); stream order kept via .
  6. #254 supersede noted: 12_web_ui.md entry prefixed 'SUPERSEDED (Forgejo #503, below)', ReleaseNew.jsx header '(07 §8; Forgejo #503)', releases-panel + popover-opaque comments updated.
  7. No backend change (files: 12_web_ui.md, ReleaseNew.jsx, releases.js, 2 test pins, 1 new test); no package.json/import change (imports only removed); docs updated same commit per law 12. Laws 1/7/8 clean (no new deps, N/A, web-only).
  8. VERIFY: targeted 4 files 27/27 pass; full node --test 1126 total / 1125 pass / 1 fail = smoke.test.js 'built SPA shell served at / and /setup' — environmental, needs live :8080 (smoke.test.js:9 unmodified by PR; failure is an HTTP 403 from whatever answers :8080 here, unreachable from this change's client-only files). vite build green (2.27s). esbuild unaffected (no sdk/ files touched). No browser drive per instructions (node tests + reasoning).

    No fixes pushed — nothing to fix. MERGE RECOMMENDATION: ready to merge.

Review of PR #508 (fix/issue-503, commit 2ae035a) — verified in scratch worktree /tmp/pr508 (symlinked node_modules, since removed). PASS — all 7 review points hold: 1. Native select (ReleaseNew.jsx:144-163): one <select id="release-tag" class="input font-mono"> bound to getTag()/onChange-setTag. Disabled empty placeholder <option value="" disabled>Pick a tag…</option> (tag starts "" so it shows first). Zero-tags: select disabled (disabled={tagsLoading()||!hasTags()}, :148) with explicit <option>No tags yet</option> (:155); loading variant 'Loading tags…' same line. Helper copy (:164-173): '{n} tags available — pick one from the list.' + no-tags fallback 'No tags yet — create one from a commit page or push one with git.' (no inline create hatch). No 'type the name' phrasing anywhere. 2. Type-to-filter loss acceptable: issue lists it as planner's call ('Either is acceptable'). Native select keeps browser prefix type-ahead; documented in code comment (:109-114) and 12_web_ui.md entry. 3. Submit gates reflect nothing-selected: create (:249) + autodraft (:255) both disabled={getBusy()||!getTag()}, plus inline guards (:43 autodraft, :69-72 create with 'Choose a tag for this release.'). 4. Old shape fully deleted: combobox input/caret/popover/option-row ids/getTagOpen+getTagActive/doc listeners/onTagKey/filterTagNames import all gone (asserted by new test 'combobox shape is gone'). filterTagNames deleted from lib/releases.js + its 3 releases-panel.test.js cases removed. tag-drop guard retirement: popover-opaque LEGACY_PANELS drops tag-drop (9→8 legacy, comment updated); threshold >=10 still holds — scan counts per-file occurrences and multi-use hooks keep it above floor (suite green). popover-viewport.test.js still pins tag-drop in PANELS but that test asserts only the ui.css :204 bound covers it, which it still does — passes, harmless. 5. Options from existing cache: useData(`tags:${ctx.full}`, () => ctx.repoClient.tags({ n: 100 })) unchanged (:30); exactly one repoClient.tags( call; refs/tags/ strip kept (:101); stream order kept via <For each={tagNames()}>. 6. #254 supersede noted: 12_web_ui.md entry prefixed 'SUPERSEDED (Forgejo #503, below)', ReleaseNew.jsx header '(07 §8; Forgejo #503)', releases-panel + popover-opaque comments updated. 7. No backend change (files: 12_web_ui.md, ReleaseNew.jsx, releases.js, 2 test pins, 1 new test); no package.json/import change (imports only removed); docs updated same commit per law 12. Laws 1/7/8 clean (no new deps, N/A, web-only). VERIFY: targeted 4 files 27/27 pass; full node --test 1126 total / 1125 pass / 1 fail = smoke.test.js 'built SPA shell served at / and /setup' — environmental, needs live :8080 (smoke.test.js:9 unmodified by PR; failure is an HTTP 403 from whatever answers :8080 here, unreachable from this change's client-only files). vite build green (2.27s). esbuild unaffected (no sdk/ files touched). No browser drive per instructions (node tests + reasoning). No fixes pushed — nothing to fix. MERGE RECOMMENDATION: ready to merge.
Author
Owner

Fixed by PR #508 (review clean — all 7 checks pass, old shape fully deleted, gates reflect selection), merged. Closing.

Fixed by PR #508 (review clean — all 7 checks pass, old shape fully deleted, gates reflect selection), merged. Closing.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crueber/walhub#503
No description provided.