mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Replace git action disables with unavailability toasts
This commit is contained in:
@@ -79,7 +79,7 @@ describe("git-actions-policy", () => {
|
||||
expect(actions.primary).toMatchObject({ id: "pull", label: "Pull" });
|
||||
});
|
||||
|
||||
it("disables push with a short pull-first message when the branch diverged", () => {
|
||||
it("keeps push clickable with a clearer message when the branch diverged", () => {
|
||||
const actions = buildGitActions(
|
||||
createInput({
|
||||
hasRemote: true,
|
||||
@@ -90,8 +90,8 @@ describe("git-actions-policy", () => {
|
||||
const pushAction = actions.secondary.find((action) => action.id === "push");
|
||||
|
||||
expect(pushAction).toMatchObject({
|
||||
disabled: true,
|
||||
description: "Pull first",
|
||||
disabled: false,
|
||||
unavailableMessage: "Push isn't available yet because there are newer changes to bring in first",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,6 +108,17 @@ describe("git-actions-policy", () => {
|
||||
expect(updateAction).toMatchObject({
|
||||
label: "Update from main",
|
||||
disabled: false,
|
||||
unavailableMessage: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses a clear sentence when pull is unavailable", () => {
|
||||
const actions = buildGitActions(createInput({ hasRemote: true }));
|
||||
const pullAction = actions.secondary.find((action) => action.id === "pull");
|
||||
|
||||
expect(pullAction).toMatchObject({
|
||||
disabled: false,
|
||||
unavailableMessage: "Pull isn't available because this branch is already up to date",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export interface GitAction {
|
||||
successLabel: string;
|
||||
disabled: boolean;
|
||||
status: ActionStatus;
|
||||
description?: string;
|
||||
unavailableMessage?: string;
|
||||
icon?: ReactElement;
|
||||
handler: () => void;
|
||||
}
|
||||
@@ -82,9 +82,9 @@ export function buildGitActions(input: BuildGitActionsInput): GitActions {
|
||||
label: "Pull",
|
||||
pendingLabel: "Pulling...",
|
||||
successLabel: "Pulled",
|
||||
disabled: input.runtime.pull.disabled || !canPull(input),
|
||||
disabled: input.runtime.pull.disabled,
|
||||
status: input.runtime.pull.status,
|
||||
description: getPullDescription(input),
|
||||
unavailableMessage: input.runtime.pull.disabled ? undefined : getPullUnavailableMessage(input),
|
||||
icon: input.runtime.pull.icon,
|
||||
handler: input.runtime.pull.handler,
|
||||
});
|
||||
@@ -94,9 +94,9 @@ export function buildGitActions(input: BuildGitActionsInput): GitActions {
|
||||
label: "Push",
|
||||
pendingLabel: "Pushing...",
|
||||
successLabel: "Pushed",
|
||||
disabled: input.runtime.push.disabled || !canPush(input),
|
||||
disabled: input.runtime.push.disabled,
|
||||
status: input.runtime.push.status,
|
||||
description: getPushDescription(input),
|
||||
unavailableMessage: input.runtime.push.disabled ? undefined : getPushUnavailableMessage(input),
|
||||
icon: input.runtime.push.icon,
|
||||
handler: input.runtime.push.handler,
|
||||
});
|
||||
@@ -108,9 +108,10 @@ export function buildGitActions(input: BuildGitActionsInput): GitActions {
|
||||
label: `Merge into ${input.baseRefLabel}`,
|
||||
pendingLabel: "Merging...",
|
||||
successLabel: "Merged",
|
||||
disabled: input.runtime["merge-branch"].disabled || !canMergeBranch(input),
|
||||
disabled: input.runtime["merge-branch"].disabled,
|
||||
status: input.runtime["merge-branch"].status,
|
||||
description: getMergeBranchDescription(input),
|
||||
unavailableMessage:
|
||||
input.runtime["merge-branch"].disabled ? undefined : getMergeBranchUnavailableMessage(input),
|
||||
icon: input.runtime["merge-branch"].icon,
|
||||
handler: input.runtime["merge-branch"].handler,
|
||||
});
|
||||
@@ -120,9 +121,12 @@ export function buildGitActions(input: BuildGitActionsInput): GitActions {
|
||||
label: `Update from ${input.baseRefLabel}`,
|
||||
pendingLabel: "Updating...",
|
||||
successLabel: "Updated",
|
||||
disabled: input.runtime["merge-from-base"].disabled || !canMergeFromBase(input),
|
||||
disabled: input.runtime["merge-from-base"].disabled,
|
||||
status: input.runtime["merge-from-base"].status,
|
||||
description: getMergeFromBaseDescription(input),
|
||||
unavailableMessage:
|
||||
input.runtime["merge-from-base"].disabled
|
||||
? undefined
|
||||
: getMergeFromBaseUnavailableMessage(input),
|
||||
icon: input.runtime["merge-from-base"].icon,
|
||||
handler: input.runtime["merge-from-base"].handler,
|
||||
});
|
||||
@@ -132,9 +136,12 @@ export function buildGitActions(input: BuildGitActionsInput): GitActions {
|
||||
label: "Archive worktree",
|
||||
pendingLabel: "Archiving...",
|
||||
successLabel: "Archived",
|
||||
disabled: input.runtime["archive-worktree"].disabled || !input.isPaseoOwnedWorktree,
|
||||
disabled: input.runtime["archive-worktree"].disabled,
|
||||
status: input.runtime["archive-worktree"].status,
|
||||
description: input.isPaseoOwnedWorktree ? undefined : "Only for worktrees",
|
||||
unavailableMessage:
|
||||
input.runtime["archive-worktree"].disabled || input.isPaseoOwnedWorktree
|
||||
? undefined
|
||||
: "Archive isn't available here because this workspace was not created as a Paseo worktree",
|
||||
icon: input.runtime["archive-worktree"].icon,
|
||||
handler: input.runtime["archive-worktree"].handler,
|
||||
});
|
||||
@@ -189,9 +196,12 @@ function buildPrAction(input: BuildGitActionsInput): GitAction {
|
||||
label: "View PR",
|
||||
pendingLabel: "View PR",
|
||||
successLabel: "View PR",
|
||||
disabled: input.runtime.pr.disabled || !input.githubFeaturesEnabled,
|
||||
disabled: input.runtime.pr.disabled,
|
||||
status: input.runtime.pr.status,
|
||||
description: input.githubFeaturesEnabled ? undefined : "GitHub unavailable",
|
||||
unavailableMessage:
|
||||
input.runtime.pr.disabled || input.githubFeaturesEnabled
|
||||
? undefined
|
||||
: "View PR isn't available right now because GitHub isn't connected",
|
||||
icon: input.runtime.pr.icon,
|
||||
handler: input.runtime.pr.handler,
|
||||
};
|
||||
@@ -202,9 +212,10 @@ function buildPrAction(input: BuildGitActionsInput): GitAction {
|
||||
label: "Create PR",
|
||||
pendingLabel: "Creating PR...",
|
||||
successLabel: "PR Created",
|
||||
disabled: input.runtime.pr.disabled || !input.githubFeaturesEnabled || input.aheadCount === 0,
|
||||
disabled: input.runtime.pr.disabled,
|
||||
status: input.runtime.pr.status,
|
||||
description: getCreatePrDescription(input),
|
||||
unavailableMessage:
|
||||
input.runtime.pr.disabled ? undefined : getCreatePrUnavailableMessage(input),
|
||||
icon: input.runtime.pr.icon,
|
||||
handler: input.runtime.pr.handler,
|
||||
};
|
||||
@@ -240,64 +251,64 @@ function canMergeFromBase(input: BuildGitActionsInput): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function getPullDescription(input: BuildGitActionsInput): string | undefined {
|
||||
function getPullUnavailableMessage(input: BuildGitActionsInput): string | undefined {
|
||||
if (!input.hasRemote) {
|
||||
return "No remote";
|
||||
return "Pull isn't available here because this branch is not connected to a remote yet";
|
||||
}
|
||||
if (input.hasUncommittedChanges) {
|
||||
return "Clean tree";
|
||||
return "Pull isn't available while you have local changes so commit or stash them first";
|
||||
}
|
||||
if (input.behindOfOrigin === 0) {
|
||||
return "Nothing to pull";
|
||||
return "Pull isn't available because this branch is already up to date";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getPushDescription(input: BuildGitActionsInput): string | undefined {
|
||||
function getPushUnavailableMessage(input: BuildGitActionsInput): string | undefined {
|
||||
if (!input.hasRemote) {
|
||||
return "No remote";
|
||||
return "Push isn't available here because this branch is not connected to a remote yet";
|
||||
}
|
||||
if (input.behindOfOrigin > 0) {
|
||||
return "Pull first";
|
||||
return "Push isn't available yet because there are newer changes to bring in first";
|
||||
}
|
||||
if (input.aheadOfOrigin === 0) {
|
||||
return "Nothing to push";
|
||||
return "Push isn't available because there is nothing new to send";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getCreatePrDescription(input: BuildGitActionsInput): string | undefined {
|
||||
function getCreatePrUnavailableMessage(input: BuildGitActionsInput): string | undefined {
|
||||
if (!input.githubFeaturesEnabled) {
|
||||
return "GitHub unavailable";
|
||||
return "Create PR isn't available right now because GitHub isn't connected";
|
||||
}
|
||||
if (input.aheadCount === 0) {
|
||||
return "No new commits";
|
||||
return "Create PR isn't available because this branch doesn't have any new commits yet";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getMergeBranchDescription(input: BuildGitActionsInput): string | undefined {
|
||||
function getMergeBranchUnavailableMessage(input: BuildGitActionsInput): string | undefined {
|
||||
if (!input.baseRefAvailable) {
|
||||
return "No base";
|
||||
return "Merge isn't available because we couldn't determine the base branch";
|
||||
}
|
||||
if (input.hasUncommittedChanges) {
|
||||
return "Clean tree";
|
||||
return "Merge isn't available while you have local changes so commit or stash them first";
|
||||
}
|
||||
if (input.aheadCount === 0) {
|
||||
return "No new commits";
|
||||
return "Merge isn't available because this branch doesn't have anything new to merge yet";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getMergeFromBaseDescription(input: BuildGitActionsInput): string | undefined {
|
||||
function getMergeFromBaseUnavailableMessage(input: BuildGitActionsInput): string | undefined {
|
||||
if (!input.baseRefAvailable) {
|
||||
return "No base";
|
||||
return "Update isn't available because we couldn't determine the base branch";
|
||||
}
|
||||
if (input.hasUncommittedChanges) {
|
||||
return "Clean tree";
|
||||
return "Update isn't available while you have local changes so commit or stash them first";
|
||||
}
|
||||
if (input.behindBaseCount === 0) {
|
||||
return "Up to date";
|
||||
return `Update isn't available because this branch is already up to date with ${input.baseRefLabel}`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback } from "react";
|
||||
import { View, Text, ActivityIndicator, Pressable } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { ChevronDown, MoreVertical } from "lucide-react-native";
|
||||
import { ChevronDown, Info, MoreVertical } from "lucide-react-native";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Shortcut } from "@/components/ui/shortcut";
|
||||
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
import type { GitAction, GitActions } from "@/components/git-actions-policy";
|
||||
|
||||
interface GitActionsSplitButtonProps {
|
||||
@@ -19,6 +20,7 @@ interface GitActionsSplitButtonProps {
|
||||
|
||||
export function GitActionsSplitButton({ gitActions }: GitActionsSplitButtonProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const toast = useToast();
|
||||
const archiveShortcutKeys = useShortcutKeys("archive-worktree");
|
||||
|
||||
const getActionDisplayLabel = useCallback((action: GitAction): string => {
|
||||
@@ -27,6 +29,20 @@ export function GitActionsSplitButton({ gitActions }: GitActionsSplitButtonProps
|
||||
return action.label;
|
||||
}, []);
|
||||
|
||||
const handleActionSelect = useCallback(
|
||||
(action: GitAction) => {
|
||||
if (action.unavailableMessage) {
|
||||
toast.show(action.unavailableMessage, {
|
||||
durationMs: 3200,
|
||||
icon: <Info size={16} color={theme.colors.foreground} />,
|
||||
});
|
||||
return;
|
||||
}
|
||||
action.handler();
|
||||
},
|
||||
[theme.colors.foreground, toast],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
{gitActions.primary ? (
|
||||
@@ -87,6 +103,7 @@ export function GitActionsSplitButton({ gitActions }: GitActionsSplitButtonProps
|
||||
: undefined
|
||||
}
|
||||
disabled={action.disabled}
|
||||
muted={Boolean(action.unavailableMessage)}
|
||||
status={action.status}
|
||||
pendingLabel={action.pendingLabel}
|
||||
successLabel={action.successLabel}
|
||||
@@ -95,8 +112,7 @@ export function GitActionsSplitButton({ gitActions }: GitActionsSplitButtonProps
|
||||
action.id === "pr" &&
|
||||
action.label === "View PR"
|
||||
}
|
||||
description={action.description}
|
||||
onSelect={action.handler}
|
||||
onSelect={() => handleActionSelect(action)}
|
||||
>
|
||||
{action.label}
|
||||
</DropdownMenuItem>
|
||||
@@ -126,11 +142,12 @@ export function GitActionsSplitButton({ gitActions }: GitActionsSplitButtonProps
|
||||
testID={`changes-menu-${action.id}`}
|
||||
leading={action.icon}
|
||||
disabled={action.disabled}
|
||||
muted={Boolean(action.unavailableMessage)}
|
||||
status={action.status}
|
||||
pendingLabel={action.pendingLabel}
|
||||
successLabel={action.successLabel}
|
||||
closeOnSelect={false}
|
||||
onSelect={action.handler}
|
||||
onSelect={() => handleActionSelect(action)}
|
||||
>
|
||||
{action.label}
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -460,6 +460,7 @@ export function DropdownMenuItem({
|
||||
description,
|
||||
onSelect,
|
||||
disabled,
|
||||
muted = false,
|
||||
destructive,
|
||||
selected,
|
||||
showSelectedCheck = false,
|
||||
@@ -477,6 +478,7 @@ export function DropdownMenuItem({
|
||||
description?: string;
|
||||
onSelect?: () => void;
|
||||
disabled?: boolean;
|
||||
muted?: boolean;
|
||||
destructive?: boolean;
|
||||
selected?: boolean;
|
||||
showSelectedCheck?: boolean;
|
||||
@@ -550,6 +552,7 @@ export function DropdownMenuItem({
|
||||
? styles.itemSelectedInteractive
|
||||
: null,
|
||||
isDisabled ? styles.itemDisabled : null,
|
||||
muted && !isDisabled ? styles.itemMuted : null,
|
||||
hovered && !pressed && !isDisabled ? styles.itemHovered : null,
|
||||
pressed && !isDisabled ? styles.itemPressed : null,
|
||||
]}
|
||||
@@ -568,6 +571,7 @@ export function DropdownMenuItem({
|
||||
destructive && !isSuccess ? styles.itemTextDestructive : null,
|
||||
isSuccess ? styles.itemTextSuccess : null,
|
||||
selected && selectedVariant === "accent" ? styles.itemTextSelectedAccent : null,
|
||||
muted && !isDisabled ? styles.itemTextMuted : null,
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
@@ -678,11 +682,17 @@ const styles = StyleSheet.create((theme) => ({
|
||||
itemDisabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
itemMuted: {
|
||||
opacity: 0.72,
|
||||
},
|
||||
itemText: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: theme.colors.foreground,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
},
|
||||
itemTextMuted: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
itemTextDestructive: {
|
||||
color: theme.colors.destructive,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user