fix: normalize new topic input slugs (#2821)

Normalize newly entered UI topics to canonical lowercase hyphenated slugs while preserving existing stored labels until replaced.
This commit is contained in:
Jason (Json)
2026-06-23 17:39:35 -06:00
committed by GitHub
parent 898b3bebba
commit c3e2b8f6f4
4 changed files with 31 additions and 12 deletions
+3 -1
View File
@@ -32,7 +32,9 @@
## Topics
- Authors may supply up to five topics through CLI or UI publish and edit surfaces.
- Stored topics preserve author-facing labels. Lookup uses normalized topic slugs.
- UI topic inputs commit new values as normalized lowercase hyphenated slugs so saved metadata
matches browse URLs. Existing stored labels remain unchanged until an author replaces them.
Lookup always uses normalized topic slugs.
- Reserved platform trust labels such as `official`, `officials`, `featured`, `verified`,
`trusted`, `curated`, and brand or channel slugs such as `openclaw`, `clawhub`, and `community`
are rejected. The canonical list lives in `RESERVED_CATALOG_TOPIC_SLUGS` inside
+21 -4
View File
@@ -69,7 +69,7 @@ describe("CatalogMetadataEditor", () => {
await waitFor(() =>
expect(onSave).toHaveBeenCalledWith({
categories: ["development", "research"],
topics: ["GPU development", "CUDA"],
topics: ["GPU development", "cuda"],
}),
);
});
@@ -107,7 +107,24 @@ describe("CatalogMetadataEditor", () => {
await waitFor(() =>
expect(onSave).toHaveBeenCalledWith({
categories: ["other"],
topics: ["Calendar", "Scheduling"],
topics: ["Calendar", "scheduling"],
}),
);
});
it("auto-hyphenates a new multi-word topic before saving", async () => {
const onSave = vi.fn(async () => {});
render(<CatalogMetadataEditor kind="skill" onSave={onSave} />);
const topicsInput = screen.getByLabelText("Topics");
fireEvent.change(topicsInput, { target: { value: "session management" } });
fireEvent.keyDown(topicsInput, { key: "Enter" });
fireEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() =>
expect(onSave).toHaveBeenCalledWith({
categories: ["other"],
topics: ["session-management"],
}),
);
});
@@ -127,7 +144,7 @@ describe("CatalogMetadataEditor", () => {
await waitFor(() =>
expect(onSave).toHaveBeenCalledWith({
categories: ["other"],
topics: ["Calendar", "Scheduling"],
topics: ["Calendar", "scheduling"],
}),
);
});
@@ -167,7 +184,7 @@ describe("CatalogMetadataEditor", () => {
await waitFor(() =>
expect(onSave).toHaveBeenCalledWith({
categories: ["other"],
topics: ["Calendar", "Scheduling"],
topics: ["Calendar", "scheduling"],
}),
);
});
+6 -6
View File
@@ -11,7 +11,7 @@ function TopicInputHarness({ initialValue = "" }: { initialValue?: string }) {
}
describe("CatalogTopicInput", () => {
it("allows typing a multi-word topic before Enter commits it", () => {
it("allows typing a multi-word topic before Enter commits its slug", () => {
render(<TopicInputHarness />);
const input = screen.getByLabelText("Topics");
@@ -22,10 +22,10 @@ describe("CatalogTopicInput", () => {
fireEvent.change(input, { target: { value: "GPU development" } });
fireEvent.keyDown(input, { key: "Enter" });
expect(screen.getByText("#gpu development")).toBeTruthy();
expect(screen.getByText("#gpu-development")).toBeTruthy();
});
it("allows typing a comma-containing topic before Enter commits it", () => {
it("allows typing punctuation before Enter commits the normalized slug", () => {
render(<TopicInputHarness />);
const input = screen.getByLabelText("Topics");
@@ -36,7 +36,7 @@ describe("CatalogTopicInput", () => {
fireEvent.change(input, { target: { value: "CI, CD" } });
fireEvent.keyDown(input, { key: "Enter" });
expect(screen.getByText("#ci, cd")).toBeTruthy();
expect(screen.getByText("#ci-cd")).toBeTruthy();
});
it("normalizes topic chips to lowercase", () => {
@@ -46,14 +46,14 @@ describe("CatalogTopicInput", () => {
expect(screen.getByText("#gpu development")).toBeTruthy();
});
it("commits a pasted multi-word topic with Enter", () => {
it("auto-hyphenates a pasted multi-word topic with Enter", () => {
render(<TopicInputHarness />);
const input = screen.getByLabelText("Topics");
fireEvent.change(input, { target: { value: "GPU Development" } });
fireEvent.keyDown(input, { key: "Enter" });
expect(screen.getByText("#gpu development")).toBeTruthy();
expect(screen.getByText("#gpu-development")).toBeTruthy();
});
it("removes topic chips with their remove button or Backspace", () => {
+1 -1
View File
@@ -66,7 +66,7 @@ export function CatalogTopicInput({ id, value, disabled, onChange }: CatalogTopi
(existingTopic) => normalizeCatalogTopic(existingTopic) === topicSlug,
);
if (!duplicate) {
onChange(formatCatalogTopicsInput([...topics, topic]));
onChange(formatCatalogTopicsInput([...topics, topicSlug ?? topic]));
}
setDraft("");
}