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,
|
||||
} from '@virtuoso.dev/message-list';
|
||||
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 { ApprovalFormProvider } from '@/contexts/ApprovalFormContext';
|
||||
import { useEntries } from '@/contexts/EntriesContext';
|
||||
@@ -86,7 +85,6 @@ const computeItemKey: VirtuosoMessageListProps<
|
||||
>['computeItemKey'] = ({ data }) => `conv-${data.patchKey}`;
|
||||
|
||||
export function ConversationList({ attempt, task }: ConversationListProps) {
|
||||
const { t } = useTranslation('common');
|
||||
const [channelData, setChannelData] =
|
||||
useState<DataWithScrollModifier<PatchTypeWithKey> | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -156,8 +154,17 @@ export function ConversationList({ attempt, task }: ConversationListProps) {
|
||||
[attempt, task]
|
||||
);
|
||||
|
||||
// Determine if content is ready to show (has data or finished loading)
|
||||
const hasContent = !loading || (channelData?.data?.length ?? 0) > 0;
|
||||
|
||||
return (
|
||||
<ApprovalFormProvider>
|
||||
<div
|
||||
className={cn(
|
||||
'h-full transition-opacity duration-300',
|
||||
hasContent ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
>
|
||||
<VirtuosoMessageListLicense
|
||||
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" />}
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
</ApprovalFormProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -944,11 +944,7 @@ export type NavbarItem = ActionDefinition | typeof NavbarDivider;
|
||||
|
||||
// Navbar action groups define which actions appear in each section
|
||||
export const NavbarActionGroups = {
|
||||
left: [
|
||||
Actions.OpenInOldUI,
|
||||
NavbarDivider,
|
||||
Actions.ArchiveWorkspace,
|
||||
] as ActionDefinition[],
|
||||
left: [Actions.OpenInOldUI] as ActionDefinition[],
|
||||
right: [
|
||||
Actions.ToggleDiffViewMode,
|
||||
Actions.ToggleAllDiffs,
|
||||
|
||||
@@ -545,9 +545,43 @@ export function SessionChatBoxContainer({
|
||||
localMessage,
|
||||
]);
|
||||
|
||||
// Don't render if no session and not in new session mode
|
||||
if (!session && !isNewSessionMode) {
|
||||
return null;
|
||||
// Render placeholder state if no session and not in new session mode
|
||||
// This maintains the visual structure during workspace transitions
|
||||
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 (
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
PERSIST_KEYS,
|
||||
useExpandedAll,
|
||||
usePaneSize,
|
||||
usePersistedExpanded,
|
||||
} from '@/stores/useUiPreferencesStore';
|
||||
import {
|
||||
useLayoutStore,
|
||||
@@ -304,6 +305,10 @@ export function WorkspacesLayout() {
|
||||
50
|
||||
);
|
||||
const isRightMainPanelVisible = useIsRightMainPanelVisible();
|
||||
const [showArchive, setShowArchive] = usePersistedExpanded(
|
||||
PERSIST_KEYS.workspacesSidebarArchived,
|
||||
false
|
||||
);
|
||||
|
||||
const defaultLayout = (): Layout => {
|
||||
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
|
||||
const renderSidebar = () => (
|
||||
<WorkspacesSidebar
|
||||
@@ -720,9 +742,13 @@ export function WorkspacesLayout() {
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
onAddWorkspace={navigateToCreate}
|
||||
onArchiveWorkspace={handleArchiveWorkspace}
|
||||
onPinWorkspace={handlePinWorkspace}
|
||||
isCreateMode={isCreateMode}
|
||||
draftTitle={persistedDraftTitle}
|
||||
onSelectCreate={navigateToCreate}
|
||||
showArchive={showArchive}
|
||||
onShowArchiveChange={setShowArchive}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import {
|
||||
PushPinIcon,
|
||||
DotsThreeIcon,
|
||||
HandIcon,
|
||||
TriangleIcon,
|
||||
PlayIcon,
|
||||
FileIcon,
|
||||
CircleIcon,
|
||||
GitPullRequestIcon,
|
||||
ArchiveIcon,
|
||||
ListIcon,
|
||||
} from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -30,6 +31,8 @@ interface WorkspaceSummaryProps {
|
||||
latestProcessStatus?: 'running' | 'completed' | 'failed' | 'killed';
|
||||
prStatus?: 'open' | 'merged' | 'closed' | 'unknown';
|
||||
onClick?: () => void;
|
||||
onArchive?: () => void;
|
||||
onPin?: () => void;
|
||||
className?: string;
|
||||
summary?: boolean;
|
||||
/** Whether this is a draft workspace (shows "Draft" instead of elapsed time) */
|
||||
@@ -52,6 +55,8 @@ export function WorkspaceSummary({
|
||||
latestProcessStatus,
|
||||
prStatus,
|
||||
onClick,
|
||||
onArchive,
|
||||
onPin,
|
||||
className,
|
||||
summary = 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 (
|
||||
<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
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'flex w-full cursor-pointer flex-col border-l-4 text-left text-low',
|
||||
isActive ? 'border-normal pl-base' : 'border-none'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'truncate group-hover:text-high pr-double',
|
||||
!summary && 'text-normal'
|
||||
'flex w-full cursor-pointer flex-col text-left px-base py-half transition-all duration-150',
|
||||
isActive
|
||||
? 'text-normal'
|
||||
: 'text-low opacity-60 hover:opacity-100 hover:text-normal'
|
||||
)}
|
||||
>
|
||||
<div className={cn('truncate pr-double', !summary && 'text-normal')}>
|
||||
{name}
|
||||
</div>
|
||||
{(!summary || isActive) && (
|
||||
@@ -179,16 +204,55 @@ export function WorkspaceSummary({
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Right-side hover zone for action overlay */}
|
||||
{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
|
||||
onClick={handleOpenCommandBar}
|
||||
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>
|
||||
</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 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 { WorkspaceSummary } from '@/components/ui-new/primitives/WorkspaceSummary';
|
||||
import { SectionHeader } from '../primitives/SectionHeader';
|
||||
import { PERSIST_KEYS } from '@/stores/useUiPreferencesStore';
|
||||
|
||||
interface WorkspacesSidebarProps {
|
||||
workspaces: Workspace[];
|
||||
@@ -13,6 +11,8 @@ interface WorkspacesSidebarProps {
|
||||
selectedWorkspaceId: string | null;
|
||||
onSelectWorkspace: (id: string) => void;
|
||||
onAddWorkspace?: () => void;
|
||||
onArchiveWorkspace?: (id: string) => void;
|
||||
onPinWorkspace?: (id: string) => void;
|
||||
searchQuery: string;
|
||||
onSearchChange: (value: string) => void;
|
||||
/** Whether we're in create mode */
|
||||
@@ -21,6 +21,10 @@ interface WorkspacesSidebarProps {
|
||||
draftTitle?: string;
|
||||
/** Handler to navigate back to create mode */
|
||||
onSelectCreate?: () => void;
|
||||
/** Whether to show archived workspaces */
|
||||
showArchive?: boolean;
|
||||
/** Handler for toggling archive view */
|
||||
onShowArchiveChange?: (show: boolean) => void;
|
||||
}
|
||||
|
||||
export function WorkspacesSidebar({
|
||||
@@ -29,11 +33,15 @@ export function WorkspacesSidebar({
|
||||
selectedWorkspaceId,
|
||||
onSelectWorkspace,
|
||||
onAddWorkspace,
|
||||
onArchiveWorkspace,
|
||||
onPinWorkspace,
|
||||
searchQuery,
|
||||
onSearchChange,
|
||||
isCreateMode = false,
|
||||
draftTitle,
|
||||
onSelectCreate,
|
||||
showArchive = false,
|
||||
onShowArchiveChange,
|
||||
}: WorkspacesSidebarProps) {
|
||||
const { t } = useTranslation(['tasks', 'common']);
|
||||
const searchLower = searchQuery.toLowerCase();
|
||||
@@ -48,8 +56,11 @@ export function WorkspacesSidebar({
|
||||
.filter((workspace) => workspace.name.toLowerCase().includes(searchLower))
|
||||
.slice(0, isSearching ? undefined : DISPLAY_LIMIT);
|
||||
|
||||
const hasArchivedWorkspaces = archivedWorkspaces.length > 0;
|
||||
|
||||
return (
|
||||
<div className="w-full h-full bg-secondary flex flex-col">
|
||||
{/* Header + Search */}
|
||||
<div className="flex flex-col gap-base">
|
||||
<SectionHeader
|
||||
title={t('common:workspaces.title')}
|
||||
@@ -65,14 +76,51 @@ export function WorkspacesSidebar({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col flex-1 overflow-y-auto">
|
||||
<CollapsibleSection
|
||||
persistKey={PERSIST_KEYS.workspacesSidebarActive}
|
||||
title={t('common:workspaces.active')}
|
||||
defaultExpanded
|
||||
className="p-base"
|
||||
contentClassName="flex flex-col gap-base min-h-[50vh]"
|
||||
>
|
||||
|
||||
{/* Scrollable workspace list */}
|
||||
<div className="flex-1 overflow-y-auto p-base">
|
||||
{showArchive ? (
|
||||
/* Archived workspaces view */
|
||||
<div className="flex flex-col gap-base">
|
||||
<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 && (
|
||||
<WorkspaceSummary
|
||||
name={draftTitle}
|
||||
@@ -99,38 +147,38 @@ export function WorkspacesSidebar({
|
||||
latestProcessStatus={workspace.latestProcessStatus}
|
||||
prStatus={workspace.prStatus}
|
||||
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>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -131,6 +131,13 @@
|
||||
"loading": "Loading...",
|
||||
"selectToStart": "Select a workspace to get started",
|
||||
"draft": "Draft",
|
||||
"viewArchive": "View Archive",
|
||||
"backToActive": "Back to Active",
|
||||
"noArchived": "No archived workspaces",
|
||||
"pin": "Pin",
|
||||
"unpin": "Unpin",
|
||||
"archive": "Archive",
|
||||
"more": "More actions",
|
||||
"rename": {
|
||||
"title": "Rename Workspace",
|
||||
"description": "Enter a new name for this workspace.",
|
||||
|
||||
@@ -131,6 +131,13 @@
|
||||
"loading": "Cargando...",
|
||||
"selectToStart": "Selecciona un espacio de trabajo para comenzar",
|
||||
"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": {
|
||||
"title": "Renombrar espacio de trabajo",
|
||||
"description": "Ingresa un nuevo nombre para este espacio de trabajo.",
|
||||
|
||||
@@ -131,6 +131,13 @@
|
||||
"loading": "読み込み中...",
|
||||
"selectToStart": "ワークスペースを選択して開始",
|
||||
"draft": "下書き",
|
||||
"viewArchive": "アーカイブを表示",
|
||||
"backToActive": "アクティブに戻る",
|
||||
"noArchived": "アーカイブされたワークスペースはありません",
|
||||
"pin": "ピン留め",
|
||||
"unpin": "ピン留め解除",
|
||||
"archive": "アーカイブ",
|
||||
"more": "その他の操作",
|
||||
"rename": {
|
||||
"title": "ワークスペースの名前を変更",
|
||||
"description": "このワークスペースの新しい名前を入力してください。",
|
||||
|
||||
@@ -131,6 +131,13 @@
|
||||
"loading": "로딩 중...",
|
||||
"selectToStart": "워크스페이스를 선택하여 시작",
|
||||
"draft": "초안",
|
||||
"viewArchive": "보관함 보기",
|
||||
"backToActive": "활성으로 돌아가기",
|
||||
"noArchived": "보관된 워크스페이스 없음",
|
||||
"pin": "고정",
|
||||
"unpin": "고정 해제",
|
||||
"archive": "보관",
|
||||
"more": "더 많은 작업",
|
||||
"rename": {
|
||||
"title": "워크스페이스 이름 변경",
|
||||
"description": "이 워크스페이스의 새 이름을 입력하세요.",
|
||||
|
||||
@@ -131,6 +131,13 @@
|
||||
"loading": "加载中...",
|
||||
"selectToStart": "选择一个工作区开始",
|
||||
"draft": "草稿",
|
||||
"viewArchive": "查看归档",
|
||||
"backToActive": "返回活跃",
|
||||
"noArchived": "没有已归档的工作区",
|
||||
"pin": "置顶",
|
||||
"unpin": "取消置顶",
|
||||
"archive": "归档",
|
||||
"more": "更多操作",
|
||||
"rename": {
|
||||
"title": "重命名工作区",
|
||||
"description": "输入此工作区的新名称。",
|
||||
|
||||
@@ -131,6 +131,13 @@
|
||||
"loading": "載入中...",
|
||||
"selectToStart": "選擇一個工作區開始",
|
||||
"draft": "草稿",
|
||||
"viewArchive": "檢視封存",
|
||||
"backToActive": "返回活躍",
|
||||
"noArchived": "沒有已封存的工作區",
|
||||
"pin": "釘選",
|
||||
"unpin": "取消釘選",
|
||||
"archive": "封存",
|
||||
"more": "更多操作",
|
||||
"rename": {
|
||||
"title": "重新命名工作區",
|
||||
"description": "輸入此工作區的新名稱。",
|
||||
|
||||
Reference in New Issue
Block a user