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,
|
RdxBadge,
|
||||||
RdxContextPanel,
|
RdxContextPanel,
|
||||||
RdxEditorActionBar,
|
RdxEditorActionBar,
|
||||||
RdxEditorPageLayout,
|
RdxEditorLayout,
|
||||||
RdxFormGrid,
|
RdxFormGrid,
|
||||||
RdxPageActions,
|
RdxPageActions,
|
||||||
RdxPageContainer,
|
RdxPageContainer,
|
||||||
@ -751,13 +751,15 @@ export default function UiKitPage() {
|
|||||||
title="Editor foundations"
|
title="Editor foundations"
|
||||||
>
|
>
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<ExampleCard title="RdxEditorPageLayout simple">
|
<ExampleCard title="RdxPageContainer + RdxEditorLayout">
|
||||||
<div className="rounded-lg border border-border bg-muted/30">
|
<div className="rounded-lg border border-border bg-muted/30">
|
||||||
<RdxEditorPageLayout complexity="simple">
|
<RdxPageContainer width="content">
|
||||||
|
<RdxEditorLayout>
|
||||||
<div className="rounded-lg border border-dashed border-border bg-background p-4 text-sm text-foreground">
|
<div className="rounded-lg border border-dashed border-border bg-background p-4 text-sm text-foreground">
|
||||||
Centered editor content
|
Centered editor content
|
||||||
</div>
|
</div>
|
||||||
</RdxEditorPageLayout>
|
</RdxEditorLayout>
|
||||||
|
</RdxPageContainer>
|
||||||
</div>
|
</div>
|
||||||
</ExampleCard>
|
</ExampleCard>
|
||||||
|
|
||||||
@ -806,7 +808,8 @@ export default function UiKitPage() {
|
|||||||
title="Simple editor composition"
|
title="Simple editor composition"
|
||||||
>
|
>
|
||||||
<div className="rounded-lg border border-border bg-muted/30">
|
<div className="rounded-lg border border-border bg-muted/30">
|
||||||
<RdxEditorPageLayout complexity="simple">
|
<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="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="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">
|
<div className="rounded-lg border border-border bg-background px-4 py-3 text-sm text-muted-foreground">
|
||||||
@ -846,7 +849,8 @@ export default function UiKitPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</RdxEditorPageLayout>
|
</RdxEditorLayout>
|
||||||
|
</RdxPageContainer>
|
||||||
</div>
|
</div>
|
||||||
</ExampleCard>
|
</ExampleCard>
|
||||||
|
|
||||||
@ -870,10 +874,14 @@ export default function UiKitPage() {
|
|||||||
|
|
||||||
<div className="flex min-h-0 flex-1 flex-col bg-muted/20">
|
<div className="flex min-h-0 flex-1 flex-col bg-muted/20">
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||||
<RdxEditorPageLayout
|
<RdxPageContainer width="full">
|
||||||
|
<RdxEditorLayout
|
||||||
aside={
|
aside={
|
||||||
<div className="flex min-w-0 flex-col gap-6">
|
<div className="flex min-w-0 flex-col gap-6">
|
||||||
<RdxPanel description="Secondary recipient controls." title="Recipient">
|
<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">
|
<div className="rounded-md border border-dashed border-border bg-muted/30 p-4 text-sm text-muted-foreground">
|
||||||
Customer selector and helper actions
|
Customer selector and helper actions
|
||||||
</div>
|
</div>
|
||||||
@ -894,7 +902,6 @@ export default function UiKitPage() {
|
|||||||
</RdxPanel>
|
</RdxPanel>
|
||||||
</div>
|
</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="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="flex min-w-0 flex-col gap-6">
|
||||||
@ -924,7 +931,8 @@ export default function UiKitPage() {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</RdxEditorPageLayout>
|
</RdxEditorLayout>
|
||||||
|
</RdxPageContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RdxEditorActionBar
|
<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";
|
import { Skeleton } from "@repo/shadcn-ui/components";
|
||||||
|
|
||||||
export const ProformaCreateSkeleton = () => {
|
export const ProformaCreateSkeleton = () => {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex flex-col overflow-hidden">
|
<RdxPageFrame
|
||||||
|
contentClassName="bg-muted/20"
|
||||||
|
header={
|
||||||
<div className="border-b px-6 py-4">
|
<div className="border-b px-6 py-4">
|
||||||
<Skeleton className="h-10 w-full max-w-md" />
|
<Skeleton className="h-10 w-full max-w-md" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 space-y-6 overflow-y-auto p-6">
|
}
|
||||||
|
>
|
||||||
|
<RdxPageContainer width="content">
|
||||||
|
<RdxEditorLayout>
|
||||||
|
<div className="space-y-6">
|
||||||
<Skeleton className="h-10 w-full" />
|
<Skeleton className="h-10 w-full" />
|
||||||
<Skeleton className="h-64 w-full" />
|
<Skeleton className="h-64 w-full" />
|
||||||
<Skeleton className="h-48 w-full" />
|
<Skeleton className="h-48 w-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</RdxEditorLayout>
|
||||||
|
</RdxPageContainer>
|
||||||
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -18,10 +18,10 @@ import { useCreateRecurringProformaFromProforma } from "../../../recurring-profo
|
|||||||
import { buildRecurringProformaDetailPath } from "../../../recurring-proformas/shared/recurring-proforma-navigation";
|
import { buildRecurringProformaDetailPath } from "../../../recurring-proformas/shared/recurring-proforma-navigation";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildCreateRecurringProformaFromProformaDefaultValues,
|
|
||||||
buildCreateRecurringProformaFromProformaInput,
|
|
||||||
type CreateRecurringProformaFromProformaForm,
|
type CreateRecurringProformaFromProformaForm,
|
||||||
CreateRecurringProformaFromProformaFormSchema,
|
CreateRecurringProformaFromProformaFormSchema,
|
||||||
|
buildCreateRecurringProformaFromProformaDefaultValues,
|
||||||
|
buildCreateRecurringProformaFromProformaInput,
|
||||||
} from "./use-create-recurring-proforma-from-proforma-dialog-controller.utils";
|
} from "./use-create-recurring-proforma-from-proforma-dialog-controller.utils";
|
||||||
|
|
||||||
const normalizeSubmitError = (error: unknown): Error | ValidationErrorCollection => {
|
const normalizeSubmitError = (error: unknown): Error | ValidationErrorCollection => {
|
||||||
@ -41,6 +41,9 @@ export interface CreateRecurringProformaFromProformaDialogControllerParams {
|
|||||||
proformaReference: string | null;
|
proformaReference: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateRecurringProformaFromProformaTarget =
|
||||||
|
CreateRecurringProformaFromProformaDialogControllerParams;
|
||||||
|
|
||||||
export const useCreateRecurringProformaFromProformaDialogController = ({
|
export const useCreateRecurringProformaFromProformaDialogController = ({
|
||||||
proformaId,
|
proformaId,
|
||||||
proformaReference,
|
proformaReference,
|
||||||
@ -51,6 +54,10 @@ export const useCreateRecurringProformaFromProformaDialogController = ({
|
|||||||
const mutation = useCreateRecurringProformaFromProforma();
|
const mutation = useCreateRecurringProformaFromProforma();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const submitInProgressRef = useRef(false);
|
const submitInProgressRef = useRef(false);
|
||||||
|
const targetRef = useRef<CreateRecurringProformaFromProformaTarget>({
|
||||||
|
proformaId,
|
||||||
|
proformaReference,
|
||||||
|
});
|
||||||
const form = useHookForm<CreateRecurringProformaFromProformaForm>({
|
const form = useHookForm<CreateRecurringProformaFromProformaForm>({
|
||||||
resolverSchema: CreateRecurringProformaFromProformaFormSchema,
|
resolverSchema: CreateRecurringProformaFromProformaFormSchema,
|
||||||
initialValues: buildCreateRecurringProformaFromProformaDefaultValues(proformaReference),
|
initialValues: buildCreateRecurringProformaFromProformaDefaultValues(proformaReference),
|
||||||
@ -77,8 +84,10 @@ export const useCreateRecurringProformaFromProformaDialogController = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openDialog = () => {
|
const openDialog = (target?: CreateRecurringProformaFromProformaTarget) => {
|
||||||
form.reset(buildCreateRecurringProformaFromProformaDefaultValues(proformaReference));
|
const nextTarget = target ?? { proformaId, proformaReference };
|
||||||
|
targetRef.current = nextTarget;
|
||||||
|
form.reset(buildCreateRecurringProformaFromProformaDefaultValues(nextTarget.proformaReference));
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -100,7 +109,7 @@ export const useCreateRecurringProformaFromProformaDialogController = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const created = await mutation.mutateAsync({
|
const created = await mutation.mutateAsync({
|
||||||
proformaId,
|
proformaId: targetRef.current.proformaId,
|
||||||
data: buildCreateRecurringProformaFromProformaInput(values),
|
data: buildCreateRecurringProformaFromProformaInput(values),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import test from "node:test";
|
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 { 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 type { ProformaListRow } from "../../../../shared";
|
||||||
import { PROFORMA_STATUS } from "../../../../shared";
|
import { PROFORMA_STATUS } from "../../../../shared";
|
||||||
|
|
||||||
import { useProformasGridColumns } from "./use-proforma-grid-columns";
|
import { useProformasGridColumns } from "./use-proforma-grid-columns";
|
||||||
|
|
||||||
const proformaFixture: ProformaListRow = {
|
const proformaFixture: ProformaListRow = {
|
||||||
@ -104,12 +105,16 @@ test("mantiene las destructivas en overflow y conserva disabled", () => {
|
|||||||
assert.equal(viewAction?.disabled, true);
|
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 didView = false;
|
||||||
let didEdit = false;
|
let didEdit = false;
|
||||||
|
let didCreateRecurring = false;
|
||||||
let didDelete = false;
|
let didDelete = false;
|
||||||
|
|
||||||
const props = getRowActionsProps("normal", {
|
const props = getRowActionsProps("normal", {
|
||||||
|
onCreateRecurringClick: () => {
|
||||||
|
didCreateRecurring = true;
|
||||||
|
},
|
||||||
onDeleteClick: () => {
|
onDeleteClick: () => {
|
||||||
didDelete = true;
|
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 === "view")?.onSelect();
|
||||||
props.primaryActions?.find((action) => action.id === "edit")?.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();
|
props.overflowActions?.find((action) => action.id === "delete")?.onSelect();
|
||||||
|
|
||||||
assert.equal(didView, true);
|
assert.equal(didView, true);
|
||||||
assert.equal(didEdit, true);
|
assert.equal(didEdit, true);
|
||||||
|
assert.equal(didCreateRecurring, true);
|
||||||
assert.equal(didDelete, 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 { 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 { DateHelper } from "@repo/rdx-utils";
|
||||||
import {
|
import { Checkbox, Spinner } from "@repo/shadcn-ui/components";
|
||||||
Checkbox,
|
|
||||||
Spinner,
|
|
||||||
} from "@repo/shadcn-ui/components";
|
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import {
|
import {
|
||||||
|
CalendarSyncIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
@ -37,6 +35,7 @@ type GridActionHandlers = {
|
|||||||
onIssueClick?: (proforma: ProformaListRow) => void;
|
onIssueClick?: (proforma: ProformaListRow) => void;
|
||||||
onArchiveClick?: (proforma: ProformaListRow) => void;
|
onArchiveClick?: (proforma: ProformaListRow) => void;
|
||||||
onDuplicateClick?: (proforma: ProformaListRow) => void;
|
onDuplicateClick?: (proforma: ProformaListRow) => void;
|
||||||
|
onCreateRecurringClick?: (proforma: ProformaListRow) => void;
|
||||||
onUnarchiveClick?: (proforma: ProformaListRow) => void;
|
onUnarchiveClick?: (proforma: ProformaListRow) => void;
|
||||||
onDeleteClick?: (proforma: ProformaListRow) => void;
|
onDeleteClick?: (proforma: ProformaListRow) => void;
|
||||||
onChangeStatusClick?: (proforma: ProformaListRow, nextStatus: ProformaStatus) => void;
|
onChangeStatusClick?: (proforma: ProformaListRow, nextStatus: ProformaStatus) => void;
|
||||||
@ -52,10 +51,8 @@ type GridActionHandlers = {
|
|||||||
isPdfDownloading?: boolean;
|
isPdfDownloading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderActionIcon = (
|
const renderActionIcon = (Icon: ComponentType<{ className?: string }>, loading?: boolean) =>
|
||||||
Icon: ComponentType<{ className?: string }>,
|
loading ? <Spinner className="size-4" /> : <Icon className="size-4" />;
|
||||||
loading?: boolean
|
|
||||||
) => (loading ? <Spinner className="size-4" /> : <Icon className="size-4" />);
|
|
||||||
|
|
||||||
export function useProformasGridColumns(
|
export function useProformasGridColumns(
|
||||||
scope: ProformaListScope,
|
scope: ProformaListScope,
|
||||||
@ -312,6 +309,7 @@ export function useProformasGridColumns(
|
|||||||
const handleView = () => actionHandlers.onViewClick?.(proforma);
|
const handleView = () => actionHandlers.onViewClick?.(proforma);
|
||||||
const handleEdit = () => actionHandlers.onEditClick?.(proforma);
|
const handleEdit = () => actionHandlers.onEditClick?.(proforma);
|
||||||
const handleDuplicate = () => actionHandlers.onDuplicateClick?.(proforma);
|
const handleDuplicate = () => actionHandlers.onDuplicateClick?.(proforma);
|
||||||
|
const handleCreateRecurring = () => actionHandlers.onCreateRecurringClick?.(proforma);
|
||||||
const handleArchive = () => actionHandlers.onArchiveClick?.(proforma);
|
const handleArchive = () => actionHandlers.onArchiveClick?.(proforma);
|
||||||
const handleUnarchive = () => actionHandlers.onUnarchiveClick?.(proforma);
|
const handleUnarchive = () => actionHandlers.onUnarchiveClick?.(proforma);
|
||||||
const handleIssue = () => actionHandlers.onIssueClick?.(proforma);
|
const handleIssue = () => actionHandlers.onIssueClick?.(proforma);
|
||||||
@ -416,6 +414,14 @@ export function useProformasGridColumns(
|
|||||||
: undefined,
|
: undefined,
|
||||||
onSelect: handleDuplicate,
|
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",
|
id: "archive",
|
||||||
label: t("pages.proformas.list.actions.archive"),
|
label: t("pages.proformas.list.actions.archive"),
|
||||||
|
|||||||
@ -32,6 +32,8 @@ import { ChangeProformaStatusDialog } from "../../../change-status";
|
|||||||
import { prepareChangeProformaStatusTargets } from "../../../change-status/utils";
|
import { prepareChangeProformaStatusTargets } from "../../../change-status/utils";
|
||||||
import { DeleteProformaDialog } from "../../../delete";
|
import { DeleteProformaDialog } from "../../../delete";
|
||||||
import { prepareDeleteProformaTargets } from "../../../delete/utils";
|
import { prepareDeleteProformaTargets } from "../../../delete/utils";
|
||||||
|
import { useCreateRecurringProformaFromProformaDialogController } from "../../../detail/controllers";
|
||||||
|
import { CreateRecurringProformaFromProformaDialog } from "../../../detail/ui/components";
|
||||||
import { downloadProformaPDFApi } from "../../../download-pdf/api";
|
import { downloadProformaPDFApi } from "../../../download-pdf/api";
|
||||||
import { IssueProformaDialog } from "../../../issue-proforma";
|
import { IssueProformaDialog } from "../../../issue-proforma";
|
||||||
import { prepareIssueProformaTarget } from "../../../issue-proforma/utils";
|
import { prepareIssueProformaTarget } from "../../../issue-proforma/utils";
|
||||||
@ -238,6 +240,10 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
|
|||||||
goHome,
|
goHome,
|
||||||
isRetrying,
|
isRetrying,
|
||||||
} = useListProformasPageController(scope);
|
} = useListProformasPageController(scope);
|
||||||
|
const createRecurringDialogCtrl = useCreateRecurringProformaFromProformaDialogController({
|
||||||
|
proformaId: "",
|
||||||
|
proformaReference: null,
|
||||||
|
});
|
||||||
|
|
||||||
const handleEditClick = (proformaId: string) => {
|
const handleEditClick = (proformaId: string) => {
|
||||||
navigate({
|
navigate({
|
||||||
@ -293,6 +299,11 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
|
|||||||
onViewClick: (proforma) => handleOpenDetailClick(proforma.id),
|
onViewClick: (proforma) => handleOpenDetailClick(proforma.id),
|
||||||
onEditClick: (proforma) => handleEditClick(proforma.id),
|
onEditClick: (proforma) => handleEditClick(proforma.id),
|
||||||
onDuplicateClick: handleDuplicateClick,
|
onDuplicateClick: handleDuplicateClick,
|
||||||
|
onCreateRecurringClick: (proforma) =>
|
||||||
|
createRecurringDialogCtrl.openDialog({
|
||||||
|
proformaId: proforma.id,
|
||||||
|
proformaReference: proforma.proformaReference,
|
||||||
|
}),
|
||||||
onIssueClick: (proformaRow) =>
|
onIssueClick: (proformaRow) =>
|
||||||
issueDialogCtrl.openDialog(prepareIssueProformaTarget(proformaRow)),
|
issueDialogCtrl.openDialog(prepareIssueProformaTarget(proformaRow)),
|
||||||
onArchiveClick: archiveDialogCtrl.openDialog,
|
onArchiveClick: archiveDialogCtrl.openDialog,
|
||||||
@ -550,6 +561,21 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
|
|||||||
targets={changeStatusDialogCtrl.targets}
|
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
|
<DeleteProformaDialog
|
||||||
isSubmitting={deleteDialogCtrl.isSubmitting}
|
isSubmitting={deleteDialogCtrl.isSubmitting}
|
||||||
onConfirm={deleteDialogCtrl.confirmDelete}
|
onConfirm={deleteDialogCtrl.confirmDelete}
|
||||||
|
|||||||
@ -88,7 +88,7 @@ Contenido visible actual:
|
|||||||
|
|
||||||
- información personal: `name`, `email`, `language_code`
|
- información personal: `name`, `email`, `language_code`
|
||||||
- modo `app-fullscreen`, sin sidebar principal
|
- 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
|
- seguridad mediante dialog para cambio de contraseña
|
||||||
- cierre de sesión reutilizando el flujo de logout existente
|
- 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 { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
||||||
import {
|
import {
|
||||||
CancelActionButton,
|
CancelActionButton,
|
||||||
RdxEditorActionBar,
|
RdxEditorActionBar,
|
||||||
RdxEditorHeader,
|
RdxEditorHeader,
|
||||||
RdxEditorPageLayout,
|
RdxEditorLayout,
|
||||||
|
RdxPageContainer,
|
||||||
|
RdxPageFrame,
|
||||||
RdxPanel,
|
RdxPanel,
|
||||||
RhfSubmitActionButton,
|
RhfSubmitActionButton,
|
||||||
SelectField,
|
SelectField,
|
||||||
@ -27,65 +29,7 @@ import { useTranslation } from "../../../i18n";
|
|||||||
import { useAccountPageController } from "../../controllers";
|
import { useAccountPageController } from "../../controllers";
|
||||||
import type { AccountPasswordForm, AccountProfileForm } from "../../entities";
|
import type { AccountPasswordForm, AccountProfileForm } from "../../entities";
|
||||||
|
|
||||||
const AccountLoadingState = ({ label }: { label: string }) => {
|
import { AccountErrorState, AccountLoadingState } from "./account-page-states";
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AccountPasswordDialog = ({
|
const AccountPasswordDialog = ({
|
||||||
ctrl,
|
ctrl,
|
||||||
@ -252,15 +196,18 @@ export const AccountPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex flex-col overflow-hidden">
|
<RdxPageFrame
|
||||||
|
contentClassName="bg-muted/20"
|
||||||
|
header={
|
||||||
<RdxEditorHeader
|
<RdxEditorHeader
|
||||||
backLabel={t("account.header.back")}
|
backLabel={t("account.header.back")}
|
||||||
onBack={navigateBack}
|
onBack={navigateBack}
|
||||||
title={t("account.title")}
|
title={t("account.title")}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
>
|
||||||
<RdxEditorPageLayout width="simple">
|
<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="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="flex min-w-0 flex-col gap-6">
|
||||||
<FormProvider {...ctrl.profileForm}>
|
<FormProvider {...ctrl.profileForm}>
|
||||||
@ -405,8 +352,8 @@ export const AccountPage = () => {
|
|||||||
</RdxPanel>
|
</RdxPanel>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</RdxEditorPageLayout>
|
</RdxEditorLayout>
|
||||||
</div>
|
</RdxPageContainer>
|
||||||
|
|
||||||
<AccountPasswordDialog
|
<AccountPasswordDialog
|
||||||
ctrl={ctrl}
|
ctrl={ctrl}
|
||||||
@ -415,6 +362,6 @@ export const AccountPage = () => {
|
|||||||
onOpenChange={handlePasswordDialogOpenChange}
|
onOpenChange={handlePasswordDialogOpenChange}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
</div>
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
export * from "./rdx-editor-action-bar.tsx";
|
export * from "./rdx-editor-action-bar.tsx";
|
||||||
export * from "./rdx-editor-header.tsx";
|
export * from "./rdx-editor-header.tsx";
|
||||||
export * from "./rdx-editor-layout.tsx";
|
export * from "./rdx-editor-layout.tsx";
|
||||||
export * from "./rdx-editor-page-layout.tsx";
|
|
||||||
export * from "./rdx-panel.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 { RdxEditorActionBar } from "./rdx-editor-action-bar.tsx";
|
||||||
import { RdxEditorHeader } from "./rdx-editor-header.tsx";
|
import { RdxEditorHeader } from "./rdx-editor-header.tsx";
|
||||||
import { RdxEditorLayout } from "./rdx-editor-layout.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", () => {
|
test("RdxEditorLayout only distributes main content and an optional aside", () => {
|
||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
@ -50,44 +49,6 @@ test("RdxEditorHeader no renderiza back sin callback", () => {
|
|||||||
|
|
||||||
import { RdxPanel } from "./rdx-panel.tsx";
|
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", () => {
|
test("RdxPanel plain renderiza header semántico y contenido sin card", () => {
|
||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
<RdxPanel
|
<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