refactor: reemplazar RdxEditorPageLayout con RdxPageContainer y RdxEditorLayout en múltiples componentes
This commit is contained in:
parent
2f8122ca77
commit
3aa3f4018b
@ -2,7 +2,7 @@ import {
|
||||
RdxBadge,
|
||||
RdxContextPanel,
|
||||
RdxEditorActionBar,
|
||||
RdxEditorPageLayout,
|
||||
RdxEditorLayout,
|
||||
RdxFormGrid,
|
||||
RdxPageActions,
|
||||
RdxPageContainer,
|
||||
@ -751,13 +751,15 @@ export default function UiKitPage() {
|
||||
title="Editor foundations"
|
||||
>
|
||||
<div className="grid gap-4">
|
||||
<ExampleCard title="RdxEditorPageLayout simple">
|
||||
<ExampleCard title="RdxPageContainer + RdxEditorLayout">
|
||||
<div className="rounded-lg border border-border bg-muted/30">
|
||||
<RdxEditorPageLayout complexity="simple">
|
||||
<div className="rounded-lg border border-dashed border-border bg-background p-4 text-sm text-foreground">
|
||||
Centered editor content
|
||||
</div>
|
||||
</RdxEditorPageLayout>
|
||||
<RdxPageContainer width="content">
|
||||
<RdxEditorLayout>
|
||||
<div className="rounded-lg border border-dashed border-border bg-background p-4 text-sm text-foreground">
|
||||
Centered editor content
|
||||
</div>
|
||||
</RdxEditorLayout>
|
||||
</RdxPageContainer>
|
||||
</div>
|
||||
</ExampleCard>
|
||||
|
||||
@ -806,47 +808,49 @@ export default function UiKitPage() {
|
||||
title="Simple editor composition"
|
||||
>
|
||||
<div className="rounded-lg border border-border bg-muted/30">
|
||||
<RdxEditorPageLayout complexity="simple">
|
||||
<div className="rounded-2xl border border-border/60 bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
|
||||
<div className="flex min-w-0 flex-col gap-6">
|
||||
<div className="rounded-lg border border-border bg-background px-4 py-3 text-sm text-muted-foreground">
|
||||
Save error aligned with editor width
|
||||
</div>
|
||||
|
||||
<RdxPageContainer width="content">
|
||||
<RdxEditorLayout>
|
||||
<div className="rounded-2xl border border-border/60 bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
|
||||
<div className="flex min-w-0 flex-col gap-6">
|
||||
<RdxPanel
|
||||
description="Primary customer information"
|
||||
icon={<PlusIcon className="size-4" />}
|
||||
title="Basic information"
|
||||
>
|
||||
<RdxFormGrid className="md:grid-cols-2">
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
|
||||
Name field
|
||||
</div>
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
|
||||
Tax ID field
|
||||
</div>
|
||||
</RdxFormGrid>
|
||||
</RdxPanel>
|
||||
<div className="rounded-lg border border-border bg-background px-4 py-3 text-sm text-muted-foreground">
|
||||
Save error aligned with editor width
|
||||
</div>
|
||||
|
||||
<RdxPanel
|
||||
description="Secondary preferences"
|
||||
icon={<PlusIcon className="size-4" />}
|
||||
title="Preferences"
|
||||
>
|
||||
<RdxFormGrid className="md:grid-cols-2">
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
|
||||
Language field
|
||||
</div>
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
|
||||
Currency field
|
||||
</div>
|
||||
</RdxFormGrid>
|
||||
</RdxPanel>
|
||||
<div className="flex min-w-0 flex-col gap-6">
|
||||
<RdxPanel
|
||||
description="Primary customer information"
|
||||
icon={<PlusIcon className="size-4" />}
|
||||
title="Basic information"
|
||||
>
|
||||
<RdxFormGrid className="md:grid-cols-2">
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
|
||||
Name field
|
||||
</div>
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
|
||||
Tax ID field
|
||||
</div>
|
||||
</RdxFormGrid>
|
||||
</RdxPanel>
|
||||
|
||||
<RdxPanel
|
||||
description="Secondary preferences"
|
||||
icon={<PlusIcon className="size-4" />}
|
||||
title="Preferences"
|
||||
>
|
||||
<RdxFormGrid className="md:grid-cols-2">
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
|
||||
Language field
|
||||
</div>
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
|
||||
Currency field
|
||||
</div>
|
||||
</RdxFormGrid>
|
||||
</RdxPanel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</RdxEditorPageLayout>
|
||||
</RdxEditorLayout>
|
||||
</RdxPageContainer>
|
||||
</div>
|
||||
</ExampleCard>
|
||||
|
||||
@ -870,61 +874,65 @@ export default function UiKitPage() {
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col bg-muted/20">
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
<RdxEditorPageLayout
|
||||
aside={
|
||||
<div className="flex min-w-0 flex-col gap-6">
|
||||
<RdxPanel description="Secondary recipient controls." title="Recipient">
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-4 text-sm text-muted-foreground">
|
||||
Customer selector and helper actions
|
||||
</div>
|
||||
</RdxPanel>
|
||||
<RdxPageContainer width="full">
|
||||
<RdxEditorLayout
|
||||
aside={
|
||||
<div className="flex min-w-0 flex-col gap-6">
|
||||
<RdxPanel
|
||||
description="Secondary recipient controls."
|
||||
title="Recipient"
|
||||
>
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-4 text-sm text-muted-foreground">
|
||||
Customer selector and helper actions
|
||||
</div>
|
||||
</RdxPanel>
|
||||
|
||||
<RdxPanel
|
||||
description="Fiscal defaults and toggles."
|
||||
title="Tax settings"
|
||||
>
|
||||
<RdxFormGrid>
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-12">
|
||||
Tax regime field
|
||||
</div>
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-12">
|
||||
Default tax selector
|
||||
</div>
|
||||
</RdxFormGrid>
|
||||
</RdxPanel>
|
||||
</div>
|
||||
}
|
||||
complexity="complex"
|
||||
>
|
||||
<div className="rounded-2xl border border-border bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
|
||||
<div className="flex min-w-0 flex-col gap-6">
|
||||
<div className="rounded-lg border border-border bg-background px-4 py-3 text-sm text-muted-foreground">
|
||||
Inline feedback aligned with the editor content
|
||||
<RdxPanel
|
||||
description="Fiscal defaults and toggles."
|
||||
title="Tax settings"
|
||||
>
|
||||
<RdxFormGrid>
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-12">
|
||||
Tax regime field
|
||||
</div>
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-12">
|
||||
Default tax selector
|
||||
</div>
|
||||
</RdxFormGrid>
|
||||
</RdxPanel>
|
||||
</div>
|
||||
|
||||
<RdxPanel title="Document information">
|
||||
<RdxFormGrid>
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-4">
|
||||
Reference field
|
||||
</div>
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-4">
|
||||
Date field
|
||||
</div>
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-4">
|
||||
Series field
|
||||
</div>
|
||||
</RdxFormGrid>
|
||||
</RdxPanel>
|
||||
|
||||
<section className="min-w-0 border-b border-border pb-6">
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-4 text-sm text-muted-foreground">
|
||||
Line editor keeps its own internal table layout and stays in the
|
||||
main surface.
|
||||
}
|
||||
>
|
||||
<div className="rounded-2xl border border-border bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
|
||||
<div className="flex min-w-0 flex-col gap-6">
|
||||
<div className="rounded-lg border border-border bg-background px-4 py-3 text-sm text-muted-foreground">
|
||||
Inline feedback aligned with the editor content
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<RdxPanel title="Document information">
|
||||
<RdxFormGrid>
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-4">
|
||||
Reference field
|
||||
</div>
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-4">
|
||||
Date field
|
||||
</div>
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-4">
|
||||
Series field
|
||||
</div>
|
||||
</RdxFormGrid>
|
||||
</RdxPanel>
|
||||
|
||||
<section className="min-w-0 border-b border-border pb-6">
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-4 text-sm text-muted-foreground">
|
||||
Line editor keeps its own internal table layout and stays in the
|
||||
main surface.
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</RdxEditorPageLayout>
|
||||
</RdxEditorLayout>
|
||||
</RdxPageContainer>
|
||||
</div>
|
||||
|
||||
<RdxEditorActionBar
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { ProformaCreateSkeleton } from "./proforma-create-skeleton";
|
||||
|
||||
test("Proforma Create skeleton comparte el frame del editor productivo", () => {
|
||||
const html = renderToStaticMarkup(<ProformaCreateSkeleton />);
|
||||
|
||||
assert.match(html, /flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden/);
|
||||
assert.match(html, /rdx-page-container/);
|
||||
assert.doesNotMatch(html, /fixed inset-0/);
|
||||
assert.equal(html.match(/overflow-y-auto/g)?.length, 1);
|
||||
});
|
||||
@ -1,16 +1,25 @@
|
||||
import { RdxEditorLayout, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
|
||||
import { Skeleton } from "@repo/shadcn-ui/components";
|
||||
|
||||
export const ProformaCreateSkeleton = () => {
|
||||
return (
|
||||
<div className="fixed inset-0 flex flex-col overflow-hidden">
|
||||
<div className="border-b px-6 py-4">
|
||||
<Skeleton className="h-10 w-full max-w-md" />
|
||||
</div>
|
||||
<div className="flex-1 space-y-6 overflow-y-auto p-6">
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-64 w-full" />
|
||||
<Skeleton className="h-48 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<RdxPageFrame
|
||||
contentClassName="bg-muted/20"
|
||||
header={
|
||||
<div className="border-b px-6 py-4">
|
||||
<Skeleton className="h-10 w-full max-w-md" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<RdxPageContainer width="content">
|
||||
<RdxEditorLayout>
|
||||
<div className="space-y-6">
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-64 w-full" />
|
||||
<Skeleton className="h-48 w-full" />
|
||||
</div>
|
||||
</RdxEditorLayout>
|
||||
</RdxPageContainer>
|
||||
</RdxPageFrame>
|
||||
);
|
||||
};
|
||||
|
||||
@ -18,10 +18,10 @@ import { useCreateRecurringProformaFromProforma } from "../../../recurring-profo
|
||||
import { buildRecurringProformaDetailPath } from "../../../recurring-proformas/shared/recurring-proforma-navigation";
|
||||
|
||||
import {
|
||||
buildCreateRecurringProformaFromProformaDefaultValues,
|
||||
buildCreateRecurringProformaFromProformaInput,
|
||||
type CreateRecurringProformaFromProformaForm,
|
||||
CreateRecurringProformaFromProformaFormSchema,
|
||||
buildCreateRecurringProformaFromProformaDefaultValues,
|
||||
buildCreateRecurringProformaFromProformaInput,
|
||||
} from "./use-create-recurring-proforma-from-proforma-dialog-controller.utils";
|
||||
|
||||
const normalizeSubmitError = (error: unknown): Error | ValidationErrorCollection => {
|
||||
@ -41,6 +41,9 @@ export interface CreateRecurringProformaFromProformaDialogControllerParams {
|
||||
proformaReference: string | null;
|
||||
}
|
||||
|
||||
type CreateRecurringProformaFromProformaTarget =
|
||||
CreateRecurringProformaFromProformaDialogControllerParams;
|
||||
|
||||
export const useCreateRecurringProformaFromProformaDialogController = ({
|
||||
proformaId,
|
||||
proformaReference,
|
||||
@ -51,6 +54,10 @@ export const useCreateRecurringProformaFromProformaDialogController = ({
|
||||
const mutation = useCreateRecurringProformaFromProforma();
|
||||
const [open, setOpen] = useState(false);
|
||||
const submitInProgressRef = useRef(false);
|
||||
const targetRef = useRef<CreateRecurringProformaFromProformaTarget>({
|
||||
proformaId,
|
||||
proformaReference,
|
||||
});
|
||||
const form = useHookForm<CreateRecurringProformaFromProformaForm>({
|
||||
resolverSchema: CreateRecurringProformaFromProformaFormSchema,
|
||||
initialValues: buildCreateRecurringProformaFromProformaDefaultValues(proformaReference),
|
||||
@ -77,8 +84,10 @@ export const useCreateRecurringProformaFromProformaDialogController = ({
|
||||
);
|
||||
};
|
||||
|
||||
const openDialog = () => {
|
||||
form.reset(buildCreateRecurringProformaFromProformaDefaultValues(proformaReference));
|
||||
const openDialog = (target?: CreateRecurringProformaFromProformaTarget) => {
|
||||
const nextTarget = target ?? { proformaId, proformaReference };
|
||||
targetRef.current = nextTarget;
|
||||
form.reset(buildCreateRecurringProformaFromProformaDefaultValues(nextTarget.proformaReference));
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
@ -100,7 +109,7 @@ export const useCreateRecurringProformaFromProformaDialogController = ({
|
||||
|
||||
try {
|
||||
const created = await mutation.mutateAsync({
|
||||
proformaId,
|
||||
proformaId: targetRef.current.proformaId,
|
||||
data: buildCreateRecurringProformaFromProformaInput(values),
|
||||
});
|
||||
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { isValidElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import type { CellContext } from "@tanstack/react-table";
|
||||
|
||||
import { RdxRowActions, type RdxRowActionsProps } from "@repo/rdx-ui/components";
|
||||
import type { CellContext } from "@tanstack/react-table";
|
||||
import { isValidElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import type { ProformaListRow } from "../../../../shared";
|
||||
import { PROFORMA_STATUS } from "../../../../shared";
|
||||
|
||||
import { useProformasGridColumns } from "./use-proforma-grid-columns";
|
||||
|
||||
const proformaFixture: ProformaListRow = {
|
||||
@ -104,12 +105,16 @@ test("mantiene las destructivas en overflow y conserva disabled", () => {
|
||||
assert.equal(viewAction?.disabled, true);
|
||||
});
|
||||
|
||||
test("preserva callbacks de view, edit y delete", () => {
|
||||
test("preserva callbacks de view, edit, crear recurrencia y delete", () => {
|
||||
let didView = false;
|
||||
let didEdit = false;
|
||||
let didCreateRecurring = false;
|
||||
let didDelete = false;
|
||||
|
||||
const props = getRowActionsProps("normal", {
|
||||
onCreateRecurringClick: () => {
|
||||
didCreateRecurring = true;
|
||||
},
|
||||
onDeleteClick: () => {
|
||||
didDelete = true;
|
||||
},
|
||||
@ -123,9 +128,29 @@ test("preserva callbacks de view, edit y delete", () => {
|
||||
|
||||
props.primaryActions?.find((action) => action.id === "view")?.onSelect();
|
||||
props.primaryActions?.find((action) => action.id === "edit")?.onSelect();
|
||||
props.overflowActions?.find((action) => action.id === "create-recurring")?.onSelect();
|
||||
props.overflowActions?.find((action) => action.id === "delete")?.onSelect();
|
||||
|
||||
assert.equal(didView, true);
|
||||
assert.equal(didEdit, true);
|
||||
assert.equal(didCreateRecurring, true);
|
||||
assert.equal(didDelete, true);
|
||||
});
|
||||
|
||||
test("solo permite crear recurrencia desde el listado normal", () => {
|
||||
const normalProps = getRowActionsProps("normal", {
|
||||
onCreateRecurringClick: () => undefined,
|
||||
});
|
||||
const archivedProps = getRowActionsProps("archived", {
|
||||
onCreateRecurringClick: () => undefined,
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
normalProps.overflowActions?.find((action) => action.id === "create-recurring")?.disabled,
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
archivedProps.overflowActions?.find((action) => action.id === "create-recurring")?.disabled,
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
import { EmptyCellValue } from "@erp/core/components";
|
||||
import { DataTableColumnHeader, RdxRowActions, type RdxRowAction } from "@repo/rdx-ui/components";
|
||||
import { DataTableColumnHeader, type RdxRowAction, RdxRowActions } from "@repo/rdx-ui/components";
|
||||
import { DateHelper } from "@repo/rdx-utils";
|
||||
import {
|
||||
Checkbox,
|
||||
Spinner,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { Checkbox, Spinner } from "@repo/shadcn-ui/components";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import {
|
||||
CalendarSyncIcon,
|
||||
CopyIcon,
|
||||
ExternalLinkIcon,
|
||||
EyeIcon,
|
||||
@ -37,6 +35,7 @@ type GridActionHandlers = {
|
||||
onIssueClick?: (proforma: ProformaListRow) => void;
|
||||
onArchiveClick?: (proforma: ProformaListRow) => void;
|
||||
onDuplicateClick?: (proforma: ProformaListRow) => void;
|
||||
onCreateRecurringClick?: (proforma: ProformaListRow) => void;
|
||||
onUnarchiveClick?: (proforma: ProformaListRow) => void;
|
||||
onDeleteClick?: (proforma: ProformaListRow) => void;
|
||||
onChangeStatusClick?: (proforma: ProformaListRow, nextStatus: ProformaStatus) => void;
|
||||
@ -52,10 +51,8 @@ type GridActionHandlers = {
|
||||
isPdfDownloading?: boolean;
|
||||
};
|
||||
|
||||
const renderActionIcon = (
|
||||
Icon: ComponentType<{ className?: string }>,
|
||||
loading?: boolean
|
||||
) => (loading ? <Spinner className="size-4" /> : <Icon className="size-4" />);
|
||||
const renderActionIcon = (Icon: ComponentType<{ className?: string }>, loading?: boolean) =>
|
||||
loading ? <Spinner className="size-4" /> : <Icon className="size-4" />;
|
||||
|
||||
export function useProformasGridColumns(
|
||||
scope: ProformaListScope,
|
||||
@ -312,6 +309,7 @@ export function useProformasGridColumns(
|
||||
const handleView = () => actionHandlers.onViewClick?.(proforma);
|
||||
const handleEdit = () => actionHandlers.onEditClick?.(proforma);
|
||||
const handleDuplicate = () => actionHandlers.onDuplicateClick?.(proforma);
|
||||
const handleCreateRecurring = () => actionHandlers.onCreateRecurringClick?.(proforma);
|
||||
const handleArchive = () => actionHandlers.onArchiveClick?.(proforma);
|
||||
const handleUnarchive = () => actionHandlers.onUnarchiveClick?.(proforma);
|
||||
const handleIssue = () => actionHandlers.onIssueClick?.(proforma);
|
||||
@ -416,6 +414,14 @@ export function useProformasGridColumns(
|
||||
: undefined,
|
||||
onSelect: handleDuplicate,
|
||||
},
|
||||
{
|
||||
id: "create-recurring",
|
||||
label: t("pages.proformas.list.actions.create_recurring"),
|
||||
icon: renderActionIcon(CalendarSyncIcon),
|
||||
disabled: !(actionHandlers.onCreateRecurringClick && isNormalScope),
|
||||
disabledReason: isNormalScope ? undefined : getScopeReason(),
|
||||
onSelect: handleCreateRecurring,
|
||||
},
|
||||
{
|
||||
id: "archive",
|
||||
label: t("pages.proformas.list.actions.archive"),
|
||||
|
||||
@ -32,6 +32,8 @@ import { ChangeProformaStatusDialog } from "../../../change-status";
|
||||
import { prepareChangeProformaStatusTargets } from "../../../change-status/utils";
|
||||
import { DeleteProformaDialog } from "../../../delete";
|
||||
import { prepareDeleteProformaTargets } from "../../../delete/utils";
|
||||
import { useCreateRecurringProformaFromProformaDialogController } from "../../../detail/controllers";
|
||||
import { CreateRecurringProformaFromProformaDialog } from "../../../detail/ui/components";
|
||||
import { downloadProformaPDFApi } from "../../../download-pdf/api";
|
||||
import { IssueProformaDialog } from "../../../issue-proforma";
|
||||
import { prepareIssueProformaTarget } from "../../../issue-proforma/utils";
|
||||
@ -238,6 +240,10 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
|
||||
goHome,
|
||||
isRetrying,
|
||||
} = useListProformasPageController(scope);
|
||||
const createRecurringDialogCtrl = useCreateRecurringProformaFromProformaDialogController({
|
||||
proformaId: "",
|
||||
proformaReference: null,
|
||||
});
|
||||
|
||||
const handleEditClick = (proformaId: string) => {
|
||||
navigate({
|
||||
@ -293,6 +299,11 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
|
||||
onViewClick: (proforma) => handleOpenDetailClick(proforma.id),
|
||||
onEditClick: (proforma) => handleEditClick(proforma.id),
|
||||
onDuplicateClick: handleDuplicateClick,
|
||||
onCreateRecurringClick: (proforma) =>
|
||||
createRecurringDialogCtrl.openDialog({
|
||||
proformaId: proforma.id,
|
||||
proformaReference: proforma.proformaReference,
|
||||
}),
|
||||
onIssueClick: (proformaRow) =>
|
||||
issueDialogCtrl.openDialog(prepareIssueProformaTarget(proformaRow)),
|
||||
onArchiveClick: archiveDialogCtrl.openDialog,
|
||||
@ -550,6 +561,21 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
|
||||
targets={changeStatusDialogCtrl.targets}
|
||||
/>
|
||||
|
||||
<CreateRecurringProformaFromProformaDialog
|
||||
form={createRecurringDialogCtrl.form}
|
||||
formId={createRecurringDialogCtrl.formId}
|
||||
isSubmitting={createRecurringDialogCtrl.isSubmitting}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
createRecurringDialogCtrl.closeDialog();
|
||||
}
|
||||
}}
|
||||
onSubmit={createRecurringDialogCtrl.onSubmit}
|
||||
onUnitChange={createRecurringDialogCtrl.changeUnit}
|
||||
open={createRecurringDialogCtrl.open}
|
||||
unit={createRecurringDialogCtrl.unit}
|
||||
/>
|
||||
|
||||
<DeleteProformaDialog
|
||||
isSubmitting={deleteDialogCtrl.isSubmitting}
|
||||
onConfirm={deleteDialogCtrl.confirmDelete}
|
||||
|
||||
@ -88,7 +88,7 @@ Contenido visible actual:
|
||||
|
||||
- información personal: `name`, `email`, `language_code`
|
||||
- modo `app-fullscreen`, sin sidebar principal
|
||||
- contenido centrado con ancho controlado mediante `RdxEditorPageLayout`
|
||||
- contenido centrado mediante `RdxPageContainer` y compuesto con `RdxEditorLayout`
|
||||
- seguridad mediante dialog para cambio de contraseña
|
||||
- cierre de sesión reutilizando el flujo de logout existente
|
||||
|
||||
|
||||
@ -0,0 +1,65 @@
|
||||
import { PageErrorState } from "@erp/core/components";
|
||||
import { RdxEditorLayout, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
export const AccountLoadingState = ({ label }: { label: string }) => (
|
||||
<RdxPageFrame contentClassName="bg-muted/20">
|
||||
<RdxPageContainer className="min-h-full" width="content">
|
||||
<RdxEditorLayout>
|
||||
<div className="flex min-h-[16rem] items-center justify-center text-sm text-muted-foreground">
|
||||
<div aria-busy="true" role="status">
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
</RdxEditorLayout>
|
||||
</RdxPageContainer>
|
||||
</RdxPageFrame>
|
||||
);
|
||||
|
||||
interface AccountErrorStateProps {
|
||||
canRetry: boolean;
|
||||
description: string;
|
||||
isRetrying: boolean;
|
||||
onRetry: () => void;
|
||||
reference?: string;
|
||||
retryLabel: string;
|
||||
retryingLabel: string;
|
||||
title: string;
|
||||
variant: ComponentProps<typeof PageErrorState>["variant"];
|
||||
}
|
||||
|
||||
export const AccountErrorState = ({
|
||||
canRetry,
|
||||
description,
|
||||
isRetrying,
|
||||
onRetry,
|
||||
reference,
|
||||
retryLabel,
|
||||
retryingLabel,
|
||||
title,
|
||||
variant,
|
||||
}: AccountErrorStateProps) => (
|
||||
<RdxPageFrame contentClassName="bg-muted/20">
|
||||
<RdxPageContainer width="content">
|
||||
<RdxEditorLayout>
|
||||
<div className="mx-auto w-full max-w-3xl">
|
||||
<PageErrorState
|
||||
description={description}
|
||||
primaryAction={
|
||||
canRetry
|
||||
? {
|
||||
label: isRetrying ? retryingLabel : retryLabel,
|
||||
onClick: onRetry,
|
||||
isLoading: isRetrying,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
reference={reference}
|
||||
title={title}
|
||||
variant={variant}
|
||||
/>
|
||||
</div>
|
||||
</RdxEditorLayout>
|
||||
</RdxPageContainer>
|
||||
</RdxPageFrame>
|
||||
);
|
||||
@ -0,0 +1,36 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { AccountErrorState, AccountLoadingState } from "./account-page-states";
|
||||
|
||||
test("Account loading usa el frame y contenedor definitivos", () => {
|
||||
const html = renderToStaticMarkup(<AccountLoadingState label="Cargando cuenta" />);
|
||||
|
||||
assert.match(html, /flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden/);
|
||||
assert.match(html, /rdx-page-container/);
|
||||
assert.match(html, /Cargando cuenta/);
|
||||
assert.doesNotMatch(html, /fixed inset-0/);
|
||||
assert.doesNotMatch(html, /rdx-editor-page-layout/);
|
||||
});
|
||||
|
||||
test("Account error permanece dentro del frame actual", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<AccountErrorState
|
||||
canRetry
|
||||
description="No se pudo cargar la cuenta"
|
||||
isRetrying={false}
|
||||
onRetry={() => undefined}
|
||||
retryingLabel="Reintentando"
|
||||
retryLabel="Reintentar"
|
||||
title="Error de cuenta"
|
||||
variant="server"
|
||||
/>
|
||||
);
|
||||
|
||||
assert.match(html, /flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden/);
|
||||
assert.match(html, /Error de cuenta/);
|
||||
assert.match(html, /Reintentar/);
|
||||
assert.doesNotMatch(html, /fixed inset-0/);
|
||||
});
|
||||
@ -1,10 +1,12 @@
|
||||
import { ErrorAlert, PageErrorState } from "@erp/core/components";
|
||||
import { ErrorAlert } from "@erp/core/components";
|
||||
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
||||
import {
|
||||
CancelActionButton,
|
||||
RdxEditorActionBar,
|
||||
RdxEditorHeader,
|
||||
RdxEditorPageLayout,
|
||||
RdxEditorLayout,
|
||||
RdxPageContainer,
|
||||
RdxPageFrame,
|
||||
RdxPanel,
|
||||
RhfSubmitActionButton,
|
||||
SelectField,
|
||||
@ -27,65 +29,7 @@ import { useTranslation } from "../../../i18n";
|
||||
import { useAccountPageController } from "../../controllers";
|
||||
import type { AccountPasswordForm, AccountProfileForm } from "../../entities";
|
||||
|
||||
const AccountLoadingState = ({ label }: { label: string }) => {
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
||||
<RdxEditorPageLayout className="min-h-full" width="simple">
|
||||
<div className="flex min-h-[16rem] items-center justify-center text-sm text-muted-foreground">
|
||||
<div aria-busy="true" role="status">
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
</RdxEditorPageLayout>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AccountErrorState = ({
|
||||
canRetry,
|
||||
description,
|
||||
isRetrying,
|
||||
onRetry,
|
||||
reference,
|
||||
retryLabel,
|
||||
retryingLabel,
|
||||
title,
|
||||
variant,
|
||||
}: {
|
||||
canRetry: boolean;
|
||||
description: string;
|
||||
isRetrying: boolean;
|
||||
onRetry: () => void;
|
||||
reference?: string;
|
||||
retryLabel: string;
|
||||
retryingLabel: string;
|
||||
title: string;
|
||||
variant: React.ComponentProps<typeof PageErrorState>["variant"];
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
||||
<RdxEditorPageLayout width="simple">
|
||||
<div className="mx-auto w-full max-w-3xl">
|
||||
<PageErrorState
|
||||
description={description}
|
||||
primaryAction={
|
||||
canRetry
|
||||
? {
|
||||
label: isRetrying ? retryingLabel : retryLabel,
|
||||
onClick: onRetry,
|
||||
isLoading: isRetrying,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
reference={reference}
|
||||
title={title}
|
||||
variant={variant}
|
||||
/>
|
||||
</div>
|
||||
</RdxEditorPageLayout>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import { AccountErrorState, AccountLoadingState } from "./account-page-states";
|
||||
|
||||
const AccountPasswordDialog = ({
|
||||
ctrl,
|
||||
@ -252,15 +196,18 @@ export const AccountPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex flex-col overflow-hidden">
|
||||
<RdxEditorHeader
|
||||
backLabel={t("account.header.back")}
|
||||
onBack={navigateBack}
|
||||
title={t("account.title")}
|
||||
/>
|
||||
|
||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
||||
<RdxEditorPageLayout width="simple">
|
||||
<RdxPageFrame
|
||||
contentClassName="bg-muted/20"
|
||||
header={
|
||||
<RdxEditorHeader
|
||||
backLabel={t("account.header.back")}
|
||||
onBack={navigateBack}
|
||||
title={t("account.title")}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<RdxPageContainer width="content">
|
||||
<RdxEditorLayout>
|
||||
<div className="rounded-2xl border border-border/60 bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
|
||||
<div className="flex min-w-0 flex-col gap-6">
|
||||
<FormProvider {...ctrl.profileForm}>
|
||||
@ -405,8 +352,8 @@ export const AccountPage = () => {
|
||||
</RdxPanel>
|
||||
</div>
|
||||
</div>
|
||||
</RdxEditorPageLayout>
|
||||
</div>
|
||||
</RdxEditorLayout>
|
||||
</RdxPageContainer>
|
||||
|
||||
<AccountPasswordDialog
|
||||
ctrl={ctrl}
|
||||
@ -415,6 +362,6 @@ export const AccountPage = () => {
|
||||
onOpenChange={handlePasswordDialogOpenChange}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
</RdxPageFrame>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
export * from "./rdx-editor-action-bar.tsx";
|
||||
export * from "./rdx-editor-header.tsx";
|
||||
export * from "./rdx-editor-layout.tsx";
|
||||
export * from "./rdx-editor-page-layout.tsx";
|
||||
export * from "./rdx-panel.tsx";
|
||||
|
||||
@ -8,7 +8,6 @@ import { RdxFormGrid } from "../form/rdx-form-grid.tsx";
|
||||
import { RdxEditorActionBar } from "./rdx-editor-action-bar.tsx";
|
||||
import { RdxEditorHeader } from "./rdx-editor-header.tsx";
|
||||
import { RdxEditorLayout } from "./rdx-editor-layout.tsx";
|
||||
import { RdxEditorPageLayout } from "./rdx-editor-page-layout.tsx";
|
||||
|
||||
test("RdxEditorLayout only distributes main content and an optional aside", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
@ -50,44 +49,6 @@ test("RdxEditorHeader no renderiza back sin callback", () => {
|
||||
|
||||
import { RdxPanel } from "./rdx-panel.tsx";
|
||||
|
||||
test("RdxEditorPageLayout aplica complejidad simple por defecto y renderiza children", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<RdxEditorPageLayout>
|
||||
<div>Editor content</div>
|
||||
</RdxEditorPageLayout>
|
||||
);
|
||||
|
||||
assert.match(html, /Editor content/);
|
||||
assert.match(html, /max-w-5xl/);
|
||||
assert.match(html, /px-4/);
|
||||
assert.match(html, /md:px-6/);
|
||||
});
|
||||
|
||||
test("RdxEditorPageLayout soporta aside opcional y complejidad complex", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<RdxEditorPageLayout aside={<div>Aside content</div>} width="full">
|
||||
<div>Main content</div>
|
||||
</RdxEditorPageLayout>
|
||||
);
|
||||
|
||||
assert.match(html, /Main content/);
|
||||
assert.match(html, /Aside content/);
|
||||
assert.match(html, /xl:grid-cols-\[minmax\(0,1fr\)_22rem\]/);
|
||||
assert.doesNotMatch(html, /max-w-5xl/);
|
||||
});
|
||||
|
||||
test("RdxEditorPageLayout no reserva aside cuando no existe incluso en complex", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<RdxEditorPageLayout width="full">
|
||||
<div>Main content</div>
|
||||
</RdxEditorPageLayout>
|
||||
);
|
||||
|
||||
assert.match(html, /Main content/);
|
||||
assert.doesNotMatch(html, /<aside/);
|
||||
assert.doesNotMatch(html, /grid-cols-\[/);
|
||||
});
|
||||
|
||||
test("RdxPanel plain renderiza header semántico y contenido sin card", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<RdxPanel
|
||||
|
||||
@ -1,58 +0,0 @@
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
||||
|
||||
export type RdxEditorWidth = "wide" | "full";
|
||||
|
||||
export interface RdxEditorPageLayoutProps
|
||||
extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
||||
children: ReactNode;
|
||||
aside?: ReactNode;
|
||||
width?: RdxEditorWidth;
|
||||
}
|
||||
|
||||
const CONTENT_WIDTH_CLASSNAME: Record<RdxEditorWidth, string> = {
|
||||
wide: "mx-auto w-full max-w-5xl",
|
||||
full: "w-full",
|
||||
};
|
||||
|
||||
const ASIDE_GRID_CLASSNAME: Record<RdxEditorWidth, string> = {
|
||||
wide: "xl:grid-cols-[minmax(0,1fr)_280px]",
|
||||
full: "xl:grid-cols-[minmax(0,1fr)_22rem]",
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Distribuye el contenido interno de un contenedor en:
|
||||
*
|
||||
* - una región principal
|
||||
* - un aside opcional.
|
||||
*
|
||||
* No controla padding, scroll ni estructura general de página.
|
||||
* Para nuevos consumidores usar RdxPageContainer + RdxEditorLayout.
|
||||
*/
|
||||
|
||||
export const RdxEditorPageLayout = ({
|
||||
children,
|
||||
aside,
|
||||
width = "wide",
|
||||
className,
|
||||
...props
|
||||
}: RdxEditorPageLayoutProps) => {
|
||||
const contentClassName = CONTENT_WIDTH_CLASSNAME[width];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("rdx-editor-page-layout min-w-0 w-full px-4 py-4 md:px-6 md:py-6", className)}
|
||||
{...props}
|
||||
>
|
||||
{aside ? (
|
||||
<div className={cn(contentClassName, "grid min-w-0 gap-6", ASIDE_GRID_CLASSNAME[width])}>
|
||||
<div className="min-w-0">{children}</div>
|
||||
<aside className="min-w-0">{aside}</aside>
|
||||
</div>
|
||||
) : (
|
||||
<div className={cn("min-w-0", contentClassName)}>{children}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user