mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
fix(folders): address review feedback
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
db41261452
commit
74dadc5a2c
6 files changed
+97
-12
No files matched your search
@@ -12,6 +12,7 @@ import {
|
||||
documentRevisions,
|
||||
documents,
|
||||
executionWorkspaces,
|
||||
folders,
|
||||
heartbeatRuns,
|
||||
instanceSettings,
|
||||
issueInboxArchives,
|
||||
@@ -68,6 +69,7 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => {
|
||||
await db.delete(routineRuns);
|
||||
await db.delete(routineTriggers);
|
||||
await db.delete(routines);
|
||||
await db.delete(folders);
|
||||
await db.delete(routineDocuments);
|
||||
await db.delete(documents);
|
||||
await db.delete(documentRevisions);
|
||||
@@ -271,6 +273,38 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => {
|
||||
expect(allRoutines.map((entry) => entry.id)).toEqual(expect.arrayContaining([routine.id, otherRoutine.id]));
|
||||
});
|
||||
|
||||
it("does not reveal folders owned by another company", async () => {
|
||||
const { companyId, agentId, projectId, svc } = await seedFixture();
|
||||
const otherCompanyId = randomUUID();
|
||||
await db.insert(companies).values({
|
||||
id: otherCompanyId,
|
||||
name: "Other company",
|
||||
issuePrefix: `T${otherCompanyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
defaultResponsibleUserId: randomUUID(),
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
const [otherFolder] = await db.insert(folders).values({
|
||||
companyId: otherCompanyId,
|
||||
kind: "routine",
|
||||
name: "Private folder",
|
||||
position: 0,
|
||||
}).returning();
|
||||
|
||||
await expect(svc.create(companyId, {
|
||||
projectId,
|
||||
folderId: otherFolder!.id,
|
||||
goalId: null,
|
||||
parentIssueId: null,
|
||||
title: "cross-company folder probe",
|
||||
description: null,
|
||||
assigneeAgentId: agentId,
|
||||
priority: "medium",
|
||||
status: "active",
|
||||
concurrencyPolicy: "coalesce_if_active",
|
||||
catchUpPolicy: "skip_missed",
|
||||
}, {})).rejects.toMatchObject({ status: 404, message: "Folder not found" });
|
||||
});
|
||||
|
||||
it("defaults activity gates to always at company scope", async () => {
|
||||
const { routine } = await seedFixture();
|
||||
|
||||
|
||||
@@ -962,12 +962,11 @@ export function routineService(
|
||||
async function assertRoutineFolder(companyId: string, folderId: string | null | undefined) {
|
||||
if (!folderId) return;
|
||||
const folder = await db
|
||||
.select({ id: folders.id, companyId: folders.companyId, kind: folders.kind })
|
||||
.select({ id: folders.id, kind: folders.kind })
|
||||
.from(folders)
|
||||
.where(eq(folders.id, folderId))
|
||||
.where(and(eq(folders.companyId, companyId), eq(folders.id, folderId)))
|
||||
.then((rows) => rows[0] ?? null);
|
||||
if (!folder) throw notFound("Folder not found");
|
||||
if (folder.companyId !== companyId) throw unprocessable("Folder must belong to same company");
|
||||
if (folder.kind !== "routine") throw unprocessable("Folder kind must match routine");
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { FolderListResult } from "@paperclipai/shared";
|
||||
import {
|
||||
AllUnfiledBanner,
|
||||
BulkBar,
|
||||
DeleteFolderDialog,
|
||||
FolderFormDialog,
|
||||
FolderRail,
|
||||
|
||||
@@ -59,7 +59,7 @@ export const FOLDER_COLORS = [
|
||||
"slate",
|
||||
];
|
||||
|
||||
const FOLDER_COLOR_VALUES: Record<string, string> = {
|
||||
const FOLDER_COLOR_VALUES: Record<(typeof FOLDER_COLORS)[number], string> = {
|
||||
indigo: "var(--folder-color-indigo)",
|
||||
violet: "var(--folder-color-violet)",
|
||||
emerald: "var(--folder-color-emerald)",
|
||||
@@ -469,6 +469,32 @@ export function MoveToMenu({
|
||||
currentFolderId: string | null | undefined;
|
||||
onMove: (folderId: string | null) => void;
|
||||
onCreateAndMove: () => void;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>Move to...</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="w-56">
|
||||
<MoveToMenuItems
|
||||
folders={folders}
|
||||
currentFolderId={currentFolderId}
|
||||
onMove={onMove}
|
||||
onCreateAndMove={onCreateAndMove}
|
||||
/>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
);
|
||||
}
|
||||
|
||||
function MoveToMenuItems({
|
||||
folders,
|
||||
currentFolderId,
|
||||
onMove,
|
||||
onCreateAndMove,
|
||||
}: {
|
||||
folders: FolderListItem[];
|
||||
currentFolderId: string | null | undefined;
|
||||
onMove: (folderId: string | null) => void;
|
||||
onCreateAndMove: () => void;
|
||||
}) {
|
||||
const [query, setQuery] = useState("");
|
||||
const visibleFolders = useMemo(() => {
|
||||
@@ -478,9 +504,7 @@ export function MoveToMenu({
|
||||
}, [folders, query]);
|
||||
|
||||
return (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>Move to...</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="w-56">
|
||||
<>
|
||||
<div className="flex items-center gap-2 px-2 py-1.5">
|
||||
<Search className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<input
|
||||
@@ -512,8 +536,7 @@ export function MoveToMenu({
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
New folder...
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -576,7 +599,7 @@ export function FolderFormDialog({
|
||||
"h-7 w-7 rounded-md border border-border",
|
||||
color === swatch && "ring-2 ring-ring ring-offset-2 ring-offset-background",
|
||||
)}
|
||||
style={{ backgroundColor: swatch }}
|
||||
style={{ backgroundColor: FOLDER_COLOR_VALUES[swatch] }}
|
||||
onClick={() => setColor(swatch)}
|
||||
/>
|
||||
))}
|
||||
@@ -671,8 +694,8 @@ export function BulkBar({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" variant="outline">Move to...</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<MoveToMenu
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<MoveToMenuItems
|
||||
folders={folders}
|
||||
currentFolderId={undefined}
|
||||
onMove={onMove}
|
||||
|
||||
@@ -4371,6 +4371,13 @@ export function CompanySkills() {
|
||||
setFolderDialogTarget(null);
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.folders.list(selectedCompanyId!, "skill") });
|
||||
},
|
||||
onError: (error) => {
|
||||
pushToast({
|
||||
tone: "error",
|
||||
title: "Folder save failed",
|
||||
body: error instanceof Error ? error.message : "Failed to update folder.",
|
||||
});
|
||||
},
|
||||
});
|
||||
const deleteFolder = useMutation({
|
||||
mutationFn: (folderId: string) => foldersApi.delete(selectedCompanyId!, folderId),
|
||||
@@ -4383,6 +4390,13 @@ export function CompanySkills() {
|
||||
]);
|
||||
pushToast({ tone: "success", title: "Folder deleted", body: "Skills moved to Unfiled." });
|
||||
},
|
||||
onError: (error) => {
|
||||
pushToast({
|
||||
tone: "error",
|
||||
title: "Folder delete failed",
|
||||
body: error instanceof Error ? error.message : "Failed to delete folder.",
|
||||
});
|
||||
},
|
||||
});
|
||||
const moveSkillToFolder = useMutation({
|
||||
mutationFn: ({ itemId, folderId }: { itemId: string; folderId: string | null }) =>
|
||||
|
||||
@@ -466,6 +466,13 @@ export function Routines() {
|
||||
setFolderDialogTarget(null);
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.folders.list(selectedCompanyId!, "routine") });
|
||||
},
|
||||
onError: (mutationError) => {
|
||||
pushToast({
|
||||
title: "Folder save failed",
|
||||
body: mutationError instanceof Error ? mutationError.message : "Paperclip could not update the folder.",
|
||||
tone: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
const deleteFolder = useMutation({
|
||||
mutationFn: (folderId: string) => foldersApi.delete(selectedCompanyId!, folderId),
|
||||
@@ -478,6 +485,13 @@ export function Routines() {
|
||||
]);
|
||||
pushToast({ title: "Folder deleted", body: "Items moved to Unfiled.", tone: "success" });
|
||||
},
|
||||
onError: (mutationError) => {
|
||||
pushToast({
|
||||
title: "Folder delete failed",
|
||||
body: mutationError instanceof Error ? mutationError.message : "Paperclip could not delete the folder.",
|
||||
tone: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
const moveRoutineToFolder = useMutation({
|
||||
mutationFn: ({ itemId, folderId }: { itemId: string; folderId: string | null }) =>
|
||||
|
||||
Reference in new issue
Block a user