@teleporthq/teleport-plugin-next-data-source 0.43.71 → 0.43.75
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/__tests__/_helpers/pagination-widget-fixture.ts +8 -1
- package/__tests__/blog-adjacent-arranged.test.ts +176 -0
- package/__tests__/blog-post-contents.test.ts +212 -0
- package/__tests__/blog-post-page-enrichment.test.ts +28 -5
- package/__tests__/blog-post-picture-stand-in.test.ts +48 -0
- package/__tests__/blog-post-reading-time.test.ts +55 -0
- package/__tests__/browser-row-policy.test.ts +5 -1
- package/__tests__/content-preset-wiring.test.ts +108 -0
- package/__tests__/fixed-list-sort.test.ts +183 -0
- package/__tests__/pagination-filter-groups.test.ts +186 -0
- package/__tests__/plain-list-sort.test.ts +215 -0
- package/__tests__/product-price-sort.ts +47 -19
- package/__tests__/rest-api-key-header.test.ts +18 -0
- package/__tests__/search-url-sync.test.ts +34 -15
- package/__tests__/shared-resource-prefetch.test.ts +65 -0
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
- package/dist/cjs/fetchers/postgresql.js +2 -1
- package/dist/cjs/fetchers/postgresql.js.map +1 -1
- package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
- package/dist/cjs/fetchers/rest-api.js +3 -1
- package/dist/cjs/fetchers/rest-api.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +3 -2
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/fetchers/utils/browser-row-policy.d.ts +7 -6
- package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -1
- package/dist/cjs/fetchers/utils/browser-row-policy.js +7 -33
- package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -1
- package/dist/cjs/filter-utils.d.ts +19 -7
- package/dist/cjs/filter-utils.d.ts.map +1 -1
- package/dist/cjs/filter-utils.js +49 -13
- package/dist/cjs/filter-utils.js.map +1 -1
- package/dist/cjs/pagination-plugin.d.ts.map +1 -1
- package/dist/cjs/pagination-plugin.js +137 -118
- package/dist/cjs/pagination-plugin.js.map +1 -1
- package/dist/cjs/product-price-sort.d.ts +0 -9
- package/dist/cjs/product-price-sort.d.ts.map +1 -1
- package/dist/cjs/product-price-sort.js +1 -13
- package/dist/cjs/product-price-sort.js.map +1 -1
- package/dist/cjs/row-order.d.ts +19 -0
- package/dist/cjs/row-order.d.ts.map +1 -0
- package/dist/cjs/row-order.js +45 -0
- package/dist/cjs/row-order.js.map +1 -0
- package/dist/cjs/sort-utils.d.ts +10 -0
- package/dist/cjs/sort-utils.d.ts.map +1 -1
- package/dist/cjs/sort-utils.js +22 -1
- package/dist/cjs/sort-utils.js.map +1 -1
- package/dist/cjs/transformations/blog-adjacent-posts.d.ts +11 -1
- package/dist/cjs/transformations/blog-adjacent-posts.d.ts.map +1 -1
- package/dist/cjs/transformations/blog-adjacent-posts.js +14 -1
- package/dist/cjs/transformations/blog-adjacent-posts.js.map +1 -1
- package/dist/cjs/transformations/blog-comments.d.ts +1 -1
- package/dist/cjs/transformations/blog-comments.d.ts.map +1 -1
- package/dist/cjs/transformations/blog-comments.js +1 -1
- package/dist/cjs/transformations/blog-comments.js.map +1 -1
- package/dist/cjs/transformations/blog-post.d.ts +14 -0
- package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
- package/dist/cjs/transformations/blog-post.js +4 -1
- package/dist/cjs/transformations/blog-post.js.map +1 -1
- package/dist/cjs/transformations/heading-anchors.d.ts +32 -8
- package/dist/cjs/transformations/heading-anchors.d.ts.map +1 -1
- package/dist/cjs/transformations/heading-anchors.js +34 -10
- package/dist/cjs/transformations/heading-anchors.js.map +1 -1
- package/dist/cjs/transformations/index.d.ts +24 -12
- package/dist/cjs/transformations/index.d.ts.map +1 -1
- package/dist/cjs/transformations/index.js +59 -11
- package/dist/cjs/transformations/index.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils.d.ts.map +1 -1
- package/dist/cjs/utils.js +52 -3
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
- package/dist/esm/fetchers/postgresql.js +3 -2
- package/dist/esm/fetchers/postgresql.js.map +1 -1
- package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
- package/dist/esm/fetchers/rest-api.js +3 -1
- package/dist/esm/fetchers/rest-api.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +4 -3
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/fetchers/utils/browser-row-policy.d.ts +7 -6
- package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -1
- package/dist/esm/fetchers/utils/browser-row-policy.js +5 -31
- package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -1
- package/dist/esm/filter-utils.d.ts +19 -7
- package/dist/esm/filter-utils.d.ts.map +1 -1
- package/dist/esm/filter-utils.js +46 -12
- package/dist/esm/filter-utils.js.map +1 -1
- package/dist/esm/pagination-plugin.d.ts.map +1 -1
- package/dist/esm/pagination-plugin.js +140 -121
- package/dist/esm/pagination-plugin.js.map +1 -1
- package/dist/esm/product-price-sort.d.ts +0 -9
- package/dist/esm/product-price-sort.d.ts.map +1 -1
- package/dist/esm/product-price-sort.js +0 -11
- package/dist/esm/product-price-sort.js.map +1 -1
- package/dist/esm/row-order.d.ts +19 -0
- package/dist/esm/row-order.d.ts.map +1 -0
- package/dist/esm/row-order.js +40 -0
- package/dist/esm/row-order.js.map +1 -0
- package/dist/esm/sort-utils.d.ts +10 -0
- package/dist/esm/sort-utils.d.ts.map +1 -1
- package/dist/esm/sort-utils.js +20 -0
- package/dist/esm/sort-utils.js.map +1 -1
- package/dist/esm/transformations/blog-adjacent-posts.d.ts +11 -1
- package/dist/esm/transformations/blog-adjacent-posts.d.ts.map +1 -1
- package/dist/esm/transformations/blog-adjacent-posts.js +14 -1
- package/dist/esm/transformations/blog-adjacent-posts.js.map +1 -1
- package/dist/esm/transformations/blog-comments.d.ts +1 -1
- package/dist/esm/transformations/blog-comments.d.ts.map +1 -1
- package/dist/esm/transformations/blog-comments.js +1 -1
- package/dist/esm/transformations/blog-comments.js.map +1 -1
- package/dist/esm/transformations/blog-post.d.ts +14 -0
- package/dist/esm/transformations/blog-post.d.ts.map +1 -1
- package/dist/esm/transformations/blog-post.js +4 -1
- package/dist/esm/transformations/blog-post.js.map +1 -1
- package/dist/esm/transformations/heading-anchors.d.ts +32 -8
- package/dist/esm/transformations/heading-anchors.d.ts.map +1 -1
- package/dist/esm/transformations/heading-anchors.js +33 -9
- package/dist/esm/transformations/heading-anchors.js.map +1 -1
- package/dist/esm/transformations/index.d.ts +24 -12
- package/dist/esm/transformations/index.d.ts.map +1 -1
- package/dist/esm/transformations/index.js +58 -11
- package/dist/esm/transformations/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils.d.ts.map +1 -1
- package/dist/esm/utils.js +52 -3
- package/dist/esm/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/fetchers/postgresql.ts +11 -15
- package/src/fetchers/rest-api.ts +3 -1
- package/src/fetchers/teleport.ts +12 -16
- package/src/fetchers/utils/browser-row-policy.ts +10 -20
- package/src/filter-utils.ts +102 -28
- package/src/pagination-plugin.ts +211 -175
- package/src/product-price-sort.ts +0 -11
- package/src/row-order.ts +54 -0
- package/src/sort-utils.ts +25 -0
- package/src/transformations/blog-adjacent-posts.ts +89 -3
- package/src/transformations/blog-comments.ts +5 -5
- package/src/transformations/blog-post.ts +64 -13
- package/src/transformations/heading-anchors.ts +79 -16
- package/src/transformations/index.ts +80 -13
- package/src/utils.ts +74 -3
package/src/pagination-plugin.ts
CHANGED
|
@@ -8,11 +8,16 @@ import * as types from '@babel/types'
|
|
|
8
8
|
import { parseExpression } from '@babel/parser'
|
|
9
9
|
import { ASTStatementOrder, GenericUtils, StringUtils } from '@teleporthq/teleport-shared'
|
|
10
10
|
import { ASTUtils, URLQueryWriter, URLSearchParamSync } from '@teleporthq/teleport-plugin-common'
|
|
11
|
-
import { generateSafeFileName } from './utils'
|
|
11
|
+
import { collectFilterLeafConditions, generateSafeFileName, pruneFilterTree } from './utils'
|
|
12
12
|
import { generateDataSourceFetcherWithCore } from './data-source-fetchers'
|
|
13
13
|
import { buildProductTransformOptions, type EntityTransformOptions } from './transformations'
|
|
14
|
-
import { appendSortsParam, DynamicSortAST, extractDynamicSort } from './sort-utils'
|
|
15
|
-
import {
|
|
14
|
+
import { appendSortsParam, DynamicSortAST, extractDynamicSort, readFixedSort } from './sort-utils'
|
|
15
|
+
import {
|
|
16
|
+
appendFiltersParam,
|
|
17
|
+
buildStaticFiltersStringifyCall,
|
|
18
|
+
pushStateIdsAsDeps,
|
|
19
|
+
pushPropIdsAsDeps,
|
|
20
|
+
} from './filter-utils'
|
|
16
21
|
import { appendItemsPathParam, extractItemsPath } from './items-path'
|
|
17
22
|
import { resolveRepeaterPerPage } from './repeater-page-size'
|
|
18
23
|
import {
|
|
@@ -121,51 +126,20 @@ function searchDefaultValueInitAST(value: SearchDefaultValue | undefined): types
|
|
|
121
126
|
return types.cloneNode(value.ast, /* deep */ true) as types.Expression
|
|
122
127
|
}
|
|
123
128
|
|
|
124
|
-
// Builds the `useState(...)` initializer for the search query
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
// three products-list controls consistent.
|
|
129
|
+
// Builds the `useState(...)` initializer for the search query: the
|
|
130
|
+
// `searchDefaultValue` seed (empty string by default), never the address. A
|
|
131
|
+
// statically generated page is rendered on the server without the visitor's
|
|
132
|
+
// `?searchKeyword=`, and the browser's first render must match it — a query
|
|
133
|
+
// seeded from `window.location.search` rendered a filled input and skipped the
|
|
134
|
+
// prefetched rows there, and React warned of a hydration mismatch. A URL-bound
|
|
135
|
+
// search adopts the address after mount instead (`pushSearchUrlSyncEffects`),
|
|
136
|
+
// as the page number does (`page-url-sync.ts`).
|
|
133
137
|
//
|
|
134
138
|
// Returns a fresh AST on every call (no shared node references) so the same
|
|
135
139
|
// initializer can seed both the immediate `ds_N_searchQuery` state and the
|
|
136
140
|
// debounced `ds_N_state.debouncedQuery` value.
|
|
137
141
|
function buildSearchQueryInitAST(usage: DataSourceUsage): types.Expression {
|
|
138
|
-
|
|
139
|
-
if (!usage.searchUrlParamKey) {
|
|
140
|
-
return fallback
|
|
141
|
-
}
|
|
142
|
-
// (typeof window !== 'undefined'
|
|
143
|
-
// ? new URLSearchParams(window.location.search).get('<key>')
|
|
144
|
-
// : null) ?? <fallback>
|
|
145
|
-
return types.logicalExpression(
|
|
146
|
-
'??',
|
|
147
|
-
types.conditionalExpression(
|
|
148
|
-
types.binaryExpression(
|
|
149
|
-
'!==',
|
|
150
|
-
types.unaryExpression('typeof', types.identifier('window')),
|
|
151
|
-
types.stringLiteral('undefined')
|
|
152
|
-
),
|
|
153
|
-
types.callExpression(
|
|
154
|
-
types.memberExpression(
|
|
155
|
-
types.newExpression(types.identifier('URLSearchParams'), [
|
|
156
|
-
types.memberExpression(
|
|
157
|
-
types.memberExpression(types.identifier('window'), types.identifier('location')),
|
|
158
|
-
types.identifier('search')
|
|
159
|
-
),
|
|
160
|
-
]),
|
|
161
|
-
types.identifier('get')
|
|
162
|
-
),
|
|
163
|
-
[types.stringLiteral(usage.searchUrlParamKey)]
|
|
164
|
-
),
|
|
165
|
-
types.nullLiteral()
|
|
166
|
-
),
|
|
167
|
-
fallback
|
|
168
|
-
)
|
|
142
|
+
return searchDefaultValueInitAST(usage.searchDefaultValue)
|
|
169
143
|
}
|
|
170
144
|
|
|
171
145
|
// ==================== UIDL-FIRST STATE MANAGEMENT ====================
|
|
@@ -213,10 +187,11 @@ interface DataSourceUsage {
|
|
|
213
187
|
// Dynamic single-column sort bound to component state (set when legacy sorts empty
|
|
214
188
|
// and cms-list-repeater declares sort/sortDirection fields)
|
|
215
189
|
dynamicSort?: DynamicSortAST
|
|
216
|
-
// Filters from resource params
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
190
|
+
// Filters from resource params, as the TREE the inspector authored: condition
|
|
191
|
+
// leaves and `{ type: 'group', operator, children }` entries (see
|
|
192
|
+
// `readFilterTree`). Anything that needs the leaves walks the tree with
|
|
193
|
+
// `collectFilterLeafConditions`; the emit sites write the tree out whole, so
|
|
194
|
+
// an `or` group reaches the data source as an `or`.
|
|
220
195
|
// tslint:disable-next-line:no-any
|
|
221
196
|
filters: any[]
|
|
222
197
|
// State IDs from dynamic filter destinations (for useMemo dependencies).
|
|
@@ -272,53 +247,26 @@ interface StateRegistry {
|
|
|
272
247
|
byArrayMapperRenderProp: Map<string, DataSourceUsage>
|
|
273
248
|
}
|
|
274
249
|
|
|
275
|
-
// The UIDL's `filters.content` array
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
//
|
|
286
|
-
// `or`-grouped conditions are flattened just like `and`-grouped ones — the
|
|
287
|
-
// downstream SQL builder always joins flat filters with `AND`, so emitting
|
|
288
|
-
// the conditions side-by-side approximates AND semantics. A future API
|
|
289
|
-
// upgrade that respects group operators would key off the original tree
|
|
290
|
-
// directly; until then, AND-flattening is the closest correct behaviour and
|
|
291
|
-
// matches what the inspector preview shows for the common single-group case
|
|
292
|
-
// the GUI emits today.
|
|
293
|
-
function flattenFilterGroups(filters: any[]): any[] {
|
|
250
|
+
// The UIDL's `filters.content` array is the tree the inspector authored:
|
|
251
|
+
// `{ type: 'group', operator: 'and' | 'or', children: [...] }` entries around
|
|
252
|
+
// `{ source, destination, operand }` conditions, nested as deep as the user
|
|
253
|
+
// grouped them. The tree is kept as it is — the generated fetchers read it with
|
|
254
|
+
// `normalizeFilterTree`, so an `or` group stays an `or`. Only junk is dropped:
|
|
255
|
+
// an entry that is neither a group nor a condition (a half-built inspector
|
|
256
|
+
// row) would otherwise reach the API as `{ source: '', destination: '' }`.
|
|
257
|
+
// Entries without an explicit `type` are the legacy flat form and are
|
|
258
|
+
// conditions.
|
|
259
|
+
function readFilterTree(filters: any[]): any[] {
|
|
294
260
|
if (!Array.isArray(filters)) {
|
|
295
261
|
return []
|
|
296
262
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
if (entry.type === 'group' && Array.isArray(entry.children)) {
|
|
303
|
-
for (const child of entry.children) {
|
|
304
|
-
walk(child)
|
|
305
|
-
}
|
|
306
|
-
return
|
|
307
|
-
}
|
|
308
|
-
// Backwards-compatible: entries without an explicit `type` (legacy flat
|
|
309
|
-
// form) and entries explicitly tagged `condition` are both treated as
|
|
310
|
-
// condition leaves.
|
|
311
|
-
if (entry.type === undefined || entry.type === 'condition') {
|
|
312
|
-
out.push(entry)
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
for (const entry of filters) {
|
|
316
|
-
walk(entry)
|
|
317
|
-
}
|
|
318
|
-
return out
|
|
263
|
+
return pruneFilterTree(
|
|
264
|
+
filters,
|
|
265
|
+
(entry: any) => entry.type === undefined || entry.type === 'condition'
|
|
266
|
+
)
|
|
319
267
|
}
|
|
320
268
|
|
|
321
|
-
// Walks
|
|
269
|
+
// Walks the filter tree to extract every `urlSearchParams` reference key
|
|
322
270
|
// (e.g. `'categoryFilter'`). Used to (a) inject `const router = useRouter()`
|
|
323
271
|
// once at the top of the component, (b) wire `router.query.<key>` into the
|
|
324
272
|
// `useMemo` dependency array so the client-side fetch reruns whenever the
|
|
@@ -327,7 +275,7 @@ function flattenFilterGroups(filters: any[]): any[] {
|
|
|
327
275
|
function collectFilterUrlSearchParamKeys(filters: any[]): string[] {
|
|
328
276
|
const seen = new Set<string>()
|
|
329
277
|
const out: string[] = []
|
|
330
|
-
for (const f of filters) {
|
|
278
|
+
for (const f of collectFilterLeafConditions(filters) as any[]) {
|
|
331
279
|
const dest = f?.destination
|
|
332
280
|
if (!ASTUtils.isUIDLDynamicReference(dest)) {
|
|
333
281
|
continue
|
|
@@ -345,6 +293,16 @@ function collectFilterUrlSearchParamKeys(filters: any[]): string[] {
|
|
|
345
293
|
return out
|
|
346
294
|
}
|
|
347
295
|
|
|
296
|
+
// The filter tree with every dynamic destination pruned — what `getStaticProps`
|
|
297
|
+
// can resolve at build time. Groups keep their operator; a group left empty by
|
|
298
|
+
// the pruning disappears with it (`pruneFilterTree`).
|
|
299
|
+
function staticFiltersOf(usage: DataSourceUsage): any[] {
|
|
300
|
+
return pruneFilterTree(
|
|
301
|
+
usage.filters || [],
|
|
302
|
+
(f: any) => !ASTUtils.isUIDLDynamicReference(f.destination)
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
348
306
|
// Returns true when any filter destination is a `urlSearchParams` dynamic
|
|
349
307
|
// reference — used by the plugin to know it needs to import `useRouter` and
|
|
350
308
|
// emit `const router = useRouter()` at the top of the component. Cheaper to
|
|
@@ -550,23 +508,28 @@ function buildStateRegistry(uidlNode: any): StateRegistry {
|
|
|
550
508
|
sorts = parentDataSource.resourceParams.sorts.content
|
|
551
509
|
}
|
|
552
510
|
|
|
553
|
-
// If legacy sorts aren't set, fall back to the
|
|
554
|
-
//
|
|
511
|
+
// If legacy sorts aren't set, fall back to the single-column sort fields
|
|
512
|
+
// on the cms-list-repeater. A FIXED one (a column chosen in the editor,
|
|
513
|
+
// e.g. a blog listing's newest first) is a static sort: read as dynamic,
|
|
514
|
+
// it made the list drop the server-side first page (`initialData` below
|
|
515
|
+
// is skipped for a dynamic sort) — the pre-rendered HTML carried no
|
|
516
|
+
// posts, and the browser fetched page 1 a second time. Only a sort
|
|
517
|
+
// bound to state (the admin panel's sortable columns) is dynamic.
|
|
555
518
|
let dynamicSort: DynamicSortAST | undefined
|
|
556
519
|
if ((!sorts || sorts.length === 0) && content.sort) {
|
|
557
|
-
|
|
520
|
+
const fixedSort = readFixedSort(content.sort, content.sortDirection)
|
|
521
|
+
if (fixedSort) {
|
|
522
|
+
sorts = [fixedSort]
|
|
523
|
+
} else {
|
|
524
|
+
dynamicSort = extractDynamicSort(content.sort, content.sortDirection)
|
|
525
|
+
}
|
|
558
526
|
}
|
|
559
527
|
|
|
560
|
-
// Extract filters from parent's resource params
|
|
561
|
-
//
|
|
562
|
-
// nested), so flatten to leaf conditions before the downstream emit
|
|
563
|
-
// sites consume `.source` / `.destination` / `.operand` directly —
|
|
564
|
-
// they expect a flat condition array. See `flattenFilterGroups`'s
|
|
565
|
-
// header comment for why AND-flattening is the correct fallback for
|
|
566
|
-
// the API endpoint's flat-condition contract.
|
|
528
|
+
// Extract filters from parent's resource params, keeping the group tree
|
|
529
|
+
// the inspector authored (see `readFilterTree`).
|
|
567
530
|
let filters: any[] = []
|
|
568
531
|
if (parentDataSource.resourceParams?.filters?.content) {
|
|
569
|
-
filters =
|
|
532
|
+
filters = readFilterTree(parentDataSource.resourceParams.filters.content)
|
|
570
533
|
}
|
|
571
534
|
|
|
572
535
|
// Split dynamic destination keys by reference type. `state`/`prop`
|
|
@@ -575,7 +538,7 @@ function buildStateRegistry(uidlNode: any): StateRegistry {
|
|
|
575
538
|
// `useRouter()` declaration injected separately.
|
|
576
539
|
const filterStateIds: string[] = []
|
|
577
540
|
const filterPropIds: string[] = []
|
|
578
|
-
for (const f of filters) {
|
|
541
|
+
for (const f of collectFilterLeafConditions(filters) as any[]) {
|
|
579
542
|
if (!ASTUtils.isUIDLDynamicReference(f.destination)) {
|
|
580
543
|
continue
|
|
581
544
|
}
|
|
@@ -838,21 +801,118 @@ function pushSearchUrlSyncEffects(
|
|
|
838
801
|
)
|
|
839
802
|
: types.identifier(vars.debouncedSearchQueryVar)
|
|
840
803
|
|
|
804
|
+
// The query starts at its default (see `buildSearchQueryInitAST`), so the
|
|
805
|
+
// pair must agree on who goes first: the read-back adopts the address, THEN
|
|
806
|
+
// the write-back may write — before that, its first run would delete the very
|
|
807
|
+
// `?<key>=` the read-back is about to adopt.
|
|
808
|
+
const adoptedRefVar = `ds_${usage.index}_searchUrlAdopted`
|
|
809
|
+
const adopterVar = `ds_${usage.index}_adoptUrlSearch`
|
|
810
|
+
effectStatements.push(
|
|
811
|
+
types.variableDeclaration('const', [
|
|
812
|
+
types.variableDeclarator(
|
|
813
|
+
types.identifier(adoptedRefVar),
|
|
814
|
+
types.callExpression(types.identifier('useRef'), [types.booleanLiteral(false)])
|
|
815
|
+
),
|
|
816
|
+
]),
|
|
817
|
+
buildUrlSearchAdopter(adopterVar, usage, vars)
|
|
818
|
+
)
|
|
819
|
+
|
|
841
820
|
// write-back: debounced query → `?<searchUrlParamKey>=`
|
|
842
821
|
effectStatements.push(
|
|
843
822
|
URLSearchParamSync.buildUrlWriteBackEffect(
|
|
844
823
|
usage.searchUrlParamKey,
|
|
845
824
|
buildDebouncedValueExpr(),
|
|
846
|
-
buildDebouncedValueExpr()
|
|
825
|
+
buildDebouncedValueExpr(),
|
|
826
|
+
undefined,
|
|
827
|
+
adoptedRefVar
|
|
847
828
|
)
|
|
848
829
|
)
|
|
849
|
-
// read-back: `?<searchUrlParamKey>=` → input
|
|
850
|
-
//
|
|
830
|
+
// read-back: `?<searchUrlParamKey>=` → the input AND the applied query at once
|
|
831
|
+
// (`ds_N_adoptUrlSearch`), so an address is not a typed change: it skips the
|
|
832
|
+
// debounce and keeps the page — `?searchKeyword=x&page=3` lands on page 3.
|
|
851
833
|
effectStatements.push(
|
|
852
|
-
URLSearchParamSync.buildUrlReadBackEffect(
|
|
834
|
+
URLSearchParamSync.buildUrlReadBackEffect(
|
|
835
|
+
usage.searchUrlParamKey,
|
|
836
|
+
adopterVar,
|
|
837
|
+
undefined,
|
|
838
|
+
adoptedRefVar
|
|
839
|
+
)
|
|
853
840
|
)
|
|
854
841
|
}
|
|
855
842
|
|
|
843
|
+
// `const ds_N_adoptUrlSearch = (update) => { … }`: the setter the search's URL
|
|
844
|
+
// read-back calls with its `(prev) => next` updater. It moves the input and the
|
|
845
|
+
// applied (debounced) query together; the combined paginated state keeps its
|
|
846
|
+
// page, which a typed change resets to 1 in the debounce effect.
|
|
847
|
+
function buildUrlSearchAdopter(
|
|
848
|
+
adopterVar: string,
|
|
849
|
+
usage: DataSourceUsage,
|
|
850
|
+
vars: ReturnType<typeof getStateVarsForUsage>
|
|
851
|
+
): types.VariableDeclaration {
|
|
852
|
+
const update = types.identifier('update')
|
|
853
|
+
const applyToDebounced =
|
|
854
|
+
usage.category === 'paginated+search'
|
|
855
|
+
? // setDs_N_state((state) => {
|
|
856
|
+
// const next = update(state.debouncedQuery)
|
|
857
|
+
// return next === state.debouncedQuery ? state : { ...state, debouncedQuery: next }
|
|
858
|
+
// })
|
|
859
|
+
types.callExpression(types.identifier(vars.setCombinedStateVar), [
|
|
860
|
+
types.arrowFunctionExpression(
|
|
861
|
+
[types.identifier('state')],
|
|
862
|
+
types.blockStatement([
|
|
863
|
+
types.variableDeclaration('const', [
|
|
864
|
+
types.variableDeclarator(
|
|
865
|
+
types.identifier('next'),
|
|
866
|
+
types.callExpression(types.identifier('update'), [
|
|
867
|
+
types.memberExpression(
|
|
868
|
+
types.identifier('state'),
|
|
869
|
+
types.identifier('debouncedQuery')
|
|
870
|
+
),
|
|
871
|
+
])
|
|
872
|
+
),
|
|
873
|
+
]),
|
|
874
|
+
types.returnStatement(
|
|
875
|
+
types.conditionalExpression(
|
|
876
|
+
types.binaryExpression(
|
|
877
|
+
'===',
|
|
878
|
+
types.identifier('next'),
|
|
879
|
+
types.memberExpression(
|
|
880
|
+
types.identifier('state'),
|
|
881
|
+
types.identifier('debouncedQuery')
|
|
882
|
+
)
|
|
883
|
+
),
|
|
884
|
+
types.identifier('state'),
|
|
885
|
+
types.objectExpression([
|
|
886
|
+
types.spreadElement(types.identifier('state')),
|
|
887
|
+
types.objectProperty(
|
|
888
|
+
types.identifier('debouncedQuery'),
|
|
889
|
+
types.identifier('next')
|
|
890
|
+
),
|
|
891
|
+
])
|
|
892
|
+
)
|
|
893
|
+
),
|
|
894
|
+
])
|
|
895
|
+
),
|
|
896
|
+
])
|
|
897
|
+
: types.callExpression(types.identifier(vars.setDebouncedSearchQueryVar), [update])
|
|
898
|
+
return types.variableDeclaration('const', [
|
|
899
|
+
types.variableDeclarator(
|
|
900
|
+
types.identifier(adopterVar),
|
|
901
|
+
types.arrowFunctionExpression(
|
|
902
|
+
[types.identifier('update')],
|
|
903
|
+
types.blockStatement([
|
|
904
|
+
types.expressionStatement(
|
|
905
|
+
types.callExpression(types.identifier(vars.setSearchQueryVar), [
|
|
906
|
+
types.identifier('update'),
|
|
907
|
+
])
|
|
908
|
+
),
|
|
909
|
+
types.expressionStatement(applyToDebounced),
|
|
910
|
+
])
|
|
911
|
+
)
|
|
912
|
+
),
|
|
913
|
+
])
|
|
914
|
+
}
|
|
915
|
+
|
|
856
916
|
// How one usage reads and writes its current page.
|
|
857
917
|
//
|
|
858
918
|
// A paginated+search list keeps the page inside a combined
|
|
@@ -1258,13 +1318,12 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
|
|
|
1258
1318
|
])
|
|
1259
1319
|
)
|
|
1260
1320
|
|
|
1261
|
-
// Immediate search query state — seeded with `searchDefaultValue
|
|
1262
|
-
// the
|
|
1263
|
-
//
|
|
1264
|
-
//
|
|
1265
|
-
//
|
|
1266
|
-
//
|
|
1267
|
-
// DataProvider to fetch instead of reusing the unfiltered SSG prefetch.
|
|
1321
|
+
// Immediate search query state — seeded with `searchDefaultValue`, like
|
|
1322
|
+
// the debounced value above. A deep-linked `?searchKeyword=` is adopted
|
|
1323
|
+
// after mount by the URL read-back, which sets both at once; the
|
|
1324
|
+
// paginated+search `initialData` is gated on `!ds_N_state.debouncedQuery`,
|
|
1325
|
+
// so the adopted query makes the DataProvider fetch filtered rows
|
|
1326
|
+
// instead of reusing the unfiltered SSG prefetch the first render shows.
|
|
1268
1327
|
stateDeclarations.push(
|
|
1269
1328
|
types.variableDeclaration('const', [
|
|
1270
1329
|
types.variableDeclarator(
|
|
@@ -2736,6 +2795,15 @@ function updateDataProviderForPlain(
|
|
|
2736
2795
|
usage: DataSourceUsage,
|
|
2737
2796
|
localized: boolean
|
|
2738
2797
|
): void {
|
|
2798
|
+
// A sort bound to state is the visitor's, which the server cannot know: rows
|
|
2799
|
+
// pre-rendered in the table's order would stand in for it, since a seeded
|
|
2800
|
+
// provider skips its first fetch — the paginated lists drop them the same way.
|
|
2801
|
+
if (usage.dynamicSort) {
|
|
2802
|
+
dp.openingElement.attributes = dp.openingElement.attributes.filter(
|
|
2803
|
+
(attr: any) => !(attr.type === 'JSXAttribute' && attr.name?.name === 'initialData')
|
|
2804
|
+
)
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2739
2807
|
const attrs = dp.openingElement.attributes
|
|
2740
2808
|
|
|
2741
2809
|
const clientCache = usage.cache?.client ? usage.cache : undefined
|
|
@@ -2774,13 +2842,15 @@ function updateDataProviderForPlain(
|
|
|
2774
2842
|
|
|
2775
2843
|
// Find the params attribute. A plain list may have none at all — but the
|
|
2776
2844
|
// fetch it was just given sends `params` regardless, and in a localized
|
|
2777
|
-
// project those have to carry the locale,
|
|
2845
|
+
// project those have to carry the locale, and a sorted list its sort, so the
|
|
2846
|
+
// attribute is created.
|
|
2778
2847
|
let paramsAttrIndex = attrs.findIndex(
|
|
2779
2848
|
(attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'params'
|
|
2780
2849
|
)
|
|
2850
|
+
const isSorted = usage.sorts.length > 0 || !!usage.dynamicSort
|
|
2781
2851
|
|
|
2782
2852
|
if (paramsAttrIndex === -1) {
|
|
2783
|
-
if (!localized) {
|
|
2853
|
+
if (!localized && !isSorted) {
|
|
2784
2854
|
return
|
|
2785
2855
|
}
|
|
2786
2856
|
attrs.push(
|
|
@@ -2814,10 +2884,22 @@ function updateDataProviderForPlain(
|
|
|
2814
2884
|
return
|
|
2815
2885
|
}
|
|
2816
2886
|
|
|
2817
|
-
//
|
|
2887
|
+
// The repeater's sort, which the paginated and searchable lists add to the
|
|
2888
|
+
// params they rebuild: kept as they come, a plain list's params never
|
|
2889
|
+
// carried it, and the rows arrived in the table's own order.
|
|
2890
|
+
if (paramsExpression.type === 'ObjectExpression' && !hasObjectKey(paramsExpression, 'sorts')) {
|
|
2891
|
+
appendSortsParam(
|
|
2892
|
+
paramsExpression.properties as types.ObjectProperty[],
|
|
2893
|
+
usage.sorts,
|
|
2894
|
+
usage.dynamicSort
|
|
2895
|
+
)
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
// Build useMemo dependencies including filter state IDs and a dynamic sort's state
|
|
2818
2899
|
const memoDeps: types.Expression[] = []
|
|
2819
2900
|
const memoSeen = new Set<string>()
|
|
2820
2901
|
pushStateIdsAsDeps(memoDeps, memoSeen, usage.filterStateIds)
|
|
2902
|
+
pushStateIdsAsDeps(memoDeps, memoSeen, usage.dynamicSort?.depStateIds ?? [])
|
|
2821
2903
|
pushPropIdsAsDeps(memoDeps, memoSeen, usage.filterPropIds)
|
|
2822
2904
|
pushUrlSearchParamMemoDeps(memoDeps, usage)
|
|
2823
2905
|
if (localized && paramsExpression.type === 'ObjectExpression') {
|
|
@@ -2837,6 +2919,14 @@ function updateDataProviderForPlain(
|
|
|
2837
2919
|
)
|
|
2838
2920
|
}
|
|
2839
2921
|
|
|
2922
|
+
const hasObjectKey = (object: types.ObjectExpression, key: string): boolean =>
|
|
2923
|
+
object.properties.some(
|
|
2924
|
+
(property) =>
|
|
2925
|
+
property.type === 'ObjectProperty' &&
|
|
2926
|
+
((property.key.type === 'Identifier' && property.key.name === key) ||
|
|
2927
|
+
(property.key.type === 'StringLiteral' && property.key.value === key))
|
|
2928
|
+
)
|
|
2929
|
+
|
|
2840
2930
|
function stabilizeDataProviderWithoutRepeater(dp: any): void {
|
|
2841
2931
|
const attrs = dp.openingElement.attributes
|
|
2842
2932
|
|
|
@@ -3650,38 +3740,12 @@ function updateGetStaticProps(
|
|
|
3650
3740
|
|
|
3651
3741
|
// Add filters if present
|
|
3652
3742
|
// Filter out dynamic destinations for getStaticProps (they can't be resolved at build time)
|
|
3653
|
-
const staticFilters = (usage
|
|
3654
|
-
(f: any) => !ASTUtils.isUIDLDynamicReference(f.destination)
|
|
3655
|
-
)
|
|
3743
|
+
const staticFilters = staticFiltersOf(usage)
|
|
3656
3744
|
if (staticFilters.length > 0) {
|
|
3657
3745
|
fetchParams.push(
|
|
3658
3746
|
types.objectProperty(
|
|
3659
3747
|
types.identifier('filters'),
|
|
3660
|
-
|
|
3661
|
-
types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
|
|
3662
|
-
[
|
|
3663
|
-
types.arrayExpression(
|
|
3664
|
-
staticFilters.map((filter: any) =>
|
|
3665
|
-
types.objectExpression([
|
|
3666
|
-
types.objectProperty(
|
|
3667
|
-
types.identifier('source'),
|
|
3668
|
-
types.stringLiteral(filter.source || '')
|
|
3669
|
-
),
|
|
3670
|
-
types.objectProperty(
|
|
3671
|
-
types.identifier('destination'),
|
|
3672
|
-
types.stringLiteral(
|
|
3673
|
-
typeof filter.destination === 'string' ? filter.destination : ''
|
|
3674
|
-
)
|
|
3675
|
-
),
|
|
3676
|
-
types.objectProperty(
|
|
3677
|
-
types.identifier('operand'),
|
|
3678
|
-
types.stringLiteral(filter.operand || '')
|
|
3679
|
-
),
|
|
3680
|
-
])
|
|
3681
|
-
)
|
|
3682
|
-
),
|
|
3683
|
-
]
|
|
3684
|
-
)
|
|
3748
|
+
buildStaticFiltersStringifyCall(staticFilters)
|
|
3685
3749
|
)
|
|
3686
3750
|
)
|
|
3687
3751
|
}
|
|
@@ -3766,9 +3830,7 @@ function updateGetStaticProps(
|
|
|
3766
3830
|
const maxPagesPropName = `${vars.propsPrefix}_maxPages`
|
|
3767
3831
|
// Use filter-specific count variable if usage has static filters
|
|
3768
3832
|
// (dynamic filters can't be resolved at build time)
|
|
3769
|
-
const staticFiltersForCount = (usage
|
|
3770
|
-
(f: any) => !ASTUtils.isUIDLDynamicReference(f.destination)
|
|
3771
|
-
)
|
|
3833
|
+
const staticFiltersForCount = staticFiltersOf(usage)
|
|
3772
3834
|
// A count scoped to an inner array is a different number from the
|
|
3773
3835
|
// whole-payload one, so it gets its own variable just like a filtered one.
|
|
3774
3836
|
const hasScopedCount = staticFiltersForCount.length > 0 || !!usage.itemsPath
|
|
@@ -3854,9 +3916,7 @@ function updateGetStaticProps(
|
|
|
3854
3916
|
usagesByFilters.forEach((usages, filtersKey) => {
|
|
3855
3917
|
const firstUsage = usages[0]
|
|
3856
3918
|
// Filter out dynamic destinations for getStaticProps (they can't be resolved at build time)
|
|
3857
|
-
const staticFiltersForCount = (firstUsage
|
|
3858
|
-
(f: any) => !ASTUtils.isUIDLDynamicReference(f.destination)
|
|
3859
|
-
)
|
|
3919
|
+
const staticFiltersForCount = staticFiltersOf(firstUsage)
|
|
3860
3920
|
const hasStaticFilters = staticFiltersForCount.length > 0
|
|
3861
3921
|
|
|
3862
3922
|
// Create unique count variable name based on filters (and inner-array
|
|
@@ -3887,31 +3947,7 @@ function updateGetStaticProps(
|
|
|
3887
3947
|
countParams.push(
|
|
3888
3948
|
types.objectProperty(
|
|
3889
3949
|
types.identifier('filters'),
|
|
3890
|
-
|
|
3891
|
-
types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
|
|
3892
|
-
[
|
|
3893
|
-
types.arrayExpression(
|
|
3894
|
-
staticFiltersForCount.map((f: any) =>
|
|
3895
|
-
types.objectExpression([
|
|
3896
|
-
types.objectProperty(
|
|
3897
|
-
types.identifier('source'),
|
|
3898
|
-
types.stringLiteral(f.source || '')
|
|
3899
|
-
),
|
|
3900
|
-
types.objectProperty(
|
|
3901
|
-
types.identifier('destination'),
|
|
3902
|
-
types.stringLiteral(
|
|
3903
|
-
typeof f.destination === 'string' ? f.destination : ''
|
|
3904
|
-
)
|
|
3905
|
-
),
|
|
3906
|
-
types.objectProperty(
|
|
3907
|
-
types.identifier('operand'),
|
|
3908
|
-
types.stringLiteral(f.operand || '')
|
|
3909
|
-
),
|
|
3910
|
-
])
|
|
3911
|
-
)
|
|
3912
|
-
),
|
|
3913
|
-
]
|
|
3914
|
-
)
|
|
3950
|
+
buildStaticFiltersStringifyCall(staticFiltersForCount)
|
|
3915
3951
|
)
|
|
3916
3952
|
)
|
|
3917
3953
|
}
|
|
@@ -191,14 +191,3 @@ export const generateSortFallbackFieldHelper = (tableName: string): string => {
|
|
|
191
191
|
|
|
192
192
|
return ['function sortFallbackField(field) {', branches + ' return field', '}'].join('\n')
|
|
193
193
|
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* A deterministic tiebreaker appended to every products ORDER BY.
|
|
197
|
-
*
|
|
198
|
-
* Without one, a paginated sort with ties — and "sort by discount" ties every
|
|
199
|
-
* undiscounted product at zero — has no defined order between equal rows, so
|
|
200
|
-
* the same product can appear on two pages or on none. `id` is on the products
|
|
201
|
-
* table from its first migration.
|
|
202
|
-
*/
|
|
203
|
-
export const generateSortTiebreakSql = (tableName: string): string =>
|
|
204
|
-
supportsProductPriceSort(tableName) ? ', "id" ASC' : ''
|
package/src/row-order.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { supportsProductPriceSort } from './product-price-sort'
|
|
2
|
+
|
|
3
|
+
interface ColumnLike {
|
|
4
|
+
name?: unknown
|
|
5
|
+
isPrimaryKey?: unknown
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The column that tells a table's rows apart, read from the table the project
|
|
10
|
+
* saved: its primary key, else a column named `id`. The products table has `id`
|
|
11
|
+
* from its first migration. Null when neither is known — then nothing is added,
|
|
12
|
+
* rather than a column that may not exist.
|
|
13
|
+
*/
|
|
14
|
+
export const rowIdentityColumn = (
|
|
15
|
+
config: Record<string, unknown>,
|
|
16
|
+
tableName: string
|
|
17
|
+
): string | null => {
|
|
18
|
+
const selectedTables = (config as { selectedTables?: Record<string, { columns?: unknown }> })
|
|
19
|
+
.selectedTables
|
|
20
|
+
const saved = selectedTables?.[tableName]?.columns
|
|
21
|
+
const columns = (Array.isArray(saved) ? (saved as ColumnLike[]) : []).filter(
|
|
22
|
+
(column) => typeof column?.name === 'string'
|
|
23
|
+
)
|
|
24
|
+
const key =
|
|
25
|
+
columns.find((column) => column.isPrimaryKey === true)?.name ??
|
|
26
|
+
columns.find((column) => column.name === 'id')?.name
|
|
27
|
+
if (typeof key === 'string') {
|
|
28
|
+
return key
|
|
29
|
+
}
|
|
30
|
+
return supportsProductPriceSort(tableName) ? 'id' : null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The `rowOrderTiebreak(fields, direction)` helper a SQL fetcher embeds: what
|
|
35
|
+
* ends an ORDER BY so that it orders every row.
|
|
36
|
+
*
|
|
37
|
+
* Rows that share every requested key have no order of their own, and the
|
|
38
|
+
* database deals them out differently for each page — "sort by discount" ties
|
|
39
|
+
* every undiscounted product at zero, and an import dates hundreds of articles
|
|
40
|
+
* alike — so a paginated list repeated some rows and never showed others.
|
|
41
|
+
* The tie follows the last key's direction, as the editor's canvas does.
|
|
42
|
+
*/
|
|
43
|
+
export const generateRowOrderTiebreakHelper = (identityColumn: string | null): string => {
|
|
44
|
+
if (!identityColumn) {
|
|
45
|
+
return "const rowOrderTiebreak = () => ''"
|
|
46
|
+
}
|
|
47
|
+
const quoted = `"${identityColumn.replace(/"/g, '""')}"`
|
|
48
|
+
return [
|
|
49
|
+
'const rowOrderTiebreak = (fields, direction) =>',
|
|
50
|
+
` fields.includes(${JSON.stringify(identityColumn)}) ? '' : ${JSON.stringify(
|
|
51
|
+
`, ${quoted} `
|
|
52
|
+
)} + direction`,
|
|
53
|
+
].join('\n')
|
|
54
|
+
}
|
package/src/sort-utils.ts
CHANGED
|
@@ -63,6 +63,31 @@ const toExpressionAndDeps = (
|
|
|
63
63
|
return { expr: parsed, deps: Array.from(deps) }
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The `{ field, order }` of a sort the UIDL FIXES — a static column and a
|
|
68
|
+
* static direction (or none: ascending) — or undefined for one bound to state.
|
|
69
|
+
* A fixed sort is a static sort: it rides on the server-side first-page fetch
|
|
70
|
+
* and the client keeps that page, which a dynamic sort cannot let it do.
|
|
71
|
+
*/
|
|
72
|
+
export const readFixedSort = (
|
|
73
|
+
sort: UIDLStaticValue | UIDLExpressionValue | undefined,
|
|
74
|
+
sortDirection: UIDLStaticValue | UIDLExpressionValue | undefined
|
|
75
|
+
): { field: string; order: 'asc' | 'desc' } | undefined => {
|
|
76
|
+
if (sort?.type !== 'static' || typeof sort.content !== 'string' || !sort.content.trim()) {
|
|
77
|
+
return undefined
|
|
78
|
+
}
|
|
79
|
+
if (
|
|
80
|
+
sortDirection &&
|
|
81
|
+
(sortDirection.type !== 'static' || typeof sortDirection.content !== 'string')
|
|
82
|
+
) {
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
const direction = String(sortDirection?.content ?? '')
|
|
86
|
+
.trim()
|
|
87
|
+
.toLowerCase()
|
|
88
|
+
return { field: sort.content.trim(), order: direction.startsWith('desc') ? 'desc' : 'asc' }
|
|
89
|
+
}
|
|
90
|
+
|
|
66
91
|
export const extractDynamicSort = (
|
|
67
92
|
sort: UIDLStaticValue | UIDLExpressionValue | undefined,
|
|
68
93
|
sortDirection: UIDLStaticValue | UIDLExpressionValue | undefined
|