diff --git a/frontend/src/i18n.tsx b/frontend/src/i18n.tsx index 7b418c2..94a00fb 100644 --- a/frontend/src/i18n.tsx +++ b/frontend/src/i18n.tsx @@ -44,6 +44,7 @@ const english = { "例如 deepseek/deepseek-v3": "For example, deepseek/deepseek-v3", "例如 siliconflow": "For example, siliconflow", "例如 team-ppio": "For example, team-ppio", + "留空则使用 Profile ID": "Leave blank to use the Profile ID", "例如 团队 PPIO": "For example, Team PPIO", "粘贴你的 API Key": "Paste your API key", "隐藏密钥": "Hide API key", diff --git a/frontend/src/pages/ProfilesPage.test.tsx b/frontend/src/pages/ProfilesPage.test.tsx index 7f0ebd2..efba164 100644 --- a/frontend/src/pages/ProfilesPage.test.tsx +++ b/frontend/src/pages/ProfilesPage.test.tsx @@ -145,6 +145,34 @@ describe("ProfilesPage", () => { }))); }); + it("saves a Profile whose name was left blank", async () => { + // The backend fills an empty label in from the existing value or the ID + // (internal/profile/write.go:71-74), so requiring one here was stricter than + // the write path and blocked a rename-to-nothing edit. + const save = vi.spyOn(api, "saveProfile").mockResolvedValue(profile()); + renderPage([profile()]); + fireEvent.click(screen.getByRole("button", { name: "编辑 团队 PPIO" })); + const label = screen.getByLabelText("名称"); + expect(label.hasAttribute("required")).toBe(false); + fireEvent.change(label, { target: { value: "" } }); + + const button = screen.getByRole("button", { name: "保存 Profile" }); + expect(button.hasAttribute("disabled")).toBe(false); + fireEvent.click(button); + await waitFor(() => expect(save).toHaveBeenCalledWith(expect.objectContaining({ id: "team-ppio", label: "" }))); + }); + + it("still refuses to save without a model", async () => { + // model has no backend fallback (write.go:50-53), so this one stays required. + const save = vi.spyOn(api, "saveProfile"); + renderPage([profile()]); + fireEvent.click(screen.getByRole("button", { name: "编辑 团队 PPIO" })); + fireEvent.change(screen.getByLabelText("模型"), { target: { value: "" } }); + + expect(screen.getByRole("button", { name: "保存 Profile" }).hasAttribute("disabled")).toBe(true); + expect(save).not.toHaveBeenCalled(); + }); + it("applies one Profile to all of its Agents", async () => { const install = vi.spyOn(api, "install").mockResolvedValue({ ok: true, diff --git a/frontend/src/pages/ProfilesPage.tsx b/frontend/src/pages/ProfilesPage.tsx index 4f4d3b3..fce905b 100644 --- a/frontend/src/pages/ProfilesPage.tsx +++ b/frontend/src/pages/ProfilesPage.tsx @@ -57,9 +57,9 @@ export function ProfilesPage() { const nameOf = (agentId: string) => status.catalog.find((item) => item.id === agentId)?.name || agentId; const providerHasKey = Boolean(editor && status.providers[editor.provider]?.has_key); - const canSave = Boolean( - editor?.id.trim() && editor.label.trim() && editor.model.trim() && editor.agentIds.length, - ); + // label is absent on purpose: the backend fills it in from the existing value + // or the ID, so demanding it here was stricter than the write path. + const canSave = Boolean(editor?.id.trim() && editor.model.trim() && editor.agentIds.length); // Creating a Profile goes through onboarding: it collects the Agent, key, // model and name in order, tests the connection, and the install writes the @@ -163,7 +163,18 @@ export function ProfilesPage() {