@carlonicora/nextjs-jsonapi 3.20.1 → 3.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BlockNoteEditor-FKYWM4NJ.mjs → BlockNoteEditor-5QBGVWE7.mjs} +2 -2
- package/dist/{BlockNoteEditor-2M2M7MT4.js → BlockNoteEditor-VEEMWB2J.js} +9 -9
- package/dist/{BlockNoteEditor-2M2M7MT4.js.map → BlockNoteEditor-VEEMWB2J.js.map} +1 -1
- package/dist/billing/index.js +335 -335
- package/dist/billing/index.mjs +1 -1
- package/dist/{chunk-HP3SPOPP.mjs → chunk-5G7I3OUB.mjs} +42 -21
- package/dist/chunk-5G7I3OUB.mjs.map +1 -0
- package/dist/{chunk-XMYHR2EF.js → chunk-XYSWOW5G.js} +142 -121
- package/dist/chunk-XYSWOW5G.js.map +1 -0
- package/dist/client/index.js +2 -2
- package/dist/client/index.mjs +1 -1
- package/dist/components/index.d.mts +10 -2
- package/dist/components/index.d.ts +10 -2
- package/dist/components/index.js +4 -2
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +3 -1
- package/dist/contexts/index.js +2 -2
- package/dist/contexts/index.mjs +1 -1
- package/dist/features/help/index.js +31 -31
- package/dist/features/help/index.mjs +1 -1
- package/dist/features/tokenusage/index.js +75 -75
- package/dist/features/tokenusage/index.mjs +1 -1
- package/package.json +1 -1
- package/src/components/containers/RoundPageContainer.tsx +6 -2
- package/src/components/containers/__tests__/RoundPageContainer.padding.spec.tsx +64 -0
- package/src/components/navigations/Header.tsx +3 -2
- package/src/components/navigations/__tests__/Header.spec.tsx +60 -0
- package/src/components/tables/ContentListTable.tsx +56 -26
- package/src/components/tables/__tests__/ContentListTable.spec.tsx +133 -10
- package/src/shadcnui/ui/badge.tsx +2 -0
- package/src/shadcnui/ui/sidebar.tsx +6 -0
- package/dist/chunk-HP3SPOPP.mjs.map +0 -1
- package/dist/chunk-XMYHR2EF.js.map +0 -1
- /package/dist/{BlockNoteEditor-FKYWM4NJ.mjs.map → BlockNoteEditor-5QBGVWE7.mjs.map} +0 -0
|
@@ -24,6 +24,7 @@ import { MicroLabel } from "@/components/typography";
|
|
|
24
24
|
import { useHeaderChildren, useHeaderLeftContent, useHeaderLogo, useHeaderMobileChildren } from "@/contexts";
|
|
25
25
|
import { useUrlRewriter } from "@/hooks";
|
|
26
26
|
import { cn, useIsMobile } from "@/index";
|
|
27
|
+
import { useOptionalSidebar } from "@/shadcnui";
|
|
27
28
|
import { ModuleWithPermissions } from "@/permissions";
|
|
28
29
|
import { useSearchParams } from "next/navigation";
|
|
29
30
|
import { Fragment, ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
|
@@ -186,6 +187,9 @@ export function RoundPageContainer({
|
|
|
186
187
|
const headerMobileChildren = useHeaderMobileChildren();
|
|
187
188
|
const [showDetails, setShowDetailsState] = useState(defaultDetailsOpen);
|
|
188
189
|
const isMobile = useIsMobile();
|
|
190
|
+
// The desktop shell drops its start padding only where a sidebar sits against it. A page
|
|
191
|
+
// without one (the customer portal) keeps the padding on both sides.
|
|
192
|
+
const hasSidebar = useOptionalSidebar() !== null;
|
|
189
193
|
const [mounted, setMounted] = useState(false);
|
|
190
194
|
|
|
191
195
|
useEffect(() => {
|
|
@@ -323,7 +327,7 @@ export function RoundPageContainer({
|
|
|
323
327
|
// render 78px tall; below the bar it became a visible gap. The
|
|
324
328
|
// swipe-up gesture region is unaffected either way — it is a gesture
|
|
325
329
|
// area, not painted chrome.
|
|
326
|
-
isMobile ? "gap-1 p-1 pt-0" : "p-2 pt-0 ps-0",
|
|
330
|
+
isMobile ? "gap-1 p-1 pt-0" : cn("p-2 pt-0", hasSidebar && "ps-0"),
|
|
327
331
|
)}
|
|
328
332
|
>
|
|
329
333
|
{/* `min-h-0 flex-1`, NOT `h-full`: the bar below is an in-flow sibling in
|
|
@@ -367,7 +371,7 @@ export function RoundPageContainer({
|
|
|
367
371
|
// render 78px tall; below the bar it became a visible gap. The
|
|
368
372
|
// swipe-up gesture region is unaffected either way — it is a gesture
|
|
369
373
|
// area, not painted chrome.
|
|
370
|
-
isMobile ? "gap-1 p-1 pt-0" : "p-2 pt-0 ps-0",
|
|
374
|
+
isMobile ? "gap-1 p-1 pt-0" : cn("p-2 pt-0", hasSidebar && "ps-0"),
|
|
371
375
|
)}
|
|
372
376
|
>
|
|
373
377
|
{/* `min-h-0 flex-1`, NOT `h-full`: MobileNavigationBar below is an in-flow
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { beforeAll, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { SharedProvider } from "../../../contexts/SharedContext";
|
|
4
|
+
import { configureI18n } from "../../../i18n";
|
|
5
|
+
import { SidebarProvider } from "../../../shadcnui";
|
|
6
|
+
import { RoundPageContainer } from "../../containers/RoundPageContainer";
|
|
7
|
+
|
|
8
|
+
vi.mock("next/navigation", () => ({
|
|
9
|
+
usePathname: () => "/",
|
|
10
|
+
useRouter: () => ({
|
|
11
|
+
push: vi.fn(),
|
|
12
|
+
replace: vi.fn(),
|
|
13
|
+
prefetch: vi.fn(),
|
|
14
|
+
back: vi.fn(),
|
|
15
|
+
forward: vi.fn(),
|
|
16
|
+
refresh: vi.fn(),
|
|
17
|
+
}),
|
|
18
|
+
useSearchParams: () => new URLSearchParams(),
|
|
19
|
+
useParams: () => ({}),
|
|
20
|
+
redirect: vi.fn(),
|
|
21
|
+
notFound: vi.fn(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
beforeAll(() => {
|
|
25
|
+
// The package Link resolves its inner component at runtime and throws if i18n
|
|
26
|
+
// was never configured (src/i18n/config.ts).
|
|
27
|
+
configureI18n({
|
|
28
|
+
useRouter: () => ({
|
|
29
|
+
push: vi.fn(),
|
|
30
|
+
replace: vi.fn(),
|
|
31
|
+
back: vi.fn(),
|
|
32
|
+
forward: vi.fn(),
|
|
33
|
+
refresh: vi.fn(),
|
|
34
|
+
prefetch: vi.fn(),
|
|
35
|
+
}),
|
|
36
|
+
useTranslations: () => (key: string) => key,
|
|
37
|
+
usePathname: () => "/",
|
|
38
|
+
Link: ({ href, children, ...rest }: { href: string; children: React.ReactNode; [key: string]: any }) => (
|
|
39
|
+
<a href={href} {...rest}>
|
|
40
|
+
{children}
|
|
41
|
+
</a>
|
|
42
|
+
),
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function withShared(node: React.ReactNode) {
|
|
47
|
+
return <SharedProvider value={{ breadcrumbs: [], title: { type: "" } }}>{node}</SharedProvider>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("RoundPageContainer shell padding", () => {
|
|
51
|
+
it("drops the start padding next to a sidebar", async () => {
|
|
52
|
+
render(
|
|
53
|
+
withShared(<SidebarProvider>{<RoundPageContainer testId="shell">content</RoundPageContainer>}</SidebarProvider>),
|
|
54
|
+
);
|
|
55
|
+
expect((await screen.findByTestId("shell")).classList.contains("ps-0")).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("keeps the start padding when the page has no sidebar", async () => {
|
|
59
|
+
render(withShared(<RoundPageContainer testId="shell">content</RoundPageContainer>));
|
|
60
|
+
const shell = await screen.findByTestId("shell");
|
|
61
|
+
expect(shell.classList.contains("p-2")).toBe(true);
|
|
62
|
+
expect(shell.classList.contains("ps-0")).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -4,7 +4,7 @@ import { useIsMobile } from "@/utils";
|
|
|
4
4
|
import { useIsBreadcrumbRootHidden } from "../../contexts/BreadcrumbRootContext";
|
|
5
5
|
import { useHeaderRootLabel } from "../../contexts/HeaderChildrenContext";
|
|
6
6
|
import { useSharedContext } from "../../contexts/SharedContext";
|
|
7
|
-
import { SidebarTrigger } from "../../shadcnui";
|
|
7
|
+
import { SidebarTrigger, useOptionalSidebar } from "../../shadcnui";
|
|
8
8
|
import { BreadcrumbNavigation } from "./Breadcrumb";
|
|
9
9
|
|
|
10
10
|
type HeaderProps = {
|
|
@@ -22,6 +22,7 @@ export function Header({ children, mobileChildren, leftContent, logo, className
|
|
|
22
22
|
const rootLabel = useHeaderRootLabel();
|
|
23
23
|
const isRootHidden = useIsBreadcrumbRootHidden();
|
|
24
24
|
const isMobile = useIsMobile();
|
|
25
|
+
const sidebar = useOptionalSidebar();
|
|
25
26
|
|
|
26
27
|
return (
|
|
27
28
|
// Outer element owns the safe-area inset; the inner row keeps h-12 so the
|
|
@@ -41,7 +42,7 @@ export function Header({ children, mobileChildren, leftContent, logo, className
|
|
|
41
42
|
>
|
|
42
43
|
<div className="bg-sidebar flex h-12 w-full flex-row items-center justify-between ps-2 pe-4">
|
|
43
44
|
{isMobile && logo && <div className="flex shrink-0 flex-row items-center pe-1">{logo}</div>}
|
|
44
|
-
<SidebarTrigger aria-label="Toggle sidebar" id="sidebar-trigger" />
|
|
45
|
+
{sidebar && <SidebarTrigger aria-label="Toggle sidebar" id="sidebar-trigger" />}
|
|
45
46
|
{leftContent}
|
|
46
47
|
<div className="flex w-full flex-row items-center justify-start">
|
|
47
48
|
<BreadcrumbNavigation items={breadcrumbs} rootLabel={rootLabel ?? undefined} showRoot={!isRootHidden} />
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { beforeAll, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { SharedProvider } from "../../../contexts/SharedContext";
|
|
4
|
+
import { configureI18n } from "../../../i18n";
|
|
5
|
+
import { SidebarProvider } from "../../../shadcnui";
|
|
6
|
+
import { Header } from "../Header";
|
|
7
|
+
|
|
8
|
+
vi.mock("next/navigation", () => ({
|
|
9
|
+
usePathname: () => "/",
|
|
10
|
+
useRouter: () => ({
|
|
11
|
+
push: vi.fn(),
|
|
12
|
+
replace: vi.fn(),
|
|
13
|
+
prefetch: vi.fn(),
|
|
14
|
+
back: vi.fn(),
|
|
15
|
+
forward: vi.fn(),
|
|
16
|
+
refresh: vi.fn(),
|
|
17
|
+
}),
|
|
18
|
+
useSearchParams: () => new URLSearchParams(),
|
|
19
|
+
useParams: () => ({}),
|
|
20
|
+
redirect: vi.fn(),
|
|
21
|
+
notFound: vi.fn(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
beforeAll(() => {
|
|
25
|
+
// The package Link resolves its inner component at runtime and throws if i18n
|
|
26
|
+
// was never configured (src/i18n/config.ts).
|
|
27
|
+
configureI18n({
|
|
28
|
+
useRouter: () => ({
|
|
29
|
+
push: vi.fn(),
|
|
30
|
+
replace: vi.fn(),
|
|
31
|
+
back: vi.fn(),
|
|
32
|
+
forward: vi.fn(),
|
|
33
|
+
refresh: vi.fn(),
|
|
34
|
+
prefetch: vi.fn(),
|
|
35
|
+
}),
|
|
36
|
+
useTranslations: () => (key: string) => key,
|
|
37
|
+
usePathname: () => "/",
|
|
38
|
+
Link: ({ href, children, ...rest }: { href: string; children: React.ReactNode; [key: string]: any }) => (
|
|
39
|
+
<a href={href} {...rest}>
|
|
40
|
+
{children}
|
|
41
|
+
</a>
|
|
42
|
+
),
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function withShared(node: React.ReactNode) {
|
|
47
|
+
return <SharedProvider value={{ breadcrumbs: [], title: { type: "" } }}>{node}</SharedProvider>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("Header", () => {
|
|
51
|
+
it("renders the sidebar toggle inside a SidebarProvider", () => {
|
|
52
|
+
render(withShared(<SidebarProvider>{<Header />}</SidebarProvider>));
|
|
53
|
+
expect(screen.queryByRole("button", { name: "Toggle sidebar" })).not.toBeNull();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("renders without a SidebarProvider and shows no sidebar toggle", () => {
|
|
57
|
+
expect(() => render(withShared(<Header />))).not.toThrow();
|
|
58
|
+
expect(screen.queryByRole("button", { name: "Toggle sidebar" })).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -69,6 +69,12 @@ type ContentListTableProps = {
|
|
|
69
69
|
*/
|
|
70
70
|
hidePagination?: boolean;
|
|
71
71
|
onRowClick?: (rowData: any) => void;
|
|
72
|
+
/**
|
|
73
|
+
* Renders a full-width detail row under an expanded row. Receives the row's
|
|
74
|
+
* `jsonApiData`. Setting it makes every row expandable; the row's own cell
|
|
75
|
+
* toggles it via `row.getToggleExpandedHandler()`.
|
|
76
|
+
*/
|
|
77
|
+
renderExpandedRow?: (rowData: any) => ReactNode;
|
|
72
78
|
};
|
|
73
79
|
|
|
74
80
|
type CellRenderer = (context: unknown) => ReactNode;
|
|
@@ -140,6 +146,14 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
|
|
|
140
146
|
data: tableData,
|
|
141
147
|
columns: tableColumns,
|
|
142
148
|
getCoreRowModel: getCoreRowModel(),
|
|
149
|
+
...(props.renderExpandedRow && {
|
|
150
|
+
getExpandedRowModel: getExpandedRowModel(),
|
|
151
|
+
// Key rows by record id so an expansion stays with its record when a search or filter reorders the rows.
|
|
152
|
+
getRowId: (row: TableContent<any>, index: number) => row.jsonApiData?.id ?? String(index),
|
|
153
|
+
getRowCanExpand: () => true,
|
|
154
|
+
onExpandedChange: setExpanded,
|
|
155
|
+
state: { expanded },
|
|
156
|
+
}),
|
|
143
157
|
...(props.expandable && {
|
|
144
158
|
getExpandedRowModel: getExpandedRowModel(),
|
|
145
159
|
getSubRows: props.getSubRows,
|
|
@@ -272,39 +286,55 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
|
|
|
272
286
|
</TableCell>
|
|
273
287
|
</TableRow>
|
|
274
288
|
{group.rows.map((row) => (
|
|
275
|
-
<
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
289
|
+
<React.Fragment key={row.id}>
|
|
290
|
+
<TableRow
|
|
291
|
+
onClick={() => onRowClick?.(row.original.jsonApiData)}
|
|
292
|
+
className={`group ${onRowClick ? "hover:bg-muted/50 cursor-pointer" : ""}`}
|
|
293
|
+
>
|
|
294
|
+
{row.getVisibleCells().map((cell) => {
|
|
295
|
+
const meta = cell.column.columnDef.meta as { className?: string } | undefined;
|
|
296
|
+
return (
|
|
297
|
+
<TableCell key={cell.id} className={meta?.className}>
|
|
298
|
+
{renderCell(cell.column.columnDef.cell, cell.getContext())}
|
|
299
|
+
</TableCell>
|
|
300
|
+
);
|
|
301
|
+
})}
|
|
302
|
+
</TableRow>
|
|
303
|
+
{props.renderExpandedRow && row.getIsExpanded() && (
|
|
304
|
+
<TableRow data-testid="content-list-table-expanded-row">
|
|
305
|
+
<TableCell colSpan={tableColumns.length} className="bg-muted/30 px-4 py-3">
|
|
306
|
+
{props.renderExpandedRow(row.original.jsonApiData)}
|
|
285
307
|
</TableCell>
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
</
|
|
308
|
+
</TableRow>
|
|
309
|
+
)}
|
|
310
|
+
</React.Fragment>
|
|
289
311
|
))}
|
|
290
312
|
</React.Fragment>
|
|
291
313
|
))
|
|
292
314
|
) : (
|
|
293
315
|
rowModel.rows.map((row) => (
|
|
294
|
-
<
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
316
|
+
<React.Fragment key={row.id}>
|
|
317
|
+
<TableRow
|
|
318
|
+
onClick={() => onRowClick?.(row.original.jsonApiData)}
|
|
319
|
+
className={`group ${onRowClick ? "hover:bg-muted/50 cursor-pointer" : ""}`}
|
|
320
|
+
>
|
|
321
|
+
{row.getVisibleCells().map((cell) => {
|
|
322
|
+
const meta = cell.column.columnDef.meta as { className?: string } | undefined;
|
|
323
|
+
return (
|
|
324
|
+
<TableCell key={cell.id} className={meta?.className}>
|
|
325
|
+
{renderCell(cell.column.columnDef.cell, cell.getContext())}
|
|
326
|
+
</TableCell>
|
|
327
|
+
);
|
|
328
|
+
})}
|
|
329
|
+
</TableRow>
|
|
330
|
+
{props.renderExpandedRow && row.getIsExpanded() && (
|
|
331
|
+
<TableRow data-testid="content-list-table-expanded-row">
|
|
332
|
+
<TableCell colSpan={tableColumns.length} className="bg-muted/30 px-4 py-3">
|
|
333
|
+
{props.renderExpandedRow(row.original.jsonApiData)}
|
|
304
334
|
</TableCell>
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
</
|
|
335
|
+
</TableRow>
|
|
336
|
+
)}
|
|
337
|
+
</React.Fragment>
|
|
308
338
|
))
|
|
309
339
|
)
|
|
310
340
|
) : (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
2
|
-
import { describe, expect, it, vi } from "vitest";
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { ContentListTable } from "../ContentListTable";
|
|
4
4
|
|
|
5
5
|
// `ContentListTable` imports `useTableGenerator` from the `../../hooks` barrel,
|
|
@@ -7,20 +7,40 @@ import { ContentListTable } from "../ContentListTable";
|
|
|
7
7
|
// barrel (spreading the actual module so the other exports survive) is the same
|
|
8
8
|
// approach the sibling `ContentListTable.test.tsx` in this folder uses; one
|
|
9
9
|
// plain column is enough here.
|
|
10
|
+
// `mockColumns.expandToggle` switches the mocked name cell to a button that
|
|
11
|
+
// toggles row expansion (the `renderExpandedRow` suite), plus a second column so
|
|
12
|
+
// the detail row's colSpan is checked against more than one column.
|
|
13
|
+
const mockColumns = vi.hoisted(() => ({ expandToggle: false }));
|
|
14
|
+
|
|
10
15
|
vi.mock("../../../hooks", async () => {
|
|
11
16
|
const actual = await vi.importActual("../../../hooks");
|
|
12
17
|
return {
|
|
13
18
|
...actual,
|
|
14
19
|
useTableGenerator: vi.fn((_type: any, params: any) => ({
|
|
15
20
|
data: params.data.map((item: any) => ({ ...item, jsonApiData: item })),
|
|
16
|
-
columns:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
columns: mockColumns.expandToggle
|
|
22
|
+
? [
|
|
23
|
+
{
|
|
24
|
+
id: "name",
|
|
25
|
+
header: "Name",
|
|
26
|
+
accessorKey: "name",
|
|
27
|
+
cell: ({ row }: any) => <button onClick={row.getToggleExpandedHandler()}>{row.original.name}</button>,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "id",
|
|
31
|
+
header: "Id",
|
|
32
|
+
accessorKey: "id",
|
|
33
|
+
cell: ({ row }: any) => row.original.id,
|
|
34
|
+
},
|
|
35
|
+
]
|
|
36
|
+
: [
|
|
37
|
+
{
|
|
38
|
+
id: "name",
|
|
39
|
+
header: "Name",
|
|
40
|
+
accessorKey: "name",
|
|
41
|
+
cell: ({ row }: any) => row.original.name,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
24
44
|
})),
|
|
25
45
|
};
|
|
26
46
|
});
|
|
@@ -60,3 +80,106 @@ describe("ContentListTable pagination footer", () => {
|
|
|
60
80
|
expect(screen.getByText("Row one")).toBeInTheDocument();
|
|
61
81
|
});
|
|
62
82
|
});
|
|
83
|
+
|
|
84
|
+
describe("ContentListTable renderExpandedRow", () => {
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
mockColumns.expandToggle = true;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
mockColumns.expandToggle = false;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// The callback receives `row.original.jsonApiData`; the mocked generator sets
|
|
94
|
+
// that to the raw data item, so `d` is `{ id, name }`.
|
|
95
|
+
const renderExpandedRow = (d: any) => `Detail for ${d.name}`;
|
|
96
|
+
|
|
97
|
+
it("renders no detail row until a row is expanded", () => {
|
|
98
|
+
render(
|
|
99
|
+
<ContentListTable
|
|
100
|
+
data={baseData}
|
|
101
|
+
fields={["name", "id"]}
|
|
102
|
+
tableGeneratorType={mockModule}
|
|
103
|
+
renderExpandedRow={renderExpandedRow}
|
|
104
|
+
/>,
|
|
105
|
+
);
|
|
106
|
+
expect(screen.queryByTestId("content-list-table-expanded-row")).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("renders the callback output in a full-width row after toggling", () => {
|
|
110
|
+
render(
|
|
111
|
+
<ContentListTable
|
|
112
|
+
data={baseData}
|
|
113
|
+
fields={["name", "id"]}
|
|
114
|
+
tableGeneratorType={mockModule}
|
|
115
|
+
renderExpandedRow={renderExpandedRow}
|
|
116
|
+
/>,
|
|
117
|
+
);
|
|
118
|
+
fireEvent.click(screen.getByText("Row one"));
|
|
119
|
+
const detailRow = screen.getByTestId("content-list-table-expanded-row");
|
|
120
|
+
expect(detailRow).toHaveTextContent("Detail for Row one");
|
|
121
|
+
const cells = detailRow.querySelectorAll("td");
|
|
122
|
+
expect(cells).toHaveLength(1);
|
|
123
|
+
expect(cells[0].getAttribute("colspan")).toBe("2");
|
|
124
|
+
|
|
125
|
+
fireEvent.click(screen.getByText("Row one"));
|
|
126
|
+
expect(screen.queryByTestId("content-list-table-expanded-row")).toBeNull();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("keeps the expansion on the record, not on the row position, when the rows change", () => {
|
|
130
|
+
const twoRows = {
|
|
131
|
+
...baseData,
|
|
132
|
+
data: [
|
|
133
|
+
{ id: "1", name: "Row one" },
|
|
134
|
+
{ id: "2", name: "Row two" },
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
const { rerender } = render(
|
|
138
|
+
<ContentListTable
|
|
139
|
+
data={twoRows}
|
|
140
|
+
fields={["name", "id"]}
|
|
141
|
+
tableGeneratorType={mockModule}
|
|
142
|
+
renderExpandedRow={renderExpandedRow}
|
|
143
|
+
/>,
|
|
144
|
+
);
|
|
145
|
+
fireEvent.click(screen.getByText("Row one"));
|
|
146
|
+
expect(screen.getByTestId("content-list-table-expanded-row")).toHaveTextContent("Detail for Row one");
|
|
147
|
+
|
|
148
|
+
// A search narrows the list: "Row two" now sits where "Row one" was.
|
|
149
|
+
rerender(
|
|
150
|
+
<ContentListTable
|
|
151
|
+
data={{ ...baseData, data: [{ id: "2", name: "Row two" }] }}
|
|
152
|
+
fields={["name", "id"]}
|
|
153
|
+
tableGeneratorType={mockModule}
|
|
154
|
+
renderExpandedRow={renderExpandedRow}
|
|
155
|
+
/>,
|
|
156
|
+
);
|
|
157
|
+
expect(screen.queryByTestId("content-list-table-expanded-row")).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("rows cannot expand without the prop", () => {
|
|
161
|
+
render(<ContentListTable data={baseData} fields={["name", "id"]} tableGeneratorType={mockModule} />);
|
|
162
|
+
fireEvent.click(screen.getByText("Row one"));
|
|
163
|
+
expect(screen.queryByTestId("content-list-table-expanded-row")).toBeNull();
|
|
164
|
+
expect(screen.queryByText("Detail for Row one")).toBeNull();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("grouped rows also expand", () => {
|
|
168
|
+
render(
|
|
169
|
+
<ContentListTable
|
|
170
|
+
data={baseData}
|
|
171
|
+
fields={["name", "id"]}
|
|
172
|
+
tableGeneratorType={mockModule}
|
|
173
|
+
groupBy="name"
|
|
174
|
+
renderExpandedRow={renderExpandedRow}
|
|
175
|
+
/>,
|
|
176
|
+
);
|
|
177
|
+
fireEvent.click(screen.getByRole("button", { name: "Row one" }));
|
|
178
|
+
const detailRow = screen.getByTestId("content-list-table-expanded-row");
|
|
179
|
+
expect(detailRow).toHaveTextContent("Detail for Row one");
|
|
180
|
+
// The detail row sits directly under the grouped data row, after the group header.
|
|
181
|
+
const dataRow = screen.getByRole("button", { name: "Row one" }).closest("tr")!;
|
|
182
|
+
expect(dataRow.nextElementSibling).toBe(detailRow);
|
|
183
|
+
expect(dataRow.previousElementSibling).toHaveTextContent("Row one");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -25,6 +25,8 @@ const badgeVariants = cva(
|
|
|
25
25
|
softGray: "bg-gray-500/10 text-gray-700 dark:bg-gray-500/15 dark:text-gray-400",
|
|
26
26
|
softOrange: "bg-orange-500/10 text-orange-700 dark:bg-orange-500/15 dark:text-orange-400",
|
|
27
27
|
softAmber: "bg-amber-500/10 text-amber-700 dark:bg-amber-500/15 dark:text-amber-400",
|
|
28
|
+
softEmerald: "bg-emerald-500/10 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-400",
|
|
29
|
+
softPurple: "bg-purple-500/10 text-purple-700 dark:bg-purple-500/15 dark:text-purple-400",
|
|
28
30
|
blue: "bg-sky-500 text-primary-foreground [a]:hover:bg-sky-500/80",
|
|
29
31
|
green: "bg-emerald-500 text-primary-foreground [a]:hover:bg-emerald-500/80",
|
|
30
32
|
red: "bg-red-500 text-primary-foreground [a]:hover:bg-red-500/80",
|
|
@@ -45,6 +45,11 @@ function useSidebar() {
|
|
|
45
45
|
return context;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** Like `useSidebar`, but returns null outside a `SidebarProvider` instead of throwing. */
|
|
49
|
+
function useOptionalSidebar(): SidebarContextProps | null {
|
|
50
|
+
return React.useContext(SidebarContext);
|
|
51
|
+
}
|
|
52
|
+
|
|
48
53
|
function SidebarProvider({
|
|
49
54
|
defaultOpen = true,
|
|
50
55
|
open: openProp,
|
|
@@ -688,5 +693,6 @@ export {
|
|
|
688
693
|
SidebarRail,
|
|
689
694
|
SidebarSeparator,
|
|
690
695
|
SidebarTrigger,
|
|
696
|
+
useOptionalSidebar,
|
|
691
697
|
useSidebar,
|
|
692
698
|
};
|