"use client"; import Link from "next/link"; import { useCallback, useEffect, useMemo, useState } from "react"; import { IconBrandGitlab, IconMenu2 } from "@tabler/icons-react"; import { Alert, AlertDescription } from "@kandev/ui/alert"; import { Button } from "@kandev/ui/button"; import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@kandev/ui/sheet"; import { PageTopbar } from "@/components/page-topbar"; import { fetchGitLabStatus } from "@/lib/api/domains/gitlab-api"; import type { GitLabStatus, Issue, MR } from "@/lib/types/gitlab"; import { MRList } from "@/components/gitlab/my-gitlab/mr-list"; import { IssueList } from "@/components/gitlab/my-gitlab/issue-list"; import { PresetsSidebar, type SidebarSelection, } from "@/components/gitlab/my-gitlab/presets-sidebar"; import { PresetsScopeBar } from "@/components/gitlab/my-gitlab/presets-scope-bar"; import { MR_PRESETS, ISSUE_PRESETS } from "@/components/gitlab/my-gitlab/presets"; import { useGitLabSearch } from "@/components/gitlab/my-gitlab/use-gitlab-search"; import { useSavedPresets, type SavedPreset } from "@/components/gitlab/my-gitlab/use-saved-presets"; import { useKnownProjects, resetKnownProjectsStore, } from "@/components/gitlab/my-gitlab/use-known-projects"; import { useCommittedQuery } from "@/components/gitlab/my-gitlab/use-committed-query"; import { ListToolbar } from "@/components/gitlab/my-gitlab/list-toolbar"; import { ResultsPagination } from "@/components/gitlab/my-gitlab/results-pagination"; import { SavePresetDialog } from "@/components/gitlab/my-gitlab/save-preset-dialog"; function PageHeader({ host, onOpenMobileSidebar, }: { host: string; onOpenMobileSidebar?: () => void; }) { return ( } actions={ onOpenMobileSidebar && ( ) } /> ); } function NotConnectedNotice() { return ( GitLab is not connected. Configure GitLab authentication in{" "} Settings → GitLab {" "} to see your merge requests and issues. ); } function resolveTitle(selection: SidebarSelection, saved: SavedPreset[]): string { if (selection.source === "saved") { return saved.find((p) => p.id === selection.id)?.label ?? "Saved query"; } const presets = selection.kind === "mr" ? MR_PRESETS : ISSUE_PRESETS; return ( presets.find((p) => p.value === selection.id)?.label ?? (selection.kind === "mr" ? "Merge requests" : "Issues") ); } function ResultsList({ selection, items, loading, error, }: { selection: SidebarSelection; items: Array; loading: boolean; error: string | null; }) { if (selection.kind === "mr") { return ; } return ; } type GitLabPageState = ReturnType; function useProjectOptions( selection: SidebarSelection, committedQuery: string, items: Array, projectFilter: string, ): string[] { const pageProjects = useMemo( () => items.filter((it) => !!it.project_path).map((it) => it.project_path), [items], ); const resetKey = `${selection.kind}:${selection.source}:${selection.id}:${committedQuery.trim()}`; const knownProjects = useKnownProjects(resetKey, pageProjects); return useMemo(() => { const set = new Set(knownProjects); if (projectFilter) set.add(projectFilter); return Array.from(set).sort(); }, [knownProjects, projectFilter]); } function useGitLabPageState() { const [selection, setSelection] = useState(() => ({ kind: "mr", source: "preset", id: MR_PRESETS[0]?.value ?? "", })); const { draft: customQuery, committed: committedQuery, setDraft: setCustomQuery, setImmediate: setQueryImmediate, commit: commitCustomQuery, } = useCommittedQuery(""); const [projectFilter, setProjectFilter] = useState(""); const [saveDialogOpen, setSaveDialogOpen] = useState(false); const { presets: savedPresets, save: saveSavedPreset, remove: removeSavedPreset, } = useSavedPresets(); const presets = selection.kind === "mr" ? MR_PRESETS : ISSUE_PRESETS; const search = useGitLabSearch( selection.kind, presets, selection.source === "preset" ? selection.id : "", committedQuery, projectFilter, ); const projectOptions = useProjectOptions( selection, committedQuery, search.rawItems, projectFilter, ); const title = useMemo(() => resolveTitle(selection, savedPresets), [selection, savedPresets]); const onSelect = useCallback( (s: SidebarSelection) => { setSelection(s); if (s.source === "saved") { const found = savedPresets.find((p) => p.id === s.id); setQueryImmediate(found?.customQuery ?? ""); setProjectFilter(found?.projectFilter ?? ""); return; } setQueryImmediate(""); setProjectFilter(""); }, [savedPresets, setQueryImmediate], ); // Use committedQuery (not the unflushed draft) so the saved preset always // matches what is currently displayed in the list. const canSaveCurrent = committedQuery.trim().length > 0 || projectFilter.length > 0; const suggestedLabel = committedQuery.trim() || (projectFilter ? `In ${projectFilter}` : "Saved query"); const onOpenSaveDialog = () => { if (canSaveCurrent) setSaveDialogOpen(true); }; const onConfirmSave = (label: string) => { const created = saveSavedPreset({ kind: selection.kind, label, customQuery: committedQuery, projectFilter, }); setSelection({ kind: selection.kind, source: "saved", id: created.id }); }; const onDeleteSaved = (id: string) => { removeSavedPreset(id); if (selection.source === "saved" && selection.id === id) { const fallbackPresets = selection.kind === "mr" ? MR_PRESETS : ISSUE_PRESETS; setSelection({ kind: selection.kind, source: "preset", id: fallbackPresets[0]?.value ?? "" }); setQueryImmediate(""); setProjectFilter(""); } }; return { selection, customQuery, committedQuery, setCustomQuery, commitCustomQuery, projectFilter, setProjectFilter, savedPresets, search, projectOptions, title, onSelect, canSaveCurrent, suggestedLabel, saveDialogOpen, setSaveDialogOpen, onOpenSaveDialog, onConfirmSave, onDeleteSaved, }; } function AuthenticatedLayout({ state }: { state: GitLabPageState }) { const { selection, search, projectOptions, title } = state; // When the user narrows by project, the toolbar badge shows how many of the // current page actually match (so it never reads "47" while three rows // render). Pagination still uses search.total so the user can navigate to // later pages that may contain more matches. const displayedCount = state.projectFilter ? search.items.length : search.total; return (
); } function useGitLabStatusFetch() { const [status, setStatus] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { let cancelled = false; fetchGitLabStatus({ cache: "no-store" }) .then((s) => { if (!cancelled) setStatus(s); }) .catch(() => { if (!cancelled) setStatus(null); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, []); return { status, loading }; } export function GitLabPageClient(_props: { workspaceId?: string } = {}) { const { status, loading: statusLoading } = useGitLabStatusFetch(); const state = useGitLabPageState(); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); useEffect(() => resetKnownProjectsStore, []); const connected = !!(status?.authenticated || status?.token_configured); const host = status?.host ?? "https://gitlab.com"; const onOpenMobileSidebar = useCallback(() => setMobileSidebarOpen(true), []); const { onSelect, onOpenSaveDialog } = state; const onMobileSidebarSelect = useCallback( (s: Parameters[0]) => { onSelect(s); setMobileSidebarOpen(false); }, [onSelect], ); const onMobileSaveCurrent = useCallback(() => { setMobileSidebarOpen(false); onOpenSaveDialog(); }, [onOpenSaveDialog]); return (
{statusLoading && (
Checking GitLab status…
)} {!statusLoading && !connected && (
)} {!statusLoading && connected && } Filters
); }