"use client"; import { useCallback, useMemo, type ReactNode } from "react"; import { ListTodo, Plus } from "lucide-react"; import { Button } from "@multica/ui/components/ui/button"; import { Skeleton } from "@multica/ui/components/ui/skeleton"; import { cn } from "@multica/ui/lib/utils"; import { useWorkspaceId } from "@multica/core/hooks"; import { ViewStoreProvider } from "@multica/core/issues/stores/view-store-context"; import { getIssueSurfaceViewStore } from "@multica/core/issues/stores/surface-view-store"; import { issueScopeKey } from "@multica/core/issues/surface/scope"; import type { Issue } from "@multica/core/types"; import { BoardView } from "../components/board-view"; import { BatchActionToolbar } from "../components/batch-action-toolbar"; import { GanttView } from "../components/gantt-view"; import { IssuesHeader } from "../components/issues-header"; import { ListView } from "../components/list-view"; import { SwimLaneView } from "../components/swimlane-view"; import { useT } from "../../i18n"; import { IssueSurfaceActionsProvider } from "./actions-context"; import { IssueSurfaceSelectionProvider } from "./selection-context"; import type { IssueCreateDefaults, IssueSurfaceProps } from "./types"; import { useIssueSurfaceController, type IssueSurfaceController, } from "./use-issue-surface-controller"; export interface IssueSurfaceRenderContext { controller: IssueSurfaceController; issues: Issue[]; } interface IssueSurfaceComponentProps extends IssueSurfaceProps { renderHeader?: (context: IssueSurfaceRenderContext) => ReactNode; renderEmpty?: (context: IssueSurfaceRenderContext) => ReactNode; renderLoading?: (context: IssueSurfaceRenderContext) => ReactNode; clientFilter?: (issue: Issue) => boolean; showClientEmpty?: (context: IssueSurfaceRenderContext) => boolean; batchToolbar?: "always" | "list" | "never"; contentClassName?: string; } export function IssueSurface({ scope, modes, surfaceKey, createDefaults, renderHeader, renderEmpty, renderLoading, clientFilter, showClientEmpty, batchToolbar = "always", contentClassName, }: IssueSurfaceComponentProps) { const wsId = useWorkspaceId(); const resolvedSurfaceKey = surfaceKey ?? issueScopeKey(scope); const store = useMemo( () => getIssueSurfaceViewStore(resolvedSurfaceKey), [resolvedSurfaceKey], ); return ( {/* Remount on data-window change: the list queries keep the previous key's data as a placeholder (keepPreviousData) so sort/filter changes within ONE surface never flash a skeleton — but reusing the mounted observer across windows made project A's cards impersonate project B (with isLoading=false, so no skeleton either) until B's fetch landed. A window-keyed remount gives the new window a fresh observer: cold window → skeleton, warm window → instant cache hit. The window identity is wsId + scope — wsId is required because the workspace layout does not remount on workspace switch and two workspaces share the same scope key (e.g. "workspace:all"). Keyed by data identity, not surfaceKey (view-preference identity). */} ); } function IssueSurfaceContent({ scope, modes, createDefaults, renderHeader, renderEmpty, renderLoading, clientFilter, showClientEmpty, batchToolbar, contentClassName, }: Omit) { const { t } = useT("projects"); const controller = useIssueSurfaceController({ scope, modes, createDefaults, }); const issues = useMemo( () => clientFilter ? controller.issues.filter((issue) => clientFilter(issue)) : controller.issues, [clientFilter, controller.issues], ); const swimlaneIssues = useMemo( () => clientFilter ? controller.swimlaneIssues.filter((issue) => clientFilter(issue)) : controller.swimlaneIssues, [clientFilter, controller.swimlaneIssues], ); const renderContext = useMemo( () => ({ controller, issues }), [controller, issues], ); const openCreateIssue = useCallback( (defaults?: IssueCreateDefaults) => { controller.openCreateIssue(defaults); }, [controller], ); const shouldShowClientEmpty = !!clientFilter && issues.length === 0 && (showClientEmpty ? showClientEmpty(renderContext) : true); const shouldShowBatchToolbar = batchToolbar !== "never" && (batchToolbar === "always" || controller.viewMode === "list"); return ( {renderHeader ? ( renderHeader(renderContext) ) : ( )} {controller.isLoading ? ( renderLoading ? ( renderLoading(renderContext) ) : ( ) ) : controller.isEmpty || shouldShowClientEmpty ? ( renderEmpty ? ( renderEmpty(renderContext) ) : (

{t(($) => $.detail.empty_issues_title)}

{t(($) => $.detail.empty_issues_hint)}

) ) : (
{controller.viewMode === "board" && ( group.issues) : issues } assigneeGroups={controller.assigneeGroups} assigneeGroupQueryKey={controller.assigneeGroupQueryKey} assigneeGroupFilter={controller.assigneeGroupFilter} visibleStatuses={controller.visibleStatuses} hiddenStatuses={controller.hiddenStatuses} onMoveIssue={controller.moveIssue} childProgressMap={controller.childProgressMap} projectMap={controller.projectMap} myIssuesScope={controller.loadMoreScope} myIssuesFilter={controller.loadMoreFilter} sort={controller.sort} projectId={controller.projectId} onCreateIssue={openCreateIssue} /> )} {controller.viewMode === "list" && ( )} {controller.viewMode === "gantt" && ( )} {controller.viewMode === "swimlane" && ( )}
)} {shouldShowBatchToolbar && }
); } function IssueSurfaceSkeleton({ mode }: { mode: string }) { if (mode === "list") { return (
{Array.from({ length: 4 }).map((_, i) => ( ))}
); } return (
{Array.from({ length: 5 }).map((_, i) => (
))}
); }