Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import type { ReviewCommentContext } from "~/reviewCommentContext";
import { useProjects } from "~/state/entities";
import { useEnvironments } from "~/state/environments";
import { useEnvironmentQuery } from "~/state/query";
import { useUiStateStore } from "~/uiStateStore";
import { useLiveRefresh } from "~/hooks/useLiveRefresh";
import { pullRequestEnvironment } from "~/state/pullRequests";
import { useAtomCommand } from "~/state/use-atom-command";
Expand Down Expand Up @@ -453,7 +454,10 @@ export function PullRequestDetailPanel({
compensationRef.current = null;
if (scroller) scroller.scrollTop = Math.max(0, scroller.scrollTop + delta);
}, [condensed]);
const [mergeMethod, setMergeMethod] = useState<PullRequestMergeMethod>("merge");
// The last method the reader picked, remembered across pull requests and sessions. A
// repository that refuses it still falls back below, so nothing here can force a bad merge.
const mergeMethod = useUiStateStore((state) => state.pullRequestMergeMethod);
const setMergeMethod = useUiStateStore((state) => state.setPullRequestMergeMethod);
const [confirmation, setConfirmation] = useState<{
readonly open: boolean;
readonly action: "merge" | "close" | "enable-auto-merge";
Expand Down Expand Up @@ -1418,9 +1422,7 @@ export function PullRequestDetailPanel({
{showsDraftToggle ? <MenuSeparator /> : null}
<MenuRadioGroup
value={selectedMergeMethod}
onValueChange={(method) =>
setMergeMethod(method as PullRequestMergeMethod)
}
onValueChange={setMergeMethod}
>
{allowedMergeMethods.map((method) => (
<MenuRadioItem key={method} value={method} disabled={actionPending}>
Expand Down
25 changes: 24 additions & 1 deletion apps/web/src/uiStateStore.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ProjectId, ThreadId } from "@t3tools/contracts";
import { ProjectId, ThreadId, type PullRequestMergeMethod } from "@t3tools/contracts";
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";

import {
Expand All @@ -12,6 +12,7 @@ import {
reorderProjects,
resolveProjectExpanded,
setDefaultAdvertisedEndpointKey,
setPullRequestMergeMethod,
setProjectExpanded,
setThreadChangedFilesExpanded,
type UiState,
Expand All @@ -24,6 +25,7 @@ function makeUiState(overrides: Partial<UiState> = {}): UiState {
threadLastVisitedAtById: {},
threadChangedFilesExpandedById: {},
defaultAdvertisedEndpointKey: null,
pullRequestMergeMethod: "merge",
...overrides,
};
}
Expand Down Expand Up @@ -144,6 +146,24 @@ describe("uiStateStore pure functions", () => {
defaultAdvertisedEndpointKey: null,
});
});

it("stores the last merge method a reader picked", () => {
const next = setPullRequestMergeMethod(makeUiState(), "squash");

expect(next.pullRequestMergeMethod).toBe("squash");
expect(setPullRequestMergeMethod(next, "squash")).toBe(next);
expect(setPullRequestMergeMethod(next, "rebase").pullRequestMergeMethod).toBe("rebase");
});

it("ignores unknown merge methods", () => {
const state = makeUiState({ pullRequestMergeMethod: "squash" });

expect(setPullRequestMergeMethod(state, "fast-forward" as PullRequestMergeMethod)).toBe(state);
expect(parsePersistedState({ pullRequestMergeMethod: "fast-forward" })).toMatchObject({
pullRequestMergeMethod: "merge",
});
expect(parsePersistedState({})).toMatchObject({ pullRequestMergeMethod: "merge" });
});
});

describe("parsePersistedState", () => {
Expand Down Expand Up @@ -183,6 +203,7 @@ describe("parsePersistedState", () => {
"turn-2": true,
},
},
pullRequestMergeMethod: "merge",
});
});

Expand Down Expand Up @@ -280,6 +301,7 @@ describe("uiStateStore persistence", () => {
},
},
defaultAdvertisedEndpointKey: "desktop-core:lan:http",
pullRequestMergeMethod: "squash",
});

