diff --git a/apps/web/src/components/settings/ProjectSettingsPanel.logic.test.ts b/apps/web/src/components/settings/ProjectSettingsPanel.logic.test.ts new file mode 100644 index 000000000000..8a72b3510ceb --- /dev/null +++ b/apps/web/src/components/settings/ProjectSettingsPanel.logic.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { projectGroupTitleNeedsUpdate } from "./ProjectSettingsPanel.logic"; + +describe("projectGroupTitleNeedsUpdate", () => { + it("updates divergent member titles even when the next title is the derived group label", () => { + expect( + projectGroupTitleNeedsUpdate(["local-title", "remote-title"], "Repository name", true), + ).toBe(true); + }); + + it("skips an untouched blur when the derived label differs from member titles", () => { + expect(projectGroupTitleNeedsUpdate(["repo-slug", "repo-slug"], "Repository Name", false)).toBe( + false, + ); + }); + + it("skips an update when every member already has the next title", () => { + expect(projectGroupTitleNeedsUpdate(["Shared name", "Shared name"], "Shared name", true)).toBe( + false, + ); + }); +}); diff --git a/apps/web/src/components/settings/ProjectSettingsPanel.logic.ts b/apps/web/src/components/settings/ProjectSettingsPanel.logic.ts new file mode 100644 index 000000000000..17ff824099fb --- /dev/null +++ b/apps/web/src/components/settings/ProjectSettingsPanel.logic.ts @@ -0,0 +1,7 @@ +export function projectGroupTitleNeedsUpdate( + memberTitles: ReadonlyArray, + nextTitle: string, + wasEdited: boolean, +): boolean { + return wasEdited && memberTitles.some((title) => title !== nextTitle); +} diff --git a/apps/web/src/components/settings/ProjectSettingsPanel.tsx b/apps/web/src/components/settings/ProjectSettingsPanel.tsx index 6768d2dc61ef..e5020e5a6352 100644 --- a/apps/web/src/components/settings/ProjectSettingsPanel.tsx +++ b/apps/web/src/components/settings/ProjectSettingsPanel.tsx @@ -112,6 +112,7 @@ import { canPickExternalProjectFavicon, ProjectFaviconPickerDialog, } from "./ProjectFaviconPickerDialog"; +import { projectGroupTitleNeedsUpdate } from "./ProjectSettingsPanel.logic"; export const PROJECT_GROUPING_MODE_LABELS: Record = { repository: "Group by repository", @@ -303,6 +304,7 @@ function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) { const removeKeybinding = useAtomCommand(serverEnvironment.removeKeybinding, { reportFailure: false, }); + const projectNameEditedRef = useRef(false); const { copyToClipboard: copyPathToClipboard } = useCopyToClipboard<{ path: string }>({ onCopy: ({ path }) => { toastManager.add({ type: "success", title: "Path copied", description: path }); @@ -391,17 +393,24 @@ function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) { ); const renameGroup = useCallback( - async (nextTitle: string) => { + async (nextTitle: string, wasEdited: boolean) => { const title = nextTitle.trim(); if (!title) { toastManager.add({ type: "warning", title: "Project title cannot be empty" }); return; } - if (title === group.displayName) return; - if (group.memberProjects.every((member) => member.title === title)) return; + if ( + !projectGroupTitleNeedsUpdate( + group.memberProjects.map((member) => member.title), + title, + wasEdited, + ) + ) { + return; + } await updateAllMembers({ title }, "Failed to rename project"); }, - [group.displayName, group.memberProjects, updateAllMembers], + [group.memberProjects, updateAllMembers], ); // ----- default model ----- @@ -765,8 +774,13 @@ function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) { className="w-full sm:w-64" aria-label="Project name" defaultValue={group.displayName} + onChange={() => { + projectNameEditedRef.current = true; + }} onBlur={(event) => { - void renameGroup(event.currentTarget.value); + const wasEdited = projectNameEditedRef.current; + projectNameEditedRef.current = false; + void renameGroup(event.currentTarget.value, wasEdited); }} onKeyDown={(event) => { if (event.key === "Enter") event.currentTarget.blur();