mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Fix sidebar shortcut hints shifting rows
This commit is contained in:
@@ -101,7 +101,10 @@ import { SidebarStatusWorkspaceList } from "@/components/sidebar/sidebar-status-
|
||||
import {
|
||||
SidebarWorkspaceRowFrame,
|
||||
SidebarWorkspaceRowContent,
|
||||
sidebarWorkspaceRowStyles,
|
||||
SidebarWorkspaceShortcutBadge,
|
||||
SidebarWorkspaceTrailingActionBase,
|
||||
SidebarWorkspaceTrailingActionOverlay,
|
||||
SidebarWorkspaceTrailingActionSlot,
|
||||
} from "@/components/sidebar/sidebar-workspace-row-content";
|
||||
import {
|
||||
useHydratedWorkspaceEntries,
|
||||
@@ -652,7 +655,10 @@ function WorkspaceRowRightGroup({
|
||||
onCopyPath?: () => void;
|
||||
onRename?: () => void;
|
||||
}) {
|
||||
const showShortcut = showShortcutBadge && shortcutNumber !== null;
|
||||
const showKebab = Boolean(onArchive && (isHovered || isTouchPlatform));
|
||||
const showKebabInSlot = showKebab && !showShortcut;
|
||||
const shouldRenderActionSlot = Boolean(onArchive || workspace.diffStat);
|
||||
return (
|
||||
<>
|
||||
{showScriptsIcon ? (
|
||||
@@ -665,30 +671,35 @@ function WorkspaceRowRightGroup({
|
||||
</View>
|
||||
) : null}
|
||||
{isCreating ? <Text style={styles.workspaceCreatingText}>Creating...</Text> : null}
|
||||
{showKebab && onArchive ? (
|
||||
<WorkspaceKebabMenu
|
||||
workspaceKey={workspace.workspaceKey}
|
||||
onCopyPath={onCopyPath}
|
||||
onCopyBranchName={onCopyBranchName}
|
||||
onRename={onRename}
|
||||
onMarkAsRead={onMarkAsRead}
|
||||
onArchive={onArchive}
|
||||
archiveLabel={archiveLabel}
|
||||
archiveStatus={archiveStatus}
|
||||
archivePendingLabel={archivePendingLabel}
|
||||
archiveShortcutKeys={archiveShortcutKeys}
|
||||
/>
|
||||
) : null}
|
||||
{!showKebab && workspace.diffStat ? (
|
||||
<DiffStat
|
||||
additions={workspace.diffStat.additions}
|
||||
deletions={workspace.diffStat.deletions}
|
||||
/>
|
||||
) : null}
|
||||
{showShortcutBadge && shortcutNumber !== null ? (
|
||||
<View style={sidebarWorkspaceRowStyles.shortcutBadge}>
|
||||
<Text style={sidebarWorkspaceRowStyles.shortcutBadgeText}>{shortcutNumber}</Text>
|
||||
</View>
|
||||
{shouldRenderActionSlot ? (
|
||||
<SidebarWorkspaceTrailingActionSlot>
|
||||
<SidebarWorkspaceTrailingActionBase
|
||||
visible={Boolean(workspace.diffStat && !showKebabInSlot && !showShortcut)}
|
||||
>
|
||||
{workspace.diffStat ? (
|
||||
<DiffStat
|
||||
additions={workspace.diffStat.additions}
|
||||
deletions={workspace.diffStat.deletions}
|
||||
/>
|
||||
) : null}
|
||||
</SidebarWorkspaceTrailingActionBase>
|
||||
<SidebarWorkspaceTrailingActionOverlay visible={showKebabInSlot}>
|
||||
{onArchive ? (
|
||||
<WorkspaceKebabMenu
|
||||
workspaceKey={workspace.workspaceKey}
|
||||
onCopyPath={onCopyPath}
|
||||
onCopyBranchName={onCopyBranchName}
|
||||
onRename={onRename}
|
||||
onMarkAsRead={onMarkAsRead}
|
||||
onArchive={onArchive}
|
||||
archiveLabel={archiveLabel}
|
||||
archiveStatus={archiveStatus}
|
||||
archivePendingLabel={archivePendingLabel}
|
||||
archiveShortcutKeys={archiveShortcutKeys}
|
||||
/>
|
||||
) : null}
|
||||
</SidebarWorkspaceTrailingActionOverlay>
|
||||
</SidebarWorkspaceTrailingActionSlot>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
@@ -1275,8 +1286,8 @@ function ProjectHeaderRow({
|
||||
removeProjectStatus={removeProjectStatus}
|
||||
/>
|
||||
{showShortcutBadge && shortcutNumber !== null ? (
|
||||
<View style={styles.shortcutBadge}>
|
||||
<Text style={styles.shortcutBadgeText}>{shortcutNumber}</Text>
|
||||
<View style={styles.projectShortcutBadgeOverlay} pointerEvents="none">
|
||||
<SidebarWorkspaceShortcutBadge number={shortcutNumber} />
|
||||
</View>
|
||||
) : null}
|
||||
</>
|
||||
@@ -1412,6 +1423,8 @@ function WorkspaceRowInner({
|
||||
isHovered={isHovered}
|
||||
isLoading={isArchiving || isCreating}
|
||||
isCreating={isCreating}
|
||||
shortcutNumber={shortcutNumber}
|
||||
showShortcutBadge={showShortcutBadge}
|
||||
>
|
||||
<WorkspaceRowRightGroup
|
||||
workspace={workspace}
|
||||
@@ -2789,6 +2802,7 @@ const styles = StyleSheet.create((theme) => ({
|
||||
textAlign: "center",
|
||||
},
|
||||
projectRow: {
|
||||
position: "relative",
|
||||
minHeight: 36,
|
||||
paddingVertical: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
@@ -2925,6 +2939,11 @@ const styles = StyleSheet.create((theme) => ({
|
||||
fontSize: theme.fontSize.sm,
|
||||
},
|
||||
projectActionTooltipShortcut: {},
|
||||
projectShortcutBadgeOverlay: {
|
||||
position: "absolute",
|
||||
top: theme.spacing[2] + 1,
|
||||
right: theme.spacing[2],
|
||||
},
|
||||
workspaceRow: {
|
||||
minHeight: 36,
|
||||
marginBottom: theme.spacing[1],
|
||||
@@ -3045,24 +3064,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
kebabButtonHovered: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
shortcutBadge: {
|
||||
minWidth: 18,
|
||||
height: 18,
|
||||
paddingHorizontal: theme.spacing[1],
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: theme.borderRadius.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.surface2,
|
||||
backgroundColor: theme.colors.surface0,
|
||||
flexShrink: 0,
|
||||
},
|
||||
shortcutBadgeText: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
lineHeight: 14,
|
||||
},
|
||||
statusDotNeedsInput: {
|
||||
backgroundColor: theme.colors.palette.amber[500],
|
||||
borderColor: theme.colors.surface0,
|
||||
|
||||
@@ -59,7 +59,9 @@ import { useClearWorkspaceAttention } from "@/hooks/use-clear-workspace-attentio
|
||||
import {
|
||||
SidebarWorkspaceRowFrame,
|
||||
SidebarWorkspaceRowContent,
|
||||
sidebarWorkspaceRowStyles,
|
||||
SidebarWorkspaceTrailingActionBase,
|
||||
SidebarWorkspaceTrailingActionOverlay,
|
||||
SidebarWorkspaceTrailingActionSlot,
|
||||
} from "@/components/sidebar/sidebar-workspace-row-content";
|
||||
import { useSidebarCollapsedSectionsStore } from "@/stores/sidebar-collapsed-sections-store";
|
||||
|
||||
@@ -611,7 +613,10 @@ function StatusWorkspaceRowInner({
|
||||
return (
|
||||
<SidebarWorkspaceRowFrame workspace={workspace}>
|
||||
{({ isHovered, hoverHandlers }) => {
|
||||
const showShortcut = showShortcutBadge && shortcutNumber !== null;
|
||||
const showKebab = Boolean(onArchive && (isHovered || isTouchPlatform));
|
||||
const showKebabInSlot = showKebab && !showShortcut;
|
||||
const shouldRenderActionSlot = Boolean(onArchive || workspace.diffStat);
|
||||
const workspaceRowStyle = getStatusWorkspaceRowStyle({ selected, isHovered });
|
||||
return (
|
||||
<View style={styles.workspaceRowContainer} {...hoverHandlers}>
|
||||
@@ -628,6 +633,8 @@ function StatusWorkspaceRowInner({
|
||||
subtitle={projectName}
|
||||
isHovered={isHovered}
|
||||
isLoading={isArchiving}
|
||||
shortcutNumber={shortcutNumber}
|
||||
showShortcutBadge={showShortcutBadge}
|
||||
>
|
||||
<>
|
||||
{showScriptsIcon ? (
|
||||
@@ -639,9 +646,11 @@ function StatusWorkspaceRowInner({
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
{showKebab && onArchive ? (
|
||||
<StatusKebabMenu
|
||||
workspaceKey={workspace.workspaceKey}
|
||||
{shouldRenderActionSlot ? (
|
||||
<StatusWorkspaceActionSlot
|
||||
workspace={workspace}
|
||||
showBase={Boolean(workspace.diffStat && !showKebabInSlot && !showShortcut)}
|
||||
showOverlay={showKebabInSlot}
|
||||
onCopyPath={onCopyPath}
|
||||
onCopyBranchName={onCopyBranchName}
|
||||
onRename={onRename}
|
||||
@@ -653,19 +662,6 @@ function StatusWorkspaceRowInner({
|
||||
archiveShortcutKeys={archiveShortcutKeys}
|
||||
/>
|
||||
) : null}
|
||||
{!showKebab && workspace.diffStat ? (
|
||||
<DiffStat
|
||||
additions={workspace.diffStat.additions}
|
||||
deletions={workspace.diffStat.deletions}
|
||||
/>
|
||||
) : null}
|
||||
{showShortcutBadge && shortcutNumber !== null ? (
|
||||
<View style={sidebarWorkspaceRowStyles.shortcutBadge}>
|
||||
<Text style={sidebarWorkspaceRowStyles.shortcutBadgeText}>
|
||||
{shortcutNumber}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</>
|
||||
</SidebarWorkspaceRowContent>
|
||||
</Pressable>
|
||||
@@ -676,6 +672,63 @@ function StatusWorkspaceRowInner({
|
||||
);
|
||||
}
|
||||
|
||||
function StatusWorkspaceActionSlot({
|
||||
workspace,
|
||||
showBase,
|
||||
showOverlay,
|
||||
onCopyPath,
|
||||
onCopyBranchName,
|
||||
onRename,
|
||||
onMarkAsRead,
|
||||
onArchive,
|
||||
archiveLabel,
|
||||
archiveStatus,
|
||||
archivePendingLabel,
|
||||
archiveShortcutKeys,
|
||||
}: {
|
||||
workspace: SidebarWorkspaceEntry;
|
||||
showBase: boolean;
|
||||
showOverlay: boolean;
|
||||
onCopyPath?: () => void;
|
||||
onCopyBranchName?: () => void;
|
||||
onRename?: () => void;
|
||||
onMarkAsRead?: () => void;
|
||||
onArchive?: () => void;
|
||||
archiveLabel?: string;
|
||||
archiveStatus?: "idle" | "pending" | "success";
|
||||
archivePendingLabel?: string;
|
||||
archiveShortcutKeys?: ShortcutKey[][] | null;
|
||||
}) {
|
||||
return (
|
||||
<SidebarWorkspaceTrailingActionSlot>
|
||||
<SidebarWorkspaceTrailingActionBase visible={showBase}>
|
||||
{workspace.diffStat ? (
|
||||
<DiffStat
|
||||
additions={workspace.diffStat.additions}
|
||||
deletions={workspace.diffStat.deletions}
|
||||
/>
|
||||
) : null}
|
||||
</SidebarWorkspaceTrailingActionBase>
|
||||
<SidebarWorkspaceTrailingActionOverlay visible={showOverlay}>
|
||||
{onArchive ? (
|
||||
<StatusKebabMenu
|
||||
workspaceKey={workspace.workspaceKey}
|
||||
onCopyPath={onCopyPath}
|
||||
onCopyBranchName={onCopyBranchName}
|
||||
onRename={onRename}
|
||||
onMarkAsRead={onMarkAsRead}
|
||||
onArchive={onArchive}
|
||||
archiveLabel={archiveLabel}
|
||||
archiveStatus={archiveStatus}
|
||||
archivePendingLabel={archivePendingLabel}
|
||||
archiveShortcutKeys={archiveShortcutKeys}
|
||||
/>
|
||||
) : null}
|
||||
</SidebarWorkspaceTrailingActionOverlay>
|
||||
</SidebarWorkspaceTrailingActionSlot>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusKebabMenu({
|
||||
workspaceKey,
|
||||
onCopyPath,
|
||||
|
||||
@@ -89,6 +89,8 @@ export const SidebarWorkspaceRowContent = memo(function SidebarWorkspaceRowConte
|
||||
isHovered,
|
||||
isLoading,
|
||||
isCreating = false,
|
||||
shortcutNumber = null,
|
||||
showShortcutBadge = false,
|
||||
children,
|
||||
}: {
|
||||
workspace: SidebarWorkspaceEntry;
|
||||
@@ -96,6 +98,8 @@ export const SidebarWorkspaceRowContent = memo(function SidebarWorkspaceRowConte
|
||||
isHovered: boolean;
|
||||
isLoading: boolean;
|
||||
isCreating?: boolean;
|
||||
shortcutNumber?: number | null;
|
||||
showShortcutBadge?: boolean;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
const workspaceBranchTextStyle = useMemo(
|
||||
@@ -108,7 +112,7 @@ export const SidebarWorkspaceRowContent = memo(function SidebarWorkspaceRowConte
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={styles.workspaceRowContent}>
|
||||
<View style={styles.workspaceRowMain}>
|
||||
<View style={styles.workspaceRowLeft}>
|
||||
<WorkspaceStatusIndicator
|
||||
@@ -122,6 +126,11 @@ export const SidebarWorkspaceRowContent = memo(function SidebarWorkspaceRowConte
|
||||
</View>
|
||||
<View style={styles.workspaceRowRight}>{children}</View>
|
||||
</View>
|
||||
{showShortcutBadge && shortcutNumber !== null ? (
|
||||
<View style={styles.shortcutBadgeOverlay} pointerEvents="none">
|
||||
<SidebarWorkspaceShortcutBadge number={shortcutNumber} />
|
||||
</View>
|
||||
) : null}
|
||||
{subtitle ? (
|
||||
<Text style={styles.workspaceSubtitle} numberOfLines={1}>
|
||||
{subtitle}
|
||||
@@ -133,7 +142,7 @@ export const SidebarWorkspaceRowContent = memo(function SidebarWorkspaceRowConte
|
||||
<ChecksBadge checks={workspace.prHint.checks} />
|
||||
</View>
|
||||
) : null}
|
||||
</>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -346,7 +355,7 @@ const checksBadgeStyles = StyleSheet.create((theme) => ({
|
||||
export const sidebarWorkspaceRowStyles = StyleSheet.create((theme) => ({
|
||||
rowRight: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
alignItems: "flex-start",
|
||||
gap: theme.spacing[2],
|
||||
flexShrink: 0,
|
||||
},
|
||||
@@ -368,12 +377,63 @@ export const sidebarWorkspaceRowStyles = StyleSheet.create((theme) => ({
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
lineHeight: 14,
|
||||
},
|
||||
hidden: { opacity: 0 },
|
||||
trailingActionSlot: {
|
||||
position: "relative",
|
||||
minWidth: 18,
|
||||
minHeight: 20,
|
||||
flexShrink: 0,
|
||||
alignItems: "flex-end",
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
trailingActionOverlay: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
export function SidebarWorkspaceShortcutBadge({ number }: { number: number }) {
|
||||
return (
|
||||
<View style={sidebarWorkspaceRowStyles.shortcutBadge}>
|
||||
<Text style={sidebarWorkspaceRowStyles.shortcutBadgeText}>{number}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarWorkspaceTrailingActionSlot({ children }: { children: ReactNode }) {
|
||||
return <View style={sidebarWorkspaceRowStyles.trailingActionSlot}>{children}</View>;
|
||||
}
|
||||
|
||||
export function SidebarWorkspaceTrailingActionBase({
|
||||
visible,
|
||||
children,
|
||||
}: {
|
||||
visible: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
if (!children) return null;
|
||||
return <View style={visible ? undefined : sidebarWorkspaceRowStyles.hidden}>{children}</View>;
|
||||
}
|
||||
|
||||
export function SidebarWorkspaceTrailingActionOverlay({
|
||||
visible,
|
||||
children,
|
||||
}: {
|
||||
visible: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
if (!visible || !children) return null;
|
||||
return <View style={sidebarWorkspaceRowStyles.trailingActionOverlay}>{children}</View>;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
workspaceRowContent: {
|
||||
position: "relative",
|
||||
},
|
||||
workspaceRowMain: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
gap: theme.spacing[2],
|
||||
width: "100%",
|
||||
@@ -386,6 +446,11 @@ const styles = StyleSheet.create((theme) => ({
|
||||
minWidth: 0,
|
||||
},
|
||||
workspaceRowRight: sidebarWorkspaceRowStyles.rowRight,
|
||||
shortcutBadgeOverlay: {
|
||||
position: "absolute",
|
||||
top: 1,
|
||||
right: 0,
|
||||
},
|
||||
workspaceStatusDot: {
|
||||
position: "relative",
|
||||
width: WORKSPACE_STATUS_DOT_WIDTH,
|
||||
|
||||
@@ -28,6 +28,9 @@ export function WorkspaceShortcutTargetsSubscriber({
|
||||
const collapsedProjectKeys = useSidebarCollapsedSectionsStore(
|
||||
(state) => state.collapsedProjectKeys,
|
||||
);
|
||||
const collapsedStatusGroupKeys = useSidebarCollapsedSectionsStore(
|
||||
(state) => state.collapsedStatusGroupKeys,
|
||||
);
|
||||
const setSidebarShortcutWorkspaceTargets = useKeyboardShortcutsStore(
|
||||
(state) => state.setSidebarShortcutWorkspaceTargets,
|
||||
);
|
||||
@@ -37,6 +40,7 @@ export function WorkspaceShortcutTargetsSubscriber({
|
||||
return buildStatusSidebarShortcutModel({
|
||||
workspaces: statusWorkspaces,
|
||||
projectNamesByKey,
|
||||
collapsedStatusGroupKeys,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -44,7 +48,14 @@ export function WorkspaceShortcutTargetsSubscriber({
|
||||
projects,
|
||||
collapsedProjectKeys,
|
||||
});
|
||||
}, [collapsedProjectKeys, groupMode, projectNamesByKey, projects, statusWorkspaces]);
|
||||
}, [
|
||||
collapsedProjectKeys,
|
||||
collapsedStatusGroupKeys,
|
||||
groupMode,
|
||||
projectNamesByKey,
|
||||
projects,
|
||||
statusWorkspaces,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || !serverId) {
|
||||
|
||||
@@ -199,6 +199,37 @@ describe("buildStatusSidebarShortcutModel", () => {
|
||||
expect(model.shortcutIndexByWorkspaceKey.get("s1:running-old")).toBe(3);
|
||||
expect(model.shortcutIndexByWorkspaceKey.get("s1:done-old")).toBe(4);
|
||||
});
|
||||
|
||||
it("excludes collapsed status groups from shortcut targets", () => {
|
||||
const workspaces = [
|
||||
workspace({
|
||||
serverId: "s1",
|
||||
workspaceId: "needs-input",
|
||||
workspaceDirectory: "/repo/needs-input",
|
||||
name: "needs input",
|
||||
projectKey: "p1",
|
||||
statusBucket: "needs_input",
|
||||
}),
|
||||
workspace({
|
||||
serverId: "s1",
|
||||
workspaceId: "running",
|
||||
workspaceDirectory: "/repo/running",
|
||||
name: "running",
|
||||
projectKey: "p1",
|
||||
statusBucket: "running",
|
||||
}),
|
||||
];
|
||||
|
||||
const model = buildStatusSidebarShortcutModel({
|
||||
workspaces,
|
||||
projectNamesByKey: new Map([["p1", "Project 1"]]),
|
||||
collapsedStatusGroupKeys: new Set(["needs_input"]),
|
||||
});
|
||||
|
||||
expect(model.shortcutTargets).toEqual([{ serverId: "s1", workspaceId: "running" }]);
|
||||
expect(model.shortcutIndexByWorkspaceKey.get("s1:needs-input")).toBeUndefined();
|
||||
expect(model.shortcutIndexByWorkspaceKey.get("s1:running")).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRelativeSidebarShortcutTarget", () => {
|
||||
|
||||
@@ -53,6 +53,7 @@ export function buildSidebarShortcutModel(input: {
|
||||
export function buildStatusSidebarShortcutModel(input: {
|
||||
workspaces: SidebarWorkspaceEntry[];
|
||||
projectNamesByKey: Map<string, string>;
|
||||
collapsedStatusGroupKeys?: ReadonlySet<string>;
|
||||
shortcutLimit?: number;
|
||||
}): SidebarShortcutModel {
|
||||
const maxShortcuts = Math.max(0, Math.floor(input.shortcutLimit ?? 9));
|
||||
@@ -61,6 +62,10 @@ export function buildStatusSidebarShortcutModel(input: {
|
||||
const shortcutIndexByWorkspaceKey = new Map<string, number>();
|
||||
|
||||
for (const group of groups) {
|
||||
if (input.collapsedStatusGroupKeys?.has(group.bucket)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const workspace of group.rows) {
|
||||
if (shortcutTargets.length >= maxShortcuts) {
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user