Group and clear workspace status in the sidebar (#1317)
* Add status grouping to the workspace sidebar * Mark workspaces as read from the sidebar * Clean package builds before release checks * Fix sidebar status follow-up checks
This commit is contained in:
@@ -41,17 +41,16 @@ import { DiffStat } from "@/components/diff-stat";
|
||||
import {
|
||||
Archive,
|
||||
CircleAlert,
|
||||
CircleCheck,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Copy,
|
||||
ExternalLink,
|
||||
FolderPlus,
|
||||
FolderGit2,
|
||||
GitPullRequest,
|
||||
Globe,
|
||||
Settings,
|
||||
SquareTerminal,
|
||||
Monitor,
|
||||
MoreVertical,
|
||||
Pencil,
|
||||
Plus,
|
||||
@@ -98,12 +97,23 @@ import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-disp
|
||||
import { shouldRenderSyncedStatusLoader } from "@/utils/status-loader";
|
||||
import { isEmphasizedStatusDotBucket } from "@/utils/status-dot-color";
|
||||
import type { SidebarStateBucket } from "@/utils/sidebar-agent-state";
|
||||
import { SidebarStatusWorkspaceList } from "@/components/sidebar/sidebar-status-list";
|
||||
import {
|
||||
SidebarWorkspaceRowFrame,
|
||||
SidebarWorkspaceRowContent,
|
||||
sidebarWorkspaceRowStyles,
|
||||
} from "@/components/sidebar/sidebar-workspace-row-content";
|
||||
import {
|
||||
useHydratedWorkspaceEntries,
|
||||
useProjectNamesMap,
|
||||
} from "@/hooks/use-status-mode-workspaces";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { Shortcut } from "@/components/ui/shortcut";
|
||||
import type { ShortcutKey } from "@/utils/format-shortcut";
|
||||
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
||||
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
||||
import { useClearWorkspaceAttention } from "@/hooks/use-clear-workspace-attention";
|
||||
import type { PrHint } from "@/git/use-pr-status-query";
|
||||
import { buildSidebarProjectRowModel } from "@/utils/sidebar-project-row-model";
|
||||
import { useSessionStore, type WorkspaceDescriptor } from "@/stores/session-store";
|
||||
@@ -119,8 +129,6 @@ import {
|
||||
archiveWorkspaceOptimistically,
|
||||
archiveWorkspacesOptimistically,
|
||||
} from "@/workspace/workspace-archive";
|
||||
import { WorkspaceHoverCard } from "@/components/workspace-hover-card";
|
||||
import { GitHubIcon } from "@/components/icons/github-icon";
|
||||
import { isWeb as platformIsWeb, isNative as platformIsNative } from "@/constants/platform";
|
||||
|
||||
const workspaceKeyExtractor = (workspace: SidebarWorkspaceEntry) => workspace.workspaceKey;
|
||||
@@ -134,12 +142,10 @@ const DEFAULT_STATUS_DOT_OFFSET = 0;
|
||||
const EMPHASIZED_STATUS_DOT_OFFSET = -1;
|
||||
const ThemedExternalLink = withUnistyles(ExternalLink);
|
||||
const ThemedGitPullRequest = withUnistyles(GitPullRequest);
|
||||
const ThemedGitHubIcon = withUnistyles(GitHubIcon);
|
||||
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
||||
const ThemedCircleAlert = withUnistyles(CircleAlert);
|
||||
const ThemedCircleCheck = withUnistyles(CircleCheck);
|
||||
const ThemedSyncedLoader = withUnistyles(SyncedLoader);
|
||||
const ThemedMonitor = withUnistyles(Monitor);
|
||||
const ThemedFolderGit2 = withUnistyles(FolderGit2);
|
||||
const ThemedFolderPlus = withUnistyles(FolderPlus);
|
||||
const ThemedGlobe = withUnistyles(Globe);
|
||||
const ThemedSquareTerminal = withUnistyles(SquareTerminal);
|
||||
@@ -229,6 +235,7 @@ interface SidebarWorkspaceListProps {
|
||||
collapsedProjectKeys: ReadonlySet<string>;
|
||||
onToggleProjectCollapsed: (projectKey: string) => void;
|
||||
shortcutIndexByWorkspaceKey: Map<string, number>;
|
||||
groupMode: "project" | "status";
|
||||
isRefreshing?: boolean;
|
||||
onRefresh?: () => void;
|
||||
onWorkspacePress?: () => void;
|
||||
@@ -281,6 +288,7 @@ interface WorkspaceRowInnerProps {
|
||||
onCopyBranchName?: () => void;
|
||||
onCopyPath?: () => void;
|
||||
onRename?: () => void;
|
||||
onMarkAsRead?: () => void;
|
||||
archiveShortcutKeys?: ShortcutKey[][] | null;
|
||||
}
|
||||
|
||||
@@ -294,7 +302,7 @@ function getWorkspaceArchiveStatus(
|
||||
return "idle";
|
||||
}
|
||||
|
||||
function useSidebarWorkspaceEntry(
|
||||
export function useSidebarWorkspaceEntry(
|
||||
serverId: string | null,
|
||||
workspaceId: string | null,
|
||||
): SidebarWorkspaceEntry | null {
|
||||
@@ -367,6 +375,23 @@ function workspaceKebabStyle({
|
||||
return [styles.kebabButton, hovered && styles.kebabButtonHovered];
|
||||
}
|
||||
|
||||
function getProjectWorkspaceRowStyle({
|
||||
isDragging,
|
||||
selected,
|
||||
isHovered,
|
||||
}: {
|
||||
isDragging: boolean;
|
||||
selected: boolean;
|
||||
isHovered: boolean;
|
||||
}) {
|
||||
return [
|
||||
styles.workspaceRow,
|
||||
isDragging && styles.workspaceRowDragging,
|
||||
selected && styles.sidebarRowSelected,
|
||||
isHovered && styles.workspaceRowHovered,
|
||||
];
|
||||
}
|
||||
|
||||
function noop() {}
|
||||
|
||||
const prBadgeStyles = StyleSheet.create((theme) => ({
|
||||
@@ -391,100 +416,6 @@ const prBadgeStyles = StyleSheet.create((theme) => ({
|
||||
|
||||
const prBadgeTextHoveredCombined = [prBadgeStyles.text, prBadgeStyles.textHovered];
|
||||
|
||||
function ChecksBadge({ checks }: { checks: PrHint["checks"] }): ReactElement | null {
|
||||
if (!checks || checks.length === 0) return null;
|
||||
|
||||
const failed = checks.filter((c) => c.status === "failure").length;
|
||||
if (failed === 0) return null;
|
||||
|
||||
const label = `${failed} failed`;
|
||||
|
||||
return (
|
||||
<View style={checksBadgeStyles.badge}>
|
||||
<ThemedGitHubIcon size={10} uniProps={redColorMapping} />
|
||||
<Text style={checksBadgeStyles.text}>{label}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const checksBadgeStyles = StyleSheet.create((theme) => ({
|
||||
badge: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 2,
|
||||
},
|
||||
text: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
lineHeight: 14,
|
||||
color: theme.colors.palette.red[500],
|
||||
},
|
||||
}));
|
||||
|
||||
function WorkspaceStatusIndicator({
|
||||
bucket,
|
||||
workspaceKind,
|
||||
loading = false,
|
||||
}: {
|
||||
bucket: SidebarWorkspaceEntry["statusBucket"];
|
||||
workspaceKind: SidebarWorkspaceEntry["workspaceKind"];
|
||||
loading?: boolean;
|
||||
}) {
|
||||
const shouldShowSyncedLoader = shouldRenderSyncedStatusLoader({ bucket });
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={styles.workspaceStatusDot}>
|
||||
<ThemedActivityIndicator size={8} uniProps={foregroundMutedColorMapping} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldShowSyncedLoader) {
|
||||
return (
|
||||
<View style={styles.workspaceStatusDot}>
|
||||
<ThemedSyncedLoader size={11} uniProps={syncedLoaderColorMapping} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (bucket === "needs_input") {
|
||||
return (
|
||||
<View style={styles.workspaceStatusDot}>
|
||||
<ThemedCircleAlert size={14} uniProps={amberColorMapping} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
let KindIcon: typeof ThemedMonitor | null;
|
||||
if (workspaceKind === "local_checkout") KindIcon = ThemedMonitor;
|
||||
else if (workspaceKind === "worktree") KindIcon = ThemedFolderGit2;
|
||||
else KindIcon = null;
|
||||
if (!KindIcon) return null;
|
||||
|
||||
const dotColorStyle = getStatusDotColorStyle(bucket);
|
||||
const statusDotSize = isEmphasizedStatusDotBucket(bucket)
|
||||
? EMPHASIZED_STATUS_DOT_SIZE
|
||||
: DEFAULT_STATUS_DOT_SIZE;
|
||||
const statusDotOffset =
|
||||
statusDotSize === EMPHASIZED_STATUS_DOT_SIZE
|
||||
? EMPHASIZED_STATUS_DOT_OFFSET
|
||||
: DEFAULT_STATUS_DOT_OFFSET;
|
||||
|
||||
return (
|
||||
<View style={styles.workspaceStatusDot}>
|
||||
<KindIcon size={14} uniProps={foregroundMutedColorMapping} />
|
||||
{dotColorStyle ? (
|
||||
<StatusDotOverlay
|
||||
dotColorStyle={dotColorStyle}
|
||||
size={statusDotSize}
|
||||
offset={statusDotOffset}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusDotOverlay({
|
||||
dotColorStyle,
|
||||
size,
|
||||
@@ -620,6 +551,9 @@ function ProjectRowTrailingActions({
|
||||
const trash2LeadingIcon = <ThemedTrash2 size={14} uniProps={foregroundMutedColorMapping} />;
|
||||
const settingsLeadingIcon = <ThemedSettings size={14} uniProps={foregroundMutedColorMapping} />;
|
||||
const copyLeadingIcon = <ThemedCopy size={14} uniProps={foregroundMutedColorMapping} />;
|
||||
const markAsReadLeadingIcon = (
|
||||
<ThemedCircleCheck size={14} uniProps={foregroundMutedColorMapping} />
|
||||
);
|
||||
const archiveLeadingIcon = <ThemedArchive size={14} uniProps={foregroundMutedColorMapping} />;
|
||||
const renameLeadingIcon = <ThemedPencil size={14} uniProps={foregroundMutedColorMapping} />;
|
||||
|
||||
@@ -695,6 +629,7 @@ function WorkspaceRowRightGroup({
|
||||
archivePendingLabel,
|
||||
archiveShortcutKeys,
|
||||
onArchive,
|
||||
onMarkAsRead,
|
||||
onCopyBranchName,
|
||||
onCopyPath,
|
||||
onRename,
|
||||
@@ -712,13 +647,14 @@ function WorkspaceRowRightGroup({
|
||||
archivePendingLabel?: string;
|
||||
archiveShortcutKeys?: ShortcutKey[][] | null;
|
||||
onArchive?: () => void;
|
||||
onMarkAsRead?: () => void;
|
||||
onCopyBranchName?: () => void;
|
||||
onCopyPath?: () => void;
|
||||
onRename?: () => void;
|
||||
}) {
|
||||
const showKebab = Boolean(onArchive && (isHovered || isTouchPlatform));
|
||||
return (
|
||||
<View style={styles.workspaceRowRight}>
|
||||
<>
|
||||
{showScriptsIcon ? (
|
||||
<View testID="workspace-globe-icon" accessibilityLabel="Scripts available">
|
||||
{hasRunningService ? (
|
||||
@@ -735,6 +671,7 @@ function WorkspaceRowRightGroup({
|
||||
onCopyPath={onCopyPath}
|
||||
onCopyBranchName={onCopyBranchName}
|
||||
onRename={onRename}
|
||||
onMarkAsRead={onMarkAsRead}
|
||||
onArchive={onArchive}
|
||||
archiveLabel={archiveLabel}
|
||||
archiveStatus={archiveStatus}
|
||||
@@ -749,11 +686,11 @@ function WorkspaceRowRightGroup({
|
||||
/>
|
||||
) : null}
|
||||
{showShortcutBadge && shortcutNumber !== null ? (
|
||||
<View style={styles.shortcutBadge}>
|
||||
<Text style={styles.shortcutBadgeText}>{shortcutNumber}</Text>
|
||||
<View style={sidebarWorkspaceRowStyles.shortcutBadge}>
|
||||
<Text style={sidebarWorkspaceRowStyles.shortcutBadgeText}>{shortcutNumber}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -762,6 +699,7 @@ function WorkspaceKebabMenu({
|
||||
onCopyPath,
|
||||
onCopyBranchName,
|
||||
onRename,
|
||||
onMarkAsRead,
|
||||
onArchive,
|
||||
archiveLabel,
|
||||
archiveStatus,
|
||||
@@ -772,6 +710,7 @@ function WorkspaceKebabMenu({
|
||||
onCopyPath?: () => void;
|
||||
onCopyBranchName?: () => void;
|
||||
onRename?: () => void;
|
||||
onMarkAsRead?: () => void;
|
||||
onArchive: () => void;
|
||||
archiveLabel?: string;
|
||||
archiveStatus?: "idle" | "pending" | "success";
|
||||
@@ -821,6 +760,15 @@ function WorkspaceKebabMenu({
|
||||
Rename workspace
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{onMarkAsRead ? (
|
||||
<DropdownMenuItem
|
||||
testID={`sidebar-workspace-menu-mark-as-read-${workspaceKey}`}
|
||||
leading={markAsReadLeadingIcon}
|
||||
onSelect={onMarkAsRead}
|
||||
>
|
||||
Mark as read
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
<DropdownMenuItem
|
||||
testID={`sidebar-workspace-menu-archive-${workspaceKey}`}
|
||||
leading={archiveLeadingIcon}
|
||||
@@ -1404,9 +1352,7 @@ function WorkspaceRowInner({
|
||||
archiveShortcutKeys,
|
||||
}: WorkspaceRowInnerProps) {
|
||||
const _isCompact = useIsCompactFormFactor();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const isTouchPlatform = platformIsNative;
|
||||
const prHint = workspace.prHint;
|
||||
const interaction = useLongPressDragInteraction({
|
||||
drag,
|
||||
menuController,
|
||||
@@ -1426,96 +1372,71 @@ function WorkspaceRowInner({
|
||||
onPress();
|
||||
}, [interaction.didLongPressRef, onPress]);
|
||||
|
||||
const handlePointerEnter = useCallback(() => setIsHovered(true), []);
|
||||
const handlePointerLeave = useCallback(() => setIsHovered(false), []);
|
||||
|
||||
const workspaceRowStyle = useCallback(
|
||||
({ pressed }: PressableStateCallbackType) => [
|
||||
styles.workspaceRow,
|
||||
isDragging && styles.workspaceRowDragging,
|
||||
selected && styles.sidebarRowSelected,
|
||||
isHovered && styles.workspaceRowHovered,
|
||||
pressed && styles.workspaceRowPressed,
|
||||
],
|
||||
[isDragging, selected, isHovered],
|
||||
);
|
||||
|
||||
const isDesktop = !isTouchPlatform;
|
||||
const showScriptsIcon = isDesktop && workspace.hasRunningScripts;
|
||||
const hasRunningService = workspace.scripts.some(
|
||||
(s) => s.lifecycle === "running" && (s.type ?? "service") === "service",
|
||||
);
|
||||
|
||||
const accessibilityState = useMemo(() => ({ selected }), [selected]);
|
||||
const workspaceBranchTextStyle = useMemo(
|
||||
() => [
|
||||
styles.workspaceBranchText,
|
||||
isHovered && styles.workspaceBranchTextHovered,
|
||||
isCreating && styles.workspaceBranchTextCreating,
|
||||
],
|
||||
[isHovered, isCreating],
|
||||
);
|
||||
|
||||
return (
|
||||
<WorkspaceHoverCard workspace={workspace} prHint={prHint} isDragging={isDragging}>
|
||||
<View
|
||||
{...dragAttributes}
|
||||
{...dragHandleProps?.listeners}
|
||||
ref={dragHandleProps?.setActivatorNodeRef as unknown as Ref<View>}
|
||||
style={styles.workspaceRowContainer}
|
||||
onPointerEnter={handlePointerEnter}
|
||||
onPointerLeave={handlePointerLeave}
|
||||
>
|
||||
<Pressable
|
||||
disabled={isArchiving}
|
||||
aria-selected={selected}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={accessibilityState}
|
||||
style={workspaceRowStyle}
|
||||
onPressIn={interaction.handlePressIn}
|
||||
onTouchMove={interaction.handleTouchMove}
|
||||
onPressOut={interaction.handlePressOut}
|
||||
onPress={handlePress}
|
||||
testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
|
||||
>
|
||||
<View style={styles.workspaceRowMain}>
|
||||
<View style={styles.workspaceRowLeft}>
|
||||
<WorkspaceStatusIndicator
|
||||
bucket={workspace.statusBucket}
|
||||
workspaceKind={workspace.workspaceKind}
|
||||
loading={isArchiving || isCreating}
|
||||
/>
|
||||
<Text style={workspaceBranchTextStyle} numberOfLines={1}>
|
||||
{workspace.name}
|
||||
</Text>
|
||||
</View>
|
||||
<WorkspaceRowRightGroup
|
||||
workspace={workspace}
|
||||
isHovered={isHovered}
|
||||
isTouchPlatform={isTouchPlatform}
|
||||
showScriptsIcon={showScriptsIcon}
|
||||
hasRunningService={hasRunningService}
|
||||
isCreating={isCreating}
|
||||
showShortcutBadge={showShortcutBadge}
|
||||
shortcutNumber={shortcutNumber}
|
||||
archiveLabel={archiveLabel}
|
||||
archiveStatus={archiveStatus}
|
||||
archivePendingLabel={archivePendingLabel}
|
||||
archiveShortcutKeys={archiveShortcutKeys}
|
||||
onArchive={onArchive}
|
||||
onCopyBranchName={onCopyBranchName}
|
||||
onCopyPath={onCopyPath}
|
||||
onRename={onRename}
|
||||
/>
|
||||
<SidebarWorkspaceRowFrame workspace={workspace} isDragging={isDragging}>
|
||||
{({ isHovered, hoverHandlers }) => {
|
||||
const isDesktop = !isTouchPlatform;
|
||||
const showScriptsIcon = isDesktop && workspace.hasRunningScripts;
|
||||
const hasRunningService = workspace.scripts.some(
|
||||
(s) => s.lifecycle === "running" && (s.type ?? "service") === "service",
|
||||
);
|
||||
const workspaceRowStyle = getProjectWorkspaceRowStyle({
|
||||
isDragging,
|
||||
selected,
|
||||
isHovered,
|
||||
});
|
||||
return (
|
||||
<View
|
||||
{...dragAttributes}
|
||||
{...dragHandleProps?.listeners}
|
||||
ref={dragHandleProps?.setActivatorNodeRef as unknown as Ref<View>}
|
||||
style={styles.workspaceRowContainer}
|
||||
{...hoverHandlers}
|
||||
>
|
||||
<Pressable
|
||||
disabled={isArchiving}
|
||||
aria-selected={selected}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={accessibilityState}
|
||||
style={workspaceRowStyle}
|
||||
onPressIn={interaction.handlePressIn}
|
||||
onTouchMove={interaction.handleTouchMove}
|
||||
onPressOut={interaction.handlePressOut}
|
||||
onPress={handlePress}
|
||||
testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
|
||||
>
|
||||
<SidebarWorkspaceRowContent
|
||||
workspace={workspace}
|
||||
isHovered={isHovered}
|
||||
isLoading={isArchiving || isCreating}
|
||||
isCreating={isCreating}
|
||||
>
|
||||
<WorkspaceRowRightGroup
|
||||
workspace={workspace}
|
||||
isHovered={isHovered}
|
||||
isTouchPlatform={isTouchPlatform}
|
||||
showScriptsIcon={showScriptsIcon}
|
||||
hasRunningService={hasRunningService}
|
||||
isCreating={isCreating}
|
||||
showShortcutBadge={showShortcutBadge}
|
||||
shortcutNumber={shortcutNumber}
|
||||
archiveLabel={archiveLabel}
|
||||
archiveStatus={archiveStatus}
|
||||
archivePendingLabel={archivePendingLabel}
|
||||
archiveShortcutKeys={archiveShortcutKeys}
|
||||
onArchive={onArchive}
|
||||
onCopyBranchName={onCopyBranchName}
|
||||
onCopyPath={onCopyPath}
|
||||
onRename={onRename}
|
||||
/>
|
||||
</SidebarWorkspaceRowContent>
|
||||
</Pressable>
|
||||
</View>
|
||||
{prHint ? (
|
||||
<View style={styles.workspacePrBadgeRow}>
|
||||
<PrBadge hint={prHint} />
|
||||
<ChecksBadge checks={prHint.checks} />
|
||||
</View>
|
||||
) : null}
|
||||
</Pressable>
|
||||
</View>
|
||||
</WorkspaceHoverCard>
|
||||
);
|
||||
}}
|
||||
</SidebarWorkspaceRowFrame>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1722,6 +1643,15 @@ function WorkspaceRowWithMenu({
|
||||
}, []);
|
||||
|
||||
const archiveShortcutKeys = useShortcutKeys("archive-worktree");
|
||||
const { hasClearableAttention, clearAttention } = useClearWorkspaceAttention({
|
||||
serverId: workspace.serverId,
|
||||
workspaceId: workspace.workspaceId,
|
||||
});
|
||||
const handleMarkAsRead = useCallback(() => {
|
||||
void clearAttention().catch((error) => {
|
||||
toast.error(error instanceof Error ? error.message : "Failed to mark workspace as read");
|
||||
});
|
||||
}, [clearAttention, toast]);
|
||||
|
||||
useKeyboardActionHandler({
|
||||
handlerId: `worktree-archive-${workspace.workspaceKey}`,
|
||||
@@ -1759,6 +1689,7 @@ function WorkspaceRowWithMenu({
|
||||
onCopyBranchName={canCopyBranchName ? handleCopyBranchName : undefined}
|
||||
onCopyPath={handleCopyPath}
|
||||
onRename={canCopyBranchName ? handleOpenRename : undefined}
|
||||
onMarkAsRead={hasClearableAttention ? handleMarkAsRead : undefined}
|
||||
archiveShortcutKeys={selected ? archiveShortcutKeys : null}
|
||||
/>
|
||||
<AdaptiveRenameModal
|
||||
@@ -2429,6 +2360,7 @@ export function SidebarWorkspaceList({
|
||||
collapsedProjectKeys,
|
||||
onToggleProjectCollapsed,
|
||||
shortcutIndexByWorkspaceKey,
|
||||
groupMode,
|
||||
isRefreshing: _isRefreshing = false,
|
||||
onRefresh: _onRefresh,
|
||||
onWorkspacePress,
|
||||
@@ -2437,6 +2369,72 @@ export function SidebarWorkspaceList({
|
||||
parentGestureRef,
|
||||
}: SidebarWorkspaceListProps) {
|
||||
const pathname = usePathname();
|
||||
|
||||
if (groupMode === "status") {
|
||||
return (
|
||||
<SidebarStatusModeWrapper
|
||||
serverId={serverId}
|
||||
shortcutIndexByWorkspaceKey={shortcutIndexByWorkspaceKey}
|
||||
onWorkspacePress={onWorkspacePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ProjectModeList
|
||||
projects={projects}
|
||||
serverId={serverId}
|
||||
collapsedProjectKeys={collapsedProjectKeys}
|
||||
onToggleProjectCollapsed={onToggleProjectCollapsed}
|
||||
shortcutIndexByWorkspaceKey={shortcutIndexByWorkspaceKey}
|
||||
onWorkspacePress={onWorkspacePress}
|
||||
onAddProject={onAddProject}
|
||||
listFooterComponent={listFooterComponent}
|
||||
parentGestureRef={parentGestureRef}
|
||||
pathname={pathname}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarStatusModeWrapper({
|
||||
serverId,
|
||||
shortcutIndexByWorkspaceKey: _projectShortcutIndex,
|
||||
onWorkspacePress,
|
||||
}: {
|
||||
serverId: string | null;
|
||||
shortcutIndexByWorkspaceKey: Map<string, number>;
|
||||
onWorkspacePress?: () => void;
|
||||
}) {
|
||||
const hydratedWorkspaces = useHydratedWorkspaceEntries(serverId);
|
||||
const projectNamesByKey = useProjectNamesMap(serverId);
|
||||
const showShortcutBadges = useShowShortcutBadges();
|
||||
|
||||
return (
|
||||
<SidebarStatusWorkspaceList
|
||||
workspaces={hydratedWorkspaces}
|
||||
projectNamesByKey={projectNamesByKey}
|
||||
serverId={serverId}
|
||||
shortcutIndexByWorkspaceKey={_projectShortcutIndex}
|
||||
showShortcutBadges={showShortcutBadges}
|
||||
onWorkspacePress={onWorkspacePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectModeList({
|
||||
projects,
|
||||
serverId,
|
||||
collapsedProjectKeys,
|
||||
onToggleProjectCollapsed,
|
||||
shortcutIndexByWorkspaceKey,
|
||||
onWorkspacePress,
|
||||
onAddProject,
|
||||
listFooterComponent,
|
||||
parentGestureRef,
|
||||
pathname,
|
||||
}: Omit<SidebarWorkspaceListProps, "groupMode" | "isRefreshing" | "onRefresh"> & {
|
||||
pathname: string;
|
||||
}) {
|
||||
const [creatingWorkspaceIds, setCreatingWorkspaceIds] = useState<Set<string>>(() => new Set());
|
||||
const creatingWorkspaceTimeoutsRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(
|
||||
new Map(),
|
||||
|
||||
Reference in New Issue
Block a user