Make sidebar sections collapsible in workspaces UI (Vibe Kanban) (#1867)
* Done! Here's a summary of the changes: ## Summary I made the sections in the right sidebar collapsible by: 1. **Created `CollapsibleSectionHeader` component** (`frontend/src/components/ui-new/primitives/CollapsibleSectionHeader.tsx`): - Combines the `SectionHeader` styling (left border accent, border-bottom) with collapsible functionality - Supports optional icon with action handler (stops propagation to avoid toggling) - Uses persisted expanded state via `usePersistedExpanded` hook - Shows caret icon that rotates when collapsed 2. **Added new persist keys** (`frontend/src/stores/useUiPreferencesStore.ts`): - `gitPanelRepositories` - for Repositories section - `gitPanelProject` - for Project section - `gitPanelAddRepositories` - for Add Repositories section - `processesSection` - for Processes section 3. **Updated `GitPanel.tsx`**: Replaced `SectionHeader` with `CollapsibleSectionHeader` for the Repositories section, wrapping all repo cards and the Advanced section inside 4. **Updated `GitPanelCreate.tsx`**: Made all three sections collapsible: - Project section - Repositories section - Add Repositories section 5. **Updated `ProcessListContainer.tsx`**: Made the Processes section collapsible while keeping the search bar always visible All sections now: - Collapse when clicking on the section header - Expand when clicking again - Persist their expanded/collapsed state across sessions * Done! I've made the Changes and Dev Server sections collapsible as well: **Updated files:** - `frontend/src/stores/useUiPreferencesStore.ts` - Added `changesSection` and `devServerSection` persist keys - `frontend/src/components/ui-new/views/FileTree.tsx` - Made the "Changes" section collapsible - `frontend/src/components/ui-new/views/PreviewControls.tsx` - Made the "Dev Server" section collapsible All sections now use the `CollapsibleSectionHeader` component and persist their collapsed/expanded state. * Cleanup script changes for workspace 05769879-a23f-49ac-81f4-b17e18f14025
This commit is contained in:
@@ -2,9 +2,10 @@ import { useEffect, useMemo, useCallback } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useExecutionProcessesContext } from '@/contexts/ExecutionProcessesContext';
|
import { useExecutionProcessesContext } from '@/contexts/ExecutionProcessesContext';
|
||||||
import { ProcessListItem } from '../primitives/ProcessListItem';
|
import { ProcessListItem } from '../primitives/ProcessListItem';
|
||||||
import { SectionHeader } from '../primitives/SectionHeader';
|
import { CollapsibleSectionHeader } from '../primitives/CollapsibleSectionHeader';
|
||||||
import { InputField } from '../primitives/InputField';
|
import { InputField } from '../primitives/InputField';
|
||||||
import { CaretUpIcon, CaretDownIcon } from '@phosphor-icons/react';
|
import { CaretUpIcon, CaretDownIcon } from '@phosphor-icons/react';
|
||||||
|
import { PERSIST_KEYS } from '@/stores/useUiPreferencesStore';
|
||||||
|
|
||||||
interface ProcessListContainerProps {
|
interface ProcessListContainerProps {
|
||||||
selectedProcessId: string | null;
|
selectedProcessId: string | null;
|
||||||
@@ -124,8 +125,11 @@ export function ProcessListContainer({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full bg-secondary flex flex-col overflow-hidden">
|
<div className="h-full w-full bg-secondary flex flex-col overflow-hidden">
|
||||||
<SectionHeader title={t('sections.processes')} />
|
<CollapsibleSectionHeader
|
||||||
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-panel scrollbar-track-transparent p-base min-h-0">
|
title={t('sections.processes')}
|
||||||
|
persistKey={PERSIST_KEYS.processesSection}
|
||||||
|
contentClassName="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-panel scrollbar-track-transparent p-base min-h-0"
|
||||||
|
>
|
||||||
{sortedProcesses.length === 0 ? (
|
{sortedProcesses.length === 0 ? (
|
||||||
<div className="h-full flex items-center justify-center text-low">
|
<div className="h-full flex items-center justify-center text-low">
|
||||||
<p className="text-sm">{t('processes.noProcesses')}</p>
|
<p className="text-sm">{t('processes.noProcesses')}</p>
|
||||||
@@ -144,7 +148,7 @@ export function ProcessListContainer({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</CollapsibleSectionHeader>
|
||||||
{searchBar}
|
{searchBar}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import type { Icon } from '@phosphor-icons/react';
|
||||||
|
import { CaretDownIcon } from '@phosphor-icons/react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import {
|
||||||
|
usePersistedExpanded,
|
||||||
|
type PersistKey,
|
||||||
|
} from '@/stores/useUiPreferencesStore';
|
||||||
|
|
||||||
|
interface CollapsibleSectionHeaderProps {
|
||||||
|
persistKey: PersistKey;
|
||||||
|
title: string;
|
||||||
|
defaultExpanded?: boolean;
|
||||||
|
icon?: Icon;
|
||||||
|
onIconClick?: () => void;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
contentClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CollapsibleSectionHeader({
|
||||||
|
persistKey,
|
||||||
|
title,
|
||||||
|
defaultExpanded = true,
|
||||||
|
icon: IconComponent,
|
||||||
|
onIconClick,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
contentClassName,
|
||||||
|
}: CollapsibleSectionHeaderProps) {
|
||||||
|
const [expanded, toggle] = usePersistedExpanded(persistKey, defaultExpanded);
|
||||||
|
|
||||||
|
const handleIconClick = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onIconClick?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('flex flex-col', className)}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggle()}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center justify-between w-full border-b px-base py-half bg-secondary border-l-half border-l-low cursor-pointer'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="font-medium truncate text-normal">{title}</span>
|
||||||
|
<div className="flex items-center gap-half">
|
||||||
|
{IconComponent && onIconClick && (
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={handleIconClick}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleIconClick(e as unknown as React.MouseEvent);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="text-low hover:text-normal"
|
||||||
|
>
|
||||||
|
<IconComponent className="size-icon-xs" weight="bold" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<CaretDownIcon
|
||||||
|
weight="fill"
|
||||||
|
className={cn(
|
||||||
|
'size-icon-xs text-low transition-transform',
|
||||||
|
!expanded && '-rotate-90'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{expanded && <div className={contentClassName}>{children}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,7 +3,8 @@ import { cn } from '@/lib/utils';
|
|||||||
import { FileTreeSearchBar } from './FileTreeSearchBar';
|
import { FileTreeSearchBar } from './FileTreeSearchBar';
|
||||||
import { FileTreeNode } from './FileTreeNode';
|
import { FileTreeNode } from './FileTreeNode';
|
||||||
import type { TreeNode } from '../types/fileTree';
|
import type { TreeNode } from '../types/fileTree';
|
||||||
import { SectionHeader } from '../primitives/SectionHeader';
|
import { CollapsibleSectionHeader } from '../primitives/CollapsibleSectionHeader';
|
||||||
|
import { PERSIST_KEYS } from '@/stores/useUiPreferencesStore';
|
||||||
|
|
||||||
interface FileTreeProps {
|
interface FileTreeProps {
|
||||||
nodes: TreeNode[];
|
nodes: TreeNode[];
|
||||||
@@ -66,7 +67,11 @@ export function FileTree({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('w-full h-full bg-secondary flex flex-col', className)}>
|
<div className={cn('w-full h-full bg-secondary flex flex-col', className)}>
|
||||||
<SectionHeader title="Changes" />
|
<CollapsibleSectionHeader
|
||||||
|
title="Changes"
|
||||||
|
persistKey={PERSIST_KEYS.changesSection}
|
||||||
|
contentClassName="flex flex-col flex-1 min-h-0"
|
||||||
|
>
|
||||||
<div className="px-base pt-base">
|
<div className="px-base pt-base">
|
||||||
<FileTreeSearchBar
|
<FileTreeSearchBar
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
@@ -80,10 +85,13 @@ export function FileTree({
|
|||||||
renderNodes(nodes)
|
renderNodes(nodes)
|
||||||
) : (
|
) : (
|
||||||
<div className="p-base text-low text-sm">
|
<div className="p-base text-low text-sm">
|
||||||
{searchQuery ? t('common:fileTree.noResults') : 'No changed files'}
|
{searchQuery
|
||||||
|
? t('common:fileTree.noResults')
|
||||||
|
: 'No changed files'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</CollapsibleSectionHeader>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import {
|
|||||||
type RepoAction,
|
type RepoAction,
|
||||||
} from '@/components/ui-new/primitives/RepoCard';
|
} from '@/components/ui-new/primitives/RepoCard';
|
||||||
import { InputField } from '@/components/ui-new/primitives/InputField';
|
import { InputField } from '@/components/ui-new/primitives/InputField';
|
||||||
import { SectionHeader } from '@/components/ui-new/primitives/SectionHeader';
|
|
||||||
import { ErrorAlert } from '@/components/ui-new/primitives/ErrorAlert';
|
import { ErrorAlert } from '@/components/ui-new/primitives/ErrorAlert';
|
||||||
import { CollapsibleSection } from '../primitives/CollapsibleSection';
|
import { CollapsibleSection } from '../primitives/CollapsibleSection';
|
||||||
|
import { CollapsibleSectionHeader } from '../primitives/CollapsibleSectionHeader';
|
||||||
import { PERSIST_KEYS } from '@/stores/useUiPreferencesStore';
|
import { PERSIST_KEYS } from '@/stores/useUiPreferencesStore';
|
||||||
|
|
||||||
export interface RepoInfo {
|
export interface RepoInfo {
|
||||||
@@ -56,8 +56,11 @@ export function GitPanel({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{error && <ErrorAlert message={error} />}
|
{error && <ErrorAlert message={error} />}
|
||||||
<SectionHeader title={t('common:sections.repositories')} />
|
<CollapsibleSectionHeader
|
||||||
<div className="flex flex-col p-base gap-base">
|
title={t('common:sections.repositories')}
|
||||||
|
persistKey={PERSIST_KEYS.gitPanelRepositories}
|
||||||
|
contentClassName="flex flex-col p-base gap-base"
|
||||||
|
>
|
||||||
<div className="flex flex-col gap-base">
|
<div className="flex flex-col gap-base">
|
||||||
{repos.map((repo) => (
|
{repos.map((repo) => (
|
||||||
<RepoCard
|
<RepoCard
|
||||||
@@ -101,7 +104,7 @@ export function GitPanel({
|
|||||||
/>
|
/>
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CollapsibleSectionHeader>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { SectionHeader } from '@/components/ui-new/primitives/SectionHeader';
|
import { CollapsibleSectionHeader } from '@/components/ui-new/primitives/CollapsibleSectionHeader';
|
||||||
import { SelectedReposList } from '@/components/ui-new/primitives/SelectedReposList';
|
import { SelectedReposList } from '@/components/ui-new/primitives/SelectedReposList';
|
||||||
import { ProjectSelectorContainer } from '@/components/ui-new/containers/ProjectSelectorContainer';
|
import { ProjectSelectorContainer } from '@/components/ui-new/containers/ProjectSelectorContainer';
|
||||||
import { RecentReposListContainer } from '@/components/ui-new/containers/RecentReposListContainer';
|
import { RecentReposListContainer } from '@/components/ui-new/containers/RecentReposListContainer';
|
||||||
import { BrowseRepoButtonContainer } from '@/components/ui-new/containers/BrowseRepoButtonContainer';
|
import { BrowseRepoButtonContainer } from '@/components/ui-new/containers/BrowseRepoButtonContainer';
|
||||||
import { CreateRepoButtonContainer } from '@/components/ui-new/containers/CreateRepoButtonContainer';
|
import { CreateRepoButtonContainer } from '@/components/ui-new/containers/CreateRepoButtonContainer';
|
||||||
import { WarningIcon } from '@phosphor-icons/react';
|
import { WarningIcon } from '@phosphor-icons/react';
|
||||||
|
import { PERSIST_KEYS } from '@/stores/useUiPreferencesStore';
|
||||||
import type { Project, GitBranch, Repo } from 'shared/types';
|
import type { Project, GitBranch, Repo } from 'shared/types';
|
||||||
|
|
||||||
interface GitPanelCreateProps {
|
interface GitPanelCreateProps {
|
||||||
@@ -50,8 +51,11 @@ export function GitPanelCreate({
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SectionHeader title={t('common:sections.project')} />
|
<CollapsibleSectionHeader
|
||||||
<div className="p-base border-b">
|
title={t('common:sections.project')}
|
||||||
|
persistKey={PERSIST_KEYS.gitPanelProject}
|
||||||
|
contentClassName="p-base border-b"
|
||||||
|
>
|
||||||
<ProjectSelectorContainer
|
<ProjectSelectorContainer
|
||||||
projects={projects}
|
projects={projects}
|
||||||
selectedProjectId={selectedProjectId}
|
selectedProjectId={selectedProjectId}
|
||||||
@@ -59,10 +63,13 @@ export function GitPanelCreate({
|
|||||||
onProjectSelect={onProjectSelect}
|
onProjectSelect={onProjectSelect}
|
||||||
onCreateProject={onCreateProject}
|
onCreateProject={onCreateProject}
|
||||||
/>
|
/>
|
||||||
</div>
|
</CollapsibleSectionHeader>
|
||||||
|
|
||||||
<SectionHeader title={t('common:sections.repositories')} />
|
<CollapsibleSectionHeader
|
||||||
<div className="p-base border-b">
|
title={t('common:sections.repositories')}
|
||||||
|
persistKey={PERSIST_KEYS.gitPanelRepositories}
|
||||||
|
contentClassName="p-base border-b"
|
||||||
|
>
|
||||||
{hasNoRepos ? (
|
{hasNoRepos ? (
|
||||||
<div className="flex items-center gap-2 p-base rounded bg-warning/10 border border-warning/20">
|
<div className="flex items-center gap-2 p-base rounded bg-warning/10 border border-warning/20">
|
||||||
<WarningIcon className="h-4 w-4 text-warning shrink-0" />
|
<WarningIcon className="h-4 w-4 text-warning shrink-0" />
|
||||||
@@ -79,9 +86,12 @@ export function GitPanelCreate({
|
|||||||
onBranchChange={onBranchChange}
|
onBranchChange={onBranchChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</CollapsibleSectionHeader>
|
||||||
<SectionHeader title={t('common:sections.addRepositories')} />
|
<CollapsibleSectionHeader
|
||||||
<div className="flex flex-col p-base gap-half">
|
title={t('common:sections.addRepositories')}
|
||||||
|
persistKey={PERSIST_KEYS.gitPanelAddRepositories}
|
||||||
|
contentClassName="flex flex-col p-base gap-half"
|
||||||
|
>
|
||||||
<p className="text-xs text-low font-medium">
|
<p className="text-xs text-low font-medium">
|
||||||
{t('common:sections.recent')}
|
{t('common:sections.recent')}
|
||||||
</p>
|
</p>
|
||||||
@@ -94,7 +104,7 @@ export function GitPanelCreate({
|
|||||||
</p>
|
</p>
|
||||||
<BrowseRepoButtonContainer onRepoRegistered={onRepoRegistered} />
|
<BrowseRepoButtonContainer onRepoRegistered={onRepoRegistered} />
|
||||||
<CreateRepoButtonContainer onRepoCreated={onRepoRegistered} />
|
<CreateRepoButtonContainer onRepoCreated={onRepoRegistered} />
|
||||||
</div>
|
</CollapsibleSectionHeader>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import {
|
|||||||
} from '@phosphor-icons/react';
|
} from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { SectionHeader } from '../primitives/SectionHeader';
|
import { CollapsibleSectionHeader } from '../primitives/CollapsibleSectionHeader';
|
||||||
import { PrimaryButton } from '../primitives/PrimaryButton';
|
import { PrimaryButton } from '../primitives/PrimaryButton';
|
||||||
import {
|
import {
|
||||||
VirtualizedProcessLogs,
|
VirtualizedProcessLogs,
|
||||||
type LogEntry,
|
type LogEntry,
|
||||||
} from '../VirtualizedProcessLogs';
|
} from '../VirtualizedProcessLogs';
|
||||||
|
import { PERSIST_KEYS } from '@/stores/useUiPreferencesStore';
|
||||||
|
|
||||||
interface PreviewControlsProps {
|
interface PreviewControlsProps {
|
||||||
logs: LogEntry[];
|
logs: LogEntry[];
|
||||||
@@ -56,9 +57,11 @@ export function PreviewControls({
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SectionHeader title="Dev Server" />
|
<CollapsibleSectionHeader
|
||||||
|
title="Dev Server"
|
||||||
<div className="flex flex-col flex-1 overflow-hidden">
|
persistKey={PERSIST_KEYS.devServerSection}
|
||||||
|
contentClassName="flex flex-col flex-1 overflow-hidden"
|
||||||
|
>
|
||||||
{/* Controls row: URL bar + Start/Stop button */}
|
{/* Controls row: URL bar + Start/Stop button */}
|
||||||
<div className="flex items-center gap-half p-base">
|
<div className="flex items-center gap-half p-base">
|
||||||
{url && (
|
{url && (
|
||||||
@@ -139,7 +142,7 @@ export function PreviewControls({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CollapsibleSectionHeader>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,18 @@ export const PERSIST_KEYS = {
|
|||||||
// Sidebar sections
|
// Sidebar sections
|
||||||
workspacesSidebarActive: 'workspaces-sidebar-active',
|
workspacesSidebarActive: 'workspaces-sidebar-active',
|
||||||
workspacesSidebarArchived: 'workspaces-sidebar-archived',
|
workspacesSidebarArchived: 'workspaces-sidebar-archived',
|
||||||
// Git panel
|
// Git panel sections
|
||||||
gitAdvancedSettings: 'git-advanced-settings',
|
gitAdvancedSettings: 'git-advanced-settings',
|
||||||
gitPanelCreateAddRepo: 'git-panel-create-add-repo',
|
gitPanelCreateAddRepo: 'git-panel-create-add-repo',
|
||||||
|
gitPanelRepositories: 'git-panel-repositories',
|
||||||
|
gitPanelProject: 'git-panel-project',
|
||||||
|
gitPanelAddRepositories: 'git-panel-add-repositories',
|
||||||
|
// Process panel sections
|
||||||
|
processesSection: 'processes-section',
|
||||||
|
// Changes panel sections
|
||||||
|
changesSection: 'changes-section',
|
||||||
|
// Preview panel sections
|
||||||
|
devServerSection: 'dev-server-section',
|
||||||
// Context bar
|
// Context bar
|
||||||
contextBarPosition: 'context-bar-position',
|
contextBarPosition: 'context-bar-position',
|
||||||
// Pane sizes
|
// Pane sizes
|
||||||
@@ -35,6 +44,12 @@ export type PersistKey =
|
|||||||
| typeof PERSIST_KEYS.workspacesSidebarArchived
|
| typeof PERSIST_KEYS.workspacesSidebarArchived
|
||||||
| typeof PERSIST_KEYS.gitAdvancedSettings
|
| typeof PERSIST_KEYS.gitAdvancedSettings
|
||||||
| typeof PERSIST_KEYS.gitPanelCreateAddRepo
|
| typeof PERSIST_KEYS.gitPanelCreateAddRepo
|
||||||
|
| typeof PERSIST_KEYS.gitPanelRepositories
|
||||||
|
| typeof PERSIST_KEYS.gitPanelProject
|
||||||
|
| typeof PERSIST_KEYS.gitPanelAddRepositories
|
||||||
|
| typeof PERSIST_KEYS.processesSection
|
||||||
|
| typeof PERSIST_KEYS.changesSection
|
||||||
|
| typeof PERSIST_KEYS.devServerSection
|
||||||
| typeof PERSIST_KEYS.sidebarWidth
|
| typeof PERSIST_KEYS.sidebarWidth
|
||||||
| typeof PERSIST_KEYS.gitPanelWidth
|
| typeof PERSIST_KEYS.gitPanelWidth
|
||||||
| typeof PERSIST_KEYS.changesPanelWidth
|
| typeof PERSIST_KEYS.changesPanelWidth
|
||||||
|
|||||||
Reference in New Issue
Block a user