mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-22 23:31:12 +02:00
feat: Enhance report panel state management and dropdown functionality in sidebar
This commit is contained in:
parent
4a576f7347
commit
f73c1d83a8
4 changed files with 62 additions and 37 deletions
|
|
@ -1,5 +1,4 @@
|
||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
|
|
||||||
import { rightPanelCollapsedAtom, rightPanelTabAtom } from "@/atoms/layout/right-panel.atom";
|
import { rightPanelCollapsedAtom, rightPanelTabAtom } from "@/atoms/layout/right-panel.atom";
|
||||||
|
|
||||||
interface ReportPanelState {
|
interface ReportPanelState {
|
||||||
|
|
@ -25,11 +24,14 @@ export const reportPanelAtom = atom<ReportPanelState>(initialState);
|
||||||
/** Derived read-only atom for checking if panel is open */
|
/** Derived read-only atom for checking if panel is open */
|
||||||
export const reportPanelOpenAtom = atom((get) => get(reportPanelAtom).isOpen);
|
export const reportPanelOpenAtom = atom((get) => get(reportPanelAtom).isOpen);
|
||||||
|
|
||||||
|
/** Snapshot of `rightPanelCollapsedAtom` taken before the report opens */
|
||||||
|
const preReportCollapsedAtom = atom<boolean | null>(null);
|
||||||
|
|
||||||
/** Action atom to open the report panel with a specific report */
|
/** Action atom to open the report panel with a specific report */
|
||||||
export const openReportPanelAtom = atom(
|
export const openReportPanelAtom = atom(
|
||||||
null,
|
null,
|
||||||
(
|
(
|
||||||
_get,
|
get,
|
||||||
set,
|
set,
|
||||||
{
|
{
|
||||||
reportId,
|
reportId,
|
||||||
|
|
@ -38,6 +40,9 @@ export const openReportPanelAtom = atom(
|
||||||
shareToken,
|
shareToken,
|
||||||
}: { reportId: number; title: string; wordCount?: number; shareToken?: string | null }
|
}: { reportId: number; title: string; wordCount?: number; shareToken?: string | null }
|
||||||
) => {
|
) => {
|
||||||
|
if (!get(reportPanelAtom).isOpen) {
|
||||||
|
set(preReportCollapsedAtom, get(rightPanelCollapsedAtom));
|
||||||
|
}
|
||||||
set(reportPanelAtom, {
|
set(reportPanelAtom, {
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
reportId,
|
reportId,
|
||||||
|
|
@ -47,12 +52,16 @@ export const openReportPanelAtom = atom(
|
||||||
});
|
});
|
||||||
set(rightPanelTabAtom, "report");
|
set(rightPanelTabAtom, "report");
|
||||||
set(rightPanelCollapsedAtom, false);
|
set(rightPanelCollapsedAtom, false);
|
||||||
set(documentsSidebarOpenAtom, true);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Action atom to close the report panel */
|
/** Action atom to close the report panel */
|
||||||
export const closeReportPanelAtom = atom(null, (_get, set) => {
|
export const closeReportPanelAtom = atom(null, (get, set) => {
|
||||||
set(reportPanelAtom, initialState);
|
set(reportPanelAtom, initialState);
|
||||||
set(rightPanelTabAtom, "sources");
|
set(rightPanelTabAtom, "sources");
|
||||||
|
const prev = get(preReportCollapsedAtom);
|
||||||
|
if (prev !== null) {
|
||||||
|
set(rightPanelCollapsedAtom, prev);
|
||||||
|
set(preReportCollapsedAtom, null);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -627,7 +627,7 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="start"
|
align="start"
|
||||||
sideOffset={12}
|
sideOffset={12}
|
||||||
className="w-[calc(100vw-2rem)] max-w-56 sm:max-w-72 sm:w-72 p-0"
|
className="w-[calc(100vw-2rem)] max-w-56 sm:max-w-72 sm:w-72 p-0 select-none"
|
||||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between px-2.5 py-2 sm:px-3 sm:py-2.5 border-b">
|
<div className="flex items-center justify-between px-2.5 py-2 sm:px-3 sm:py-2.5 border-b">
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { useLongPress } from "@/hooks/use-long-press";
|
import { useLongPress } from "@/hooks/use-long-press";
|
||||||
|
|
@ -20,6 +19,8 @@ interface ChatListItemProps {
|
||||||
name: string;
|
name: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
|
dropdownOpen?: boolean;
|
||||||
|
onDropdownOpenChange?: (open: boolean) => void;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
onRename?: () => void;
|
onRename?: () => void;
|
||||||
onArchive?: () => void;
|
onArchive?: () => void;
|
||||||
|
|
@ -30,6 +31,8 @@ export function ChatListItem({
|
||||||
name,
|
name,
|
||||||
isActive,
|
isActive,
|
||||||
archived,
|
archived,
|
||||||
|
dropdownOpen: controlledOpen,
|
||||||
|
onDropdownOpenChange,
|
||||||
onClick,
|
onClick,
|
||||||
onRename,
|
onRename,
|
||||||
onArchive,
|
onArchive,
|
||||||
|
|
@ -37,11 +40,13 @@ export function ChatListItem({
|
||||||
}: ChatListItemProps) {
|
}: ChatListItemProps) {
|
||||||
const t = useTranslations("sidebar");
|
const t = useTranslations("sidebar");
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [internalOpen, setInternalOpen] = useState(false);
|
||||||
|
const dropdownOpen = controlledOpen ?? internalOpen;
|
||||||
|
const setDropdownOpen = onDropdownOpenChange ?? setInternalOpen;
|
||||||
const animatedName = useTypewriter(name);
|
const animatedName = useTypewriter(name);
|
||||||
|
|
||||||
const { handlers: longPressHandlers, wasLongPress } = useLongPress(
|
const { handlers: longPressHandlers, wasLongPress } = useLongPress(
|
||||||
useCallback(() => setDropdownOpen(true), [])
|
useCallback(() => setDropdownOpen(true), [setDropdownOpen])
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
|
|
@ -68,12 +73,12 @@ export function ChatListItem({
|
||||||
{/* Actions dropdown - trigger hidden on mobile, long-press opens it instead */}
|
{/* Actions dropdown - trigger hidden on mobile, long-press opens it instead */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute right-0 top-0 bottom-0 flex items-center pr-1 pl-6 rounded-r-md",
|
"pointer-events-none absolute right-0 top-0 bottom-0 flex items-center pr-1 pl-6 rounded-r-md",
|
||||||
isActive
|
isActive
|
||||||
? "bg-gradient-to-l from-accent from-60% to-transparent"
|
? "bg-gradient-to-l from-accent from-60% to-transparent"
|
||||||
: "bg-gradient-to-l from-sidebar from-60% to-transparent group-hover/item:from-accent",
|
: "bg-gradient-to-l from-sidebar from-60% to-transparent group-hover/item:from-accent",
|
||||||
isMobile
|
isMobile
|
||||||
? "opacity-0 pointer-events-none"
|
? "opacity-0"
|
||||||
: isActive
|
: isActive
|
||||||
? "opacity-100"
|
? "opacity-100"
|
||||||
: "opacity-0 group-hover/item:opacity-100"
|
: "opacity-0 group-hover/item:opacity-100"
|
||||||
|
|
@ -81,7 +86,7 @@ export function ChatListItem({
|
||||||
>
|
>
|
||||||
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" className="h-6 w-6">
|
<Button variant="ghost" size="icon" className="pointer-events-auto h-6 w-6">
|
||||||
<MoreHorizontal className="h-3.5 w-3.5 text-muted-foreground" />
|
<MoreHorizontal className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
<span className="sr-only">{t("more_options")}</span>
|
<span className="sr-only">{t("more_options")}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -118,8 +123,7 @@ export function ChatListItem({
|
||||||
)}
|
)}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{onArchive && onDelete && <DropdownMenuSeparator />}
|
{onDelete && (
|
||||||
{onDelete && (
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { FolderOpen, PenSquare } from "lucide-react";
|
import { FolderOpen, PenSquare } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
|
@ -89,6 +90,7 @@ export function Sidebar({
|
||||||
isResizing = false,
|
isResizing = false,
|
||||||
}: SidebarProps) {
|
}: SidebarProps) {
|
||||||
const t = useTranslations("sidebar");
|
const t = useTranslations("sidebar");
|
||||||
|
const [openDropdownChatId, setOpenDropdownChatId] = useState<number | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -103,6 +105,12 @@ export function Sidebar({
|
||||||
{/* Resize handle on right border */}
|
{/* Resize handle on right border */}
|
||||||
{!isCollapsed && onResizeMouseDown && (
|
{!isCollapsed && onResizeMouseDown && (
|
||||||
<div
|
<div
|
||||||
|
role="slider"
|
||||||
|
aria-label="Resize sidebar"
|
||||||
|
aria-valuemin={0}
|
||||||
|
aria-valuemax={100}
|
||||||
|
aria-valuenow={50}
|
||||||
|
tabIndex={0}
|
||||||
onMouseDown={onResizeMouseDown}
|
onMouseDown={onResizeMouseDown}
|
||||||
className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-border active:bg-border z-10"
|
className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-border active:bg-border z-10"
|
||||||
/>
|
/>
|
||||||
|
|
@ -209,18 +217,20 @@ export function Sidebar({
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col gap-0.5 max-h-full overflow-y-auto scrollbar-thin scrollbar-thumb-muted-foreground/20 scrollbar-track-transparent ${sharedChats.length > 4 ? "pb-8" : ""}`}
|
className={`flex flex-col gap-0.5 max-h-full overflow-y-auto scrollbar-thin scrollbar-thumb-muted-foreground/20 scrollbar-track-transparent ${sharedChats.length > 4 ? "pb-8" : ""}`}
|
||||||
>
|
>
|
||||||
{sharedChats.slice(0, 20).map((chat) => (
|
{sharedChats.slice(0, 20).map((chat) => (
|
||||||
<ChatListItem
|
<ChatListItem
|
||||||
key={chat.id}
|
key={chat.id}
|
||||||
name={chat.name}
|
name={chat.name}
|
||||||
isActive={chat.id === activeChatId}
|
isActive={chat.id === activeChatId}
|
||||||
archived={chat.archived}
|
archived={chat.archived}
|
||||||
onClick={() => onChatSelect(chat)}
|
dropdownOpen={openDropdownChatId === chat.id}
|
||||||
onRename={() => onChatRename?.(chat)}
|
onDropdownOpenChange={(open) => setOpenDropdownChatId(open ? chat.id : null)}
|
||||||
onArchive={() => onChatArchive?.(chat)}
|
onClick={() => onChatSelect(chat)}
|
||||||
onDelete={() => onChatDelete?.(chat)}
|
onRename={() => onChatRename?.(chat)}
|
||||||
/>
|
onArchive={() => onChatArchive?.(chat)}
|
||||||
))}
|
onDelete={() => onChatDelete?.(chat)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
{/* Gradient fade indicator when more than 4 items */}
|
{/* Gradient fade indicator when more than 4 items */}
|
||||||
{sharedChats.length > 4 && (
|
{sharedChats.length > 4 && (
|
||||||
|
|
@ -281,18 +291,20 @@ export function Sidebar({
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col gap-0.5 h-full overflow-y-auto scrollbar-thin scrollbar-thumb-muted-foreground/20 scrollbar-track-transparent ${chats.length > 4 ? "pb-8" : ""}`}
|
className={`flex flex-col gap-0.5 h-full overflow-y-auto scrollbar-thin scrollbar-thumb-muted-foreground/20 scrollbar-track-transparent ${chats.length > 4 ? "pb-8" : ""}`}
|
||||||
>
|
>
|
||||||
{chats.slice(0, 20).map((chat) => (
|
{chats.slice(0, 20).map((chat) => (
|
||||||
<ChatListItem
|
<ChatListItem
|
||||||
key={chat.id}
|
key={chat.id}
|
||||||
name={chat.name}
|
name={chat.name}
|
||||||
isActive={chat.id === activeChatId}
|
isActive={chat.id === activeChatId}
|
||||||
archived={chat.archived}
|
archived={chat.archived}
|
||||||
onClick={() => onChatSelect(chat)}
|
dropdownOpen={openDropdownChatId === chat.id}
|
||||||
onRename={() => onChatRename?.(chat)}
|
onDropdownOpenChange={(open) => setOpenDropdownChatId(open ? chat.id : null)}
|
||||||
onArchive={() => onChatArchive?.(chat)}
|
onClick={() => onChatSelect(chat)}
|
||||||
onDelete={() => onChatDelete?.(chat)}
|
onRename={() => onChatRename?.(chat)}
|
||||||
/>
|
onArchive={() => onChatArchive?.(chat)}
|
||||||
))}
|
onDelete={() => onChatDelete?.(chat)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
{/* Gradient fade indicator when more than 4 items */}
|
{/* Gradient fade indicator when more than 4 items */}
|
||||||
{chats.length > 4 && (
|
{chats.length > 4 && (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue