feat: enhance OneDrive file creation tool by adding parent folder selection and handling expired accounts

This commit is contained in:
Anish Sarkar 2026-03-29 05:08:04 +05:30
parent c325f53941
commit 5d467cdfc2
2 changed files with 160 additions and 30 deletions

View file

@ -109,7 +109,32 @@ def create_create_onedrive_file_tool(
"connector_type": "onedrive", "connector_type": "onedrive",
} }
context = {"accounts": accounts} parent_folders: dict[int, list[dict[str, str]]] = {}
for acc in accounts:
cid = acc["id"]
if acc.get("auth_expired"):
parent_folders[cid] = []
continue
try:
client = OneDriveClient(session=db_session, connector_id=cid)
items, err = await client.list_children("root")
if err:
logger.warning("Failed to list folders for connector %s: %s", cid, err)
parent_folders[cid] = []
else:
parent_folders[cid] = [
{"folder_id": item["id"], "name": item["name"]}
for item in items
if item.get("folder") is not None and item.get("id") and item.get("name")
]
except Exception:
logger.warning("Error fetching folders for connector %s", cid, exc_info=True)
parent_folders[cid] = []
context: dict[str, Any] = {
"accounts": accounts,
"parent_folders": parent_folders,
}
approval = interrupt( approval = interrupt(
{ {

View file

@ -35,6 +35,7 @@ interface InterruptResult {
}>; }>;
context?: { context?: {
accounts?: OneDriveAccount[]; accounts?: OneDriveAccount[];
parent_folders?: Record<number, Array<{ folder_id: string; name: string }>>;
error?: string; error?: string;
}; };
} }
@ -107,6 +108,7 @@ function ApprovalCard({
const accounts = interruptData.context?.accounts ?? []; const accounts = interruptData.context?.accounts ?? [];
const validAccounts = accounts.filter((a) => !a.auth_expired); const validAccounts = accounts.filter((a) => !a.auth_expired);
const expiredAccounts = accounts.filter((a) => a.auth_expired);
const defaultAccountId = useMemo(() => { const defaultAccountId = useMemo(() => {
if (validAccounts.length === 1) return String(validAccounts[0].id); if (validAccounts.length === 1) return String(validAccounts[0].id);
@ -114,6 +116,18 @@ function ApprovalCard({
}, [validAccounts]); }, [validAccounts]);
const [selectedAccountId, setSelectedAccountId] = useState<string>(defaultAccountId); const [selectedAccountId, setSelectedAccountId] = useState<string>(defaultAccountId);
const [parentFolderId, setParentFolderId] = useState<string>("__root__");
const parentFolders = interruptData.context?.parent_folders ?? {};
const availableParentFolders = useMemo(() => {
if (!selectedAccountId) return [];
return parentFolders[Number(selectedAccountId)] ?? [];
}, [selectedAccountId, parentFolders]);
const handleAccountChange = useCallback((value: string) => {
setSelectedAccountId(value);
setParentFolderId("__root__");
}, []);
const isNameValid = useMemo(() => { const isNameValid = useMemo(() => {
const name = pendingEdits?.name ?? args.name; const name = pendingEdits?.name ?? args.name;
@ -138,10 +152,23 @@ function ApprovalCard({
...args, ...args,
...(pendingEdits && { name: pendingEdits.name, content: pendingEdits.content }), ...(pendingEdits && { name: pendingEdits.name, content: pendingEdits.content }),
connector_id: selectedAccountId ? Number(selectedAccountId) : null, connector_id: selectedAccountId ? Number(selectedAccountId) : null,
parent_folder_id: parentFolderId === "__root__" ? null : parentFolderId,
}, },
}, },
}); });
}, [phase, isPanelOpen, canApprove, allowedDecisions, pendingEdits, setProcessing, onDecision, interruptData, args, selectedAccountId]); }, [
phase,
setProcessing,
isPanelOpen,
canApprove,
allowedDecisions,
onDecision,
interruptData,
args,
selectedAccountId,
parentFolderId,
pendingEdits,
]);
useEffect(() => { useEffect(() => {
const handler = (e: KeyboardEvent) => { const handler = (e: KeyboardEvent) => {
@ -153,56 +180,134 @@ function ApprovalCard({
return ( return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300"> <div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300">
{/* Header */}
<div className="flex items-start justify-between px-5 pt-5 pb-4 select-none"> <div className="flex items-start justify-between px-5 pt-5 pb-4 select-none">
<div> <div>
<p className="text-sm font-semibold text-foreground"> <p className="text-sm font-semibold text-foreground">
{phase === "rejected" ? "OneDrive File Rejected" : phase === "processing" || phase === "complete" ? "OneDrive File Approved" : "Create OneDrive File"} {phase === "rejected"
? "Word Document Rejected"
: phase === "processing" || phase === "complete"
? "Word Document Approved"
: "Create Word Document"}
</p> </p>
{phase === "processing" ? ( {phase === "processing" ? (
<TextShimmerLoader text={pendingEdits ? "Creating file with your changes" : "Creating file"} size="sm" /> <TextShimmerLoader
text={pendingEdits ? "Creating file with your changes" : "Creating file"}
size="sm"
/>
) : phase === "complete" ? ( ) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">{pendingEdits ? "File created with your changes" : "File created"}</p> <p className="text-xs text-muted-foreground mt-0.5">
{pendingEdits ? "File created with your changes" : "File created"}
</p>
) : phase === "rejected" ? ( ) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">File creation was cancelled</p> <p className="text-xs text-muted-foreground mt-0.5">File creation was cancelled</p>
) : ( ) : (
<p className="text-xs text-muted-foreground mt-0.5">Requires your approval to proceed</p> <p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
</p>
)} )}
</div> </div>
{phase === "pending" && canEdit && ( {phase === "pending" && canEdit && (
<Button size="sm" variant="ghost" className="rounded-lg text-muted-foreground -mt-1 -mr-2" onClick={() => { <Button
setIsPanelOpen(true); size="sm"
openHitlEditPanel({ variant="ghost"
title: pendingEdits?.name ?? args.name ?? "", className="rounded-lg text-muted-foreground -mt-1 -mr-2"
content: pendingEdits?.content ?? args.content ?? "", onClick={() => {
toolName: "OneDrive File", setIsPanelOpen(true);
onSave: (newName, newContent) => { setIsPanelOpen(false); setPendingEdits({ name: newName, content: newContent }); }, openHitlEditPanel({
onClose: () => setIsPanelOpen(false), title: pendingEdits?.name ?? args.name ?? "",
}); content: pendingEdits?.content ?? args.content ?? "",
}}> toolName: "Word Document",
<Pen className="size-3.5" /> Edit onSave: (newName, newContent) => {
setIsPanelOpen(false);
setPendingEdits({ name: newName, content: newContent });
},
onClose: () => setIsPanelOpen(false),
});
}}
>
<Pen className="size-3.5" />
Edit
</Button> </Button>
)} )}
</div> </div>
{/* Context section — pickers in pending */}
{phase === "pending" && interruptData.context && ( {phase === "pending" && interruptData.context && (
<> <>
<div className="mx-5 h-px bg-border/50" /> <div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 space-y-4 select-none"> <div className="px-5 py-4 space-y-4 select-none">
{interruptData.context.error ? ( {interruptData.context.error ? (
<p className="text-sm text-destructive">{interruptData.context.error}</p> <p className="text-sm text-destructive">{interruptData.context.error}</p>
) : accounts.length > 0 ? ( ) : (
<div className="space-y-2"> <>
<p className="text-xs font-medium text-muted-foreground">OneDrive Account <span className="text-destructive">*</span></p> {accounts.length > 0 && (
<Select value={selectedAccountId} onValueChange={setSelectedAccountId}> <div className="space-y-2">
<SelectTrigger className="w-full"><SelectValue placeholder="Select an account" /></SelectTrigger> <p className="text-xs font-medium text-muted-foreground">
<SelectContent> OneDrive Account <span className="text-destructive">*</span>
{validAccounts.map((account) => ( </p>
<SelectItem key={account.id} value={String(account.id)}>{account.name}</SelectItem> <Select value={selectedAccountId} onValueChange={handleAccountChange}>
))} <SelectTrigger className="w-full">
</SelectContent> <SelectValue placeholder="Select an account" />
</Select> </SelectTrigger>
</div> <SelectContent>
) : null} {validAccounts.map((account) => (
<SelectItem key={account.id} value={String(account.id)}>
{account.name}
</SelectItem>
))}
{expiredAccounts.map((a) => (
<div
key={a.id}
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
>
{a.name} (expired, retry after re-auth)
</div>
))}
</SelectContent>
</Select>
</div>
)}
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
File Type
</p>
<Select value="docx" disabled>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="docx">Word Document (.docx)</SelectItem>
</SelectContent>
</Select>
</div>
{selectedAccountId && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">Parent Folder</p>
<Select value={parentFolderId} onValueChange={setParentFolderId}>
<SelectTrigger className="w-full">
<SelectValue placeholder="OneDrive Root" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__root__">OneDrive Root</SelectItem>
{availableParentFolders.map((folder) => (
<SelectItem key={folder.folder_id} value={folder.folder_id}>
{folder.name}
</SelectItem>
))}
</SelectContent>
</Select>
{availableParentFolders.length === 0 && (
<p className="text-xs text-muted-foreground">
No folders found. File will be created at OneDrive root.
</p>
)}
</div>
)}
</>
)}
</div> </div>
</> </>
)} )}