mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
chore: ran linting
This commit is contained in:
parent
376f120502
commit
91463b3701
8 changed files with 273 additions and 337 deletions
|
|
@ -1,13 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
CircleAlert,
|
||||
ListFilter,
|
||||
Search,
|
||||
Trash,
|
||||
Upload,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { CircleAlert, ListFilter, Search, Trash, Upload, X } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { useMemo, useRef, useState } from "react";
|
||||
|
|
@ -97,78 +90,78 @@ export function DocumentsFilters({
|
|||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-64 !p-0 overflow-hidden" align="end">
|
||||
<div>
|
||||
{/* Search input */}
|
||||
<div className="p-2 border-b border-border/50">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-0.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search types"
|
||||
value={typeSearchQuery}
|
||||
onChange={(e) => setTypeSearchQuery(e.target.value)}
|
||||
className="h-6 pl-6 text-sm bg-transparent border-0 focus-visible:ring-0"
|
||||
/>
|
||||
</div>
|
||||
<PopoverContent className="w-64 !p-0 overflow-hidden" align="end">
|
||||
<div>
|
||||
{/* Search input */}
|
||||
<div className="p-2 border-b border-border/50">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-0.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search types"
|
||||
value={typeSearchQuery}
|
||||
onChange={(e) => setTypeSearchQuery(e.target.value)}
|
||||
className="h-6 pl-6 text-sm bg-transparent border-0 focus-visible:ring-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[300px] overflow-y-auto overflow-x-hidden py-1.5 px-1.5">
|
||||
{filteredTypes.length === 0 ? (
|
||||
<div className="py-6 text-center text-sm text-muted-foreground">
|
||||
No types found
|
||||
</div>
|
||||
) : (
|
||||
filteredTypes.map((value: DocumentTypeEnum, i) => (
|
||||
<button
|
||||
type="button"
|
||||
key={value}
|
||||
className="flex w-full items-center gap-2.5 py-2 px-3 rounded-md hover:bg-muted/50 transition-colors cursor-pointer text-left"
|
||||
onClick={() => onToggleType(value, !activeTypes.includes(value))}
|
||||
>
|
||||
{/* Icon */}
|
||||
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-muted/50 text-foreground/80">
|
||||
{getDocumentTypeIcon(value, "h-4 w-4")}
|
||||
</div>
|
||||
{/* Text content */}
|
||||
<div className="flex flex-col min-w-0 flex-1 gap-0.5">
|
||||
<span className="text-[13px] font-medium text-foreground truncate leading-tight">
|
||||
{getDocumentTypeLabel(value)}
|
||||
</span>
|
||||
<span className="text-[11px] text-muted-foreground leading-tight">
|
||||
{typeCounts.get(value)} document
|
||||
{(typeCounts.get(value) ?? 0) !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</div>
|
||||
{/* Checkbox */}
|
||||
<Checkbox
|
||||
id={`${id}-${i}`}
|
||||
checked={activeTypes.includes(value)}
|
||||
onCheckedChange={(checked: boolean) => onToggleType(value, !!checked)}
|
||||
className="h-4 w-4 shrink-0 rounded border-muted-foreground/30 data-[state=checked]:bg-primary data-[state=checked]:border-primary"
|
||||
/>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{activeTypes.length > 0 && (
|
||||
<div className="px-3 pt-1.5 pb-1.5 border-t border-border/50">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full h-7 text-[11px] text-muted-foreground hover:text-foreground"
|
||||
onClick={() => {
|
||||
activeTypes.forEach((t) => {
|
||||
onToggleType(t, false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Clear filters
|
||||
</Button>
|
||||
<div className="max-h-[300px] overflow-y-auto overflow-x-hidden py-1.5 px-1.5">
|
||||
{filteredTypes.length === 0 ? (
|
||||
<div className="py-6 text-center text-sm text-muted-foreground">
|
||||
No types found
|
||||
</div>
|
||||
) : (
|
||||
filteredTypes.map((value: DocumentTypeEnum, i) => (
|
||||
<button
|
||||
type="button"
|
||||
key={value}
|
||||
className="flex w-full items-center gap-2.5 py-2 px-3 rounded-md hover:bg-muted/50 transition-colors cursor-pointer text-left"
|
||||
onClick={() => onToggleType(value, !activeTypes.includes(value))}
|
||||
>
|
||||
{/* Icon */}
|
||||
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-muted/50 text-foreground/80">
|
||||
{getDocumentTypeIcon(value, "h-4 w-4")}
|
||||
</div>
|
||||
{/* Text content */}
|
||||
<div className="flex flex-col min-w-0 flex-1 gap-0.5">
|
||||
<span className="text-[13px] font-medium text-foreground truncate leading-tight">
|
||||
{getDocumentTypeLabel(value)}
|
||||
</span>
|
||||
<span className="text-[11px] text-muted-foreground leading-tight">
|
||||
{typeCounts.get(value)} document
|
||||
{(typeCounts.get(value) ?? 0) !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</div>
|
||||
{/* Checkbox */}
|
||||
<Checkbox
|
||||
id={`${id}-${i}`}
|
||||
checked={activeTypes.includes(value)}
|
||||
onCheckedChange={(checked: boolean) => onToggleType(value, !!checked)}
|
||||
className="h-4 w-4 shrink-0 rounded border-muted-foreground/30 data-[state=checked]:bg-primary data-[state=checked]:border-primary"
|
||||
/>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{activeTypes.length > 0 && (
|
||||
<div className="px-3 pt-1.5 pb-1.5 border-t border-border/50">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full h-7 text-[11px] text-muted-foreground hover:text-foreground"
|
||||
onClick={() => {
|
||||
activeTypes.forEach((t) => {
|
||||
onToggleType(t, false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Search Input */}
|
||||
<div className="relative flex-1 min-w-0">
|
||||
|
|
@ -202,44 +195,44 @@ export function DocumentsFilters({
|
|||
|
||||
{/* Bulk Delete Button */}
|
||||
{selectedIds.size > 0 && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" size="sm" className="h-9 shrink-0 gap-1.5 px-2.5">
|
||||
<Trash size={14} />
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-destructive-foreground/20 text-[10px] font-medium">
|
||||
{selectedIds.size}
|
||||
</span>
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent className="max-w-md">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:gap-4">
|
||||
<div
|
||||
className="flex size-10 shrink-0 items-center justify-center rounded-full bg-destructive/10 text-destructive"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<CircleAlert size={18} strokeWidth={2} />
|
||||
</div>
|
||||
<AlertDialogHeader className="flex-1">
|
||||
<AlertDialogTitle>
|
||||
Delete {selectedIds.size} document{selectedIds.size !== 1 ? "s" : ""}?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the selected{" "}
|
||||
{selectedIds.size === 1 ? "document" : "documents"} from your search space.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" size="sm" className="h-9 shrink-0 gap-1.5 px-2.5">
|
||||
<Trash size={14} />
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-destructive-foreground/20 text-[10px] font-medium">
|
||||
{selectedIds.size}
|
||||
</span>
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent className="max-w-md">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:gap-4">
|
||||
<div
|
||||
className="flex size-10 shrink-0 items-center justify-center rounded-full bg-destructive/10 text-destructive"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<CircleAlert size={18} strokeWidth={2} />
|
||||
</div>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={onBulkDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<AlertDialogHeader className="flex-1">
|
||||
<AlertDialogTitle>
|
||||
Delete {selectedIds.size} document{selectedIds.size !== 1 ? "s" : ""}?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the selected{" "}
|
||||
{selectedIds.size === 1 ? "document" : "documents"} from your search space.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
</div>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={onBulkDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
|
||||
{/* Upload Button */}
|
||||
|
|
|
|||
|
|
@ -137,13 +137,7 @@ function formatAbsoluteDate(dateStr: string): string {
|
|||
});
|
||||
}
|
||||
|
||||
function DocumentNameTooltip({
|
||||
doc,
|
||||
className,
|
||||
}: {
|
||||
doc: Document;
|
||||
className?: string;
|
||||
}) {
|
||||
function DocumentNameTooltip({ doc, className }: { doc: Document; className?: string }) {
|
||||
const textRef = useRef<HTMLSpanElement>(null);
|
||||
const [isTruncated, setIsTruncated] = useState(false);
|
||||
|
||||
|
|
@ -167,9 +161,7 @@ function DocumentNameTooltip({
|
|||
</TooltipTrigger>
|
||||
<TooltipContent side="top" align="start" className="max-w-sm">
|
||||
<div className="space-y-1 text-xs">
|
||||
{isTruncated && (
|
||||
<p className="font-medium text-sm break-words">{doc.title}</p>
|
||||
)}
|
||||
{isTruncated && <p className="font-medium text-sm break-words">{doc.title}</p>}
|
||||
<p>
|
||||
<span className="text-muted-foreground">Owner:</span>{" "}
|
||||
{doc.created_by_name || doc.created_by_email || "—"}
|
||||
|
|
@ -235,8 +227,7 @@ function RowContextMenu({
|
|||
const isEditable = EDITABLE_DOCUMENT_TYPES.includes(
|
||||
doc.document_type as (typeof EDITABLE_DOCUMENT_TYPES)[number]
|
||||
);
|
||||
const isBeingProcessed =
|
||||
doc.status?.state === "pending" || doc.status?.state === "processing";
|
||||
const isBeingProcessed = doc.status?.state === "pending" || doc.status?.state === "processing";
|
||||
const isFileFailed = doc.document_type === "FILE" && doc.status?.state === "failed";
|
||||
const shouldShowDelete = !NON_DELETABLE_DOCUMENT_TYPES.includes(
|
||||
doc.document_type as (typeof NON_DELETABLE_DOCUMENT_TYPES)[number]
|
||||
|
|
@ -255,8 +246,7 @@ function RowContextMenu({
|
|||
{isEditable && (
|
||||
<ContextMenuItem
|
||||
onClick={() =>
|
||||
!isEditDisabled &&
|
||||
router.push(`/dashboard/${searchSpaceId}/editor/${doc.id}`)
|
||||
!isEditDisabled && router.push(`/dashboard/${searchSpaceId}/editor/${doc.id}`)
|
||||
}
|
||||
disabled={isEditDisabled}
|
||||
>
|
||||
|
|
@ -348,7 +338,9 @@ export function DocumentsTableShell({
|
|||
observe(desktopScrollRef.current, desktopSentinelRef.current);
|
||||
observe(mobileScrollRef.current, mobileSentinelRef.current);
|
||||
|
||||
return () => { for (const o of observers) o.disconnect(); };
|
||||
return () => {
|
||||
for (const o of observers) o.disconnect();
|
||||
};
|
||||
}, [onLoadMore, hasMore, loadingMore]);
|
||||
|
||||
const handleViewDocument = useCallback(async (doc: Document) => {
|
||||
|
|
@ -375,30 +367,27 @@ export function DocumentsTableShell({
|
|||
setViewingLoading(false);
|
||||
}, []);
|
||||
|
||||
const handleDeleteFromMenu = useCallback(
|
||||
async () => {
|
||||
if (!deleteDoc) return;
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const ok = await deleteDocument(deleteDoc.id);
|
||||
if (!ok) toast.error("Failed to delete document");
|
||||
} catch (error: unknown) {
|
||||
console.error("Error deleting document:", error);
|
||||
const status =
|
||||
(error as { response?: { status?: number } })?.response?.status ??
|
||||
(error as { status?: number })?.status;
|
||||
if (status === 409) {
|
||||
toast.error("Document is now being processed. Please try again later.");
|
||||
} else {
|
||||
toast.error("Failed to delete document");
|
||||
}
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setDeleteDoc(null);
|
||||
const handleDeleteFromMenu = useCallback(async () => {
|
||||
if (!deleteDoc) return;
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const ok = await deleteDocument(deleteDoc.id);
|
||||
if (!ok) toast.error("Failed to delete document");
|
||||
} catch (error: unknown) {
|
||||
console.error("Error deleting document:", error);
|
||||
const status =
|
||||
(error as { response?: { status?: number } })?.response?.status ??
|
||||
(error as { status?: number })?.status;
|
||||
if (status === 409) {
|
||||
toast.error("Document is now being processed. Please try again later.");
|
||||
} else {
|
||||
toast.error("Failed to delete document");
|
||||
}
|
||||
},
|
||||
[deleteDoc, deleteDocument]
|
||||
);
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setDeleteDoc(null);
|
||||
}
|
||||
}, [deleteDoc, deleteDocument]);
|
||||
|
||||
const sorted = React.useMemo(
|
||||
() => sortDocuments(documents, sortKey, sortDesc),
|
||||
|
|
@ -477,9 +466,7 @@ export function DocumentsTableShell({
|
|||
</span>
|
||||
</TableHead>
|
||||
<TableHead className="w-12 text-center h-8 pl-0 pr-3">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
Status
|
||||
</span>
|
||||
<span className="text-xs font-medium text-muted-foreground">Status</span>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
|
@ -560,25 +547,23 @@ export function DocumentsTableShell({
|
|||
<motion.tr
|
||||
initial={!isSearchMode && index < 20 ? { opacity: 0 } : false}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={!isSearchMode && index < 20 ? { duration: 0.15, delay: index * 0.02 } : { duration: 0 }}
|
||||
transition={
|
||||
!isSearchMode && index < 20
|
||||
? { duration: 0.15, delay: index * 0.02 }
|
||||
: { duration: 0 }
|
||||
}
|
||||
className={`border-b border-border/50 transition-colors ${
|
||||
isSelected
|
||||
? "bg-primary/5 hover:bg-primary/8"
|
||||
: "hover:bg-muted/30"
|
||||
isSelected ? "bg-primary/5 hover:bg-primary/8" : "hover:bg-muted/30"
|
||||
}`}
|
||||
>
|
||||
<TableCell className="w-10 pl-3 pr-0 py-1.5 text-center">
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={(v) =>
|
||||
canSelect && toggleOne(doc.id, !!v)
|
||||
}
|
||||
onCheckedChange={(v) => canSelect && toggleOne(doc.id, !!v)}
|
||||
disabled={!canSelect}
|
||||
aria-label={
|
||||
canSelect
|
||||
? "Select row"
|
||||
: "Cannot select while processing"
|
||||
canSelect ? "Select row" : "Cannot select while processing"
|
||||
}
|
||||
className={`border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary ${!canSelect ? "opacity-40 cursor-not-allowed" : ""}`}
|
||||
/>
|
||||
|
|
@ -594,10 +579,7 @@ export function DocumentsTableShell({
|
|||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="flex items-center justify-center">
|
||||
{getDocumentTypeIcon(
|
||||
doc.document_type,
|
||||
"h-4 w-4"
|
||||
)}
|
||||
{getDocumentTypeIcon(doc.document_type, "h-4 w-4")}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
|
|
@ -614,9 +596,7 @@ export function DocumentsTableShell({
|
|||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{hasMore && (
|
||||
<div ref={desktopSentinelRef} className="py-3" />
|
||||
)}
|
||||
{hasMore && <div ref={desktopSentinelRef} className="py-3" />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -670,7 +650,10 @@ export function DocumentsTableShell({
|
|||
</motion.div>
|
||||
</div>
|
||||
) : (
|
||||
<div ref={mobileScrollRef} className="md:hidden divide-y divide-border/50 flex-1 overflow-auto">
|
||||
<div
|
||||
ref={mobileScrollRef}
|
||||
className="md:hidden divide-y divide-border/50 flex-1 overflow-auto"
|
||||
>
|
||||
{sorted.map((doc, index) => {
|
||||
const isSelected = selectedIds.has(doc.id);
|
||||
const canSelect = isSelectable(doc);
|
||||
|
|
@ -685,7 +668,11 @@ export function DocumentsTableShell({
|
|||
<motion.div
|
||||
initial={!isSearchMode && index < 20 ? { opacity: 0 } : false}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={!isSearchMode && index < 20 ? { duration: 0.15, delay: index * 0.03 } : { duration: 0 }}
|
||||
transition={
|
||||
!isSearchMode && index < 20
|
||||
? { duration: 0.15, delay: index * 0.03 }
|
||||
: { duration: 0 }
|
||||
}
|
||||
className={`px-3 py-2 transition-colors ${
|
||||
isSelected ? "bg-primary/5" : "hover:bg-muted/20"
|
||||
}`}
|
||||
|
|
@ -693,15 +680,9 @@ export function DocumentsTableShell({
|
|||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={(v) =>
|
||||
canSelect && toggleOne(doc.id, !!v)
|
||||
}
|
||||
onCheckedChange={(v) => canSelect && toggleOne(doc.id, !!v)}
|
||||
disabled={!canSelect}
|
||||
aria-label={
|
||||
canSelect
|
||||
? "Select row"
|
||||
: "Cannot select while processing"
|
||||
}
|
||||
aria-label={canSelect ? "Select row" : "Cannot select while processing"}
|
||||
className={`border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary shrink-0 ${!canSelect ? "opacity-40 cursor-not-allowed" : ""}`}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
|
|
@ -714,10 +695,7 @@ export function DocumentsTableShell({
|
|||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="flex items-center justify-center">
|
||||
{getDocumentTypeIcon(
|
||||
doc.document_type,
|
||||
"h-4 w-4"
|
||||
)}
|
||||
{getDocumentTypeIcon(doc.document_type, "h-4 w-4")}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
|
|
@ -731,9 +709,7 @@ export function DocumentsTableShell({
|
|||
</RowContextMenu>
|
||||
);
|
||||
})}
|
||||
{hasMore && (
|
||||
<div ref={mobileSentinelRef} className="py-3" />
|
||||
)}
|
||||
{hasMore && <div ref={mobileSentinelRef} className="py-3" />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -761,8 +737,8 @@ export function DocumentsTableShell({
|
|||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete document?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete this document from
|
||||
your search space.
|
||||
This action cannot be undone. This will permanently delete this document from your
|
||||
search space.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
|
|
|
|||
|
|
@ -12,9 +12,5 @@ export default function DashboardLayout({
|
|||
}) {
|
||||
const { search_space_id } = use(params);
|
||||
|
||||
return (
|
||||
<DashboardClientLayout searchSpaceId={search_space_id}>
|
||||
{children}
|
||||
</DashboardClientLayout>
|
||||
);
|
||||
return <DashboardClientLayout searchSpaceId={search_space_id}>{children}</DashboardClientLayout>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,35 +190,33 @@ export const ConnectorIndicator: FC = () => {
|
|||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<TooltipIconButton
|
||||
data-joyride="connector-icon"
|
||||
tooltip={
|
||||
hasConnectors ? `Manage ${activeConnectorsCount} connectors` : "Connect your data"
|
||||
}
|
||||
side="bottom"
|
||||
className={cn(
|
||||
"size-[34px] rounded-full p-1 flex items-center justify-center transition-colors relative",
|
||||
"hover:bg-muted-foreground/15 dark:hover:bg-muted-foreground/30",
|
||||
"outline-none focus:outline-none focus-visible:outline-none font-semibold text-xs",
|
||||
"border-0 ring-0 focus:ring-0 shadow-none focus:shadow-none"
|
||||
)}
|
||||
aria-label={
|
||||
hasConnectors ? `View ${activeConnectorsCount} connectors` : "Add your first connector"
|
||||
}
|
||||
onClick={() => handleOpenChange(true)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Spinner size="sm" />
|
||||
) : (
|
||||
<>
|
||||
<Cable className="size-4 stroke-[1.5px]" />
|
||||
{activeConnectorsCount > 0 && (
|
||||
<span className="absolute -top-0.5 right-0 flex items-center justify-center min-w-[16px] h-4 px-1 text-[10px] font-medium rounded-full bg-primary text-primary-foreground shadow-sm">
|
||||
{activeConnectorsCount > 99 ? "99+" : activeConnectorsCount}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TooltipIconButton>
|
||||
data-joyride="connector-icon"
|
||||
tooltip={hasConnectors ? `Manage ${activeConnectorsCount} connectors` : "Connect your data"}
|
||||
side="bottom"
|
||||
className={cn(
|
||||
"size-[34px] rounded-full p-1 flex items-center justify-center transition-colors relative",
|
||||
"hover:bg-muted-foreground/15 dark:hover:bg-muted-foreground/30",
|
||||
"outline-none focus:outline-none focus-visible:outline-none font-semibold text-xs",
|
||||
"border-0 ring-0 focus:ring-0 shadow-none focus:shadow-none"
|
||||
)}
|
||||
aria-label={
|
||||
hasConnectors ? `View ${activeConnectorsCount} connectors` : "Add your first connector"
|
||||
}
|
||||
onClick={() => handleOpenChange(true)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Spinner size="sm" />
|
||||
) : (
|
||||
<>
|
||||
<Cable className="size-4 stroke-[1.5px]" />
|
||||
{activeConnectorsCount > 0 && (
|
||||
<span className="absolute -top-0.5 right-0 flex items-center justify-center min-w-[16px] h-4 px-1 text-[10px] font-medium rounded-full bg-primary text-primary-foreground shadow-sm">
|
||||
{activeConnectorsCount > 99 ? "99+" : activeConnectorsCount}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</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 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>
|
||||
|
|
|
|||
|
|
@ -316,7 +316,13 @@ export function LayoutDataProvider({
|
|||
badge: announcementUnreadCount > 0 ? formatInboxCount(announcementUnreadCount) : undefined,
|
||||
},
|
||||
],
|
||||
[pathname, isInboxSidebarOpen, isDocumentsSidebarOpen, totalUnreadCount, announcementUnreadCount]
|
||||
[
|
||||
pathname,
|
||||
isInboxSidebarOpen,
|
||||
isDocumentsSidebarOpen,
|
||||
totalUnreadCount,
|
||||
announcementUnreadCount,
|
||||
]
|
||||
);
|
||||
|
||||
// Handlers
|
||||
|
|
|
|||
|
|
@ -12,9 +12,7 @@ import type { DocumentTypeEnum } from "@/contracts/types/document.types";
|
|||
import { useDocuments } from "@/hooks/use-documents";
|
||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import {
|
||||
DocumentsFilters,
|
||||
} from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters";
|
||||
import { DocumentsFilters } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters";
|
||||
import {
|
||||
DocumentsTableShell,
|
||||
type SortKey,
|
||||
|
|
@ -68,17 +66,19 @@ export function DocumentsSidebar({ open, onOpenChange }: DocumentsSidebarProps)
|
|||
|
||||
// --- Search mode state ---
|
||||
const searchApiLoadedRef = useRef(0);
|
||||
const [searchItems, setSearchItems] = useState<Array<{
|
||||
id: number;
|
||||
search_space_id: number;
|
||||
document_type: string;
|
||||
title: string;
|
||||
created_by_id: string | null;
|
||||
created_by_name: string | null;
|
||||
created_by_email: string | null;
|
||||
created_at: string;
|
||||
status: { state: "ready" | "pending" | "processing" | "failed"; reason?: string };
|
||||
}>>([]);
|
||||
const [searchItems, setSearchItems] = useState<
|
||||
Array<{
|
||||
id: number;
|
||||
search_space_id: number;
|
||||
document_type: string;
|
||||
title: string;
|
||||
created_by_id: string | null;
|
||||
created_by_name: string | null;
|
||||
created_by_email: string | null;
|
||||
created_at: string;
|
||||
status: { state: "ready" | "pending" | "processing" | "failed"; reason?: string };
|
||||
}>
|
||||
>([]);
|
||||
const [searchLoadingMore, setSearchLoadingMore] = useState(false);
|
||||
const [searchInitialLoading, setSearchInitialLoading] = useState(false);
|
||||
const [searchHasMore, setSearchHasMore] = useState(false);
|
||||
|
|
|
|||
|
|
@ -13,88 +13,74 @@ const fs = require("fs");
|
|||
const path = require("path");
|
||||
|
||||
const replacements = [
|
||||
[
|
||||
"__NEXT_PUBLIC_FASTAPI_BACKEND_URL__",
|
||||
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000",
|
||||
],
|
||||
[
|
||||
"__NEXT_PUBLIC_ELECTRIC_URL__",
|
||||
process.env.NEXT_PUBLIC_ELECTRIC_URL || "http://localhost:5133",
|
||||
],
|
||||
[
|
||||
"__NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE__",
|
||||
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "LOCAL",
|
||||
],
|
||||
[
|
||||
"__NEXT_PUBLIC_ETL_SERVICE__",
|
||||
process.env.NEXT_PUBLIC_ETL_SERVICE || "DOCLING",
|
||||
],
|
||||
[
|
||||
"__NEXT_PUBLIC_DEPLOYMENT_MODE__",
|
||||
process.env.NEXT_PUBLIC_DEPLOYMENT_MODE || "self-hosted",
|
||||
],
|
||||
[
|
||||
"__NEXT_PUBLIC_ELECTRIC_AUTH_MODE__",
|
||||
process.env.NEXT_PUBLIC_ELECTRIC_AUTH_MODE || "insecure",
|
||||
],
|
||||
[
|
||||
"__NEXT_PUBLIC_FASTAPI_BACKEND_URL__",
|
||||
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000",
|
||||
],
|
||||
["__NEXT_PUBLIC_ELECTRIC_URL__", process.env.NEXT_PUBLIC_ELECTRIC_URL || "http://localhost:5133"],
|
||||
[
|
||||
"__NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE__",
|
||||
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "LOCAL",
|
||||
],
|
||||
["__NEXT_PUBLIC_ETL_SERVICE__", process.env.NEXT_PUBLIC_ETL_SERVICE || "DOCLING"],
|
||||
["__NEXT_PUBLIC_DEPLOYMENT_MODE__", process.env.NEXT_PUBLIC_DEPLOYMENT_MODE || "self-hosted"],
|
||||
["__NEXT_PUBLIC_ELECTRIC_AUTH_MODE__", process.env.NEXT_PUBLIC_ELECTRIC_AUTH_MODE || "insecure"],
|
||||
];
|
||||
|
||||
let filesProcessed = 0;
|
||||
let filesModified = 0;
|
||||
|
||||
function walk(dir) {
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(full);
|
||||
} else if (entry.name.endsWith(".js")) {
|
||||
filesProcessed++;
|
||||
let content = fs.readFileSync(full, "utf8");
|
||||
let changed = false;
|
||||
for (const [placeholder, value] of replacements) {
|
||||
if (content.includes(placeholder)) {
|
||||
content = content.replaceAll(placeholder, value);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
fs.writeFileSync(full, content);
|
||||
filesModified++;
|
||||
}
|
||||
}
|
||||
}
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(full);
|
||||
} else if (entry.name.endsWith(".js")) {
|
||||
filesProcessed++;
|
||||
let content = fs.readFileSync(full, "utf8");
|
||||
let changed = false;
|
||||
for (const [placeholder, value] of replacements) {
|
||||
if (content.includes(placeholder)) {
|
||||
content = content.replaceAll(placeholder, value);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
fs.writeFileSync(full, content);
|
||||
filesModified++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[entrypoint] Replacing environment variable placeholders...");
|
||||
for (const [placeholder, value] of replacements) {
|
||||
console.log(` ${placeholder} -> ${value}`);
|
||||
console.log(` ${placeholder} -> ${value}`);
|
||||
}
|
||||
|
||||
walk(path.join(__dirname, ".next"));
|
||||
|
||||
const serverJs = path.join(__dirname, "server.js");
|
||||
if (fs.existsSync(serverJs)) {
|
||||
let content = fs.readFileSync(serverJs, "utf8");
|
||||
let changed = false;
|
||||
filesProcessed++;
|
||||
for (const [placeholder, value] of replacements) {
|
||||
if (content.includes(placeholder)) {
|
||||
content = content.replaceAll(placeholder, value);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
fs.writeFileSync(serverJs, content);
|
||||
filesModified++;
|
||||
}
|
||||
let content = fs.readFileSync(serverJs, "utf8");
|
||||
let changed = false;
|
||||
filesProcessed++;
|
||||
for (const [placeholder, value] of replacements) {
|
||||
if (content.includes(placeholder)) {
|
||||
content = content.replaceAll(placeholder, value);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
fs.writeFileSync(serverJs, content);
|
||||
filesModified++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[entrypoint] Done. Scanned ${filesProcessed} files, modified ${filesModified}.`
|
||||
);
|
||||
console.log(`[entrypoint] Done. Scanned ${filesProcessed} files, modified ${filesModified}.`);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type {
|
||||
DocumentSortBy,
|
||||
DocumentTypeEnum,
|
||||
SortOrder,
|
||||
} from "@/contracts/types/document.types";
|
||||
import type { DocumentSortBy, DocumentTypeEnum, SortOrder } from "@/contracts/types/document.types";
|
||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||
import type { SyncHandle } from "@/lib/electric/client";
|
||||
import { useElectricClient } from "@/lib/electric/context";
|
||||
|
|
@ -77,7 +73,9 @@ export function useDocuments(
|
|||
|
||||
const apiLoadedCountRef = useRef(0);
|
||||
const initialLoadDoneRef = useRef(false);
|
||||
const prevParamsRef = useRef<{ sortBy: string; sortOrder: string; typeFilterKey: string } | null>(null);
|
||||
const prevParamsRef = useRef<{ sortBy: string; sortOrder: string; typeFilterKey: string } | null>(
|
||||
null
|
||||
);
|
||||
// Snapshot of all doc IDs from Electric's first callback after initial load.
|
||||
// Anything appearing in subsequent callbacks NOT in this set is genuinely new.
|
||||
const electricBaselineIdsRef = useRef<Set<number> | null>(null);
|
||||
|
|
@ -211,9 +209,7 @@ export function useDocuments(
|
|||
} catch (err) {
|
||||
if (cancelled) return;
|
||||
console.error("[useDocuments] Initial load failed:", err);
|
||||
setError(
|
||||
err instanceof Error ? err : new Error("Failed to load documents")
|
||||
);
|
||||
setError(err instanceof Error ? err : new Error("Failed to load documents"));
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
|
|
@ -302,9 +298,7 @@ export function useDocuments(
|
|||
WHERE search_space_id = $1
|
||||
ORDER BY created_at DESC`;
|
||||
|
||||
const liveQuery = await db.live.query<DocumentElectric>(query, [
|
||||
spaceId,
|
||||
]);
|
||||
const liveQuery = await db.live.query<DocumentElectric>(query, [spaceId]);
|
||||
|
||||
if (!mounted) {
|
||||
liveQuery.unsubscribe?.();
|
||||
|
|
@ -319,11 +313,8 @@ export function useDocuments(
|
|||
|
||||
const unknownUserIds = validItems
|
||||
.filter(
|
||||
(
|
||||
doc
|
||||
): doc is DocumentElectric & { created_by_id: string } =>
|
||||
doc.created_by_id !== null &&
|
||||
!userCacheRef.current.has(doc.created_by_id)
|
||||
(doc): doc is DocumentElectric & { created_by_id: string } =>
|
||||
doc.created_by_id !== null && !userCacheRef.current.has(doc.created_by_id)
|
||||
)
|
||||
.map((doc) => doc.created_by_id);
|
||||
|
||||
|
|
@ -343,14 +334,10 @@ export function useDocuments(
|
|||
prev.map((doc) => ({
|
||||
...doc,
|
||||
created_by_name: doc.created_by_id
|
||||
? (userCacheRef.current.get(
|
||||
doc.created_by_id
|
||||
) ?? null)
|
||||
? (userCacheRef.current.get(doc.created_by_id) ?? null)
|
||||
: null,
|
||||
created_by_email: doc.created_by_id
|
||||
? (emailCacheRef.current.get(
|
||||
doc.created_by_id
|
||||
) ?? null)
|
||||
? (emailCacheRef.current.get(doc.created_by_id) ?? null)
|
||||
: null,
|
||||
}))
|
||||
);
|
||||
|
|
@ -389,9 +376,7 @@ export function useDocuments(
|
|||
// Update existing docs (status changes, title edits)
|
||||
let updated = prev.map((doc) => {
|
||||
if (liveIds.has(doc.id)) {
|
||||
const liveItem = validItems.find(
|
||||
(v) => v.id === doc.id
|
||||
);
|
||||
const liveItem = validItems.find((v) => v.id === doc.id);
|
||||
if (liveItem) {
|
||||
return electricToDisplayDoc(liveItem);
|
||||
}
|
||||
|
|
@ -415,8 +400,7 @@ export function useDocuments(
|
|||
if (isFullySynced && validItems.length > 0) {
|
||||
const counts: Record<string, number> = {};
|
||||
for (const item of validItems) {
|
||||
counts[item.document_type] =
|
||||
(counts[item.document_type] || 0) + 1;
|
||||
counts[item.document_type] = (counts[item.document_type] || 0) + 1;
|
||||
}
|
||||
setTypeCounts(counts);
|
||||
setTotal(validItems.length);
|
||||
|
|
@ -456,10 +440,7 @@ export function useDocuments(
|
|||
const prevSearchSpaceIdRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
prevSearchSpaceIdRef.current !== null &&
|
||||
prevSearchSpaceIdRef.current !== searchSpaceId
|
||||
) {
|
||||
if (prevSearchSpaceIdRef.current !== null && prevSearchSpaceIdRef.current !== searchSpaceId) {
|
||||
setDocuments([]);
|
||||
setTypeCounts({});
|
||||
setTotal(0);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue