mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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("");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user