diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index aec38cbc1b..47018936d3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -7,6 +7,7 @@ import Image from 'next/image' import Link from 'next/link' import { useParams, usePathname, useRouter } from 'next/navigation' import { usePostHog } from 'posthog-js/react' +import { flushSync } from 'react-dom' import { Blimp, Button, @@ -101,12 +102,17 @@ import { usePermissionConfig } from '@/hooks/use-permission-config' import { useSettingsNavigation } from '@/hooks/use-settings-navigation' import { useTaskEvents } from '@/hooks/use-task-events' import { SIDEBAR_WIDTH } from '@/stores/constants' +import { useDraftTaskStore } from '@/stores/draft-tasks/store' import { useFolderStore } from '@/stores/folders/store' import { useSearchModalStore } from '@/stores/modals/search/store' import { useSidebarStore } from '@/stores/sidebar/store' const logger = createLogger('Sidebar') +function isPlaceholderTask(id: string): boolean { + return id === 'new' || id.startsWith('draft-') +} + export function SidebarTooltip({ children, label, @@ -197,7 +203,7 @@ const SidebarTaskItem = memo(function SidebarTaskItem({ (isCurrentRoute || isSelected || isMenuOpen) && 'bg-[var(--surface-active)]' )} onClick={(e) => { - if (task.id === 'new') return + if (isPlaceholderTask(task.id)) return if (e.metaKey || e.ctrlKey) return if (e.shiftKey) { e.preventDefault() @@ -209,14 +215,14 @@ const SidebarTaskItem = memo(function SidebarTaskItem({ }) } }} - onContextMenu={task.id !== 'new' ? (e) => onContextMenu(e, task.id) : undefined} - draggable={task.id !== 'new'} - onDragStart={task.id !== 'new' ? handleDragStart : undefined} - onDragEnd={task.id !== 'new' ? handleDragEnd : undefined} + onContextMenu={!isPlaceholderTask(task.id) ? (e) => onContextMenu(e, task.id) : undefined} + draggable={!isPlaceholderTask(task.id)} + onDragStart={!isPlaceholderTask(task.id) ? handleDragStart : undefined} + onDragEnd={!isPlaceholderTask(task.id) ? handleDragEnd : undefined} >
{task.name}
- {task.id !== 'new' && ( + {!isPlaceholderTask(task.id) && (
{isActive && !isCurrentRoute && !isMenuOpen && ( @@ -634,6 +640,11 @@ export const Sidebar = memo(function Sidebar() { [workspaces, workspaceId] ) + const handleNewTaskFromNav = useCallback(() => { + flushSync(() => useDraftTaskStore.getState().createDraft()) + router.push(`/workspace/${workspaceId}/home`) + }, [router, workspaceId]) + const topNavItems = useMemo( () => [ { @@ -641,6 +652,7 @@ export const Sidebar = memo(function Sidebar() { label: 'Home', icon: Home, href: `/workspace/${workspaceId}/home`, + onClick: handleNewTaskFromNav, }, { id: 'search', @@ -649,7 +661,7 @@ export const Sidebar = memo(function Sidebar() { onClick: openSearchModal, }, ], - [workspaceId, openSearchModal] + [workspaceId, openSearchModal, handleNewTaskFromNav] ) const workspaceNavItems = useMemo( @@ -725,24 +737,53 @@ export const Sidebar = memo(function Sidebar() { useTaskEvents(workspaceId) - const tasks = useMemo( - () => - fetchedTasks.length > 0 - ? fetchedTasks.map((t) => ({ - ...t, - href: `/workspace/${workspaceId}/task/${t.id}`, - })) - : [ - { - id: 'new', - name: 'New task', - href: `/workspace/${workspaceId}/home`, - isActive: false, - isUnread: false, - }, - ], - [fetchedTasks, workspaceId] - ) + const draftTaskId = useDraftTaskStore((s) => s.draftTaskId) + const prevFetchedTaskIdsRef = useRef>(new Set(fetchedTasks.map((t) => t.id))) + + useEffect(() => { + const currentIds = new Set(fetchedTasks.map((t) => t.id)) + if (draftTaskId) { + const hasNewTask = fetchedTasks.some((t) => !prevFetchedTaskIdsRef.current.has(t.id)) + if (hasNewTask) { + useDraftTaskStore.getState().removeDraft() + } + } + prevFetchedTaskIdsRef.current = currentIds + }, [draftTaskId, fetchedTasks]) + + const tasks = useMemo(() => { + const mapped = fetchedTasks.map((t) => ({ + ...t, + href: `/workspace/${workspaceId}/task/${t.id}`, + })) + + if (draftTaskId) { + const hasNewTask = fetchedTasks.some((t) => !prevFetchedTaskIdsRef.current.has(t.id)) + if (!hasNewTask) { + mapped.unshift({ + id: draftTaskId, + name: 'New task', + href: `/workspace/${workspaceId}/home`, + isActive: false, + isUnread: false, + updatedAt: new Date(), + }) + } + } + + if (mapped.length === 0) { + mapped.push({ + id: 'new', + name: 'New task', + href: `/workspace/${workspaceId}/home`, + isActive: false, + isUnread: false, + updatedAt: new Date(), + }) + } + + return mapped + }, [fetchedTasks, workspaceId, draftTaskId]) const { data: fetchedTables = [] } = useTablesList(workspaceId) const { data: fetchedFiles = [] } = useWorkspaceFiles(workspaceId) @@ -784,7 +825,10 @@ export const Sidebar = memo(function Sidebar() { [fetchedKnowledgeBases, workspaceId, permissionConfig.hideKnowledgeBaseTab] ) - const taskIds = useMemo(() => tasks.map((t) => t.id).filter((id) => id !== 'new'), [tasks]) + const taskIds = useMemo( + () => tasks.map((t) => t.id).filter((id) => !isPlaceholderTask(id)), + [tasks] + ) const { selectedTasks, handleTaskClick } = useTaskSelection({ taskIds }) @@ -1088,9 +1132,12 @@ export const Sidebar = memo(function Sidebar() { const tasksPrimaryAction = useMemo( () => ({ label: 'New task', - onSelect: () => navigateToPage(`/workspace/${workspaceId}/home`), + onSelect: () => { + flushSync(() => useDraftTaskStore.getState().createDraft()) + router.push(`/workspace/${workspaceId}/home`) + }, }), - [navigateToPage, workspaceId] + [router, workspaceId] ) const workflowsPrimaryAction = useMemo( @@ -1109,10 +1156,10 @@ export const Sidebar = memo(function Sidebar() { [toggleCollapsed] ) - const handleNewTask = useCallback( - () => navigateToPage(`/workspace/${workspaceId}/home`), - [navigateToPage, workspaceId] - ) + const handleNewTask = useCallback(() => { + flushSync(() => useDraftTaskStore.getState().createDraft()) + router.push(`/workspace/${workspaceId}/home`) + }, [router, workspaceId]) const handleSeeMoreTasks = useCallback(() => setVisibleTaskCount((prev) => prev + 5), []) @@ -1453,7 +1500,8 @@ export const Sidebar = memo(function Sidebar() { {tasks.slice(0, visibleTaskCount).map((task) => { const isCurrentRoute = task.id !== 'new' && pathname === task.href const isRenaming = taskFlyoutRename.editingId === task.id - const isSelected = task.id !== 'new' && selectedTasks.has(task.id) + const isSelected = + !isPlaceholderTask(task.id) && selectedTasks.has(task.id) if (isRenaming) { return ( diff --git a/apps/sim/stores/draft-tasks/store.ts b/apps/sim/stores/draft-tasks/store.ts new file mode 100644 index 0000000000..bd1959b8f4 --- /dev/null +++ b/apps/sim/stores/draft-tasks/store.ts @@ -0,0 +1,31 @@ +import { create } from 'zustand' +import { devtools } from 'zustand/middleware' +import { generateShortId } from '@/lib/core/utils/uuid' + +interface DraftTaskState { + /** ID of the current draft task, or null if none exists */ + draftTaskId: string | null + /** Creates a draft task (reuses existing if one exists). Returns the draft ID. */ + createDraft: () => string + /** Removes the current draft task */ + removeDraft: () => void +} + +export const useDraftTaskStore = create()( + devtools( + (set, get) => ({ + draftTaskId: null, + + createDraft: () => { + const existing = get().draftTaskId + if (existing) return existing + const id = `draft-${generateShortId(8)}` + set({ draftTaskId: id }) + return id + }, + + removeDraft: () => set({ draftTaskId: null }), + }), + { name: 'draft-task-store' } + ) +)