authorgravatar for 154406804+MohtashamMurshid@users.noreply.github.comMohtasham Murshid <154406804+MohtashamMurshid@users.noreply.github.com> 2026-08-25 22:55:42+08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-25 20:25:42+05:30
loge67074f80933a27bd3cdc4e24f486358407690fb
tree6f7ba1fbc6d253fd5a984e4578555d3c29e905f2
parent5d7665396083d285132d67038813862a93337ca5
signature Signed by PGP key B5690EEEBB952194

fix(web): keep grouped project renames (#7831)


3 files changed, 49 insertions(+), 5 deletions(-)

apps/web/src/components/settings/ProjectSettingsPanel.logic.test.ts created+23
...@@ -0,0 +1,23 @@
1import { describe, expect, it } from "vite-plus/test";
2
3import { projectGroupTitleNeedsUpdate } from "./ProjectSettingsPanel.logic";
4
5describe("projectGroupTitleNeedsUpdate", () => {
6 it("updates divergent member titles even when the next title is the derived group label", () => {
7 expect(
8 projectGroupTitleNeedsUpdate(["local-title", "remote-title"], "Repository name", true),
9 ).toBe(true);
10 });
11
12 it("skips an untouched blur when the derived label differs from member titles", () => {
13 expect(projectGroupTitleNeedsUpdate(["repo-slug", "repo-slug"], "Repository Name", false)).toBe(
14 false,
15 );
16 });
17
18 it("skips an update when every member already has the next title", () => {
19 expect(projectGroupTitleNeedsUpdate(["Shared name", "Shared name"], "Shared name", true)).toBe(
20 false,
21 );
22 });
23});
apps/web/src/components/settings/ProjectSettingsPanel.logic.ts created+7
...@@ -0,0 +1,7 @@
1export function projectGroupTitleNeedsUpdate(
2 memberTitles: ReadonlyArray<string>,
3 nextTitle: string,
4 wasEdited: boolean,
5): boolean {
6 return wasEdited && memberTitles.some((title) => title !== nextTitle);
7}
apps/web/src/components/settings/ProjectSettingsPanel.tsx+19-5
...@@ -113,6 +113,7 @@ import {...@@ -113,6 +113,7 @@ import {
113 canPickExternalProjectFavicon,113 canPickExternalProjectFavicon,
114 ProjectFaviconPickerDialog,114 ProjectFaviconPickerDialog,
115} from "./ProjectFaviconPickerDialog";115} from "./ProjectFaviconPickerDialog";
116import { projectGroupTitleNeedsUpdate } from "./ProjectSettingsPanel.logic";
116117
117export const PROJECT_GROUPING_MODE_LABELS: Record<SidebarProjectGroupingMode, string> = {118export const PROJECT_GROUPING_MODE_LABELS: Record<SidebarProjectGroupingMode, string> = {
118 repository: "Group by repository",119 repository: "Group by repository",
...@@ -304,6 +305,7 @@ function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) {...@@ -304,6 +305,7 @@ function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) {
304 const removeKeybinding = useAtomCommand(serverEnvironment.removeKeybinding, {305 const removeKeybinding = useAtomCommand(serverEnvironment.removeKeybinding, {
305 reportFailure: false,306 reportFailure: false,
306 });307 });
308 const projectNameEditedRef = useRef(false);
307 const { copyToClipboard: copyPathToClipboard } = useCopyToClipboard<{ path: string }>({309 const { copyToClipboard: copyPathToClipboard } = useCopyToClipboard<{ path: string }>({
308 onCopy: ({ path }) => {310 onCopy: ({ path }) => {
309 toastManager.add({ type: "success", title: "Path copied", description: path });311 toastManager.add({ type: "success", title: "Path copied", description: path });
...@@ -392,17 +394,24 @@ function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) {...@@ -392,17 +394,24 @@ function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) {
392 );394 );
393395
394 const renameGroup = useCallback(396 const renameGroup = useCallback(
395 async (nextTitle: string) => {397 async (nextTitle: string, wasEdited: boolean) => {
396 const title = nextTitle.trim();398 const title = nextTitle.trim();
397 if (!title) {399 if (!title) {
398 toastManager.add({ type: "warning", title: "Project title cannot be empty" });400 toastManager.add({ type: "warning", title: "Project title cannot be empty" });
399 return;401 return;
400 }402 }
401 if (title === group.displayName) return;403 if (
402 if (group.memberProjects.every((member) => member.title === title)) return;404 !projectGroupTitleNeedsUpdate(
405 group.memberProjects.map((member) => member.title),
406 title,
407 wasEdited,
408 )
409 ) {
410 return;
411 }
403 await updateAllMembers({ title }, "Failed to rename project");412 await updateAllMembers({ title }, "Failed to rename project");
404 },413 },
405 [group.displayName, group.memberProjects, updateAllMembers],414 [group.memberProjects, updateAllMembers],
406 );415 );
407416
408 // ----- default model -----417 // ----- default model -----
...@@ -767,8 +776,13 @@ function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) {...@@ -767,8 +776,13 @@ function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) {
767 className="w-full sm:w-64"776 className="w-full sm:w-64"
768 aria-label="Project name"777 aria-label="Project name"
769 defaultValue={group.displayName}778 defaultValue={group.displayName}
779 onChange={() => {
780 projectNameEditedRef.current = true;
781 }}
770 onBlur={(event) => {782 onBlur={(event) => {
771 void renameGroup(event.currentTarget.value);783 const wasEdited = projectNameEditedRef.current;
784 projectNameEditedRef.current = false;
785 void renameGroup(event.currentTarget.value, wasEdited);
772 }}786 }}
773 onKeyDown={(event) => {787 onKeyDown={(event) => {
774 if (event.key === "Enter") event.currentTarget.blur();788 if (event.key === "Enter") event.currentTarget.blur();