diff --git a/modules/core/src/web/components/page-header.tsx b/modules/core/src/web/components/page-header.tsx index 2050c1dc..d03ce0ae 100644 --- a/modules/core/src/web/components/page-header.tsx +++ b/modules/core/src/web/components/page-header.tsx @@ -4,49 +4,66 @@ import { ChevronLeftIcon } from "lucide-react"; import type { ReactNode } from "react"; interface PageHeaderProps { - backIcon?: ReactNode; title: ReactNode; description?: ReactNode; - status?: string; rightSlot?: ReactNode; + showBackButton?: boolean; + onBackClick?: () => void; + backButtonLabel?: string; + + statusSlot?: ReactNode; + className?: string; } -export function PageHeader({ - backIcon, +export const PageHeader = ({ title, description, rightSlot, + showBackButton = false, + onBackClick, + backButtonLabel = "Volver", + statusSlot, className, -}: PageHeaderProps) { +}: PageHeaderProps) => { return ( -
- {/* Lado izquierdo */} +
-
- {backIcon && ( +
+ {showBackButton && ( )} -
-

- {title} -

- {description &&
{description}
} +
+
+

+ {title} +

+ + {statusSlot} +
+ + {description &&

{description}

}
- {/* Lado derecho parametrizable */} -
{rightSlot}
-
+ {rightSlot &&
{rightSlot}
} +
); -} +}; diff --git a/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/cancel-form-button.tsx b/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/cancel-form-button.tsx index 40c3dab7..181b3f97 100644 --- a/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/cancel-form-button.tsx +++ b/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/cancel-form-button.tsx @@ -1,16 +1,16 @@ import { Button } from "@repo/shadcn-ui/components"; -import { cn } from '@repo/shadcn-ui/lib/utils'; +import { cn } from "@repo/shadcn-ui/lib/utils"; import { XIcon } from "lucide-react"; -import * as React from "react"; -import { useCallback } from "react"; +import type * as React from "react"; +import { useCallback, useState } from "react"; import { useNavigate } from "react-router-dom"; + import { useTranslation } from "../../../i18n.ts"; import { useUnsavedChangesContext } from "../use-unsaved-changes-notifier"; export type CancelFormButtonProps = { - formId?: string; - to?: string; /// Ruta a la que navegar si no se pasa onCancel - onCancel?: () => void | Promise; // Prioritaria sobre "to" + to?: string; + onCancel?: () => void | Promise; label?: string; variant?: React.ComponentProps["variant"]; size?: React.ComponentProps["size"]; @@ -20,50 +20,57 @@ export type CancelFormButtonProps = { }; export const CancelFormButton = ({ - formId, to, onCancel, label, variant = "outline", size = "default", className, - disabled, + disabled = false, "data-testid": dataTestId = "cancel-button", }: CancelFormButtonProps) => { const navigate = useNavigate(); - const { t } = useTranslation(); - const defaultLabel = t ? t("common.cancel") : "Cancel"; const { requestConfirm } = useUnsavedChangesContext(); + const [isRunning, setIsRunning] = useState(false); + + const defaultLabel = t("common.cancel"); + const computedDisabled = disabled || isRunning; + const handleClick = useCallback(async () => { + if (computedDisabled) return; + const ok = requestConfirm ? await requestConfirm() : true; if (!ok) return; - if (onCancel) { - await onCancel(); - return; - } + try { + setIsRunning(true); - if (to) { - navigate(to); + if (onCancel) { + await onCancel(); + return; + } + + if (to) { + navigate(to); + } + } finally { + setIsRunning(false); } - // si no hay ni onCancel ni to → no hace nada - }, [requestConfirm, onCancel, to, navigate]); + }, [computedDisabled, requestConfirm, onCancel, to, navigate]); return ( ); diff --git a/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/form-commit-button-group.tsx b/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/form-commit-button-group.tsx index a82ea18d..0ab979e2 100644 --- a/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/form-commit-button-group.tsx +++ b/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/form-commit-button-group.tsx @@ -17,6 +17,7 @@ import { Trash2Icon, } from "lucide-react"; import { useFormContext } from "react-hook-form"; +import { useTranslation } from "react-i18next"; import { CancelFormButton, type CancelFormButtonProps } from "./cancel-form-button"; import { type SubmitButtonProps, SubmitFormButton } from "./submit-form-button"; @@ -25,7 +26,7 @@ type Align = "start" | "center" | "end" | "between"; type GroupSubmitButtonProps = Omit; -export type UpdateCommitButtonGroupProps = { +export type FormCommitButtonGroupProps = { className?: string; align?: Align; // default "end" gap?: string; // default "gap-2" @@ -52,7 +53,7 @@ const alignToJustify: Record = { between: "justify-between", }; -export const UpdateCommitButtonGroup = ({ +export const FormCommitButtonGroup = ({ className, align = "end", gap = "gap-2", @@ -70,82 +71,97 @@ export const UpdateCommitButtonGroup = ({ onPreview, onDuplicate, onBack, -}: UpdateCommitButtonGroupProps) => { - const showCancel = cancel?.show ?? true; - const hasSecondaryActions = onReset || onPreview || onDuplicate || onBack || onDelete; - - // ⛳️ RHF opcional: auto-detectar isSubmitting si no se pasó isLoading - let rhfIsSubmitting = false; +}: FormCommitButtonGroupProps) => { + const { t } = useTranslation(); const ctx = useFormContext(); - rhfIsSubmitting = !!ctx?.formState?.isSubmitting; - + const rhfIsSubmitting = !!ctx.formState.isSubmitting; const busy = isLoading ?? rhfIsSubmitting; + + const showCancel = cancel?.show ?? true; const computedDisabled = !!(disabled || (preventDoubleSubmit && busy)); + const hasSecondaryActions = !!(onReset || onPreview || onDuplicate || onBack || onDelete); return (
- {showCancel && } - {submit && } + {showCancel && ( + + )} + + {submit && ( + + )} - {/* Menú de acciones adicionales */} {hasSecondaryActions && ( + } /> + {onReset && ( - + - Deshacer cambios + {t("common.resetChanges")} )} + {onPreview && ( - + - Vista previa + {t("common.preview")} )} + {onDuplicate && ( - + - Duplicar + {t("common.duplicate")} )} + {onBack && ( - + - Volver + {t("common.back")} )} + {onDelete && ( <> - Eliminar + {t("common.delete")} )} diff --git a/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/submit-form-button.tsx b/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/submit-form-button.tsx index 327c8aee..3f131d46 100644 --- a/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/submit-form-button.tsx +++ b/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/submit-form-button.tsx @@ -16,7 +16,7 @@ export type SubmitButtonProps = { size?: React.ComponentProps["size"]; className?: string; - preventDoubleSubmit?: boolean; // Evita múltiples submits mientras loading + preventDoubleSubmit?: boolean; hasChanges?: boolean; onClick?: React.MouseEventHandler; @@ -36,47 +36,35 @@ export const SubmitFormButton = ({ preventDoubleSubmit = true, hasChanges = false, onClick, - disabled, + disabled = false, children, "data-testid": dataTestId = "submit-button", }: SubmitButtonProps) => { const { t } = useTranslation(); - const defaultLabel = t ? t("common.save") : "Save"; - const defaultLabelIsLoading = t ? t("common.saving") : "Saving..."; + const { formState } = useFormContext(); - // ⛳️ RHF opcional: auto-detectar isSubmitting si no se pasó isLoading - let rhfIsSubmitting = false; - try { - const ctx = useFormContext(); - rhfIsSubmitting = !!ctx?.formState?.isSubmitting; - } catch { - // No hay provider de RHF; ignorar - } - const busy = isLoading ?? rhfIsSubmitting; + const busy = isLoading ?? formState.isSubmitting; + const computedDisabled = disabled || (preventDoubleSubmit && busy); - const computedDisabled = !!(disabled || (preventDoubleSubmit && busy)); - const dataState = busy ? "loading" : "idle"; - - const handleClick: React.MouseEventHandler = (e) => { + const handleClick: React.MouseEventHandler = (event) => { if (preventDoubleSubmit && busy) { - // Evitar submits duplicados durante loading - e.preventDefault(); - e.stopPropagation(); + event.preventDefault(); + event.stopPropagation(); return; } - onClick?.(e); + + onClick?.(event); }; return (