Export: tasks in top-level folder, smart search expansion
- 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 <noreply@paperclip.ing>
This commit is contained in:
parent
cf30ddb924
commit
ef652a2766
2 changed files with 54 additions and 5 deletions
|
|
@ -1758,9 +1758,8 @@ export function companyPortabilityService(db: Db) {
|
||||||
for (const issue of selectedIssueRows) {
|
for (const issue of selectedIssueRows) {
|
||||||
const taskSlug = taskSlugByIssueId.get(issue.id)!;
|
const taskSlug = taskSlugByIssueId.get(issue.id)!;
|
||||||
const projectSlug = issue.projectId ? (projectSlugById.get(issue.projectId) ?? null) : null;
|
const projectSlug = issue.projectId ? (projectSlugById.get(issue.projectId) ?? null) : null;
|
||||||
const taskPath = projectSlug
|
// All tasks go in top-level tasks/ folder, never nested under projects/
|
||||||
? `projects/${projectSlug}/tasks/${taskSlug}/TASK.md`
|
const taskPath = `tasks/${taskSlug}/TASK.md`;
|
||||||
: `tasks/${taskSlug}/TASK.md`;
|
|
||||||
const assigneeSlug = issue.assigneeAgentId ? (idToSlug.get(issue.assigneeAgentId) ?? null) : null;
|
const assigneeSlug = issue.assigneeAgentId ? (idToSlug.get(issue.assigneeAgentId) ?? null) : null;
|
||||||
files[taskPath] = buildMarkdown(
|
files[taskPath] = buildMarkdown(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import type { CompanyPortabilityExportResult } from "@paperclipai/shared";
|
import type { CompanyPortabilityExportResult } from "@paperclipai/shared";
|
||||||
import { useCompany } from "../context/CompanyContext";
|
import { useCompany } from "../context/CompanyContext";
|
||||||
|
|
@ -126,6 +126,27 @@ function filterTree(nodes: FileTreeNode[], query: string): FileTreeNode[] {
|
||||||
.filter((n): n is FileTreeNode => n !== null);
|
.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<string> {
|
||||||
|
const dirs = new Set<string>();
|
||||||
|
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 */
|
/** Sort tree: checked files first, then unchecked */
|
||||||
function sortByChecked(nodes: FileTreeNode[], checkedFiles: Set<string>): FileTreeNode[] {
|
function sortByChecked(nodes: FileTreeNode[], checkedFiles: Set<string>): FileTreeNode[] {
|
||||||
return nodes.map((node) => {
|
return nodes.map((node) => {
|
||||||
|
|
@ -510,6 +531,7 @@ export function CompanyExport() {
|
||||||
const [expandedDirs, setExpandedDirs] = useState<Set<string>>(new Set());
|
const [expandedDirs, setExpandedDirs] = useState<Set<string>>(new Set());
|
||||||
const [checkedFiles, setCheckedFiles] = useState<Set<string>>(new Set());
|
const [checkedFiles, setCheckedFiles] = useState<Set<string>>(new Set());
|
||||||
const [treeSearch, setTreeSearch] = useState("");
|
const [treeSearch, setTreeSearch] = useState("");
|
||||||
|
const savedExpandedRef = useRef<Set<string> | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBreadcrumbs([
|
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() {
|
function handleDownload() {
|
||||||
if (!exportData) return;
|
if (!exportData) return;
|
||||||
downloadTar(exportData, checkedFiles);
|
downloadTar(exportData, checkedFiles);
|
||||||
|
|
@ -729,7 +779,7 @@ export function CompanyExport() {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={treeSearch}
|
value={treeSearch}
|
||||||
onChange={(e) => setTreeSearch(e.target.value)}
|
onChange={(e) => handleSearchChange(e.target.value)}
|
||||||
placeholder="Search files..."
|
placeholder="Search files..."
|
||||||
className="w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
className="w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue