Skip to content

Commit 0eb58a7

Browse files
authored
fix: replace hardcoded tag options with free-form text input (#284)
* fix: replace hardcoded tag options with free-form text input Tags in upload and edit recording dialogs were limited to a fixed set (TvT, COOP, Zeus, Training). Replace with a text input that accepts any value, with datalist suggestions populated from existing tags. * fix: use unique datalist IDs and update tests for free-form tag input Address review feedback: use createUniqueId() for datalist IDs to avoid potential duplicates. Update dialog and upload tests to work with the new text input instead of the removed tag button group.
1 parent 779cbbf commit 0eb58a7

File tree

6 files changed

+44
-82
lines changed

6 files changed

+44
-82
lines changed

ui/src/pages/recording-selector/RecordingSelector.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,7 @@ export function RecordingSelector(): JSX.Element {
419419
<Show when={showUpload() && authenticated()}>
420420
<UploadDialog
421421
maps={uniqueMaps()}
422+
tags={uniqueTags()}
422423
onUpload={handleUpload}
423424
onCancel={() => setShowUpload(false)}
424425
uploading={uploading()}

ui/src/pages/recording-selector/__tests__/RecordingSelector.test.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1297,11 +1297,9 @@ describe("RecordingSelector (Admin)", () => {
12971297
const mapInput = screen.getByPlaceholderText(/altis/) as HTMLInputElement;
12981298
fireEvent.input(mapInput, { target: { value: "altis" } });
12991299

1300-
// Select a tag — find the TvT button inside the upload dialog
1301-
const tagLabel = screen.getByText("TAG");
1302-
const tagGroup = tagLabel.nextElementSibling!;
1303-
const tvtBtn = within(tagGroup as HTMLElement).getByText("TvT");
1304-
fireEvent.click(tvtBtn);
1300+
// Type a tag in the free-form input
1301+
const tagInput = screen.getByPlaceholderText("e.g. TvT, COOP, Zeus") as HTMLInputElement;
1302+
fireEvent.input(tagInput, { target: { value: "TvT" } });
13051303

13061304
// Submit
13071305
fireEvent.click(screen.getByTestId("upload-submit"));

ui/src/pages/recording-selector/__tests__/dialogs.test.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,29 +69,26 @@ describe("EditModal", () => {
6969
});
7070
});
7171

72-
it("shows tag buttons and allows selection", () => {
72+
it("shows tag input and allows free-form entry", () => {
7373
const onClose = vi.fn();
7474
const onSave = vi.fn();
7575

7676
const { container } = render(() => (
77-
<EditModal rec={mockRec} tags={[]} onClose={onClose} onSave={onSave} />
77+
<EditModal rec={mockRec} tags={["TvT", "COOP"]} onClose={onClose} onSave={onSave} />
7878
));
7979

80-
// All tag option buttons should be rendered
81-
expect(screen.getByText("TvT")).not.toBeNull();
82-
expect(screen.getByText("COOP")).not.toBeNull();
83-
expect(screen.getByText("Zeus")).not.toBeNull();
84-
expect(screen.getByText("Training")).not.toBeNull();
85-
expect(screen.getByText("None")).not.toBeNull();
80+
// Tag input should show current value
81+
const tagInput = screen.getByPlaceholderText("e.g. TvT, COOP, Zeus") as HTMLInputElement;
82+
expect(tagInput.value).toBe("TvT");
8683

87-
// Select a different tag
88-
fireEvent.click(screen.getByText("COOP"));
84+
// Type a custom tag
85+
fireEvent.input(tagInput, { target: { value: "CustomTag" } });
8986

9087
// Submit and verify the new tag is sent
9188
fireEvent.submit(container.querySelector("form")!);
9289
expect(onSave).toHaveBeenCalledWith("42", {
9390
missionName: "Op Thunder",
94-
tag: "COOP",
91+
tag: "CustomTag",
9592
date: expectedDateUTC,
9693
});
9794
});

ui/src/pages/recording-selector/constants.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ export const SIDE_HEX: Record<string, string> = {
3535
CIV: "#A78BFA",
3636
};
3737

38-
/** Tag values for the tag selector. Empty string = "None" (no tag). */
39-
export const TAG_OPTIONS = ["TvT", "COOP", "Zeus", "Training", ""] as const;
4038

4139
export const LOCALE_LABELS: Record<Locale, { label: string; flag: string }> = {
4240
cs: { label: "\u010Ce\u0161tina", flag: "\uD83C\uDDE8\uD83C\uDDFF" },

ui/src/pages/recording-selector/dialogs.module.css

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -48,30 +48,6 @@
4848
font-weight: 600;
4949
}
5050

51-
.editTagGroup {
52-
display: flex;
53-
gap: 4px;
54-
flex-wrap: wrap;
55-
}
56-
57-
.editTagBtn {
58-
padding: 6px 12px;
59-
border-radius: 6px;
60-
cursor: pointer;
61-
font-size: var(--font-size-md);
62-
font-family: var(--font-mono);
63-
font-weight: 600;
64-
background: rgba(255, 255, 255, 0.03);
65-
color: var(--text-dimmer);
66-
border: 1px solid rgba(255, 255, 255, 0.06);
67-
transition: 0.15s;
68-
}
69-
70-
.editTagBtnActive {
71-
background: color-mix(in srgb, var(--accent-primary) 15%, transparent);
72-
color: var(--accent-primary);
73-
border-color: color-mix(in srgb, var(--accent-primary) 30%, transparent);
74-
}
7551

7652
/* ── Delete confirm ── */
7753
.deleteBody {

ui/src/pages/recording-selector/dialogs.tsx

Lines changed: 32 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { createSignal, Show, For } from "solid-js";
1+
import { createSignal, createUniqueId, Show, For } from "solid-js";
22
import type { JSX } from "solid-js";
33
import type { Recording } from "../../data/types";
44
import { EditIcon, XIcon, CheckIcon, UploadIcon, FilePlusIcon, RefreshCwIcon, AlertTriangleIcon, TrashIcon } from "../../components/Icons";
55
import { formatDuration, formatDate, stripRecordingExtension, isoToLocalInput, localInputToIso } from "./helpers";
6-
import { TAG_OPTIONS } from "./constants";
76
import ui from "../../components/ui.module.css";
87
import styles from "./dialogs.module.css";
98

@@ -15,6 +14,7 @@ export function EditModal(props: {
1514
onClose: () => void;
1615
onSave: (id: string, data: { missionName?: string; tag?: string; date?: string }) => void;
1716
}): JSX.Element {
17+
const tagListId = createUniqueId();
1818
// eslint-disable-next-line solid/reactivity -- intentional one-time init for form state
1919
const [name, setName] = createSignal(props.rec.missionName);
2020
// eslint-disable-next-line solid/reactivity -- intentional one-time init for form state
@@ -80,28 +80,22 @@ export function EditModal(props: {
8080
/>
8181
</div>
8282

83-
{/* Tag + Date side by side */}
84-
<div style={{ display: "flex", gap: "12px" }}>
85-
<div class={styles.editField} style={{ flex: "1" }}>
86-
<label class={styles.editLabel}>Tag</label>
87-
<div class={styles.editTagGroup}>
88-
<For each={TAG_OPTIONS}>
89-
{(t) => {
90-
const active = () => tag() === t;
91-
return (
92-
<button
93-
type="button"
94-
class={styles.editTagBtn}
95-
classList={{ [styles.editTagBtnActive]: active() }}
96-
onClick={() => setTag(t)}
97-
>
98-
{t || "None"}
99-
</button>
100-
);
101-
}}
102-
</For>
103-
</div>
104-
</div>
83+
{/* Tag */}
84+
<div class={styles.editField}>
85+
<label class={styles.editLabel}>Tag</label>
86+
<input
87+
type="text"
88+
value={tag()}
89+
onInput={(e) => setTag(e.currentTarget.value)}
90+
placeholder="e.g. TvT, COOP, Zeus"
91+
list={tagListId}
92+
class={ui.input}
93+
/>
94+
<datalist id={tagListId}>
95+
<For each={props.tags}>
96+
{(t) => <option value={t} />}
97+
</For>
98+
</datalist>
10599
</div>
106100

107101
{/* Date */}
@@ -131,10 +125,12 @@ export function EditModal(props: {
131125

132126
export function UploadDialog(props: {
133127
maps: string[];
128+
tags?: string[];
134129
onUpload: (data: { file: File; name: string; map: string; tag: string; date: string }) => void;
135130
onCancel: () => void;
136131
uploading: boolean;
137132
}): JSX.Element {
133+
const tagListId = createUniqueId();
138134
const [dragOver, setDragOver] = createSignal(false);
139135
const [file, setFile] = createSignal<File | null>(null);
140136
const [name, setName] = createSignal("");
@@ -252,23 +248,19 @@ export function UploadDialog(props: {
252248
{/* Tag */}
253249
<div class={styles.editField}>
254250
<label class={styles.editLabel}>TAG</label>
255-
<div class={styles.editTagGroup}>
256-
<For each={TAG_OPTIONS}>
257-
{(t) => {
258-
const active = () => tag() === t;
259-
return (
260-
<button
261-
type="button"
262-
class={styles.editTagBtn}
263-
classList={{ [styles.editTagBtnActive]: active() }}
264-
onClick={() => setTag(t)}
265-
>
266-
{t || "None"}
267-
</button>
268-
);
269-
}}
251+
<input
252+
type="text"
253+
value={tag()}
254+
onInput={(e) => setTag(e.currentTarget.value)}
255+
placeholder="e.g. TvT, COOP, Zeus"
256+
list={tagListId}
257+
class={ui.input}
258+
/>
259+
<datalist id={tagListId}>
260+
<For each={props.tags ?? []}>
261+
{(t) => <option value={t} />}
270262
</For>
271-
</div>
263+
</datalist>
272264
</div>
273265

274266
{/* Date */}

0 commit comments

Comments
 (0)