perf(app): limit initial sidebar workspace rendering
Large project, status, and pinned groups now render 20 rows until expanded. Newly discovered workspaces are prepended without disturbing the saved order.
This commit is contained in:
@@ -94,6 +94,8 @@ import type { StatusGroup } from "@/hooks/sidebar-status-view-model";
|
|||||||
import { SidebarWorkspaceMenu } from "@/components/sidebar/sidebar-workspace-menu";
|
import { SidebarWorkspaceMenu } from "@/components/sidebar/sidebar-workspace-menu";
|
||||||
import { useLongPressDragInteraction } from "@/components/sidebar/use-long-press-drag-interaction";
|
import { useLongPressDragInteraction } from "@/components/sidebar/use-long-press-drag-interaction";
|
||||||
import { PinnedSectionHeader } from "@/components/sidebar/pinned-section-header";
|
import { PinnedSectionHeader } from "@/components/sidebar/pinned-section-header";
|
||||||
|
import { SidebarGroupToggleRow } from "@/components/sidebar/sidebar-group-toggle-row";
|
||||||
|
import { useLimitedSidebarGroup } from "@/components/sidebar/use-limited-sidebar-group";
|
||||||
import {
|
import {
|
||||||
SidebarWorkspaceRowFrame,
|
SidebarWorkspaceRowFrame,
|
||||||
SidebarWorkspaceRowContent,
|
SidebarWorkspaceRowContent,
|
||||||
@@ -1632,6 +1634,12 @@ function ProjectBlock({
|
|||||||
supportsPinningByServerId: ReadonlyMap<string, boolean>;
|
supportsPinningByServerId: ReadonlyMap<string, boolean>;
|
||||||
onToggleWorkspacePin: ToggleSidebarWorkspacePin;
|
onToggleWorkspacePin: ToggleSidebarWorkspacePin;
|
||||||
}) {
|
}) {
|
||||||
|
const {
|
||||||
|
visibleItems: visibleWorkspaces,
|
||||||
|
expanded: workspacesExpanded,
|
||||||
|
canToggle: canToggleWorkspaces,
|
||||||
|
toggleExpanded: toggleWorkspacesExpanded,
|
||||||
|
} = useLimitedSidebarGroup(project.workspaces);
|
||||||
const rowModel = useMemo(
|
const rowModel = useMemo(
|
||||||
() =>
|
() =>
|
||||||
buildSidebarProjectRowModel({
|
buildSidebarProjectRowModel({
|
||||||
@@ -1784,19 +1792,28 @@ function ProjectBlock({
|
|||||||
if (!collapsed) {
|
if (!collapsed) {
|
||||||
if (project.workspaces.length > 0) {
|
if (project.workspaces.length > 0) {
|
||||||
projectChildren = (
|
projectChildren = (
|
||||||
<DraggableList
|
<>
|
||||||
testID={`sidebar-workspace-list-${project.projectKey}`}
|
<DraggableList
|
||||||
data={project.workspaces}
|
testID={`sidebar-workspace-list-${project.projectKey}`}
|
||||||
keyExtractor={workspaceKeyExtractor}
|
data={visibleWorkspaces}
|
||||||
renderItem={renderWorkspace}
|
keyExtractor={workspaceKeyExtractor}
|
||||||
onDragEnd={handleWorkspaceDragEnd}
|
renderItem={renderWorkspace}
|
||||||
extraData={activeWorkspaceSelectionKey(activeWorkspaceSelection)}
|
onDragEnd={handleWorkspaceDragEnd}
|
||||||
scrollEnabled={false}
|
extraData={activeWorkspaceSelectionKey(activeWorkspaceSelection)}
|
||||||
useDragHandle
|
scrollEnabled={false}
|
||||||
nestable={useNestable}
|
useDragHandle
|
||||||
simultaneousGestureRef={parentGestureRef}
|
nestable={useNestable}
|
||||||
containerStyle={styles.workspaceListContainer}
|
simultaneousGestureRef={parentGestureRef}
|
||||||
/>
|
containerStyle={styles.workspaceListContainer}
|
||||||
|
/>
|
||||||
|
{canToggleWorkspaces ? (
|
||||||
|
<SidebarGroupToggleRow
|
||||||
|
expanded={workspacesExpanded}
|
||||||
|
onPress={toggleWorkspacesExpanded}
|
||||||
|
testID={`sidebar-project-show-more-${project.projectKey}`}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
} else if (rowModel.trailingAction.kind === "new_workspace") {
|
} else if (rowModel.trailingAction.kind === "new_workspace") {
|
||||||
projectChildren = (
|
projectChildren = (
|
||||||
@@ -2073,6 +2090,12 @@ function ProjectModeList({
|
|||||||
const selectionEnabled = isWorkspaceRoute;
|
const selectionEnabled = isWorkspaceRoute;
|
||||||
const activeWorkspaceSelection = useActiveWorkspaceSelection();
|
const activeWorkspaceSelection = useActiveWorkspaceSelection();
|
||||||
const { pinnedChats, unpinnedProjects } = pinnedGroups;
|
const { pinnedChats, unpinnedProjects } = pinnedGroups;
|
||||||
|
const {
|
||||||
|
visibleItems: visiblePinnedChats,
|
||||||
|
expanded: pinnedChatsExpanded,
|
||||||
|
canToggle: canTogglePinnedChats,
|
||||||
|
toggleExpanded: togglePinnedChatsExpanded,
|
||||||
|
} = useLimitedSidebarGroup(pinnedChats);
|
||||||
const projectIconTargets = useMemo(
|
const projectIconTargets = useMemo(
|
||||||
() =>
|
() =>
|
||||||
projects.flatMap((project) => {
|
projects.flatMap((project) => {
|
||||||
@@ -2330,7 +2353,18 @@ function ProjectModeList({
|
|||||||
{pinnedChats.length > 0 ? (
|
{pinnedChats.length > 0 ? (
|
||||||
<View style={styles.pinnedSection} testID="sidebar-pinned-section">
|
<View style={styles.pinnedSection} testID="sidebar-pinned-section">
|
||||||
<PinnedSectionHeader collapsed={pinnedCollapsed} onToggle={togglePinnedCollapsed} />
|
<PinnedSectionHeader collapsed={pinnedCollapsed} onToggle={togglePinnedCollapsed} />
|
||||||
{pinnedCollapsed ? null : pinnedChats.map(renderPinnedChat)}
|
{pinnedCollapsed ? null : (
|
||||||
|
<>
|
||||||
|
{visiblePinnedChats.map(renderPinnedChat)}
|
||||||
|
{canTogglePinnedChats ? (
|
||||||
|
<SidebarGroupToggleRow
|
||||||
|
expanded={pinnedChatsExpanded}
|
||||||
|
onPress={togglePinnedChatsExpanded}
|
||||||
|
testID="sidebar-pinned-show-more"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
{unpinnedProjects.length > 0 || hasActiveHostFilter ? listHeaderComponent : null}
|
{unpinnedProjects.length > 0 || hasActiveHostFilter ? listHeaderComponent : null}
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Pressable, Text, View, type PressableStateCallbackType } from "react-native";
|
||||||
|
import { ChevronDown, ChevronUp } from "lucide-react-native";
|
||||||
|
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||||
|
import { isWeb } from "@/constants/platform";
|
||||||
|
import type { Theme } from "@/styles/theme";
|
||||||
|
|
||||||
|
const foregroundMutedColorMapping = (theme: Theme) => ({
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
});
|
||||||
|
const foregroundColorMapping = (theme: Theme) => ({
|
||||||
|
color: theme.colors.foreground,
|
||||||
|
});
|
||||||
|
const ThemedChevronDown = withUnistyles(ChevronDown);
|
||||||
|
const ThemedChevronUp = withUnistyles(ChevronUp);
|
||||||
|
|
||||||
|
export function SidebarGroupToggleRow({
|
||||||
|
expanded,
|
||||||
|
onPress,
|
||||||
|
testID,
|
||||||
|
}: {
|
||||||
|
expanded: boolean;
|
||||||
|
onPress: () => void;
|
||||||
|
testID: string;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const label = t(
|
||||||
|
expanded ? "sidebar.workspace.actions.showLess" : "sidebar.workspace.actions.showMore",
|
||||||
|
);
|
||||||
|
const rowStyle = useCallback(
|
||||||
|
({ hovered = false, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [
|
||||||
|
styles.row,
|
||||||
|
(Boolean(hovered) || pressed) && styles.rowHovered,
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole={isWeb ? undefined : "button"}
|
||||||
|
accessibilityLabel={label}
|
||||||
|
onPress={onPress}
|
||||||
|
style={rowStyle}
|
||||||
|
testID={testID}
|
||||||
|
>
|
||||||
|
{({ hovered, pressed }) => (
|
||||||
|
<>
|
||||||
|
<View style={styles.iconSlot}>
|
||||||
|
{expanded ? (
|
||||||
|
<ThemedChevronUp
|
||||||
|
size={14}
|
||||||
|
uniProps={hovered || pressed ? foregroundColorMapping : foregroundMutedColorMapping}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ThemedChevronDown
|
||||||
|
size={14}
|
||||||
|
uniProps={hovered || pressed ? foregroundColorMapping : foregroundMutedColorMapping}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Text style={hovered || pressed ? styles.textHovered : styles.text} numberOfLines={1}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create((theme) => ({
|
||||||
|
row: {
|
||||||
|
minHeight: 32,
|
||||||
|
marginLeft: theme.spacing[6],
|
||||||
|
marginRight: theme.spacing[1],
|
||||||
|
paddingVertical: theme.spacing[1],
|
||||||
|
paddingHorizontal: theme.spacing[2],
|
||||||
|
borderRadius: theme.borderRadius.md,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[2],
|
||||||
|
userSelect: "none",
|
||||||
|
},
|
||||||
|
rowHovered: {
|
||||||
|
backgroundColor: theme.colors.surfaceSidebarHover,
|
||||||
|
},
|
||||||
|
iconSlot: {
|
||||||
|
width: theme.iconSize.sm,
|
||||||
|
height: theme.iconSize.sm,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
minWidth: 0,
|
||||||
|
flexShrink: 1,
|
||||||
|
},
|
||||||
|
textHovered: {
|
||||||
|
color: theme.colors.foreground,
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
minWidth: 0,
|
||||||
|
flexShrink: 1,
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -42,6 +42,8 @@ import {
|
|||||||
import { useSidebarCollapsedSectionsStore } from "@/stores/sidebar-collapsed-sections-store";
|
import { useSidebarCollapsedSectionsStore } from "@/stores/sidebar-collapsed-sections-store";
|
||||||
import { SidebarWorkspaceMenu } from "@/components/sidebar/sidebar-workspace-menu";
|
import { SidebarWorkspaceMenu } from "@/components/sidebar/sidebar-workspace-menu";
|
||||||
import { PinnedSectionHeader } from "@/components/sidebar/pinned-section-header";
|
import { PinnedSectionHeader } from "@/components/sidebar/pinned-section-header";
|
||||||
|
import { SidebarGroupToggleRow } from "@/components/sidebar/sidebar-group-toggle-row";
|
||||||
|
import { useLimitedSidebarGroup } from "@/components/sidebar/use-limited-sidebar-group";
|
||||||
import type { ToggleSidebarWorkspacePin } from "@/hooks/use-sidebar-workspace-pin";
|
import type { ToggleSidebarWorkspacePin } from "@/hooks/use-sidebar-workspace-pin";
|
||||||
|
|
||||||
// Themed icon wrappers
|
// Themed icon wrappers
|
||||||
@@ -93,6 +95,12 @@ export function SidebarStatusWorkspaceList({
|
|||||||
const togglePinnedCollapsed = useSidebarCollapsedSectionsStore(
|
const togglePinnedCollapsed = useSidebarCollapsedSectionsStore(
|
||||||
(state) => state.togglePinnedCollapsed,
|
(state) => state.togglePinnedCollapsed,
|
||||||
);
|
);
|
||||||
|
const {
|
||||||
|
visibleItems: visiblePinnedWorkspaces,
|
||||||
|
expanded: pinnedWorkspacesExpanded,
|
||||||
|
canToggle: canTogglePinnedWorkspaces,
|
||||||
|
toggleExpanded: togglePinnedWorkspacesExpanded,
|
||||||
|
} = useLimitedSidebarGroup(pinnedWorkspaces);
|
||||||
|
|
||||||
const statusShortcutIndex = showShortcutBadges ? shortcutIndexByWorkspaceKey : new Map();
|
const statusShortcutIndex = showShortcutBadges ? shortcutIndexByWorkspaceKey : new Map();
|
||||||
const content = (
|
const content = (
|
||||||
@@ -100,9 +108,9 @@ export function SidebarStatusWorkspaceList({
|
|||||||
{pinnedWorkspaces.length > 0 ? (
|
{pinnedWorkspaces.length > 0 ? (
|
||||||
<View style={styles.pinnedSection} testID="sidebar-pinned-section">
|
<View style={styles.pinnedSection} testID="sidebar-pinned-section">
|
||||||
<PinnedSectionHeader collapsed={pinnedCollapsed} onToggle={togglePinnedCollapsed} />
|
<PinnedSectionHeader collapsed={pinnedCollapsed} onToggle={togglePinnedCollapsed} />
|
||||||
{pinnedCollapsed
|
{pinnedCollapsed ? null : (
|
||||||
? null
|
<>
|
||||||
: pinnedWorkspaces.map((workspace) => (
|
{visiblePinnedWorkspaces.map((workspace) => (
|
||||||
<StatusWorkspaceRow
|
<StatusWorkspaceRow
|
||||||
key={workspace.workspaceKey}
|
key={workspace.workspaceKey}
|
||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
@@ -119,6 +127,15 @@ export function SidebarStatusWorkspaceList({
|
|||||||
onWorkspacePress={onWorkspacePress}
|
onWorkspacePress={onWorkspacePress}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{canTogglePinnedWorkspaces ? (
|
||||||
|
<SidebarGroupToggleRow
|
||||||
|
expanded={pinnedWorkspacesExpanded}
|
||||||
|
onPress={togglePinnedWorkspacesExpanded}
|
||||||
|
testID="sidebar-pinned-show-more"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
{listHeaderComponent}
|
{listHeaderComponent}
|
||||||
@@ -188,38 +205,92 @@ function StatusGroupList({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{groups.map((group) => (
|
{groups.map((group) => (
|
||||||
<View key={group.bucket} style={styles.statusGroupBlock}>
|
<StatusGroupRows
|
||||||
<StatusGroupHeader group={group} collapsed={collapsedStatusGroupKeys.has(group.bucket)} />
|
key={group.bucket}
|
||||||
{!collapsedStatusGroupKeys.has(group.bucket) ? (
|
group={group}
|
||||||
<View
|
collapsed={collapsedStatusGroupKeys.has(group.bucket)}
|
||||||
style={styles.statusWorkspaceListContainer}
|
projectNamesByKey={projectNamesByKey}
|
||||||
testID={`sidebar-status-group-rows-${group.bucket}`}
|
shortcutIndex={shortcutIndex}
|
||||||
>
|
showShortcutBadges={showShortcutBadges}
|
||||||
{group.rows.map((workspace) => (
|
onWorkspacePress={onWorkspacePress}
|
||||||
<StatusWorkspaceRow
|
hostLabelByServerId={hostLabelByServerId}
|
||||||
key={workspace.workspaceKey}
|
showHostLabels={showHostLabels}
|
||||||
workspace={workspace}
|
supportsPinningByServerId={supportsPinningByServerId}
|
||||||
subtitle={buildStatusRowSubtitle({
|
onToggleWorkspacePin={onToggleWorkspacePin}
|
||||||
projectName: projectNamesByKey.get(workspace.projectKey) ?? "",
|
/>
|
||||||
hostLabel: showHostLabels
|
|
||||||
? (hostLabelByServerId.get(workspace.serverId) ?? workspace.serverId)
|
|
||||||
: null,
|
|
||||||
})}
|
|
||||||
shortcutNumber={shortcutIndex.get(workspace.workspaceKey) ?? null}
|
|
||||||
showShortcutBadge={showShortcutBadges}
|
|
||||||
canPin={supportsPinningByServerId.get(workspace.serverId) === true}
|
|
||||||
onToggleWorkspacePin={onToggleWorkspacePin}
|
|
||||||
onWorkspacePress={onWorkspacePress}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function StatusGroupRows({
|
||||||
|
group,
|
||||||
|
collapsed,
|
||||||
|
projectNamesByKey,
|
||||||
|
shortcutIndex,
|
||||||
|
showShortcutBadges,
|
||||||
|
onWorkspacePress,
|
||||||
|
hostLabelByServerId,
|
||||||
|
showHostLabels,
|
||||||
|
supportsPinningByServerId,
|
||||||
|
onToggleWorkspacePin,
|
||||||
|
}: {
|
||||||
|
group: StatusGroup;
|
||||||
|
collapsed: boolean;
|
||||||
|
projectNamesByKey: Map<string, string>;
|
||||||
|
shortcutIndex: Map<string, number>;
|
||||||
|
showShortcutBadges: boolean;
|
||||||
|
onWorkspacePress?: () => void;
|
||||||
|
hostLabelByServerId: ReadonlyMap<string, string>;
|
||||||
|
showHostLabels: boolean;
|
||||||
|
supportsPinningByServerId: ReadonlyMap<string, boolean>;
|
||||||
|
onToggleWorkspacePin: ToggleSidebarWorkspacePin;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
visibleItems: visibleWorkspaces,
|
||||||
|
expanded: workspacesExpanded,
|
||||||
|
canToggle: canToggleWorkspaces,
|
||||||
|
toggleExpanded: toggleWorkspacesExpanded,
|
||||||
|
} = useLimitedSidebarGroup(group.rows);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.statusGroupBlock}>
|
||||||
|
<StatusGroupHeader group={group} collapsed={collapsed} />
|
||||||
|
{!collapsed ? (
|
||||||
|
<View
|
||||||
|
style={styles.statusWorkspaceListContainer}
|
||||||
|
testID={`sidebar-status-group-rows-${group.bucket}`}
|
||||||
|
>
|
||||||
|
{visibleWorkspaces.map((workspace) => (
|
||||||
|
<StatusWorkspaceRow
|
||||||
|
key={workspace.workspaceKey}
|
||||||
|
workspace={workspace}
|
||||||
|
subtitle={buildStatusRowSubtitle({
|
||||||
|
projectName: projectNamesByKey.get(workspace.projectKey) ?? "",
|
||||||
|
hostLabel: showHostLabels
|
||||||
|
? (hostLabelByServerId.get(workspace.serverId) ?? workspace.serverId)
|
||||||
|
: null,
|
||||||
|
})}
|
||||||
|
shortcutNumber={shortcutIndex.get(workspace.workspaceKey) ?? null}
|
||||||
|
showShortcutBadge={showShortcutBadges}
|
||||||
|
canPin={supportsPinningByServerId.get(workspace.serverId) === true}
|
||||||
|
onToggleWorkspacePin={onToggleWorkspacePin}
|
||||||
|
onWorkspacePress={onWorkspacePress}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{canToggleWorkspaces ? (
|
||||||
|
<SidebarGroupToggleRow
|
||||||
|
expanded={workspacesExpanded}
|
||||||
|
onPress={toggleWorkspacesExpanded}
|
||||||
|
testID={`sidebar-status-show-more-${group.bucket}`}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Status mode breaks the project grouping, so the row needs the project name to stay
|
// Status mode breaks the project grouping, so the row needs the project name to stay
|
||||||
// legible; the host is appended after a middle dot once labels are active.
|
// legible; the host is appended after a middle dot once labels are active.
|
||||||
function buildStatusRowSubtitle({
|
function buildStatusRowSubtitle({
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
const INITIAL_VISIBLE_ITEMS = 20;
|
||||||
|
|
||||||
|
export function useLimitedSidebarGroup<T>(items: readonly T[]) {
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const visibleItems = useMemo(
|
||||||
|
() => (expanded ? items.slice() : items.slice(0, INITIAL_VISIBLE_ITEMS)),
|
||||||
|
[expanded, items],
|
||||||
|
);
|
||||||
|
const canToggle = items.length > INITIAL_VISIBLE_ITEMS;
|
||||||
|
const toggleExpanded = useCallback(() => setExpanded((current) => !current), []);
|
||||||
|
|
||||||
|
return { visibleItems, expanded, canToggle, toggleExpanded };
|
||||||
|
}
|
||||||
@@ -471,7 +471,7 @@ describe("computeSidebarOrderUpdates", () => {
|
|||||||
expect(updates).toEqual({ projectOrder: null, workspaceOrders: [] });
|
expect(updates).toEqual({ projectOrder: null, workspaceOrders: [] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("appends unseen projects and workspaces to the persisted orders", () => {
|
it("appends unseen projects while putting unseen workspaces before the saved order", () => {
|
||||||
const projects = [
|
const projects = [
|
||||||
sidebarProject({ projectKey: "project-a", workspaceKeys: ["ws-1", "ws-2"] }),
|
sidebarProject({ projectKey: "project-a", workspaceKeys: ["ws-1", "ws-2"] }),
|
||||||
sidebarProject({ projectKey: "project-b", workspaceKeys: ["ws-3"] }),
|
sidebarProject({ projectKey: "project-b", workspaceKeys: ["ws-3"] }),
|
||||||
@@ -485,11 +485,33 @@ describe("computeSidebarOrderUpdates", () => {
|
|||||||
|
|
||||||
expect(updates.projectOrder).toEqual(["project-a", "project-b"]);
|
expect(updates.projectOrder).toEqual(["project-a", "project-b"]);
|
||||||
expect(updates.workspaceOrders).toEqual([
|
expect(updates.workspaceOrders).toEqual([
|
||||||
{ projectKey: "project-a", order: ["srv:ws-1", "srv:ws-2"] },
|
{ projectKey: "project-a", order: ["srv:ws-2", "srv:ws-1"] },
|
||||||
{ projectKey: "project-b", order: ["srv:ws-3"] },
|
{ projectKey: "project-b", order: ["srv:ws-3"] },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves the saved workspace order behind multiple newly discovered workspaces", () => {
|
||||||
|
const projects = [
|
||||||
|
sidebarProject({
|
||||||
|
projectKey: "project-a",
|
||||||
|
workspaceKeys: ["newest", "newer", "old-a", "old-b"],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const updates = computeSidebarOrderUpdates({
|
||||||
|
projects,
|
||||||
|
persistedProjectOrder: ["project-a"],
|
||||||
|
getWorkspaceOrder: () => ["srv:old-b", "srv:old-a"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updates.workspaceOrders).toEqual([
|
||||||
|
{
|
||||||
|
projectKey: "project-a",
|
||||||
|
order: ["srv:newest", "srv:newer", "srv:old-b", "srv:old-a"],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("returns no project-order update when persisted order already covers visible keys", () => {
|
it("returns no project-order update when persisted order already covers visible keys", () => {
|
||||||
const projects = [
|
const projects = [
|
||||||
sidebarProject({ projectKey: "project-a", workspaceKeys: ["ws-1"] }),
|
sidebarProject({ projectKey: "project-a", workspaceKeys: ["ws-1"] }),
|
||||||
|
|||||||
@@ -477,6 +477,23 @@ export function appendMissingOrderKeys(input: {
|
|||||||
return [...input.currentOrder, ...missingKeys];
|
return [...input.currentOrder, ...missingKeys];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function prependMissingOrderKeys(input: {
|
||||||
|
currentOrder: string[];
|
||||||
|
visibleKeys: string[];
|
||||||
|
}): string[] {
|
||||||
|
if (input.visibleKeys.length === 0) {
|
||||||
|
return input.currentOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingKeys = new Set(input.currentOrder);
|
||||||
|
const missingKeys = input.visibleKeys.filter((key) => !existingKeys.has(key));
|
||||||
|
if (missingKeys.length === 0) {
|
||||||
|
return input.currentOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...missingKeys, ...input.currentOrder];
|
||||||
|
}
|
||||||
|
|
||||||
export interface SidebarOrderUpdates {
|
export interface SidebarOrderUpdates {
|
||||||
projectOrder: string[] | null;
|
projectOrder: string[] | null;
|
||||||
workspaceOrders: Array<{ projectKey: string; order: string[] }>;
|
workspaceOrders: Array<{ projectKey: string; order: string[] }>;
|
||||||
@@ -500,7 +517,7 @@ export function computeSidebarOrderUpdates(input: {
|
|||||||
const workspaceOrders: Array<{ projectKey: string; order: string[] }> = [];
|
const workspaceOrders: Array<{ projectKey: string; order: string[] }> = [];
|
||||||
for (const project of input.projects) {
|
for (const project of input.projects) {
|
||||||
const persistedWorkspaceOrder = input.getWorkspaceOrder(project.projectKey);
|
const persistedWorkspaceOrder = input.getWorkspaceOrder(project.projectKey);
|
||||||
const nextWorkspaceOrder = appendMissingOrderKeys({
|
const nextWorkspaceOrder = prependMissingOrderKeys({
|
||||||
currentOrder: persistedWorkspaceOrder,
|
currentOrder: persistedWorkspaceOrder,
|
||||||
visibleKeys: project.workspaces.map((workspace) => workspace.workspaceKey),
|
visibleKeys: project.workspaces.map((workspace) => workspace.workspaceKey),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -926,6 +926,8 @@ export const ar: TranslationResources = {
|
|||||||
actions: {
|
actions: {
|
||||||
menu: "إجراءات Workspace",
|
menu: "إجراءات Workspace",
|
||||||
newWorkspace: "مساحة عمل جديدة",
|
newWorkspace: "مساحة عمل جديدة",
|
||||||
|
showMore: "عرض المزيد",
|
||||||
|
showLess: "عرض أقل",
|
||||||
createWorkspaceFor: "قم بإنشاء مساحة عمل جديدة لـ{{projectName}}",
|
createWorkspaceFor: "قم بإنشاء مساحة عمل جديدة لـ{{projectName}}",
|
||||||
copyPath: "نسخ المسار",
|
copyPath: "نسخ المسار",
|
||||||
copyBranchName: "انسخ اسم الفرع",
|
copyBranchName: "انسخ اسم الفرع",
|
||||||
|
|||||||
@@ -936,6 +936,8 @@ export const en = {
|
|||||||
actions: {
|
actions: {
|
||||||
menu: "Workspace actions",
|
menu: "Workspace actions",
|
||||||
newWorkspace: "New workspace",
|
newWorkspace: "New workspace",
|
||||||
|
showMore: "Show more",
|
||||||
|
showLess: "Show less",
|
||||||
createWorkspaceFor: "Create a new workspace for {{projectName}}",
|
createWorkspaceFor: "Create a new workspace for {{projectName}}",
|
||||||
copyPath: "Copy path",
|
copyPath: "Copy path",
|
||||||
copyBranchName: "Copy branch name",
|
copyBranchName: "Copy branch name",
|
||||||
|
|||||||
@@ -957,6 +957,8 @@ export const es: TranslationResources = {
|
|||||||
actions: {
|
actions: {
|
||||||
menu: "AccionesWorkspace",
|
menu: "AccionesWorkspace",
|
||||||
newWorkspace: "Nuevo espacio de trabajo",
|
newWorkspace: "Nuevo espacio de trabajo",
|
||||||
|
showMore: "Mostrar más",
|
||||||
|
showLess: "Mostrar menos",
|
||||||
createWorkspaceFor: "Crea un nuevo espacio de trabajo para{{projectName}}",
|
createWorkspaceFor: "Crea un nuevo espacio de trabajo para{{projectName}}",
|
||||||
copyPath: "Copiar ruta",
|
copyPath: "Copiar ruta",
|
||||||
copyBranchName: "Copiar nombre de sucursal",
|
copyBranchName: "Copiar nombre de sucursal",
|
||||||
|
|||||||
@@ -956,6 +956,8 @@ export const fr: TranslationResources = {
|
|||||||
actions: {
|
actions: {
|
||||||
menu: "ActionsWorkspace",
|
menu: "ActionsWorkspace",
|
||||||
newWorkspace: "Nouvel espace de travail",
|
newWorkspace: "Nouvel espace de travail",
|
||||||
|
showMore: "Afficher plus",
|
||||||
|
showLess: "Afficher moins",
|
||||||
createWorkspaceFor: "Créer un nouvel espace de travail pour{{projectName}}",
|
createWorkspaceFor: "Créer un nouvel espace de travail pour{{projectName}}",
|
||||||
copyPath: "Copier le chemin",
|
copyPath: "Copier le chemin",
|
||||||
copyBranchName: "Copier le nom de la branche",
|
copyBranchName: "Copier le nom de la branche",
|
||||||
|
|||||||
@@ -937,6 +937,8 @@ export const ja: TranslationResources = {
|
|||||||
actions: {
|
actions: {
|
||||||
menu: "ワークスペースアクション",
|
menu: "ワークスペースアクション",
|
||||||
newWorkspace: "新しいワークスペース",
|
newWorkspace: "新しいワークスペース",
|
||||||
|
showMore: "さらに表示",
|
||||||
|
showLess: "表示を減らす",
|
||||||
createWorkspaceFor: "{{projectName}}の新しいワークスペースを作成",
|
createWorkspaceFor: "{{projectName}}の新しいワークスペースを作成",
|
||||||
copyPath: "パスをコピー",
|
copyPath: "パスをコピー",
|
||||||
copyBranchName: "ブランチ名をコピー",
|
copyBranchName: "ブランチ名をコピー",
|
||||||
|
|||||||
@@ -948,6 +948,8 @@ export const ptBR: TranslationResources = {
|
|||||||
actions: {
|
actions: {
|
||||||
menu: "Ações do workspace",
|
menu: "Ações do workspace",
|
||||||
newWorkspace: "Novo workspace",
|
newWorkspace: "Novo workspace",
|
||||||
|
showMore: "Mostrar mais",
|
||||||
|
showLess: "Mostrar menos",
|
||||||
createWorkspaceFor: "Criar um novo workspace para {{projectName}}",
|
createWorkspaceFor: "Criar um novo workspace para {{projectName}}",
|
||||||
copyPath: "Copiar caminho",
|
copyPath: "Copiar caminho",
|
||||||
copyBranchName: "Copiar nome da branch",
|
copyBranchName: "Copiar nome da branch",
|
||||||
|
|||||||
@@ -948,6 +948,8 @@ export const ru: TranslationResources = {
|
|||||||
actions: {
|
actions: {
|
||||||
menu: "Действия Workspace",
|
menu: "Действия Workspace",
|
||||||
newWorkspace: "Новое рабочее пространство",
|
newWorkspace: "Новое рабочее пространство",
|
||||||
|
showMore: "Показать ещё",
|
||||||
|
showLess: "Показать меньше",
|
||||||
createWorkspaceFor: "Создайте новое рабочее пространство для{{projectName}}.",
|
createWorkspaceFor: "Создайте новое рабочее пространство для{{projectName}}.",
|
||||||
copyPath: "Копировать путь",
|
copyPath: "Копировать путь",
|
||||||
copyBranchName: "Скопировать название ветки",
|
copyBranchName: "Скопировать название ветки",
|
||||||
|
|||||||
@@ -916,6 +916,8 @@ export const zhCN: TranslationResources = {
|
|||||||
actions: {
|
actions: {
|
||||||
menu: "Workspace 操作",
|
menu: "Workspace 操作",
|
||||||
newWorkspace: "新建 workspace",
|
newWorkspace: "新建 workspace",
|
||||||
|
showMore: "显示更多",
|
||||||
|
showLess: "收起",
|
||||||
createWorkspaceFor: "为 {{projectName}} 新建 workspace",
|
createWorkspaceFor: "为 {{projectName}} 新建 workspace",
|
||||||
copyPath: "复制路径",
|
copyPath: "复制路径",
|
||||||
copyBranchName: "复制分支名称",
|
copyBranchName: "复制分支名称",
|
||||||
|
|||||||
Reference in New Issue
Block a user