@marimo-team/islands 0.25.3-dev162 → 0.25.3-dev168
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/{ErrorBoundary-D6GPQ8dk.js → ErrorBoundary-Db2eWg1e.js} +1 -1
- package/dist/{code-visibility-DEIiTBMH.js → code-visibility-BfRdYY7f.js} +1 -1
- package/dist/{common-BmlT-bPP.js → common-Ceqqp1YL.js} +138 -139
- package/dist/{data-grid-overlay-editor-20Ge8ID1.js → data-grid-overlay-editor-klwtd2IQ.js} +1 -1
- package/dist/{feedback-button-zWcCfXEI.js → feedback-button-BZb6atqx.js} +2 -2
- package/dist/{glide-data-editor-Dn1Ekucb.js → glide-data-editor-BOcECwyE.js} +3 -3
- package/dist/{globals-D_fhTQMd.js → globals-BxQbqY16.js} +1 -1
- package/dist/main.js +1983 -1957
- package/dist/{number-overlay-editor-ELtg9k3M.js → number-overlay-editor-DaPfxMR3.js} +1 -1
- package/dist/{reveal-component-CduO3VgT.js → reveal-component-RQYxUMYN.js} +2 -2
- package/package.json +1 -1
- package/src/components/data-table/TableBottomBar.tsx +4 -1
- package/src/components/data-table/__tests__/pagination.test.tsx +28 -1
- package/src/components/data-table/data-table.tsx +4 -0
- package/src/components/data-table/pagination.tsx +44 -33
- package/src/plugins/impl/DataTablePlugin.tsx +40 -4
- package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +270 -0
- package/src/stories/data-table.stories.tsx +31 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
|
|
2
2
|
import { t } from "./react-sHtZdjeP.js";
|
|
3
|
-
import { a as n } from "./glide-data-editor-
|
|
3
|
+
import { a as n } from "./glide-data-editor-BOcECwyE.js";
|
|
4
4
|
//#region ../node_modules/.pnpm/@glideapps+glide-data-grid@6.0.4-alpha24_lodash@4.17.21_marked@15.0.12_react-dom@19.3.0_27cffe7f1bc30287d59ac27db3240c5b/node_modules/@glideapps/glide-data-grid/dist/esm/internal/data-grid-overlay-editor/private/number-overlay-editor-style.js
|
|
5
5
|
var r = /* @__PURE__ */ e(t()), i = /*#__PURE__*/ n("div")({
|
|
6
6
|
name: "NumberOverlayEditorStyle",
|
|
@@ -4,14 +4,14 @@ import { t as o } from "./react-sHtZdjeP.js";
|
|
|
4
4
|
import { t as s } from "./compiler-runtime-CldRdSsv.js";
|
|
5
5
|
import { m as c, t as l } from "./tooltip-CJ4Rqasp.js";
|
|
6
6
|
import { k as u } from "./error-entries-BpUJSLUZ.js";
|
|
7
|
-
import { $ as d, Q as f, Z as p, an as m, i as h, l as g, ln as _, n as v, on as y, r as b, t as x, u as S } from "./common-
|
|
7
|
+
import { $ as d, Q as f, Z as p, an as m, i as h, l as g, ln as _, n as v, on as y, r as b, t as x, u as S } from "./common-Ceqqp1YL.js";
|
|
8
8
|
import { M as C } from "./select-CkF_GBxO.js";
|
|
9
9
|
import { J as ee } from "./dropdown-menu-BWnnAstC.js";
|
|
10
10
|
import { _ as w } from "./toDate-rySyYxqk.js";
|
|
11
11
|
import { t as T } from "./jsx-runtime-3BD1kT0s.js";
|
|
12
12
|
import { q as E } from "./use-restore-focus-CmGwRNId.js";
|
|
13
13
|
import { A as D, C as O, b as k } from "./useTheme-TLkqJsvo.js";
|
|
14
|
-
import { a as A, n as te, t as ne } from "./code-visibility-
|
|
14
|
+
import { a as A, n as te, t as ne } from "./code-visibility-BfRdYY7f.js";
|
|
15
15
|
/* empty css */
|
|
16
16
|
var j = c("sticky-note", [["path", {
|
|
17
17
|
d: "M21 9a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 15 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2z",
|
package/package.json
CHANGED
|
@@ -17,6 +17,7 @@ import type { DataTableSelection } from "./types";
|
|
|
17
17
|
|
|
18
18
|
interface TableBottomBarProps<TData> {
|
|
19
19
|
pagination: boolean;
|
|
20
|
+
hasNextPage?: boolean;
|
|
20
21
|
totalColumns: number;
|
|
21
22
|
selection?: DataTableSelection;
|
|
22
23
|
onRowSelectionChange?: (value: RowSelectionState) => void;
|
|
@@ -30,6 +31,7 @@ interface TableBottomBarProps<TData> {
|
|
|
30
31
|
|
|
31
32
|
export const TableBottomBar = <TData,>({
|
|
32
33
|
pagination,
|
|
34
|
+
hasNextPage,
|
|
33
35
|
totalColumns,
|
|
34
36
|
selection,
|
|
35
37
|
onRowSelectionChange,
|
|
@@ -147,7 +149,7 @@ export const TableBottomBar = <TData,>({
|
|
|
147
149
|
const { totalColumns: effectiveTotalColumns, hiddenColumns } =
|
|
148
150
|
getColumnCountForDisplay(table, totalColumns);
|
|
149
151
|
const { rowsAndColumns, hiddenSuffix } = prettifyRowColumnCount({
|
|
150
|
-
numRows: table.getRowCount(),
|
|
152
|
+
numRows: hasNextPage === undefined ? table.getRowCount() : "too_many",
|
|
151
153
|
totalColumns: effectiveTotalColumns,
|
|
152
154
|
hiddenColumns,
|
|
153
155
|
locale,
|
|
@@ -178,6 +180,7 @@ export const TableBottomBar = <TData,>({
|
|
|
178
180
|
<DataTablePagination
|
|
179
181
|
table={table}
|
|
180
182
|
tableLoading={tableLoading}
|
|
183
|
+
hasNextPage={hasNextPage}
|
|
181
184
|
showPageSizeSelector={showPageSizeSelector}
|
|
182
185
|
/>
|
|
183
186
|
)}
|
|
@@ -9,9 +9,11 @@ import { DataTablePagination, matchingPageRanges } from "../pagination";
|
|
|
9
9
|
function PaginationHarness({
|
|
10
10
|
totalPages = 3,
|
|
11
11
|
loading = false,
|
|
12
|
+
hasNextPage,
|
|
12
13
|
}: {
|
|
13
14
|
totalPages?: number;
|
|
14
15
|
loading?: boolean;
|
|
16
|
+
hasNextPage?: boolean;
|
|
15
17
|
}) {
|
|
16
18
|
const table = useReactTable({
|
|
17
19
|
locale: "en-US",
|
|
@@ -23,7 +25,11 @@ function PaginationHarness({
|
|
|
23
25
|
});
|
|
24
26
|
return (
|
|
25
27
|
<TooltipProvider>
|
|
26
|
-
<DataTablePagination
|
|
28
|
+
<DataTablePagination
|
|
29
|
+
table={table}
|
|
30
|
+
tableLoading={loading}
|
|
31
|
+
hasNextPage={hasNextPage}
|
|
32
|
+
/>
|
|
27
33
|
</TooltipProvider>
|
|
28
34
|
);
|
|
29
35
|
}
|
|
@@ -140,3 +146,24 @@ test("prefix 1 with small totalPages", () => {
|
|
|
140
146
|
test("prefix 1 with totalPages=1", () => {
|
|
141
147
|
expect(matchingPageRanges("1", 1)).toEqual([[1, 1]]);
|
|
142
148
|
});
|
|
149
|
+
|
|
150
|
+
test("unknown totals use lookahead for navigation without a last page or page picker", () => {
|
|
151
|
+
const { rerender } = render(
|
|
152
|
+
<PaginationHarness totalPages={-1} hasNextPage={true} />,
|
|
153
|
+
);
|
|
154
|
+
expect(screen.getByText("1")).toBeInTheDocument();
|
|
155
|
+
expect(
|
|
156
|
+
screen.queryByRole("button", { name: "Last page" }),
|
|
157
|
+
).not.toBeInTheDocument();
|
|
158
|
+
expect(
|
|
159
|
+
screen.queryByRole("button", { name: /Choose page/ }),
|
|
160
|
+
).not.toBeInTheDocument();
|
|
161
|
+
expect(screen.getByRole("button", { name: "Previous page" })).toBeDisabled();
|
|
162
|
+
fireEvent.click(screen.getByRole("button", { name: "Next page" }));
|
|
163
|
+
expect(screen.getByText("2")).toBeInTheDocument();
|
|
164
|
+
rerender(<PaginationHarness totalPages={-1} hasNextPage={false} />);
|
|
165
|
+
expect(screen.getByRole("button", { name: "Next page" })).toBeDisabled();
|
|
166
|
+
fireEvent.click(screen.getByRole("button", { name: "Previous page" }));
|
|
167
|
+
expect(screen.getByText("1")).toBeInTheDocument();
|
|
168
|
+
expect(screen.getByRole("button", { name: "Previous page" })).toBeDisabled();
|
|
169
|
+
});
|
|
@@ -86,6 +86,7 @@ interface DataTableProps<TData> extends Partial<ExportActionProps> {
|
|
|
86
86
|
setSorting?: OnChangeFn<SortingState>; // controlled sorting
|
|
87
87
|
// Pagination
|
|
88
88
|
totalRows: number | TooManyRows;
|
|
89
|
+
hasNextPage?: boolean;
|
|
89
90
|
// JSON-serialized size of the currently-rendered data. Forwarded to
|
|
90
91
|
// ExportActions so hosts can size-gate the Export button via downloadSizeLimitAtom.
|
|
91
92
|
sizeBytes?: number | null;
|
|
@@ -143,6 +144,7 @@ const DataTableInternal = <TData,>({
|
|
|
143
144
|
selection,
|
|
144
145
|
totalColumns,
|
|
145
146
|
totalRows,
|
|
147
|
+
hasNextPage,
|
|
146
148
|
sizeBytes,
|
|
147
149
|
sizeBytesIsLoading,
|
|
148
150
|
manualSorting = false,
|
|
@@ -255,6 +257,7 @@ const DataTableInternal = <TData,>({
|
|
|
255
257
|
getCoreRowModel: getCoreRowModel(),
|
|
256
258
|
// pagination
|
|
257
259
|
rowCount: totalRows === "too_many" ? undefined : totalRows,
|
|
260
|
+
pageCount: hasNextPage === undefined ? undefined : -1,
|
|
258
261
|
...(setPaginationState
|
|
259
262
|
? {
|
|
260
263
|
onPaginationChange: setPaginationState,
|
|
@@ -432,6 +435,7 @@ const DataTableInternal = <TData,>({
|
|
|
432
435
|
className="pt-1.5 pb-0.5 border-t border-border"
|
|
433
436
|
totalColumns={totalColumns}
|
|
434
437
|
pagination={pagination}
|
|
438
|
+
hasNextPage={hasNextPage}
|
|
435
439
|
selection={selection}
|
|
436
440
|
onRowSelectionChange={onRowSelectionChange}
|
|
437
441
|
table={table}
|
|
@@ -41,21 +41,24 @@ import { PluralWord } from "@/utils/pluralize";
|
|
|
41
41
|
interface DataTablePaginationProps<TData> {
|
|
42
42
|
table: Table<TData>;
|
|
43
43
|
tableLoading?: boolean;
|
|
44
|
+
hasNextPage?: boolean;
|
|
44
45
|
showPageSizeSelector?: boolean;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
export const DataTablePagination = <TData,>({
|
|
48
49
|
table,
|
|
49
50
|
tableLoading,
|
|
51
|
+
hasNextPage,
|
|
50
52
|
showPageSizeSelector,
|
|
51
53
|
}: DataTablePaginationProps<TData>) => {
|
|
52
54
|
const { locale } = useLocale();
|
|
53
55
|
|
|
54
|
-
const currentPage = Math.min(
|
|
55
|
-
table.getState().pagination.pageIndex + 1,
|
|
56
|
-
table.getPageCount(),
|
|
57
|
-
);
|
|
58
56
|
const totalPages = table.getPageCount();
|
|
57
|
+
const hasKnownTotal = totalPages !== -1;
|
|
58
|
+
const pageNumber = table.getState().pagination.pageIndex + 1;
|
|
59
|
+
const currentPage = hasKnownTotal
|
|
60
|
+
? Math.min(pageNumber, totalPages)
|
|
61
|
+
: pageNumber;
|
|
59
62
|
|
|
60
63
|
const pageSize = table.getState().pagination.pageSize;
|
|
61
64
|
|
|
@@ -142,16 +145,22 @@ export const DataTablePagination = <TData,>({
|
|
|
142
145
|
</Tooltip>
|
|
143
146
|
<div className="flex items-center justify-center text-xs font-medium gap-1">
|
|
144
147
|
<span>Page</span>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
148
|
+
{hasKnownTotal ? (
|
|
149
|
+
<>
|
|
150
|
+
<PageSelector
|
|
151
|
+
currentPage={currentPage}
|
|
152
|
+
totalPages={totalPages}
|
|
153
|
+
onPageChange={(page) =>
|
|
154
|
+
handlePageChange(() => table.setPageIndex(page))
|
|
155
|
+
}
|
|
156
|
+
/>
|
|
157
|
+
<span className="shrink-0">
|
|
158
|
+
of {prettyNumber(totalPages, locale)}
|
|
159
|
+
</span>
|
|
160
|
+
</>
|
|
161
|
+
) : (
|
|
162
|
+
<span>{prettyNumber(currentPage, locale)}</span>
|
|
163
|
+
)}
|
|
155
164
|
</div>
|
|
156
165
|
<Tooltip content="Next page">
|
|
157
166
|
<Button
|
|
@@ -162,29 +171,31 @@ export const DataTablePagination = <TData,>({
|
|
|
162
171
|
className="h-6 w-5 p-0"
|
|
163
172
|
onClick={() => handlePageChange(() => table.nextPage())}
|
|
164
173
|
onMouseDown={Events.preventFocus}
|
|
165
|
-
disabled={!table.getCanNextPage()}
|
|
174
|
+
disabled={tableLoading || !(hasNextPage ?? table.getCanNextPage())}
|
|
166
175
|
>
|
|
167
176
|
<ChevronRight className="h-4 w-4" />
|
|
168
177
|
</Button>
|
|
169
178
|
</Tooltip>
|
|
170
|
-
|
|
171
|
-
<
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
179
|
+
{hasKnownTotal && (
|
|
180
|
+
<Tooltip content="Last page">
|
|
181
|
+
<Button
|
|
182
|
+
size="xs"
|
|
183
|
+
variant="text"
|
|
184
|
+
data-testid="last-page-button"
|
|
185
|
+
aria-label="Last page"
|
|
186
|
+
className="hidden h-6 w-5 p-0 lg:flex"
|
|
187
|
+
onClick={() =>
|
|
188
|
+
handlePageChange(() =>
|
|
189
|
+
table.setPageIndex(table.getPageCount() - 1),
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
onMouseDown={Events.preventFocus}
|
|
193
|
+
disabled={!table.getCanNextPage()}
|
|
194
|
+
>
|
|
195
|
+
<ChevronsRight className="h-4 w-4" />
|
|
196
|
+
</Button>
|
|
197
|
+
</Tooltip>
|
|
198
|
+
)}
|
|
188
199
|
</div>
|
|
189
200
|
</div>
|
|
190
201
|
);
|
|
@@ -231,6 +231,7 @@ type DataTableFunctions = {
|
|
|
231
231
|
}) => Promise<{
|
|
232
232
|
data: TableData<T>;
|
|
233
233
|
total_rows: number | TooManyRows;
|
|
234
|
+
has_next_page?: boolean | null;
|
|
234
235
|
cell_styles?: CellStyleState | null;
|
|
235
236
|
cell_hover_texts?: Record<string, Record<string, string | null>> | null;
|
|
236
237
|
raw_data?: TableData<T> | null;
|
|
@@ -343,6 +344,7 @@ export const DataTablePlugin = createPlugin<S>("marimo-table")
|
|
|
343
344
|
z.object({
|
|
344
345
|
data: z.union([z.string(), z.array(z.looseObject({}))]),
|
|
345
346
|
total_rows: z.union([z.number(), z.literal(TOO_MANY_ROWS)]),
|
|
347
|
+
has_next_page: z.boolean().nullish(),
|
|
346
348
|
cell_styles: z
|
|
347
349
|
.record(z.string(), z.record(z.string(), z.looseObject({})))
|
|
348
350
|
.nullable(),
|
|
@@ -507,6 +509,7 @@ interface DataTableSearchProps {
|
|
|
507
509
|
filters?: ColumnFiltersState;
|
|
508
510
|
setFilters?: SetFilters;
|
|
509
511
|
hasStableRowId: boolean;
|
|
512
|
+
hasNextPage?: boolean;
|
|
510
513
|
}
|
|
511
514
|
|
|
512
515
|
export const LoadingDataTableComponent = memo(
|
|
@@ -550,11 +553,35 @@ export const LoadingDataTableComponent = memo(
|
|
|
550
553
|
setPaginationState({ pageIndex: 0, pageSize: props.pageSize });
|
|
551
554
|
}
|
|
552
555
|
|
|
556
|
+
const resetLazyPage = useEvent(() => {
|
|
557
|
+
if (props.lazy) {
|
|
558
|
+
setPaginationState((state) =>
|
|
559
|
+
state.pageIndex === 0 ? state : { ...state, pageIndex: 0 },
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const handleSortingChange: OnChangeFn<SortingState> = useEvent(
|
|
565
|
+
(updater) => {
|
|
566
|
+
setSorting(updater);
|
|
567
|
+
resetLazyPage();
|
|
568
|
+
},
|
|
569
|
+
);
|
|
570
|
+
const handleFiltersChange: SetFilters = useEvent((updater) => {
|
|
571
|
+
setFilters(updater);
|
|
572
|
+
resetLazyPage();
|
|
573
|
+
});
|
|
574
|
+
const handleSearchQueryChange = useEvent((query: string) => {
|
|
575
|
+
setSearchQuery(query);
|
|
576
|
+
resetLazyPage();
|
|
577
|
+
});
|
|
578
|
+
|
|
553
579
|
// Data loading
|
|
554
580
|
const { data, error, isPending, isFetching } = useAsyncData<{
|
|
555
581
|
rows: T[];
|
|
556
582
|
rawRows?: T[];
|
|
557
583
|
totalRows: number | TooManyRows;
|
|
584
|
+
hasNextPage?: boolean;
|
|
558
585
|
cellStyles: CellStyleState | undefined | null;
|
|
559
586
|
cellHoverTexts?: Record<string, Record<string, string | null>> | null;
|
|
560
587
|
}>(async () => {
|
|
@@ -571,6 +598,7 @@ export const LoadingDataTableComponent = memo(
|
|
|
571
598
|
let tableData = props.data;
|
|
572
599
|
let rawTableData: TableData<T> | undefined | null = props.rawData;
|
|
573
600
|
let totalRows = props.totalRows;
|
|
601
|
+
let hasNextPage: boolean | undefined;
|
|
574
602
|
let cellStyles = props.cellStyles;
|
|
575
603
|
let cellHoverTexts = props.cellHoverTexts;
|
|
576
604
|
|
|
@@ -614,6 +642,7 @@ export const LoadingDataTableComponent = memo(
|
|
|
614
642
|
tableData = searchResults.data;
|
|
615
643
|
rawTableData = searchResults.raw_data;
|
|
616
644
|
totalRows = searchResults.total_rows;
|
|
645
|
+
hasNextPage = searchResults.has_next_page ?? undefined;
|
|
617
646
|
cellStyles = searchResults.cell_styles || {};
|
|
618
647
|
cellHoverTexts = searchResults.cell_hover_texts || {};
|
|
619
648
|
}
|
|
@@ -626,6 +655,7 @@ export const LoadingDataTableComponent = memo(
|
|
|
626
655
|
rows: tableData,
|
|
627
656
|
rawRows: rawData,
|
|
628
657
|
totalRows: totalRows,
|
|
658
|
+
hasNextPage,
|
|
629
659
|
cellStyles,
|
|
630
660
|
cellHoverTexts,
|
|
631
661
|
};
|
|
@@ -775,13 +805,16 @@ export const LoadingDataTableComponent = memo(
|
|
|
775
805
|
rawData={data?.rawRows}
|
|
776
806
|
columnSummaries={columnSummaries}
|
|
777
807
|
sorting={sorting}
|
|
778
|
-
setSorting={
|
|
808
|
+
setSorting={handleSortingChange}
|
|
779
809
|
searchQuery={searchQuery}
|
|
780
|
-
setSearchQuery={
|
|
810
|
+
setSearchQuery={handleSearchQueryChange}
|
|
781
811
|
filters={filters}
|
|
782
|
-
setFilters={
|
|
812
|
+
setFilters={handleFiltersChange}
|
|
783
813
|
reloading={isFetching && !isPending}
|
|
784
814
|
totalRows={data?.totalRows ?? props.totalRows}
|
|
815
|
+
hasNextPage={
|
|
816
|
+
props.lazy ? !error && (data?.hasNextPage ?? false) : undefined
|
|
817
|
+
}
|
|
785
818
|
sizeBytes={sizeBytes}
|
|
786
819
|
sizeBytesIsLoading={sizeBytesIsLoading}
|
|
787
820
|
paginationState={paginationState}
|
|
@@ -831,6 +864,7 @@ const DataTableComponent = ({
|
|
|
831
864
|
data,
|
|
832
865
|
rawData,
|
|
833
866
|
totalRows,
|
|
867
|
+
hasNextPage,
|
|
834
868
|
sizeBytes,
|
|
835
869
|
sizeBytesIsLoading,
|
|
836
870
|
maxColumns,
|
|
@@ -1130,7 +1164,8 @@ const DataTableComponent = ({
|
|
|
1130
1164
|
<>
|
|
1131
1165
|
{/* When the totalRows is "too_many" and the pageSize is the same as the
|
|
1132
1166
|
* number of rows, we are likely displaying all the data (could be more, but we don't know the total). */}
|
|
1133
|
-
{
|
|
1167
|
+
{hasNextPage === undefined &&
|
|
1168
|
+
totalRows === TOO_MANY_ROWS &&
|
|
1134
1169
|
paginationState.pageSize === data.length && (
|
|
1135
1170
|
<Banner className="mb-1 rounded">
|
|
1136
1171
|
Previewing the first {paginationState.pageSize} rows.
|
|
@@ -1168,6 +1203,7 @@ const DataTableComponent = ({
|
|
|
1168
1203
|
maxHeight={maxHeight}
|
|
1169
1204
|
sorting={sorting}
|
|
1170
1205
|
totalRows={totalRows}
|
|
1206
|
+
hasNextPage={hasNextPage}
|
|
1171
1207
|
sizeBytes={sizeBytes}
|
|
1172
1208
|
sizeBytesIsLoading={sizeBytesIsLoading}
|
|
1173
1209
|
totalColumns={totalColumns}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
render,
|
|
11
11
|
screen,
|
|
12
12
|
waitFor,
|
|
13
|
+
within,
|
|
13
14
|
} from "@testing-library/react";
|
|
14
15
|
import { Provider } from "jotai";
|
|
15
16
|
import { useState } from "react";
|
|
@@ -540,6 +541,275 @@ describe("table pagination and static notebook control suppression", () => {
|
|
|
540
541
|
mockIsStatic.mockReturnValue(false);
|
|
541
542
|
});
|
|
542
543
|
|
|
544
|
+
it("preserves the preview banner for ordinary paginated tables with unknown totals", async () => {
|
|
545
|
+
const props = makeProps();
|
|
546
|
+
render(
|
|
547
|
+
<Wrapper>
|
|
548
|
+
<LoadingDataTableComponent {...props} totalRows="too_many" />
|
|
549
|
+
</Wrapper>,
|
|
550
|
+
);
|
|
551
|
+
expect(
|
|
552
|
+
await screen.findByText("Previewing the first 10 rows."),
|
|
553
|
+
).toBeInTheDocument();
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it("pages lazy results with unknown totals and resets search, sorting, and filters to page one", async () => {
|
|
557
|
+
const props = makeProps();
|
|
558
|
+
const search = vi
|
|
559
|
+
.fn()
|
|
560
|
+
.mockImplementation(
|
|
561
|
+
async (request: {
|
|
562
|
+
page_number: number;
|
|
563
|
+
query?: string;
|
|
564
|
+
sort?: { by: string; descending: boolean }[];
|
|
565
|
+
}) => ({
|
|
566
|
+
data: [
|
|
567
|
+
{ id: request.page_number + 1, name: request.query || "item" },
|
|
568
|
+
],
|
|
569
|
+
total_rows: "too_many",
|
|
570
|
+
has_next_page: request.page_number === 0,
|
|
571
|
+
}),
|
|
572
|
+
);
|
|
573
|
+
render(
|
|
574
|
+
<Wrapper>
|
|
575
|
+
<LoadingDataTableComponent
|
|
576
|
+
{...props}
|
|
577
|
+
lazy={true}
|
|
578
|
+
totalRows="too_many"
|
|
579
|
+
selection={null}
|
|
580
|
+
showColumnSummaries={false}
|
|
581
|
+
search={search}
|
|
582
|
+
/>
|
|
583
|
+
</Wrapper>,
|
|
584
|
+
);
|
|
585
|
+
await waitFor(() =>
|
|
586
|
+
expect(screen.getByTestId("next-page-button")).toBeEnabled(),
|
|
587
|
+
);
|
|
588
|
+
expect(screen.getByText("Unknown, 2 columns")).toBeInTheDocument();
|
|
589
|
+
expect(screen.queryByTestId("last-page-button")).not.toBeInTheDocument();
|
|
590
|
+
expect(screen.queryByText(/Previewing the first/)).not.toBeInTheDocument();
|
|
591
|
+
fireEvent.click(screen.getByTestId("next-page-button"));
|
|
592
|
+
await waitFor(() =>
|
|
593
|
+
expect(search).toHaveBeenLastCalledWith(
|
|
594
|
+
expect.objectContaining({ page_number: 1 }),
|
|
595
|
+
),
|
|
596
|
+
);
|
|
597
|
+
await waitFor(() =>
|
|
598
|
+
expect(screen.getByTestId("next-page-button")).toBeDisabled(),
|
|
599
|
+
);
|
|
600
|
+
fireEvent.click(screen.getByTestId("previous-page-button"));
|
|
601
|
+
await waitFor(() =>
|
|
602
|
+
expect(screen.getByTestId("next-page-button")).toBeEnabled(),
|
|
603
|
+
);
|
|
604
|
+
fireEvent.click(screen.getByTestId("next-page-button"));
|
|
605
|
+
await waitFor(() =>
|
|
606
|
+
expect(screen.getByTestId("next-page-button")).toBeDisabled(),
|
|
607
|
+
);
|
|
608
|
+
search.mockClear();
|
|
609
|
+
fireEvent.change(screen.getByPlaceholderText("Search..."), {
|
|
610
|
+
target: { value: "new" },
|
|
611
|
+
});
|
|
612
|
+
await waitFor(() =>
|
|
613
|
+
expect(search).toHaveBeenCalledWith(
|
|
614
|
+
expect.objectContaining({ query: "new", page_number: 0 }),
|
|
615
|
+
),
|
|
616
|
+
);
|
|
617
|
+
expect(
|
|
618
|
+
search.mock.calls.every(([request]) => request.page_number === 0),
|
|
619
|
+
).toBe(true);
|
|
620
|
+
await waitFor(() =>
|
|
621
|
+
expect(screen.getByTestId("next-page-button")).toBeEnabled(),
|
|
622
|
+
);
|
|
623
|
+
fireEvent.click(screen.getByTestId("next-page-button"));
|
|
624
|
+
await waitFor(() =>
|
|
625
|
+
expect(screen.getByTestId("next-page-button")).toBeDisabled(),
|
|
626
|
+
);
|
|
627
|
+
search.mockClear();
|
|
628
|
+
fireEvent.click(
|
|
629
|
+
within(screen.getByRole("columnheader", { name: /name/ })).getByRole(
|
|
630
|
+
"button",
|
|
631
|
+
{ name: "Sort column ascending" },
|
|
632
|
+
),
|
|
633
|
+
);
|
|
634
|
+
await waitFor(() =>
|
|
635
|
+
expect(search).toHaveBeenCalledWith(
|
|
636
|
+
expect.objectContaining({
|
|
637
|
+
page_number: 0,
|
|
638
|
+
sort: [{ by: "name", descending: false }],
|
|
639
|
+
}),
|
|
640
|
+
),
|
|
641
|
+
);
|
|
642
|
+
expect(
|
|
643
|
+
search.mock.calls.every(([request]) => request.page_number === 0),
|
|
644
|
+
).toBe(true);
|
|
645
|
+
await waitFor(() =>
|
|
646
|
+
expect(screen.getByTestId("next-page-button")).toBeEnabled(),
|
|
647
|
+
);
|
|
648
|
+
fireEvent.click(screen.getByTestId("next-page-button"));
|
|
649
|
+
await waitFor(() =>
|
|
650
|
+
expect(search).toHaveBeenLastCalledWith(
|
|
651
|
+
expect.objectContaining({ page_number: 1 }),
|
|
652
|
+
),
|
|
653
|
+
);
|
|
654
|
+
await waitFor(() =>
|
|
655
|
+
expect(screen.getByTestId("next-page-button")).toBeDisabled(),
|
|
656
|
+
);
|
|
657
|
+
search.mockClear();
|
|
658
|
+
const columnMenu = within(
|
|
659
|
+
screen.getByRole("columnheader", { name: /name/ }),
|
|
660
|
+
).getByRole("button", { name: "Column options" });
|
|
661
|
+
fireEvent.keyDown(columnMenu, { key: "Enter" });
|
|
662
|
+
fireEvent.click(await screen.findByRole("menuitem", { name: "Filter" }));
|
|
663
|
+
fireEvent.change(screen.getByPlaceholderText("Text…"), {
|
|
664
|
+
target: { value: "item" },
|
|
665
|
+
});
|
|
666
|
+
fireEvent.click(screen.getByLabelText("Apply filter"));
|
|
667
|
+
await waitFor(() =>
|
|
668
|
+
expect(search).toHaveBeenCalledWith(
|
|
669
|
+
expect.objectContaining({
|
|
670
|
+
page_number: 0,
|
|
671
|
+
filters: expect.objectContaining({ children: expect.any(Array) }),
|
|
672
|
+
}),
|
|
673
|
+
),
|
|
674
|
+
);
|
|
675
|
+
expect(
|
|
676
|
+
search.mock.calls.every(([request]) => request.page_number === 0),
|
|
677
|
+
).toBe(true);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it.each(["success", "error"] as const)(
|
|
681
|
+
"ignores a stale lazy page %s after the query changes",
|
|
682
|
+
async (outcome) => {
|
|
683
|
+
const props = makeProps();
|
|
684
|
+
const initial = {
|
|
685
|
+
data: [{ id: 1, name: "initial-page" }],
|
|
686
|
+
total_rows: "too_many" as const,
|
|
687
|
+
has_next_page: true,
|
|
688
|
+
};
|
|
689
|
+
const oldPage = new Deferred<typeof initial>();
|
|
690
|
+
const newQuery = new Deferred<typeof initial>();
|
|
691
|
+
const search = vi
|
|
692
|
+
.fn()
|
|
693
|
+
.mockResolvedValueOnce(initial)
|
|
694
|
+
.mockReturnValueOnce(oldPage.promise)
|
|
695
|
+
.mockReturnValueOnce(newQuery.promise);
|
|
696
|
+
render(
|
|
697
|
+
<Wrapper>
|
|
698
|
+
<LoadingDataTableComponent
|
|
699
|
+
{...props}
|
|
700
|
+
lazy={true}
|
|
701
|
+
totalRows="too_many"
|
|
702
|
+
selection={null}
|
|
703
|
+
showColumnSummaries={false}
|
|
704
|
+
search={search}
|
|
705
|
+
/>
|
|
706
|
+
</Wrapper>,
|
|
707
|
+
);
|
|
708
|
+
await screen.findByText("initial-page");
|
|
709
|
+
fireEvent.click(screen.getByTestId("next-page-button"));
|
|
710
|
+
await waitFor(() =>
|
|
711
|
+
expect(search).toHaveBeenLastCalledWith(
|
|
712
|
+
expect.objectContaining({ page_number: 1 }),
|
|
713
|
+
),
|
|
714
|
+
);
|
|
715
|
+
expect(screen.getByTestId("next-page-button")).toBeDisabled();
|
|
716
|
+
|
|
717
|
+
fireEvent.change(screen.getByPlaceholderText("Search..."), {
|
|
718
|
+
target: { value: "current" },
|
|
719
|
+
});
|
|
720
|
+
await waitFor(() =>
|
|
721
|
+
expect(search).toHaveBeenLastCalledWith(
|
|
722
|
+
expect.objectContaining({ query: "current", page_number: 0 }),
|
|
723
|
+
),
|
|
724
|
+
);
|
|
725
|
+
await act(async () => {
|
|
726
|
+
newQuery.resolve({
|
|
727
|
+
data: [{ id: 2, name: "latest-page" }],
|
|
728
|
+
total_rows: "too_many",
|
|
729
|
+
has_next_page: false,
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
await screen.findByText("latest-page");
|
|
733
|
+
|
|
734
|
+
await act(async () => {
|
|
735
|
+
if (outcome === "success") {
|
|
736
|
+
oldPage.resolve({
|
|
737
|
+
...initial,
|
|
738
|
+
data: [{ id: 3, name: "stale-result" }],
|
|
739
|
+
});
|
|
740
|
+
} else {
|
|
741
|
+
oldPage.reject(new Error("stale-page-error"));
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
expect(screen.getByText("latest-page")).toBeInTheDocument();
|
|
745
|
+
expect(screen.queryByText("stale-result")).not.toBeInTheDocument();
|
|
746
|
+
expect(screen.queryByText("stale-page-error")).not.toBeInTheDocument();
|
|
747
|
+
expect(screen.getByTestId("previous-page-button")).toBeDisabled();
|
|
748
|
+
expect(screen.getByTestId("next-page-button")).toBeDisabled();
|
|
749
|
+
},
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
it("recovers from a failed lazy page by navigating back", async () => {
|
|
753
|
+
const props = makeProps();
|
|
754
|
+
const initial = {
|
|
755
|
+
data: [{ id: 1, name: "initial-page" }],
|
|
756
|
+
total_rows: "too_many" as const,
|
|
757
|
+
has_next_page: true,
|
|
758
|
+
};
|
|
759
|
+
const failedPage = new Deferred<typeof initial>();
|
|
760
|
+
const search = vi
|
|
761
|
+
.fn()
|
|
762
|
+
.mockResolvedValueOnce(initial)
|
|
763
|
+
.mockReturnValueOnce(failedPage.promise)
|
|
764
|
+
.mockResolvedValueOnce({
|
|
765
|
+
...initial,
|
|
766
|
+
data: [{ id: 1, name: "recovered-page" }],
|
|
767
|
+
})
|
|
768
|
+
.mockResolvedValueOnce({
|
|
769
|
+
...initial,
|
|
770
|
+
data: [{ id: 2, name: "last-page" }],
|
|
771
|
+
has_next_page: false,
|
|
772
|
+
});
|
|
773
|
+
render(
|
|
774
|
+
<Wrapper>
|
|
775
|
+
<LoadingDataTableComponent
|
|
776
|
+
{...props}
|
|
777
|
+
lazy={true}
|
|
778
|
+
totalRows="too_many"
|
|
779
|
+
selection={null}
|
|
780
|
+
showColumnSummaries={false}
|
|
781
|
+
search={search}
|
|
782
|
+
/>
|
|
783
|
+
</Wrapper>,
|
|
784
|
+
);
|
|
785
|
+
await screen.findByText("initial-page");
|
|
786
|
+
fireEvent.click(screen.getByTestId("next-page-button"));
|
|
787
|
+
await waitFor(() =>
|
|
788
|
+
expect(search).toHaveBeenLastCalledWith(
|
|
789
|
+
expect.objectContaining({ page_number: 1 }),
|
|
790
|
+
),
|
|
791
|
+
);
|
|
792
|
+
expect(screen.getByTestId("next-page-button")).toBeDisabled();
|
|
793
|
+
expect(screen.getByText("initial-page")).toBeInTheDocument();
|
|
794
|
+
await act(async () => {
|
|
795
|
+
failedPage.reject(new Error("page-fetch-failed"));
|
|
796
|
+
});
|
|
797
|
+
await screen.findByText("page-fetch-failed");
|
|
798
|
+
expect(screen.getByTestId("next-page-button")).toBeDisabled();
|
|
799
|
+
expect(screen.getByTestId("previous-page-button")).toBeEnabled();
|
|
800
|
+
|
|
801
|
+
fireEvent.click(screen.getByTestId("previous-page-button"));
|
|
802
|
+
await screen.findByText("recovered-page");
|
|
803
|
+
expect(search).toHaveBeenLastCalledWith(
|
|
804
|
+
expect.objectContaining({ page_number: 0 }),
|
|
805
|
+
);
|
|
806
|
+
expect(screen.queryByText("page-fetch-failed")).not.toBeInTheDocument();
|
|
807
|
+
expect(screen.getByTestId("next-page-button")).toBeEnabled();
|
|
808
|
+
fireEvent.click(screen.getByTestId("next-page-button"));
|
|
809
|
+
await screen.findByText("last-page");
|
|
810
|
+
expect(screen.getByTestId("next-page-button")).toBeDisabled();
|
|
811
|
+
});
|
|
812
|
+
|
|
543
813
|
it("requests only the first page with the new configured page size", async () => {
|
|
544
814
|
const props = makeProps();
|
|
545
815
|
const view = (pageSize: number) => (
|