mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-27 01:36:30 +02:00
chore: ran linting
This commit is contained in:
parent
00a617ef17
commit
aa66928154
44 changed files with 2025 additions and 1658 deletions
|
|
@ -38,7 +38,9 @@ export function DocumentTypeChip({ type, className }: { type: string; className?
|
|||
className={`inline-flex items-center gap-1.5 rounded bg-muted/40 px-2 py-1 text-xs text-muted-foreground max-w-full overflow-hidden ${className ?? ""}`}
|
||||
>
|
||||
<span className="opacity-80 flex-shrink-0">{icon}</span>
|
||||
<span ref={textRef} className="truncate min-w-0">{fullLabel}</span>
|
||||
<span ref={textRef} className="truncate min-w-0">
|
||||
{fullLabel}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -68,9 +68,7 @@ export function DocumentsFilters({
|
|||
const filteredTypes = useMemo(() => {
|
||||
if (!typeSearchQuery.trim()) return uniqueTypes;
|
||||
const query = typeSearchQuery.toLowerCase();
|
||||
return uniqueTypes.filter((type) =>
|
||||
getDocumentTypeLabel(type).toLowerCase().includes(query)
|
||||
);
|
||||
return uniqueTypes.filter((type) => getDocumentTypeLabel(type).toLowerCase().includes(query));
|
||||
}, [uniqueTypes, typeSearchQuery]);
|
||||
|
||||
const typeCounts = useMemo(() => {
|
||||
|
|
@ -156,94 +154,95 @@ export function DocumentsFilters({
|
|||
|
||||
{/* Filter Buttons Group */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{/* Type Filter */}
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-9 gap-2 border-dashed border-border/60 text-muted-foreground hover:text-foreground hover:border-border"
|
||||
>
|
||||
<FileType size={14} className="text-muted-foreground" />
|
||||
<span className="hidden sm:inline">Type</span>
|
||||
{activeTypes.length > 0 && (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-primary text-[10px] font-medium text-primary-foreground">
|
||||
{activeTypes.length}
|
||||
</span>
|
||||
)}
|
||||
</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>
|
||||
</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
|
||||
{/* Type Filter */}
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-9 gap-2 border-dashed border-border/60 text-muted-foreground hover:text-foreground hover:border-border"
|
||||
>
|
||||
<FileType size={14} className="text-muted-foreground" />
|
||||
<span className="hidden sm:inline">Type</span>
|
||||
{activeTypes.length > 0 && (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-primary text-[10px] font-medium text-primary-foreground">
|
||||
{activeTypes.length}
|
||||
</span>
|
||||
)}
|
||||
</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>
|
||||
) : (
|
||||
filteredTypes.map((value: DocumentTypeEnum, i) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
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))}
|
||||
</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
|
||||
key={value}
|
||||
type="button"
|
||||
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);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{/* 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>
|
||||
))
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Bulk Delete Button */}
|
||||
{selectedIds.size > 0 && (
|
||||
|
|
@ -255,22 +254,14 @@ export function DocumentsFilters({
|
|||
exit={{ opacity: 0, scale: 0.9 }}
|
||||
>
|
||||
{/* Mobile: icon with count */}
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="h-9 gap-1.5 px-2.5 md:hidden"
|
||||
>
|
||||
<Button variant="destructive" size="sm" className="h-9 gap-1.5 px-2.5 md:hidden">
|
||||
<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>
|
||||
{/* Desktop: full button */}
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="h-9 gap-2 hidden md:flex"
|
||||
>
|
||||
<Button variant="destructive" size="sm" className="h-9 gap-2 hidden md:flex">
|
||||
<Trash size={14} />
|
||||
Delete
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-destructive-foreground/20 text-[10px] font-medium">
|
||||
|
|
@ -288,9 +279,12 @@ export function DocumentsFilters({
|
|||
<CircleAlert size={18} strokeWidth={2} />
|
||||
</div>
|
||||
<AlertDialogHeader className="flex-1">
|
||||
<AlertDialogTitle>Delete {selectedIds.size} document{selectedIds.size !== 1 ? "s" : ""}?</AlertDialogTitle>
|
||||
<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.
|
||||
This action cannot be undone. This will permanently delete the selected{" "}
|
||||
{selectedIds.size === 1 ? "document" : "documents"} from your search space.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,20 @@
|
|||
"use client";
|
||||
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { AlertCircle, Calendar, CheckCircle2, ChevronDown, ChevronUp, Clock, FileText, FileX, Loader2, Network, Plus, User } from "lucide-react";
|
||||
import {
|
||||
AlertCircle,
|
||||
Calendar,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Clock,
|
||||
FileText,
|
||||
FileX,
|
||||
Loader2,
|
||||
Network,
|
||||
Plus,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { useRef, useState, useEffect, useCallback } from "react";
|
||||
|
|
@ -10,12 +23,7 @@ import { JsonMetadataViewer } from "@/components/json-metadata-viewer";
|
|||
import { MarkdownViewer } from "@/components/markdown-viewer";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import {
|
||||
|
|
@ -35,7 +43,7 @@ import type { ColumnVisibility, Document, DocumentStatus } from "./types";
|
|||
// Status indicator component for document processing status
|
||||
function StatusIndicator({ status }: { status?: DocumentStatus }) {
|
||||
const state = status?.state ?? "ready";
|
||||
|
||||
|
||||
switch (state) {
|
||||
case "pending":
|
||||
return (
|
||||
|
|
@ -176,12 +184,10 @@ function SortableHeader({
|
|||
>
|
||||
{icon && <span className="opacity-60">{icon}</span>}
|
||||
{children}
|
||||
<span className={`transition-opacity ${isActive ? "opacity-100" : "opacity-0 group-hover:opacity-50"}`}>
|
||||
{isActive && sortDesc ? (
|
||||
<ChevronDown size={14} />
|
||||
) : (
|
||||
<ChevronUp size={14} />
|
||||
)}
|
||||
<span
|
||||
className={`transition-opacity ${isActive ? "opacity-100" : "opacity-0 group-hover:opacity-50"}`}
|
||||
>
|
||||
{isActive && sortDesc ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
|
|
@ -300,8 +306,10 @@ export function DocumentsTableShell({
|
|||
|
||||
// Only consider selectable documents for "select all" logic
|
||||
const selectableDocs = sorted.filter(isSelectable);
|
||||
const allSelectedOnPage = selectableDocs.length > 0 && selectableDocs.every((d) => selectedIds.has(d.id));
|
||||
const someSelectedOnPage = selectableDocs.some((d) => selectedIds.has(d.id)) && !allSelectedOnPage;
|
||||
const allSelectedOnPage =
|
||||
selectableDocs.length > 0 && selectableDocs.every((d) => selectedIds.has(d.id));
|
||||
const someSelectedOnPage =
|
||||
selectableDocs.some((d) => selectedIds.has(d.id)) && !allSelectedOnPage;
|
||||
|
||||
const toggleAll = (checked: boolean) => {
|
||||
const next = new Set(selectedIds);
|
||||
|
|
@ -388,10 +396,7 @@ export function DocumentsTableShell({
|
|||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="w-[35%] py-2.5 max-w-0 border-r border-border/40">
|
||||
<Skeleton
|
||||
className="h-4"
|
||||
style={{ width: `${widthPercent}%` }}
|
||||
/>
|
||||
<Skeleton className="h-4" style={{ width: `${widthPercent}%` }} />
|
||||
</TableCell>
|
||||
{columnVisibility.document_type && (
|
||||
<TableCell className="w-[20%] min-w-[120px] max-w-[200px] py-2.5 border-r border-border/40 overflow-hidden">
|
||||
|
|
@ -429,24 +434,15 @@ export function DocumentsTableShell({
|
|||
<div className="flex items-start gap-3">
|
||||
<Skeleton className="h-4 w-4 mt-0.5 rounded" />
|
||||
<div className="flex-1 min-w-0 space-y-2">
|
||||
<Skeleton
|
||||
className="h-4"
|
||||
style={{ width: `${widthPercent}%` }}
|
||||
/>
|
||||
<Skeleton className="h-4" style={{ width: `${widthPercent}%` }} />
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Skeleton className="h-5 w-20 rounded" />
|
||||
{columnVisibility.created_by && (
|
||||
<Skeleton className="h-3 w-14" />
|
||||
)}
|
||||
{columnVisibility.created_at && (
|
||||
<Skeleton className="h-3 w-20" />
|
||||
)}
|
||||
{columnVisibility.created_by && <Skeleton className="h-3 w-14" />}
|
||||
{columnVisibility.created_at && <Skeleton className="h-3 w-20" />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{columnVisibility.status && (
|
||||
<Skeleton className="h-5 w-5 rounded-full" />
|
||||
)}
|
||||
{columnVisibility.status && <Skeleton className="h-5 w-5 rounded-full" />}
|
||||
<Skeleton className="h-7 w-7 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -549,9 +545,7 @@ export function DocumentsTableShell({
|
|||
)}
|
||||
{columnVisibility.status && (
|
||||
<TableHead className="w-20 text-center">
|
||||
<span className="text-sm font-medium text-muted-foreground/70">
|
||||
Status
|
||||
</span>
|
||||
<span className="text-sm font-medium text-muted-foreground/70">Status</span>
|
||||
</TableHead>
|
||||
)}
|
||||
<TableHead className="w-10">
|
||||
|
|
@ -580,9 +574,7 @@ export function DocumentsTableShell({
|
|||
},
|
||||
}}
|
||||
className={`border-b border-border/40 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-8 px-0 py-2.5 text-center">
|
||||
|
|
@ -591,7 +583,9 @@ export function DocumentsTableShell({
|
|||
checked={isSelected}
|
||||
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 ${!canSelect ? "opacity-40 cursor-not-allowed" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -639,7 +633,9 @@ export function DocumentsTableShell({
|
|||
<TableCell className="w-32 py-2.5 text-sm text-foreground border-r border-border/40">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="cursor-default">{formatRelativeDate(doc.created_at)}</span>
|
||||
<span className="cursor-default">
|
||||
{formatRelativeDate(doc.created_at)}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
{formatAbsoluteDate(doc.created_at)}
|
||||
|
|
@ -720,9 +716,7 @@ export function DocumentsTableShell({
|
|||
<div className="flex flex-wrap items-center gap-2">
|
||||
<DocumentTypeChip type={doc.document_type} />
|
||||
{columnVisibility.created_by && doc.created_by_name && (
|
||||
<span className="text-xs text-foreground">
|
||||
{doc.created_by_name}
|
||||
</span>
|
||||
<span className="text-xs text-foreground">{doc.created_by_name}</span>
|
||||
)}
|
||||
{columnVisibility.created_at && (
|
||||
<Tooltip>
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@ export function RowActions({
|
|||
);
|
||||
|
||||
// Documents in "pending" or "processing" state should show disabled delete
|
||||
const isBeingProcessed = document.status?.state === "pending" || document.status?.state === "processing";
|
||||
const isBeingProcessed =
|
||||
document.status?.state === "pending" || document.status?.state === "processing";
|
||||
|
||||
// SURFSENSE_DOCS are system-managed and should not show delete at all
|
||||
const shouldShowDelete = !NON_DELETABLE_DOCUMENT_TYPES.includes(
|
||||
|
|
@ -67,8 +68,9 @@ export function RowActions({
|
|||
} catch (error: unknown) {
|
||||
console.error("Error deleting document:", error);
|
||||
// Check for 409 Conflict (document started processing after UI loaded)
|
||||
const status = (error as { response?: { status?: number } })?.response?.status
|
||||
?? (error as { status?: number })?.status;
|
||||
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 {
|
||||
|
|
@ -92,7 +94,11 @@ export function RowActions({
|
|||
// Editable documents: show 3-dot dropdown with edit + delete
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted/80">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted/80"
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">Open menu</span>
|
||||
</Button>
|
||||
|
|
@ -101,7 +107,9 @@ export function RowActions({
|
|||
<DropdownMenuItem
|
||||
onClick={() => !isEditDisabled && handleEdit()}
|
||||
disabled={isEditDisabled}
|
||||
className={isEditDisabled ? "text-muted-foreground cursor-not-allowed opacity-50" : ""}
|
||||
className={
|
||||
isEditDisabled ? "text-muted-foreground cursor-not-allowed opacity-50" : ""
|
||||
}
|
||||
>
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
<span>Edit</span>
|
||||
|
|
@ -110,7 +118,11 @@ export function RowActions({
|
|||
<DropdownMenuItem
|
||||
onClick={() => !isDeleteDisabled && setIsDeleteOpen(true)}
|
||||
disabled={isDeleteDisabled}
|
||||
className={isDeleteDisabled ? "text-muted-foreground cursor-not-allowed opacity-50" : "text-destructive focus:text-destructive"}
|
||||
className={
|
||||
isDeleteDisabled
|
||||
? "text-muted-foreground cursor-not-allowed opacity-50"
|
||||
: "text-destructive focus:text-destructive"
|
||||
}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
<span>Delete</span>
|
||||
|
|
@ -150,7 +162,9 @@ export function RowActions({
|
|||
<DropdownMenuItem
|
||||
onClick={() => !isEditDisabled && handleEdit()}
|
||||
disabled={isEditDisabled}
|
||||
className={isEditDisabled ? "text-muted-foreground cursor-not-allowed opacity-50" : ""}
|
||||
className={
|
||||
isEditDisabled ? "text-muted-foreground cursor-not-allowed opacity-50" : ""
|
||||
}
|
||||
>
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
<span>Edit</span>
|
||||
|
|
@ -159,7 +173,11 @@ export function RowActions({
|
|||
<DropdownMenuItem
|
||||
onClick={() => !isDeleteDisabled && setIsDeleteOpen(true)}
|
||||
disabled={isDeleteDisabled}
|
||||
className={isDeleteDisabled ? "text-muted-foreground cursor-not-allowed opacity-50" : "text-destructive focus:text-destructive"}
|
||||
className={
|
||||
isDeleteDisabled
|
||||
? "text-muted-foreground cursor-not-allowed opacity-50"
|
||||
: "text-destructive focus:text-destructive"
|
||||
}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
<span>Delete</span>
|
||||
|
|
|
|||
|
|
@ -116,13 +116,15 @@ export default function DocumentsTable() {
|
|||
created_by_id: item.created_by_id ?? null,
|
||||
created_by_name: item.created_by_name ?? null,
|
||||
created_at: item.created_at,
|
||||
status: (item as { status?: { state: "ready" | "pending" | "processing" | "failed"; reason?: string } }).status ?? { state: "ready" as const },
|
||||
status: (
|
||||
item as {
|
||||
status?: { state: "ready" | "pending" | "processing" | "failed"; reason?: string };
|
||||
}
|
||||
).status ?? { state: "ready" as const },
|
||||
}))
|
||||
: paginatedRealtimeDocuments;
|
||||
|
||||
const displayTotal = isSearchMode
|
||||
? searchResponse?.total || 0
|
||||
: sortedRealtimeDocuments.length;
|
||||
const displayTotal = isSearchMode ? searchResponse?.total || 0 : sortedRealtimeDocuments.length;
|
||||
|
||||
const loading = isSearchMode ? isSearchLoading : realtimeLoading;
|
||||
const error = isSearchMode ? searchError : realtimeError;
|
||||
|
|
@ -149,13 +151,13 @@ export default function DocumentsTable() {
|
|||
// Filter out pending/processing documents - they cannot be deleted
|
||||
// For real-time mode, use sortedRealtimeDocuments (which has status)
|
||||
// For search mode, use searchResponse items (need to safely access status)
|
||||
const allDocs = isSearchMode
|
||||
? (searchResponse?.items || []).map(item => ({
|
||||
id: item.id,
|
||||
status: (item as { status?: { state: string } }).status,
|
||||
}))
|
||||
: sortedRealtimeDocuments.map(doc => ({ id: doc.id, status: doc.status }));
|
||||
|
||||
const allDocs = isSearchMode
|
||||
? (searchResponse?.items || []).map((item) => ({
|
||||
id: item.id,
|
||||
status: (item as { status?: { state: string } }).status,
|
||||
}))
|
||||
: sortedRealtimeDocuments.map((doc) => ({ id: doc.id, status: doc.status }));
|
||||
|
||||
const selectedDocs = allDocs.filter((doc) => selectedIds.has(doc.id));
|
||||
const deletableIds = selectedDocs
|
||||
.filter((doc) => doc.status?.state !== "pending" && doc.status?.state !== "processing")
|
||||
|
|
@ -163,7 +165,9 @@ export default function DocumentsTable() {
|
|||
const inProgressCount = selectedIds.size - deletableIds.length;
|
||||
|
||||
if (inProgressCount > 0) {
|
||||
toast.warning(`${inProgressCount} document(s) are pending or processing and cannot be deleted.`);
|
||||
toast.warning(
|
||||
`${inProgressCount} document(s) are pending or processing and cannot be deleted.`
|
||||
);
|
||||
}
|
||||
|
||||
if (deletableIds.length === 0) {
|
||||
|
|
@ -180,8 +184,9 @@ export default function DocumentsTable() {
|
|||
await deleteDocumentMutation({ id });
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
const status = (error as { response?: { status?: number } })?.response?.status
|
||||
?? (error as { status?: number })?.status;
|
||||
const status =
|
||||
(error as { response?: { status?: number } })?.response?.status ??
|
||||
(error as { status?: number })?.status;
|
||||
if (status === 409) conflictCount++;
|
||||
return false;
|
||||
}
|
||||
|
|
@ -195,13 +200,13 @@ export default function DocumentsTable() {
|
|||
} else {
|
||||
toast.error(t("delete_partial_failed"));
|
||||
}
|
||||
|
||||
|
||||
// If in search mode, refetch search results to reflect deletion
|
||||
if (isSearchMode) {
|
||||
await refetchSearch();
|
||||
}
|
||||
// Real-time mode: Electric will sync the deletion automatically
|
||||
|
||||
|
||||
setSelectedIds(new Set());
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
@ -210,21 +215,24 @@ export default function DocumentsTable() {
|
|||
};
|
||||
|
||||
// Single document delete handler for RowActions
|
||||
const handleDeleteDocument = useCallback(async (id: number): Promise<boolean> => {
|
||||
try {
|
||||
await deleteDocumentMutation({ id });
|
||||
toast.success(t("delete_success") || "Document deleted");
|
||||
// If in search mode, refetch search results to reflect deletion
|
||||
if (isSearchMode) {
|
||||
await refetchSearch();
|
||||
const handleDeleteDocument = useCallback(
|
||||
async (id: number): Promise<boolean> => {
|
||||
try {
|
||||
await deleteDocumentMutation({ id });
|
||||
toast.success(t("delete_success") || "Document deleted");
|
||||
// If in search mode, refetch search results to reflect deletion
|
||||
if (isSearchMode) {
|
||||
await refetchSearch();
|
||||
}
|
||||
// Real-time mode: Electric will sync the deletion automatically
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error("Error deleting document:", e);
|
||||
return false;
|
||||
}
|
||||
// Real-time mode: Electric will sync the deletion automatically
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error("Error deleting document:", e);
|
||||
return false;
|
||||
}
|
||||
}, [deleteDocumentMutation, isSearchMode, refetchSearch, t]);
|
||||
},
|
||||
[deleteDocumentMutation, isSearchMode, refetchSearch, t]
|
||||
);
|
||||
|
||||
const handleSortChange = useCallback((key: SortKey) => {
|
||||
setSortKey((currentKey) => {
|
||||
|
|
|
|||
|
|
@ -2,4 +2,3 @@ import { atom } from "jotai";
|
|||
|
||||
// Atom to control the connector dialog open state from anywhere in the app
|
||||
export const connectorDialogOpenAtom = atom(false);
|
||||
|
||||
|
|
|
|||
|
|
@ -191,7 +191,9 @@ export const ConnectorIndicator: FC<{ hideTrigger?: boolean }> = ({ hideTrigger
|
|||
{!hideTrigger && (
|
||||
<TooltipIconButton
|
||||
data-joyride="connector-icon"
|
||||
tooltip={hasConnectors ? `Manage ${activeConnectorsCount} connectors` : "Connect your data"}
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -346,13 +346,13 @@ export const useConnectorDialog = () => {
|
|||
const connectorId = parseInt(params.connectorId, 10);
|
||||
newConnector = result.data.find((c: SearchSourceConnector) => c.id === connectorId);
|
||||
|
||||
// If we found the connector, find the matching OAuth/Composio connector by type
|
||||
if (newConnector) {
|
||||
const connectorType = newConnector.connector_type;
|
||||
oauthConnector =
|
||||
OAUTH_CONNECTORS.find((c) => c.connectorType === connectorType) ||
|
||||
COMPOSIO_CONNECTORS.find((c) => c.connectorType === connectorType);
|
||||
}
|
||||
// If we found the connector, find the matching OAuth/Composio connector by type
|
||||
if (newConnector) {
|
||||
const connectorType = newConnector.connector_type;
|
||||
oauthConnector =
|
||||
OAUTH_CONNECTORS.find((c) => c.connectorType === connectorType) ||
|
||||
COMPOSIO_CONNECTORS.find((c) => c.connectorType === connectorType);
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have a connector yet, try to find by connector param
|
||||
|
|
@ -361,12 +361,12 @@ export const useConnectorDialog = () => {
|
|||
OAUTH_CONNECTORS.find((c) => c.id === params.connector) ||
|
||||
COMPOSIO_CONNECTORS.find((c) => c.id === params.connector);
|
||||
|
||||
if (oauthConnector) {
|
||||
const oauthConnectorType = oauthConnector.connectorType;
|
||||
newConnector = result.data.find(
|
||||
(c: SearchSourceConnector) => c.connector_type === oauthConnectorType
|
||||
);
|
||||
}
|
||||
if (oauthConnector) {
|
||||
const oauthConnectorType = oauthConnector.connectorType;
|
||||
newConnector = result.data.find(
|
||||
(c: SearchSourceConnector) => c.connector_type === oauthConnectorType
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (newConnector && oauthConnector) {
|
||||
|
|
@ -679,11 +679,11 @@ export const useConnectorDialog = () => {
|
|||
},
|
||||
});
|
||||
|
||||
const successMessage =
|
||||
currentConnectorType === "MCP_CONNECTOR"
|
||||
? `${connector.name} added successfully`
|
||||
: `${connectorTitle} connected and syncing started!`;
|
||||
toast.success(successMessage);
|
||||
const successMessage =
|
||||
currentConnectorType === "MCP_CONNECTOR"
|
||||
? `${connector.name} added successfully`
|
||||
: `${connectorTitle} connected and syncing started!`;
|
||||
toast.success(successMessage);
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete("modal");
|
||||
|
|
|
|||
|
|
@ -8,172 +8,167 @@ import { cn } from "@/lib/utils";
|
|||
|
||||
// ///////////////////////////////////////////////////////////////////////////
|
||||
// Types
|
||||
export type AnimationVariant =
|
||||
| "circle"
|
||||
| "rectangle"
|
||||
| "gif"
|
||||
| "polygon"
|
||||
| "circle-blur";
|
||||
export type AnimationVariant = "circle" | "rectangle" | "gif" | "polygon" | "circle-blur";
|
||||
export type AnimationStart =
|
||||
| "top-left"
|
||||
| "top-right"
|
||||
| "bottom-left"
|
||||
| "bottom-right"
|
||||
| "center"
|
||||
| "top-center"
|
||||
| "bottom-center"
|
||||
| "bottom-up"
|
||||
| "top-down"
|
||||
| "left-right"
|
||||
| "right-left";
|
||||
| "top-left"
|
||||
| "top-right"
|
||||
| "bottom-left"
|
||||
| "bottom-right"
|
||||
| "center"
|
||||
| "top-center"
|
||||
| "bottom-center"
|
||||
| "bottom-up"
|
||||
| "top-down"
|
||||
| "left-right"
|
||||
| "right-left";
|
||||
|
||||
interface Animation {
|
||||
name: string;
|
||||
css: string;
|
||||
name: string;
|
||||
css: string;
|
||||
}
|
||||
|
||||
// ///////////////////////////////////////////////////////////////////////////
|
||||
// Helper functions
|
||||
|
||||
const getPositionCoords = (position: AnimationStart) => {
|
||||
switch (position) {
|
||||
case "top-left":
|
||||
return { cx: "0", cy: "0" };
|
||||
case "top-right":
|
||||
return { cx: "40", cy: "0" };
|
||||
case "bottom-left":
|
||||
return { cx: "0", cy: "40" };
|
||||
case "bottom-right":
|
||||
return { cx: "40", cy: "40" };
|
||||
case "top-center":
|
||||
return { cx: "20", cy: "0" };
|
||||
case "bottom-center":
|
||||
return { cx: "20", cy: "40" };
|
||||
case "bottom-up":
|
||||
case "top-down":
|
||||
case "left-right":
|
||||
case "right-left":
|
||||
return { cx: "20", cy: "20" };
|
||||
}
|
||||
switch (position) {
|
||||
case "top-left":
|
||||
return { cx: "0", cy: "0" };
|
||||
case "top-right":
|
||||
return { cx: "40", cy: "0" };
|
||||
case "bottom-left":
|
||||
return { cx: "0", cy: "40" };
|
||||
case "bottom-right":
|
||||
return { cx: "40", cy: "40" };
|
||||
case "top-center":
|
||||
return { cx: "20", cy: "0" };
|
||||
case "bottom-center":
|
||||
return { cx: "20", cy: "40" };
|
||||
case "bottom-up":
|
||||
case "top-down":
|
||||
case "left-right":
|
||||
case "right-left":
|
||||
return { cx: "20", cy: "20" };
|
||||
}
|
||||
};
|
||||
|
||||
const generateSVG = (variant: AnimationVariant, start: AnimationStart) => {
|
||||
if (variant === "circle-blur") {
|
||||
if (start === "center") {
|
||||
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><defs><filter id="blur"><feGaussianBlur stdDeviation="2"/></filter></defs><circle cx="20" cy="20" r="18" fill="white" filter="url(%23blur)"/></svg>`;
|
||||
}
|
||||
const positionCoords = getPositionCoords(start);
|
||||
if (!positionCoords) {
|
||||
throw new Error(`Invalid start position: ${start}`);
|
||||
}
|
||||
const { cx, cy } = positionCoords;
|
||||
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><defs><filter id="blur"><feGaussianBlur stdDeviation="2"/></filter></defs><circle cx="${cx}" cy="${cy}" r="18" fill="white" filter="url(%23blur)"/></svg>`;
|
||||
}
|
||||
if (variant === "circle-blur") {
|
||||
if (start === "center") {
|
||||
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><defs><filter id="blur"><feGaussianBlur stdDeviation="2"/></filter></defs><circle cx="20" cy="20" r="18" fill="white" filter="url(%23blur)"/></svg>`;
|
||||
}
|
||||
const positionCoords = getPositionCoords(start);
|
||||
if (!positionCoords) {
|
||||
throw new Error(`Invalid start position: ${start}`);
|
||||
}
|
||||
const { cx, cy } = positionCoords;
|
||||
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><defs><filter id="blur"><feGaussianBlur stdDeviation="2"/></filter></defs><circle cx="${cx}" cy="${cy}" r="18" fill="white" filter="url(%23blur)"/></svg>`;
|
||||
}
|
||||
|
||||
if (start === "center") return;
|
||||
if (start === "center") return;
|
||||
|
||||
if (variant === "rectangle") return "";
|
||||
if (variant === "rectangle") return "";
|
||||
|
||||
const positionCoords = getPositionCoords(start);
|
||||
if (!positionCoords) {
|
||||
throw new Error(`Invalid start position: ${start}`);
|
||||
}
|
||||
const { cx, cy } = positionCoords;
|
||||
const positionCoords = getPositionCoords(start);
|
||||
if (!positionCoords) {
|
||||
throw new Error(`Invalid start position: ${start}`);
|
||||
}
|
||||
const { cx, cy } = positionCoords;
|
||||
|
||||
if (variant === "circle") {
|
||||
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><circle cx="${cx}" cy="${cy}" r="20" fill="white"/></svg>`;
|
||||
}
|
||||
if (variant === "circle") {
|
||||
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><circle cx="${cx}" cy="${cy}" r="20" fill="white"/></svg>`;
|
||||
}
|
||||
|
||||
return "";
|
||||
return "";
|
||||
};
|
||||
|
||||
const getTransformOrigin = (start: AnimationStart) => {
|
||||
switch (start) {
|
||||
case "top-left":
|
||||
return "top left";
|
||||
case "top-right":
|
||||
return "top right";
|
||||
case "bottom-left":
|
||||
return "bottom left";
|
||||
case "bottom-right":
|
||||
return "bottom right";
|
||||
case "top-center":
|
||||
return "top center";
|
||||
case "bottom-center":
|
||||
return "bottom center";
|
||||
case "bottom-up":
|
||||
case "top-down":
|
||||
case "left-right":
|
||||
case "right-left":
|
||||
return "center";
|
||||
}
|
||||
switch (start) {
|
||||
case "top-left":
|
||||
return "top left";
|
||||
case "top-right":
|
||||
return "top right";
|
||||
case "bottom-left":
|
||||
return "bottom left";
|
||||
case "bottom-right":
|
||||
return "bottom right";
|
||||
case "top-center":
|
||||
return "top center";
|
||||
case "bottom-center":
|
||||
return "bottom center";
|
||||
case "bottom-up":
|
||||
case "top-down":
|
||||
case "left-right":
|
||||
case "right-left":
|
||||
return "center";
|
||||
}
|
||||
};
|
||||
|
||||
export const createAnimation = (
|
||||
variant: AnimationVariant,
|
||||
start: AnimationStart = "center",
|
||||
blur = false,
|
||||
url?: string,
|
||||
variant: AnimationVariant,
|
||||
start: AnimationStart = "center",
|
||||
blur = false,
|
||||
url?: string
|
||||
): Animation => {
|
||||
const svg = generateSVG(variant, start);
|
||||
const transformOrigin = getTransformOrigin(start);
|
||||
const svg = generateSVG(variant, start);
|
||||
const transformOrigin = getTransformOrigin(start);
|
||||
|
||||
if (variant === "rectangle") {
|
||||
const getClipPath = (direction: AnimationStart) => {
|
||||
switch (direction) {
|
||||
case "bottom-up":
|
||||
return {
|
||||
from: "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)",
|
||||
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
|
||||
};
|
||||
case "top-down":
|
||||
return {
|
||||
from: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)",
|
||||
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
|
||||
};
|
||||
case "left-right":
|
||||
return {
|
||||
from: "polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%)",
|
||||
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
|
||||
};
|
||||
case "right-left":
|
||||
return {
|
||||
from: "polygon(100% 0%, 100% 0%, 100% 100%, 100% 100%)",
|
||||
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
|
||||
};
|
||||
case "top-left":
|
||||
return {
|
||||
from: "polygon(0% 0%, 0% 0%, 0% 0%, 0% 0%)",
|
||||
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
|
||||
};
|
||||
case "top-right":
|
||||
return {
|
||||
from: "polygon(100% 0%, 100% 0%, 100% 0%, 100% 0%)",
|
||||
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
|
||||
};
|
||||
case "bottom-left":
|
||||
return {
|
||||
from: "polygon(0% 100%, 0% 100%, 0% 100%, 0% 100%)",
|
||||
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
|
||||
};
|
||||
case "bottom-right":
|
||||
return {
|
||||
from: "polygon(100% 100%, 100% 100%, 100% 100%, 100% 100%)",
|
||||
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
from: "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)",
|
||||
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
|
||||
};
|
||||
}
|
||||
};
|
||||
if (variant === "rectangle") {
|
||||
const getClipPath = (direction: AnimationStart) => {
|
||||
switch (direction) {
|
||||
case "bottom-up":
|
||||
return {
|
||||
from: "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)",
|
||||
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
|
||||
};
|
||||
case "top-down":
|
||||
return {
|
||||
from: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)",
|
||||
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
|
||||
};
|
||||
case "left-right":
|
||||
return {
|
||||
from: "polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%)",
|
||||
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
|
||||
};
|
||||
case "right-left":
|
||||
return {
|
||||
from: "polygon(100% 0%, 100% 0%, 100% 100%, 100% 100%)",
|
||||
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
|
||||
};
|
||||
case "top-left":
|
||||
return {
|
||||
from: "polygon(0% 0%, 0% 0%, 0% 0%, 0% 0%)",
|
||||
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
|
||||
};
|
||||
case "top-right":
|
||||
return {
|
||||
from: "polygon(100% 0%, 100% 0%, 100% 0%, 100% 0%)",
|
||||
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
|
||||
};
|
||||
case "bottom-left":
|
||||
return {
|
||||
from: "polygon(0% 100%, 0% 100%, 0% 100%, 0% 100%)",
|
||||
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
|
||||
};
|
||||
case "bottom-right":
|
||||
return {
|
||||
from: "polygon(100% 100%, 100% 100%, 100% 100%, 100% 100%)",
|
||||
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
from: "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)",
|
||||
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const clipPath = getClipPath(start);
|
||||
const clipPath = getClipPath(start);
|
||||
|
||||
return {
|
||||
name: `${variant}-${start}${blur ? "-blur" : ""}`,
|
||||
css: `
|
||||
return {
|
||||
name: `${variant}-${start}${blur ? "-blur" : ""}`,
|
||||
css: `
|
||||
::view-transition-group(root) {
|
||||
animation-duration: 0.7s;
|
||||
animation-timing-function: var(--expo-out);
|
||||
|
|
@ -218,12 +213,12 @@ export const createAnimation = (
|
|||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
}
|
||||
if (variant === "circle" && start == "center") {
|
||||
return {
|
||||
name: `${variant}-${start}${blur ? "-blur" : ""}`,
|
||||
css: `
|
||||
};
|
||||
}
|
||||
if (variant === "circle" && start == "center") {
|
||||
return {
|
||||
name: `${variant}-${start}${blur ? "-blur" : ""}`,
|
||||
css: `
|
||||
::view-transition-group(root) {
|
||||
animation-duration: 0.7s;
|
||||
animation-timing-function: var(--expo-out);
|
||||
|
|
@ -268,12 +263,12 @@ export const createAnimation = (
|
|||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
}
|
||||
if (variant === "gif") {
|
||||
return {
|
||||
name: `${variant}-${start}`,
|
||||
css: `
|
||||
};
|
||||
}
|
||||
if (variant === "gif") {
|
||||
return {
|
||||
name: `${variant}-${start}`,
|
||||
css: `
|
||||
::view-transition-group(root) {
|
||||
animation-timing-function: var(--expo-in);
|
||||
}
|
||||
|
|
@ -302,14 +297,14 @@ export const createAnimation = (
|
|||
mask-size: 2000vmax;
|
||||
}
|
||||
}`,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (variant === "circle-blur") {
|
||||
if (start === "center") {
|
||||
return {
|
||||
name: `${variant}-${start}`,
|
||||
css: `
|
||||
if (variant === "circle-blur") {
|
||||
if (start === "center") {
|
||||
return {
|
||||
name: `${variant}-${start}`,
|
||||
css: `
|
||||
::view-transition-group(root) {
|
||||
animation-timing-function: var(--expo-out);
|
||||
}
|
||||
|
|
@ -334,12 +329,12 @@ export const createAnimation = (
|
|||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: `${variant}-${start}`,
|
||||
css: `
|
||||
return {
|
||||
name: `${variant}-${start}`,
|
||||
css: `
|
||||
::view-transition-group(root) {
|
||||
animation-timing-function: var(--expo-out);
|
||||
}
|
||||
|
|
@ -364,41 +359,41 @@ export const createAnimation = (
|
|||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (variant === "polygon") {
|
||||
const getPolygonClipPaths = (position: AnimationStart) => {
|
||||
switch (position) {
|
||||
case "top-left":
|
||||
return {
|
||||
darkFrom: "polygon(50% -71%, -50% 71%, -50% 71%, 50% -71%)",
|
||||
darkTo: "polygon(50% -71%, -50% 71%, 50% 171%, 171% 50%)",
|
||||
lightFrom: "polygon(171% 50%, 50% 171%, 50% 171%, 171% 50%)",
|
||||
lightTo: "polygon(171% 50%, 50% 171%, -50% 71%, 50% -71%)",
|
||||
};
|
||||
case "top-right":
|
||||
return {
|
||||
darkFrom: "polygon(150% -71%, 250% 71%, 250% 71%, 150% -71%)",
|
||||
darkTo: "polygon(150% -71%, 250% 71%, 50% 171%, -71% 50%)",
|
||||
lightFrom: "polygon(-71% 50%, 50% 171%, 50% 171%, -71% 50%)",
|
||||
lightTo: "polygon(-71% 50%, 50% 171%, 250% 71%, 150% -71%)",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
darkFrom: "polygon(50% -71%, -50% 71%, -50% 71%, 50% -71%)",
|
||||
darkTo: "polygon(50% -71%, -50% 71%, 50% 171%, 171% 50%)",
|
||||
lightFrom: "polygon(171% 50%, 50% 171%, 50% 171%, 171% 50%)",
|
||||
lightTo: "polygon(171% 50%, 50% 171%, -50% 71%, 50% -71%)",
|
||||
};
|
||||
}
|
||||
};
|
||||
if (variant === "polygon") {
|
||||
const getPolygonClipPaths = (position: AnimationStart) => {
|
||||
switch (position) {
|
||||
case "top-left":
|
||||
return {
|
||||
darkFrom: "polygon(50% -71%, -50% 71%, -50% 71%, 50% -71%)",
|
||||
darkTo: "polygon(50% -71%, -50% 71%, 50% 171%, 171% 50%)",
|
||||
lightFrom: "polygon(171% 50%, 50% 171%, 50% 171%, 171% 50%)",
|
||||
lightTo: "polygon(171% 50%, 50% 171%, -50% 71%, 50% -71%)",
|
||||
};
|
||||
case "top-right":
|
||||
return {
|
||||
darkFrom: "polygon(150% -71%, 250% 71%, 250% 71%, 150% -71%)",
|
||||
darkTo: "polygon(150% -71%, 250% 71%, 50% 171%, -71% 50%)",
|
||||
lightFrom: "polygon(-71% 50%, 50% 171%, 50% 171%, -71% 50%)",
|
||||
lightTo: "polygon(-71% 50%, 50% 171%, 250% 71%, 150% -71%)",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
darkFrom: "polygon(50% -71%, -50% 71%, -50% 71%, 50% -71%)",
|
||||
darkTo: "polygon(50% -71%, -50% 71%, 50% 171%, 171% 50%)",
|
||||
lightFrom: "polygon(171% 50%, 50% 171%, 50% 171%, 171% 50%)",
|
||||
lightTo: "polygon(171% 50%, 50% 171%, -50% 71%, 50% -71%)",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const clipPaths = getPolygonClipPaths(start);
|
||||
const clipPaths = getPolygonClipPaths(start);
|
||||
|
||||
return {
|
||||
name: `${variant}-${start}${blur ? "-blur" : ""}`,
|
||||
css: `
|
||||
return {
|
||||
name: `${variant}-${start}${blur ? "-blur" : ""}`,
|
||||
css: `
|
||||
::view-transition-group(root) {
|
||||
animation-duration: 0.7s;
|
||||
animation-timing-function: var(--expo-out);
|
||||
|
|
@ -443,35 +438,35 @@ export const createAnimation = (
|
|||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Handle circle variants with start positions using clip-path
|
||||
if (variant === "circle" && start !== "center") {
|
||||
const getClipPathPosition = (position: AnimationStart) => {
|
||||
switch (position) {
|
||||
case "top-left":
|
||||
return "0% 0%";
|
||||
case "top-right":
|
||||
return "100% 0%";
|
||||
case "bottom-left":
|
||||
return "0% 100%";
|
||||
case "bottom-right":
|
||||
return "100% 100%";
|
||||
case "top-center":
|
||||
return "50% 0%";
|
||||
case "bottom-center":
|
||||
return "50% 100%";
|
||||
default:
|
||||
return "50% 50%";
|
||||
}
|
||||
};
|
||||
// Handle circle variants with start positions using clip-path
|
||||
if (variant === "circle" && start !== "center") {
|
||||
const getClipPathPosition = (position: AnimationStart) => {
|
||||
switch (position) {
|
||||
case "top-left":
|
||||
return "0% 0%";
|
||||
case "top-right":
|
||||
return "100% 0%";
|
||||
case "bottom-left":
|
||||
return "0% 100%";
|
||||
case "bottom-right":
|
||||
return "100% 100%";
|
||||
case "top-center":
|
||||
return "50% 0%";
|
||||
case "bottom-center":
|
||||
return "50% 100%";
|
||||
default:
|
||||
return "50% 50%";
|
||||
}
|
||||
};
|
||||
|
||||
const clipPosition = getClipPathPosition(start);
|
||||
const clipPosition = getClipPathPosition(start);
|
||||
|
||||
return {
|
||||
name: `${variant}-${start}${blur ? "-blur" : ""}`,
|
||||
css: `
|
||||
return {
|
||||
name: `${variant}-${start}${blur ? "-blur" : ""}`,
|
||||
css: `
|
||||
::view-transition-group(root) {
|
||||
animation-duration: 1s;
|
||||
animation-timing-function: var(--expo-out);
|
||||
|
|
@ -516,12 +511,12 @@ export const createAnimation = (
|
|||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: `${variant}-${start}${blur ? "-blur" : ""}`,
|
||||
css: `
|
||||
return {
|
||||
name: `${variant}-${start}${blur ? "-blur" : ""}`,
|
||||
css: `
|
||||
::view-transition-group(root) {
|
||||
animation-timing-function: var(--expo-in);
|
||||
}
|
||||
|
|
@ -549,237 +544,229 @@ export const createAnimation = (
|
|||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// ///////////////////////////////////////////////////////////////////////////
|
||||
// Custom hook for theme toggle functionality
|
||||
export const useThemeToggle = ({
|
||||
variant = "circle",
|
||||
start = "center",
|
||||
blur = false,
|
||||
gifUrl = "",
|
||||
variant = "circle",
|
||||
start = "center",
|
||||
blur = false,
|
||||
gifUrl = "",
|
||||
}: {
|
||||
variant?: AnimationVariant;
|
||||
start?: AnimationStart;
|
||||
blur?: boolean;
|
||||
gifUrl?: string;
|
||||
variant?: AnimationVariant;
|
||||
start?: AnimationStart;
|
||||
blur?: boolean;
|
||||
gifUrl?: string;
|
||||
} = {}) => {
|
||||
const { theme, setTheme, resolvedTheme } = useTheme();
|
||||
const { theme, setTheme, resolvedTheme } = useTheme();
|
||||
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
// Sync isDark state with resolved theme after hydration
|
||||
useEffect(() => {
|
||||
setIsDark(resolvedTheme === "dark");
|
||||
}, [resolvedTheme]);
|
||||
// Sync isDark state with resolved theme after hydration
|
||||
useEffect(() => {
|
||||
setIsDark(resolvedTheme === "dark");
|
||||
}, [resolvedTheme]);
|
||||
|
||||
const styleId = "theme-transition-styles";
|
||||
const styleId = "theme-transition-styles";
|
||||
|
||||
const updateStyles = useCallback((css: string) => {
|
||||
if (typeof window === "undefined") return;
|
||||
const updateStyles = useCallback((css: string) => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
let styleElement = document.getElementById(styleId) as HTMLStyleElement;
|
||||
let styleElement = document.getElementById(styleId) as HTMLStyleElement;
|
||||
|
||||
if (!styleElement) {
|
||||
styleElement = document.createElement("style");
|
||||
styleElement.id = styleId;
|
||||
document.head.appendChild(styleElement);
|
||||
}
|
||||
if (!styleElement) {
|
||||
styleElement = document.createElement("style");
|
||||
styleElement.id = styleId;
|
||||
document.head.appendChild(styleElement);
|
||||
}
|
||||
|
||||
styleElement.textContent = css;
|
||||
}, []);
|
||||
styleElement.textContent = css;
|
||||
}, []);
|
||||
|
||||
const toggleTheme = useCallback(() => {
|
||||
setIsDark(!isDark);
|
||||
const toggleTheme = useCallback(() => {
|
||||
setIsDark(!isDark);
|
||||
|
||||
const animation = createAnimation(variant, start, blur, gifUrl);
|
||||
const animation = createAnimation(variant, start, blur, gifUrl);
|
||||
|
||||
updateStyles(animation.css);
|
||||
updateStyles(animation.css);
|
||||
|
||||
if (typeof window === "undefined") return;
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const switchTheme = () => {
|
||||
setTheme(theme === "light" ? "dark" : "light");
|
||||
};
|
||||
const switchTheme = () => {
|
||||
setTheme(theme === "light" ? "dark" : "light");
|
||||
};
|
||||
|
||||
if (!document.startViewTransition) {
|
||||
switchTheme();
|
||||
return;
|
||||
}
|
||||
if (!document.startViewTransition) {
|
||||
switchTheme();
|
||||
return;
|
||||
}
|
||||
|
||||
document.startViewTransition(switchTheme);
|
||||
}, [theme, setTheme, variant, start, blur, gifUrl, updateStyles, isDark]);
|
||||
document.startViewTransition(switchTheme);
|
||||
}, [theme, setTheme, variant, start, blur, gifUrl, updateStyles, isDark]);
|
||||
|
||||
const setCrazyLightTheme = useCallback(() => {
|
||||
setIsDark(false);
|
||||
const setCrazyLightTheme = useCallback(() => {
|
||||
setIsDark(false);
|
||||
|
||||
const animation = createAnimation(variant, start, blur, gifUrl);
|
||||
const animation = createAnimation(variant, start, blur, gifUrl);
|
||||
|
||||
updateStyles(animation.css);
|
||||
updateStyles(animation.css);
|
||||
|
||||
if (typeof window === "undefined") return;
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const switchTheme = () => {
|
||||
setTheme("light");
|
||||
};
|
||||
const switchTheme = () => {
|
||||
setTheme("light");
|
||||
};
|
||||
|
||||
if (!document.startViewTransition) {
|
||||
switchTheme();
|
||||
return;
|
||||
}
|
||||
if (!document.startViewTransition) {
|
||||
switchTheme();
|
||||
return;
|
||||
}
|
||||
|
||||
document.startViewTransition(switchTheme);
|
||||
}, [setTheme, variant, start, blur, gifUrl, updateStyles]);
|
||||
document.startViewTransition(switchTheme);
|
||||
}, [setTheme, variant, start, blur, gifUrl, updateStyles]);
|
||||
|
||||
const setCrazyDarkTheme = useCallback(() => {
|
||||
setIsDark(true);
|
||||
const setCrazyDarkTheme = useCallback(() => {
|
||||
setIsDark(true);
|
||||
|
||||
const animation = createAnimation(variant, start, blur, gifUrl);
|
||||
const animation = createAnimation(variant, start, blur, gifUrl);
|
||||
|
||||
updateStyles(animation.css);
|
||||
updateStyles(animation.css);
|
||||
|
||||
if (typeof window === "undefined") return;
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const switchTheme = () => {
|
||||
setTheme("dark");
|
||||
};
|
||||
const switchTheme = () => {
|
||||
setTheme("dark");
|
||||
};
|
||||
|
||||
if (!document.startViewTransition) {
|
||||
switchTheme();
|
||||
return;
|
||||
}
|
||||
if (!document.startViewTransition) {
|
||||
switchTheme();
|
||||
return;
|
||||
}
|
||||
|
||||
document.startViewTransition(switchTheme);
|
||||
}, [setTheme, variant, start, blur, gifUrl, updateStyles]);
|
||||
document.startViewTransition(switchTheme);
|
||||
}, [setTheme, variant, start, blur, gifUrl, updateStyles]);
|
||||
|
||||
const setCrazySystemTheme = useCallback(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const setCrazySystemTheme = useCallback(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const prefersDark = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)",
|
||||
).matches;
|
||||
setIsDark(prefersDark);
|
||||
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
setIsDark(prefersDark);
|
||||
|
||||
const animation = createAnimation(variant, start, blur, gifUrl);
|
||||
const animation = createAnimation(variant, start, blur, gifUrl);
|
||||
|
||||
updateStyles(animation.css);
|
||||
updateStyles(animation.css);
|
||||
|
||||
const switchTheme = () => {
|
||||
setTheme("system");
|
||||
};
|
||||
const switchTheme = () => {
|
||||
setTheme("system");
|
||||
};
|
||||
|
||||
if (!document.startViewTransition) {
|
||||
switchTheme();
|
||||
return;
|
||||
}
|
||||
if (!document.startViewTransition) {
|
||||
switchTheme();
|
||||
return;
|
||||
}
|
||||
|
||||
document.startViewTransition(switchTheme);
|
||||
}, [setTheme, variant, start, blur, gifUrl, updateStyles]);
|
||||
document.startViewTransition(switchTheme);
|
||||
}, [setTheme, variant, start, blur, gifUrl, updateStyles]);
|
||||
|
||||
return {
|
||||
isDark,
|
||||
setIsDark,
|
||||
toggleTheme,
|
||||
setCrazyLightTheme,
|
||||
setCrazyDarkTheme,
|
||||
setCrazySystemTheme,
|
||||
};
|
||||
return {
|
||||
isDark,
|
||||
setIsDark,
|
||||
toggleTheme,
|
||||
setCrazyLightTheme,
|
||||
setCrazyDarkTheme,
|
||||
setCrazySystemTheme,
|
||||
};
|
||||
};
|
||||
|
||||
// ///////////////////////////////////////////////////////////////////////////
|
||||
// Theme Toggle Button Component (Sun/Moon Style)
|
||||
|
||||
export const ThemeToggleButton = ({
|
||||
className = "",
|
||||
variant = "circle",
|
||||
start = "center",
|
||||
blur = false,
|
||||
gifUrl = "",
|
||||
className = "",
|
||||
variant = "circle",
|
||||
start = "center",
|
||||
blur = false,
|
||||
gifUrl = "",
|
||||
}: {
|
||||
className?: string;
|
||||
variant?: AnimationVariant;
|
||||
start?: AnimationStart;
|
||||
blur?: boolean;
|
||||
gifUrl?: string;
|
||||
className?: string;
|
||||
variant?: AnimationVariant;
|
||||
start?: AnimationStart;
|
||||
blur?: boolean;
|
||||
gifUrl?: string;
|
||||
}) => {
|
||||
const { isDark, toggleTheme } = useThemeToggle({
|
||||
variant,
|
||||
start,
|
||||
blur,
|
||||
gifUrl,
|
||||
});
|
||||
const clipId = useId();
|
||||
const clipPathId = `theme-toggle-clip-${clipId}`;
|
||||
const { isDark, toggleTheme } = useThemeToggle({
|
||||
variant,
|
||||
start,
|
||||
blur,
|
||||
gifUrl,
|
||||
});
|
||||
const clipId = useId();
|
||||
const clipPathId = `theme-toggle-clip-${clipId}`;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"size-10 cursor-pointer rounded-full p-2 transition-all duration-300 active:scale-95 bg-transparent",
|
||||
isDark ? "text-white" : "text-black",
|
||||
className,
|
||||
)}
|
||||
onClick={toggleTheme}
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
strokeLinecap="round"
|
||||
viewBox="0 0 32 32"
|
||||
>
|
||||
<clipPath id={clipPathId}>
|
||||
<motion.path
|
||||
animate={{ y: isDark ? 10 : 0, x: isDark ? -12 : 0 }}
|
||||
transition={{ ease: "easeInOut", duration: 0.35 }}
|
||||
d="M0-5h30a1 1 0 0 0 9 13v24H0Z"
|
||||
/>
|
||||
</clipPath>
|
||||
<g clipPath={`url(#${clipPathId})`}>
|
||||
<motion.circle
|
||||
animate={{ r: isDark ? 10 : 8 }}
|
||||
transition={{ ease: "easeInOut", duration: 0.35 }}
|
||||
cx="16"
|
||||
cy="16"
|
||||
/>
|
||||
<motion.g
|
||||
animate={{
|
||||
rotate: isDark ? -100 : 0,
|
||||
scale: isDark ? 0.5 : 1,
|
||||
opacity: isDark ? 0 : 1,
|
||||
}}
|
||||
transition={{ ease: "easeInOut", duration: 0.35 }}
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<path d="M16 5.5v-4" />
|
||||
<path d="M16 30.5v-4" />
|
||||
<path d="M1.5 16h4" />
|
||||
<path d="M26.5 16h4" />
|
||||
<path d="m23.4 8.6 2.8-2.8" />
|
||||
<path d="m5.7 26.3 2.9-2.9" />
|
||||
<path d="m5.8 5.8 2.8 2.8" />
|
||||
<path d="m23.4 23.4 2.9 2.9" />
|
||||
</motion.g>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"size-10 cursor-pointer rounded-full p-2 transition-all duration-300 active:scale-95 bg-transparent",
|
||||
isDark ? "text-white" : "text-black",
|
||||
className
|
||||
)}
|
||||
onClick={toggleTheme}
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
strokeLinecap="round"
|
||||
viewBox="0 0 32 32"
|
||||
>
|
||||
<clipPath id={clipPathId}>
|
||||
<motion.path
|
||||
animate={{ y: isDark ? 10 : 0, x: isDark ? -12 : 0 }}
|
||||
transition={{ ease: "easeInOut", duration: 0.35 }}
|
||||
d="M0-5h30a1 1 0 0 0 9 13v24H0Z"
|
||||
/>
|
||||
</clipPath>
|
||||
<g clipPath={`url(#${clipPathId})`}>
|
||||
<motion.circle
|
||||
animate={{ r: isDark ? 10 : 8 }}
|
||||
transition={{ ease: "easeInOut", duration: 0.35 }}
|
||||
cx="16"
|
||||
cy="16"
|
||||
/>
|
||||
<motion.g
|
||||
animate={{
|
||||
rotate: isDark ? -100 : 0,
|
||||
scale: isDark ? 0.5 : 1,
|
||||
opacity: isDark ? 0 : 1,
|
||||
}}
|
||||
transition={{ ease: "easeInOut", duration: 0.35 }}
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<path d="M16 5.5v-4" />
|
||||
<path d="M16 30.5v-4" />
|
||||
<path d="M1.5 16h4" />
|
||||
<path d="M26.5 16h4" />
|
||||
<path d="m23.4 8.6 2.8-2.8" />
|
||||
<path d="m5.7 26.3 2.9-2.9" />
|
||||
<path d="m5.8 5.8 2.8 2.8" />
|
||||
<path d="m23.4 23.4 2.9 2.9" />
|
||||
</motion.g>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
// ///////////////////////////////////////////////////////////////////////////
|
||||
// Backwards compatible export (alias for ThemeToggleButton with default settings)
|
||||
export function ThemeTogglerComponent() {
|
||||
return (
|
||||
<ThemeToggleButton
|
||||
variant="circle"
|
||||
start="top-right"
|
||||
className="size-8"
|
||||
/>
|
||||
);
|
||||
return <ThemeToggleButton variant="circle" start="top-right" className="size-8" />;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ export function useDocuments(
|
|||
(doc: DocumentElectric): DocumentDisplay => ({
|
||||
...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,
|
||||
status: doc.status ?? { state: "ready" },
|
||||
}),
|
||||
|
|
@ -232,7 +232,15 @@ export function useDocuments(
|
|||
const handle = await client.syncShape({
|
||||
table: "documents",
|
||||
where: `search_space_id = ${spaceId}`,
|
||||
columns: ["id", "document_type", "search_space_id", "title", "created_by_id", "created_at", "status"],
|
||||
columns: [
|
||||
"id",
|
||||
"document_type",
|
||||
"search_space_id",
|
||||
"title",
|
||||
"created_by_id",
|
||||
"created_at",
|
||||
"status",
|
||||
],
|
||||
primaryKey: ["id"],
|
||||
});
|
||||
|
||||
|
|
@ -258,7 +266,10 @@ export function useDocuments(
|
|||
// Set up live query
|
||||
const db = client.db as {
|
||||
live?: {
|
||||
query: <T>(sql: string, params?: (number | string)[]) => Promise<{
|
||||
query: <T>(
|
||||
sql: string,
|
||||
params?: (number | string)[]
|
||||
) => Promise<{
|
||||
subscribe: (cb: (result: { rows: T[] }) => void) => void;
|
||||
unsubscribe?: () => void;
|
||||
}>;
|
||||
|
|
@ -297,8 +308,7 @@ export function useDocuments(
|
|||
if (!mounted || !result.rows) return;
|
||||
|
||||
// DEBUG: Log first few raw documents to see what's coming from Electric
|
||||
console.log("[useDocuments] Raw data sample:", result.rows.slice(0, 3));
|
||||
|
||||
console.log("[useDocuments] Raw data sample:", result.rows.slice(0, 3));
|
||||
|
||||
const validItems = result.rows.filter(isValidDocument);
|
||||
const isFullySynced = syncHandleRef.current?.isUpToDate ?? false;
|
||||
|
|
@ -309,8 +319,9 @@ export function useDocuments(
|
|||
|
||||
// Fetch user names for new users (non-blocking)
|
||||
const unknownUserIds = validItems
|
||||
.filter((doc): doc is DocumentElectric & { created_by_id: string } =>
|
||||
doc.created_by_id !== null && !userCacheRef.current.has(doc.created_by_id)
|
||||
.filter(
|
||||
(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);
|
||||
|
||||
|
|
@ -326,7 +337,7 @@ 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,
|
||||
}))
|
||||
);
|
||||
|
|
@ -358,7 +369,9 @@ export function useDocuments(
|
|||
// Case 2: Electric is fully synced - TRUST IT COMPLETELY (handles bulk deletes)
|
||||
if (isFullySynced) {
|
||||
const liveDocs = deduplicateAndSort(validItems.map(electricToDisplayDoc));
|
||||
console.log(`[useDocuments] Synced update: ${liveDocs.length} docs (was ${prev.length})`);
|
||||
console.log(
|
||||
`[useDocuments] Synced update: ${liveDocs.length} docs (was ${prev.length})`
|
||||
);
|
||||
return liveDocs;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -444,9 +444,9 @@ export async function initElectric(userId: string): Promise<ElectricClient> {
|
|||
// in use-inbox.ts generating different sync keys on each render.
|
||||
// That's now fixed (rounded to midnight UTC in getSyncCutoffDate).
|
||||
// We can safely use shapeKey for fast incremental sync.
|
||||
|
||||
|
||||
const shapeKey = `${userId}_v${SYNC_VERSION}_${table}_${where?.replace(/[^a-zA-Z0-9]/g, "_") || "all"}`;
|
||||
|
||||
|
||||
// Type assertion to PGlite with electric extension
|
||||
const pgWithElectric = db as unknown as {
|
||||
electric: {
|
||||
|
|
@ -495,9 +495,7 @@ export async function initElectric(userId: string): Promise<ElectricClient> {
|
|||
// Parse the WHERE clause to build a DELETE statement
|
||||
// The WHERE clause is already validated and formatted
|
||||
await tx.exec(`DELETE FROM ${table} WHERE ${validatedWhere}`);
|
||||
debugLog(
|
||||
`[Electric] 🗑️ Cleared ${table} rows matching: ${validatedWhere}`
|
||||
);
|
||||
debugLog(`[Electric] 🗑️ Cleared ${table} rows matching: ${validatedWhere}`);
|
||||
} else {
|
||||
// No WHERE clause means we're syncing the entire table
|
||||
await tx.exec(`DELETE FROM ${table}`);
|
||||
|
|
@ -514,10 +512,7 @@ export async function initElectric(userId: string): Promise<ElectricClient> {
|
|||
},
|
||||
};
|
||||
|
||||
debugLog(
|
||||
"[Electric] syncShapeToTable config:",
|
||||
JSON.stringify(shapeConfig, null, 2)
|
||||
);
|
||||
debugLog("[Electric] syncShapeToTable config:", JSON.stringify(shapeConfig, null, 2));
|
||||
|
||||
let shape: { unsubscribe: () => void; isUpToDate: boolean; stream: unknown };
|
||||
try {
|
||||
|
|
@ -550,9 +545,7 @@ export async function initElectric(userId: string): Promise<ElectricClient> {
|
|||
retryError instanceof Error ? retryError.message : String(retryError);
|
||||
if (retryMessage.includes("Already syncing")) {
|
||||
// Still syncing - create a placeholder handle that indicates the table is being synced
|
||||
debugWarn(
|
||||
`[Electric] ${table} still syncing, creating placeholder handle`
|
||||
);
|
||||
debugWarn(`[Electric] ${table} still syncing, creating placeholder handle`);
|
||||
const placeholderHandle: SyncHandle = {
|
||||
unsubscribe: () => {
|
||||
debugLog(`[Electric] Placeholder unsubscribe for: ${cacheKey}`);
|
||||
|
|
@ -656,9 +649,7 @@ export async function initElectric(userId: string): Promise<ElectricClient> {
|
|||
|
||||
// Also check stream's isUpToDate property immediately
|
||||
if (stream?.isUpToDate) {
|
||||
debugLog(
|
||||
`[Electric] ✅ Stream isUpToDate is true immediately for ${table}`
|
||||
);
|
||||
debugLog(`[Electric] ✅ Stream isUpToDate is true immediately for ${table}`);
|
||||
resolveInitialSync();
|
||||
}
|
||||
}
|
||||
|
|
@ -671,9 +662,7 @@ export async function initElectric(userId: string): Promise<ElectricClient> {
|
|||
}
|
||||
|
||||
if (shape.isUpToDate || stream?.isUpToDate) {
|
||||
debugLog(
|
||||
`[Electric] ✅ Sync completed (detected via polling) for ${table}`
|
||||
);
|
||||
debugLog(`[Electric] ✅ Sync completed (detected via polling) for ${table}`);
|
||||
clearInterval(pollInterval);
|
||||
resolveInitialSync();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue