feat: add task comments feature with CRUD operations
- Introduced a new `task_comments` table in the database schema. - Implemented task comments API endpoints for listing, creating, and deleting comments. - Enhanced the task detail dialog to display comments and allow users to add new comments. - Updated task row component to handle click events for viewing task details. - Added a theme provider to manage light/dark mode across the application. - Refactored Milkdown editor to use Crepe for improved markdown editing experience. - Updated global styles to accommodate new editor and theme changes. - Enhanced task filtering and sorting functionality in the tasks page.
This commit is contained in:
@@ -9,6 +9,13 @@ import {
|
||||
PanelLeft,
|
||||
ChevronUp,
|
||||
ChevronDown,
|
||||
Settings,
|
||||
Sparkles,
|
||||
Check,
|
||||
Sun,
|
||||
Moon,
|
||||
Monitor,
|
||||
Palette
|
||||
} from 'lucide-react';
|
||||
import { trpc } from '@/lib/trpc';
|
||||
import {
|
||||
@@ -25,7 +32,30 @@ import {
|
||||
SidebarProvider,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { AIChatPanel } from '@/components/ai/AIChatPanel';
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ to: '/', icon: House, label: 'Home' },
|
||||
@@ -72,6 +102,21 @@ export function AppShell({ children }: AppShellProps) {
|
||||
setSidebarCollapsedMutation.mutate({ collapsed: !value });
|
||||
};
|
||||
|
||||
// AI token dialog state (shared between sidebar gear menu and AIChatPanel prompt)
|
||||
const [tokenDialogOpen, setTokenDialogOpen] = useState(false);
|
||||
const [tokenInput, setTokenInput] = useState('');
|
||||
const [saved, setSaved] = useState(false);
|
||||
const hasTokenQuery = trpc.ai.hasToken.useQuery();
|
||||
const utils = trpc.useUtils();
|
||||
const setTokenMutation = trpc.ai.setToken.useMutation({
|
||||
onSuccess: () => {
|
||||
setSaved(true);
|
||||
setTokenInput('');
|
||||
void utils.ai.hasToken.invalidate();
|
||||
setTimeout(() => setSaved(false), 2000);
|
||||
},
|
||||
});
|
||||
|
||||
// Curtain is disabled on home page and on /projects without a selected project
|
||||
const searchObj = routerState.location.search as Record<string, unknown>;
|
||||
const curtainEnabled =
|
||||
@@ -141,11 +186,15 @@ export function AppShell({ children }: AppShellProps) {
|
||||
}, [openCurtain, closeCurtain]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarProvider open={open} onOpenChange={handleOpenChange}>
|
||||
<AppSidebar currentPath={currentPath} />
|
||||
<AppSidebar
|
||||
currentPath={currentPath}
|
||||
setTokenDialogOpen={setTokenDialogOpen}
|
||||
/>
|
||||
<SidebarInset className="overflow-hidden">
|
||||
{/* AI Chat layer: always mounted behind the content panel */}
|
||||
<AIChatPanel />
|
||||
<AIChatPanel onOpenSettings={() => setTokenDialogOpen(true)} />
|
||||
|
||||
{/* Content panel: slides down to reveal chat */}
|
||||
<motion.div
|
||||
@@ -158,12 +207,12 @@ export function AppShell({ children }: AppShellProps) {
|
||||
<div className={`absolute right-0 top-0 flex items-end justify-center pt-8 pointer-events-none select-none${!curtainEnabled ? ' hidden' : ''}`}>
|
||||
<div className="flex flex-col items-center gap-1.5 pr-2">
|
||||
{curtainOpen ? (
|
||||
<ChevronDown size={10} className="text-muted-foreground/30" />
|
||||
<ChevronDown size={10} />
|
||||
) : (
|
||||
<ChevronUp size={10} className="text-muted-foreground/30" />
|
||||
<ChevronUp size={10} />
|
||||
)}
|
||||
<span
|
||||
className="text-[9px] text-muted-foreground/30 tracking-widest uppercase font-medium"
|
||||
className="text-[9px] tracking-widest uppercase font-medium"
|
||||
style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)' }}
|
||||
>
|
||||
{curtainOpen ? 'back to app' : 'scrolling up for Adiuva'}
|
||||
@@ -173,11 +222,62 @@ export function AppShell({ children }: AppShellProps) {
|
||||
</motion.div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
|
||||
{/* AI Token Dialog — rendered outside Sidebar to avoid layout conflicts */}
|
||||
<Dialog open={tokenDialogOpen} onOpenChange={(open) => {
|
||||
setTokenDialogOpen(open);
|
||||
if (!open) { setTokenInput(''); setSaved(false); }
|
||||
}}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>AI Provider</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure your AI provider credentials for chat, summaries, and suggestions.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">GitHub Copilot Token</label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Paste your token here"
|
||||
value={tokenInput}
|
||||
onChange={(e) => setTokenInput(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Your token is stored securely in the OS keychain.
|
||||
{hasTokenQuery.data === true && (
|
||||
<span className="text-green-600 ml-1">A token is currently stored.</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
{saved && (
|
||||
<span className="flex items-center gap-1 text-sm text-green-600 mr-auto">
|
||||
<Check size={14} />
|
||||
Saved
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
disabled={!tokenInput.trim() || setTokenMutation.isPending}
|
||||
onClick={() => setTokenMutation.mutate({ token: tokenInput.trim() })}
|
||||
>
|
||||
{setTokenMutation.isPending ? 'Saving...' : 'Save Token'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AppSidebar({ currentPath }: { currentPath: string }) {
|
||||
interface AppSidebarProps {
|
||||
currentPath: string;
|
||||
setTokenDialogOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
function AppSidebar({ currentPath, setTokenDialogOpen }: AppSidebarProps) {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<Sidebar collapsible="icon">
|
||||
@@ -241,9 +341,50 @@ function AppSidebar({ currentPath }: { currentPath: string }) {
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
|
||||
{/* Collapse toggle — spec: useSidebar() + custom trigger */}
|
||||
{/* Settings gear + Collapse toggle */}
|
||||
<SidebarFooter>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton tooltip="Settings">
|
||||
<Settings />
|
||||
<span>Settings</span>
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="right" align="end" className="w-56">
|
||||
<DropdownMenuItem onSelect={() => setTokenDialogOpen(true)}>
|
||||
<Sparkles className="mr-2 size-4" />
|
||||
AI Provider
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Palette className="mr-2 size-4" />
|
||||
<span>Theme</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem onSelect={() => setTheme('light')}>
|
||||
<Sun className="mr-2 size-4" />
|
||||
Light
|
||||
{theme === 'light' && <Check className="ml-auto size-4" />}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => setTheme('dark')}>
|
||||
<Moon className="mr-2 size-4" />
|
||||
Dark
|
||||
{theme === 'dark' && <Check className="ml-auto size-4" />}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => setTheme('system')}>
|
||||
<Monitor className="mr-2 size-4" />
|
||||
System
|
||||
{theme === 'system' && <Check className="ml-auto size-4" />}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton onClick={toggleSidebar} tooltip="Toggle Sidebar">
|
||||
<PanelLeft />
|
||||
@@ -252,6 +393,7 @@ function AppSidebar({ currentPath }: { currentPath: string }) {
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarFooter>
|
||||
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user