sidebar ui changes, loading state fixes (#2039)
* tons of sidebar ui cleanup, loading states etc * lint fix * fix archive store * i18n --------- Co-authored-by: Louis Knight-Webb <louis@bloop.ai>
This commit is contained in:
@@ -7,9 +7,8 @@ import {
|
|||||||
VirtuosoMessageListProps,
|
VirtuosoMessageListProps,
|
||||||
} from '@virtuoso.dev/message-list';
|
} from '@virtuoso.dev/message-list';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { SpinnerGapIcon } from '@phosphor-icons/react';
|
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import NewDisplayConversationEntry from './NewDisplayConversationEntry';
|
import NewDisplayConversationEntry from './NewDisplayConversationEntry';
|
||||||
import { ApprovalFormProvider } from '@/contexts/ApprovalFormContext';
|
import { ApprovalFormProvider } from '@/contexts/ApprovalFormContext';
|
||||||
import { useEntries } from '@/contexts/EntriesContext';
|
import { useEntries } from '@/contexts/EntriesContext';
|
||||||
@@ -86,7 +85,6 @@ const computeItemKey: VirtuosoMessageListProps<
|
|||||||
>['computeItemKey'] = ({ data }) => `conv-${data.patchKey}`;
|
>['computeItemKey'] = ({ data }) => `conv-${data.patchKey}`;
|
||||||
|
|
||||||
export function ConversationList({ attempt, task }: ConversationListProps) {
|
export function ConversationList({ attempt, task }: ConversationListProps) {
|
||||||
const { t } = useTranslation('common');
|
|
||||||
const [channelData, setChannelData] =
|
const [channelData, setChannelData] =
|
||||||
useState<DataWithScrollModifier<PatchTypeWithKey> | null>(null);
|
useState<DataWithScrollModifier<PatchTypeWithKey> | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -156,8 +154,17 @@ export function ConversationList({ attempt, task }: ConversationListProps) {
|
|||||||
[attempt, task]
|
[attempt, task]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Determine if content is ready to show (has data or finished loading)
|
||||||
|
const hasContent = !loading || (channelData?.data?.length ?? 0) > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ApprovalFormProvider>
|
<ApprovalFormProvider>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'h-full transition-opacity duration-300',
|
||||||
|
hasContent ? 'opacity-100' : 'opacity-0'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<VirtuosoMessageListLicense
|
<VirtuosoMessageListLicense
|
||||||
licenseKey={import.meta.env.VITE_PUBLIC_REACT_VIRTUOSO_LICENSE_KEY}
|
licenseKey={import.meta.env.VITE_PUBLIC_REACT_VIRTUOSO_LICENSE_KEY}
|
||||||
>
|
>
|
||||||
@@ -173,12 +180,7 @@ export function ConversationList({ attempt, task }: ConversationListProps) {
|
|||||||
Footer={() => <div className="h-2" />}
|
Footer={() => <div className="h-2" />}
|
||||||
/>
|
/>
|
||||||
</VirtuosoMessageListLicense>
|
</VirtuosoMessageListLicense>
|
||||||
{loading && !channelData?.data?.length && (
|
|
||||||
<div className="absolute inset-0 bg-primary flex flex-col gap-2 justify-center items-center">
|
|
||||||
<SpinnerGapIcon className="h-8 w-8 animate-spin" />
|
|
||||||
<p>{t('states.loadingHistory')}</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</ApprovalFormProvider>
|
</ApprovalFormProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -944,11 +944,7 @@ export type NavbarItem = ActionDefinition | typeof NavbarDivider;
|
|||||||
|
|
||||||
// Navbar action groups define which actions appear in each section
|
// Navbar action groups define which actions appear in each section
|
||||||
export const NavbarActionGroups = {
|
export const NavbarActionGroups = {
|
||||||
left: [
|
left: [Actions.OpenInOldUI] as ActionDefinition[],
|
||||||
Actions.OpenInOldUI,
|
|
||||||
NavbarDivider,
|
|
||||||
Actions.ArchiveWorkspace,
|
|
||||||
] as ActionDefinition[],
|
|
||||||
right: [
|
right: [
|
||||||
Actions.ToggleDiffViewMode,
|
Actions.ToggleDiffViewMode,
|
||||||
Actions.ToggleAllDiffs,
|
Actions.ToggleAllDiffs,
|
||||||
|
|||||||
@@ -545,9 +545,43 @@ export function SessionChatBoxContainer({
|
|||||||
localMessage,
|
localMessage,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Don't render if no session and not in new session mode
|
// Render placeholder state if no session and not in new session mode
|
||||||
if (!session && !isNewSessionMode) {
|
// This maintains the visual structure during workspace transitions
|
||||||
return null;
|
const isPlaceholderMode = !session && !isNewSessionMode;
|
||||||
|
|
||||||
|
// In placeholder mode, render a disabled version to maintain visual structure
|
||||||
|
if (isPlaceholderMode) {
|
||||||
|
return (
|
||||||
|
<SessionChatBox
|
||||||
|
status="idle"
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
projectId={projectId}
|
||||||
|
editor={{
|
||||||
|
value: '',
|
||||||
|
onChange: () => {},
|
||||||
|
}}
|
||||||
|
actions={{
|
||||||
|
onSend: () => {},
|
||||||
|
onQueue: () => {},
|
||||||
|
onCancelQueue: () => {},
|
||||||
|
onStop: () => {},
|
||||||
|
onPasteFiles: () => {},
|
||||||
|
}}
|
||||||
|
session={{
|
||||||
|
sessions: [],
|
||||||
|
selectedSessionId: undefined,
|
||||||
|
onSelectSession: () => {},
|
||||||
|
isNewSessionMode: false,
|
||||||
|
onNewSession: undefined,
|
||||||
|
}}
|
||||||
|
stats={{
|
||||||
|
filesChanged: 0,
|
||||||
|
linesAdded: 0,
|
||||||
|
linesRemoved: 0,
|
||||||
|
onViewCode: undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
PERSIST_KEYS,
|
PERSIST_KEYS,
|
||||||
useExpandedAll,
|
useExpandedAll,
|
||||||
usePaneSize,
|
usePaneSize,
|
||||||
|
usePersistedExpanded,
|
||||||
} from '@/stores/useUiPreferencesStore';
|
} from '@/stores/useUiPreferencesStore';
|
||||||
import {
|
import {
|
||||||
useLayoutStore,
|
useLayoutStore,
|
||||||
@@ -304,6 +305,10 @@ export function WorkspacesLayout() {
|
|||||||
50
|
50
|
||||||
);
|
);
|
||||||
const isRightMainPanelVisible = useIsRightMainPanelVisible();
|
const isRightMainPanelVisible = useIsRightMainPanelVisible();
|
||||||
|
const [showArchive, setShowArchive] = usePersistedExpanded(
|
||||||
|
PERSIST_KEYS.workspacesSidebarArchived,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
const defaultLayout = (): Layout => {
|
const defaultLayout = (): Layout => {
|
||||||
let layout = { 'left-main': 50, 'right-main': 50 };
|
let layout = { 'left-main': 50, 'right-main': 50 };
|
||||||
@@ -710,6 +715,23 @@ export function WorkspacesLayout() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Action handlers for sidebar workspace actions
|
||||||
|
const { executeAction } = useActions();
|
||||||
|
|
||||||
|
const handleArchiveWorkspace = useCallback(
|
||||||
|
(workspaceId: string) => {
|
||||||
|
executeAction(Actions.ArchiveWorkspace, workspaceId);
|
||||||
|
},
|
||||||
|
[executeAction]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePinWorkspace = useCallback(
|
||||||
|
(workspaceId: string) => {
|
||||||
|
executeAction(Actions.PinWorkspace, workspaceId);
|
||||||
|
},
|
||||||
|
[executeAction]
|
||||||
|
);
|
||||||
|
|
||||||
// Render sidebar with persisted draft title
|
// Render sidebar with persisted draft title
|
||||||
const renderSidebar = () => (
|
const renderSidebar = () => (
|
||||||
<WorkspacesSidebar
|
<WorkspacesSidebar
|
||||||
@@ -720,9 +742,13 @@ export function WorkspacesLayout() {
|
|||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
onSearchChange={setSearchQuery}
|
onSearchChange={setSearchQuery}
|
||||||
onAddWorkspace={navigateToCreate}
|
onAddWorkspace={navigateToCreate}
|
||||||
|
onArchiveWorkspace={handleArchiveWorkspace}
|
||||||
|
onPinWorkspace={handlePinWorkspace}
|
||||||
isCreateMode={isCreateMode}
|
isCreateMode={isCreateMode}
|
||||||
draftTitle={persistedDraftTitle}
|
draftTitle={persistedDraftTitle}
|
||||||
onSelectCreate={navigateToCreate}
|
onSelectCreate={navigateToCreate}
|
||||||
|
showArchive={showArchive}
|
||||||
|
onShowArchiveChange={setShowArchive}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
PushPinIcon,
|
PushPinIcon,
|
||||||
DotsThreeIcon,
|
|
||||||
HandIcon,
|
HandIcon,
|
||||||
TriangleIcon,
|
TriangleIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
FileIcon,
|
FileIcon,
|
||||||
CircleIcon,
|
CircleIcon,
|
||||||
GitPullRequestIcon,
|
GitPullRequestIcon,
|
||||||
|
ArchiveIcon,
|
||||||
|
ListIcon,
|
||||||
} 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';
|
||||||
@@ -30,6 +31,8 @@ interface WorkspaceSummaryProps {
|
|||||||
latestProcessStatus?: 'running' | 'completed' | 'failed' | 'killed';
|
latestProcessStatus?: 'running' | 'completed' | 'failed' | 'killed';
|
||||||
prStatus?: 'open' | 'merged' | 'closed' | 'unknown';
|
prStatus?: 'open' | 'merged' | 'closed' | 'unknown';
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
onArchive?: () => void;
|
||||||
|
onPin?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
summary?: boolean;
|
summary?: boolean;
|
||||||
/** Whether this is a draft workspace (shows "Draft" instead of elapsed time) */
|
/** Whether this is a draft workspace (shows "Draft" instead of elapsed time) */
|
||||||
@@ -52,6 +55,8 @@ export function WorkspaceSummary({
|
|||||||
latestProcessStatus,
|
latestProcessStatus,
|
||||||
prStatus,
|
prStatus,
|
||||||
onClick,
|
onClick,
|
||||||
|
onArchive,
|
||||||
|
onPin,
|
||||||
className,
|
className,
|
||||||
summary = false,
|
summary = false,
|
||||||
isDraft = false,
|
isDraft = false,
|
||||||
@@ -69,21 +74,41 @@ export function WorkspaceSummary({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleArchive = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onArchive?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePin = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onPin?.();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('group relative', className)}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
'group relative rounded-sm transition-all duration-100 overflow-hidden',
|
||||||
|
isActive ? 'bg-tertiary' : '',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Selection indicator - thin colored tab on the left */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'absolute left-0 top-1 bottom-1 w-0.5 rounded-full transition-colors duration-100',
|
||||||
|
isActive ? 'bg-brand' : 'bg-transparent'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex w-full cursor-pointer flex-col border-l-4 text-left text-low',
|
'flex w-full cursor-pointer flex-col text-left px-base py-half transition-all duration-150',
|
||||||
isActive ? 'border-normal pl-base' : 'border-none'
|
isActive
|
||||||
)}
|
? 'text-normal'
|
||||||
>
|
: 'text-low opacity-60 hover:opacity-100 hover:text-normal'
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'truncate group-hover:text-high pr-double',
|
|
||||||
!summary && 'text-normal'
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<div className={cn('truncate pr-double', !summary && 'text-normal')}>
|
||||||
{name}
|
{name}
|
||||||
</div>
|
</div>
|
||||||
{(!summary || isActive) && (
|
{(!summary || isActive) && (
|
||||||
@@ -179,16 +204,55 @@ export function WorkspaceSummary({
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Right-side hover zone for action overlay */}
|
||||||
{workspaceId && (
|
{workspaceId && (
|
||||||
<div className="absolute right-0 top-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div className="absolute right-0 top-0 bottom-0 w-16 group/actions">
|
||||||
|
{/* Sliding action overlay - only appears when hovering this zone */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'absolute right-0 top-0 bottom-0 flex items-center',
|
||||||
|
'translate-x-full group-hover/actions:translate-x-0',
|
||||||
|
'transition-transform duration-150 ease-out'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Gradient fade from transparent to pill background */}
|
||||||
|
<div className="h-full w-6 pointer-events-none bg-gradient-to-r from-transparent to-secondary" />
|
||||||
|
{/* Action pill */}
|
||||||
|
<div className="flex items-center gap-0.5 pr-base h-full bg-secondary">
|
||||||
|
<button
|
||||||
|
onClick={handlePin}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
className={cn(
|
||||||
|
'p-1.5 rounded-sm transition-colors duration-100',
|
||||||
|
'hover:bg-tertiary',
|
||||||
|
isPinned ? 'text-brand' : 'text-low hover:text-normal'
|
||||||
|
)}
|
||||||
|
title={isPinned ? t('workspaces.unpin') : t('workspaces.pin')}
|
||||||
|
>
|
||||||
|
<PushPinIcon
|
||||||
|
className="size-icon-xs"
|
||||||
|
weight={isPinned ? 'fill' : 'regular'}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleArchive}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
className="p-1.5 rounded-sm text-low hover:text-normal hover:bg-tertiary transition-colors duration-100"
|
||||||
|
title={t('workspaces.archive')}
|
||||||
|
>
|
||||||
|
<ArchiveIcon className="size-icon-xs" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleOpenCommandBar}
|
onClick={handleOpenCommandBar}
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
className="p-half rounded-sm hover:bg-tertiary text-low hover:text-high focus:outline-none"
|
className="p-1.5 rounded-sm text-low hover:text-normal hover:bg-tertiary transition-colors duration-100"
|
||||||
|
title={t('workspaces.more')}
|
||||||
>
|
>
|
||||||
<DotsThreeIcon className="size-icon-sm" weight="bold" />
|
<ListIcon className="size-icon-xs" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { PlusIcon } from '@phosphor-icons/react';
|
import { PlusIcon, ArrowLeftIcon, ArchiveIcon } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { Workspace } from '@/components/ui-new/hooks/useWorkspaces';
|
import type { Workspace } from '@/components/ui-new/hooks/useWorkspaces';
|
||||||
import { CollapsibleSection } from '@/components/ui-new/primitives/CollapsibleSection';
|
|
||||||
import { InputField } from '@/components/ui-new/primitives/InputField';
|
import { InputField } from '@/components/ui-new/primitives/InputField';
|
||||||
import { WorkspaceSummary } from '@/components/ui-new/primitives/WorkspaceSummary';
|
import { WorkspaceSummary } from '@/components/ui-new/primitives/WorkspaceSummary';
|
||||||
import { SectionHeader } from '../primitives/SectionHeader';
|
import { SectionHeader } from '../primitives/SectionHeader';
|
||||||
import { PERSIST_KEYS } from '@/stores/useUiPreferencesStore';
|
|
||||||
|
|
||||||
interface WorkspacesSidebarProps {
|
interface WorkspacesSidebarProps {
|
||||||
workspaces: Workspace[];
|
workspaces: Workspace[];
|
||||||
@@ -13,6 +11,8 @@ interface WorkspacesSidebarProps {
|
|||||||
selectedWorkspaceId: string | null;
|
selectedWorkspaceId: string | null;
|
||||||
onSelectWorkspace: (id: string) => void;
|
onSelectWorkspace: (id: string) => void;
|
||||||
onAddWorkspace?: () => void;
|
onAddWorkspace?: () => void;
|
||||||
|
onArchiveWorkspace?: (id: string) => void;
|
||||||
|
onPinWorkspace?: (id: string) => void;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
onSearchChange: (value: string) => void;
|
onSearchChange: (value: string) => void;
|
||||||
/** Whether we're in create mode */
|
/** Whether we're in create mode */
|
||||||
@@ -21,6 +21,10 @@ interface WorkspacesSidebarProps {
|
|||||||
draftTitle?: string;
|
draftTitle?: string;
|
||||||
/** Handler to navigate back to create mode */
|
/** Handler to navigate back to create mode */
|
||||||
onSelectCreate?: () => void;
|
onSelectCreate?: () => void;
|
||||||
|
/** Whether to show archived workspaces */
|
||||||
|
showArchive?: boolean;
|
||||||
|
/** Handler for toggling archive view */
|
||||||
|
onShowArchiveChange?: (show: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspacesSidebar({
|
export function WorkspacesSidebar({
|
||||||
@@ -29,11 +33,15 @@ export function WorkspacesSidebar({
|
|||||||
selectedWorkspaceId,
|
selectedWorkspaceId,
|
||||||
onSelectWorkspace,
|
onSelectWorkspace,
|
||||||
onAddWorkspace,
|
onAddWorkspace,
|
||||||
|
onArchiveWorkspace,
|
||||||
|
onPinWorkspace,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
isCreateMode = false,
|
isCreateMode = false,
|
||||||
draftTitle,
|
draftTitle,
|
||||||
onSelectCreate,
|
onSelectCreate,
|
||||||
|
showArchive = false,
|
||||||
|
onShowArchiveChange,
|
||||||
}: WorkspacesSidebarProps) {
|
}: WorkspacesSidebarProps) {
|
||||||
const { t } = useTranslation(['tasks', 'common']);
|
const { t } = useTranslation(['tasks', 'common']);
|
||||||
const searchLower = searchQuery.toLowerCase();
|
const searchLower = searchQuery.toLowerCase();
|
||||||
@@ -48,8 +56,11 @@ export function WorkspacesSidebar({
|
|||||||
.filter((workspace) => workspace.name.toLowerCase().includes(searchLower))
|
.filter((workspace) => workspace.name.toLowerCase().includes(searchLower))
|
||||||
.slice(0, isSearching ? undefined : DISPLAY_LIMIT);
|
.slice(0, isSearching ? undefined : DISPLAY_LIMIT);
|
||||||
|
|
||||||
|
const hasArchivedWorkspaces = archivedWorkspaces.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full bg-secondary flex flex-col">
|
<div className="w-full h-full bg-secondary flex flex-col">
|
||||||
|
{/* Header + Search */}
|
||||||
<div className="flex flex-col gap-base">
|
<div className="flex flex-col gap-base">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
title={t('common:workspaces.title')}
|
title={t('common:workspaces.title')}
|
||||||
@@ -65,14 +76,51 @@ export function WorkspacesSidebar({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col flex-1 overflow-y-auto">
|
|
||||||
<CollapsibleSection
|
{/* Scrollable workspace list */}
|
||||||
persistKey={PERSIST_KEYS.workspacesSidebarActive}
|
<div className="flex-1 overflow-y-auto p-base">
|
||||||
title={t('common:workspaces.active')}
|
{showArchive ? (
|
||||||
defaultExpanded
|
/* Archived workspaces view */
|
||||||
className="p-base"
|
<div className="flex flex-col gap-base">
|
||||||
contentClassName="flex flex-col gap-base min-h-[50vh]"
|
<span className="text-sm font-medium text-low">
|
||||||
>
|
{t('common:workspaces.archived')}
|
||||||
|
</span>
|
||||||
|
{filteredArchivedWorkspaces.length === 0 ? (
|
||||||
|
<span className="text-sm text-low opacity-60">
|
||||||
|
{t('common:workspaces.noArchived')}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
filteredArchivedWorkspaces.map((workspace) => (
|
||||||
|
<WorkspaceSummary
|
||||||
|
summary
|
||||||
|
key={workspace.id}
|
||||||
|
name={workspace.name}
|
||||||
|
workspaceId={workspace.id}
|
||||||
|
filesChanged={workspace.filesChanged}
|
||||||
|
linesAdded={workspace.linesAdded}
|
||||||
|
linesRemoved={workspace.linesRemoved}
|
||||||
|
isActive={selectedWorkspaceId === workspace.id}
|
||||||
|
isRunning={workspace.isRunning}
|
||||||
|
isPinned={workspace.isPinned}
|
||||||
|
hasPendingApproval={workspace.hasPendingApproval}
|
||||||
|
hasRunningDevServer={workspace.hasRunningDevServer}
|
||||||
|
hasUnseenActivity={workspace.hasUnseenActivity}
|
||||||
|
latestProcessCompletedAt={workspace.latestProcessCompletedAt}
|
||||||
|
latestProcessStatus={workspace.latestProcessStatus}
|
||||||
|
prStatus={workspace.prStatus}
|
||||||
|
onClick={() => onSelectWorkspace(workspace.id)}
|
||||||
|
onArchive={() => onArchiveWorkspace?.(workspace.id)}
|
||||||
|
onPin={() => onPinWorkspace?.(workspace.id)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* Active workspaces view */
|
||||||
|
<div className="flex flex-col gap-base">
|
||||||
|
<span className="text-sm font-medium text-low">
|
||||||
|
{t('common:workspaces.active')}
|
||||||
|
</span>
|
||||||
{draftTitle && (
|
{draftTitle && (
|
||||||
<WorkspaceSummary
|
<WorkspaceSummary
|
||||||
name={draftTitle}
|
name={draftTitle}
|
||||||
@@ -99,38 +147,38 @@ export function WorkspacesSidebar({
|
|||||||
latestProcessStatus={workspace.latestProcessStatus}
|
latestProcessStatus={workspace.latestProcessStatus}
|
||||||
prStatus={workspace.prStatus}
|
prStatus={workspace.prStatus}
|
||||||
onClick={() => onSelectWorkspace(workspace.id)}
|
onClick={() => onSelectWorkspace(workspace.id)}
|
||||||
|
onArchive={() => onArchiveWorkspace?.(workspace.id)}
|
||||||
|
onPin={() => onPinWorkspace?.(workspace.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</CollapsibleSection>
|
|
||||||
<CollapsibleSection
|
|
||||||
persistKey={PERSIST_KEYS.workspacesSidebarArchived}
|
|
||||||
title={t('common:workspaces.archived')}
|
|
||||||
defaultExpanded
|
|
||||||
className="px-base pb-half"
|
|
||||||
>
|
|
||||||
{filteredArchivedWorkspaces.map((workspace) => (
|
|
||||||
<WorkspaceSummary
|
|
||||||
summary
|
|
||||||
key={workspace.id}
|
|
||||||
name={workspace.name}
|
|
||||||
workspaceId={workspace.id}
|
|
||||||
filesChanged={workspace.filesChanged}
|
|
||||||
linesAdded={workspace.linesAdded}
|
|
||||||
linesRemoved={workspace.linesRemoved}
|
|
||||||
isActive={selectedWorkspaceId === workspace.id}
|
|
||||||
isRunning={workspace.isRunning}
|
|
||||||
isPinned={workspace.isPinned}
|
|
||||||
hasPendingApproval={workspace.hasPendingApproval}
|
|
||||||
hasRunningDevServer={workspace.hasRunningDevServer}
|
|
||||||
hasUnseenActivity={workspace.hasUnseenActivity}
|
|
||||||
latestProcessCompletedAt={workspace.latestProcessCompletedAt}
|
|
||||||
latestProcessStatus={workspace.latestProcessStatus}
|
|
||||||
prStatus={workspace.prStatus}
|
|
||||||
onClick={() => onSelectWorkspace(workspace.id)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</CollapsibleSection>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fixed footer toggle - only show if there are archived workspaces */}
|
||||||
|
{hasArchivedWorkspaces && (
|
||||||
|
<div className="border-t border-primary p-base">
|
||||||
|
<button
|
||||||
|
onClick={() => onShowArchiveChange?.(!showArchive)}
|
||||||
|
className="w-full flex items-center gap-base text-sm text-low hover:text-normal transition-colors duration-100"
|
||||||
|
>
|
||||||
|
{showArchive ? (
|
||||||
|
<>
|
||||||
|
<ArrowLeftIcon className="size-icon-xs" />
|
||||||
|
<span>{t('common:workspaces.backToActive')}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ArchiveIcon className="size-icon-xs" />
|
||||||
|
<span>{t('common:workspaces.viewArchive')}</span>
|
||||||
|
<span className="ml-auto text-xs bg-tertiary px-1.5 py-0.5 rounded">
|
||||||
|
{archivedWorkspaces.length}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,13 @@
|
|||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"selectToStart": "Select a workspace to get started",
|
"selectToStart": "Select a workspace to get started",
|
||||||
"draft": "Draft",
|
"draft": "Draft",
|
||||||
|
"viewArchive": "View Archive",
|
||||||
|
"backToActive": "Back to Active",
|
||||||
|
"noArchived": "No archived workspaces",
|
||||||
|
"pin": "Pin",
|
||||||
|
"unpin": "Unpin",
|
||||||
|
"archive": "Archive",
|
||||||
|
"more": "More actions",
|
||||||
"rename": {
|
"rename": {
|
||||||
"title": "Rename Workspace",
|
"title": "Rename Workspace",
|
||||||
"description": "Enter a new name for this workspace.",
|
"description": "Enter a new name for this workspace.",
|
||||||
|
|||||||
@@ -131,6 +131,13 @@
|
|||||||
"loading": "Cargando...",
|
"loading": "Cargando...",
|
||||||
"selectToStart": "Selecciona un espacio de trabajo para comenzar",
|
"selectToStart": "Selecciona un espacio de trabajo para comenzar",
|
||||||
"draft": "Borrador",
|
"draft": "Borrador",
|
||||||
|
"viewArchive": "Ver archivo",
|
||||||
|
"backToActive": "Volver a activos",
|
||||||
|
"noArchived": "No hay espacios de trabajo archivados",
|
||||||
|
"pin": "Fijar",
|
||||||
|
"unpin": "Desfijar",
|
||||||
|
"archive": "Archivar",
|
||||||
|
"more": "Más acciones",
|
||||||
"rename": {
|
"rename": {
|
||||||
"title": "Renombrar espacio de trabajo",
|
"title": "Renombrar espacio de trabajo",
|
||||||
"description": "Ingresa un nuevo nombre para este espacio de trabajo.",
|
"description": "Ingresa un nuevo nombre para este espacio de trabajo.",
|
||||||
|
|||||||
@@ -131,6 +131,13 @@
|
|||||||
"loading": "読み込み中...",
|
"loading": "読み込み中...",
|
||||||
"selectToStart": "ワークスペースを選択して開始",
|
"selectToStart": "ワークスペースを選択して開始",
|
||||||
"draft": "下書き",
|
"draft": "下書き",
|
||||||
|
"viewArchive": "アーカイブを表示",
|
||||||
|
"backToActive": "アクティブに戻る",
|
||||||
|
"noArchived": "アーカイブされたワークスペースはありません",
|
||||||
|
"pin": "ピン留め",
|
||||||
|
"unpin": "ピン留め解除",
|
||||||
|
"archive": "アーカイブ",
|
||||||
|
"more": "その他の操作",
|
||||||
"rename": {
|
"rename": {
|
||||||
"title": "ワークスペースの名前を変更",
|
"title": "ワークスペースの名前を変更",
|
||||||
"description": "このワークスペースの新しい名前を入力してください。",
|
"description": "このワークスペースの新しい名前を入力してください。",
|
||||||
|
|||||||
@@ -131,6 +131,13 @@
|
|||||||
"loading": "로딩 중...",
|
"loading": "로딩 중...",
|
||||||
"selectToStart": "워크스페이스를 선택하여 시작",
|
"selectToStart": "워크스페이스를 선택하여 시작",
|
||||||
"draft": "초안",
|
"draft": "초안",
|
||||||
|
"viewArchive": "보관함 보기",
|
||||||
|
"backToActive": "활성으로 돌아가기",
|
||||||
|
"noArchived": "보관된 워크스페이스 없음",
|
||||||
|
"pin": "고정",
|
||||||
|
"unpin": "고정 해제",
|
||||||
|
"archive": "보관",
|
||||||
|
"more": "더 많은 작업",
|
||||||
"rename": {
|
"rename": {
|
||||||
"title": "워크스페이스 이름 변경",
|
"title": "워크스페이스 이름 변경",
|
||||||
"description": "이 워크스페이스의 새 이름을 입력하세요.",
|
"description": "이 워크스페이스의 새 이름을 입력하세요.",
|
||||||
|
|||||||
@@ -131,6 +131,13 @@
|
|||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
"selectToStart": "选择一个工作区开始",
|
"selectToStart": "选择一个工作区开始",
|
||||||
"draft": "草稿",
|
"draft": "草稿",
|
||||||
|
"viewArchive": "查看归档",
|
||||||
|
"backToActive": "返回活跃",
|
||||||
|
"noArchived": "没有已归档的工作区",
|
||||||
|
"pin": "置顶",
|
||||||
|
"unpin": "取消置顶",
|
||||||
|
"archive": "归档",
|
||||||
|
"more": "更多操作",
|
||||||
"rename": {
|
"rename": {
|
||||||
"title": "重命名工作区",
|
"title": "重命名工作区",
|
||||||
"description": "输入此工作区的新名称。",
|
"description": "输入此工作区的新名称。",
|
||||||
|
|||||||
@@ -131,6 +131,13 @@
|
|||||||
"loading": "載入中...",
|
"loading": "載入中...",
|
||||||
"selectToStart": "選擇一個工作區開始",
|
"selectToStart": "選擇一個工作區開始",
|
||||||
"draft": "草稿",
|
"draft": "草稿",
|
||||||
|
"viewArchive": "檢視封存",
|
||||||
|
"backToActive": "返回活躍",
|
||||||
|
"noArchived": "沒有已封存的工作區",
|
||||||
|
"pin": "釘選",
|
||||||
|
"unpin": "取消釘選",
|
||||||
|
"archive": "封存",
|
||||||
|
"more": "更多操作",
|
||||||
"rename": {
|
"rename": {
|
||||||
"title": "重新命名工作區",
|
"title": "重新命名工作區",
|
||||||
"description": "輸入此工作區的新名稱。",
|
"description": "輸入此工作區的新名稱。",
|
||||||
|
|||||||
Reference in New Issue
Block a user