Skip to content

Commit 7eb36bf

Browse files
author
PR Batch Tester
committed
Merge remote-tracking branch 'origin/main' into HEAD
2 parents 158f0e8 + 00cdceb commit 7eb36bf

95 files changed

Lines changed: 4496 additions & 745 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/mobile/src/components/AndroidScreenHeader.tsx‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AppText as Text } from "./AppText";
66
import { cn } from "../lib/cn";
77
import { MaterialIconButton } from "./MaterialIconButton";
88
import { AndroidAnchoredMenu } from "./AndroidAnchoredMenu";
9+
import { splitAndroidHeaderActions } from "./headerMenu.android";
910
import { useScaledTextRole } from "../features/settings/appearance/useScaledTextRole";
1011
import { useMaterialToolbarLayout } from "./useMaterialToolbarLayout";
1112

@@ -16,6 +17,8 @@ export interface AndroidHeaderAction {
1617
readonly disabled?: boolean;
1718
readonly loading?: boolean;
1819
readonly selected?: boolean;
20+
readonly destructive?: boolean;
21+
readonly overflowOnly?: boolean;
1922
}
2023

2124
export function AndroidHeaderIconButton(props: {
@@ -45,10 +48,10 @@ export function AndroidScreenHeader(props: {
4548
props.embedded,
4649
);
4750
const [headerWidth, setHeaderWidth] = useState(0);
48-
const actions = props.actions ?? [];
49-
const directCount = actions.length > 2 ? (headerWidth >= 600 ? 3 : 1) : actions.length;
50-
const visibleActions = actions.slice(0, directCount);
51-
const overflowActions = actions.slice(directCount);
51+
const { visibleActions, overflowActions } = splitAndroidHeaderActions(
52+
props.actions ?? [],
53+
headerWidth >= 600,
54+
);
5255

5356
return (
5457
<View
@@ -115,6 +118,7 @@ export function AndroidScreenHeader(props: {
115118
title: action.accessibilityLabel,
116119
attributes: {
117120
disabled: Boolean(action.disabled),
121+
destructive: action.destructive,
118122
state: action.selected ? "on" : undefined,
119123
},
120124
}))}

‎apps/mobile/src/components/AppSymbol.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ const ANDROID_ICON_BY_SF_SYMBOL = {
175175
circle: IconCircle,
176176
"circle.dashed": IconCircleDashed,
177177
"circle.inset.filled": IconCircleDot,
178+
"circle.dashed.inset.filled": IconCircleDot,
178179
clock: IconClock,
179180
timer: IconClock,
180181
ticket: IconTicket,

‎apps/mobile/src/components/headerMenu.android.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,16 @@ export function findHeaderMenuAction(
4141
}
4242
return undefined;
4343
}
44+
45+
export function splitAndroidHeaderActions<Action extends { readonly overflowOnly?: boolean }>(
46+
actions: ReadonlyArray<Action>,
47+
wide: boolean,
48+
) {
49+
const inline = actions.filter((action) => !action.overflowOnly);
50+
const directCount = actions.length > 2 ? (wide ? 3 : 1) : inline.length;
51+
const visibleActions = inline.slice(0, directCount);
52+
return {
53+
visibleActions,
54+
overflowActions: actions.filter((action) => !visibleActions.includes(action)),
55+
};
56+
}

‎apps/mobile/src/components/headerMenu.test.ts‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { describe, expect, it } from "vite-plus/test";
2-
import { androidHeaderMenuActions, findHeaderMenuAction } from "./headerMenu.android";
2+
import {
3+
androidHeaderMenuActions,
4+
findHeaderMenuAction,
5+
splitAndroidHeaderActions,
6+
} from "./headerMenu.android";
37
import type { ScreenHeaderMenuItem } from "./ScreenHeader.types";
48

59
const items: ReadonlyArray<ScreenHeaderMenuItem> = [
@@ -63,4 +67,36 @@ describe("header menus", () => {
6367
expect(findHeaderMenuAction(items, "sessions")).toBeUndefined();
6468
expect(findHeaderMenuAction(items, "removed-session")).toBeUndefined();
6569
});
70+
71+
it("keeps overflow-only Android header actions in the overflow menu", () => {
72+
const action = (accessibilityLabel: string, overflowOnly?: boolean) => ({
73+
accessibilityLabel,
74+
icon: "trash" as const,
75+
onPress: () => {},
76+
overflowOnly,
77+
});
78+
const labels = (
79+
split: ReturnType<typeof splitAndroidHeaderActions<ReturnType<typeof action>>>,
80+
) => ({
81+
visible: split.visibleActions.map((item) => item.accessibilityLabel),
82+
overflow: split.overflowActions.map((item) => item.accessibilityLabel),
83+
});
84+
const open = action("Open");
85+
const remove = action("Delete", true);
86+
expect(labels(splitAndroidHeaderActions([open, action("Comment")], false))).toEqual({
87+
visible: ["Open", "Comment"],
88+
overflow: [],
89+
});
90+
expect(labels(splitAndroidHeaderActions([open, remove], true))).toEqual({
91+
visible: ["Open"],
92+
overflow: ["Delete"],
93+
});
94+
expect(labels(splitAndroidHeaderActions([remove], true))).toEqual({
95+
visible: [],
96+
overflow: ["Delete"],
97+
});
98+
expect(
99+
labels(splitAndroidHeaderActions([action("Review"), action("Comment"), open, remove], true)),
100+
).toEqual({ visible: ["Review", "Comment", "Open"], overflow: ["Delete"] });
101+
});
66102
});

‎apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx‎

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,20 @@ import {
1717
PR_ROW_EXIT,
1818
PR_POSITION_LAYOUT,
1919
} from "./pullRequestAnimations";
20-
import { openOnProviderLabel } from "./pullRequestActions";
20+
import {
21+
canPerformPullRequestAction,
22+
openOnProviderLabel,
23+
PR_ACTION_ICONS,
24+
PR_ACTION_LABELS,
25+
} from "./pullRequestActions";
2126
import * as Clipboard from "expo-clipboard";
2227
import { ControlPillMenu } from "../../components/ControlPillMenu";
2328
import { MaterialIconButton } from "../../components/MaterialIconButton";
2429
import { MaterialButton } from "../../components/MaterialButton";
2530
import { usePullRequestReviewDraft } from "./usePullRequestReviewDraft";
2631
import { usePullRequestFilesViewed } from "./usePullRequestFilesViewed";
2732
import { usePullRequestPermissions } from "./usePullRequestPermissions";
33+
import { usePullRequestAction } from "./usePullRequestAction";
2834
import { annotatePullRequestDiff, reviewPosition } from "./pullRequestDiffComments";
2935
import { usePullRequestFileFilter } from "./usePullRequestFileFilter";
3036
import { isPullRequestTestFile } from "./pullRequestFileFilters";
@@ -341,6 +347,15 @@ export function PullRequestRouteScreen({ route }: StaticScreenProps<PullRequestP
341347
const summary = params.initialSummary;
342348
const identity = pr ?? summary;
343349
const permissions = usePullRequestPermissions(params, pr);
350+
const action = usePullRequestAction(params, pr);
351+
const showsDeleteBranch = pr !== null && canPerformPullRequestAction(pr, "delete-branch");
352+
const confirmDeleteBranch = () =>
353+
pr &&
354+
action.confirm(
355+
"delete-branch",
356+
PR_ACTION_LABELS["delete-branch"],
357+
`Delete ${pr.headBranch} from ${pr.headRepositoryNameWithOwner ?? pr.repository}? Local branches and worktrees are not changed.`,
358+
);
344359
const stack = usePullRequestStack(params, pr);
345360
const refreshActivity = useAtomRefresh(composerPullRequests.activity(requestTarget(params)));
346361
const refreshGuide = useAtomRefresh(composerPullRequests.reviewGuide(requestTarget(params)));
@@ -417,6 +432,18 @@ export function PullRequestRouteScreen({ route }: StaticScreenProps<PullRequestP
417432
},
418433
]
419434
: []),
435+
...(showsDeleteBranch
436+
? [
437+
{
438+
accessibilityLabel: PR_ACTION_LABELS["delete-branch"],
439+
icon: PR_ACTION_ICONS["delete-branch"],
440+
disabled: !action.available("delete-branch"),
441+
destructive: true,
442+
overflowOnly: true,
443+
onPress: confirmDeleteBranch,
444+
},
445+
]
446+
: []),
420447
]}
421448
/>
422449
) : null}
@@ -456,6 +483,25 @@ export function PullRequestRouteScreen({ route }: StaticScreenProps<PullRequestP
456483
</NativeHeaderToolbar.Label>
457484
</NativeHeaderToolbar.MenuAction>
458485
) : null}
486+
{showsDeleteBranch ? (
487+
<NativeHeaderToolbar.MenuAction
488+
icon={PR_ACTION_ICONS["delete-branch"]}
489+
destructive
490+
disabled={!action.available("delete-branch")}
491+
subtitle={
492+
action.busy
493+
? "Deleting…"
494+
: connected
495+
? pr.headBranch
496+
: "Reconnect to delete this branch."
497+
}
498+
onPress={confirmDeleteBranch}
499+
>
500+
<NativeHeaderToolbar.Label>
501+
{PR_ACTION_LABELS["delete-branch"]}
502+
</NativeHeaderToolbar.Label>
503+
</NativeHeaderToolbar.MenuAction>
504+
) : null}
459505
</NativeHeaderToolbar.Menu>
460506
</NativeHeaderToolbar>
461507
<View

