diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json
index 8a606707..d0bb5f87 100644
--- a/packages/cli/tsconfig.json
+++ b/packages/cli/tsconfig.json
@@ -10,6 +10,6 @@
"module": "ESNext",
"skipLibCheck": true
},
- "include": ["/**/*.ts"],
+ "include": ["**/*.ts"],
"exclude": ["dist", "node_modules"]
}
diff --git a/packages/next-admin/src/components/Cell.test.tsx b/packages/next-admin/src/components/Cell.test.tsx
new file mode 100644
index 00000000..44725095
--- /dev/null
+++ b/packages/next-admin/src/components/Cell.test.tsx
@@ -0,0 +1,277 @@
+import { describe, it, expect, vi } from "vitest";
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import Cell from "./Cell";
+import { ConfigProvider } from "../context/ConfigContext";
+import { ListDataFieldValue } from "../types";
+
+const mockConfigValue = {
+ basePath: "/admin",
+ apiBasePath: "/api/admin",
+ isAppDir: true,
+ options: {},
+ nextAdminContext: { locale: "en" },
+};
+
+const CellWrapper = ({ children }: { children: React.ReactNode }) => (
+ {children}
+);
+
+describe("Cell component", () => {
+ describe("boolean values", () => {
+ it("should render default boolean formatting for true", () => {
+ const cell: ListDataFieldValue = {
+ type: "scalar",
+ value: true,
+ __nextadmin_formatted: "true",
+ };
+
+ const { container } = render(
+
+ |
+
+ );
+
+ expect(container.textContent).toBe("true");
+ });
+
+ it("should render default boolean formatting for false", () => {
+ const cell: ListDataFieldValue = {
+ type: "scalar",
+ value: false,
+ __nextadmin_formatted: "false",
+ };
+
+ const { container } = render(
+
+ |
+
+ );
+
+ expect(container.textContent).toBe("false");
+ });
+
+ it("should render string formatter result for boolean", () => {
+ const cell: ListDataFieldValue = {
+ type: "scalar",
+ value: true,
+ __nextadmin_formatted: "Yes",
+ };
+
+ const { container } = render(
+
+ |
+
+ );
+
+ expect(container.textContent).toBe("Yes");
+ });
+
+ it("should render ReactNode formatter result for boolean", () => {
+ const cell: ListDataFieldValue = {
+ type: "scalar",
+ value: true,
+ __nextadmin_formatted:
Custom
,
+ };
+
+ render(
+
+ |
+
+ );
+
+ const element = screen.getByTestId("custom-format");
+ expect(element).toBeDefined();
+ expect(element.textContent).toBe("Custom");
+ });
+
+ it("should render complex ReactNode formatter for boolean", () => {
+ const cell: ListDataFieldValue = {
+ type: "scalar",
+ value: false,
+ __nextadmin_formatted: (
+ Unpublished
+ ),
+ };
+
+ render(
+
+ |
+
+ );
+
+ const element = screen.getByTestId("bool-strong");
+ expect(element).toBeDefined();
+ expect(element.tagName).toBe("STRONG");
+ expect(element.textContent).toBe("Unpublished");
+ });
+
+ it("should call formatter function for boolean and render ReactNode result", () => {
+ const cell: ListDataFieldValue = {
+ type: "scalar",
+ value: true,
+ __nextadmin_formatted: "true",
+ };
+
+ const formatter = vi.fn((value: boolean) => (
+ Published
+ ));
+
+ render(
+
+ true}
+ />
+ |
+ );
+
+ expect(formatter).toHaveBeenCalled();
+ const element = screen.getByTestId("formatted-bool");
+ expect(element).toBeDefined();
+ expect(element.textContent).toBe("Published");
+ });
+
+ it("should call formatter function for boolean false and render result", () => {
+ const cell: ListDataFieldValue = {
+ type: "scalar",
+ value: false,
+ __nextadmin_formatted: "false",
+ };
+
+ const formatter = vi.fn((value: boolean) => (
+ Not Published
+ ));
+
+ render(
+
+ false}
+ />
+ |
+ );
+
+ expect(formatter).toHaveBeenCalled();
+ const element = screen.getByTestId("unpublished");
+ expect(element).toBeDefined();
+ expect(element.textContent).toBe("Not Published");
+ });
+
+ it("should call formatter function that returns string for boolean", () => {
+ const cell: ListDataFieldValue = {
+ type: "scalar",
+ value: true,
+ __nextadmin_formatted: "true",
+ };
+
+ const formatter = vi.fn((value: boolean) => (value ? "Yes" : "No"));
+
+ const { container } = render(
+
+ true}
+ />
+ |
+ );
+
+ expect(formatter).toHaveBeenCalled();
+ expect(container.textContent).toBe("Yes");
+ });
+ });
+
+ describe("other scalar types", () => {
+ it("should render string values", () => {
+ const cell: ListDataFieldValue = {
+ type: "scalar",
+ value: "test string",
+ __nextadmin_formatted: "test string",
+ };
+
+ const { container } = render(
+
+ |
+
+ );
+
+ expect(container.textContent).toBe("test string");
+ });
+
+ it("should render number values", () => {
+ const cell: ListDataFieldValue = {
+ type: "scalar",
+ value: 42,
+ __nextadmin_formatted: "42",
+ };
+
+ const { container } = render(
+
+ |
+
+ );
+
+ expect(container.textContent).toBe("42");
+ });
+ });
+
+ describe("count type", () => {
+ it("should render count values", () => {
+ const cell: ListDataFieldValue = {
+ type: "count",
+ value: 5,
+ __nextadmin_formatted: "5",
+ };
+
+ const { container } = render(
+
+ |
+
+ );
+
+ expect(container.textContent).toBe("5");
+ });
+ });
+
+ describe("date type", () => {
+ it("should render date values", () => {
+ const date = new Date("2024-01-01");
+ const cell: ListDataFieldValue = {
+ type: "date",
+ value: date,
+ __nextadmin_formatted: "2024-01-01",
+ };
+
+ const { container } = render(
+
+ |
+
+ );
+
+ expect(container.textContent).toContain("2024-01-01");
+ });
+ });
+
+ describe("ReactElement at top level", () => {
+ it("should render ReactElement directly when provided as __nextadmin_formatted", () => {
+ const cell: ListDataFieldValue = {
+ type: "scalar",
+ value: "test",
+ __nextadmin_formatted: Top Level,
+ };
+
+ render(
+
+ |
+
+ );
+
+ const element = screen.getByTestId("top-level");
+ expect(element).toBeDefined();
+ expect(element.textContent).toBe("Top Level");
+ });
+ });
+});
diff --git a/packages/next-admin/src/components/Cell.tsx b/packages/next-admin/src/components/Cell.tsx
index b2dc0c1f..e641356b 100644
--- a/packages/next-admin/src/components/Cell.tsx
+++ b/packages/next-admin/src/components/Cell.tsx
@@ -89,6 +89,10 @@ export default function Cell({ cell, formatter, copyable, getRawData }: Props) {
);
} else if (cell.type === "scalar" && typeof cell.value === "boolean") {
+ // Check if cellValue is a React element (from a custom formatter)
+ if (React.isValidElement(cellValue)) {
+ return renderCustomElement(cellValue);
+ }
return (