Show project icons in status-grouped sidebar rows (#2416)
* feat: show project icons in sidebar status view * test(sidebar): keep status icon regression active The ten-second mock can finish during app startup, removing the Working bucket before the icon assertion. Keep the seeded workspace active for the full scenario. --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
co-authored by
Mohamed Boudra
parent
d9b72e1c76
commit
87ef631ac4
@@ -122,6 +122,7 @@ test.describe("Model B sidebar shape", () => {
|
|||||||
repoPrefix: "model-b-status-active-",
|
repoPrefix: "model-b-status-active-",
|
||||||
title: "Working workspace",
|
title: "Working workspace",
|
||||||
initialPrompt: "stay busy",
|
initialPrompt: "stay busy",
|
||||||
|
model: "one-minute-stream",
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -152,6 +153,14 @@ test.describe("Model B sidebar shape", () => {
|
|||||||
// the status view.
|
// the status view.
|
||||||
await expect(sidebar.locator('[data-testid^="workspace-tab-"]')).toHaveCount(0);
|
await expect(sidebar.locator('[data-testid^="workspace-tab-"]')).toHaveCount(0);
|
||||||
|
|
||||||
|
// Status mode drops the project grouping, so each row leads its subtitle
|
||||||
|
// with the project's icon to keep projects distinguishable.
|
||||||
|
for (const workspaceId of [idleProject.workspaceId, activeMock.workspaceId]) {
|
||||||
|
await expect(
|
||||||
|
page.getByTestId(`sidebar-row-project-icon-${getServerId()}:${workspaceId}`).first(),
|
||||||
|
).toBeVisible({ timeout: 60_000 });
|
||||||
|
}
|
||||||
|
|
||||||
// The busy workspace is grouped under Working, the idle one under Done:
|
// The busy workspace is grouped under Working, the idle one under Done:
|
||||||
// changing one workspace's status moved only that row.
|
// changing one workspace's status moved only that row.
|
||||||
const workingRows = page.getByTestId("sidebar-status-group-rows-running");
|
const workingRows = page.getByTestId("sidebar-status-group-rows-running");
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ import { useClearWorkspaceAttention } from "@/hooks/use-clear-workspace-attentio
|
|||||||
import type { PrHint } from "@/git/use-pr-status-query";
|
import type { PrHint } from "@/git/use-pr-status-query";
|
||||||
import {
|
import {
|
||||||
buildSidebarProjectRowModel,
|
buildSidebarProjectRowModel,
|
||||||
resolveSidebarProjectIconTarget,
|
resolveSidebarProjectIconTargets,
|
||||||
resolveSidebarProjectLocalPath,
|
resolveSidebarProjectLocalPath,
|
||||||
type SidebarProjectHostTarget,
|
type SidebarProjectHostTarget,
|
||||||
} from "@/utils/sidebar-project-row-model";
|
} from "@/utils/sidebar-project-row-model";
|
||||||
@@ -1952,6 +1952,16 @@ export function SidebarWorkspaceList({
|
|||||||
const supportsPinningByServerId = useHostFeatureMap(serverIds, "workspacePinning");
|
const supportsPinningByServerId = useHostFeatureMap(serverIds, "workspacePinning");
|
||||||
const onToggleWorkspacePin = useSidebarWorkspacePinController();
|
const onToggleWorkspacePin = useSidebarWorkspacePinController();
|
||||||
const showHostLabels = useMemo(() => shouldShowSidebarHostLabels(projects), [projects]);
|
const showHostLabels = useMemo(() => shouldShowSidebarHostLabels(projects), [projects]);
|
||||||
|
// Status mode drops the project grouping, so its rows carry their own project
|
||||||
|
// icon. Project mode fetches the same icons inside ProjectModeList for its
|
||||||
|
// project headers, so only the active mode requests them.
|
||||||
|
const statusProjectIconTargets = useMemo(
|
||||||
|
() => (groupMode === "status" ? resolveSidebarProjectIconTargets(projects) : []),
|
||||||
|
[groupMode, projects],
|
||||||
|
);
|
||||||
|
const statusProjectIconByProjectKey = useProjectIconDataByProjectKey({
|
||||||
|
projects: statusProjectIconTargets,
|
||||||
|
});
|
||||||
|
|
||||||
const content =
|
const content =
|
||||||
groupMode === "status" ? (
|
groupMode === "status" ? (
|
||||||
@@ -1960,6 +1970,7 @@ export function SidebarWorkspaceList({
|
|||||||
pinnedGroups={pinnedGroups}
|
pinnedGroups={pinnedGroups}
|
||||||
workspaceEntriesByKey={workspaceEntriesByKey}
|
workspaceEntriesByKey={workspaceEntriesByKey}
|
||||||
projectNamesByKey={projectNamesByKey}
|
projectNamesByKey={projectNamesByKey}
|
||||||
|
projectIconByProjectKey={statusProjectIconByProjectKey}
|
||||||
shortcutIndexByWorkspaceKey={shortcutIndexByWorkspaceKey}
|
shortcutIndexByWorkspaceKey={shortcutIndexByWorkspaceKey}
|
||||||
onWorkspacePress={onWorkspacePress}
|
onWorkspacePress={onWorkspacePress}
|
||||||
hostLabelByServerId={hostLabelByServerId}
|
hostLabelByServerId={hostLabelByServerId}
|
||||||
@@ -1998,6 +2009,7 @@ function SidebarStatusModeWrapper({
|
|||||||
pinnedGroups,
|
pinnedGroups,
|
||||||
workspaceEntriesByKey,
|
workspaceEntriesByKey,
|
||||||
projectNamesByKey,
|
projectNamesByKey,
|
||||||
|
projectIconByProjectKey,
|
||||||
shortcutIndexByWorkspaceKey: _projectShortcutIndex,
|
shortcutIndexByWorkspaceKey: _projectShortcutIndex,
|
||||||
onWorkspacePress,
|
onWorkspacePress,
|
||||||
hostLabelByServerId,
|
hostLabelByServerId,
|
||||||
@@ -2010,6 +2022,7 @@ function SidebarStatusModeWrapper({
|
|||||||
pinnedGroups: PinnedSidebarGroups;
|
pinnedGroups: PinnedSidebarGroups;
|
||||||
workspaceEntriesByKey: ReadonlyMap<string, SidebarWorkspaceEntry>;
|
workspaceEntriesByKey: ReadonlyMap<string, SidebarWorkspaceEntry>;
|
||||||
projectNamesByKey: Map<string, string>;
|
projectNamesByKey: Map<string, string>;
|
||||||
|
projectIconByProjectKey: ReadonlyMap<string, string | null>;
|
||||||
shortcutIndexByWorkspaceKey: Map<string, number>;
|
shortcutIndexByWorkspaceKey: Map<string, number>;
|
||||||
onWorkspacePress?: () => void;
|
onWorkspacePress?: () => void;
|
||||||
hostLabelByServerId: ReadonlyMap<string, string>;
|
hostLabelByServerId: ReadonlyMap<string, string>;
|
||||||
@@ -2028,6 +2041,7 @@ function SidebarStatusModeWrapper({
|
|||||||
return entry ? [entry] : [];
|
return entry ? [entry] : [];
|
||||||
})}
|
})}
|
||||||
projectNamesByKey={projectNamesByKey}
|
projectNamesByKey={projectNamesByKey}
|
||||||
|
projectIconByProjectKey={projectIconByProjectKey}
|
||||||
shortcutIndexByWorkspaceKey={_projectShortcutIndex}
|
shortcutIndexByWorkspaceKey={_projectShortcutIndex}
|
||||||
showShortcutBadges={showShortcutBadges}
|
showShortcutBadges={showShortcutBadges}
|
||||||
onWorkspacePress={onWorkspacePress}
|
onWorkspacePress={onWorkspacePress}
|
||||||
@@ -2099,14 +2113,7 @@ function ProjectModeList({
|
|||||||
canToggle: canTogglePinnedChats,
|
canToggle: canTogglePinnedChats,
|
||||||
toggleExpanded: togglePinnedChatsExpanded,
|
toggleExpanded: togglePinnedChatsExpanded,
|
||||||
} = useLimitedSidebarGroup(pinnedChats);
|
} = useLimitedSidebarGroup(pinnedChats);
|
||||||
const projectIconTargets = useMemo(
|
const projectIconTargets = useMemo(() => resolveSidebarProjectIconTargets(projects), [projects]);
|
||||||
() =>
|
|
||||||
projects.flatMap((project) => {
|
|
||||||
const target = resolveSidebarProjectIconTarget(project);
|
|
||||||
return target ? [{ projectKey: project.projectKey, ...target }] : [];
|
|
||||||
}),
|
|
||||||
[projects],
|
|
||||||
);
|
|
||||||
const nativeScrollGestureProps = useMemo(
|
const nativeScrollGestureProps = useMemo(
|
||||||
() =>
|
() =>
|
||||||
parentGestureRef
|
parentGestureRef
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ interface StatusWorkspaceListProps {
|
|||||||
groups: StatusGroup[];
|
groups: StatusGroup[];
|
||||||
pinnedWorkspaces: SidebarWorkspaceEntry[];
|
pinnedWorkspaces: SidebarWorkspaceEntry[];
|
||||||
projectNamesByKey: Map<string, string>;
|
projectNamesByKey: Map<string, string>;
|
||||||
|
projectIconByProjectKey: ReadonlyMap<string, string | null>;
|
||||||
shortcutIndexByWorkspaceKey: Map<string, number>;
|
shortcutIndexByWorkspaceKey: Map<string, number>;
|
||||||
showShortcutBadges: boolean;
|
showShortcutBadges: boolean;
|
||||||
onWorkspacePress?: () => void;
|
onWorkspacePress?: () => void;
|
||||||
@@ -79,6 +80,7 @@ export function SidebarStatusWorkspaceList({
|
|||||||
groups,
|
groups,
|
||||||
pinnedWorkspaces,
|
pinnedWorkspaces,
|
||||||
projectNamesByKey,
|
projectNamesByKey,
|
||||||
|
projectIconByProjectKey,
|
||||||
shortcutIndexByWorkspaceKey,
|
shortcutIndexByWorkspaceKey,
|
||||||
showShortcutBadges,
|
showShortcutBadges,
|
||||||
onWorkspacePress,
|
onWorkspacePress,
|
||||||
@@ -114,11 +116,12 @@ export function SidebarStatusWorkspaceList({
|
|||||||
<StatusWorkspaceRow
|
<StatusWorkspaceRow
|
||||||
key={workspace.workspaceKey}
|
key={workspace.workspaceKey}
|
||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
subtitle={buildStatusRowSubtitle({
|
{...buildStatusRowProjectPresentation({
|
||||||
projectName: projectNamesByKey.get(workspace.projectKey) ?? "",
|
workspace,
|
||||||
hostLabel: showHostLabels
|
projectNamesByKey,
|
||||||
? (hostLabelByServerId.get(workspace.serverId) ?? workspace.serverId)
|
projectIconByProjectKey,
|
||||||
: null,
|
hostLabelByServerId,
|
||||||
|
showHostLabels,
|
||||||
})}
|
})}
|
||||||
shortcutNumber={statusShortcutIndex.get(workspace.workspaceKey) ?? null}
|
shortcutNumber={statusShortcutIndex.get(workspace.workspaceKey) ?? null}
|
||||||
showShortcutBadge={showShortcutBadges}
|
showShortcutBadge={showShortcutBadges}
|
||||||
@@ -143,6 +146,7 @@ export function SidebarStatusWorkspaceList({
|
|||||||
groups={groups}
|
groups={groups}
|
||||||
collapsedStatusGroupKeys={collapsedStatusGroupKeys}
|
collapsedStatusGroupKeys={collapsedStatusGroupKeys}
|
||||||
projectNamesByKey={projectNamesByKey}
|
projectNamesByKey={projectNamesByKey}
|
||||||
|
projectIconByProjectKey={projectIconByProjectKey}
|
||||||
shortcutIndex={statusShortcutIndex}
|
shortcutIndex={statusShortcutIndex}
|
||||||
showShortcutBadges={showShortcutBadges}
|
showShortcutBadges={showShortcutBadges}
|
||||||
onWorkspacePress={onWorkspacePress}
|
onWorkspacePress={onWorkspacePress}
|
||||||
@@ -183,6 +187,7 @@ function StatusGroupList({
|
|||||||
groups,
|
groups,
|
||||||
collapsedStatusGroupKeys,
|
collapsedStatusGroupKeys,
|
||||||
projectNamesByKey,
|
projectNamesByKey,
|
||||||
|
projectIconByProjectKey,
|
||||||
shortcutIndex,
|
shortcutIndex,
|
||||||
showShortcutBadges,
|
showShortcutBadges,
|
||||||
onWorkspacePress,
|
onWorkspacePress,
|
||||||
@@ -194,6 +199,7 @@ function StatusGroupList({
|
|||||||
groups: StatusGroup[];
|
groups: StatusGroup[];
|
||||||
collapsedStatusGroupKeys: ReadonlySet<string>;
|
collapsedStatusGroupKeys: ReadonlySet<string>;
|
||||||
projectNamesByKey: Map<string, string>;
|
projectNamesByKey: Map<string, string>;
|
||||||
|
projectIconByProjectKey: ReadonlyMap<string, string | null>;
|
||||||
shortcutIndex: Map<string, number>;
|
shortcutIndex: Map<string, number>;
|
||||||
showShortcutBadges: boolean;
|
showShortcutBadges: boolean;
|
||||||
onWorkspacePress?: () => void;
|
onWorkspacePress?: () => void;
|
||||||
@@ -210,6 +216,7 @@ function StatusGroupList({
|
|||||||
group={group}
|
group={group}
|
||||||
collapsed={collapsedStatusGroupKeys.has(group.bucket)}
|
collapsed={collapsedStatusGroupKeys.has(group.bucket)}
|
||||||
projectNamesByKey={projectNamesByKey}
|
projectNamesByKey={projectNamesByKey}
|
||||||
|
projectIconByProjectKey={projectIconByProjectKey}
|
||||||
shortcutIndex={shortcutIndex}
|
shortcutIndex={shortcutIndex}
|
||||||
showShortcutBadges={showShortcutBadges}
|
showShortcutBadges={showShortcutBadges}
|
||||||
onWorkspacePress={onWorkspacePress}
|
onWorkspacePress={onWorkspacePress}
|
||||||
@@ -227,6 +234,7 @@ function StatusGroupRows({
|
|||||||
group,
|
group,
|
||||||
collapsed,
|
collapsed,
|
||||||
projectNamesByKey,
|
projectNamesByKey,
|
||||||
|
projectIconByProjectKey,
|
||||||
shortcutIndex,
|
shortcutIndex,
|
||||||
showShortcutBadges,
|
showShortcutBadges,
|
||||||
onWorkspacePress,
|
onWorkspacePress,
|
||||||
@@ -238,6 +246,7 @@ function StatusGroupRows({
|
|||||||
group: StatusGroup;
|
group: StatusGroup;
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
projectNamesByKey: Map<string, string>;
|
projectNamesByKey: Map<string, string>;
|
||||||
|
projectIconByProjectKey: ReadonlyMap<string, string | null>;
|
||||||
shortcutIndex: Map<string, number>;
|
shortcutIndex: Map<string, number>;
|
||||||
showShortcutBadges: boolean;
|
showShortcutBadges: boolean;
|
||||||
onWorkspacePress?: () => void;
|
onWorkspacePress?: () => void;
|
||||||
@@ -265,11 +274,12 @@ function StatusGroupRows({
|
|||||||
<StatusWorkspaceRow
|
<StatusWorkspaceRow
|
||||||
key={workspace.workspaceKey}
|
key={workspace.workspaceKey}
|
||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
subtitle={buildStatusRowSubtitle({
|
{...buildStatusRowProjectPresentation({
|
||||||
projectName: projectNamesByKey.get(workspace.projectKey) ?? "",
|
workspace,
|
||||||
hostLabel: showHostLabels
|
projectNamesByKey,
|
||||||
? (hostLabelByServerId.get(workspace.serverId) ?? workspace.serverId)
|
projectIconByProjectKey,
|
||||||
: null,
|
hostLabelByServerId,
|
||||||
|
showHostLabels,
|
||||||
})}
|
})}
|
||||||
shortcutNumber={shortcutIndex.get(workspace.workspaceKey) ?? null}
|
shortcutNumber={shortcutIndex.get(workspace.workspaceKey) ?? null}
|
||||||
showShortcutBadge={showShortcutBadges}
|
showShortcutBadge={showShortcutBadges}
|
||||||
@@ -306,6 +316,36 @@ function buildStatusRowSubtitle({
|
|||||||
return projectName ? `${projectName} · ${hostLabel}` : hostLabel;
|
return projectName ? `${projectName} · ${hostLabel}` : hostLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface StatusRowProjectPresentation {
|
||||||
|
subtitle: string;
|
||||||
|
projectName: string;
|
||||||
|
projectIconDataUri: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildStatusRowProjectPresentation({
|
||||||
|
workspace,
|
||||||
|
projectNamesByKey,
|
||||||
|
projectIconByProjectKey,
|
||||||
|
hostLabelByServerId,
|
||||||
|
showHostLabels,
|
||||||
|
}: {
|
||||||
|
workspace: SidebarWorkspaceEntry;
|
||||||
|
projectNamesByKey: Map<string, string>;
|
||||||
|
projectIconByProjectKey: ReadonlyMap<string, string | null>;
|
||||||
|
hostLabelByServerId: ReadonlyMap<string, string>;
|
||||||
|
showHostLabels: boolean;
|
||||||
|
}): StatusRowProjectPresentation {
|
||||||
|
const projectName = projectNamesByKey.get(workspace.projectKey) ?? "";
|
||||||
|
const hostLabel = showHostLabels
|
||||||
|
? (hostLabelByServerId.get(workspace.serverId) ?? workspace.serverId)
|
||||||
|
: null;
|
||||||
|
return {
|
||||||
|
subtitle: buildStatusRowSubtitle({ projectName, hostLabel }),
|
||||||
|
projectName,
|
||||||
|
projectIconDataUri: projectIconByProjectKey.get(workspace.projectKey) ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function StatusGroupHeader({ group, collapsed }: { group: StatusGroup; collapsed: boolean }) {
|
function StatusGroupHeader({ group, collapsed }: { group: StatusGroup; collapsed: boolean }) {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const toggleStatusGroupCollapsed = useSidebarCollapsedSectionsStore(
|
const toggleStatusGroupCollapsed = useSidebarCollapsedSectionsStore(
|
||||||
@@ -391,6 +431,8 @@ function StatusGroupIcon({ bucket }: { bucket: StatusGroup["bucket"] }) {
|
|||||||
const StatusWorkspaceRow = memo(function StatusWorkspaceRow({
|
const StatusWorkspaceRow = memo(function StatusWorkspaceRow({
|
||||||
workspace,
|
workspace,
|
||||||
subtitle,
|
subtitle,
|
||||||
|
projectName,
|
||||||
|
projectIconDataUri,
|
||||||
shortcutNumber,
|
shortcutNumber,
|
||||||
showShortcutBadge,
|
showShortcutBadge,
|
||||||
canPin,
|
canPin,
|
||||||
@@ -400,6 +442,8 @@ const StatusWorkspaceRow = memo(function StatusWorkspaceRow({
|
|||||||
}: {
|
}: {
|
||||||
workspace: SidebarWorkspaceEntry;
|
workspace: SidebarWorkspaceEntry;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
|
projectName: string;
|
||||||
|
projectIconDataUri: string | null;
|
||||||
shortcutNumber: number | null;
|
shortcutNumber: number | null;
|
||||||
showShortcutBadge: boolean;
|
showShortcutBadge: boolean;
|
||||||
canPin: boolean;
|
canPin: boolean;
|
||||||
@@ -422,6 +466,8 @@ const StatusWorkspaceRow = memo(function StatusWorkspaceRow({
|
|||||||
<StatusWorkspaceRowWithMenu
|
<StatusWorkspaceRowWithMenu
|
||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
subtitle={subtitle}
|
subtitle={subtitle}
|
||||||
|
projectName={projectName}
|
||||||
|
projectIconDataUri={projectIconDataUri}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
shortcutNumber={shortcutNumber}
|
shortcutNumber={shortcutNumber}
|
||||||
showShortcutBadge={showShortcutBadge}
|
showShortcutBadge={showShortcutBadge}
|
||||||
@@ -436,6 +482,8 @@ const StatusWorkspaceRow = memo(function StatusWorkspaceRow({
|
|||||||
function StatusWorkspaceRowWithMenu({
|
function StatusWorkspaceRowWithMenu({
|
||||||
workspace,
|
workspace,
|
||||||
subtitle,
|
subtitle,
|
||||||
|
projectName,
|
||||||
|
projectIconDataUri,
|
||||||
selected,
|
selected,
|
||||||
shortcutNumber,
|
shortcutNumber,
|
||||||
showShortcutBadge,
|
showShortcutBadge,
|
||||||
@@ -446,6 +494,8 @@ function StatusWorkspaceRowWithMenu({
|
|||||||
}: {
|
}: {
|
||||||
workspace: SidebarWorkspaceEntry;
|
workspace: SidebarWorkspaceEntry;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
|
projectName: string;
|
||||||
|
projectIconDataUri: string | null;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
shortcutNumber: number | null;
|
shortcutNumber: number | null;
|
||||||
showShortcutBadge: boolean;
|
showShortcutBadge: boolean;
|
||||||
@@ -554,6 +604,8 @@ function StatusWorkspaceRowWithMenu({
|
|||||||
<StatusWorkspaceRowInner
|
<StatusWorkspaceRowInner
|
||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
subtitle={subtitle}
|
subtitle={subtitle}
|
||||||
|
projectName={projectName}
|
||||||
|
projectIconDataUri={projectIconDataUri}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
shortcutNumber={shortcutNumber}
|
shortcutNumber={shortcutNumber}
|
||||||
showShortcutBadge={showShortcutBadge}
|
showShortcutBadge={showShortcutBadge}
|
||||||
@@ -589,6 +641,8 @@ function StatusWorkspaceRowWithMenu({
|
|||||||
function StatusWorkspaceRowInner({
|
function StatusWorkspaceRowInner({
|
||||||
workspace,
|
workspace,
|
||||||
subtitle,
|
subtitle,
|
||||||
|
projectName,
|
||||||
|
projectIconDataUri,
|
||||||
selected,
|
selected,
|
||||||
shortcutNumber,
|
shortcutNumber,
|
||||||
showShortcutBadge,
|
showShortcutBadge,
|
||||||
@@ -609,6 +663,8 @@ function StatusWorkspaceRowInner({
|
|||||||
}: {
|
}: {
|
||||||
workspace: SidebarWorkspaceEntry;
|
workspace: SidebarWorkspaceEntry;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
|
projectName: string;
|
||||||
|
projectIconDataUri: string | null;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
shortcutNumber: number | null;
|
shortcutNumber: number | null;
|
||||||
showShortcutBadge: boolean;
|
showShortcutBadge: boolean;
|
||||||
@@ -662,6 +718,8 @@ function StatusWorkspaceRowInner({
|
|||||||
<SidebarWorkspaceRowContent
|
<SidebarWorkspaceRowContent
|
||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
subtitle={subtitle}
|
subtitle={subtitle}
|
||||||
|
subtitleProjectName={projectName}
|
||||||
|
subtitleProjectIconDataUri={projectIconDataUri}
|
||||||
scriptIconKind={scriptIconKind}
|
scriptIconKind={scriptIconKind}
|
||||||
isHovered={isHovered}
|
isHovered={isHovered}
|
||||||
isLoading={isArchiving}
|
isLoading={isArchiving}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { View } from "react-native";
|
||||||
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
|
import { ProjectIconView } from "@/components/project-icon-view";
|
||||||
|
import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-display-name";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decorative project marker for a sidebar row's subtitle line. Status mode breaks
|
||||||
|
* the project grouping, so the icon is what makes two same-named branches from
|
||||||
|
* different projects tell apart at a glance — the project name next to it carries
|
||||||
|
* the meaning, so the icon itself stays out of the accessibility tree.
|
||||||
|
*/
|
||||||
|
export function SidebarSubtitleProjectIcon({
|
||||||
|
projectKey,
|
||||||
|
projectName,
|
||||||
|
iconDataUri,
|
||||||
|
testID,
|
||||||
|
}: {
|
||||||
|
projectKey: string;
|
||||||
|
projectName: string;
|
||||||
|
iconDataUri: string | null;
|
||||||
|
testID?: string;
|
||||||
|
}) {
|
||||||
|
const placeholderInitial = projectIconPlaceholderLabelFromDisplayName(projectName)
|
||||||
|
.charAt(0)
|
||||||
|
.toUpperCase();
|
||||||
|
if (!iconDataUri && !placeholderInitial) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={styles.slot}
|
||||||
|
testID={testID}
|
||||||
|
accessibilityElementsHidden
|
||||||
|
importantForAccessibility="no-hide-descendants"
|
||||||
|
>
|
||||||
|
<ProjectIconView
|
||||||
|
iconDataUri={iconDataUri}
|
||||||
|
initial={placeholderInitial}
|
||||||
|
projectKey={projectKey}
|
||||||
|
imageStyle={styles.icon}
|
||||||
|
fallbackStyle={styles.fallback}
|
||||||
|
textStyle={styles.fallbackText}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create((theme) => ({
|
||||||
|
slot: {
|
||||||
|
width: 12,
|
||||||
|
// Match the 14px subtitle line so the icon does not change row density.
|
||||||
|
height: 14,
|
||||||
|
flexShrink: 0,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
borderRadius: theme.borderRadius.sm,
|
||||||
|
},
|
||||||
|
fallback: {
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
borderRadius: theme.borderRadius.sm,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
fallbackText: {
|
||||||
|
fontSize: 8,
|
||||||
|
lineHeight: 12,
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Monitor,
|
Monitor,
|
||||||
SquareTerminal,
|
SquareTerminal,
|
||||||
} from "lucide-react-native";
|
} from "lucide-react-native";
|
||||||
|
import { SidebarSubtitleProjectIcon } from "@/components/sidebar/sidebar-subtitle-project-icon";
|
||||||
import { WorkspaceHoverCard } from "@/components/workspace-hover-card";
|
import { WorkspaceHoverCard } from "@/components/workspace-hover-card";
|
||||||
import { SyncedLoader } from "@/components/synced-loader";
|
import { SyncedLoader } from "@/components/synced-loader";
|
||||||
import type { SidebarWorkspaceEntry } from "@/hooks/use-sidebar-workspaces-list";
|
import type { SidebarWorkspaceEntry } from "@/hooks/use-sidebar-workspaces-list";
|
||||||
@@ -93,6 +94,8 @@ export function SidebarWorkspaceRowFrame({
|
|||||||
export const SidebarWorkspaceRowContent = memo(function SidebarWorkspaceRowContent({
|
export const SidebarWorkspaceRowContent = memo(function SidebarWorkspaceRowContent({
|
||||||
workspace,
|
workspace,
|
||||||
subtitle,
|
subtitle,
|
||||||
|
subtitleProjectName = null,
|
||||||
|
subtitleProjectIconDataUri = null,
|
||||||
scriptIconKind = null,
|
scriptIconKind = null,
|
||||||
isHovered,
|
isHovered,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -104,6 +107,9 @@ export const SidebarWorkspaceRowContent = memo(function SidebarWorkspaceRowConte
|
|||||||
}: {
|
}: {
|
||||||
workspace: SidebarWorkspaceEntry;
|
workspace: SidebarWorkspaceEntry;
|
||||||
subtitle?: string | null;
|
subtitle?: string | null;
|
||||||
|
/** Project named by the subtitle. Set it to lead the subtitle line with that project's icon. */
|
||||||
|
subtitleProjectName?: string | null;
|
||||||
|
subtitleProjectIconDataUri?: string | null;
|
||||||
scriptIconKind?: SidebarWorkspaceScriptIconKind | null;
|
scriptIconKind?: SidebarWorkspaceScriptIconKind | null;
|
||||||
isHovered: boolean;
|
isHovered: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
@@ -148,9 +154,19 @@ export const SidebarWorkspaceRowContent = memo(function SidebarWorkspaceRowConte
|
|||||||
<View style={sidebarWorkspaceRowStyles.rowRight}>{children}</View>
|
<View style={sidebarWorkspaceRowStyles.rowRight}>{children}</View>
|
||||||
</View>
|
</View>
|
||||||
{subtitle ? (
|
{subtitle ? (
|
||||||
<Text style={styles.workspaceSubtitle} numberOfLines={1}>
|
<View style={styles.workspaceSubtitleRow}>
|
||||||
{subtitle}
|
{subtitleProjectName ? (
|
||||||
</Text>
|
<SidebarSubtitleProjectIcon
|
||||||
|
projectKey={workspace.projectKey}
|
||||||
|
projectName={subtitleProjectName}
|
||||||
|
iconDataUri={subtitleProjectIconDataUri}
|
||||||
|
testID={`sidebar-row-project-icon-${workspace.workspaceKey}`}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<Text style={styles.workspaceSubtitle} numberOfLines={1}>
|
||||||
|
{subtitle}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
{workspace.prHint ? (
|
{workspace.prHint ? (
|
||||||
<View style={styles.workspacePrBadgeRow}>
|
<View style={styles.workspacePrBadgeRow}>
|
||||||
@@ -567,10 +583,18 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
workspaceBranchTextHovered: {
|
workspaceBranchTextHovered: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
|
workspaceSubtitleRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[1],
|
||||||
|
minWidth: 0,
|
||||||
|
},
|
||||||
workspaceSubtitle: {
|
workspaceSubtitle: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
lineHeight: 14,
|
lineHeight: 14,
|
||||||
|
flexShrink: 1,
|
||||||
|
minWidth: 0,
|
||||||
},
|
},
|
||||||
workspacePrBadgeRow: {
|
workspacePrBadgeRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
|
|||||||
@@ -43,6 +43,19 @@ export function resolveSidebarProjectIconTarget(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SidebarProjectIconTarget extends SidebarProjectHostTarget {
|
||||||
|
projectKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveSidebarProjectIconTargets(
|
||||||
|
projects: readonly SidebarProjectEntry[],
|
||||||
|
): SidebarProjectIconTarget[] {
|
||||||
|
return projects.flatMap((project) => {
|
||||||
|
const target = resolveSidebarProjectIconTarget(project);
|
||||||
|
return target ? [{ projectKey: project.projectKey, ...target }] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveSidebarProjectLocalPath(
|
export function resolveSidebarProjectLocalPath(
|
||||||
project: SidebarProjectEntry,
|
project: SidebarProjectEntry,
|
||||||
localServerId: string | null,
|
localServerId: string | null,
|
||||||
|
|||||||
Reference in New Issue
Block a user