feat(mobile): add project edit panel on long press (#584)
- Add draggable project list with reorder support - Use MobileOverlayPanel for consistent bottom sheet UI - Enhance useLongPress hook with movement detection - Integrate with ProjectEditDialog for editing project details Co-authored-by: Jovines <jovines@qq.com>
This commit is contained in:
committed by
GitHub
co-authored by
Jovines
parent
c9bcf79ca7
commit
855412ce79
@@ -7,7 +7,32 @@ import type { Session } from '@opencode-ai/sdk/v2';
|
|||||||
import type { ProjectEntry } from '@/lib/api/types';
|
import type { ProjectEntry } from '@/lib/api/types';
|
||||||
import { cn, formatDirectoryName } from '@/lib/utils';
|
import { cn, formatDirectoryName } from '@/lib/utils';
|
||||||
import { getAgentColor } from '@/lib/agentColors';
|
import { getAgentColor } from '@/lib/agentColors';
|
||||||
import { RiLoader4Line, RiAddLine } from '@remixicon/react';
|
import {
|
||||||
|
RiLoader4Line,
|
||||||
|
RiAddLine,
|
||||||
|
RiDragMove2Line,
|
||||||
|
RiDeleteBinLine,
|
||||||
|
RiEditLine,
|
||||||
|
RiArrowUpLine,
|
||||||
|
RiArrowDownLine,
|
||||||
|
} from '@remixicon/react';
|
||||||
|
import {
|
||||||
|
DndContext,
|
||||||
|
closestCenter,
|
||||||
|
KeyboardSensor,
|
||||||
|
PointerSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
type DragEndEvent,
|
||||||
|
} from '@dnd-kit/core';
|
||||||
|
import {
|
||||||
|
arrayMove,
|
||||||
|
SortableContext,
|
||||||
|
sortableKeyboardCoordinates,
|
||||||
|
useSortable,
|
||||||
|
verticalListSortingStrategy,
|
||||||
|
} from '@dnd-kit/sortable';
|
||||||
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import type { SessionContextUsage } from '@/stores/types/sessionTypes';
|
import type { SessionContextUsage } from '@/stores/types/sessionTypes';
|
||||||
import { PROJECT_ICON_MAP, PROJECT_COLOR_MAP, getProjectIconImageUrl } from '@/lib/projectMeta';
|
import { PROJECT_ICON_MAP, PROJECT_COLOR_MAP, getProjectIconImageUrl } from '@/lib/projectMeta';
|
||||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
@@ -23,7 +48,9 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { ProjectEditDialog } from '@/components/layout/ProjectEditDialog';
|
||||||
import { useDrawerSwipe } from '@/hooks/useDrawerSwipe';
|
import { useDrawerSwipe } from '@/hooks/useDrawerSwipe';
|
||||||
|
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
|
||||||
|
|
||||||
interface MobileSessionStatusBarProps {
|
interface MobileSessionStatusBarProps {
|
||||||
onSessionSwitch?: (sessionId: string) => void;
|
onSessionSwitch?: (sessionId: string) => void;
|
||||||
@@ -486,7 +513,7 @@ function SessionStatusHeader({
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Hook for long press
|
// Hook for long press with movement detection
|
||||||
function useLongPress(
|
function useLongPress(
|
||||||
onLongPress: () => void,
|
onLongPress: () => void,
|
||||||
onClick: () => void,
|
onClick: () => void,
|
||||||
@@ -494,20 +521,43 @@ function useLongPress(
|
|||||||
) {
|
) {
|
||||||
const timerRef = React.useRef<NodeJS.Timeout | null>(null);
|
const timerRef = React.useRef<NodeJS.Timeout | null>(null);
|
||||||
const isLongPress = React.useRef(false);
|
const isLongPress = React.useRef(false);
|
||||||
|
const startPosRef = React.useRef<{ x: number; y: number } | null>(null);
|
||||||
|
const hasMovedRef = React.useRef(false);
|
||||||
|
const MOVE_THRESHOLD = 10; // pixels
|
||||||
|
|
||||||
const start = React.useCallback(() => {
|
const start = React.useCallback((clientX: number, clientY: number) => {
|
||||||
isLongPress.current = false;
|
isLongPress.current = false;
|
||||||
|
hasMovedRef.current = false;
|
||||||
|
startPosRef.current = { x: clientX, y: clientY };
|
||||||
timerRef.current = setTimeout(() => {
|
timerRef.current = setTimeout(() => {
|
||||||
|
if (!hasMovedRef.current) {
|
||||||
isLongPress.current = true;
|
isLongPress.current = true;
|
||||||
onLongPress();
|
onLongPress();
|
||||||
|
}
|
||||||
}, ms);
|
}, ms);
|
||||||
}, [onLongPress, ms]);
|
}, [onLongPress, ms]);
|
||||||
|
|
||||||
|
const move = React.useCallback((clientX: number, clientY: number) => {
|
||||||
|
if (!startPosRef.current) return;
|
||||||
|
|
||||||
|
const dx = Math.abs(clientX - startPosRef.current.x);
|
||||||
|
const dy = Math.abs(clientY - startPosRef.current.y);
|
||||||
|
|
||||||
|
if (dx > MOVE_THRESHOLD || dy > MOVE_THRESHOLD) {
|
||||||
|
hasMovedRef.current = true;
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
timerRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const end = React.useCallback(() => {
|
const end = React.useCallback(() => {
|
||||||
if (timerRef.current) {
|
if (timerRef.current) {
|
||||||
clearTimeout(timerRef.current);
|
clearTimeout(timerRef.current);
|
||||||
timerRef.current = null;
|
timerRef.current = null;
|
||||||
}
|
}
|
||||||
|
startPosRef.current = null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleClick = React.useCallback(() => {
|
const handleClick = React.useCallback(() => {
|
||||||
@@ -517,15 +567,271 @@ function useLongPress(
|
|||||||
}, [onClick]);
|
}, [onClick]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onMouseDown: start,
|
onMouseDown: (e: React.MouseEvent) => start(e.clientX, e.clientY),
|
||||||
onMouseUp: end,
|
onMouseUp: end,
|
||||||
onMouseLeave: end,
|
onMouseLeave: end,
|
||||||
onTouchStart: start,
|
onMouseMove: (e: React.MouseEvent) => move(e.clientX, e.clientY),
|
||||||
|
onTouchStart: (e: React.TouchEvent) => {
|
||||||
|
const touch = e.touches[0];
|
||||||
|
start(touch.clientX, touch.clientY);
|
||||||
|
},
|
||||||
|
onTouchMove: (e: React.TouchEvent) => {
|
||||||
|
const touch = e.touches[0];
|
||||||
|
move(touch.clientX, touch.clientY);
|
||||||
|
},
|
||||||
onTouchEnd: end,
|
onTouchEnd: end,
|
||||||
onClick: handleClick,
|
onClick: handleClick,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sortable project item for edit panel
|
||||||
|
interface SortableProjectItemProps {
|
||||||
|
project: ProjectEntry;
|
||||||
|
isFirst: boolean;
|
||||||
|
isLast: boolean;
|
||||||
|
onMoveUp: () => void;
|
||||||
|
onMoveDown: () => void;
|
||||||
|
onEdit: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
formatProjectLabel: (project: ProjectEntry) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SortableProjectItem({
|
||||||
|
project,
|
||||||
|
isFirst,
|
||||||
|
isLast,
|
||||||
|
onMoveUp,
|
||||||
|
onMoveDown,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
formatProjectLabel,
|
||||||
|
}: SortableProjectItemProps) {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({ id: project.id });
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
zIndex: isDragging ? 10 : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [imageFailed, setImageFailed] = React.useState(false);
|
||||||
|
const ProjectIcon = project.icon ? PROJECT_ICON_MAP[project.icon] : null;
|
||||||
|
const projectIconImageUrl = !imageFailed ? getProjectIconImageUrl(project) : null;
|
||||||
|
const projectColorVar = project.color ? (PROJECT_COLOR_MAP[project.color] ?? null) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 p-3 bg-[var(--surface-elevated)] rounded-lg border border-[var(--interactive-border)]",
|
||||||
|
isDragging && "shadow-lg opacity-90"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Drag handle */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex-shrink-0 p-1.5 text-[var(--surface-mutedForeground)] hover:text-[var(--surface-foreground)] cursor-grab active:cursor-grabbing touch-none"
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
>
|
||||||
|
<RiDragMove2Line className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Project info */}
|
||||||
|
<div className="flex-1 flex items-center gap-2 min-w-0">
|
||||||
|
{projectIconImageUrl ? (
|
||||||
|
<span
|
||||||
|
className="inline-flex h-5 w-5 items-center justify-center overflow-hidden rounded-[2px] flex-shrink-0"
|
||||||
|
style={project.iconBackground ? { backgroundColor: project.iconBackground } : undefined}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={projectIconImageUrl}
|
||||||
|
alt=""
|
||||||
|
className="h-full w-full object-contain"
|
||||||
|
draggable={false}
|
||||||
|
onError={() => setImageFailed(true)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : ProjectIcon ? (
|
||||||
|
<ProjectIcon
|
||||||
|
className="h-5 w-5 flex-shrink-0"
|
||||||
|
style={projectColorVar ? { color: projectColorVar } : undefined}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="h-5 w-5 rounded bg-[var(--surface-muted)] flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
<span className="text-sm text-[var(--surface-foreground)] truncate">
|
||||||
|
{formatProjectLabel(project)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{/* Move up/down buttons (for non-drag sorting) */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onMoveUp}
|
||||||
|
disabled={isFirst}
|
||||||
|
className="p-1.5 rounded text-[var(--surface-mutedForeground)] hover:text-[var(--surface-foreground)] hover:bg-[var(--interactive-hover)] disabled:opacity-30 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<RiArrowUpLine className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onMoveDown}
|
||||||
|
disabled={isLast}
|
||||||
|
className="p-1.5 rounded text-[var(--surface-mutedForeground)] hover:text-[var(--surface-foreground)] hover:bg-[var(--interactive-hover)] disabled:opacity-30 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<RiArrowDownLine className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="w-px h-5 bg-[var(--interactive-border)] mx-1" />
|
||||||
|
|
||||||
|
{/* Edit button */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onEdit}
|
||||||
|
className="p-1.5 rounded text-[var(--surface-mutedForeground)] hover:text-[var(--primary-base)] hover:bg-[var(--primary-base)]/10"
|
||||||
|
>
|
||||||
|
<RiEditLine className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Delete button */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onDelete}
|
||||||
|
className="p-1.5 rounded text-[var(--surface-mutedForeground)] hover:text-[var(--status-error)] hover:bg-[var(--status-error)]/10"
|
||||||
|
>
|
||||||
|
<RiDeleteBinLine className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Project edit panel for mobile
|
||||||
|
interface ProjectEditPanelProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
projects: ProjectEntry[];
|
||||||
|
onReorder: (fromIndex: number, toIndex: number) => void;
|
||||||
|
onEdit: (project: ProjectEntry) => void;
|
||||||
|
onDelete: (project: ProjectEntry) => void;
|
||||||
|
homeDirectory: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProjectEditPanel({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
projects,
|
||||||
|
onReorder,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
homeDirectory,
|
||||||
|
}: ProjectEditPanelProps) {
|
||||||
|
const [localProjects, setLocalProjects] = React.useState(projects);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setLocalProjects(projects);
|
||||||
|
}, [projects, isOpen]);
|
||||||
|
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
distance: 8,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
useSensor(KeyboardSensor, {
|
||||||
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDragEnd = (event: DragEndEvent) => {
|
||||||
|
const { active, over } = event;
|
||||||
|
if (over && active.id !== over.id) {
|
||||||
|
const oldIndex = localProjects.findIndex((p) => p.id === active.id);
|
||||||
|
const newIndex = localProjects.findIndex((p) => p.id === over.id);
|
||||||
|
|
||||||
|
setLocalProjects((items) => arrayMove(items, oldIndex, newIndex));
|
||||||
|
onReorder(oldIndex, newIndex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMoveUp = (index: number) => {
|
||||||
|
if (index > 0) {
|
||||||
|
setLocalProjects((items) => arrayMove(items, index, index - 1));
|
||||||
|
onReorder(index, index - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMoveDown = (index: number) => {
|
||||||
|
if (index < localProjects.length - 1) {
|
||||||
|
setLocalProjects((items) => arrayMove(items, index, index + 1));
|
||||||
|
onReorder(index, index + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatProjectLabel = (project: ProjectEntry): string => {
|
||||||
|
return project.label?.trim()
|
||||||
|
|| formatDirectoryName(project.path, homeDirectory)
|
||||||
|
|| project.path;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MobileOverlayPanel
|
||||||
|
open={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
title="Edit Projects"
|
||||||
|
footer={
|
||||||
|
<p className="text-xs text-[var(--surface-mutedForeground)] text-center">
|
||||||
|
Drag items to reorder, or use arrows to move. Tap edit to change details.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
>
|
||||||
|
<SortableContext
|
||||||
|
items={localProjects.map((p) => p.id)}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
|
{localProjects.map((project, index) => (
|
||||||
|
<SortableProjectItem
|
||||||
|
key={project.id}
|
||||||
|
project={project}
|
||||||
|
isFirst={index === 0}
|
||||||
|
isLast={index === localProjects.length - 1}
|
||||||
|
onMoveUp={() => handleMoveUp(index)}
|
||||||
|
onMoveDown={() => handleMoveDown(index)}
|
||||||
|
onEdit={() => onEdit(project)}
|
||||||
|
onDelete={() => onDelete(project)}
|
||||||
|
formatProjectLabel={formatProjectLabel}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
|
||||||
|
{localProjects.length === 0 && (
|
||||||
|
<div className="text-center py-8 text-[var(--surface-mutedForeground)]">
|
||||||
|
No projects to edit
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</MobileOverlayPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Project button component with long press support
|
// Project button component with long press support
|
||||||
interface ProjectButtonProps {
|
interface ProjectButtonProps {
|
||||||
project: ProjectEntry;
|
project: ProjectEntry;
|
||||||
@@ -533,7 +839,7 @@ interface ProjectButtonProps {
|
|||||||
status: { hasRunning: boolean; hasUnread: boolean };
|
status: { hasRunning: boolean; hasUnread: boolean };
|
||||||
projectColorVar: string | null;
|
projectColorVar: string | null;
|
||||||
onProjectSwitch: () => void;
|
onProjectSwitch: () => void;
|
||||||
onRemoveProject?: () => void;
|
onOpenEditPanel?: () => void;
|
||||||
formatProjectLabel: (project: ProjectEntry) => string;
|
formatProjectLabel: (project: ProjectEntry) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,7 +849,7 @@ function ProjectButton({
|
|||||||
status,
|
status,
|
||||||
projectColorVar,
|
projectColorVar,
|
||||||
onProjectSwitch,
|
onProjectSwitch,
|
||||||
onRemoveProject,
|
onOpenEditPanel,
|
||||||
formatProjectLabel,
|
formatProjectLabel,
|
||||||
}: ProjectButtonProps) {
|
}: ProjectButtonProps) {
|
||||||
const [imageFailed, setImageFailed] = React.useState(false);
|
const [imageFailed, setImageFailed] = React.useState(false);
|
||||||
@@ -556,8 +862,8 @@ function ProjectButton({
|
|||||||
|
|
||||||
const longPressHandlers = useLongPress(
|
const longPressHandlers = useLongPress(
|
||||||
() => {
|
() => {
|
||||||
if (onRemoveProject) {
|
if (onOpenEditPanel) {
|
||||||
onRemoveProject();
|
onOpenEditPanel();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onProjectSwitch,
|
onProjectSwitch,
|
||||||
@@ -639,8 +945,10 @@ function ProjectBar({
|
|||||||
homeDirectory
|
homeDirectory
|
||||||
}: ProjectBarProps) {
|
}: ProjectBarProps) {
|
||||||
const scrollRef = React.useRef<HTMLDivElement>(null);
|
const scrollRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const [editPanelOpen, setEditPanelOpen] = React.useState(false);
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);
|
||||||
const [projectToDelete, setProjectToDelete] = React.useState<ProjectEntry | null>(null);
|
const [projectToDelete, setProjectToDelete] = React.useState<ProjectEntry | null>(null);
|
||||||
|
const reorderProjects = useProjectsStore((state) => state.reorderProjects);
|
||||||
|
|
||||||
// Scroll active project into view
|
// Scroll active project into view
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -652,7 +960,29 @@ function ProjectBar({
|
|||||||
}
|
}
|
||||||
}, [activeProjectId]);
|
}, [activeProjectId]);
|
||||||
|
|
||||||
const handleLongPress = (project: ProjectEntry) => {
|
const handleOpenEditPanel = () => {
|
||||||
|
setEditPanelOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReorder = (fromIndex: number, toIndex: number) => {
|
||||||
|
reorderProjects(fromIndex, toIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [editingProject, setEditingProject] = React.useState<ProjectEntry | null>(null);
|
||||||
|
const updateProjectMeta = useProjectsStore((state) => state.updateProjectMeta);
|
||||||
|
|
||||||
|
const handleEditProject = (project: ProjectEntry) => {
|
||||||
|
setEditingProject(project);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveProjectEdit = (data: { label: string; icon: string | null; color: string | null; iconBackground: string | null }) => {
|
||||||
|
if (editingProject) {
|
||||||
|
updateProjectMeta(editingProject.id, data);
|
||||||
|
}
|
||||||
|
setEditingProject(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteProject = (project: ProjectEntry) => {
|
||||||
setProjectToDelete(project);
|
setProjectToDelete(project);
|
||||||
setDeleteDialogOpen(true);
|
setDeleteDialogOpen(true);
|
||||||
};
|
};
|
||||||
@@ -737,7 +1067,7 @@ function ProjectBar({
|
|||||||
status={status}
|
status={status}
|
||||||
projectColorVar={projectColorVar}
|
projectColorVar={projectColorVar}
|
||||||
onProjectSwitch={() => onProjectSwitch(project.id)}
|
onProjectSwitch={() => onProjectSwitch(project.id)}
|
||||||
onRemoveProject={onRemoveProject ? () => handleLongPress(project) : undefined}
|
onOpenEditPanel={handleOpenEditPanel}
|
||||||
formatProjectLabel={formatProjectLabel}
|
formatProjectLabel={formatProjectLabel}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -773,6 +1103,34 @@ function ProjectBar({
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Project edit panel */}
|
||||||
|
<ProjectEditPanel
|
||||||
|
isOpen={editPanelOpen}
|
||||||
|
onClose={() => setEditPanelOpen(false)}
|
||||||
|
projects={projects}
|
||||||
|
onReorder={handleReorder}
|
||||||
|
onEdit={handleEditProject}
|
||||||
|
onDelete={handleDeleteProject}
|
||||||
|
homeDirectory={homeDirectory}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Project edit dialog */}
|
||||||
|
{editingProject && (
|
||||||
|
<ProjectEditDialog
|
||||||
|
open={!!editingProject}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) setEditingProject(null);
|
||||||
|
}}
|
||||||
|
projectId={editingProject.id}
|
||||||
|
projectName={editingProject.label || formatDirectoryName(editingProject.path, homeDirectory)}
|
||||||
|
projectPath={editingProject.path}
|
||||||
|
initialIcon={editingProject.icon}
|
||||||
|
initialColor={editingProject.color}
|
||||||
|
initialIconBackground={editingProject.iconBackground}
|
||||||
|
onSave={handleSaveProjectEdit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user