chore: ran linting

This commit is contained in:
Anish Sarkar 2026-03-06 12:17:57 +05:30
parent 376f120502
commit 91463b3701
8 changed files with 273 additions and 337 deletions

View file

@ -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 */}

View file

@ -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>

View file

@ -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>;
}

View file

@ -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>

View file

@ -316,7 +316,13 @@ export function LayoutDataProvider({
badge: announcementUnreadCount > 0 ? formatInboxCount(announcementUnreadCount) : undefined,
},
],
[pathname, isInboxSidebarOpen, isDocumentsSidebarOpen, totalUnreadCount, announcementUnreadCount]
[
pathname,
isInboxSidebarOpen,
isDocumentsSidebarOpen,
totalUnreadCount,
announcementUnreadCount,
]
);
// Handlers

View file

@ -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);

View file

@ -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}.`);

View file

@ -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);