Añadir confirmación de eliminación en acciones en línea de filas y mejorar pruebas para la tabla de cotizaciones
This commit is contained in:
parent
2527c2c785
commit
ec87ba82f0
@ -1,6 +1,6 @@
|
|||||||
import { act } from "react";
|
import { act } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import type { ButtonHTMLAttributes } from "react";
|
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
||||||
import { QuoteItemsInlineRowActions } from "./QuoteItemsInlineRowActions";
|
import { QuoteItemsInlineRowActions } from "./QuoteItemsInlineRowActions";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@ -12,6 +12,15 @@ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|||||||
jest.mock("i18next", () => ({ t: (key: string) => key }));
|
jest.mock("i18next", () => ({ t: (key: string) => key }));
|
||||||
jest.mock("@/ui", () => ({
|
jest.mock("@/ui", () => ({
|
||||||
Button: (props: ButtonHTMLAttributes<HTMLButtonElement>) => <button {...props} />,
|
Button: (props: ButtonHTMLAttributes<HTMLButtonElement>) => <button {...props} />,
|
||||||
|
AlertDialog: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||||
|
AlertDialogAction: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||||
|
AlertDialogCancel: ({ children }: { children: ReactNode }) => <button>{children}</button>,
|
||||||
|
AlertDialogContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||||
|
AlertDialogDescription: ({ children }: { children: ReactNode }) => <p>{children}</p>,
|
||||||
|
AlertDialogFooter: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||||
|
AlertDialogHeader: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||||
|
AlertDialogTitle: ({ children }: { children: ReactNode }) => <h2>{children}</h2>,
|
||||||
|
AlertDialogTrigger: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("QuoteItemsInlineRowActions", () => {
|
describe("QuoteItemsInlineRowActions", () => {
|
||||||
@ -29,7 +38,7 @@ describe("QuoteItemsInlineRowActions", () => {
|
|||||||
container.remove();
|
container.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reuses table meta actions for the active row index", () => {
|
it("reuses table meta actions and only deletes after confirmation", () => {
|
||||||
const insertItem = jest.fn();
|
const insertItem = jest.fn();
|
||||||
const duplicateItems = jest.fn();
|
const duplicateItems = jest.fn();
|
||||||
const deleteItems = jest.fn();
|
const deleteItems = jest.fn();
|
||||||
@ -41,15 +50,22 @@ describe("QuoteItemsInlineRowActions", () => {
|
|||||||
root.render(<QuoteItemsInlineRowActions row={row} table={table} onDelete={onDelete} />);
|
root.render(<QuoteItemsInlineRowActions row={row} table={table} onDelete={onDelete} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = Array.from(container.querySelectorAll("button"));
|
const getButton = (label: string) =>
|
||||||
act(() => buttons[0].click());
|
Array.from(container.querySelectorAll("button")).find((button) => button.textContent === label) as HTMLButtonElement;
|
||||||
act(() => buttons[1].click());
|
|
||||||
act(() => buttons[2].click());
|
act(() => getButton("common.insert_row_above").click());
|
||||||
act(() => buttons[3].click());
|
act(() => getButton("common.insert_row_below").click());
|
||||||
|
act(() => getButton("common.duplicate_row").click());
|
||||||
|
act(() => getButton("common.remove_row").click());
|
||||||
|
|
||||||
expect(insertItem).toHaveBeenNthCalledWith(1, 2);
|
expect(insertItem).toHaveBeenNthCalledWith(1, 2);
|
||||||
expect(insertItem).toHaveBeenNthCalledWith(2, 3);
|
expect(insertItem).toHaveBeenNthCalledWith(2, 3);
|
||||||
expect(duplicateItems).toHaveBeenCalledWith(2);
|
expect(duplicateItems).toHaveBeenCalledWith(2);
|
||||||
|
expect(deleteItems).not.toHaveBeenCalled();
|
||||||
|
expect(onDelete).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
act(() => getButton("quotes.inline_row_actions.delete_confirmation.confirm").click());
|
||||||
|
|
||||||
expect(deleteItems).toHaveBeenCalledWith(2);
|
expect(deleteItems).toHaveBeenCalledWith(2);
|
||||||
expect(onDelete).toHaveBeenCalledTimes(1);
|
expect(onDelete).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,18 +1,31 @@
|
|||||||
import { Button } from "@/ui";
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
Button,
|
||||||
|
} from "@/ui";
|
||||||
import { Row, RowData, Table } from "@tanstack/react-table";
|
import { Row, RowData, Table } from "@tanstack/react-table";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { ChevronDownIcon, ChevronUpIcon, CopyIcon, Trash2Icon } from "lucide-react";
|
import { ChevronDownIcon, ChevronUpIcon, CopyIcon, Trash2Icon } from "lucide-react";
|
||||||
|
|
||||||
export const QuoteItemsInlineRowActions = <TData extends RowData>({
|
export const QuoteItemsInlineRowActions = <TData extends RowData>({ row, table, onDelete }: {
|
||||||
row,
|
|
||||||
table,
|
|
||||||
onDelete,
|
|
||||||
}: {
|
|
||||||
row: Row<TData>;
|
row: Row<TData>;
|
||||||
table: Table<TData>;
|
table: Table<TData>;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
}) => (
|
}) => {
|
||||||
<div className='flex flex-wrap items-center gap-2'>
|
const handleDelete = () => {
|
||||||
|
table.options.meta?.deleteItems(row.index);
|
||||||
|
onDelete();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex flex-wrap items-center gap-2'>
|
||||||
<Button type='button' variant='outline' size='sm' onClick={() => table.options.meta?.insertItem(row.index)}>
|
<Button type='button' variant='outline' size='sm' onClick={() => table.options.meta?.insertItem(row.index)}>
|
||||||
<ChevronUpIcon className='w-4 h-4 mr-2' />
|
<ChevronUpIcon className='w-4 h-4 mr-2' />
|
||||||
{t("common.insert_row_above")}
|
{t("common.insert_row_above")}
|
||||||
@ -30,17 +43,30 @@ export const QuoteItemsInlineRowActions = <TData extends RowData>({
|
|||||||
<CopyIcon className='w-4 h-4 mr-2' />
|
<CopyIcon className='w-4 h-4 mr-2' />
|
||||||
{t("common.duplicate_row")}
|
{t("common.duplicate_row")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<AlertDialog>
|
||||||
type='button'
|
<AlertDialogTrigger asChild>
|
||||||
variant='destructive'
|
<Button type='button' variant='destructive' size='sm'>
|
||||||
size='sm'
|
<Trash2Icon className='w-4 h-4 mr-2' />
|
||||||
onClick={() => {
|
{t("common.remove_row")}
|
||||||
table.options.meta?.deleteItems(row.index);
|
</Button>
|
||||||
onDelete();
|
</AlertDialogTrigger>
|
||||||
}}
|
<AlertDialogContent>
|
||||||
>
|
<AlertDialogHeader>
|
||||||
<Trash2Icon className='w-4 h-4 mr-2' />
|
<AlertDialogTitle>{t("quotes.inline_row_actions.delete_confirmation.title")}</AlertDialogTitle>
|
||||||
{t("common.remove_row")}
|
<AlertDialogDescription>
|
||||||
</Button>
|
{t("quotes.inline_row_actions.delete_confirmation.description")}
|
||||||
</div>
|
</AlertDialogDescription>
|
||||||
);
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
||||||
|
<AlertDialogAction asChild>
|
||||||
|
<Button type='button' variant='destructive' onClick={handleDelete}>
|
||||||
|
{t("quotes.inline_row_actions.delete_confirmation.confirm")}
|
||||||
|
</Button>
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@ -0,0 +1,193 @@
|
|||||||
|
import { act } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import type {
|
||||||
|
ButtonHTMLAttributes,
|
||||||
|
HTMLAttributes,
|
||||||
|
ReactNode,
|
||||||
|
TdHTMLAttributes,
|
||||||
|
} from "react";
|
||||||
|
import { QuoteItemsSortableDataTable } from "./QuoteItemsSortableDataTable";
|
||||||
|
|
||||||
|
let latestTableMeta: any;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
var IS_REACT_ACT_ENVIRONMENT: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
|
||||||
|
jest.mock("@dnd-kit/core", () => ({
|
||||||
|
DndContext: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||||
|
DragOverlay: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||||
|
MeasuringStrategy: { Always: "always" },
|
||||||
|
MouseSensor: class {},
|
||||||
|
TouchSensor: class {},
|
||||||
|
KeyboardSensor: class {},
|
||||||
|
closestCenter: jest.fn(),
|
||||||
|
defaultDropAnimation: { duration: 0 },
|
||||||
|
useSensor: jest.fn(),
|
||||||
|
useSensors: jest.fn(() => []),
|
||||||
|
}));
|
||||||
|
jest.mock("@dnd-kit/sortable", () => ({
|
||||||
|
SortableContext: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||||
|
verticalListSortingStrategy: "vertical",
|
||||||
|
}));
|
||||||
|
jest.mock("@dnd-kit/utilities", () => ({ CSS: { Transform: { toString: () => "" } } }));
|
||||||
|
jest.mock("@/components", () => ({
|
||||||
|
ButtonGroup: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||||
|
DataTableColumnHeader: () => null,
|
||||||
|
}));
|
||||||
|
jest.mock("@/ui", () => ({
|
||||||
|
Badge: ({ children }: { children: ReactNode }) => <span>{children}</span>,
|
||||||
|
Button: ({ children, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||||
|
<button {...props}>{children}</button>
|
||||||
|
),
|
||||||
|
Card: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||||
|
CardContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||||
|
CardFooter: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||||
|
CardHeader: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||||
|
CardTitle: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||||
|
}));
|
||||||
|
jest.mock("@/ui/table", () => ({
|
||||||
|
Table: ({ children }: { children: ReactNode }) => <table>{children}</table>,
|
||||||
|
TableBody: ({ children }: { children: ReactNode }) => <tbody>{children}</tbody>,
|
||||||
|
TableCell: ({ children, ...props }: TdHTMLAttributes<HTMLTableCellElement>) => <td {...props}>{children}</td>,
|
||||||
|
TableHead: ({ children }: { children: ReactNode }) => <th>{children}</th>,
|
||||||
|
TableHeader: ({ children }: { children: ReactNode }) => <thead>{children}</thead>,
|
||||||
|
TableRow: ({ children, ...props }: HTMLAttributes<HTMLTableRowElement>) => <tr {...props}>{children}</tr>,
|
||||||
|
}));
|
||||||
|
jest.mock("./QuoteItemsSortableDataTableToolbar", () => ({ QuoteItemsSortableDataTableToolbar: () => null }));
|
||||||
|
jest.mock("./QuoteItemsSortableTableRow", () => ({
|
||||||
|
QuoteItemsSortableTableRow: ({ id, isActive, onActivate, children }: any) => (
|
||||||
|
<tr data-row-id={id} data-active={String(isActive)} onClick={onActivate} onFocusCapture={onActivate}>
|
||||||
|
{children}
|
||||||
|
</tr>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
jest.mock("./QuoteItemsInlineRowActions", () => ({
|
||||||
|
QuoteItemsInlineRowActions: ({ row, onDelete }: any) => (
|
||||||
|
<button data-inline-row-id={row.id} onClick={onDelete}>delete active</button>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("@tanstack/react-table", () => ({
|
||||||
|
flexRender: (value: unknown) => value,
|
||||||
|
getCoreRowModel: jest.fn(),
|
||||||
|
useReactTable: (options: any) => {
|
||||||
|
latestTableMeta = options.meta;
|
||||||
|
const rows = options.data.map((item: { id: string }, index: number) => ({
|
||||||
|
id: item.id,
|
||||||
|
index,
|
||||||
|
original: item,
|
||||||
|
getVisibleCells: () => [{ id: `${item.id}-cell`, column: { getSize: () => 100, columnDef: { cell: item.id } }, getContext: () => ({}) }],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const table = {
|
||||||
|
options: { meta: options.meta },
|
||||||
|
getHeaderGroups: () => [],
|
||||||
|
getRowModel: () => ({ rows }),
|
||||||
|
getSelectedRowModel: () => ({ rows: [], rowsById: {} }),
|
||||||
|
resetRowSelection: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return table;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
append: jest.fn(),
|
||||||
|
insert: jest.fn(),
|
||||||
|
remove: jest.fn(),
|
||||||
|
move: jest.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const renderTable = (root: ReturnType<typeof createRoot>, data: { id: string }[]) => {
|
||||||
|
act(() => {
|
||||||
|
root.render(
|
||||||
|
<QuoteItemsSortableDataTable
|
||||||
|
actions={actions}
|
||||||
|
columns={[]}
|
||||||
|
data={data}
|
||||||
|
defaultValues={{ items: [{}] }}
|
||||||
|
showLineDiscountColumn={true}
|
||||||
|
hasLineDiscounts={false}
|
||||||
|
onShowLineDiscountColumnChange={jest.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("QuoteItemsSortableDataTable active row", () => {
|
||||||
|
let container: HTMLDivElement;
|
||||||
|
let root: ReturnType<typeof createRoot>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
container = document.createElement("div");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
root = createRoot(container);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
act(() => root.unmount());
|
||||||
|
container.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows one inline band for the row activated by click or focus", () => {
|
||||||
|
renderTable(root, [{ id: "a" }, { id: "b" }]);
|
||||||
|
|
||||||
|
act(() => (container.querySelector('[data-row-id="a"]') as HTMLTableRowElement).click());
|
||||||
|
expect(container.querySelector('[data-inline-row-id="a"]')).not.toBeNull();
|
||||||
|
expect(container.querySelectorAll("[data-inline-row-id]")).toHaveLength(1);
|
||||||
|
|
||||||
|
act(() =>
|
||||||
|
(container.querySelector('[data-row-id="b"]') as HTMLTableRowElement).dispatchEvent(
|
||||||
|
new FocusEvent("focusin", { bubbles: true }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(container.querySelector('[data-inline-row-id="a"]')).toBeNull();
|
||||||
|
expect(container.querySelector('[data-inline-row-id="b"]')).not.toBeNull();
|
||||||
|
expect(container.querySelectorAll("[data-inline-row-id]")).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the active row identity after reorder and clears it after deletion", () => {
|
||||||
|
renderTable(root, [{ id: "a" }, { id: "b" }]);
|
||||||
|
act(() => (container.querySelector('[data-row-id="a"]') as HTMLTableRowElement).click());
|
||||||
|
|
||||||
|
renderTable(root, [{ id: "b" }, { id: "a" }]);
|
||||||
|
expect(container.querySelector('[data-inline-row-id="a"]')).not.toBeNull();
|
||||||
|
|
||||||
|
act(() => (container.querySelector('[data-inline-row-id="a"]') as HTMLButtonElement).click());
|
||||||
|
expect(container.querySelector('[data-inline-row-id]')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("duplicates the current form value rather than the useFieldArray field snapshot", () => {
|
||||||
|
const getItem = jest.fn(() => ({ id: "a", description: "edited without blurring" }));
|
||||||
|
const insert = jest.fn();
|
||||||
|
|
||||||
|
renderTable(root, [{ id: "a" }]);
|
||||||
|
latestTableMeta = undefined;
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
root.render(
|
||||||
|
<QuoteItemsSortableDataTable
|
||||||
|
actions={{ ...actions, getItem, insert }}
|
||||||
|
columns={[]}
|
||||||
|
data={[{ id: "a", description: "stale value" }]}
|
||||||
|
defaultValues={{ items: [{}] }}
|
||||||
|
showLineDiscountColumn={true}
|
||||||
|
hasLineDiscounts={false}
|
||||||
|
onShowLineDiscountColumnChange={jest.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => latestTableMeta.duplicateItems(0));
|
||||||
|
|
||||||
|
expect(getItem).toHaveBeenCalledWith(0);
|
||||||
|
expect(insert).toHaveBeenCalledWith(
|
||||||
|
1,
|
||||||
|
{ id: undefined, description: "edited without blurring" },
|
||||||
|
{ shouldFocus: true },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -74,6 +74,7 @@ export type QuoteItemsSortableDataTableProps<
|
|||||||
actions: Omit<UseFieldArrayReturn<FieldValues, "items">, "fields"> & {
|
actions: Omit<UseFieldArrayReturn<FieldValues, "items">, "fields"> & {
|
||||||
pickCatalogArticle?: () => void;
|
pickCatalogArticle?: () => void;
|
||||||
pickBlock?: () => void;
|
pickBlock?: () => void;
|
||||||
|
getItem?: (rowIndex: number) => TData | undefined;
|
||||||
};
|
};
|
||||||
showLineDiscountColumn: boolean;
|
showLineDiscountColumn: boolean;
|
||||||
hasLineDiscounts: boolean;
|
hasLineDiscounts: boolean;
|
||||||
@ -179,8 +180,8 @@ export function QuoteItemsSortableDataTable<TData extends RowData & RowIdData, T
|
|||||||
},
|
},
|
||||||
duplicateItems: (rowIndex?: number) => {
|
duplicateItems: (rowIndex?: number) => {
|
||||||
if (rowIndex != undefined) {
|
if (rowIndex != undefined) {
|
||||||
const originalData = table.getRowModel().rows[rowIndex].original;
|
const originalData = actions.getItem?.(rowIndex) ?? table.getRowModel().rows[rowIndex].original;
|
||||||
actions.insert(rowIndex + 1, originalData, { shouldFocus: true });
|
actions.insert(rowIndex + 1, { ...originalData, id: undefined }, { shouldFocus: true });
|
||||||
} else if (table.getSelectedRowModel().rows.length) {
|
} else if (table.getSelectedRowModel().rows.length) {
|
||||||
const lastIndex =
|
const lastIndex =
|
||||||
table.getSelectedRowModel().rows[table.getSelectedRowModel().rows.length - 1].index;
|
table.getSelectedRowModel().rows[table.getSelectedRowModel().rows.length - 1].index;
|
||||||
|
|||||||
@ -252,6 +252,7 @@ export const QuoteDetailsCardEditor = ({
|
|||||||
<QuoteItemsSortableDataTable
|
<QuoteItemsSortableDataTable
|
||||||
actions={{
|
actions={{
|
||||||
...fieldActions,
|
...fieldActions,
|
||||||
|
getItem: (rowIndex) => getValues(`items.${rowIndex}`),
|
||||||
pickCatalogArticle: () => setArticlePickerDialogOpen(true),
|
pickCatalogArticle: () => setArticlePickerDialogOpen(true),
|
||||||
pickBlock: () => setBlockPickerDialogOpen(true),
|
pickBlock: () => setBlockPickerDialogOpen(true),
|
||||||
}}
|
}}
|
||||||
@ -300,7 +301,10 @@ export const QuoteDetailsCardEditor = ({
|
|||||||
className={cn(isCollapsed && "min-w-[50px] transition-all duration-300 ease-in-out")}
|
className={cn(isCollapsed && "min-w-[50px] transition-all duration-300 ease-in-out")}
|
||||||
>
|
>
|
||||||
<QuoteItemsSortableDataTable
|
<QuoteItemsSortableDataTable
|
||||||
actions={fieldActions}
|
actions={{
|
||||||
|
...fieldActions,
|
||||||
|
getItem: (rowIndex) => getValues(`items.${rowIndex}`),
|
||||||
|
}}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={fields}
|
data={fields}
|
||||||
defaultValues={defaultValues}
|
defaultValues={defaultValues}
|
||||||
|
|||||||
@ -284,6 +284,13 @@
|
|||||||
"confirm": "Hide column"
|
"confirm": "Hide column"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"inline_row_actions": {
|
||||||
|
"delete_confirmation": {
|
||||||
|
"title": "Remove row",
|
||||||
|
"description": "Are you sure you want to remove this row? This action cannot be undone.",
|
||||||
|
"confirm": "Remove row"
|
||||||
|
}
|
||||||
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"draft": "Draft",
|
"draft": "Draft",
|
||||||
"ready": "Ready",
|
"ready": "Ready",
|
||||||
|
|||||||
@ -293,6 +293,13 @@
|
|||||||
"confirm": "Ocultar columna"
|
"confirm": "Ocultar columna"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"inline_row_actions": {
|
||||||
|
"delete_confirmation": {
|
||||||
|
"title": "Eliminar fila",
|
||||||
|
"description": "¿Seguro que quieres eliminar esta fila? Esta acción no se puede deshacer.",
|
||||||
|
"confirm": "Eliminar fila"
|
||||||
|
}
|
||||||
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"draft": "Borrador",
|
"draft": "Borrador",
|
||||||
"ready": "Preparado",
|
"ready": "Preparado",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user