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" });
|
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(
|
const actions = buildGitActions(
|
||||||
createInput({
|
createInput({
|
||||||
hasRemote: true,
|
hasRemote: true,
|
||||||
@@ -90,8 +90,8 @@ describe("git-actions-policy", () => {
|
|||||||
const pushAction = actions.secondary.find((action) => action.id === "push");
|
const pushAction = actions.secondary.find((action) => action.id === "push");
|
||||||
|
|
||||||
expect(pushAction).toMatchObject({
|
expect(pushAction).toMatchObject({
|
||||||
disabled: true,
|
disabled: false,
|
||||||
description: "Pull first",
|
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({
|
expect(updateAction).toMatchObject({
|
||||||
label: "Update from main",
|
label: "Update from main",
|
||||||
disabled: false,
|
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;
|
successLabel: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
status: ActionStatus;
|
status: ActionStatus;
|
||||||
description?: string;
|
unavailableMessage?: string;
|
||||||
icon?: ReactElement;
|
icon?: ReactElement;
|
||||||
handler: () => void;
|
handler: () => void;
|
||||||
}
|
}
|
||||||
@@ -82,9 +82,9 @@ export function buildGitActions(input: BuildGitActionsInput): GitActions {
|
|||||||
label: "Pull",
|
label: "Pull",
|
||||||
pendingLabel: "Pulling...",
|
pendingLabel: "Pulling...",
|
||||||
successLabel: "Pulled",
|
successLabel: "Pulled",
|
||||||
disabled: input.runtime.pull.disabled || !canPull(input),
|
disabled: input.runtime.pull.disabled,
|
||||||
status: input.runtime.pull.status,
|
status: input.runtime.pull.status,
|
||||||
description: getPullDescription(input),
|
unavailableMessage: input.runtime.pull.disabled ? undefined : getPullUnavailableMessage(input),
|
||||||
icon: input.runtime.pull.icon,
|
icon: input.runtime.pull.icon,
|
||||||
handler: input.runtime.pull.handler,
|
handler: input.runtime.pull.handler,
|
||||||
});
|
});
|
||||||
@@ -94,9 +94,9 @@ export function buildGitActions(input: BuildGitActionsInput): GitActions {
|
|||||||
label: "Push",
|
label: "Push",
|
||||||
pendingLabel: "Pushing...",
|
pendingLabel: "Pushing...",
|
||||||
successLabel: "Pushed",
|
successLabel: "Pushed",
|
||||||
disabled: input.runtime.push.disabled || !canPush(input),
|
disabled: input.runtime.push.disabled,
|
||||||
status: input.runtime.push.status,
|
status: input.runtime.push.status,
|
||||||
description: getPushDescription(input),
|
unavailableMessage: input.runtime.push.disabled ? undefined : getPushUnavailableMessage(input),
|
||||||
icon: input.runtime.push.icon,
|
icon: input.runtime.push.icon,
|
||||||
handler: input.runtime.push.handler,
|
handler: input.runtime.push.handler,
|
||||||
});
|
});
|
||||||
@@ -108,9 +108,10 @@ export function buildGitActions(input: BuildGitActionsInput): GitActions {
|
|||||||
label: `Merge into ${input.baseRefLabel}`,
|
label: `Merge into ${input.baseRefLabel}`,
|
||||||
pendingLabel: "Merging...",
|
pendingLabel: "Merging...",
|
||||||
successLabel: "Merged",
|
successLabel: "Merged",
|
||||||
disabled: input.runtime["merge-branch"].disabled || !canMergeBranch(input),
|
disabled: input.runtime["merge-branch"].disabled,
|
||||||
status: input.runtime["merge-branch"].status,
|
status: input.runtime["merge-branch"].status,
|
||||||
description: getMergeBranchDescription(input),
|
unavailableMessage:
|
||||||
|
input.runtime["merge-branch"].disabled ? undefined : getMergeBranchUnavailableMessage(input),
|
||||||
icon: input.runtime["merge-branch"].icon,
|
icon: input.runtime["merge-branch"].icon,
|
||||||
handler: input.runtime["merge-branch"].handler,
|
handler: input.runtime["merge-branch"].handler,
|
||||||
});
|
});
|
||||||
@@ -120,9 +121,12 @@ export function buildGitActions(input: BuildGitActionsInput): GitActions {
|
|||||||
label: `Update from ${input.baseRefLabel}`,
|
label: `Update from ${input.baseRefLabel}`,
|
||||||
pendingLabel: "Updating...",
|
pendingLabel: "Updating...",
|
||||||
successLabel: "Updated",
|
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,
|
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,
|
icon: input.runtime["merge-from-base"].icon,
|
||||||
handler: input.runtime["merge-from-base"].handler,
|
handler: input.runtime["merge-from-base"].handler,
|
||||||
});
|
});
|
||||||
@@ -132,9 +136,12 @@ export function buildGitActions(input: BuildGitActionsInput): GitActions {
|
|||||||
label: "Archive worktree",
|
label: "Archive worktree",
|
||||||
pendingLabel: "Archiving...",
|
pendingLabel: "Archiving...",
|
||||||
successLabel: "Archived",
|
successLabel: "Archived",
|
||||||
disabled: input.runtime["archive-worktree"].disabled || !input.isPaseoOwnedWorktree,
|
disabled: input.runtime["archive-worktree"].disabled,
|
||||||
status: input.runtime["archive-worktree"].status,
|
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,
|
icon: input.runtime["archive-worktree"].icon,
|
||||||
handler: input.runtime["archive-worktree"].handler,
|
handler: input.runtime["archive-worktree"].handler,
|
||||||
});
|
});
|
||||||
@@ -189,9 +196,12 @@ function buildPrAction(input: BuildGitActionsInput): GitAction {
|
|||||||
label: "View PR",
|
label: "View PR",
|
||||||
pendingLabel: "View PR",
|
pendingLabel: "View PR",
|
||||||
successLabel: "View PR",
|
successLabel: "View PR",
|
||||||
disabled: input.runtime.pr.disabled || !input.githubFeaturesEnabled,
|
disabled: input.runtime.pr.disabled,
|
||||||
status: input.runtime.pr.status,
|
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,
|
icon: input.runtime.pr.icon,
|
||||||
handler: input.runtime.pr.handler,
|
handler: input.runtime.pr.handler,
|
||||||
};
|
};
|
||||||
@@ -202,9 +212,10 @@ function buildPrAction(input: BuildGitActionsInput): GitAction {
|
|||||||
label: "Create PR",
|
label: "Create PR",
|
||||||
pendingLabel: "Creating PR...",
|
pendingLabel: "Creating PR...",
|
||||||
successLabel: "PR Created",
|
successLabel: "PR Created",
|
||||||
disabled: input.runtime.pr.disabled || !input.githubFeaturesEnabled || input.aheadCount === 0,
|
disabled: input.runtime.pr.disabled,
|
||||||
status: input.runtime.pr.status,
|
status: input.runtime.pr.status,
|
||||||
description: getCreatePrDescription(input),
|
unavailableMessage:
|
||||||
|
input.runtime.pr.disabled ? undefined : getCreatePrUnavailableMessage(input),
|
||||||
icon: input.runtime.pr.icon,
|
icon: input.runtime.pr.icon,
|
||||||
handler: input.runtime.pr.handler,
|
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) {
|
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) {
|
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) {
|
if (input.behindOfOrigin === 0) {
|
||||||
return "Nothing to pull";
|
return "Pull isn't available because this branch is already up to date";
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPushDescription(input: BuildGitActionsInput): string | undefined {
|
function getPushUnavailableMessage(input: BuildGitActionsInput): string | undefined {
|
||||||
if (!input.hasRemote) {
|
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) {
|
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) {
|
if (input.aheadOfOrigin === 0) {
|
||||||
return "Nothing to push";
|
return "Push isn't available because there is nothing new to send";
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCreatePrDescription(input: BuildGitActionsInput): string | undefined {
|
function getCreatePrUnavailableMessage(input: BuildGitActionsInput): string | undefined {
|
||||||
if (!input.githubFeaturesEnabled) {
|
if (!input.githubFeaturesEnabled) {
|
||||||
return "GitHub unavailable";
|
return "Create PR isn't available right now because GitHub isn't connected";
|
||||||
}
|
}
|
||||||
if (input.aheadCount === 0) {
|
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;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMergeBranchDescription(input: BuildGitActionsInput): string | undefined {
|
function getMergeBranchUnavailableMessage(input: BuildGitActionsInput): string | undefined {
|
||||||
if (!input.baseRefAvailable) {
|
if (!input.baseRefAvailable) {
|
||||||
return "No base";
|
return "Merge isn't available because we couldn't determine the base branch";
|
||||||
}
|
}
|
||||||
if (input.hasUncommittedChanges) {
|
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) {
|
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;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMergeFromBaseDescription(input: BuildGitActionsInput): string | undefined {
|
function getMergeFromBaseUnavailableMessage(input: BuildGitActionsInput): string | undefined {
|
||||||
if (!input.baseRefAvailable) {
|
if (!input.baseRefAvailable) {
|
||||||
return "No base";
|
return "Update isn't available because we couldn't determine the base branch";
|
||||||
}
|
}
|
||||||
if (input.hasUncommittedChanges) {
|
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) {
|
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;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { View, Text, ActivityIndicator, Pressable } from "react-native";
|
import { View, Text, ActivityIndicator, Pressable } from "react-native";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { ChevronDown, MoreVertical } from "lucide-react-native";
|
import { ChevronDown, Info, MoreVertical } from "lucide-react-native";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Shortcut } from "@/components/ui/shortcut";
|
import { Shortcut } from "@/components/ui/shortcut";
|
||||||
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
||||||
|
import { useToast } from "@/contexts/toast-context";
|
||||||
import type { GitAction, GitActions } from "@/components/git-actions-policy";
|
import type { GitAction, GitActions } from "@/components/git-actions-policy";
|
||||||
|
|
||||||
interface GitActionsSplitButtonProps {
|
interface GitActionsSplitButtonProps {
|
||||||
@@ -19,6 +20,7 @@ interface GitActionsSplitButtonProps {
|
|||||||
|
|
||||||
export function GitActionsSplitButton({ gitActions }: GitActionsSplitButtonProps) {
|
export function GitActionsSplitButton({ gitActions }: GitActionsSplitButtonProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
|
const toast = useToast();
|
||||||
const archiveShortcutKeys = useShortcutKeys("archive-worktree");
|
const archiveShortcutKeys = useShortcutKeys("archive-worktree");
|
||||||
|
|
||||||
const getActionDisplayLabel = useCallback((action: GitAction): string => {
|
const getActionDisplayLabel = useCallback((action: GitAction): string => {
|
||||||
@@ -27,6 +29,20 @@ export function GitActionsSplitButton({ gitActions }: GitActionsSplitButtonProps
|
|||||||
return action.label;
|
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 (
|
return (
|
||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
{gitActions.primary ? (
|
{gitActions.primary ? (
|
||||||
@@ -87,6 +103,7 @@ export function GitActionsSplitButton({ gitActions }: GitActionsSplitButtonProps
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
disabled={action.disabled}
|
disabled={action.disabled}
|
||||||
|
muted={Boolean(action.unavailableMessage)}
|
||||||
status={action.status}
|
status={action.status}
|
||||||
pendingLabel={action.pendingLabel}
|
pendingLabel={action.pendingLabel}
|
||||||
successLabel={action.successLabel}
|
successLabel={action.successLabel}
|
||||||
@@ -95,8 +112,7 @@ export function GitActionsSplitButton({ gitActions }: GitActionsSplitButtonProps
|
|||||||
action.id === "pr" &&
|
action.id === "pr" &&
|
||||||
action.label === "View PR"
|
action.label === "View PR"
|
||||||
}
|
}
|
||||||
description={action.description}
|
onSelect={() => handleActionSelect(action)}
|
||||||
onSelect={action.handler}
|
|
||||||
>
|
>
|
||||||
{action.label}
|
{action.label}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -126,11 +142,12 @@ export function GitActionsSplitButton({ gitActions }: GitActionsSplitButtonProps
|
|||||||
testID={`changes-menu-${action.id}`}
|
testID={`changes-menu-${action.id}`}
|
||||||
leading={action.icon}
|
leading={action.icon}
|
||||||
disabled={action.disabled}
|
disabled={action.disabled}
|
||||||
|
muted={Boolean(action.unavailableMessage)}
|
||||||
status={action.status}
|
status={action.status}
|
||||||
pendingLabel={action.pendingLabel}
|
pendingLabel={action.pendingLabel}
|
||||||
successLabel={action.successLabel}
|
successLabel={action.successLabel}
|
||||||
closeOnSelect={false}
|
closeOnSelect={false}
|
||||||
onSelect={action.handler}
|
onSelect={() => handleActionSelect(action)}
|
||||||
>
|
>
|
||||||
{action.label}
|
{action.label}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@@ -460,6 +460,7 @@ export function DropdownMenuItem({
|
|||||||
description,
|
description,
|
||||||
onSelect,
|
onSelect,
|
||||||
disabled,
|
disabled,
|
||||||
|
muted = false,
|
||||||
destructive,
|
destructive,
|
||||||
selected,
|
selected,
|
||||||
showSelectedCheck = false,
|
showSelectedCheck = false,
|
||||||
@@ -477,6 +478,7 @@ export function DropdownMenuItem({
|
|||||||
description?: string;
|
description?: string;
|
||||||
onSelect?: () => void;
|
onSelect?: () => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
muted?: boolean;
|
||||||
destructive?: boolean;
|
destructive?: boolean;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
showSelectedCheck?: boolean;
|
showSelectedCheck?: boolean;
|
||||||
@@ -550,6 +552,7 @@ export function DropdownMenuItem({
|
|||||||
? styles.itemSelectedInteractive
|
? styles.itemSelectedInteractive
|
||||||
: null,
|
: null,
|
||||||
isDisabled ? styles.itemDisabled : null,
|
isDisabled ? styles.itemDisabled : null,
|
||||||
|
muted && !isDisabled ? styles.itemMuted : null,
|
||||||
hovered && !pressed && !isDisabled ? styles.itemHovered : null,
|
hovered && !pressed && !isDisabled ? styles.itemHovered : null,
|
||||||
pressed && !isDisabled ? styles.itemPressed : null,
|
pressed && !isDisabled ? styles.itemPressed : null,
|
||||||
]}
|
]}
|
||||||
@@ -568,6 +571,7 @@ export function DropdownMenuItem({
|
|||||||
destructive && !isSuccess ? styles.itemTextDestructive : null,
|
destructive && !isSuccess ? styles.itemTextDestructive : null,
|
||||||
isSuccess ? styles.itemTextSuccess : null,
|
isSuccess ? styles.itemTextSuccess : null,
|
||||||
selected && selectedVariant === "accent" ? styles.itemTextSelectedAccent : null,
|
selected && selectedVariant === "accent" ? styles.itemTextSelectedAccent : null,
|
||||||
|
muted && !isDisabled ? styles.itemTextMuted : null,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
@@ -678,11 +682,17 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
itemDisabled: {
|
itemDisabled: {
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
},
|
},
|
||||||
|
itemMuted: {
|
||||||
|
opacity: 0.72,
|
||||||
|
},
|
||||||
itemText: {
|
itemText: {
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
fontWeight: theme.fontWeight.normal,
|
fontWeight: theme.fontWeight.normal,
|
||||||
},
|
},
|
||||||
|
itemTextMuted: {
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
},
|
||||||
itemTextDestructive: {
|
itemTextDestructive: {
|
||||||
color: theme.colors.destructive,
|
color: theme.colors.destructive,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user