From ef652a2766751de95b47081d328aab25be01d9af Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 15 Mar 2026 16:39:11 -0500 Subject: [PATCH] Export: tasks in top-level folder, smart search expansion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move all tasks to top-level tasks/ folder (no longer nested under projects/slug/tasks/). The project slug is still in the frontmatter for association. - Search auto-expands parent dirs of matched files so matches are always visible in the tree - Restores previous expansion state when search is cleared - All files already loaded in memory — search works across everything with no pagination limit Co-Authored-By: Paperclip --- server/src/services/company-portability.ts | 5 +- ui/src/pages/CompanyExport.tsx | 54 +++++++++++++++++++++- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/server/src/services/company-portability.ts b/server/src/services/company-portability.ts index c8d0df37..57e20489 100644 --- a/server/src/services/company-portability.ts +++ b/server/src/services/company-portability.ts @@ -1758,9 +1758,8 @@ export function companyPortabilityService(db: Db) { for (const issue of selectedIssueRows) { const taskSlug = taskSlugByIssueId.get(issue.id)!; const projectSlug = issue.projectId ? (projectSlugById.get(issue.projectId) ?? null) : null; - const taskPath = projectSlug - ? `projects/${projectSlug}/tasks/${taskSlug}/TASK.md` - : `tasks/${taskSlug}/TASK.md`; + // All tasks go in top-level tasks/ folder, never nested under projects/ + const taskPath = `tasks/${taskSlug}/TASK.md`; const assigneeSlug = issue.assigneeAgentId ? (idToSlug.get(issue.assigneeAgentId) ?? null) : null; files[taskPath] = buildMarkdown( { diff --git a/ui/src/pages/CompanyExport.tsx b/ui/src/pages/CompanyExport.tsx index 69802d33..2b963bb2 100644 --- a/ui/src/pages/CompanyExport.tsx +++ b/ui/src/pages/CompanyExport.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useMutation } from "@tanstack/react-query"; import type { CompanyPortabilityExportResult } from "@paperclipai/shared"; import { useCompany } from "../context/CompanyContext"; @@ -126,6 +126,27 @@ function filterTree(nodes: FileTreeNode[], query: string): FileTreeNode[] { .filter((n): n is FileTreeNode => n !== null); } +/** Collect all ancestor dir paths for files that match a filter */ +function collectMatchedParentDirs(nodes: FileTreeNode[], query: string): Set { + const dirs = new Set(); + const lower = query.toLowerCase(); + + function walk(node: FileTreeNode, ancestors: string[]) { + if (node.kind === "file") { + if (node.name.toLowerCase().includes(lower) || node.path.toLowerCase().includes(lower)) { + for (const a of ancestors) dirs.add(a); + } + } else { + for (const child of node.children) { + walk(child, [...ancestors, node.path]); + } + } + } + + for (const node of nodes) walk(node, []); + return dirs; +} + /** Sort tree: checked files first, then unchecked */ function sortByChecked(nodes: FileTreeNode[], checkedFiles: Set): FileTreeNode[] { return nodes.map((node) => { @@ -510,6 +531,7 @@ export function CompanyExport() { const [expandedDirs, setExpandedDirs] = useState>(new Set()); const [checkedFiles, setCheckedFiles] = useState>(new Set()); const [treeSearch, setTreeSearch] = useState(""); + const savedExpandedRef = useRef | null>(null); useEffect(() => { setBreadcrumbs([ @@ -634,6 +656,34 @@ export function CompanyExport() { }); } + function handleSearchChange(query: string) { + const wasSearching = treeSearch.length > 0; + const isSearching = query.length > 0; + + if (isSearching && !wasSearching) { + // Save current expansion state before search + savedExpandedRef.current = new Set(expandedDirs); + } + + setTreeSearch(query); + + if (isSearching) { + // Expand all parent dirs of matched files + const matchedParents = collectMatchedParentDirs(tree, query); + setExpandedDirs((prev) => { + const next = new Set(prev); + for (const d of matchedParents) next.add(d); + return next; + }); + } else if (wasSearching) { + // Restore pre-search expansion state + if (savedExpandedRef.current) { + setExpandedDirs(savedExpandedRef.current); + savedExpandedRef.current = null; + } + } + } + function handleDownload() { if (!exportData) return; downloadTar(exportData, checkedFiles); @@ -729,7 +779,7 @@ export function CompanyExport() { setTreeSearch(e.target.value)} + onChange={(e) => handleSearchChange(e.target.value)} placeholder="Search files..." className="w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground" />