Skip to content

Commit 8df8565

Browse files
committed
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.
1 parent 1fd5875 commit 8df8565

4 files changed

Lines changed: 60 additions & 5 deletions

File tree

surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useAtom, useAtomValue, useSetAtom } from "jotai";
55
import {
66
FolderInput,
77
FolderPlus,
8+
FolderSync,
89
ListFilter,
910
Plus,
1011
Settings2,
@@ -32,9 +33,12 @@ import type { FolderDisplay } from "@/components/documents/FolderNode";
3233
import { FolderPickerDialog } from "@/components/documents/FolderPickerDialog";
3334
import { FolderTreeView } from "@/components/documents/FolderTreeView";
3435
import { VersionHistoryDialog } from "@/components/documents/version-history";
35-
import { useIsSelfHosted, useRuntimeConfig } from "@/components/providers/runtime-config";
36+
import { useOptionalRuntimeConfig, useRuntimeConfig } from "@/components/providers/runtime-config";
3637
import { EXPORT_FILE_EXTENSIONS } from "@/components/shared/ExportMenuItems";
37-
import { DEFAULT_EXCLUDE_PATTERNS } from "@/components/sources/FolderWatchDialog";
38+
import {
39+
DEFAULT_EXCLUDE_PATTERNS,
40+
FolderWatchDialog,
41+
} from "@/components/sources/FolderWatchDialog";
3842
import {
3943
AlertDialog,
4044
AlertDialogAction,
@@ -187,13 +191,27 @@ export function EmbeddedDocumentsMenu({
187191
* OAuth or open the existing account's config. In anonymous mode, `gate`
188192
* intercepts every item to trigger the login flow.
189193
*/
190-
export function EmbeddedImportMenu({ gate }: { gate?: (feature: string) => void }) {
194+
export function EmbeddedImportMenu({
195+
gate,
196+
onFolderWatched,
197+
}: {
198+
gate?: (feature: string) => void;
199+
onFolderWatched?: () => void;
200+
}) {
191201
const { openDialog } = useDocumentUploadDialog();
192202
const setImportRequest = useSetAtom(importConnectorRequestAtom);
193-
const selfHosted = useIsSelfHosted();
203+
// Provider is absent on anonymous /free pages, where every item is login-gated
204+
// anyway — defaulting to hosted (Composio) there is cosmetic.
205+
const selfHosted = useOptionalRuntimeConfig()?.deploymentMode === "self-hosted";
194206
const { isConnectorEnabled, getConnectorStatusMessage } = useConnectorStatus();
195207
const { data: connectors } = useAtomValue(connectorsAtom);
196208

209+
// Watch Local Folder is a desktop-app feature (needs the Electron folder watcher).
210+
const { isDesktop } = usePlatform();
211+
const params = useParams();
212+
const workspaceId = getWorkspaceIdNumber(params) ?? 0;
213+
const [folderWatchOpen, setFolderWatchOpen] = useState(false);
214+
197215
// Native Google Drive connector self-hosted only; hosted deployments use Composio.
198216
const driveType = selfHosted
199217
? EnumConnectorName.GOOGLE_DRIVE_CONNECTOR
@@ -223,6 +241,14 @@ export function EmbeddedImportMenu({ gate }: { gate?: (feature: string) => void
223241
<Upload className="h-4 w-4" />
224242
Upload Files
225243
</DropdownMenuItem>
244+
{isDesktop && (
245+
<DropdownMenuItem
246+
onSelect={() => (gate ? gate("watch local folders") : setFolderWatchOpen(true))}
247+
>
248+
<FolderSync className="h-4 w-4" />
249+
Watch Local Folder
250+
</DropdownMenuItem>
251+
)}
226252
<DropdownMenuSeparator />
227253
{cloudItems.map((item) => {
228254
const enabled = gate ? true : isConnectorEnabled(item.type);
@@ -297,6 +323,14 @@ export function EmbeddedImportMenu({ gate }: { gate?: (feature: string) => void
297323
);
298324
})}
299325
</DropdownMenuContent>
326+
{isDesktop && !gate && (
327+
<FolderWatchDialog
328+
open={folderWatchOpen}
329+
onOpenChange={setFolderWatchOpen}
330+
workspaceId={workspaceId}
331+
onSuccess={onFolderWatched}
332+
/>
333+
)}
300334
</DropdownMenu>
301335
);
302336
}
@@ -1180,7 +1214,7 @@ function AuthenticatedDocumentsSidebarBase({
11801214
contentClassName="px-0"
11811215
persistentAction={
11821216
<div className="flex items-center gap-0.5">
1183-
<EmbeddedImportMenu />
1217+
<EmbeddedImportMenu onFolderWatched={refreshWatchedIds} />
11841218
<EmbeddedDocumentsMenu
11851219
typeCounts={typeCounts}
11861220
activeTypes={activeTypes}

surfsense_web/components/providers/ZeroProvider.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ async function fetchZeroContext(isDesktop: boolean): Promise<LoadedZeroContext |
3535
const response = await authenticatedFetch(buildBackendUrl("/zero/context"), {
3636
skipAuthRedirect: true,
3737
});
38+
if (response.status === 401) {
39+
// Auth is dead (refresh already failed inside authenticatedFetch). This
40+
// provider gates the whole app tree, so nothing below it (e.g.
41+
// DashboardShell) can run its own redirect — do it here.
42+
handleUnauthorized();
43+
return null;
44+
}
3845
if (!response.ok) return null;
3946

4047
return {
@@ -234,6 +241,12 @@ function ZeroClientProvider({
234241
function WebZeroProvider({ children }: { children: React.ReactNode }) {
235242
const session = useSession();
236243

244+
// Same reasoning as fetchZeroContext: this provider blocks the whole tree,
245+
// so the login redirect must happen here, not in a child that never mounts.
246+
useEffect(() => {
247+
if (session.status === "unauthenticated") handleUnauthorized();
248+
}, [session.status]);
249+
237250
if (session.status !== "authenticated") {
238251
return null;
239252
}

surfsense_web/components/providers/runtime-config.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ export function useRuntimeConfig() {
3131
return context;
3232
}
3333

34+
/** For components that may render outside RuntimeConfigProvider (e.g. anonymous /free pages). */
35+
export function useOptionalRuntimeConfig(): RuntimeConfigValue | null {
36+
return useContext(RuntimeConfigContext);
37+
}
38+
3439
export function useIsLocalAuth() {
3540
return useRuntimeConfig().authType === "LOCAL";
3641
}

surfsense_web/lib/apis/base-api.service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ class BaseApiService {
108108

109109
const refreshRetryKey = getRefreshRetryKey(mergedOptions.method, url);
110110
if (this.isDesktopClient && !desktopAccessToken && !isNoAuthEndpoint) {
111+
// Desktop refresh token is gone/revoked — send the user to /desktop/login
112+
// (same treatment as a server 401 below) instead of erroring in place.
113+
handleUnauthorized();
111114
throw new AuthenticationError("You are not authenticated. Please login again.");
112115
}
113116

0 commit comments

Comments
 (0)