@aglyn/plugins-forms 1.0.0-beta.199 → 1.0.0-beta.200

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.
@@ -17,120 +17,42 @@
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
19
  import * as Aglyn from "@aglyn/aglyn";
20
- import { buildRoute, isFormArchived, PageHeaderActions, pluginDocsHelp, readCampaignIds, Route } from "@aglyn/aglyn";
20
+ import { buildRoute, isFormArchived, PageHeaderActions, pluginDocsHelp, Route } from "@aglyn/aglyn";
21
21
  import { useConsoleWidgetSlot } from "@aglyn/aglyn/app-utils/console-widget-slot-context";
22
22
  import { ICON_VARIANT_SHOW_DETAIL } from "@aglyn/shared-data-enums";
23
23
  import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline, mdiContentCopy, mdiEyeOutline, mdiVectorSquare } from "@aglyn/shared-data-mdi";
24
24
  import { AppLink, CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
25
25
  import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
26
26
  import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
27
+ import ListQueryNotices, { listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
27
28
  import ListTable, { ListRowActions, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
28
- import { inMemoryListField } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
29
+ import { hiddenFilterVisibility } from "@aglyn/shared-ui-jsx/const/list-filter";
30
+ import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
29
31
  import { TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
30
- import { usePagedRowsFilter } from "@aglyn/shared-ui-jsx/hooks/use-paged-rows-filter";
32
+ import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
31
33
  import QuotaReadoutComponent from "@aglyn/shared-ui-jsx/components/quota-readout.component";
32
34
  import { CreateArtifactDrawer } from "@aglyn/shared-ui-jsx-forms";
33
35
  import { Alert, Button, Stack, Typography } from "@mui/material";
34
- import { collection, doc, updateDoc } from "firebase/firestore";
35
- import { DUPLICATE_MENU_LABEL, useConsoleHostRoute, useDuplicateResource, useFirestore, useHostCampaigns, useHostResourceApi, useLiveArtifactCount, usePagedCollection } from "@aglyn/tenant-feature-instance";
36
- import { collectionPage } from "@aglyn/tenant-feature-instance/hooks/host-collection-queries";
36
+ import { collection, doc, getCountFromServer, query, updateDoc, where } from "firebase/firestore";
37
+ import { DUPLICATE_MENU_LABEL, useConsoleHostRoute, useDuplicateResource, useFirestore, useHostCampaigns, useHostResourceApi, useLiveArtifactCount } from "@aglyn/tenant-feature-instance";
38
+ import { useListQuery } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
37
39
  import { useRouter } from "next/navigation";
38
- import { useCallback, useMemo, useState } from "react";
40
+ import { useCallback, useEffect, useMemo, useState } from "react";
39
41
  import { BUNDLE_ID } from "../constants/bundle-common.js";
40
- /*
41
- * What the forms grid's Filters panel offers (AGL-3317, AGL-3330): every
42
- * column the table shows, typed, and the stored fields a site sorts its
43
- * forms out by, as columns kept out of sight.
44
- *
45
- * EVERY ONE NARROWS THE LOADED WINDOW; none is served. The list is a paged
46
- * listener over the document id (`collectionPage`), so a filter matches over
47
- * what the window has read, which the first filter widens
48
- * (`usePagedRowsFilter`) and the caption under the chips owns up to. The
49
- * query never changes shape, so no filter here needs an index. The figures
50
- * are the `stats` counters `/api/forms/submit` keeps on the form document
51
- * itself, read with the row.
52
- *
53
- * - Submissions and Leads are numbers. Leads is ABSENT on a form that does
54
- * not route to leads — the column's dash — so `is empty` finds those and
55
- * `= 0` finds a routed form that has produced none.
56
- * - Last submission, Updated and Created are days. Created is a column
57
- * hidden by default: `/api/hosts/resources` stamps it on every form it
58
- * makes.
59
- * - Status (active or retired), Lead routing (`routing.lead`) and Campaign
60
- * (`campaignIds`) are hidden columns, matched on keys derived by
61
- * {@link formFilterRow} so the row keeps the stored fields its menu reads.
62
- * A form's campaigns are ids, matched member by member and byte for byte,
63
- * as the campaigns list matches its `listIds`.
64
- *
65
- * Nothing else a form stores is a filter. It keeps no type and no template,
66
- * and its plan slot is not its own: a retired form keeps one, which is the
67
- * Status filter's answer.
68
- */ export const FORM_FILTER_FIELDS = [
69
- inMemoryListField('displayName', 'text'),
70
- inMemoryListField('slug', 'text'),
71
- inMemoryListField('submissions', 'number', 'stats.submissions'),
72
- inMemoryListField('leads', 'number', 'stats.leads'),
73
- inMemoryListField('lastSubmission', 'date', 'stats.lastSubmissionAtMs'),
74
- inMemoryListField('updatedAt', 'date'),
75
- inMemoryListField('createdAt', 'date'),
76
- inMemoryListField('status', 'select', 'statusKey'),
77
- inMemoryListField('leadRouting', 'select', 'leadRoutingKey'),
78
- _extends({}, inMemoryListField('campaignIds', 'select', 'campaignKeys'), {
79
- tokensPath: 'campaignKeys',
80
- verbatimTokens: true
81
- })
82
- ];
83
- export const FORM_FILTER_HEADERS = {
84
- displayName: 'Display name',
85
- slug: 'Slug',
86
- submissions: 'Submissions',
87
- leads: 'Leads',
88
- lastSubmission: 'Last submission',
89
- updatedAt: 'Updated',
90
- createdAt: 'Created',
91
- status: 'Status',
92
- leadRouting: 'Lead routing',
93
- campaignIds: 'Campaign'
94
- };
95
- const FORM_STATUS_OPTIONS = [
96
- {
97
- value: 'active',
98
- label: 'Active'
99
- },
100
- {
101
- value: 'retired',
102
- label: 'Retired'
103
- }
104
- ];
105
- const FORM_LEAD_ROUTING_OPTIONS = [
106
- {
107
- value: 'on',
108
- label: 'On'
109
- },
110
- {
111
- value: 'off',
112
- label: 'Off'
113
- }
114
- ];
115
- /** What the quick search reads: the name, the slug and the form's id. */ export const FORM_SEARCH_FIELDS = [
42
+ import { FORM_LEAD_ROUTING_OPTIONS, FORM_LIST_FILTER_FIELDS, FORM_LIST_FILTER_HEADERS, FORM_LIST_QUERY, FORM_LIST_SELECT_FIELDS, FORM_STATUS_OPTIONS, formListRequest } from "./form-list-query.js";
43
+ /**
44
+ * The filterable columns the table shows. The rest of the declaration —
45
+ * Status, Lead routing, Campaign — reaches the Filters panel as hidden
46
+ * columns (`hiddenFilterColumns`).
47
+ */ const FORM_VISIBLE_FILTER_COLUMNS = [
116
48
  'displayName',
117
49
  'slug',
118
- '$id'
50
+ 'submissions',
51
+ 'leads',
52
+ 'lastSubmission',
53
+ 'updatedAt'
119
54
  ];
120
- const FORM_HIDDEN_COLUMNS = {
121
- createdAt: false,
122
- status: false,
123
- leadRouting: false,
124
- campaignIds: false
125
- };
126
- /** A form row with the keys its hidden filters match on. */ export const formFilterRow = (form)=>{
127
- var _form_routing;
128
- return _extends({}, form, {
129
- statusKey: isFormArchived(form) ? 'retired' : 'active',
130
- leadRoutingKey: (form == null ? void 0 : (_form_routing = form.routing) == null ? void 0 : _form_routing.lead) === true ? 'on' : 'off',
131
- campaignKeys: readCampaignIds(form)
132
- });
133
- };
55
+ const FORM_HIDDEN_COLUMNS = hiddenFilterVisibility(FORM_LIST_FILTER_FIELDS, FORM_VISIBLE_FILTER_COLUMNS);
134
56
  /**
135
57
  * THE FORM CATALOG, WITH ITS SHAPE VISIBLE BEFORE THERE IS ANYTHING IN IT.
136
58
  *
@@ -193,6 +115,7 @@ const FORM_HIDDEN_COLUMNS = {
193
115
  const [createError, setCreateError] = useState(null);
194
116
  const [creating, setCreating] = useState(false);
195
117
  const [retireError, setRetireError] = useState(null);
118
+ /** Bumped by each retire or restore, so the retired count is asked again. */ const [retiredEpoch, setRetiredEpoch] = useState(0);
196
119
  /**
197
120
  * Retire a form, or put it back (AGL-2671).
198
121
  *
@@ -213,8 +136,11 @@ const FORM_HIDDEN_COLUMNS = {
213
136
  setRetireError(null);
214
137
  try {
215
138
  await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {
216
- archivedAt: retired ? Date.now() : null
139
+ archivedAt: retired ? Date.now() : null,
140
+ // The mirror the list's query asks (AGL-3330).
141
+ retired
217
142
  });
143
+ setRetiredEpoch((epoch)=>epoch + 1);
218
144
  } catch (unused) {
219
145
  setRetireError(retired ? 'Could not retire that form. Try again shortly.' : 'Could not restore that form. Try again shortly.');
220
146
  }
@@ -222,77 +148,70 @@ const FORM_HIDDEN_COLUMNS = {
222
148
  firestore,
223
149
  hostId
224
150
  ]);
225
- /**
226
- * The list PAGES, over an ordered walk.
151
+ /*
152
+ * THE LIST IS ITS QUERY (AGL-3330, on the AGL-3321 list query plan).
153
+ *
154
+ * The grid's Filters panel and search box edit the clauses and the words;
155
+ * `useListQuery` puts every one of them on ONE Firestore query and pages
156
+ * that query, so a page is a page of matches and a form whose only match
157
+ * is past the first page is still found. What one query cannot hold — a
158
+ * second range, a second array clause — is refused by name above the list
159
+ * (`ListQueryNotices`) rather than matched over the rows read. See
160
+ * `form-list-query.ts` for every field and the indexes they cost.
227
161
  *
228
- * `collectionPage` holds the ordering decision and the reason it is the
229
- * document id rather than `displayName` — briefly, `orderBy` matches only
230
- * documents that HAVE the field, and the resources route stores an
231
- * allow-list it never checks for presence, so ordering on a name would hide
232
- * every form created without one rather than mis-sorting the list.
162
+ * Unfiltered it reads in document order, as it always has: `orderBy` on a
163
+ * name would drop every form stored without one rather than mis-sort it.
233
164
  *
234
- * The page is NOT re-sorted: sorting a server-ordered window in the browser
235
- * is what makes a pseudo-random sample look like the first page.
236
- */ const { status, data: formData, rows: formWindow, hasMore, page, setPage, pageSize, setPageSize } = usePagedCollection((pageLimit)=>collectionPage(collection(firestore, 'hosts', hostId, 'forms'), pageLimit), [
165
+ * A retired form is a TOMBSTONE, not a row, unless the reader asks for
166
+ * Status (AGL-2671). The query leaves them out (`retired == false`), so a
167
+ * page is a page of forms in use; a Status clause replaces that with the
168
+ * reader's own choice. Retiring a form removes it from the list, and
169
+ * without the Status filter that would be a one-way door: the row is the
170
+ * only route back to Restore.
171
+ */ const [formClauses, setFormClauses] = useState([]);
172
+ const gridFilter = useListGridFilter({
173
+ clauses: formClauses,
174
+ onChange: setFormClauses,
175
+ selectFields: FORM_LIST_SELECT_FIELDS
176
+ });
177
+ const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.length > 0;
178
+ const formsCollection = useMemo(()=>collection(firestore, 'hosts', hostId, 'forms'), [
237
179
  firestore,
238
180
  hostId
239
- ], {
181
+ ]);
182
+ const { status, rows: forms, hasMore, page, setPage, pageSize, setPageSize, plan } = useListQuery({
183
+ collection: formsCollection,
184
+ declaration: FORM_LIST_QUERY,
185
+ request: formListRequest(gridFilter.clauses, gridFilter.searchWords),
186
+ deps: [
187
+ firestore,
188
+ hostId
189
+ ],
240
190
  idField: '$id'
241
191
  });
242
192
  /*
243
- * A retired form is a TOMBSTONE, not a row — unless the reader asks for the
244
- * tombstones (AGL-2671). Client-side because it has to be: Firestore cannot
245
- * ask for the ABSENCE of a field, so a form created through the resources
246
- * route (which carries no `archivedAt` at all) and one archived later are
247
- * not one value to filter on. The cost is that a tombstone spends a slot in
248
- * whichever page it falls in.
249
- *
250
- * The Filters panel's Status is how the reader asks (AGL-3317). With no
251
- * Status clause the list leaves the tombstones out, as it always has, and
252
- * reads one page; a Status clause hands the panel every row the window
253
- * read, retired included, and the clause itself picks which to show. That
254
- * default is the absence of a clause rather than a clause the list starts
255
- * with, because a starting clause would widen the window on every visit to
256
- * answer a question nobody asked. Retiring a form removes it from the list,
257
- * and without the Status filter that would be a one-way door: the row is
258
- * the only route back to Restore.
259
- */ const [formClauses, setFormClauses] = useState([]);
260
- const askedStatus = formClauses.some((clause)=>clause.field === 'status');
261
- const formRows = useMemo(()=>formWindow.filter((form)=>askedStatus || !isFormArchived(form)).map(formFilterRow), [
262
- formWindow,
263
- askedStatus
264
- ]);
265
- const formWindowRows = useMemo(()=>formData == null ? void 0 : formData.filter((form)=>askedStatus || !isFormArchived(form)).map(formFilterRow), [
266
- formData,
267
- askedStatus
268
- ]);
269
- /*
270
193
  * The Campaign filter's choices: the campaigns placed on this site, by
271
- * name, and any campaign a loaded form is filed under that the site's list
272
- * does not name — one since taken off this site — by its id, so the form
273
- * filed under it can still be found.
194
+ * name, and any campaign a clause names that the list does not, by its id,
195
+ * so the chip can still be read and cleared.
274
196
  *
275
- * Read only once a loaded form IS filed under a campaign. The names cost
276
- * the org lookup and up to fifty campaign documents, which a catalog with
277
- * no filed form would pay on every visit for a filter with nothing to find;
278
- * there the column has no choices and the panel leaves it out.
279
- */ const filedCampaignIds = useMemo(()=>[
280
- ...new Set((formData != null ? formData : []).flatMap((form)=>readCampaignIds(form)))
281
- ], [
282
- formData
283
- ]);
197
+ * Read once the reader opens the Filters panel, not on every visit: the
198
+ * names cost the org lookup and up to fifty campaign documents, which a
199
+ * reader who came to open one form would pay for a filter they never
200
+ * touched. Until they land the column has no choices, and the panel lists
201
+ * it a moment later.
202
+ */ const [wantsCampaigns, setWantsCampaigns] = useState(false);
284
203
  const siteCampaigns = useHostCampaigns(hostId, {
285
- enabled: filedCampaignIds.length > 0
204
+ enabled: wantsCampaigns || formClauses.some((clause)=>clause.field === 'campaignIds')
286
205
  });
287
206
  const formFilterOptions = useMemo(()=>{
288
207
  const named = siteCampaigns.options.map((option)=>({
289
208
  value: option.value,
290
209
  label: option.label
291
210
  }));
292
- const unnamed = siteCampaigns.ready ? filedCampaignIds.filter((id)=>!named.some((option)=>option.value === id)).map((id)=>({
293
- value: id,
294
- label: id
295
- })) : [];
211
+ const unnamed = formClauses.filter((clause)=>clause.field === 'campaignIds').flatMap((clause)=>clause.value.split(',')).map((value)=>value.trim()).filter((value)=>value && !named.some((option)=>option.value === value)).map((value)=>({
212
+ value,
213
+ label: value
214
+ }));
296
215
  return {
297
216
  status: FORM_STATUS_OPTIONS,
298
217
  leadRouting: FORM_LEAD_ROUTING_OPTIONS,
@@ -303,35 +222,35 @@ const FORM_HIDDEN_COLUMNS = {
303
222
  };
304
223
  }, [
305
224
  siteCampaigns.options,
306
- siteCampaigns.ready,
307
- filedCampaignIds
225
+ formClauses
308
226
  ]);
309
- const formFilter = usePagedRowsFilter({
310
- data: formWindowRows,
311
- rows: formRows,
312
- hasMore,
313
- page,
314
- setPage,
315
- pageSize,
316
- setPageSize
317
- }, {
318
- fields: FORM_FILTER_FIELDS,
319
- options: formFilterOptions,
320
- headers: FORM_FILTER_HEADERS,
321
- search: FORM_SEARCH_FIELDS,
322
- clauses: formClauses,
323
- onChange: setFormClauses
324
- });
325
- // The rows on screen: the page, or the page of the matches.
326
- const forms = formFilter.rows;
327
227
  /*
328
228
  * The COUNT is a server aggregate, not the length of a page. `forms` is one
329
229
  * page, and publishing that as the site's form count would read as room to
330
230
  * spare on a site that is already at the ceiling.
331
231
  */ const liveFormCount = useLiveArtifactCount(hostId, 'forms');
332
- // Pending or refused, the page window stands in: a LOWER bound, never a
333
- // confident zero.
334
- const formsUsed = liveFormCount != null ? liveFormCount : formWindow.length;
232
+ // Pending or refused, the page stands in: a LOWER bound, never a confident
233
+ // zero.
234
+ const formsUsed = liveFormCount != null ? liveFormCount : forms.length;
235
+ /*
236
+ * How many forms are retired, for the "keep their slot" line (AGL-2674).
237
+ * An aggregate, because the list no longer reads a retired row unless it is
238
+ * asked for one; asked again after this card retires or restores a form.
239
+ */ const [retiredCount, setRetiredCount] = useState(null);
240
+ useEffect(()=>{
241
+ let active = true;
242
+ getCountFromServer(query(formsCollection, where('retired', '==', true))).then((snapshot)=>{
243
+ if (active) setRetiredCount(snapshot.data().count);
244
+ }).catch(()=>{
245
+ // The line is a courtesy; without the count it is simply not shown.
246
+ });
247
+ return ()=>{
248
+ active = false;
249
+ };
250
+ }, [
251
+ formsCollection,
252
+ retiredEpoch
253
+ ]);
335
254
  /**
336
255
  * Name first, then create (AGL-700).
337
256
  *
@@ -482,21 +401,25 @@ const FORM_HIDDEN_COLUMNS = {
482
401
  return (value == null ? void 0 : (_value_toLocaleString = value.toLocaleString) == null ? void 0 : _value_toLocaleString.call(value)) || '--';
483
402
  }
484
403
  },
404
+ /*
405
+ * Status and Lead routing, hidden until Columns shows them (AGL-3330).
406
+ * Each is a select the Filters panel offers either way, and draws the
407
+ * choice its value names. Status reads the FACT, `archivedAt`, rather
408
+ * than the `retired` mirror the query asks, so a form the backfill has
409
+ * not reached yet still reads true.
410
+ */ {
411
+ field: 'status',
412
+ headerName: 'Status',
413
+ minWidth: 110,
414
+ valueGetter: (_value, row)=>String(isFormArchived(row))
415
+ },
485
416
  {
486
- // Hidden until Columns shows it; filterable either way (AGL-3330).
487
- field: 'createdAt',
488
- headerName: 'Created',
489
- minWidth: 170,
490
- flex: 1,
491
- type: 'date',
492
- valueGetter: (value)=>{
493
- var _ref;
494
- var _value_toDate;
495
- return (_ref = value == null ? void 0 : (_value_toDate = value.toDate) == null ? void 0 : _value_toDate.call(value)) != null ? _ref : null;
496
- },
497
- valueFormatter: (value)=>{
498
- var _value_toLocaleString;
499
- return (value == null ? void 0 : (_value_toLocaleString = value.toLocaleString) == null ? void 0 : _value_toLocaleString.call(value)) || '--';
417
+ field: 'leadRouting',
418
+ headerName: 'Lead routing',
419
+ minWidth: 120,
420
+ valueGetter: (_value, row)=>{
421
+ var _row_routing;
422
+ return String((row == null ? void 0 : (_row_routing = row.routing) == null ? void 0 : _row_routing.lead) === true);
500
423
  }
501
424
  },
502
425
  listActionsColumn((row)=>{
@@ -613,7 +536,7 @@ const FORM_HIDDEN_COLUMNS = {
613
536
  limit: Aglyn.checkQuota(org, 'formsPerHost', formsUsed).limit,
614
537
  noun: "form"
615
538
  }),
616
- formWindow.some((form)=>isFormArchived(form)) ? /*#__PURE__*/ _jsx(Typography, {
539
+ retiredCount ? /*#__PURE__*/ _jsx(Typography, {
617
540
  variant: "caption",
618
541
  color: "text.secondary",
619
542
  children: 'Retired forms keep their slot'
@@ -656,21 +579,43 @@ const FORM_HIDDEN_COLUMNS = {
656
579
  },
657
580
  children: retireError
658
581
  }) : null,
659
- /*#__PURE__*/ _jsx(ListFilterChips, _extends({}, formFilter.chipsProps)),
660
- formFilter.filtering && hasMore ? /*#__PURE__*/ _jsx(Typography, {
661
- variant: "caption",
662
- color: "text.secondary",
663
- children: `Filtering the ${formFilter.read} forms read so far — the next page reads more.`
664
- }) : null,
582
+ /*#__PURE__*/ _jsx(ListFilterChips, {
583
+ fields: FORM_LIST_FILTER_FIELDS,
584
+ headers: FORM_LIST_FILTER_HEADERS,
585
+ clauses: gridFilter.clauses,
586
+ onChange: gridFilter.setClauses,
587
+ options: formFilterOptions
588
+ }),
589
+ /*#__PURE__*/ _jsx(ListQueryNotices, {
590
+ refused: listQueryRefusals(plan.refused, {
591
+ fields: FORM_LIST_FILTER_FIELDS,
592
+ headers: FORM_LIST_FILTER_HEADERS,
593
+ options: formFilterOptions
594
+ }),
595
+ notices: plan.notices
596
+ }),
665
597
  /*#__PURE__*/ _jsx(ListTable, _extends({
666
598
  rowHeight: TABLE_ROW_HEIGHT,
667
- columns: formFilter.filterColumns(columns),
599
+ columns: listFilterGridColumns(columns, FORM_LIST_FILTER_FIELDS, formFilterOptions, FORM_LIST_FILTER_HEADERS),
668
600
  initialState: {
669
601
  columns: {
670
602
  columnVisibilityModel: FORM_HIDDEN_COLUMNS
671
603
  }
604
+ },
605
+ /*
606
+ * The grid must NOT also filter. The query answers the panel and
607
+ * the search, so a second pass in the browser could only drop rows
608
+ * that already matched — it compares what a column DRAWS, and the
609
+ * query compares what the document stores.
610
+ */ filterMode: "server",
611
+ filterModel: gridFilter.filterModel,
612
+ onFilterModelChange: gridFilter.onFilterModelChange,
613
+ quickFilter: true,
614
+ // The Campaign filter's names are read once the panel is opened.
615
+ onPreferencePanelOpen: (params)=>{
616
+ if (params.openedPanelValue === 'filters') setWantsCampaigns(true);
672
617
  }
673
- }, formFilter.gridProps, formFilter.filtering ? {
618
+ }, filtering ? {
674
619
  noRowsLabel: 'No forms match these filters'
675
620
  } : {
676
621
  noRowsLabel: 'No forms yet',
@@ -691,7 +636,14 @@ const FORM_HIDDEN_COLUMNS = {
691
636
  // Paged by the footer below, so the grid must not also slice.
692
637
  hideFooter: true
693
638
  })),
694
- /*#__PURE__*/ _jsx(ListPagination, _extends({}, formFilter.pagination)),
639
+ /*#__PURE__*/ _jsx(ListPagination, {
640
+ page: page,
641
+ pageSize: pageSize,
642
+ rowCount: forms.length,
643
+ hasMore: hasMore,
644
+ onPageChange: setPage,
645
+ onPageSizeChange: setPageSize
646
+ }),
695
647
  /*#__PURE__*/ _jsx(CreateArtifactDrawer, {
696
648
  open: createOpen,
697
649
  onClose: ()=>setCreateOpen(false),