‎apps/mobile/src/features/pull-requests/pullRequestActions.test.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,19 @@ describe("mobile pull request actions", () => {
112112
expect(mergeSummary(unknown, true)).toBe("Squash");
113113
});
114114

115+
it("offers deleting the source branch only once merged or closed on a host that supports it", () => {
116+
const capabilities = { ...detail().capabilities, deleteBranch: true };
117+
expect(canPerformPullRequestAction(detail({ capabilities }), "delete-branch")).toBe(false);
118+
for (const state of ["merged", "closed"] as const) {
119+
expect(canPerformPullRequestAction(detail({ state, capabilities }), "delete-branch")).toBe(
120+
true,
121+
);
122+
expect(canPerformPullRequestAction(detail({ state }), "delete-branch")).toBe(false);
123+
}
124+
const refused = detail({ state: "merged", capabilities, viewerPermissions: { actions: [] } });
125+
expect(canPerformPullRequestAction(refused, "delete-branch")).toBe(false);
126+
});
127+
115128
it("cannot merge when the repository has disabled every offered strategy", () => {
116129
const pr = detail({ mergeCapabilities: { merge: false, squash: false, rebase: false } });
117130
expect(canPerformPullRequestAction(pr, "merge")).toBe(false);

‎apps/mobile/src/features/pull-requests/pullRequestActions.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ type ActionDetail = Pick<
1313
> & {
1414
readonly capabilities: Pick<
1515
PullRequestDetail["capabilities"],
16-
"actions" | "mergeMethods" | "updateMethods"
16+
"actions" | "mergeMethods" | "updateMethods" | "deleteBranch"
1717
>;
1818
readonly viewerPermissions: Pick<
1919
PullRequestDetail["viewerPermissions"],
@@ -41,6 +41,7 @@ export const PR_ACTION_LABELS = {
4141
"enable-auto-merge": "Enable auto-merge",
4242
"disable-auto-merge": "Disable auto-merge",
4343
revert: "Create revert pull request",
44+
"delete-branch": "Delete branch",
4445
"approve-workflows": "Approve workflows",
4546
} as const satisfies Record<PullRequestAction, string>;
4647

@@ -54,6 +55,7 @@ export const PR_ACTION_ICONS = {
5455
"enable-auto-merge": "clock",
5556
"disable-auto-merge": "xmark",
5657
revert: "arrow.uturn.backward",
58+
"delete-branch": "trash",
5759
"approve-workflows": "checkmark",
5860
} as const satisfies Record<PullRequestAction, AppSymbolName>;
5961

@@ -68,6 +70,7 @@ export const ALL_PR_ACTIONS: readonly PullRequestAction[] = [
6870
"close",
6971
"reopen",
7072
"revert",
73+
"delete-branch",
7174
];
7275

7376
export function allowedMergeMethods(pr: ActionDetail) {
@@ -86,6 +89,8 @@ export function canPerformPullRequestAction(pr: ActionDetail, action: PullReques
8689
return false;
8790
if (action === "reopen") return pr.state === "closed";
8891
if (action === "revert") return pr.state === "merged";
92+
if (action === "delete-branch")
93+
return pr.state !== "open" && pr.capabilities.deleteBranch === true;
8994
if (pr.state !== "open") return false;
9095
switch (action) {
9196
case "update-branch":

‎apps/mobile/src/features/pull-requests/usePullRequestAction.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { pullRequestTarget, type PullRequestParams } from "./usePullRequestDiff"
1414
import { usePullRequestPermissions } from "./usePullRequestPermissions";
1515

1616
/** Small row actions stay in place, preserving expanded rows and scroll position. */
17-
export function usePullRequestAction(params: PullRequestParams, pr: PullRequestDetail) {
17+
export function usePullRequestAction(params: PullRequestParams, pr: PullRequestDetail | null) {
1818
const target = pullRequestTarget(params);
1919
const permissions = usePullRequestPermissions(params, pr);
2020
const run = useAtomCommand(composerPullRequests.runAction, { reportFailure: false });
@@ -23,7 +23,7 @@ export function usePullRequestAction(params: PullRequestParams, pr: PullRequestD
2323
const pending = useRef(false);
2424
const [busy, setBusy] = useState(false);
2525
const available = (action: PullRequestAction) =>
26-
!busy && permissions.canWrite && canPerformPullRequestAction(pr, action);
26+
pr !== null && !busy && permissions.canWrite && canPerformPullRequestAction(pr, action);
2727
const perform = async (action: PullRequestAction, updateMethod?: PullRequestUpdateMethod) => {
2828
if (pending.current || !available(action)) return;
2929
pending.current = true;
@@ -51,7 +51,7 @@ export function usePullRequestAction(params: PullRequestParams, pr: PullRequestD
5151
{ text: "Cancel", style: "cancel" },
5252
{
5353
text: title,
54-
style: action === "close" ? "destructive" : "default",
54+
style: action === "close" || action === "delete-branch" ? "destructive" : "default",
5555
onPress: () => void perform(action),
5656
},
5757
]);

‎apps/mobile/src/features/threads/thread-list-v2-items.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ const STATUS_LABEL_BY_STATUS: Partial<Record<ThreadListV2Status, StatusLabel>> =
9292
},
9393
waiting: {
9494
label: "Waiting",
95-
icon: "clock",
95+
icon: "circle.dashed.inset.filled",
9696
className: "text-foreground-muted",
9797
iconTintClassName: "accent-foreground-muted",
9898
},

‎apps/server/src/environment/ServerEnvironment.test.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,13 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
118118
expect(descriptor.capabilities.serverSelfUpdate).toBe(
119119
mode === "web" ? undefined : "desktop-managed",
120120
);
121+
expect(descriptor.capabilities).toMatchObject({
122+
settledWorktreeCleanup: true,
123+
storageCleanupRun: true,
124+
projectWorktreeCleanup: true,
125+
worktreesDirectory: true,
126+
threadAutoSettlementScope: true,
127+
});
121128
}
122129
}),
123130
);

0 commit comments

Comments
 (0)