From 5f996cf69e47f24fecd54e5d1304fc4d1fbaf4a1 Mon Sep 17 00:00:00 2001 From: ecarjat <6874700+ecarjat@users.noreply.github.com> Date: Fri, 24 Jul 2026 11:46:19 +0200 Subject: [PATCH] feat: add folder-grouped tabular review rows --- .../20260724_02_tabular_folder_rows.sql | 68 +++++ backend/schema.sql | 38 ++- .../integration/tabular.routes.test.ts | 127 ++++++++- backend/src/routes/tabular.ts | 251 +++++++++++++++++- .../src/app/(pages)/tabular-reviews/page.tsx | 2 + .../app/components/assistant/InitialView.tsx | 2 + .../components/projects/ProjectWorkspace.tsx | 2 + frontend/src/app/components/shared/types.ts | 16 +- .../src/app/components/tabular/NewTRModal.tsx | 19 ++ frontend/src/app/lib/mikeApi.ts | 2 + 10 files changed, 510 insertions(+), 17 deletions(-) create mode 100644 backend/migrations/20260724_02_tabular_folder_rows.sql diff --git a/backend/migrations/20260724_02_tabular_folder_rows.sql b/backend/migrations/20260724_02_tabular_folder_rows.sql new file mode 100644 index 000000000..7cbb50314 --- /dev/null +++ b/backend/migrations/20260724_02_tabular_folder_rows.sql @@ -0,0 +1,68 @@ +-- Support one tabular-review row per project folder, with multiple source documents. +alter table public.tabular_reviews + add column if not exists document_grouping text not null default 'document' + check (document_grouping in ('document', 'folder')); + +create table if not exists public.tabular_review_rows ( + id uuid primary key default gen_random_uuid(), + review_id uuid not null references public.tabular_reviews(id) on delete cascade, + label text not null, + row_type text not null check (row_type in ('document', 'folder')), + folder_id uuid references public.project_subfolders(id) on delete set null, + document_id uuid references public.documents(id) on delete set null, + sort_index integer not null default 0, + created_at timestamptz not null default now() +); +create index if not exists idx_tabular_review_rows_review + on public.tabular_review_rows(review_id, sort_index); +alter table public.tabular_review_rows enable row level security; + +create table if not exists public.tabular_review_row_sources ( + row_id uuid not null references public.tabular_review_rows(id) on delete cascade, + document_id uuid not null references public.documents(id) on delete cascade, + sort_index integer not null default 0, + created_at timestamptz not null default now(), + primary key (row_id, document_id) +); +create index if not exists idx_tabular_review_row_sources_document + on public.tabular_review_row_sources(document_id); +alter table public.tabular_review_row_sources enable row level security; + +alter table public.tabular_cells + add column if not exists row_id uuid references public.tabular_review_rows(id) on delete cascade; +alter table public.tabular_cells + alter column document_id drop not null; +create index if not exists idx_tabular_cells_review_row + on public.tabular_cells(review_id, row_id, column_index); + +-- Preserve every existing document row when upgrading an active database. +insert into public.tabular_review_rows (review_id, label, row_type, document_id, sort_index) +select distinct on (cell.review_id, cell.document_id) + cell.review_id, + coalesce(document.filename, 'Untitled document'), + 'document', + cell.document_id, + row_number() over (partition by cell.review_id order by cell.document_id) - 1 +from public.tabular_cells cell +join public.documents document on document.id = cell.document_id +where cell.row_id is null + and not exists ( + select 1 from public.tabular_review_rows row + where row.review_id = cell.review_id and row.document_id = cell.document_id + ); + +insert into public.tabular_review_row_sources (row_id, document_id) +select row.id, row.document_id +from public.tabular_review_rows row +where row.document_id is not null +on conflict (row_id, document_id) do nothing; + +update public.tabular_cells cell +set row_id = row.id +from public.tabular_review_rows row +where cell.row_id is null + and row.review_id = cell.review_id + and row.document_id = cell.document_id; + +revoke all on public.tabular_review_rows from anon, authenticated; +revoke all on public.tabular_review_row_sources from anon, authenticated; diff --git a/backend/schema.sql b/backend/schema.sql index 6819473b9..7f570c5d4 100644 --- a/backend/schema.sql +++ b/backend/schema.sql @@ -588,6 +588,7 @@ create table if not exists public.tabular_reviews ( document_ids jsonb, workflow_id uuid references public.workflows(id) on delete set null, practice text, + document_grouping text not null default 'document' check (document_grouping in ('document', 'folder')), shared_with jsonb not null default '[]'::jsonb, created_at timestamptz not null default now(), updated_at timestamptz not null default now() @@ -680,10 +681,40 @@ as $$ order by vp.created_at desc; $$; -create table if not exists public.tabular_cells ( +create table if not exists public.tabular_review_rows ( id uuid primary key default gen_random_uuid(), review_id uuid not null references public.tabular_reviews(id) on delete cascade, + label text not null, + row_type text not null check (row_type in ('document', 'folder')), + folder_id uuid references public.project_subfolders(id) on delete set null, + document_id uuid references public.documents(id) on delete set null, + sort_index integer not null default 0, + created_at timestamptz not null default now() +); + +create index if not exists idx_tabular_review_rows_review + on public.tabular_review_rows(review_id, sort_index); + +alter table public.tabular_review_rows enable row level security; + +create table if not exists public.tabular_review_row_sources ( + row_id uuid not null references public.tabular_review_rows(id) on delete cascade, document_id uuid not null references public.documents(id) on delete cascade, + sort_index integer not null default 0, + created_at timestamptz not null default now(), + primary key (row_id, document_id) +); + +create index if not exists idx_tabular_review_row_sources_document + on public.tabular_review_row_sources(document_id); + +alter table public.tabular_review_row_sources enable row level security; + +create table if not exists public.tabular_cells ( + id uuid primary key default gen_random_uuid(), + review_id uuid not null references public.tabular_reviews(id) on delete cascade, + row_id uuid references public.tabular_review_rows(id) on delete cascade, + document_id uuid references public.documents(id) on delete cascade, column_index integer not null, content text, citations jsonb, @@ -694,6 +725,9 @@ create table if not exists public.tabular_cells ( create index if not exists idx_tabular_cells_review on public.tabular_cells(review_id, document_id, column_index); +create index if not exists idx_tabular_cells_review_row + on public.tabular_cells(review_id, row_id, column_index); + create or replace function public.get_tabular_reviews_overview( p_user_id text, p_user_email text default null, @@ -875,6 +909,8 @@ revoke all on public.chats from anon, authenticated; revoke all on public.chat_messages from anon, authenticated; revoke all on public.tabular_reviews from anon, authenticated; revoke all on public.tabular_cells from anon, authenticated; +revoke all on public.tabular_review_rows from anon, authenticated; +revoke all on public.tabular_review_row_sources from anon, authenticated; revoke all on public.tabular_review_chats from anon, authenticated; revoke all on public.tabular_review_chat_messages from anon, authenticated; revoke all on public.user_api_keys from anon, authenticated; diff --git a/backend/src/__tests__/integration/tabular.routes.test.ts b/backend/src/__tests__/integration/tabular.routes.test.ts index 6ee9db10c..02ae34567 100644 --- a/backend/src/__tests__/integration/tabular.routes.test.ts +++ b/backend/src/__tests__/integration/tabular.routes.test.ts @@ -178,6 +178,31 @@ describe("tabular.routes", () => { data: { id: "r9", title: "Gamma", document_ids: ["d1"] }, error: null, }; + supabaseState.tables.documents = { + data: [ + { + id: "d1", + filename: "Agreement.pdf", + file_type: "pdf", + folder_id: null, + }, + ], + error: null, + }; + supabaseState.tables.tabular_review_rows = { + data: [ + { + id: "row-1", + review_id: "r9", + label: "Agreement.pdf", + row_type: "document", + folder_id: null, + document_id: "d1", + sort_index: 0, + }, + ], + error: null, + }; // d2 is not accessible — it must be filtered out of the insert. filterAccessibleDocumentIds.mockResolvedValue(["d1"]); @@ -199,13 +224,14 @@ describe("tabular.routes", () => { expect(reviewInsert?.payload).toMatchObject({ document_ids: ["d1"], }); - // Cells are created for accessible docs × columns only (1 × 1). + // Cells are created for accessible review rows × columns only (1 × 1). const cellInsert = supabaseState.inserts.find( (i) => i.table === "tabular_cells", ); expect(cellInsert?.payload).toEqual([ { review_id: "r9", + row_id: "row-1", document_id: "d1", column_index: 0, status: "pending", @@ -213,6 +239,105 @@ describe("tabular.routes", () => { ]); }); + it("groups project-folder documents into one review row", async () => { + supabaseState.tables.tabular_reviews = { + data: { id: "r10", title: "Grouped", document_ids: ["d1", "d2", "d3"] }, + error: null, + }; + supabaseState.tables.documents = { + data: [ + { id: "d1", filename: "A.pdf", file_type: "pdf", folder_id: "f1" }, + { id: "d2", filename: "B.pdf", file_type: "pdf", folder_id: "f1" }, + { id: "d3", filename: "Loose.pdf", file_type: "pdf", folder_id: null }, + ], + error: null, + }; + supabaseState.tables.project_subfolders = { + data: [{ id: "f1", name: "Contracts", parent_folder_id: null }], + error: null, + }; + supabaseState.tables.tabular_review_rows = { + data: [ + { + id: "row-folder", + review_id: "r10", + label: "Contracts", + row_type: "folder", + folder_id: "f1", + document_id: null, + sort_index: 0, + }, + { + id: "row-document", + review_id: "r10", + label: "Loose.pdf", + row_type: "document", + folder_id: null, + document_id: "d3", + sort_index: 1, + }, + ], + error: null, + }; + + const res = await request(app) + .post("/tabular-review") + .set(...AUTH) + .send({ + title: "Grouped", + project_id: "p1", + document_ids: ["d1", "d2", "d3"], + document_grouping: "folder", + columns_config: [{ index: 0, name: "Col", prompt: "p" }], + }); + + expect(res.status).toBe(201); + expect(supabaseState.inserts.find((i) => i.table === "tabular_reviews")?.payload) + .toMatchObject({ document_grouping: "folder" }); + expect(supabaseState.inserts.find((i) => i.table === "tabular_review_rows")?.payload) + .toEqual([ + { + review_id: "r10", + label: "Contracts", + row_type: "folder", + folder_id: "f1", + document_id: null, + sort_index: 0, + }, + { + review_id: "r10", + label: "Loose.pdf", + row_type: "document", + folder_id: null, + document_id: "d3", + sort_index: 1, + }, + ]); + expect(supabaseState.inserts.find((i) => i.table === "tabular_review_row_sources")?.payload) + .toEqual([ + { row_id: "row-folder", document_id: "d1", sort_index: 0 }, + { row_id: "row-folder", document_id: "d2", sort_index: 1 }, + { row_id: "row-document", document_id: "d3", sort_index: 0 }, + ]); + expect(supabaseState.inserts.find((i) => i.table === "tabular_cells")?.payload) + .toEqual([ + { + review_id: "r10", + row_id: "row-folder", + document_id: null, + column_index: 0, + status: "pending", + }, + { + review_id: "r10", + row_id: "row-document", + document_id: "d3", + column_index: 0, + status: "pending", + }, + ]); + }); + it("returns 404 when project access is denied", async () => { checkProjectAccess.mockResolvedValue({ ok: false }); diff --git a/backend/src/routes/tabular.ts b/backend/src/routes/tabular.ts index 27c28c3d4..c45e459d2 100644 --- a/backend/src/routes/tabular.ts +++ b/backend/src/routes/tabular.ts @@ -70,6 +70,207 @@ function formatPromptSuffix(format?: string, tags?: string[]): string { export const tabularRouter = Router(); +type DocumentGrouping = "document" | "folder"; +type ReviewRow = { + id: string; + review_id: string; + label: string; + row_type: "document" | "folder"; + folder_id: string | null; + document_id: string | null; + sort_index: number; + source_document_ids?: string[]; +}; +type SourceDocument = { + id: string; + filename: string; + file_type: string | null; + folder_id?: string | null; + created_at?: string | null; +}; +type SupabaseDb = ReturnType; + +function normalizeGrouping(value: unknown): DocumentGrouping { + return value === "folder" ? "folder" : "document"; +} + +async function fetchSourceDocuments( + db: SupabaseDb, + documentIds: string[], +): Promise { + if (documentIds.length === 0) return []; + const { data } = await db + .from("documents") + .select("id, filename, file_type, folder_id, created_at") + .in("id", documentIds); + const position = new Map(documentIds.map((id, index) => [id, index])); + return ((data ?? []) as SourceDocument[]).sort( + (a, b) => (position.get(a.id) ?? 0) - (position.get(b.id) ?? 0), + ); +} + +async function getFolderPathMap( + db: SupabaseDb, + projectId: string | null | undefined, +): Promise> { + if (!projectId) return new Map(); + const { data } = await db + .from("project_subfolders") + .select("id, name, parent_folder_id") + .eq("project_id", projectId); + const folders = (data ?? []) as { + id: string; + name: string; + parent_folder_id: string | null; + }[]; + const byId = new Map(folders.map((folder) => [folder.id, folder])); + const paths = new Map(); + const resolve = (id: string): string => { + const existing = paths.get(id); + if (existing) return existing; + const folder = byId.get(id); + if (!folder) return "Unknown folder"; + const path = folder.parent_folder_id + ? `${resolve(folder.parent_folder_id)} / ${folder.name}` + : folder.name; + paths.set(id, path); + return path; + }; + for (const folder of folders) resolve(folder.id); + return paths; +} + +async function createRowsForReview( + db: SupabaseDb, + reviewId: string, + projectId: string | null | undefined, + documentIds: string[], + columns: Column[], + grouping: DocumentGrouping, +): Promise { + const docs = await fetchSourceDocuments(db, documentIds); + const folderPaths = await getFolderPathMap(db, projectId); + const inputs: { + label: string; + row_type: "document" | "folder"; + folder_id: string | null; + document_id: string | null; + sourceIds: string[]; + }[] = []; + + if (grouping === "folder" && projectId) { + const byFolder = new Map(); + for (const doc of docs) { + if (!doc.folder_id) { + inputs.push({ + label: doc.filename, + row_type: "document", + folder_id: null, + document_id: doc.id, + sourceIds: [doc.id], + }); + continue; + } + byFolder.set(doc.folder_id, [...(byFolder.get(doc.folder_id) ?? []), doc]); + } + for (const [folderId, folderDocs] of byFolder) { + inputs.push({ + label: folderPaths.get(folderId) ?? "Unknown folder", + row_type: "folder", + folder_id: folderId, + document_id: null, + sourceIds: folderDocs.map((doc) => doc.id), + }); + } + } else { + for (const doc of docs) { + inputs.push({ + label: doc.filename, + row_type: "document", + folder_id: null, + document_id: doc.id, + sourceIds: [doc.id], + }); + } + } + + inputs.sort((a, b) => a.label.localeCompare(b.label)); + const { data, error } = await db + .from("tabular_review_rows") + .insert( + inputs.map((input, sort_index) => ({ + review_id: reviewId, + label: input.label, + row_type: input.row_type, + folder_id: input.folder_id, + document_id: input.document_id, + sort_index, + })), + ) + .select("*"); + if (error) throw new Error(error.message); + const rows = ((data ?? []) as ReviewRow[]).sort( + (a, b) => a.sort_index - b.sort_index, + ); + const sources = rows.flatMap((row) => + (inputs[row.sort_index]?.sourceIds ?? []).map((document_id, sort_index) => ({ + row_id: row.id, + document_id, + sort_index, + })), + ); + if (sources.length) { + const { error: sourceError } = await db + .from("tabular_review_row_sources") + .insert(sources); + if (sourceError) throw new Error(sourceError.message); + } + const cells = rows.flatMap((row) => + columns.map((column) => ({ + review_id: reviewId, + row_id: row.id, + document_id: row.document_id, + column_index: column.index, + status: "pending", + })), + ); + if (cells.length) { + const { error: cellError } = await db.from("tabular_cells").insert(cells); + if (cellError) throw new Error(cellError.message); + } + return rows; +} + +async function loadReviewRows( + db: SupabaseDb, + reviewId: string, +): Promise { + const { data } = await db + .from("tabular_review_rows") + .select("*") + .eq("review_id", reviewId) + .order("sort_index", { ascending: true }); + const rows = (data ?? []) as ReviewRow[]; + if (!rows.length) return rows; + const { data: sources } = await db + .from("tabular_review_row_sources") + .select("row_id, document_id") + .in("row_id", rows.map((row) => row.id)) + .order("sort_index", { ascending: true }); + const byRow = new Map(); + for (const source of sources ?? []) { + byRow.set(source.row_id, [ + ...(byRow.get(source.row_id) ?? []), + source.document_id, + ]); + } + return rows.map((row) => ({ + ...row, + source_document_ids: + byRow.get(row.id) ?? (row.document_id ? [row.document_id] : []), + })); +} + function providerLabel(provider: Provider): string { if (provider === "claude") return "Anthropic"; if (provider === "openai") return "OpenAI"; @@ -111,13 +312,21 @@ tabularRouter.get("/", requireAuth, async (req, res) => { tabularRouter.post("/", requireAuth, async (req, res) => { const userId = res.locals.userId as string; const userEmail = res.locals.userEmail as string | undefined; - const { title, document_ids, columns_config, workflow_id, project_id } = + const { + title, + document_ids, + columns_config, + workflow_id, + project_id, + document_grouping, + } = req.body as { title?: string; document_ids: string[]; columns_config: { index: number; name: string; prompt: string }[]; workflow_id?: string; project_id?: string; + document_grouping?: DocumentGrouping; }; const db = createServerSupabase(); @@ -139,6 +348,7 @@ tabularRouter.post("/", requireAuth, async (req, res) => { db, ) : []; + const grouping = normalizeGrouping(document_grouping); const { data: review, error } = await db .from("tabular_reviews") .insert({ @@ -148,6 +358,7 @@ tabularRouter.post("/", requireAuth, async (req, res) => { document_ids: allowedDocumentIds, project_id: project_id ?? null, workflow_id: workflow_id ?? null, + document_grouping: grouping, }) .select("*") .single(); @@ -156,15 +367,21 @@ tabularRouter.post("/", requireAuth, async (req, res) => { .status(500) .json({ detail: error?.message ?? "Failed to create review" }); - const cells = allowedDocumentIds.flatMap((docId) => - columns_config.map((col) => ({ - review_id: review.id, - document_id: docId, - column_index: col.index, - status: "pending", - })), - ); - if (cells.length) await db.from("tabular_cells").insert(cells); + try { + await createRowsForReview( + db, + review.id, + project_id ?? null, + allowedDocumentIds, + columns_config, + grouping, + ); + } catch (error) { + await db.from("tabular_reviews").delete().eq("id", review.id); + return void res.status(500).json({ + detail: error instanceof Error ? error.message : "Failed to create review rows", + }); + } res.status(201).json(review); }); @@ -262,15 +479,20 @@ tabularRouter.get("/:reviewId", requireAuth, async (req, res) => { .from("tabular_cells") .select("*") .eq("review_id", reviewId); + const rows = await loadReviewRows(db, reviewId); const cellDocIds = [...new Set((cells ?? []).map((c) => c.document_id))]; + const rowDocIds = rows.flatMap( + (row) => row.source_document_ids ?? [], + ); const hasExplicitDocIds = Array.isArray(review.document_ids); const explicitDocIds = hasExplicitDocIds ? (review.document_ids as string[]) : []; - const docIds = - hasExplicitDocIds - ? explicitDocIds - : cellDocIds; + const docIds = hasExplicitDocIds + ? explicitDocIds + : rowDocIds.length + ? rowDocIds + : cellDocIds; const docsResult = docIds.length > 0 ? await db.from("documents").select("*").in("id", docIds) @@ -287,6 +509,7 @@ tabularRouter.get("/:reviewId", requireAuth, async (req, res) => { ...cell, content: parseCellContent(cell.content), })), + rows, documents: docs, }); }); diff --git a/frontend/src/app/(pages)/tabular-reviews/page.tsx b/frontend/src/app/(pages)/tabular-reviews/page.tsx index 99e88c6ed..a4a164a88 100644 --- a/frontend/src/app/(pages)/tabular-reviews/page.tsx +++ b/frontend/src/app/(pages)/tabular-reviews/page.tsx @@ -216,6 +216,7 @@ export default function TabularReviewsPage() { columnsConfig?: | import("@/app/components/shared/types").ColumnConfig[] | null, + documentGrouping?: "document" | "folder", ) => { setCreating(true); try { @@ -223,6 +224,7 @@ export default function TabularReviewsPage() { title, document_ids: documentIds ?? [], columns_config: columnsConfig ?? [], + document_grouping: documentGrouping, ...(projectId && { project_id: projectId }), }); router.push( diff --git a/frontend/src/app/components/assistant/InitialView.tsx b/frontend/src/app/components/assistant/InitialView.tsx index e31b80ddf..de3fa7626 100644 --- a/frontend/src/app/components/assistant/InitialView.tsx +++ b/frontend/src/app/components/assistant/InitialView.tsx @@ -115,11 +115,13 @@ export function InitialView({ onSubmit }: InitialViewProps) { projectId?: string, documentIds?: string[], columnsConfig?: Workflow["columns_config"], + documentGrouping?: "document" | "folder", ) { const review = await createTabularReview({ title, document_ids: documentIds ?? [], columns_config: columnsConfig ?? [], + document_grouping: documentGrouping, ...(projectId && { project_id: projectId }), }); setNewTROpen(false); diff --git a/frontend/src/app/components/projects/ProjectWorkspace.tsx b/frontend/src/app/components/projects/ProjectWorkspace.tsx index 743f6a1e7..b67002e7c 100644 --- a/frontend/src/app/components/projects/ProjectWorkspace.tsx +++ b/frontend/src/app/components/projects/ProjectWorkspace.tsx @@ -294,6 +294,7 @@ export function ProjectWorkspaceProvider({ _projectId?: string, documentIds?: string[], columnsConfig?: ColumnConfig[] | null, + documentGrouping?: "document" | "folder", ) { setCreatingReview(true); try { @@ -303,6 +304,7 @@ export function ProjectWorkspaceProvider({ title: title || undefined, document_ids: documentIds ?? readyDocs.map((d) => d.id), columns_config: columnsConfig ?? [], + document_grouping: documentGrouping, project_id: projectId, }); setProjectReviews((prev) => (prev ? [review, ...prev] : prev)); diff --git a/frontend/src/app/components/shared/types.ts b/frontend/src/app/components/shared/types.ts index 5ef38011d..3d6b33bca 100644 --- a/frontend/src/app/components/shared/types.ts +++ b/frontend/src/app/components/shared/types.ts @@ -555,6 +555,7 @@ export interface TabularReview { title: string | null; columns_config: ColumnConfig[] | null; document_ids?: string[] | null; + document_grouping?: "document" | "folder"; workflow_id: string | null; practice?: string | null; /** Per-review email list. Used so standalone (project_id null) reviews can be shared directly. */ @@ -569,7 +570,8 @@ export interface TabularReview { export interface TabularCell { id: string; review_id: string; - document_id: string; + row_id?: string | null; + document_id: string | null; column_index: number; content: { summary: string; @@ -580,6 +582,17 @@ export interface TabularCell { created_at: string; } +export interface TabularReviewRow { + id: string; + review_id: string; + label: string; + row_type: "document" | "folder"; + folder_id: string | null; + document_id: string | null; + sort_index: number; + source_document_ids: string[]; +} + // Workflows export interface WorkflowOpenSourceSubmission { @@ -637,5 +650,6 @@ export interface ChatDetailOut { export interface TabularReviewDetailOut { review: TabularReview; cells: TabularCell[]; + rows?: TabularReviewRow[]; documents: Document[]; } diff --git a/frontend/src/app/components/tabular/NewTRModal.tsx b/frontend/src/app/components/tabular/NewTRModal.tsx index fd969cbce..191e1b63c 100644 --- a/frontend/src/app/components/tabular/NewTRModal.tsx +++ b/frontend/src/app/components/tabular/NewTRModal.tsx @@ -28,6 +28,7 @@ interface Props { projectId?: string, documentIds?: string[], columnsConfig?: Workflow["columns_config"], + documentGrouping?: "document" | "folder", ) => void; projects?: Project[]; /** When provided, skip the project/directory picker and show only these docs */ @@ -59,6 +60,7 @@ export function NewTRModal({ [], ); const [selectedDocuments, setSelectedDocuments] = useState([]); + const [groupBySubfolder, setGroupBySubfolder] = useState(false); const [uploading, setUploading] = useState(false); const fileInputRef = useRef(null); @@ -115,6 +117,7 @@ export function NewTRModal({ setProjectDocs([]); setExtraStandaloneDocs([]); setSelectedDocuments([]); + setGroupBySubfolder(false); setSelectedWorkflowId(null); onClose(); } @@ -145,6 +148,9 @@ export function NewTRModal({ ? selectedDocuments.map((document) => document.id) : undefined, selectedWorkflow?.columns_config ?? undefined, + groupBySubfolder && (isProjectMode || underProject) + ? "folder" + : "document", ); handleClose(); } @@ -402,6 +408,19 @@ export function NewTRModal({ showTabs={!isProjectMode && !underProject} /> )} + {(isProjectMode || underProject) && ( + + )} )} diff --git a/frontend/src/app/lib/mikeApi.ts b/frontend/src/app/lib/mikeApi.ts index 6dbaa1e6e..5c64fc280 100644 --- a/frontend/src/app/lib/mikeApi.ts +++ b/frontend/src/app/lib/mikeApi.ts @@ -1058,6 +1058,7 @@ export async function createTabularReview(payload: { columns_config: { index: number; name: string; prompt: string }[]; workflow_id?: string; project_id?: string; + document_grouping?: "document" | "folder"; }): Promise { return apiRequest("/tabular-review", { method: "POST", @@ -1079,6 +1080,7 @@ export async function updateTabularReview( columns_config?: { index: number; name: string; prompt: string }[]; document_ids?: string[]; project_id?: string | null; + document_grouping?: "document" | "folder"; shared_with?: string[]; }, ): Promise {