mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-05 22:02:39 +02:00
feat: fixed connectors dialog navigation, Implement bulk document deletion and improve search space filtering
- Added bulk delete functionality for documents in DocumentsTableShell and DocumentsSidebar. - Enhanced search space retrieval to exclude spaces marked for deletion in read_search_spaces. - Updated connector dialog to synchronize URL parameters when opened externally. - Improved layout behavior to handle search space deletion and redirection more effectively.
This commit is contained in:
parent
7c3aedf811
commit
d61e29e74b
7 changed files with 312 additions and 97 deletions
|
|
@ -258,7 +258,10 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
|||
</TooltipIconButton>
|
||||
)}
|
||||
|
||||
<DialogContent className="max-w-3xl w-[95vw] sm:w-full h-[75vh] sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden border border-border ring-0 dark:ring-0 bg-muted dark:bg-muted text-foreground focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 [&>button]:right-4 sm:[&>button]:right-12 [&>button]:top-6 sm:[&>button]:top-10 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button_svg]:size-5 select-none">
|
||||
<DialogContent
|
||||
onFocusOutside={(e) => e.preventDefault()}
|
||||
className="max-w-3xl w-[95vw] sm:w-full h-[75vh] sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden border border-border ring-0 dark:ring-0 bg-muted dark:bg-muted text-foreground focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 [&>button]:right-4 sm:[&>button]:right-12 [&>button]:top-6 sm:[&>button]:top-10 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button_svg]:size-5 select-none"
|
||||
>
|
||||
<DialogTitle className="sr-only">Manage Connectors</DialogTitle>
|
||||
{/* YouTube Crawler View - shown when adding YouTube videos */}
|
||||
{isYouTubeView && searchSpaceId ? (
|
||||
|
|
|
|||
|
|
@ -181,6 +181,24 @@ export const useConnectorDialog = () => {
|
|||
[searchSpaceId, indexConnector, updateConnector, refetchAllConnectors]
|
||||
);
|
||||
|
||||
// When the dialog is opened externally (via setConnectorDialogOpen atom from
|
||||
// thread.tsx / DocumentsSidebar.tsx), the URL is not updated. Sync it here
|
||||
// so that other handlers that read window.location.href see modal=connectors.
|
||||
const activeTabRef = useRef(activeTab);
|
||||
activeTabRef.current = activeTab;
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const url = new URL(window.location.href);
|
||||
const modalParam = url.searchParams.get("modal");
|
||||
const tabParam = url.searchParams.get("tab");
|
||||
if (modalParam !== "connectors" || (tabParam !== "all" && tabParam !== "active")) {
|
||||
url.searchParams.set("modal", "connectors");
|
||||
url.searchParams.set("tab", activeTabRef.current);
|
||||
window.history.replaceState({ modal: true }, "", url.toString());
|
||||
}
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Synchronize state with URL query params
|
||||
useEffect(() => {
|
||||
try {
|
||||
|
|
@ -1647,12 +1665,13 @@ export const useConnectorDialog = () => {
|
|||
[activeTab, isStartingIndexing, isDisconnecting, isSaving, isCreatingConnector, setIsOpen]
|
||||
);
|
||||
|
||||
// Handle tab change
|
||||
// Handle tab change — only update React state.
|
||||
// Avoid window.history.replaceState here: Next.js intercepts it, triggers a
|
||||
// searchParams update/transition, and the resulting concurrent re-render can
|
||||
// cause Radix Dialog's DismissableLayer to detect a transient focus-outside
|
||||
// event, which fires onOpenChange(false) and closes the dialog.
|
||||
const handleTabChange = useCallback((value: string) => {
|
||||
setActiveTab(value);
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("tab", value);
|
||||
window.history.replaceState({ modal: true }, "", url.toString());
|
||||
}, []);
|
||||
|
||||
// Handle scroll
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
|
||||
// Atoms
|
||||
const { data: user } = useAtomValue(currentUserAtom);
|
||||
const { data: searchSpacesData, refetch: refetchSearchSpaces } = useAtomValue(searchSpacesAtom);
|
||||
const { data: searchSpacesData, refetch: refetchSearchSpaces, isSuccess: searchSpacesLoaded } = useAtomValue(searchSpacesAtom);
|
||||
const { mutateAsync: deleteSearchSpace } = useAtomValue(deleteSearchSpaceMutationAtom);
|
||||
const currentThreadState = useAtomValue(currentThreadAtom);
|
||||
const resetCurrentThread = useSetAtom(resetCurrentThreadAtom);
|
||||
|
|
@ -276,6 +276,17 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
return searchSpaces.find((s) => s.id === Number(searchSpaceId)) ?? null;
|
||||
}, [searchSpaceId, searchSpaces]);
|
||||
|
||||
// Safety redirect: if the current search space is no longer in the user's list
|
||||
// (e.g. deleted by background task, membership revoked), redirect to a valid space.
|
||||
useEffect(() => {
|
||||
if (!searchSpacesLoaded || !searchSpaceId || isDeletingSearchSpace || isLeavingSearchSpace) return;
|
||||
if (searchSpaces.length > 0 && !activeSearchSpace) {
|
||||
router.replace(`/dashboard/${searchSpaces[0].id}/new-chat`);
|
||||
} else if (searchSpaces.length === 0 && searchSpacesLoaded) {
|
||||
router.replace("/dashboard");
|
||||
}
|
||||
}, [searchSpacesLoaded, searchSpaceId, searchSpaces, activeSearchSpace, isDeletingSearchSpace, isLeavingSearchSpace, router]);
|
||||
|
||||
// Transform and split chats into private and shared based on visibility
|
||||
const { myChats, sharedChats } = useMemo(() => {
|
||||
if (!threadsData?.threads) return { myChats: [], sharedChats: [] };
|
||||
|
|
@ -384,17 +395,27 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
setIsDeletingSearchSpace(true);
|
||||
try {
|
||||
await deleteSearchSpace({ id: searchSpaceToDelete.id });
|
||||
refetchSearchSpaces();
|
||||
if (Number(searchSpaceId) === searchSpaceToDelete.id && searchSpaces.length > 1) {
|
||||
const remaining = searchSpaces.filter((s) => s.id !== searchSpaceToDelete.id);
|
||||
if (remaining.length > 0) {
|
||||
router.push(`/dashboard/${remaining[0].id}/new-chat`);
|
||||
|
||||
const isCurrentSpace = Number(searchSpaceId) === searchSpaceToDelete.id;
|
||||
|
||||
// Await refetch so we have the freshest list (backend now hides [DELETING] spaces)
|
||||
const result = await refetchSearchSpaces();
|
||||
const updatedSpaces = (result.data ?? []).filter(
|
||||
(s) => s.id !== searchSpaceToDelete.id
|
||||
);
|
||||
|
||||
if (isCurrentSpace) {
|
||||
if (updatedSpaces.length > 0) {
|
||||
router.push(`/dashboard/${updatedSpaces[0].id}/new-chat`);
|
||||
} else {
|
||||
router.push("/dashboard");
|
||||
}
|
||||
} else if (searchSpaces.length === 1) {
|
||||
router.push("/dashboard");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting search space:", error);
|
||||
toast.error(
|
||||
t.has("delete_space_error") ? t("delete_space_error") : "Failed to delete search space"
|
||||
);
|
||||
} finally {
|
||||
setIsDeletingSearchSpace(false);
|
||||
setShowDeleteSearchSpaceDialog(false);
|
||||
|
|
@ -405,8 +426,8 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
deleteSearchSpace,
|
||||
refetchSearchSpaces,
|
||||
searchSpaceId,
|
||||
searchSpaces,
|
||||
router,
|
||||
t,
|
||||
]);
|
||||
|
||||
const confirmLeaveSearchSpace = useCallback(async () => {
|
||||
|
|
@ -414,23 +435,30 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
setIsLeavingSearchSpace(true);
|
||||
try {
|
||||
await searchSpacesApiService.leaveSearchSpace(searchSpaceToLeave.id);
|
||||
refetchSearchSpaces();
|
||||
if (Number(searchSpaceId) === searchSpaceToLeave.id && searchSpaces.length > 1) {
|
||||
const remaining = searchSpaces.filter((s) => s.id !== searchSpaceToLeave.id);
|
||||
if (remaining.length > 0) {
|
||||
router.push(`/dashboard/${remaining[0].id}/new-chat`);
|
||||
|
||||
const isCurrentSpace = Number(searchSpaceId) === searchSpaceToLeave.id;
|
||||
|
||||
const result = await refetchSearchSpaces();
|
||||
const updatedSpaces = (result.data ?? []).filter(
|
||||
(s) => s.id !== searchSpaceToLeave.id
|
||||
);
|
||||
|
||||
if (isCurrentSpace) {
|
||||
if (updatedSpaces.length > 0) {
|
||||
router.push(`/dashboard/${updatedSpaces[0].id}/new-chat`);
|
||||
} else {
|
||||
router.push("/dashboard");
|
||||
}
|
||||
} else if (searchSpaces.length === 1) {
|
||||
router.push("/dashboard");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error leaving search space:", error);
|
||||
toast.error(t.has("leave_error") ? t("leave_error") : "Failed to leave search space");
|
||||
} finally {
|
||||
setIsLeavingSearchSpace(false);
|
||||
setShowLeaveSearchSpaceDialog(false);
|
||||
setSearchSpaceToLeave(null);
|
||||
}
|
||||
}, [searchSpaceToLeave, refetchSearchSpaces, searchSpaceId, searchSpaces, router]);
|
||||
}, [searchSpaceToLeave, refetchSearchSpaces, searchSpaceId, router, t]);
|
||||
|
||||
const handleNavItemClick = useCallback(
|
||||
(item: NavItem) => {
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ export function DocumentsSidebar({
|
|||
loadingMore: realtimeLoadingMore,
|
||||
hasMore: realtimeHasMore,
|
||||
loadMore: realtimeLoadMore,
|
||||
removeItems: realtimeRemoveItems,
|
||||
error: realtimeError,
|
||||
} = useDocuments(searchSpaceId, activeTypes, sortKey, sortDesc ? "desc" : "asc");
|
||||
|
||||
|
|
@ -137,6 +138,7 @@ export function DocumentsSidebar({
|
|||
await deleteDocumentMutation({ id });
|
||||
toast.success(t("delete_success") || "Document deleted");
|
||||
setSidebarDocs((prev) => prev.filter((d) => d.id !== id));
|
||||
realtimeRemoveItems([id]);
|
||||
if (isSearchMode) {
|
||||
searchRemoveItems([id]);
|
||||
}
|
||||
|
|
@ -146,7 +148,30 @@ export function DocumentsSidebar({
|
|||
return false;
|
||||
}
|
||||
},
|
||||
[deleteDocumentMutation, isSearchMode, t, searchRemoveItems, setSidebarDocs]
|
||||
[deleteDocumentMutation, isSearchMode, t, searchRemoveItems, realtimeRemoveItems, setSidebarDocs]
|
||||
);
|
||||
|
||||
const handleBulkDeleteDocuments = useCallback(
|
||||
async (ids: number[]): Promise<{ success: number; failed: number }> => {
|
||||
const successIds: number[] = [];
|
||||
const results = await Promise.allSettled(
|
||||
ids.map(async (id) => {
|
||||
await deleteDocumentMutation({ id });
|
||||
successIds.push(id);
|
||||
})
|
||||
);
|
||||
if (successIds.length > 0) {
|
||||
setSidebarDocs((prev) => prev.filter((d) => !successIds.includes(d.id)));
|
||||
realtimeRemoveItems(successIds);
|
||||
if (isSearchMode) {
|
||||
searchRemoveItems(successIds);
|
||||
}
|
||||
}
|
||||
const success = results.filter((r) => r.status === "fulfilled").length;
|
||||
const failed = results.filter((r) => r.status === "rejected").length;
|
||||
return { success, failed };
|
||||
},
|
||||
[deleteDocumentMutation, isSearchMode, searchRemoveItems, realtimeRemoveItems, setSidebarDocs]
|
||||
);
|
||||
|
||||
const sortKeyRef = useRef(sortKey);
|
||||
|
|
@ -319,6 +344,7 @@ export function DocumentsSidebar({
|
|||
sortDesc={sortDesc}
|
||||
onSortChange={handleSortChange}
|
||||
deleteDocument={handleDeleteDocument}
|
||||
bulkDeleteDocuments={handleBulkDeleteDocuments}
|
||||
searchSpaceId={String(searchSpaceId)}
|
||||
hasMore={hasMore}
|
||||
loadingMore={loadingMore}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue