Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions apps/array/src/renderer/components/DotsCircleSpinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useEffect, useState } from "react";

const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
const INTERVAL = 80;

interface DotsCircleSpinnerProps {
size?: number;
className?: string;
}

export function DotsCircleSpinner({
size = 12,
className,
}: DotsCircleSpinnerProps) {
const [frameIndex, setFrameIndex] = useState(0);

useEffect(() => {
const timer = setInterval(() => {
setFrameIndex((prev) => (prev + 1) % FRAMES.length);
}, INTERVAL);

return () => clearInterval(timer);
}, []);

return (
<span
className={className}
style={{
display: "inline-flex",
width: size,
height: size,
alignItems: "center",
justifyContent: "center",
fontSize: size,
lineHeight: 1,
}}
>
{FRAMES[frameIndex]}
</span>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { SidebarItemAction } from "../types";

const INDENT_SIZE = 12;
const INDENT_SIZE = 8;

interface SidebarItemProps {
depth: number;
Expand All @@ -27,27 +27,36 @@ export function SidebarItem({
return (
<button
type="button"
className="focus-visible:-outline-offset-2 flex w-full cursor-pointer items-center border-0 bg-transparent px-2 py-1 text-left font-mono text-[12px] text-gray-11 transition-colors hover:bg-gray-3 focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent-8 data-[active]:bg-gray-3"
className="group focus-visible:-outline-offset-2 flex w-full cursor-pointer items-start border-transparent border-y bg-transparent px-2 py-1 text-left font-mono text-[12px] text-gray-11 transition-colors hover:bg-gray-3 focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent-8 data-[active]:border-accent-8 data-[active]:bg-accent-4 data-[active]:text-gray-12"
data-active={isActive || undefined}
style={{
paddingLeft: `${depth * INDENT_SIZE + 8}px`,
gap: "6px",
gap: "4px",
}}
onClick={onClick}
onContextMenu={onContextMenu}
>
{icon && <span className="flex shrink-0 items-center">{icon}</span>}
{icon && (
<span
className="flex shrink-0 items-center text-gray-10 group-data-[active]:text-gray-11"
style={{ height: "18px" }}
>
{icon}
</span>
)}
<span className="flex min-w-0 flex-1 flex-col overflow-hidden">
<span className="overflow-hidden text-ellipsis whitespace-nowrap">
{label}
<span className="flex items-center gap-1" style={{ height: "18px" }}>
<span className="min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap">
{label}
</span>
{endContent}
</span>
{subtitle && (
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-[10px] text-gray-10">
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-[10px] text-gray-10 group-data-[active]:text-gray-11">
{subtitle}
</span>
)}
</span>
{endContent}
</button>
);
}
172 changes: 105 additions & 67 deletions apps/array/src/renderer/features/sidebar/components/SidebarMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,41 @@
import { RenameTaskDialog } from "@components/RenameTaskDialog";
import type { DragDropEvents } from "@dnd-kit/react";
import { DragDropProvider, DragOverlay, PointerSensor } from "@dnd-kit/react";
import { useTaskExecutionStore } from "@features/task-detail/stores/taskExecutionStore";
import { useTasks } from "@features/tasks/hooks/useTasks";
import { useTaskStore } from "@features/tasks/stores/taskStore";
import { useMeQuery } from "@hooks/useMeQuery";
import { useTaskContextMenu } from "@hooks/useTaskContextMenu";
import { FolderIcon } from "@phosphor-icons/react";
import { FolderIcon, FolderOpenIcon } from "@phosphor-icons/react";
import { Box, Flex } from "@radix-ui/themes";
import { useRegisteredFoldersStore } from "@renderer/stores/registeredFoldersStore";
import type { Task } from "@shared/types";
import { useNavigationStore } from "@stores/navigationStore";
import { memo } from "react";
import { memo, useCallback } from "react";
import { useWorkspaceStore } from "@/renderer/features/workspace/stores/workspaceStore";
import { useSidebarData } from "../hooks/useSidebarData";
import { useSidebarStore } from "../stores/sidebarStore";
import { useTaskViewedStore } from "../stores/taskViewedStore";
import { HomeItem } from "./items/HomeItem";
import { NewTaskItem } from "./items/NewTaskItem";
import { ProjectsItem } from "./items/ProjectsItem";
import { TaskItem } from "./items/TaskItem";
import { ViewItem } from "./items/ViewItem";
import { SidebarSection } from "./SidebarSection";
import { SortableFolderSection } from "./SortableFolderSection";

function SidebarMenuComponent() {
const { view, navigateToTaskList, navigateToTask, navigateToTaskInput } =
useNavigationStore();
const { view, navigateToTask, navigateToTaskInput } = useNavigationStore();

const activeFilters = useTaskStore((state) => state.activeFilters);
const setActiveFilters = useTaskStore((state) => state.setActiveFilters);
const { data: currentUser } = useMeQuery();
const { data: allTasks = [] } = useTasks();
const { folders, removeFolder } = useRegisteredFoldersStore();

const collapsedSections = useSidebarStore((state) => state.collapsedSections);
const toggleSection = useSidebarStore((state) => state.toggleSection);
const folderOrder = useSidebarStore((state) => state.folderOrder);
const reorderFolders = useSidebarStore((state) => state.reorderFolders);
const workspaces = useWorkspaceStore.use.workspaces();
const taskStates = useTaskExecutionStore((state) => state.taskStates);
const markAsViewed = useTaskViewedStore((state) => state.markAsViewed);

const { showContextMenu, renameTask, renameDialogOpen, setRenameDialogOpen } =
useTaskContextMenu();
Expand All @@ -44,6 +46,35 @@ function SidebarMenuComponent() {
currentUser,
});

const handleDragOver: DragDropEvents["dragover"] = useCallback(
(event) => {
const source = event.operation.source;
const target = event.operation.target;

// type is at sortable level, not in data
if (source?.type !== "folder" || target?.type !== "folder") {
return;
}

const sourceId = source?.id;
const targetId = target?.id;

if (!sourceId || !targetId || sourceId === targetId) return;

const sourceIndex = folderOrder.indexOf(String(sourceId));
const targetIndex = folderOrder.indexOf(String(targetId));

if (
sourceIndex !== -1 &&
targetIndex !== -1 &&
sourceIndex !== targetIndex
) {
reorderFolders(sourceIndex, targetIndex);
}
},
[folderOrder, reorderFolders],
);

const taskMap = new Map<string, Task>();
for (const task of allTasks) {
taskMap.set(task.id, task);
Expand All @@ -53,21 +84,10 @@ function SidebarMenuComponent() {
navigateToTaskInput();
};

const handleViewClick = (filters: typeof activeFilters) => {
setActiveFilters(filters);
navigateToTaskList();
};

const handleProjectClick = (repository: string) => {
const newFilters = { ...activeFilters };
newFilters.repository = [{ value: repository, operator: "is" }];
setActiveFilters(newFilters);
navigateToTaskList();
};

const handleTaskClick = (taskId: string) => {
const task = taskMap.get(taskId);
if (task) {
markAsViewed(taskId);
navigateToTask(task);
}
};
Expand Down Expand Up @@ -135,59 +155,77 @@ function SidebarMenuComponent() {
overflowX: "hidden",
}}
>
<Flex direction="column" p="2">
<Flex direction="column" py="2">
<HomeItem
isActive={sidebarData.isHomeActive}
onClick={handleHomeClick}
/>

{sidebarData.views.map((view) => (
<ViewItem
key={view.id}
label={view.label}
isActive={sidebarData.activeViewId === view.id}
onClick={() => handleViewClick(view.filters)}
/>
))}

<ProjectsItem
repositories={sidebarData.repositories}
isLoading={sidebarData.isLoading}
activeRepository={sidebarData.activeRepository}
onProjectClick={handleProjectClick}
/>

{sidebarData.folders.map((folder, index) => (
<SidebarSection
key={folder.id}
id={folder.id}
label={folder.name}
icon={<FolderIcon size={14} weight="regular" />}
isExpanded={!collapsedSections.has(folder.id)}
onToggle={() => toggleSection(folder.id)}
addSpacingBefore={index === 0}
onContextMenu={(e) => handleFolderContextMenu(folder.id, e)}
>
<NewTaskItem onClick={() => handleFolderNewTask(folder.id)} />
{folder.tasks.map((task) => (
<TaskItem
key={task.id}
id={task.id}
label={task.title}
status={task.status}
isActive={sidebarData.activeTaskId === task.id}
worktreeName={workspaces[task.id]?.worktreeName ?? undefined}
worktreePath={
workspaces[task.id]?.worktreePath ??
workspaces[task.id]?.folderPath
<DragDropProvider
onDragOver={handleDragOver}
sensors={[
PointerSensor.configure({
activationConstraints: {
distance: { value: 5 },
},
}),
]}
>
{sidebarData.folders.map((folder, index) => {
const isExpanded = !collapsedSections.has(folder.id);
return (
<SortableFolderSection
key={folder.id}
id={folder.id}
index={index}
label={folder.name}
icon={
isExpanded ? (
<FolderOpenIcon size={14} weight="regular" />
) : (
<FolderIcon size={14} weight="regular" />
)
}
workspaceMode={taskStates[task.id]?.workspaceMode}
onClick={() => handleTaskClick(task.id)}
onContextMenu={(e) => handleTaskContextMenu(task.id, e)}
/>
))}
</SidebarSection>
))}
isExpanded={isExpanded}
onToggle={() => toggleSection(folder.id)}
onContextMenu={(e) => handleFolderContextMenu(folder.id, e)}
>
<NewTaskItem onClick={() => handleFolderNewTask(folder.id)} />
{folder.tasks.map((task) => (
<TaskItem
key={task.id}
id={task.id}
label={task.title}
isActive={sidebarData.activeTaskId === task.id}
worktreeName={
workspaces[task.id]?.worktreeName ?? undefined
}
worktreePath={
workspaces[task.id]?.worktreePath ??
workspaces[task.id]?.folderPath
}
workspaceMode={taskStates[task.id]?.workspaceMode}
lastActivityAt={task.lastActivityAt}
isGenerating={task.isGenerating}
isUnread={task.isUnread}
onClick={() => handleTaskClick(task.id)}
onContextMenu={(e) => handleTaskContextMenu(task.id, e)}
/>
))}
</SortableFolderSection>
);
})}
<DragOverlay>
{(source) =>
source?.type === "folder" ? (
<div className="flex w-full items-center gap-1 rounded bg-gray-2 px-2 py-1 font-mono text-[12px] text-gray-11 shadow-lg">
<FolderIcon size={14} weight="regular" />
<span className="font-medium">{source.data?.label}</span>
</div>
) : null
}
</DragOverlay>
</DragDropProvider>
</Flex>
</Box>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,28 @@ export function SidebarSection({
<Collapsible.Trigger asChild>
<button
type="button"
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-2 py-1 text-left font-mono text-[12px] text-gray-12 transition-colors hover:bg-gray-3"
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-2 py-1 text-left font-mono text-[12px] text-gray-11 transition-colors hover:bg-gray-3"
style={{
marginTop: addSpacingBefore ? "16px" : undefined,
marginTop: addSpacingBefore ? "12px" : undefined,
paddingLeft: "8px",
}}
onContextMenu={onContextMenu}
>
<span className="flex flex-1 items-center" style={{ gap: "6px" }}>
<span
className="flex min-w-0 flex-1 items-center"
style={{ gap: "4px" }}
>
{icon && <span className="flex shrink-0 items-center">{icon}</span>}
<span className="overflow-hidden text-ellipsis whitespace-nowrap font-medium">
{label}
</span>
<span className="flex items-center">
{isExpanded ? (
<CaretDownIcon size={12} weight="fill" />
) : (
<CaretRightIcon size={12} weight="fill" />
)}
</span>
</span>
<span className="flex shrink-0 items-center text-gray-10">
{isExpanded ? (
<CaretDownIcon size={12} />
) : (
<CaretRightIcon size={12} />
)}
</span>
</button>
</Collapsible.Trigger>
Expand Down
Loading