@teleporthq/teleport-plugin-next-data-source 0.43.75 → 0.44.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/__tests__/_helpers/pagination-widget-fixture.ts +87 -4
- package/__tests__/booking-item-transform.test.ts +120 -0
- package/__tests__/browser-row-policy.test.ts +8 -2
- package/__tests__/fetchers.test.ts +1 -1
- package/__tests__/fixed-list-sort.test.ts +1 -1
- package/__tests__/infinite-scroll.test.ts +154 -5
- package/__tests__/list-sort-order.test.ts +61 -0
- package/__tests__/null-filter-where.test.ts +132 -0
- package/__tests__/pg-date-parser.test.ts +73 -0
- package/__tests__/plain-list-sort.test.ts +1 -1
- package/__tests__/product-price-sort.ts +11 -6
- package/__tests__/range-control.test.ts +159 -0
- package/__tests__/search-input-marker.test.ts +86 -0
- package/__tests__/search-url-sync.test.ts +34 -8
- package/__tests__/table-read-route-protected-tables.test.ts +33 -3
- package/dist/cjs/count-effect.d.ts +7 -1
- package/dist/cjs/count-effect.d.ts.map +1 -1
- package/dist/cjs/count-effect.js +15 -5
- package/dist/cjs/count-effect.js.map +1 -1
- package/dist/cjs/fetchers/pg-client-import.d.ts +12 -0
- package/dist/cjs/fetchers/pg-client-import.d.ts.map +1 -0
- package/dist/cjs/fetchers/pg-client-import.js +15 -0
- package/dist/cjs/fetchers/pg-client-import.js.map +1 -0
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
- package/dist/cjs/fetchers/postgresql.js +3 -1
- package/dist/cjs/fetchers/postgresql.js.map +1 -1
- package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
- package/dist/cjs/fetchers/raw-query.js +2 -1
- package/dist/cjs/fetchers/raw-query.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +4 -2
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/infinite-scroll.d.ts +25 -2
- package/dist/cjs/infinite-scroll.d.ts.map +1 -1
- package/dist/cjs/infinite-scroll.js +144 -33
- package/dist/cjs/infinite-scroll.js.map +1 -1
- package/dist/cjs/list-sort-order.d.ts +14 -0
- package/dist/cjs/list-sort-order.d.ts.map +1 -0
- package/dist/cjs/list-sort-order.js +36 -0
- package/dist/cjs/list-sort-order.js.map +1 -0
- package/dist/cjs/pagination-controls.d.ts +22 -3
- package/dist/cjs/pagination-controls.d.ts.map +1 -1
- package/dist/cjs/pagination-controls.js +29 -3
- package/dist/cjs/pagination-controls.js.map +1 -1
- package/dist/cjs/pagination-plugin.d.ts.map +1 -1
- package/dist/cjs/pagination-plugin.js +137 -101
- package/dist/cjs/pagination-plugin.js.map +1 -1
- package/dist/cjs/range-control.d.ts +45 -0
- package/dist/cjs/range-control.d.ts.map +1 -0
- package/dist/cjs/range-control.js +165 -0
- package/dist/cjs/range-control.js.map +1 -0
- package/dist/cjs/row-order.d.ts +3 -2
- package/dist/cjs/row-order.d.ts.map +1 -1
- package/dist/cjs/row-order.js +5 -4
- package/dist/cjs/row-order.js.map +1 -1
- package/dist/cjs/search-input.d.ts +42 -0
- package/dist/cjs/search-input.d.ts.map +1 -0
- package/dist/cjs/search-input.js +158 -0
- package/dist/cjs/search-input.js.map +1 -0
- package/dist/cjs/transformations/booking-item.d.ts +24 -0
- package/dist/cjs/transformations/booking-item.d.ts.map +1 -0
- package/dist/cjs/transformations/booking-item.js +27 -0
- package/dist/cjs/transformations/booking-item.js.map +1 -0
- package/dist/cjs/transformations/index.d.ts +4 -2
- package/dist/cjs/transformations/index.d.ts.map +1 -1
- package/dist/cjs/transformations/index.js +14 -5
- package/dist/cjs/transformations/index.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/count-effect.d.ts +7 -1
- package/dist/esm/count-effect.d.ts.map +1 -1
- package/dist/esm/count-effect.js +15 -5
- package/dist/esm/count-effect.js.map +1 -1
- package/dist/esm/fetchers/pg-client-import.d.ts +12 -0
- package/dist/esm/fetchers/pg-client-import.d.ts.map +1 -0
- package/dist/esm/fetchers/pg-client-import.js +12 -0
- package/dist/esm/fetchers/pg-client-import.js.map +1 -0
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
- package/dist/esm/fetchers/postgresql.js +3 -1
- package/dist/esm/fetchers/postgresql.js.map +1 -1
- package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
- package/dist/esm/fetchers/raw-query.js +2 -1
- package/dist/esm/fetchers/raw-query.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +4 -2
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/infinite-scroll.d.ts +25 -2
- package/dist/esm/infinite-scroll.d.ts.map +1 -1
- package/dist/esm/infinite-scroll.js +142 -32
- package/dist/esm/infinite-scroll.js.map +1 -1
- package/dist/esm/list-sort-order.d.ts +14 -0
- package/dist/esm/list-sort-order.d.ts.map +1 -0
- package/dist/esm/list-sort-order.js +32 -0
- package/dist/esm/list-sort-order.js.map +1 -0
- package/dist/esm/pagination-controls.d.ts +22 -3
- package/dist/esm/pagination-controls.d.ts.map +1 -1
- package/dist/esm/pagination-controls.js +28 -2
- package/dist/esm/pagination-controls.js.map +1 -1
- package/dist/esm/pagination-plugin.d.ts.map +1 -1
- package/dist/esm/pagination-plugin.js +138 -102
- package/dist/esm/pagination-plugin.js.map +1 -1
- package/dist/esm/range-control.d.ts +45 -0
- package/dist/esm/range-control.d.ts.map +1 -0
- package/dist/esm/range-control.js +137 -0
- package/dist/esm/range-control.js.map +1 -0
- package/dist/esm/row-order.d.ts +3 -2
- package/dist/esm/row-order.d.ts.map +1 -1
- package/dist/esm/row-order.js +5 -4
- package/dist/esm/row-order.js.map +1 -1
- package/dist/esm/search-input.d.ts +42 -0
- package/dist/esm/search-input.d.ts.map +1 -0
- package/dist/esm/search-input.js +129 -0
- package/dist/esm/search-input.js.map +1 -0
- package/dist/esm/transformations/booking-item.d.ts +24 -0
- package/dist/esm/transformations/booking-item.d.ts.map +1 -0
- package/dist/esm/transformations/booking-item.js +23 -0
- package/dist/esm/transformations/booking-item.js.map +1 -0
- package/dist/esm/transformations/index.d.ts +4 -2
- package/dist/esm/transformations/index.d.ts.map +1 -1
- package/dist/esm/transformations/index.js +14 -5
- package/dist/esm/transformations/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/count-effect.ts +31 -8
- package/src/fetchers/pg-client-import.ts +16 -0
- package/src/fetchers/postgresql.ts +15 -9
- package/src/fetchers/raw-query.ts +2 -1
- package/src/fetchers/teleport.ts +11 -8
- package/src/infinite-scroll.ts +240 -79
- package/src/list-sort-order.ts +34 -0
- package/src/pagination-controls.ts +26 -3
- package/src/pagination-plugin.ts +163 -135
- package/src/range-control.ts +207 -0
- package/src/row-order.ts +5 -4
- package/src/search-input.ts +190 -0
- package/src/transformations/booking-item.ts +71 -0
- package/src/transformations/index.ts +25 -2
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/* tslint:disable:function-constructor */
|
|
2
|
+
import { generateDataSourceFetcherWithCore } from '../src/data-source-fetchers'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A board's "No status" column lists the rows whose status is empty. The GUI
|
|
6
|
+
* sends that condition as `{ operand: '=', destination: null }` (and "has a
|
|
7
|
+
* status" as `'!='` + null), the one null shape every fetcher answers with
|
|
8
|
+
* `IS NULL` / `IS NOT NULL`. An operand the fetchers do not know is dropped
|
|
9
|
+
* with its null value — and a list whose only condition was dropped reads the
|
|
10
|
+
* whole table: every row in "No status", whose move button then overwrote the
|
|
11
|
+
* real statuses. Verdicts come from the SQL the executed route sends.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
interface Captured {
|
|
15
|
+
sql: string
|
|
16
|
+
params: unknown[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const bootRoute = (type: 'teleport' | 'postgresql') => {
|
|
20
|
+
const dataSource = {
|
|
21
|
+
id: 'ds-1',
|
|
22
|
+
name: 'db',
|
|
23
|
+
type,
|
|
24
|
+
config:
|
|
25
|
+
type === 'teleport'
|
|
26
|
+
? {
|
|
27
|
+
host: 'teleporthq.secrets.TELEPORT_DB_HOST',
|
|
28
|
+
port: 5432,
|
|
29
|
+
user: 'teleporthq.secrets.TELEPORT_DB_USER',
|
|
30
|
+
password: 'teleporthq.secrets.TELEPORT_DB_PASSWORD',
|
|
31
|
+
database: 'teleporthq.secrets.TELEPORT_DB_NAME',
|
|
32
|
+
selectedTables: {
|
|
33
|
+
team_events: { columns: [{ name: 'id' }, { name: 'priority' }] },
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
: { host: 'h', port: 5432, user: 'u', password: 'p', database: 'd' },
|
|
37
|
+
}
|
|
38
|
+
const code = generateDataSourceFetcherWithCore(dataSource as never, 'team_events', false, {})
|
|
39
|
+
.replace(
|
|
40
|
+
"import { Client, types as pgTypes } from 'pg'",
|
|
41
|
+
"const { Client, types: pgTypes } = require('pg')"
|
|
42
|
+
)
|
|
43
|
+
.replace(/^export default async function handler/m, 'async function handler')
|
|
44
|
+
.replace(/^export \{[^}]*\}\s*$/m, '')
|
|
45
|
+
.replace(/^export default \{[^}]*\}\s*$/m, '')
|
|
46
|
+
.concat('\nmodule.exports = { handler: handler }')
|
|
47
|
+
|
|
48
|
+
const captured: Captured[] = []
|
|
49
|
+
class FakeClient {
|
|
50
|
+
public async connect(): Promise<void> {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
public async end(): Promise<void> {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
public async query(sql: string, params: unknown[] = []): Promise<{ rows: any[] }> {
|
|
57
|
+
captured.push({ sql, params })
|
|
58
|
+
return { rows: [] }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const moduleObj: { exports: any } = { exports: {} }
|
|
62
|
+
new Function('require', 'module', 'exports', 'process', 'console', code)(
|
|
63
|
+
(name: string) => {
|
|
64
|
+
if (name === 'pg') {
|
|
65
|
+
return { Client: FakeClient }
|
|
66
|
+
}
|
|
67
|
+
throw new Error('unexpected require: ' + name)
|
|
68
|
+
},
|
|
69
|
+
moduleObj,
|
|
70
|
+
moduleObj.exports,
|
|
71
|
+
{ env: {} },
|
|
72
|
+
{ error: () => undefined, warn: () => undefined, log: () => undefined }
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return async (filters: unknown): Promise<Captured> => {
|
|
76
|
+
captured.length = 0
|
|
77
|
+
let status = 0
|
|
78
|
+
const res = {
|
|
79
|
+
status(statusCode: number) {
|
|
80
|
+
status = statusCode
|
|
81
|
+
return this
|
|
82
|
+
},
|
|
83
|
+
json() {
|
|
84
|
+
return this
|
|
85
|
+
},
|
|
86
|
+
setHeader() {
|
|
87
|
+
return this
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
await moduleObj.exports.handler(
|
|
91
|
+
{ method: 'GET', query: { filters: JSON.stringify(filters) }, headers: {} },
|
|
92
|
+
res
|
|
93
|
+
)
|
|
94
|
+
expect(status).toBe(200)
|
|
95
|
+
const select = captured.find((entry) => /FROM\s+"?team_events"?/i.test(entry.sql))
|
|
96
|
+
if (!select) {
|
|
97
|
+
throw new Error('the route never read team_events')
|
|
98
|
+
}
|
|
99
|
+
return select
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The domain → UIDL mapper's output for one condition in the inspector's group.
|
|
104
|
+
const condition = (operand: string, destination: unknown = null) => [
|
|
105
|
+
{
|
|
106
|
+
type: 'group',
|
|
107
|
+
operator: 'and',
|
|
108
|
+
children: [{ type: 'condition', source: 'priority', destination, operand }],
|
|
109
|
+
},
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
describe.each(['teleport', 'postgresql'] as const)('the %s read route', (type) => {
|
|
113
|
+
const read = bootRoute(type)
|
|
114
|
+
|
|
115
|
+
it('answers "is empty" (= null) with IS NULL', async () => {
|
|
116
|
+
expect((await read(condition('='))).sql).toMatch(/WHERE\s+priority IS NULL/)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('answers "is not empty" (!= null) with IS NOT NULL', async () => {
|
|
120
|
+
expect((await read(condition('!='))).sql).toMatch(/WHERE\s+priority IS NOT NULL/)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('still binds a plain value as a parameter', async () => {
|
|
124
|
+
const select = await read(condition('=', 'high'))
|
|
125
|
+
expect(select.sql).toMatch(/WHERE\s+priority = \$1/)
|
|
126
|
+
expect(select.params).toEqual(expect.arrayContaining(['high']))
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('reads the whole table for an operand it does not know — why the mapper never sends one', async () => {
|
|
130
|
+
expect((await read(condition('is_null'))).sql).not.toMatch(/WHERE/)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* tslint:disable:function-constructor */
|
|
2
|
+
import { generateDataSourceFetcherWithCore } from '../src/data-source-fetchers'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A Postgres DATE is a calendar day. node-pg reads it as midnight in the
|
|
6
|
+
* server's time zone, so a server east of UTC served `2026-10-09` as
|
|
7
|
+
* `2026-10-08T21:00:00.000Z`: the list showed the day before, and an edit form
|
|
8
|
+
* that loaded the row saved that day back. Every Postgres fetcher keeps the
|
|
9
|
+
* day as its own text; a stand-in driver without `types` still loads.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
type ParserSpy = Array<{ oid: number; parse: (value: string) => unknown }>
|
|
13
|
+
|
|
14
|
+
const bootFetcher = (type: 'teleport' | 'postgresql', withTypes: boolean): ParserSpy => {
|
|
15
|
+
const dataSource = {
|
|
16
|
+
id: 'ds-1',
|
|
17
|
+
name: 'db',
|
|
18
|
+
type,
|
|
19
|
+
config:
|
|
20
|
+
type === 'teleport'
|
|
21
|
+
? {
|
|
22
|
+
host: 'teleporthq.secrets.TELEPORT_DB_HOST',
|
|
23
|
+
port: 5432,
|
|
24
|
+
user: 'teleporthq.secrets.TELEPORT_DB_USER',
|
|
25
|
+
password: 'teleporthq.secrets.TELEPORT_DB_PASSWORD',
|
|
26
|
+
database: 'teleporthq.secrets.TELEPORT_DB_NAME',
|
|
27
|
+
selectedTables: { orders: { columns: [{ name: 'id' }, { name: 'due' }] } },
|
|
28
|
+
}
|
|
29
|
+
: { host: 'h', port: 5432, user: 'u', password: 'p', database: 'd' },
|
|
30
|
+
}
|
|
31
|
+
const code = generateDataSourceFetcherWithCore(dataSource as never, 'orders', false, {})
|
|
32
|
+
expect(code.startsWith("import { Client, types as pgTypes } from 'pg'")).toBe(true)
|
|
33
|
+
const runnable = code
|
|
34
|
+
.replace(
|
|
35
|
+
"import { Client, types as pgTypes } from 'pg'",
|
|
36
|
+
"const { Client, types: pgTypes } = require('pg')"
|
|
37
|
+
)
|
|
38
|
+
.replace(/^export default async function handler/m, 'async function handler')
|
|
39
|
+
.replace(/^export \{[^}]*\}\s*$/m, '')
|
|
40
|
+
.replace(/^export default \{[^}]*\}\s*$/m, '')
|
|
41
|
+
const registered: ParserSpy = []
|
|
42
|
+
const types = {
|
|
43
|
+
setTypeParser: (oid: number, parse: (value: string) => unknown) =>
|
|
44
|
+
registered.push({ oid, parse }),
|
|
45
|
+
}
|
|
46
|
+
new Function('require', 'module', 'exports', 'process', 'console', runnable)(
|
|
47
|
+
(name: string) => {
|
|
48
|
+
if (name === 'pg') {
|
|
49
|
+
return withTypes ? { Client: class {}, types } : { Client: class {} }
|
|
50
|
+
}
|
|
51
|
+
throw new Error('unexpected require: ' + name)
|
|
52
|
+
},
|
|
53
|
+
{ exports: {} },
|
|
54
|
+
{},
|
|
55
|
+
{ env: {} },
|
|
56
|
+
{ error: () => undefined, warn: () => undefined, log: () => undefined }
|
|
57
|
+
)
|
|
58
|
+
return registered
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe.each(['teleport', 'postgresql'] as const)('%s fetcher DATE columns', (type) => {
|
|
62
|
+
it('keeps a DATE as the day it names', () => {
|
|
63
|
+
const registered = bootFetcher(type, true)
|
|
64
|
+
const date = registered.find(({ oid }) => oid === 1082)
|
|
65
|
+
expect(date).toBeDefined()
|
|
66
|
+
expect(date?.parse('2026-10-09')).toBe('2026-10-09')
|
|
67
|
+
expect(registered.map(({ oid }) => oid)).toEqual([1082])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('loads with a driver that has no type registry', () => {
|
|
71
|
+
expect(bootFetcher(type, false)).toEqual([])
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -182,7 +182,7 @@ describe('a plain list fetches with its repeater’s sort', () => {
|
|
|
182
182
|
it('sends a sort bound to state, and refetches when the state changes', async () => {
|
|
183
183
|
const code = squash(await runPlugin('state'))
|
|
184
184
|
expect(code).toContain(
|
|
185
|
-
'sorts: JSON.stringify([{ field: sortBy.split("-")[0], order: sortBy.split("-")[1] }])'
|
|
185
|
+
'sorts: JSON.stringify([{ field: sortBy.split("-")?.[0], order: sortBy.split("-")?.[1] }])'
|
|
186
186
|
)
|
|
187
187
|
expect(code).toMatch(/\}\), \[sortBy\]\)\}/)
|
|
188
188
|
})
|
|
@@ -121,8 +121,8 @@ describe('the generated Postgres route', () => {
|
|
|
121
121
|
it('keeps a plain-column ORDER BY to fall back to', () => {
|
|
122
122
|
// If a database ever rejects the sub-select, the page must degrade to the
|
|
123
123
|
// previous ordering rather than serve nothing.
|
|
124
|
-
expect(productsRoute).toContain('const
|
|
125
|
-
expect(productsRoute).toContain('if (!
|
|
124
|
+
expect(productsRoute).toContain('const usedRewrittenSort = orderBySql !== plainOrderBySql')
|
|
125
|
+
expect(productsRoute).toContain('if (!usedRewrittenSort) throw sortError')
|
|
126
126
|
expect(productsRoute).toContain('baseSql + plainOrderBySql + sqlTail')
|
|
127
127
|
})
|
|
128
128
|
|
|
@@ -227,6 +227,13 @@ describe('stable pagination', () => {
|
|
|
227
227
|
expect(rowIdentityColumn({}, 'teleport_orders')).toBeNull()
|
|
228
228
|
})
|
|
229
229
|
|
|
230
|
+
it('breaks ties on every listing a visitor sorts: the blog listing and the booking page too', () => {
|
|
231
|
+
// "Sort by reading time" ties every five-minute post, "by price" every
|
|
232
|
+
// item at the same price.
|
|
233
|
+
expect(rowIdentityColumn({}, 'teleport_blog_posts')).toBe('id')
|
|
234
|
+
expect(rowIdentityColumn({}, 'teleport_booking_items')).toBe('id')
|
|
235
|
+
})
|
|
236
|
+
|
|
230
237
|
it('is applied to the real and the fallback ORDER BY, and to a single sort column', () => {
|
|
231
238
|
for (const route of [
|
|
232
239
|
generatePostgreSQLFetcher(PG_CONFIG, 'teleport_products'),
|
|
@@ -274,10 +281,8 @@ describe('the fetcher a generated storefront actually gets', () => {
|
|
|
274
281
|
})
|
|
275
282
|
|
|
276
283
|
it('keeps the plain-column fallback and the tiebreaker', () => {
|
|
277
|
-
expect(teleportProducts).toContain(
|
|
278
|
-
|
|
279
|
-
)
|
|
280
|
-
expect(teleportProducts).toContain('if (!usedDiscountAwareSort) throw sortError')
|
|
284
|
+
expect(teleportProducts).toContain('const usedRewrittenSort = orderBySql !== plainOrderBySql')
|
|
285
|
+
expect(teleportProducts).toContain('if (!usedRewrittenSort) throw sortError')
|
|
281
286
|
expect(teleportProducts).toContain('baseSql + plainOrderBySql + sqlTail')
|
|
282
287
|
expect(teleportProducts).toContain('const rowOrderTiebreak = (fields, direction) =>')
|
|
283
288
|
expect(teleportProducts).toMatch(/\$\{tiebreak\}/)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import * as types from '@babel/types'
|
|
2
|
+
import generator from '@babel/generator'
|
|
3
|
+
import { ComponentStructure } from '@teleporthq/teleport-types'
|
|
4
|
+
import { createNextArrayMapperPaginationPlugin } from '../src/pagination-plugin'
|
|
5
|
+
import {
|
|
6
|
+
makeUidlNode,
|
|
7
|
+
makeWidgetComponentChunk,
|
|
8
|
+
RANGE_DRAWN_TEXT,
|
|
9
|
+
type RepeaterOptions,
|
|
10
|
+
} from './_helpers/pagination-widget-fixture'
|
|
11
|
+
|
|
12
|
+
type ControlOptions = Parameters<typeof makeWidgetComponentChunk>[0]
|
|
13
|
+
|
|
14
|
+
const runPlugin = async (repeater: RepeaterOptions, controls: ControlOptions): Promise<string> => {
|
|
15
|
+
const chunk = makeWidgetComponentChunk(controls)
|
|
16
|
+
const structure: ComponentStructure = {
|
|
17
|
+
uidl: { name: 'TestComponent', node: makeUidlNode(repeater) },
|
|
18
|
+
chunks: [chunk],
|
|
19
|
+
dependencies: {},
|
|
20
|
+
options: { dataSources: {}, extractedResources: {} },
|
|
21
|
+
} as never
|
|
22
|
+
await createNextArrayMapperPaginationPlugin()(structure)
|
|
23
|
+
return generator(chunk.content as types.Node).code
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const collapse = (code: string): string => code.replace(/\s+/g, ' ')
|
|
27
|
+
|
|
28
|
+
const TOTAL_STATE = 'const [ds_0_total, setDs_0_total] = useState(null)'
|
|
29
|
+
const DRAWN = `ds_0_total === null ? "${RANGE_DRAWN_TEXT}" :`
|
|
30
|
+
const FROM_BY_PAGE = '(ds_0_total === 0 ? 0 : (ds_0_page - 1) * 20 + 1).toLocaleString("en-US")'
|
|
31
|
+
const FROM_FIRST_ROW = '(ds_0_total === 0 ? 0 : 1).toLocaleString("en-US")'
|
|
32
|
+
const TO = 'Math.min(ds_0_page * 20, ds_0_total).toLocaleString("en-US")'
|
|
33
|
+
const TOTAL = 'ds_0_total.toLocaleString("en-US")'
|
|
34
|
+
|
|
35
|
+
describe('pagination plugin — range control', () => {
|
|
36
|
+
it('fills the authored words with the page window in buttons mode', async () => {
|
|
37
|
+
const flat = collapse(await runPlugin({}, { controls: ['previous', 'range', 'next'] }))
|
|
38
|
+
|
|
39
|
+
// The total is a state of its own — `maxPages` cannot give it back — and
|
|
40
|
+
// the count effect fills both from the same response.
|
|
41
|
+
expect(flat).toContain(TOTAL_STATE)
|
|
42
|
+
expect(flat).toContain('setDs_0_maxPages(')
|
|
43
|
+
expect(flat).toContain('setDs_0_total(data.count)')
|
|
44
|
+
|
|
45
|
+
// The drawn text stays until the count has answered; then the template.
|
|
46
|
+
expect(flat).toContain(`${DRAWN} \`Showing \${${FROM_BY_PAGE}}–\${${TO}} of \${${TOTAL}}\``)
|
|
47
|
+
// The marker and template ride along, so the DOM carries the contract.
|
|
48
|
+
expect(flat).toContain('data-tq-pagination-control="range"')
|
|
49
|
+
expect(flat).toContain('data-tq-range-template=')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('counts from the first row in an appending list and survives the mode pruning', async () => {
|
|
53
|
+
const flat = collapse(
|
|
54
|
+
await runPlugin(
|
|
55
|
+
{ infiniteScroll: true, infiniteScrollLoadMore: true },
|
|
56
|
+
{ controls: ['previous', 'range', 'load-more', 'next'] }
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
// Previous/Next are pruned for an appending list; the range is not.
|
|
61
|
+
expect(flat).not.toContain('thq-previous-elm')
|
|
62
|
+
expect(flat).not.toContain('thq-next-elm')
|
|
63
|
+
expect(flat).toContain('thq-load-more-elm')
|
|
64
|
+
expect(flat).toContain('data-tq-pagination-control="range"')
|
|
65
|
+
|
|
66
|
+
// Everything loaded so far: 1 .. min(pages × perPage, total).
|
|
67
|
+
expect(flat).toContain(`${DRAWN} \`Showing \${${FROM_FIRST_ROW}}–\${${TO}} of \${${TOTAL}}\``)
|
|
68
|
+
expect(flat).not.toContain(FROM_BY_PAGE)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('is kept and wired beside a sentinel-driven infinite scroll too', async () => {
|
|
72
|
+
const flat = collapse(await runPlugin({ infiniteScroll: true }, { controls: ['range'] }))
|
|
73
|
+
|
|
74
|
+
expect(flat).toContain('ds_0_sentinelRef')
|
|
75
|
+
expect(flat).toContain(TOTAL_STATE)
|
|
76
|
+
expect(flat).toContain(FROM_FIRST_ROW)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('survives numbered mode with the page window', async () => {
|
|
80
|
+
const flat = collapse(
|
|
81
|
+
await runPlugin(
|
|
82
|
+
{ paginationMode: 'numbered' },
|
|
83
|
+
{ controls: ['first', 'previous', 'pages', 'range', 'next', 'last'] }
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
expect(flat).toContain('ds_0_pageTokens.map(token =>')
|
|
88
|
+
expect(flat).toContain('data-tq-pagination-control="range"')
|
|
89
|
+
expect(flat).toContain(FROM_BY_PAGE)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('reads the page off the combined state beside a search input', async () => {
|
|
93
|
+
const flat = collapse(
|
|
94
|
+
await runPlugin({ searchEnabled: true }, { controls: ['previous', 'range', 'next'] })
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
expect(flat).toContain(TOTAL_STATE)
|
|
98
|
+
expect(flat).toContain('setDs_0_total(data.count)')
|
|
99
|
+
expect(flat).toContain(
|
|
100
|
+
'(ds_0_total === 0 ? 0 : (ds_0_state.page - 1) * 20 + 1).toLocaleString("en-US")'
|
|
101
|
+
)
|
|
102
|
+
expect(flat).toContain('Math.min(ds_0_state.page * 20, ds_0_total).toLocaleString("en-US")')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('reads a template the JSX printer escaped the braces of', async () => {
|
|
106
|
+
const flat = collapse(
|
|
107
|
+
await runPlugin(
|
|
108
|
+
{},
|
|
109
|
+
{
|
|
110
|
+
controls: ['range'],
|
|
111
|
+
rangeTemplate: 'Showing {from}–{to} of {total}',
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
expect(flat).not.toContain('`Showing {')
|
|
116
|
+
expect(flat).toMatch(/`Showing \$\{[^`]*–\$\{[^`]*of \$\{[^`]*`/)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('falls back to "{from}–{to} of {total}" when no template is authored', async () => {
|
|
120
|
+
const flat = collapse(await runPlugin({}, { controls: ['range'], rangeTemplate: null }))
|
|
121
|
+
|
|
122
|
+
expect(flat).not.toContain('data-tq-range-template')
|
|
123
|
+
expect(flat).toContain(`${DRAWN} \`\${${FROM_BY_PAGE}}–\${${TO}} of \${${TOTAL}}\``)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('wires a range outside every widget to the mapper it names, wherever it sits', async () => {
|
|
127
|
+
// Placed BEFORE the provider — a toolbar above the list — so the
|
|
128
|
+
// container-ownership rule could never have reached it.
|
|
129
|
+
const flat = collapse(
|
|
130
|
+
await runPlugin(
|
|
131
|
+
{ nodeId: 'mapper-1' },
|
|
132
|
+
{ controls: ['previous', 'next'], standaloneRange: { forNodeId: 'mapper-1' } }
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
expect(flat).toContain('data-tq-pagination-for="mapper-1"')
|
|
137
|
+
expect(flat).toContain(TOTAL_STATE)
|
|
138
|
+
expect(flat).toContain(`${DRAWN} \`Showing \${${FROM_BY_PAGE}}–\${${TO}} of \${${TOTAL}}\``)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('leaves a range naming a mapper this component does not render as drawn', async () => {
|
|
142
|
+
const flat = collapse(
|
|
143
|
+
await runPlugin(
|
|
144
|
+
{ nodeId: 'mapper-1' },
|
|
145
|
+
{ controls: ['previous', 'next'], standaloneRange: { forNodeId: 'someone-else' } }
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
expect(flat).toContain(`>${RANGE_DRAWN_TEXT}</div>`)
|
|
150
|
+
expect(flat).not.toContain('ds_0_total === null')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('changes nothing for a component without a range control', async () => {
|
|
154
|
+
const flat = collapse(await runPlugin({}, { controls: ['previous', 'next'] }))
|
|
155
|
+
|
|
156
|
+
expect(flat).not.toContain('ds_0_total')
|
|
157
|
+
expect(flat).not.toContain('setDs_0_total')
|
|
158
|
+
})
|
|
159
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as types from '@babel/types'
|
|
2
|
+
import generator from '@babel/generator'
|
|
3
|
+
import { ComponentStructure } from '@teleporthq/teleport-types'
|
|
4
|
+
import { createNextArrayMapperPaginationPlugin } from '../src/pagination-plugin'
|
|
5
|
+
import {
|
|
6
|
+
makeMultiMapperUidl,
|
|
7
|
+
makeWidgetComponentChunk,
|
|
8
|
+
type RepeaterOptions,
|
|
9
|
+
} from './_helpers/pagination-widget-fixture'
|
|
10
|
+
|
|
11
|
+
type ControlOptions = Parameters<typeof makeWidgetComponentChunk>[0]
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A page's OWN search box, marked `data-tq-search-for` with its mapper's node
|
|
15
|
+
* id, is wired like the platform's `search-input`: controlled by the mapper's
|
|
16
|
+
* query state. It is paired by the id it names, never by its place among the
|
|
17
|
+
* platform inputs.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const runPlugin = async (
|
|
21
|
+
repeaters: RepeaterOptions[],
|
|
22
|
+
controls: ControlOptions
|
|
23
|
+
): Promise<string> => {
|
|
24
|
+
const chunk = makeWidgetComponentChunk(controls)
|
|
25
|
+
const structure: ComponentStructure = {
|
|
26
|
+
uidl: { name: 'TestComponent', node: makeMultiMapperUidl(repeaters) },
|
|
27
|
+
chunks: [chunk],
|
|
28
|
+
dependencies: {},
|
|
29
|
+
options: { dataSources: {}, extractedResources: {} },
|
|
30
|
+
} as never
|
|
31
|
+
await createNextArrayMapperPaginationPlugin()(structure)
|
|
32
|
+
return generator(chunk.content as types.Node).code
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const collapse = (code: string): string => code.replace(/\s+/g, ' ')
|
|
36
|
+
|
|
37
|
+
const wiredTo = (index: number): string =>
|
|
38
|
+
`onChange={e => setDs_${index}_searchQuery(e.target.value)} value={ds_${index}_searchQuery}`
|
|
39
|
+
|
|
40
|
+
describe('pagination plugin — a page’s own search box', () => {
|
|
41
|
+
it('controls a marked input by the query of the mapper it names, and keeps Enter from submitting', async () => {
|
|
42
|
+
const flat = collapse(
|
|
43
|
+
await runPlugin([{ searchEnabled: true, nodeId: 'TQ_orders' }], {
|
|
44
|
+
controls: ['previous', 'next'],
|
|
45
|
+
standaloneSearch: [{ name: 'hm-search', forNodeId: 'TQ_orders' }],
|
|
46
|
+
})
|
|
47
|
+
)
|
|
48
|
+
expect(flat).toContain(`name="hm-search" data-tq-search-for="TQ_orders" ${wiredTo(0)}`)
|
|
49
|
+
expect(flat).toContain('onKeyDown={e => { if (e.key === "Enter") e.preventDefault(); }}')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('pairs by the id named, whatever the order: the platform input keeps its place', async () => {
|
|
53
|
+
const flat = collapse(
|
|
54
|
+
await runPlugin(
|
|
55
|
+
[
|
|
56
|
+
{ searchEnabled: true, nodeId: 'TQ_jobs' },
|
|
57
|
+
{ searchEnabled: true, nodeId: 'TQ_orders' },
|
|
58
|
+
],
|
|
59
|
+
{
|
|
60
|
+
controls: ['previous', 'next'],
|
|
61
|
+
mappers: 2,
|
|
62
|
+
standaloneSearch: [
|
|
63
|
+
{ name: 'hm-search', forNodeId: 'TQ_orders' },
|
|
64
|
+
{ name: 'platform-search' },
|
|
65
|
+
],
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
// The marked box searches the second mapper; the platform box, first of its kind, the first.
|
|
70
|
+
expect(flat).toContain(`data-tq-search-for="TQ_orders" ${wiredTo(1)}`)
|
|
71
|
+
expect(flat).toContain(
|
|
72
|
+
`name="platform-search" className="products-list-search-input" ${wiredTo(0)}`
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('leaves a marked input alone when no search-enabled mapper carries its id', async () => {
|
|
77
|
+
const flat = collapse(
|
|
78
|
+
await runPlugin([{ searchEnabled: true, nodeId: 'TQ_orders' }], {
|
|
79
|
+
controls: ['previous', 'next'],
|
|
80
|
+
standaloneSearch: [{ name: 'hm-search', forNodeId: 'TQ_elsewhere' }],
|
|
81
|
+
})
|
|
82
|
+
)
|
|
83
|
+
expect(flat).toContain('name="hm-search" data-tq-search-for="TQ_elsewhere" />')
|
|
84
|
+
expect(flat).not.toContain('setDs_0_searchQuery(e.target.value)')
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -40,7 +40,7 @@ const makeComponentChunk = (): ChunkDefinition => {
|
|
|
40
40
|
// A `data-source-list > cms-list-repeater` UIDL: a paginated + search-enabled
|
|
41
41
|
// products list, optionally bound to a URL search-param key.
|
|
42
42
|
// tslint:disable-next-line:no-any
|
|
43
|
-
const makeUidlNode = (searchUrlParamKey?: string): any => ({
|
|
43
|
+
const makeUidlNode = (searchUrlParamKey?: string, pageUrlParamKey?: string): any => ({
|
|
44
44
|
type: 'data-source-list',
|
|
45
45
|
content: {
|
|
46
46
|
renderPropIdentifier: 'items',
|
|
@@ -60,6 +60,7 @@ const makeUidlNode = (searchUrlParamKey?: string): any => ({
|
|
|
60
60
|
searchEnabled: true,
|
|
61
61
|
searchDebounce: 300,
|
|
62
62
|
...(searchUrlParamKey ? { searchUrlParamKey } : {}),
|
|
63
|
+
...(pageUrlParamKey ? { pageUrlParamKey } : {}),
|
|
63
64
|
nodes: { list: { type: 'element', content: { elementType: 'div' } } },
|
|
64
65
|
},
|
|
65
66
|
},
|
|
@@ -67,10 +68,17 @@ const makeUidlNode = (searchUrlParamKey?: string): any => ({
|
|
|
67
68
|
},
|
|
68
69
|
})
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
interface RunOptions {
|
|
72
|
+
paginated?: boolean
|
|
73
|
+
pageUrlParamKey?: string
|
|
74
|
+
infiniteScroll?: boolean
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const runPlugin = async (searchUrlParamKey?: string, options: RunOptions = {}): Promise<string> => {
|
|
71
78
|
const chunk = makeComponentChunk()
|
|
72
|
-
const node = makeUidlNode(searchUrlParamKey)
|
|
73
|
-
node.content.nodes.success.content.paginated = paginated
|
|
79
|
+
const node = makeUidlNode(searchUrlParamKey, options.pageUrlParamKey)
|
|
80
|
+
node.content.nodes.success.content.paginated = options.paginated ?? true
|
|
81
|
+
node.content.nodes.success.content.infiniteScroll = !!options.infiniteScroll
|
|
74
82
|
const structure: ComponentStructure = {
|
|
75
83
|
uidl: { name: 'TestComponent', node },
|
|
76
84
|
chunks: [chunk],
|
|
@@ -108,19 +116,37 @@ describe('pagination plugin — search input URL two-way sync (searchUrlParamKey
|
|
|
108
116
|
)
|
|
109
117
|
expect(code).toContain('}, [ds_0_state.debouncedQuery, router.isReady])')
|
|
110
118
|
|
|
111
|
-
// Read-back (URL → input AND applied query at once,
|
|
112
|
-
// functional setState bail-out (loop-free), then
|
|
119
|
+
// Read-back (URL → input AND applied query at once, from page 1: the page
|
|
120
|
+
// is not in the address), functional setState bail-out (loop-free), then
|
|
121
|
+
// the handshake flag.
|
|
113
122
|
expect(code).toContain('const __urlValue = router.query.searchKeyword')
|
|
114
123
|
expect(code).toContain('ds_0_adoptUrlSearch(prev => prev === __nextValue ? prev : __nextValue)')
|
|
115
124
|
expect(code).toContain('ds_0_searchUrlAdopted.current = true')
|
|
116
125
|
expect(code).toContain('}, [router.query.searchKeyword, router.isReady])')
|
|
117
126
|
expect(code.replace(/\s+/g, ' ')).toContain(
|
|
118
|
-
'const ds_0_adoptUrlSearch = update => { setDs_0_searchQuery(update); setDs_0_state(state => { const next = update(state.debouncedQuery); return next === state.debouncedQuery ? state : { ...state, debouncedQuery: next }; }); };'
|
|
127
|
+
'const ds_0_adoptUrlSearch = update => { setDs_0_searchQuery(update); setDs_0_state(state => { const next = update(state.debouncedQuery); return next === state.debouncedQuery ? state : { ...state, page: 1, debouncedQuery: next }; }); };'
|
|
128
|
+
)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('starts an appending list on page 1 when the address brings a query', async () => {
|
|
132
|
+
// The scroll sentinel has usually fetched page 2 before the router is
|
|
133
|
+
// ready: kept, the query's first answer was its page 2 — empty when only a
|
|
134
|
+
// few rows match.
|
|
135
|
+
const code = await runPlugin('q', { infiniteScroll: true })
|
|
136
|
+
expect(code.replace(/\s+/g, ' ')).toContain(
|
|
137
|
+
'return next === state.debouncedQuery ? state : { ...state, page: 1, debouncedQuery: next };'
|
|
138
|
+
)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('keeps the page of a list whose page is in the address too', async () => {
|
|
142
|
+
const code = await runPlugin('searchKeyword', { pageUrlParamKey: 'page' })
|
|
143
|
+
expect(code.replace(/\s+/g, ' ')).toContain(
|
|
144
|
+
'return next === state.debouncedQuery ? state : { ...state, debouncedQuery: next };'
|
|
119
145
|
)
|
|
120
146
|
})
|
|
121
147
|
|
|
122
148
|
it('adopts the address into both queries of a list without pages too', async () => {
|
|
123
|
-
const code = await runPlugin('searchKeyword', false)
|
|
149
|
+
const code = await runPlugin('searchKeyword', { paginated: false })
|
|
124
150
|
expect(code).not.toContain('window.location.search')
|
|
125
151
|
expect(code.replace(/\s+/g, ' ')).toContain(
|
|
126
152
|
'const ds_0_adoptUrlSearch = update => { setDs_0_searchQuery(update); setDs_0_debouncedQuery(update); };'
|
|
@@ -62,7 +62,10 @@ function bootTableRoute(
|
|
|
62
62
|
})
|
|
63
63
|
// The route is an ES module; run it as a script with the same bindings.
|
|
64
64
|
const code = esm
|
|
65
|
-
.replace(
|
|
65
|
+
.replace(
|
|
66
|
+
"import { Client, types as pgTypes } from 'pg'",
|
|
67
|
+
"const { Client, types: pgTypes } = require('pg')"
|
|
68
|
+
)
|
|
66
69
|
.replace(/^export default async function handler/m, 'async function handler')
|
|
67
70
|
.replace(/^export \{[^}]*\}\s*$/m, '')
|
|
68
71
|
.replace(/^export default \{[^}]*\}\s*$/m, '')
|
|
@@ -436,7 +439,7 @@ describe('buildProductTransformOptions — the roles reach the emitted module',
|
|
|
436
439
|
})
|
|
437
440
|
|
|
438
441
|
describe('resolveTrustedReaderRoles', () => {
|
|
439
|
-
it(
|
|
442
|
+
it("reads the roles off the admin folder, never off one admin page's own roles", () => {
|
|
440
443
|
const roles = TableAccess.resolveTrustedReaderRoles({
|
|
441
444
|
enabled: true,
|
|
442
445
|
dataSourceId: 'ds-1',
|
|
@@ -487,7 +490,34 @@ describe('resolveTrustedReaderRoles', () => {
|
|
|
487
490
|
envKeys: {},
|
|
488
491
|
customUserProperties: [],
|
|
489
492
|
})
|
|
490
|
-
|
|
493
|
+
// `staff` opens `/admin/orders` only: a page's role is no reader of every
|
|
494
|
+
// protected table (an admin page may also admit staff).
|
|
495
|
+
expect(roles).toEqual(['admin'])
|
|
491
496
|
expect(TableAccess.resolveTrustedReaderRoles(undefined)).toEqual([])
|
|
492
497
|
})
|
|
498
|
+
|
|
499
|
+
it('refuses staff admitted to some admin pages the accounts table on its read route', async () => {
|
|
500
|
+
const staffPagesAuth: UIDLAuthentication = {
|
|
501
|
+
...ADMIN_PANEL_AUTH,
|
|
502
|
+
roles: ['admin', 'staff', 'user'],
|
|
503
|
+
pageProtection: {
|
|
504
|
+
dashboard: {
|
|
505
|
+
requiresAuth: true,
|
|
506
|
+
allowedRoles: ['admin', 'staff'],
|
|
507
|
+
pageName: 'dashboard',
|
|
508
|
+
route: '/admin/dashboard',
|
|
509
|
+
},
|
|
510
|
+
tasks: {
|
|
511
|
+
requiresAuth: true,
|
|
512
|
+
allowedRoles: ['admin', 'staff'],
|
|
513
|
+
pageName: 'tasks',
|
|
514
|
+
route: '/admin/tasks',
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
}
|
|
518
|
+
const options = buildProductTransformOptions({ authentication: staffPagesAuth })
|
|
519
|
+
const users = bootTableRoute('users', options.trustedReaderRoles || [])
|
|
520
|
+
expect((await users({ handler: 'handler', session: { role: 'staff' } })).status).toBe(403)
|
|
521
|
+
expect((await users({ handler: 'handler', session: { role: 'admin' } })).status).toBe(200)
|
|
522
|
+
})
|
|
493
523
|
})
|
|
@@ -19,6 +19,12 @@ interface CountEffectOptions {
|
|
|
19
19
|
perPage: number;
|
|
20
20
|
/** Name of the `setDs_N_maxPages` setter. */
|
|
21
21
|
setMaxPagesVar: string;
|
|
22
|
+
/**
|
|
23
|
+
* Name of the `setDs_N_total` setter, declared only when the component draws
|
|
24
|
+
* a `range` control ("Showing 1–8 of 12"): the page count alone cannot say
|
|
25
|
+
* how many rows there are.
|
|
26
|
+
*/
|
|
27
|
+
setTotalVar?: string;
|
|
22
28
|
/**
|
|
23
29
|
* Params sent to the count endpoint: the search query (when the list has
|
|
24
30
|
* one), `queryColumns`, and the filter array. Built by the caller because
|
|
@@ -38,7 +44,7 @@ interface CountEffectOptions {
|
|
|
38
44
|
}
|
|
39
45
|
/** `const ds_N_countSeq = useRef(0)` */
|
|
40
46
|
export declare const buildCountSeqDeclaration: (countSeqRefVar: string) => types.Statement;
|
|
41
|
-
export declare const buildCountFetchStatement: (options: Pick<CountEffectOptions, 'fileName' | 'perPage' | 'setMaxPagesVar' | 'urlParams' | 'countSeqRefVar'>) => types.Statement;
|
|
47
|
+
export declare const buildCountFetchStatement: (options: Pick<CountEffectOptions, 'fileName' | 'perPage' | 'setMaxPagesVar' | 'setTotalVar' | 'urlParams' | 'countSeqRefVar'>) => types.Statement;
|
|
42
48
|
/**
|
|
43
49
|
* The whole `useEffect(...)` wrapper.
|
|
44
50
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"count-effect.d.ts","sourceRoot":"","sources":["../../src/count-effect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC;;;;;;;;;;;;GAYG;AAEH,UAAU,kBAAkB;IAC1B,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAA;IAChB,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAA;IACf,6CAA6C;IAC7C,cAAc,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,SAAS,EAAE,KAAK,CAAC,cAAc,EAAE,CAAA;IACjC,iDAAiD;IACjD,IAAI,EAAE,KAAK,CAAC,UAAU,EAAE,CAAA;IACxB;;;;;;OAMG;IACH,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,wCAAwC;AACxC,eAAO,MAAM,wBAAwB,mBAAoB,MAAM,KAAG,MAAM,SAMpE,CAAA;
|
|
1
|
+
{"version":3,"file":"count-effect.d.ts","sourceRoot":"","sources":["../../src/count-effect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC;;;;;;;;;;;;GAYG;AAEH,UAAU,kBAAkB;IAC1B,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAA;IAChB,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAA;IACf,6CAA6C;IAC7C,cAAc,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,SAAS,EAAE,KAAK,CAAC,cAAc,EAAE,CAAA;IACjC,iDAAiD;IACjD,IAAI,EAAE,KAAK,CAAC,UAAU,EAAE,CAAA;IACxB;;;;;;OAMG;IACH,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,wCAAwC;AACxC,eAAO,MAAM,wBAAwB,mBAAoB,MAAM,KAAG,MAAM,SAMpE,CAAA;AA4EJ,eAAO,MAAM,wBAAwB,YAC1B,KACP,kBAAkB,EAClB,UAAU,GAAG,SAAS,GAAG,gBAAgB,GAAG,aAAa,GAAG,WAAW,GAAG,gBAAgB,CAC3F,KACA,MAAM,SAqDN,CAAA;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,YAAa,kBAAkB,KAAG,MAAM,mBAYvE,CAAA"}
|