diff --git a/server/src/services/company-portability.ts b/server/src/services/company-portability.ts index 68487364..c8d0df37 100644 --- a/server/src/services/company-portability.ts +++ b/server/src/services/company-portability.ts @@ -580,8 +580,9 @@ function renderYamlBlock(value: unknown, indentLevel: number): string[] { function renderFrontmatter(frontmatter: Record) { const lines: string[] = ["---"]; for (const [key, value] of orderedYamlEntries(frontmatter)) { + // Skip null/undefined values — don't export empty fields + if (value === null || value === undefined) continue; const scalar = - value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" || diff --git a/ui/src/pages/CompanyExport.tsx b/ui/src/pages/CompanyExport.tsx index 3164ac18..69802d33 100644 --- a/ui/src/pages/CompanyExport.tsx +++ b/ui/src/pages/CompanyExport.tsx @@ -20,6 +20,7 @@ import { FolderOpen, Info, Package, + Search, } from "lucide-react"; // ── Tree types ──────────────────────────────────────────────────────── @@ -64,7 +65,8 @@ function buildFileTree(files: Record): FileTreeNode[] { function sortNode(node: FileTreeNode) { node.children.sort((a, b) => { - if (a.kind !== b.kind) return a.kind === "dir" ? -1 : 1; + // Files before directories so PROJECT.md appears above tasks/ + if (a.kind !== b.kind) return a.kind === "file" ? -1 : 1; return a.name.localeCompare(b.name); }); node.children.forEach(sortNode); @@ -100,6 +102,48 @@ function fileIcon(name: string) { return FileText; } +/** Returns true if the path looks like a task file (e.g. tasks/slug/TASK.md or projects/x/tasks/slug/TASK.md) */ +function isTaskPath(filePath: string): boolean { + return /(?:^|\/)tasks\//.test(filePath); +} + +/** Filter tree nodes whose path (or descendant paths) match a search string */ +function filterTree(nodes: FileTreeNode[], query: string): FileTreeNode[] { + if (!query) return nodes; + const lower = query.toLowerCase(); + return nodes + .map((node) => { + if (node.kind === "file") { + return node.name.toLowerCase().includes(lower) || node.path.toLowerCase().includes(lower) + ? node + : null; + } + const filteredChildren = filterTree(node.children, query); + return filteredChildren.length > 0 + ? { ...node, children: filteredChildren } + : null; + }) + .filter((n): n is FileTreeNode => n !== null); +} + +/** Sort tree: checked files first, then unchecked */ +function sortByChecked(nodes: FileTreeNode[], checkedFiles: Set): FileTreeNode[] { + return nodes.map((node) => { + if (node.kind === "dir") { + return { ...node, children: sortByChecked(node.children, checkedFiles) }; + } + return node; + }).sort((a, b) => { + if (a.kind !== b.kind) return a.kind === "file" ? -1 : 1; + if (a.kind === "file" && b.kind === "file") { + const aChecked = checkedFiles.has(a.path); + const bChecked = checkedFiles.has(b.path); + if (aChecked !== bChecked) return aChecked ? -1 : 1; + } + return a.name.localeCompare(b.name); + }); +} + // ── Tar helpers (reused from CompanySettings) ───────────────────────── function createTarArchive(files: Record, rootPath: string): Uint8Array { @@ -345,6 +389,11 @@ function parseFrontmatter(content: string): { data: FrontmatterData; body: strin if (kvMatch) { const key = kvMatch[1]; const val = kvMatch[2].trim().replace(/^["']|["']$/g, ""); + // Skip null values + if (val === "null") { + currentKey = null; + continue; + } if (val) { data[key] = val; currentKey = null; @@ -460,6 +509,7 @@ export function CompanyExport() { const [selectedFile, setSelectedFile] = useState(null); const [expandedDirs, setExpandedDirs] = useState>(new Set()); const [checkedFiles, setCheckedFiles] = useState>(new Set()); + const [treeSearch, setTreeSearch] = useState(""); useEffect(() => { setBreadcrumbs([ @@ -476,9 +526,12 @@ export function CompanyExport() { }), onSuccess: (result) => { setExportData(result); - // Check all files by default - const allFiles = new Set(Object.keys(result.files)); - setCheckedFiles(allFiles); + // Check all files EXCEPT tasks by default + const checked = new Set(); + for (const filePath of Object.keys(result.files)) { + if (!isTaskPath(filePath)) checked.add(filePath); + } + setCheckedFiles(checked); // Expand top-level dirs const tree = buildFileTree(result.files); const topDirs = new Set(); @@ -512,6 +565,12 @@ export function CompanyExport() { [exportData], ); + const displayTree = useMemo(() => { + let result = tree; + if (treeSearch) result = filterTree(result, treeSearch); + return sortByChecked(result, checkedFiles); + }, [tree, treeSearch, checkedFiles]); + const totalFiles = useMemo(() => countFiles(tree), [tree]); const selectedCount = checkedFiles.size; @@ -656,25 +715,39 @@ export function CompanyExport() { )} {/* Two-column layout */} -
-