feat: implement responsive row action dropdowns and enhance mobile sidebar navigation

This commit is contained in:
Anish Sarkar 2025-12-28 23:25:22 +05:30
parent a10bfe32cd
commit 3bea989868
16 changed files with 256 additions and 191 deletions

View file

@ -257,8 +257,10 @@ export function DashboardClientLayout({
<div className="flex items-center justify-between w-full gap-2 px-4">
<div className="flex items-center gap-2">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="h-6" />
<DashboardBreadcrumb />
<div className="hidden md:flex items-center gap-2">
<Separator orientation="vertical" className="h-6" />
<DashboardBreadcrumb />
</div>
</div>
<div className="flex items-center gap-2">
<LanguageSwitcher />

View file

@ -278,14 +278,17 @@ export default function ConnectorsPage() {
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="mb-8 flex items-center justify-between"
className="mb-8 flex items-center justify-between gap-2"
>
<div>
<h1 className="text-3xl font-bold tracking-tight">{t("title")}</h1>
<p className="text-muted-foreground mt-2">{t("subtitle")}</p>
<h1 className="text-xl md:text-3xl font-bold tracking-tight">{t("title")}</h1>
<p className="text-xs md:text-base text-muted-foreground mt-2">{t("subtitle")}</p>
</div>
<Button onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}>
<Plus className="mr-2 h-4 w-4" />
<Button
className="h-8 text-xs px-3 md:h-10 md:text-sm md:px-4"
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
>
<Plus className="mr-2 h-3 w-3 md:h-4 md:w-4" />
{t("add_connector")}
</Button>
</motion.div>

View file

@ -75,14 +75,14 @@ export function DocumentsFilters({
return (
<motion.div
className="flex flex-wrap items-center justify-between gap-3"
className="flex flex-wrap items-center justify-start gap-3 w-full"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 30, delay: 0.1 }}
>
<div className="flex items-center gap-3">
<div className="flex items-center gap-3 flex-wrap w-full sm:w-auto">
<motion.div
className="relative"
className="relative w-full sm:w-auto"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
@ -90,7 +90,7 @@ export function DocumentsFilters({
<Input
id={`${id}-input`}
ref={inputRef}
className="peer min-w-60 ps-9"
className="peer w-full sm:min-w-60 ps-9"
value={searchValue}
onChange={(e) => onSearch(e.target.value)}
placeholder={t("filter_placeholder")}
@ -231,11 +231,11 @@ export function DocumentsFilters({
</DropdownMenu>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-3 w-full sm:w-auto sm:ml-auto">
{selectedIds.size > 0 && (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button className="ml-auto" variant="outline">
<Button className="w-full sm:w-auto" variant="outline">
<Trash
className="-ms-1 me-2 opacity-60"
size={16}

View file

@ -83,8 +83,8 @@ export function DocumentsTableShell({
const toggleAll = (checked: boolean) => {
const next = new Set(selectedIds);
if (checked) sorted.forEach((d) => next.add(d.id));
else sorted.forEach((d) => next.delete(d.id));
if (checked) sorted.forEach((d) => { next.add(d.id); });
else sorted.forEach((d) => { next.delete(d.id); });
setSelectedIds(next);
};
@ -323,26 +323,16 @@ export function DocumentsTableShell({
const icon = getDocumentTypeIcon(doc.document_type);
return (
<div key={doc.id} className="p-3">
<div className="flex items-start gap-3">
<div className="flex items-center gap-3">
<Checkbox
checked={selectedIds.has(doc.id)}
onCheckedChange={(v) => toggleOne(doc.id, !!v)}
aria-label="Select row"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
<span className="text-muted-foreground shrink-0">{icon}</span>
<div className="font-medium truncate">{doc.title}</div>
</div>
<RowActions
document={doc}
deleteDocument={deleteDocument}
refreshDocuments={async () => {
await onRefresh();
}}
searchSpaceId={searchSpaceId as string}
/>
<div className="flex items-center gap-2 min-w-0">
<span className="text-muted-foreground shrink-0">{icon}</span>
<div className="font-medium truncate">{doc.title}</div>
</div>
<div className="mt-1 flex flex-wrap items-center gap-2">
<DocumentTypeChip type={doc.document_type} />
@ -371,6 +361,14 @@ export function DocumentsTableShell({
</div>
)}
</div>
<RowActions
document={doc}
deleteDocument={deleteDocument}
refreshDocuments={async () => {
await onRefresh();
}}
searchSpaceId={searchSpaceId as string}
/>
</div>
</div>
);

View file

@ -1,6 +1,6 @@
"use client";
import { FileText, Pencil, Trash2 } from "lucide-react";
import { FileText, MoreHorizontal, Pencil, Trash2 } from "lucide-react";
import { motion } from "motion/react";
import { useRouter } from "next/navigation";
import { useState } from "react";
@ -16,6 +16,12 @@ import {
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import type { Document } from "./types";
@ -57,53 +63,108 @@ export function RowActions({
return (
<div className="flex items-center justify-end gap-1">
{/* Edit Button */}
<Tooltip>
<TooltipTrigger asChild>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted/80"
onClick={handleEdit}
{/* Desktop Actions */}
<div className="hidden md:flex items-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<Pencil className="h-4 w-4" />
<span className="sr-only">Edit Document</span>
</Button>
</motion.div>
</TooltipTrigger>
<TooltipContent side="top">
<p>Edit Document</p>
</TooltipContent>
</Tooltip>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted/80"
onClick={handleEdit}
>
<Pencil className="h-4 w-4" />
<span className="sr-only">Edit Document</span>
</Button>
</motion.div>
</TooltipTrigger>
<TooltipContent side="top">
<p>Edit Document</p>
</TooltipContent>
</Tooltip>
{/* View Metadata Button */}
<Tooltip>
<TooltipTrigger asChild>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted/80"
onClick={() => setIsMetadataOpen(true)}
<Tooltip>
<TooltipTrigger asChild>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<FileText className="h-4 w-4" />
<span className="sr-only">View Metadata</span>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted/80"
onClick={() => setIsMetadataOpen(true)}
>
<FileText className="h-4 w-4" />
<span className="sr-only">View Metadata</span>
</Button>
</motion.div>
</TooltipTrigger>
<TooltipContent side="top">
<p>View Metadata</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
onClick={() => setIsDeleteOpen(true)}
disabled={isDeleting}
>
<Trash2 className="h-4 w-4" />
<span className="sr-only">Delete</span>
</Button>
</motion.div>
</TooltipTrigger>
<TooltipContent side="top">
<p>Delete</p>
</TooltipContent>
</Tooltip>
</div>
{/* Mobile Actions Dropdown */}
<div className="flex md:hidden">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground">
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>
</motion.div>
</TooltipTrigger>
<TooltipContent side="top">
<p>View Metadata</p>
</TooltipContent>
</Tooltip>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem onClick={handleEdit}>
<Pencil className="mr-2 h-4 w-4" />
<span>Edit</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsMetadataOpen(true)}>
<FileText className="mr-2 h-4 w-4" />
<span>Metadata</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setIsDeleteOpen(true)}
className="text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<JsonMetadataViewer
title={document.title}
metadata={document.document_metadata}
@ -111,30 +172,6 @@ export function RowActions({
onOpenChange={setIsMetadataOpen}
/>
{/* Delete Button */}
<Tooltip>
<TooltipTrigger asChild>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
onClick={() => setIsDeleteOpen(true)}
disabled={isDeleting}
>
<Trash2 className="h-4 w-4" />
<span className="sr-only">Delete</span>
</Button>
</motion.div>
</TooltipTrigger>
<TooltipContent side="top">
<p>Delete</p>
</TooltipContent>
</Tooltip>
<AlertDialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}>
<AlertDialogContent>
<AlertDialogHeader>

View file

@ -225,8 +225,8 @@ export default function DocumentsTable() {
transition={{ delay: 0.1 }}
>
<div>
<h2 className="text-2xl font-bold tracking-tight">{t("title")}</h2>
<p className="text-muted-foreground">{t("subtitle")}</p>
<h2 className="text-xl md:text-2xl font-bold tracking-tight">{t("title")}</h2>
<p className="text-xs md:text-sm text-muted-foreground">{t("subtitle")}</p>
</div>
<Button onClick={refreshCurrentView} variant="outline" size="sm">
<RefreshCw className="w-4 h-4 mr-2" />

View file

@ -506,8 +506,8 @@ export default function LogsManagePage() {
transition={{ delay: 0.1 }}
>
<div>
<h2 className="text-2xl font-bold tracking-tight">{t("title")}</h2>
<p className="text-muted-foreground">{t("subtitle")}</p>
<h2 className="text-xl md:text-2xl font-bold tracking-tight">{t("title")}</h2>
<p className="text-xs md:text-sm text-muted-foreground">{t("subtitle")}</p>
</div>
<Button onClick={handleRefresh} variant="outline" size="sm">
<RefreshCw className="w-4 h-4 mr-2" />
@ -521,48 +521,10 @@ export default function LogsManagePage() {
uniqueLevels={uniqueLevels}
uniqueStatuses={uniqueStatuses}
inputRef={inputRef}
onBulkDelete={handleDeleteRows}
id={id}
/>
{/* Delete Button */}
{table.getSelectedRowModel().rows.length > 0 && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="flex justify-end"
>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">
<Trash className="-ms-1 me-2 opacity-60" size={16} strokeWidth={2} />
{t("delete_selected")}
<span className="-me-1 ms-3 inline-flex h-5 max-h-full items-center rounded border border-border bg-background px-1 font-[inherit] text-[0.625rem] font-medium text-muted-foreground/70">
{table.getSelectedRowModel().rows.length}
</span>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<div className="flex flex-col gap-2 max-sm:items-center sm:flex-row sm:gap-4">
<div className="flex size-9 shrink-0 items-center justify-center rounded-full border border-border">
<CircleAlert className="opacity-80" size={16} strokeWidth={2} />
</div>
<AlertDialogHeader>
<AlertDialogTitle>{t("confirm_title")}</AlertDialogTitle>
<AlertDialogDescription>
{t("confirm_delete_desc", { count: table.getSelectedRowModel().rows.length })}
</AlertDialogDescription>
</AlertDialogHeader>
</div>
<AlertDialogFooter>
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
<AlertDialogAction onClick={handleDeleteRows}>{t("delete")}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</motion.div>
)}
{/* Logs Table */}
<LogsTable
table={table}
@ -713,29 +675,31 @@ function LogsFilters({
uniqueLevels,
uniqueStatuses,
inputRef,
onBulkDelete,
id,
}: {
table: any;
uniqueLevels: string[];
uniqueStatuses: string[];
inputRef: React.RefObject<HTMLInputElement | null>;
onBulkDelete: () => Promise<void>;
id: string;
}) {
const t = useTranslations("logs");
return (
<motion.div
className="flex flex-wrap items-center justify-between gap-3"
className="flex flex-wrap items-center justify-start gap-3 w-full"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<div className="flex items-center gap-3">
<div className="flex items-center gap-3 flex-wrap w-full sm:w-auto">
{/* Search Input */}
<motion.div className="relative" variants={fadeInScale}>
<motion.div className="relative w-full sm:w-auto" variants={fadeInScale}>
<Input
ref={inputRef}
className={cn(
"peer min-w-60 ps-9",
"peer w-full sm:min-w-60 ps-9",
Boolean(table.getColumn("message")?.getFilterValue()) && "pe-9"
)}
value={(table.getColumn("message")?.getFilterValue() ?? "") as string}
@ -806,6 +770,39 @@ function LogsFilters({
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex items-center gap-3 w-full sm:w-auto sm:ml-auto">
{table.getSelectedRowModel().rows.length > 0 && (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button className="w-full sm:w-auto" variant="outline">
<Trash className="-ms-1 me-2 opacity-60" size={16} strokeWidth={2} />
{t("delete_selected")}
<span className="-me-1 ms-3 inline-flex h-5 max-h-full items-center rounded border border-border bg-background px-1 font-[inherit] text-[0.625rem] font-medium text-muted-foreground/70">
{table.getSelectedRowModel().rows.length}
</span>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<div className="flex flex-col gap-2 max-sm:items-center sm:flex-row sm:gap-4">
<div className="flex size-9 shrink-0 items-center justify-center rounded-full border border-border">
<CircleAlert className="opacity-80" size={16} strokeWidth={2} />
</div>
<AlertDialogHeader>
<AlertDialogTitle>{t("confirm_title")}</AlertDialogTitle>
<AlertDialogDescription>
{t("confirm_delete_desc", { count: table.getSelectedRowModel().rows.length })}
</AlertDialogDescription>
</AlertDialogHeader>
</div>
<AlertDialogFooter>
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
<AlertDialogAction onClick={onBulkDelete}>{t("delete")}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</div>
</motion.div>
);
}
@ -973,6 +970,7 @@ function LogsTable({
style={{ width: `${header.getSize()}px` }}
className={cn(
"h-12 px-4 py-3",
header.column.id === "select" ? "ps-4 pe-0" : "",
// keep Created At header from wrapping and align it
header.column.id === "created_at" ? "whitespace-nowrap text-right" : ""
)}
@ -1030,7 +1028,8 @@ function LogsTable({
<TableCell
key={cell.id}
className={cn(
"px-4 py-3 align-middle overflow-hidden",
"px-4 py-3 align-middle",
cell.column.id === "select" ? "ps-4 pe-0" : "overflow-hidden",
isCreatedAt
? "whitespace-nowrap text-xs text-muted-foreground text-right"
: "",