persistState(state);
Expand All @@ -303,6 +325,7 @@ describe("uiStateStore persistence", () => {
"turn-2": true,
},
},
pullRequestMergeMethod: "squash",
});
expect(parsePersistedState(persisted)).toEqual({
...state,
Expand Down
31 changes: 30 additions & 1 deletion apps/web/src/uiStateStore.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import type { PullRequestMergeMethod } from "@t3tools/contracts";
import { Debouncer } from "@tanstack/react-pacer";
import { create } from "zustand";
import { normalizeProjectPathForComparison } from "./lib/projectPaths";

export const PERSISTED_STATE_KEY = "t3code:ui-state:v1";
const THREAD_CHANGED_FILES_EXPANSION_VERSION = 1;
const MERGE_METHODS = [
"merge",
"squash",
"rebase",
] as const satisfies readonly PullRequestMergeMethod[];
const LEGACY_PERSISTED_STATE_KEYS = [
"t3code:renderer-state:v8",
"t3code:renderer-state:v7",
Expand All @@ -27,6 +33,7 @@ export interface PersistedUiState {
defaultAdvertisedEndpointKey?: string | null;
threadChangedFilesExpansionVersion?: typeof THREAD_CHANGED_FILES_EXPANSION_VERSION;
threadChangedFilesExpandedById?: Record<string, Record<string, boolean>>;
pullRequestMergeMethod?: string;
}

export interface UiProjectState {
Expand All @@ -43,14 +50,20 @@ export interface UiEndpointState {
defaultAdvertisedEndpointKey: string | null;
}

export interface UiState extends UiProjectState, UiThreadState, UiEndpointState {}
export interface UiPullRequestState {
pullRequestMergeMethod: PullRequestMergeMethod;
}

export interface UiState
extends UiProjectState, UiThreadState, UiEndpointState, UiPullRequestState {}

const initialState: UiState = {
projectExpandedById: {},
projectOrder: [],
threadLastVisitedAtById: {},
threadChangedFilesExpandedById: {},
defaultAdvertisedEndpointKey: null,
pullRequestMergeMethod: "merge",
};

const LEGACY_PROJECT_CWD_PREFERENCE_PREFIX = "legacy-project-cwd:";
Expand Down Expand Up @@ -135,6 +148,9 @@ export function parsePersistedState(parsed: PersistedUiState): UiState {
parsed.defaultAdvertisedEndpointKey.length > 0
? parsed.defaultAdvertisedEndpointKey
: null,
pullRequestMergeMethod:
MERGE_METHODS.find((method) => method === parsed.pullRequestMergeMethod) ??
initialState.pullRequestMergeMethod,
};
}

Expand Down Expand Up @@ -207,6 +223,7 @@ export function persistState(state: UiState): void {
defaultAdvertisedEndpointKey: state.defaultAdvertisedEndpointKey,
threadChangedFilesExpansionVersion: THREAD_CHANGED_FILES_EXPANSION_VERSION,
threadChangedFilesExpandedById: state.threadChangedFilesExpandedById,
pullRequestMergeMethod: state.pullRequestMergeMethod,
} satisfies PersistedUiState),
);
if (!legacyKeysCleanedUp) {
Expand Down Expand Up @@ -304,6 +321,16 @@ export function setDefaultAdvertisedEndpointKey(state: UiState, key: string | nu
};
}

export function setPullRequestMergeMethod(state: UiState, method: PullRequestMergeMethod): UiState {
if (!MERGE_METHODS.includes(method) || state.pullRequestMergeMethod === method) {
return state;
}
return {
...state,
pullRequestMergeMethod: method,
};
}

export function resolveProjectExpanded(
projectExpandedById: Readonly<Record<string, boolean>>,
preferenceKeys: readonly string[],
Expand Down Expand Up @@ -386,6 +413,7 @@ interface UiStateStore extends UiState {
markThreadUnread: (threadId: string, latestTurnCompletedAt: string | null | undefined) => void;
setThreadChangedFilesExpanded: (threadId: string, turnId: string, expanded: boolean) => void;
setDefaultAdvertisedEndpointKey: (key: string | null) => void;
setPullRequestMergeMethod: (method: PullRequestMergeMethod) => void;
setProjectExpanded: (projectIds: string | readonly string[], expanded: boolean) => void;
reorderProjects: (
currentProjectOrder: readonly string[],
Expand All @@ -404,6 +432,7 @@ export const useUiStateStore = create<UiStateStore>((set) => ({
set((state) => setThreadChangedFilesExpanded(state, threadId, turnId, expanded)),
setDefaultAdvertisedEndpointKey: (key) =>
set((state) => setDefaultAdvertisedEndpointKey(state, key)),
setPullRequestMergeMethod: (method) => set((state) => setPullRequestMergeMethod(state, method)),
setProjectExpanded: (projectIds, expanded) =>
set((state) => setProjectExpanded(state, projectIds, expanded)),
reorderProjects: (currentProjectOrder, draggedProjectIds, targetProjectIds) =>
Expand Down
Loading