diff --git a/ui/src/lib/company-export-selection.test.ts b/ui/src/lib/company-export-selection.test.ts new file mode 100644 index 000000000..91828e4aa --- /dev/null +++ b/ui/src/lib/company-export-selection.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { buildInitialExportCheckedFiles } from "./company-export-selection"; + +describe("buildInitialExportCheckedFiles", () => { + it("checks non-task files and recurring task packages by default", () => { + const checked = buildInitialExportCheckedFiles( + [ + "README.md", + ".paperclip.yaml", + "tasks/one-off/TASK.md", + "tasks/recurring/TASK.md", + "tasks/recurring/notes.md", + ], + [ + { path: "tasks/one-off/TASK.md", recurring: false }, + { path: "tasks/recurring/TASK.md", recurring: true }, + ], + new Set(), + ); + + expect(Array.from(checked).sort()).toEqual([ + ".paperclip.yaml", + "README.md", + "tasks/recurring/TASK.md", + "tasks/recurring/notes.md", + ]); + }); + + it("preserves previous manual selections for one-time tasks", () => { + const checked = buildInitialExportCheckedFiles( + ["README.md", "tasks/one-off/TASK.md"], + [{ path: "tasks/one-off/TASK.md", recurring: false }], + new Set(["tasks/one-off/TASK.md"]), + ); + + expect(Array.from(checked).sort()).toEqual([ + "README.md", + "tasks/one-off/TASK.md", + ]); + }); +}); diff --git a/ui/src/lib/company-export-selection.ts b/ui/src/lib/company-export-selection.ts new file mode 100644 index 000000000..2b4d59be8 --- /dev/null +++ b/ui/src/lib/company-export-selection.ts @@ -0,0 +1,56 @@ +import type { CompanyPortabilityIssueManifestEntry } from "@paperclipai/shared"; + +function isTaskPath(filePath: string): boolean { + return /(?:^|\/)tasks\//.test(filePath); +} + +function buildRecurringTaskPrefixes( + issues: Array>, +): Set { + const prefixes = new Set(); + + for (const issue of issues) { + if (!issue.recurring) continue; + + const filePath = issue.path.trim(); + if (!filePath) continue; + + prefixes.add(filePath); + + const lastSlash = filePath.lastIndexOf("/"); + if (lastSlash >= 0) { + prefixes.add(`${filePath.slice(0, lastSlash + 1)}`); + } + } + + return prefixes; +} + +function isRecurringTaskFile(filePath: string, recurringTaskPrefixes: Set): boolean { + for (const prefix of recurringTaskPrefixes) { + if (filePath === prefix || filePath.startsWith(prefix)) return true; + } + return false; +} + +export function buildInitialExportCheckedFiles( + filePaths: string[], + issues: Array>, + previousCheckedFiles: Set, +): Set { + const next = new Set(); + const recurringTaskPrefixes = buildRecurringTaskPrefixes(issues); + + for (const filePath of filePaths) { + if (previousCheckedFiles.has(filePath)) { + next.add(filePath); + continue; + } + + if (!isTaskPath(filePath) || isRecurringTaskFile(filePath, recurringTaskPrefixes)) { + next.add(filePath); + } + } + + return next; +} diff --git a/ui/src/pages/CompanyExport.tsx b/ui/src/pages/CompanyExport.tsx index e0a3f4e7f..298785d9d 100644 --- a/ui/src/pages/CompanyExport.tsx +++ b/ui/src/pages/CompanyExport.tsx @@ -17,6 +17,7 @@ import { PageSkeleton } from "../components/PageSkeleton"; import { MarkdownBody } from "../components/MarkdownBody"; import { cn } from "../lib/utils"; import { createZipArchive } from "../lib/zip"; +import { buildInitialExportCheckedFiles } from "../lib/company-export-selection"; import { getPortableFileDataUrl, getPortableFileText, isPortableImageFile } from "../lib/portable-files"; import { Download, @@ -34,11 +35,6 @@ import { PackageFileTree, } from "../components/PackageFileTree"; -/** 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); -} - /** * Extract the set of agent/project/task slugs that are "checked" based on * which file paths are in the checked set. @@ -588,14 +584,13 @@ export function CompanyExport() { }), onSuccess: (result) => { setExportData(result); - setCheckedFiles((prev) => { - const next = new Set(); - for (const filePath of Object.keys(result.files)) { - if (prev.has(filePath)) next.add(filePath); - else if (!isTaskPath(filePath)) next.add(filePath); - } - return next; - }); + setCheckedFiles((prev) => + buildInitialExportCheckedFiles( + Object.keys(result.files), + result.manifest.issues, + prev, + ), + ); // Expand top-level dirs (except tasks — collapsed by default) const tree = buildFileTree(result.files); const topDirs = new Set();