feat: enhance document import functionality and runtime configuration

- Added support for watching local folders in the EmbeddedImportMenu, including a new FolderSync icon.
- Introduced onFolderWatched callback to handle folder watch success.
- Updated runtime configuration hooks to allow optional access outside the RuntimeConfigProvider.
- Improved error handling in ZeroProvider for unauthorized access scenarios.
- Refactored base API service to manage desktop authentication more effectively.
This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-07-06 21:31:35 -07:00
parent 1fd58752a3
commit 8df8565e0a
4 changed files with 60 additions and 5 deletions

View file

@ -5,6 +5,7 @@ import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { import {
FolderInput, FolderInput,
FolderPlus, FolderPlus,
FolderSync,
ListFilter, ListFilter,
Plus, Plus,
Settings2, Settings2,
@ -32,9 +33,12 @@ import type { FolderDisplay } from "@/components/documents/FolderNode";
import { FolderPickerDialog } from "@/components/documents/FolderPickerDialog"; import { FolderPickerDialog } from "@/components/documents/FolderPickerDialog";
import { FolderTreeView } from "@/components/documents/FolderTreeView"; import { FolderTreeView } from "@/components/documents/FolderTreeView";
import { VersionHistoryDialog } from "@/components/documents/version-history"; import { VersionHistoryDialog } from "@/components/documents/version-history";
import { useIsSelfHosted, useRuntimeConfig } from "@/components/providers/runtime-config"; import { useOptionalRuntimeConfig, useRuntimeConfig } from "@/components/providers/runtime-config";
import { EXPORT_FILE_EXTENSIONS } from "@/components/shared/ExportMenuItems"; import { EXPORT_FILE_EXTENSIONS } from "@/components/shared/ExportMenuItems";
import { DEFAULT_EXCLUDE_PATTERNS } from "@/components/sources/FolderWatchDialog"; import {
DEFAULT_EXCLUDE_PATTERNS,
FolderWatchDialog,
} from "@/components/sources/FolderWatchDialog";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@ -187,13 +191,27 @@ export function EmbeddedDocumentsMenu({
* OAuth or open the existing account's config. In anonymous mode, `gate` * OAuth or open the existing account's config. In anonymous mode, `gate`
* intercepts every item to trigger the login flow. * intercepts every item to trigger the login flow.
*/ */
export function EmbeddedImportMenu({ gate }: { gate?: (feature: string) => void }) { export function EmbeddedImportMenu({
gate,
onFolderWatched,
}: {
gate?: (feature: string) => void;
onFolderWatched?: () => void;
}) {
const { openDialog } = useDocumentUploadDialog(); const { openDialog } = useDocumentUploadDialog();
const setImportRequest = useSetAtom(importConnectorRequestAtom); const setImportRequest = useSetAtom(importConnectorRequestAtom);
const selfHosted = useIsSelfHosted(); // Provider is absent on anonymous /free pages, where every item is login-gated
// anyway — defaulting to hosted (Composio) there is cosmetic.
const selfHosted = useOptionalRuntimeConfig()?.deploymentMode === "self-hosted";
const { isConnectorEnabled, getConnectorStatusMessage } = useConnectorStatus(); const { isConnectorEnabled, getConnectorStatusMessage } = useConnectorStatus();
const { data: connectors } = useAtomValue(connectorsAtom); const { data: connectors } = useAtomValue(connectorsAtom);
// Watch Local Folder is a desktop-app feature (needs the Electron folder watcher).
const { isDesktop } = usePlatform();
const params = useParams();
const workspaceId = getWorkspaceIdNumber(params) ?? 0;
const [folderWatchOpen, setFolderWatchOpen] = useState(false);
// Native Google Drive connector self-hosted only; hosted deployments use Composio. // Native Google Drive connector self-hosted only; hosted deployments use Composio.
const driveType = selfHosted const driveType = selfHosted
? EnumConnectorName.GOOGLE_DRIVE_CONNECTOR ? EnumConnectorName.GOOGLE_DRIVE_CONNECTOR
@ -223,6 +241,14 @@ export function EmbeddedImportMenu({ gate }: { gate?: (feature: string) => void
<Upload className="h-4 w-4" /> <Upload className="h-4 w-4" />
Upload Files Upload Files
</DropdownMenuItem> </DropdownMenuItem>
{isDesktop && (
<DropdownMenuItem
onSelect={() => (gate ? gate("watch local folders") : setFolderWatchOpen(true))}
>
<FolderSync className="h-4 w-4" />
Watch Local Folder
</DropdownMenuItem>
)}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
{cloudItems.map((item) => { {cloudItems.map((item) => {
const enabled = gate ? true : isConnectorEnabled(item.type); const enabled = gate ? true : isConnectorEnabled(item.type);
@ -297,6 +323,14 @@ export function EmbeddedImportMenu({ gate }: { gate?: (feature: string) => void
); );
})} })}
</DropdownMenuContent> </DropdownMenuContent>
{isDesktop && !gate && (
<FolderWatchDialog
open={folderWatchOpen}
onOpenChange={setFolderWatchOpen}
workspaceId={workspaceId}
onSuccess={onFolderWatched}
/>
)}
</DropdownMenu> </DropdownMenu>
); );
} }
@ -1180,7 +1214,7 @@ function AuthenticatedDocumentsSidebarBase({
contentClassName="px-0" contentClassName="px-0"
persistentAction={ persistentAction={
<div className="flex items-center gap-0.5"> <div className="flex items-center gap-0.5">
<EmbeddedImportMenu /> <EmbeddedImportMenu onFolderWatched={refreshWatchedIds} />
<EmbeddedDocumentsMenu <EmbeddedDocumentsMenu
typeCounts={typeCounts} typeCounts={typeCounts}
activeTypes={activeTypes} activeTypes={activeTypes}

View file

@ -35,6 +35,13 @@ async function fetchZeroContext(isDesktop: boolean): Promise<LoadedZeroContext |
const response = await authenticatedFetch(buildBackendUrl("/zero/context"), { const response = await authenticatedFetch(buildBackendUrl("/zero/context"), {
skipAuthRedirect: true, skipAuthRedirect: true,
}); });
if (response.status === 401) {
// Auth is dead (refresh already failed inside authenticatedFetch). This
// provider gates the whole app tree, so nothing below it (e.g.
// DashboardShell) can run its own redirect — do it here.
handleUnauthorized();
return null;
}
if (!response.ok) return null; if (!response.ok) return null;
return { return {
@ -234,6 +241,12 @@ function ZeroClientProvider({
function WebZeroProvider({ children }: { children: React.ReactNode }) { function WebZeroProvider({ children }: { children: React.ReactNode }) {
const session = useSession(); const session = useSession();
// Same reasoning as fetchZeroContext: this provider blocks the whole tree,
// so the login redirect must happen here, not in a child that never mounts.
useEffect(() => {
if (session.status === "unauthenticated") handleUnauthorized();
}, [session.status]);
if (session.status !== "authenticated") { if (session.status !== "authenticated") {
return null; return null;
} }

View file

@ -31,6 +31,11 @@ export function useRuntimeConfig() {
return context; return context;
} }
/** For components that may render outside RuntimeConfigProvider (e.g. anonymous /free pages). */
export function useOptionalRuntimeConfig(): RuntimeConfigValue | null {
return useContext(RuntimeConfigContext);
}
export function useIsLocalAuth() { export function useIsLocalAuth() {
return useRuntimeConfig().authType === "LOCAL"; return useRuntimeConfig().authType === "LOCAL";
} }

View file

@ -108,6 +108,9 @@ class BaseApiService {
const refreshRetryKey = getRefreshRetryKey(mergedOptions.method, url); const refreshRetryKey = getRefreshRetryKey(mergedOptions.method, url);
if (this.isDesktopClient && !desktopAccessToken && !isNoAuthEndpoint) { if (this.isDesktopClient && !desktopAccessToken && !isNoAuthEndpoint) {
// Desktop refresh token is gone/revoked — send the user to /desktop/login
// (same treatment as a server 401 below) instead of erroring in place.
handleUnauthorized();
throw new AuthenticationError("You are not authenticated. Please login again."); throw new AuthenticationError("You are not authenticated. Please login again.");
} }