Fix: /dashboard 500 – buttonCls aus Client-Modul herausgelöst
Server-Komponenten (Dashboard, Aufträge, Suche, Checklisten, Auftragsdetail) riefen buttonCls() aus einem "use client"-Modul auf; das bricht zur Laufzeit (gemeldet von L8). Neu: src/components/work-orders/button-cls.ts (server-sicher), action-form re-exportiert. Nachweis: authentifizierter HTTP-Smoke (Backoffice 13 Seiten, Monteur 6 Seiten) alle 200, /dashboard vorher 500. tsc/lint grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
|||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { PageHead } from "@/components/mockup-ui";
|
import { PageHead } from "@/components/mockup-ui";
|
||||||
import { buttonCls } from "@/components/work-orders/action-form";
|
import { buttonCls } from "@/components/work-orders/button-cls";
|
||||||
import { pageContext } from "@/components/work-orders/page-context";
|
import { pageContext } from "@/components/work-orders/page-context";
|
||||||
import { Field, inputCls } from "@/components/work-orders/ui";
|
import { Field, inputCls } from "@/components/work-orders/ui";
|
||||||
import { isoDay, parseListParams, toQuery, type Preset } from "@/lib/work-orders/filters";
|
import { isoDay, parseListParams, toQuery, type Preset } from "@/lib/work-orders/filters";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Link from "next/link";
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
import { PageHead } from "@/components/mockup-ui";
|
import { PageHead } from "@/components/mockup-ui";
|
||||||
import { buttonCls } from "@/components/work-orders/action-form";
|
import { buttonCls } from "@/components/work-orders/button-cls";
|
||||||
import { pageContext } from "@/components/work-orders/page-context";
|
import { pageContext } from "@/components/work-orders/page-context";
|
||||||
import { Empty, Field, inputCls, Section, StatusBadge } from "@/components/work-orders/ui";
|
import { Empty, Field, inputCls, Section, StatusBadge } from "@/components/work-orders/ui";
|
||||||
import { isoDay, parseListParams } from "@/lib/work-orders/filters";
|
import { isoDay, parseListParams } from "@/lib/work-orders/filters";
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { z } from "zod";
|
|||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import { PageHead, Pill } from "@/components/mockup-ui";
|
import { PageHead, Pill } from "@/components/mockup-ui";
|
||||||
import { Modal } from "@/components/modal";
|
import { Modal } from "@/components/modal";
|
||||||
import { ActionForm, buttonCls } from "@/components/work-orders/action-form";
|
import { ActionForm } from "@/components/work-orders/action-form";
|
||||||
|
import { buttonCls } from "@/components/work-orders/button-cls";
|
||||||
import { SettingsTemplatesNav, templatesPageContext } from "@/components/work-orders/settings-nav";
|
import { SettingsTemplatesNav, templatesPageContext } from "@/components/work-orders/settings-nav";
|
||||||
import { Check, Empty, Field, inputCls, Section } from "@/components/work-orders/ui";
|
import { Check, Empty, Field, inputCls, Section } from "@/components/work-orders/ui";
|
||||||
import { DEFAULT_CHECKLIST_ITEMS, DEFAULT_REQUIRED_PHOTOS } from "@/lib/work-orders/defaults";
|
import { DEFAULT_CHECKLIST_ITEMS, DEFAULT_REQUIRED_PHOTOS } from "@/lib/work-orders/defaults";
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { notFound } from "next/navigation";
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { ArrowLeft, Pencil, Siren, UsersRound } from "lucide-react";
|
import { ArrowLeft, Pencil, Siren, UsersRound } from "lucide-react";
|
||||||
import { Modal } from "@/components/modal";
|
import { Modal } from "@/components/modal";
|
||||||
import { ActionForm, buttonCls } from "@/components/work-orders/action-form";
|
import { ActionForm } from "@/components/work-orders/action-form";
|
||||||
|
import { buttonCls } from "@/components/work-orders/button-cls";
|
||||||
import {
|
import {
|
||||||
ChecklistTab,
|
ChecklistTab,
|
||||||
DocumentsTab,
|
DocumentsTab,
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { getTranslations } from "next-intl/server";
|
|||||||
import { LayoutGrid, Plus, RefreshCw, Rows3, X } from "lucide-react";
|
import { LayoutGrid, Plus, RefreshCw, Rows3, X } from "lucide-react";
|
||||||
import { PageHead } from "@/components/mockup-ui";
|
import { PageHead } from "@/components/mockup-ui";
|
||||||
import { Modal } from "@/components/modal";
|
import { Modal } from "@/components/modal";
|
||||||
import { ActionForm, buttonCls } from "@/components/work-orders/action-form";
|
import { ActionForm } from "@/components/work-orders/action-form";
|
||||||
|
import { buttonCls } from "@/components/work-orders/button-cls";
|
||||||
import { pageContext } from "@/components/work-orders/page-context";
|
import { pageContext } from "@/components/work-orders/page-context";
|
||||||
import { Empty, Field, GroupIcon, inputCls, LinkTabs } from "@/components/work-orders/ui";
|
import { Empty, Field, GroupIcon, inputCls, LinkTabs } from "@/components/work-orders/ui";
|
||||||
import { WorkOrderFields } from "@/components/work-orders/work-order-fields";
|
import { WorkOrderFields } from "@/components/work-orders/work-order-fields";
|
||||||
|
|||||||
@@ -6,22 +6,12 @@ import { AlertCircle, CheckCircle2 } from "lucide-react";
|
|||||||
import { IDLE_STATE, type ActionState } from "@/lib/work-orders/action-state";
|
import { IDLE_STATE, type ActionState } from "@/lib/work-orders/action-state";
|
||||||
import type { CompletionBlocker } from "@/lib/work-orders/status";
|
import type { CompletionBlocker } from "@/lib/work-orders/status";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { buttonCls, type ButtonVariant } from "@/components/work-orders/button-cls";
|
||||||
|
|
||||||
|
export { buttonCls };
|
||||||
|
|
||||||
type Action = (prev: ActionState, fd: FormData) => Promise<ActionState>;
|
type Action = (prev: ActionState, fd: FormData) => Promise<ActionState>;
|
||||||
|
|
||||||
const VARIANTS = {
|
|
||||||
primary: "bg-cta text-cta-foreground hover:opacity-90",
|
|
||||||
default: "bg-primary text-primary-foreground hover:opacity-90",
|
|
||||||
outline: "border border-border bg-background hover:bg-muted",
|
|
||||||
danger: "border border-[var(--risk)] bg-background text-[var(--risk)] hover:bg-muted",
|
|
||||||
ghost: "hover:bg-muted text-muted-foreground",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const buttonCls = (variant: keyof typeof VARIANTS = "default") =>
|
|
||||||
cn(
|
|
||||||
"inline-flex min-h-11 items-center justify-center gap-2 rounded-lg px-4 font-heading text-sm font-semibold transition-colors focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-none disabled:opacity-50",
|
|
||||||
VARIANTS[variant],
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Form bound to a work order server action. Shows translated errors (incl. structured
|
* Form bound to a work order server action. Shows translated errors (incl. structured
|
||||||
@@ -44,7 +34,7 @@ export function ActionForm({
|
|||||||
submitLabel: string;
|
submitLabel: string;
|
||||||
pendingLabel?: string;
|
pendingLabel?: string;
|
||||||
successText?: string;
|
successText?: string;
|
||||||
variant?: keyof typeof VARIANTS;
|
variant?: ButtonVariant;
|
||||||
namespace?: "workOrders" | "settingsTemplates";
|
namespace?: "workOrders" | "settingsTemplates";
|
||||||
className?: string;
|
className?: string;
|
||||||
footerClassName?: string;
|
footerClassName?: string;
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button class names shared by server and client components. Kept out of "use client" modules:
|
||||||
|
* calling a function exported from a client module inside a server component fails at runtime.
|
||||||
|
*/
|
||||||
|
const VARIANTS = {
|
||||||
|
primary: "bg-cta text-cta-foreground hover:opacity-90",
|
||||||
|
default: "bg-primary text-primary-foreground hover:opacity-90",
|
||||||
|
outline: "border border-border bg-background hover:bg-muted",
|
||||||
|
danger: "border border-[var(--risk)] bg-background text-[var(--risk)] hover:bg-muted",
|
||||||
|
ghost: "hover:bg-muted text-muted-foreground",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ButtonVariant = keyof typeof VARIANTS;
|
||||||
|
|
||||||
|
export const buttonCls = (variant: ButtonVariant = "default") =>
|
||||||
|
cn(
|
||||||
|
"inline-flex min-h-11 items-center justify-center gap-2 rounded-lg px-4 font-heading text-sm font-semibold transition-colors focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-none disabled:opacity-50",
|
||||||
|
VARIANTS[variant],
|
||||||
|
);
|
||||||
@@ -27,7 +27,8 @@ import {
|
|||||||
removePhotoRequirementAction,
|
removePhotoRequirementAction,
|
||||||
} from "@/server/actions/work_orders/work-orders";
|
} from "@/server/actions/work_orders/work-orders";
|
||||||
import { formatDate, formatDateTime } from "@/lib/work-orders/time";
|
import { formatDate, formatDateTime } from "@/lib/work-orders/time";
|
||||||
import { ActionForm, buttonCls } from "@/components/work-orders/action-form";
|
import { ActionForm } from "@/components/work-orders/action-form";
|
||||||
|
import { buttonCls } from "@/components/work-orders/button-cls";
|
||||||
import { Check, Dl, Empty, Field, inputCls, Section } from "@/components/work-orders/ui";
|
import { Check, Dl, Empty, Field, inputCls, Section } from "@/components/work-orders/ui";
|
||||||
import type { CompletionBlocker } from "@/lib/work-orders/status";
|
import type { CompletionBlocker } from "@/lib/work-orders/status";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user