@byline/host-tanstack-start 4.4.1 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin-shell/chrome/th-sortable.d.ts +8 -1
- package/dist/admin-shell/chrome/th-sortable.js +2 -1
- package/dist/admin-shell/collections/create.d.ts +3 -1
- package/dist/admin-shell/collections/create.js +7 -3
- package/dist/admin-shell/collections/edit.d.ts +3 -1
- package/dist/admin-shell/collections/edit.js +21 -13
- package/dist/admin-shell/collections/list.js +49 -20
- package/dist/routes/create-collection-create-route.js +8 -1
- package/dist/routes/create-collection-edit-route.js +5 -2
- package/dist/routes/create-collection-list-route.js +2 -14
- package/dist/routes/list-return-state.d.ts +45 -0
- package/dist/routes/list-return-state.js +43 -0
- package/dist/server-fns/collections/index.d.ts +1 -0
- package/dist/server-fns/collections/index.js +1 -0
- package/dist/server-fns/collections/list-view-state.d.ts +45 -0
- package/dist/server-fns/collections/list-view-state.js +59 -0
- package/dist/server-fns/collections/list.js +37 -14
- package/dist/server-fns/collections/set-list-view-preference.d.ts +18 -0
- package/dist/server-fns/collections/set-list-view-preference.js +32 -0
- package/package.json +8 -8
- package/src/admin-shell/chrome/th-sortable.tsx +9 -0
- package/src/admin-shell/collections/create.tsx +12 -5
- package/src/admin-shell/collections/edit.tsx +12 -4
- package/src/admin-shell/collections/list.tsx +49 -7
- package/src/routes/create-collection-create-route.tsx +9 -0
- package/src/routes/create-collection-edit-route.tsx +4 -0
- package/src/routes/create-collection-list-route.tsx +2 -12
- package/src/routes/list-return-state.test.node.ts +55 -0
- package/src/routes/list-return-state.ts +77 -0
- package/src/server-fns/collections/index.ts +1 -0
- package/src/server-fns/collections/list-view-state.test.node.ts +141 -0
- package/src/server-fns/collections/list-view-state.ts +137 -0
- package/src/server-fns/collections/list.ts +51 -26
- package/src/server-fns/collections/set-list-view-preference.ts +67 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
-
import {
|
|
2
|
+
import { getPreferenceCommand } from "@byline/admin/admin-preferences";
|
|
3
|
+
import { getAdminBylineClient, getAdminRequestContext } from "@byline/client/server";
|
|
3
4
|
import { ERR_NOT_FOUND, buildRelationSummaryPopulateMap, getCollectionAdminConfig, getCollectionDefinition, getCollectionSchemasForPath, getLogger, getServerConfig } from "@byline/core";
|
|
4
5
|
import { ensureCollection } from "../../integrations/api-utils.js";
|
|
6
|
+
import { bylineCore } from "../../integrations/byline-core.js";
|
|
7
|
+
import { resolveListViewState, sortableFieldNames } from "./list-view-state.js";
|
|
5
8
|
import { serialise } from "./utils.js";
|
|
6
9
|
const getCollectionDocuments = createServerFn({
|
|
7
10
|
method: 'GET'
|
|
@@ -16,23 +19,42 @@ const getCollectionDocuments = createServerFn({
|
|
|
16
19
|
}).log(getLogger());
|
|
17
20
|
const client = getAdminBylineClient();
|
|
18
21
|
const handle = client.collection(path);
|
|
19
|
-
const pageSize = params.page_size ?? 20;
|
|
20
22
|
const where = {};
|
|
21
23
|
if (params.status) where.status = params.status;
|
|
22
24
|
if (params.query) where.query = params.query;
|
|
23
25
|
const adminConfig = getCollectionAdminConfig(path);
|
|
24
|
-
const configuredSort =
|
|
26
|
+
const configuredSort = true !== config.definition.orderable && adminConfig?.defaultSort != null ? {
|
|
25
27
|
order: String(adminConfig.defaultSort.field),
|
|
26
28
|
desc: 'desc' === adminConfig.defaultSort.direction
|
|
27
|
-
};
|
|
28
|
-
const defaultSort = true === config.definition.orderable ? {
|
|
29
|
-
order_key: 'asc'
|
|
30
|
-
} : null != configuredSort ? {
|
|
31
|
-
[configuredSort.order]: configuredSort.desc ? 'desc' : 'asc'
|
|
32
29
|
} : void 0;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
let preference = null;
|
|
31
|
+
const adminStore = bylineCore().adminStore;
|
|
32
|
+
if (null != adminStore && (null == params.page_size || null == params.order)) try {
|
|
33
|
+
const context = await getAdminRequestContext();
|
|
34
|
+
const res = await getPreferenceCommand(context, {
|
|
35
|
+
scope: `collections.${path}.list`
|
|
36
|
+
}, {
|
|
37
|
+
store: adminStore
|
|
38
|
+
});
|
|
39
|
+
preference = res.value ?? null;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
getLogger().warn({
|
|
42
|
+
err,
|
|
43
|
+
collection: path
|
|
44
|
+
}, 'list-view preference read failed — using defaults');
|
|
45
|
+
}
|
|
46
|
+
const viewState = resolveListViewState({
|
|
47
|
+
params: {
|
|
48
|
+
page_size: params.page_size,
|
|
49
|
+
order: params.order,
|
|
50
|
+
desc: params.desc
|
|
51
|
+
},
|
|
52
|
+
preference,
|
|
53
|
+
orderable: true === config.definition.orderable,
|
|
54
|
+
sortableFields: sortableFieldNames(config.definition.fields),
|
|
55
|
+
configuredSort
|
|
56
|
+
});
|
|
57
|
+
const pageSize = viewState.pageSize;
|
|
36
58
|
const populateMap = buildRelationSummaryPopulateMap(config.definition.fields, (targetPath)=>({
|
|
37
59
|
def: getCollectionDefinition(targetPath),
|
|
38
60
|
admin: getCollectionAdminConfig(targetPath)
|
|
@@ -41,7 +63,7 @@ const getCollectionDocuments = createServerFn({
|
|
|
41
63
|
const populate = hasRelations ? populateMap : void 0;
|
|
42
64
|
const result = await handle.find({
|
|
43
65
|
where: Object.keys(where).length > 0 ? where : void 0,
|
|
44
|
-
sort:
|
|
66
|
+
sort: viewState.sort,
|
|
45
67
|
locale: params.locale ?? 'en',
|
|
46
68
|
page: params.page,
|
|
47
69
|
pageSize,
|
|
@@ -64,8 +86,9 @@ const getCollectionDocuments = createServerFn({
|
|
|
64
86
|
docs,
|
|
65
87
|
meta: {
|
|
66
88
|
...result.meta,
|
|
67
|
-
order:
|
|
68
|
-
desc:
|
|
89
|
+
order: viewState.metaOrder,
|
|
90
|
+
desc: viewState.metaDesc,
|
|
91
|
+
pageSize
|
|
69
92
|
},
|
|
70
93
|
included: {
|
|
71
94
|
collection: {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
export interface SetListViewPreferenceInput {
|
|
9
|
+
collection: string;
|
|
10
|
+
value: {
|
|
11
|
+
page_size?: number;
|
|
12
|
+
order?: string;
|
|
13
|
+
desc?: boolean;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export declare const setListViewPreference: import("@tanstack/react-start").RequiredFetcher<undefined, (input: SetListViewPreferenceInput) => SetListViewPreferenceInput, Promise<{
|
|
17
|
+
ok: true;
|
|
18
|
+
}>>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
+
import { setPreferenceCommand } from "@byline/admin/admin-preferences";
|
|
3
|
+
import { getAdminRequestContext } from "@byline/client/server";
|
|
4
|
+
import { ERR_NOT_FOUND, getLogger } from "@byline/core";
|
|
5
|
+
import { ensureCollection } from "../../integrations/api-utils.js";
|
|
6
|
+
import { bylineCore } from "../../integrations/byline-core.js";
|
|
7
|
+
const setListViewPreference = createServerFn({
|
|
8
|
+
method: 'POST'
|
|
9
|
+
}).validator((input)=>input).handler(async ({ data })=>{
|
|
10
|
+
const config = await ensureCollection(data.collection);
|
|
11
|
+
if (!config) throw ERR_NOT_FOUND({
|
|
12
|
+
message: 'Collection not found',
|
|
13
|
+
details: {
|
|
14
|
+
collectionPath: data.collection
|
|
15
|
+
}
|
|
16
|
+
}).log(getLogger());
|
|
17
|
+
const adminStore = bylineCore().adminStore;
|
|
18
|
+
if (null == adminStore) return {
|
|
19
|
+
ok: true
|
|
20
|
+
};
|
|
21
|
+
const context = await getAdminRequestContext();
|
|
22
|
+
await setPreferenceCommand(context, {
|
|
23
|
+
scope: `collections.${data.collection}.list`,
|
|
24
|
+
value: data.value
|
|
25
|
+
}, {
|
|
26
|
+
store: adminStore
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
ok: true
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
export { setListViewPreference };
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"private": false,
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
|
-
"version": "4.
|
|
6
|
+
"version": "4.6.0",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=20.9.0"
|
|
9
9
|
},
|
|
@@ -118,13 +118,13 @@
|
|
|
118
118
|
"react-json-view-lite": "^2.5.0",
|
|
119
119
|
"react-swipeable": "^7.0.2",
|
|
120
120
|
"zod": "^4.4.3",
|
|
121
|
-
"@byline/admin": "4.
|
|
122
|
-
"@byline/client": "4.
|
|
123
|
-
"@byline/auth": "4.
|
|
124
|
-
"@byline/
|
|
125
|
-
"@byline/
|
|
126
|
-
"@byline/
|
|
127
|
-
"@byline/
|
|
121
|
+
"@byline/admin": "4.6.0",
|
|
122
|
+
"@byline/client": "4.6.0",
|
|
123
|
+
"@byline/auth": "4.6.0",
|
|
124
|
+
"@byline/core": "4.6.0",
|
|
125
|
+
"@byline/ai": "4.6.0",
|
|
126
|
+
"@byline/ui": "4.6.0",
|
|
127
|
+
"@byline/i18n": "4.6.0"
|
|
128
128
|
},
|
|
129
129
|
"peerDependencies": {
|
|
130
130
|
"@tanstack/react-router": "^1.167.0",
|
|
@@ -37,6 +37,13 @@ export interface TableHeadingCellSortableProps extends TableHeadingCellProps {
|
|
|
37
37
|
*/
|
|
38
38
|
activeOrder?: string
|
|
39
39
|
activeDesc?: boolean
|
|
40
|
+
/**
|
|
41
|
+
* Fired after a sort click navigates, with the new order/direction.
|
|
42
|
+
* The collection list uses this to persist the choice as a sticky
|
|
43
|
+
* per-user preference; surfaces that omit it (admin-users) are
|
|
44
|
+
* unaffected.
|
|
45
|
+
*/
|
|
46
|
+
onSort?: (order: string, desc: boolean) => void
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
export function TableHeadingCellSortable({
|
|
@@ -48,6 +55,7 @@ export function TableHeadingCellSortable({
|
|
|
48
55
|
className,
|
|
49
56
|
activeOrder,
|
|
50
57
|
activeDesc,
|
|
58
|
+
onSort,
|
|
51
59
|
...rest
|
|
52
60
|
}: TableHeadingCellSortableProps & {
|
|
53
61
|
ref?: React.RefObject<HTMLTableCellElement>
|
|
@@ -68,6 +76,7 @@ export function TableHeadingCellSortable({
|
|
|
68
76
|
to: location.pathname as never,
|
|
69
77
|
search: params,
|
|
70
78
|
})
|
|
79
|
+
onSort?.(fieldName, descending)
|
|
71
80
|
}
|
|
72
81
|
}
|
|
73
82
|
|
|
@@ -14,6 +14,7 @@ import { useTranslation } from '@byline/i18n/react'
|
|
|
14
14
|
import { Container, Section, useToastManager } from '@byline/ui/react'
|
|
15
15
|
|
|
16
16
|
import { getAdminRoutePath } from '../../routes/admin-path.js'
|
|
17
|
+
import { decodeListReturnState } from '../../routes/list-return-state.js'
|
|
17
18
|
import { createCollectionDocument } from '../../server-fns/collections/index.js'
|
|
18
19
|
import { useNavigate } from '../chrome/loose-router.js'
|
|
19
20
|
import { useTanStackNavigationGuard } from './tanstack-navigation-guard.js'
|
|
@@ -27,11 +28,14 @@ export const CreateView = ({
|
|
|
27
28
|
collectionDefinition,
|
|
28
29
|
adminConfig,
|
|
29
30
|
initialData,
|
|
31
|
+
from,
|
|
30
32
|
}: {
|
|
31
33
|
collectionDefinition: CollectionDefinition
|
|
32
34
|
adminConfig?: CollectionAdminConfig
|
|
33
35
|
// biome-ignore lint/suspicious/noExplicitAny: shape is collection-specific
|
|
34
36
|
initialData?: Record<string, any>
|
|
37
|
+
/** URL-encoded list search state to return to on cancel — see list-return-state.ts. */
|
|
38
|
+
from?: string
|
|
35
39
|
}) => {
|
|
36
40
|
const toastManager = useToastManager()
|
|
37
41
|
const { t } = useTranslation('byline-admin')
|
|
@@ -62,20 +66,22 @@ export const CreateView = ({
|
|
|
62
66
|
},
|
|
63
67
|
})
|
|
64
68
|
// Create → edit: land the editor on the new document, where the rest
|
|
65
|
-
// of the work (content, status, relations) happens.
|
|
66
|
-
//
|
|
67
|
-
//
|
|
69
|
+
// of the work (content, status, relations) happens. `from` threads
|
|
70
|
+
// forward so closing the brand-new document also returns to the
|
|
71
|
+
// originating list. The list-view fallback covers callers running
|
|
72
|
+
// against an older server fn that doesn't return the new document's
|
|
73
|
+
// id — it restores the return state directly.
|
|
68
74
|
if (result?.documentId) {
|
|
69
75
|
navigate({
|
|
70
76
|
to: getAdminRoutePath('collections', '$collection', '$id'),
|
|
71
77
|
params: { collection: path, id: result.documentId } as never,
|
|
72
|
-
search: { action: 'created' },
|
|
78
|
+
search: { action: 'created', from },
|
|
73
79
|
})
|
|
74
80
|
} else {
|
|
75
81
|
navigate({
|
|
76
82
|
to: getAdminRoutePath('collections', '$collection'),
|
|
77
83
|
params: { collection: path },
|
|
78
|
-
search: { action: 'created' },
|
|
84
|
+
search: { ...decodeListReturnState(from), action: 'created' },
|
|
79
85
|
})
|
|
80
86
|
}
|
|
81
87
|
} catch (err) {
|
|
@@ -120,6 +126,7 @@ export const CreateView = ({
|
|
|
120
126
|
navigate({
|
|
121
127
|
to: getAdminRoutePath('collections', '$collection'),
|
|
122
128
|
params: { collection: path },
|
|
129
|
+
search: decodeListReturnState(from),
|
|
123
130
|
})
|
|
124
131
|
}
|
|
125
132
|
collectionPath={path}
|
|
@@ -44,6 +44,7 @@ export const EditView = ({
|
|
|
44
44
|
locale,
|
|
45
45
|
contentLocales,
|
|
46
46
|
defaultContentLocale,
|
|
47
|
+
returnSearch,
|
|
47
48
|
}: {
|
|
48
49
|
collectionDefinition: CollectionDefinition
|
|
49
50
|
adminConfig?: CollectionAdminConfig
|
|
@@ -51,6 +52,8 @@ export const EditView = ({
|
|
|
51
52
|
locale?: string
|
|
52
53
|
contentLocales: ReadonlyArray<ContentLocaleOption>
|
|
53
54
|
defaultContentLocale: string
|
|
55
|
+
/** URL-encoded list search state to return to on close — see list-return-state.ts. */
|
|
56
|
+
returnSearch?: Record<string, unknown>
|
|
54
57
|
}) => {
|
|
55
58
|
const toastManager = useToastManager()
|
|
56
59
|
const { t } = useTranslation('byline-admin')
|
|
@@ -77,7 +80,7 @@ export const EditView = ({
|
|
|
77
80
|
navigate({
|
|
78
81
|
to: getAdminRoutePath('collections', '$collection', '$id'),
|
|
79
82
|
params: { collection: path, id: String(initialData.id) },
|
|
80
|
-
search: { locale: newLocale },
|
|
83
|
+
search: (prev: Record<string, unknown>) => ({ ...prev, locale: newLocale }),
|
|
81
84
|
})
|
|
82
85
|
}
|
|
83
86
|
|
|
@@ -196,10 +199,13 @@ export const EditView = ({
|
|
|
196
199
|
status: 'success',
|
|
197
200
|
message: t('collections.edit.duplicatedSuccessMessage', { label: singular }),
|
|
198
201
|
})
|
|
199
|
-
// Navigate to the new document's edit view.
|
|
202
|
+
// Navigate to the new document's edit view. Threads the current
|
|
203
|
+
// search (including `from`) forward so closing the duplicate
|
|
204
|
+
// returns to the originating list.
|
|
200
205
|
navigate({
|
|
201
206
|
to: getAdminRoutePath('collections', '$collection', '$id'),
|
|
202
207
|
params: { collection: path, id: result.documentId },
|
|
208
|
+
search: (prev: Record<string, unknown>) => ({ ...prev }),
|
|
203
209
|
})
|
|
204
210
|
} catch (err) {
|
|
205
211
|
console.error('Duplicate error:', err)
|
|
@@ -274,7 +280,7 @@ export const EditView = ({
|
|
|
274
280
|
navigate({
|
|
275
281
|
to: getAdminRoutePath('collections', '$collection', '$id'),
|
|
276
282
|
params: { collection: path, id: String(initialData.id) },
|
|
277
|
-
search: { locale: targetLocale },
|
|
283
|
+
search: (prev: Record<string, unknown>) => ({ ...prev, locale: targetLocale }),
|
|
278
284
|
})
|
|
279
285
|
} catch (err) {
|
|
280
286
|
console.error('Copy to locale error:', err)
|
|
@@ -326,7 +332,7 @@ export const EditView = ({
|
|
|
326
332
|
navigate({
|
|
327
333
|
to: getAdminRoutePath('collections', '$collection', '$id'),
|
|
328
334
|
params: { collection: path, id: String(initialData.id) },
|
|
329
|
-
search: { locale: defaultContentLocale },
|
|
335
|
+
search: (prev: Record<string, unknown>) => ({ ...prev, locale: defaultContentLocale }),
|
|
330
336
|
})
|
|
331
337
|
} catch (err) {
|
|
332
338
|
console.error('Delete locale error:', err)
|
|
@@ -371,6 +377,7 @@ export const EditView = ({
|
|
|
371
377
|
navigate({
|
|
372
378
|
to: getAdminRoutePath('collections', '$collection'),
|
|
373
379
|
params: { collection: path },
|
|
380
|
+
search: returnSearch,
|
|
374
381
|
})
|
|
375
382
|
} catch (err) {
|
|
376
383
|
console.error('Delete error:', err)
|
|
@@ -533,6 +540,7 @@ export const EditView = ({
|
|
|
533
540
|
navigate({
|
|
534
541
|
to: getAdminRoutePath('collections', '$collection'),
|
|
535
542
|
params: { collection: path },
|
|
543
|
+
search: returnSearch,
|
|
536
544
|
})
|
|
537
545
|
}
|
|
538
546
|
collectionPath={path}
|
|
@@ -44,6 +44,8 @@ import {
|
|
|
44
44
|
import cx from 'classnames'
|
|
45
45
|
|
|
46
46
|
import { getAdminRoutePath } from '../../routes/admin-path.js'
|
|
47
|
+
import { encodeListReturnState } from '../../routes/list-return-state.js'
|
|
48
|
+
import { setListViewPreference } from '../../server-fns/collections/index.js'
|
|
47
49
|
import { Link, useNavigate } from '../chrome/loose-router.js'
|
|
48
50
|
import { RouterPager } from '../chrome/router-pager.js'
|
|
49
51
|
import { SortAscendingIcon } from '../chrome/sort-icons.js'
|
|
@@ -199,6 +201,7 @@ export const ListView = ({
|
|
|
199
201
|
// the stored order and "drop between A and B" maps onto the wrong
|
|
200
202
|
// neighbour ids.
|
|
201
203
|
const searchParams = location.search as {
|
|
204
|
+
page_size?: number
|
|
202
205
|
order?: string
|
|
203
206
|
desc?: boolean
|
|
204
207
|
query?: string
|
|
@@ -269,6 +272,22 @@ export const ListView = ({
|
|
|
269
272
|
[workflowStatuses, t]
|
|
270
273
|
)
|
|
271
274
|
|
|
275
|
+
// Same identity-stability rationale as `statusItems` above: this Select is
|
|
276
|
+
// now controlled by `value`, so a fresh `items` array identity on every
|
|
277
|
+
// render would trip Base UI's SelectRoot store-sync loop.
|
|
278
|
+
// 15 is also the list server fn's default page size (see list-view-state.ts)
|
|
279
|
+
// — the default must stay a selectable option so a fresh user with no stored
|
|
280
|
+
// preference sees the value the control claims.
|
|
281
|
+
const pageSizeItems = useMemo(
|
|
282
|
+
() => [
|
|
283
|
+
{ value: '15', label: '15' },
|
|
284
|
+
{ value: '30', label: '30' },
|
|
285
|
+
{ value: '50', label: '50' },
|
|
286
|
+
{ value: '100', label: '100' },
|
|
287
|
+
],
|
|
288
|
+
[]
|
|
289
|
+
)
|
|
290
|
+
|
|
272
291
|
const handleOnSearch = (query: string): void => {
|
|
273
292
|
if (query != null && query.length > 0) {
|
|
274
293
|
const params = structuredClone(location.search)
|
|
@@ -309,6 +328,21 @@ export const ListView = ({
|
|
|
309
328
|
})
|
|
310
329
|
}
|
|
311
330
|
|
|
331
|
+
// Quietly persist sticky view keys as a per-user preference. Fire-and-
|
|
332
|
+
// forget: the navigation already happened, so a failed save must never
|
|
333
|
+
// toast, block, or roll anything back.
|
|
334
|
+
const persistListPreference = (value: {
|
|
335
|
+
page_size?: number
|
|
336
|
+
order?: string
|
|
337
|
+
desc?: boolean
|
|
338
|
+
}): void => {
|
|
339
|
+
setListViewPreference({
|
|
340
|
+
data: { collection: data.included.collection.path as string, value },
|
|
341
|
+
}).catch((err: unknown) => {
|
|
342
|
+
console.warn('list-view preference save failed:', err)
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
|
|
312
346
|
function handleOnPageSizeChange(value: string | null): void {
|
|
313
347
|
if (typeof value !== 'string' || value.length === 0) return
|
|
314
348
|
const params = structuredClone(location.search)
|
|
@@ -319,6 +353,7 @@ export const ListView = ({
|
|
|
319
353
|
params: { collection: data.included.collection.path },
|
|
320
354
|
search: params,
|
|
321
355
|
})
|
|
356
|
+
persistListPreference({ page_size: Number.parseInt(value, 10) })
|
|
322
357
|
}
|
|
323
358
|
|
|
324
359
|
return (
|
|
@@ -339,6 +374,7 @@ export const ListView = ({
|
|
|
339
374
|
<Link
|
|
340
375
|
to={getAdminRoutePath('collections', '$collection', 'create')}
|
|
341
376
|
params={{ collection: data.included.collection.path }}
|
|
377
|
+
search={{ from: encodeListReturnState(location.search as Record<string, unknown>) }}
|
|
342
378
|
/>
|
|
343
379
|
}
|
|
344
380
|
>
|
|
@@ -460,6 +496,11 @@ export const ListView = ({
|
|
|
460
496
|
collection: data.included.collection.path,
|
|
461
497
|
id: document.id,
|
|
462
498
|
}}
|
|
499
|
+
search={{
|
|
500
|
+
from: encodeListReturnState(
|
|
501
|
+
location.search as Record<string, unknown>
|
|
502
|
+
),
|
|
503
|
+
}}
|
|
463
504
|
>
|
|
464
505
|
{column.formatter
|
|
465
506
|
? renderFormatted(
|
|
@@ -509,6 +550,7 @@ export const ListView = ({
|
|
|
509
550
|
className={column.className}
|
|
510
551
|
activeOrder={activeOrder}
|
|
511
552
|
activeDesc={activeDesc}
|
|
553
|
+
onSort={(order, desc) => persistListPreference({ order, desc })}
|
|
512
554
|
/>
|
|
513
555
|
)
|
|
514
556
|
})}
|
|
@@ -536,6 +578,11 @@ export const ListView = ({
|
|
|
536
578
|
collection: data.included.collection.path,
|
|
537
579
|
id: document.id,
|
|
538
580
|
}}
|
|
581
|
+
search={{
|
|
582
|
+
from: encodeListReturnState(
|
|
583
|
+
location.search as Record<string, unknown>
|
|
584
|
+
),
|
|
585
|
+
}}
|
|
539
586
|
>
|
|
540
587
|
{column.formatter
|
|
541
588
|
? renderFormatted(
|
|
@@ -583,13 +630,8 @@ export const ListView = ({
|
|
|
583
630
|
id="page_size"
|
|
584
631
|
name="page_size"
|
|
585
632
|
size="sm"
|
|
586
|
-
|
|
587
|
-
items={
|
|
588
|
-
{ value: '15', label: '15' },
|
|
589
|
-
{ value: '30', label: '30' },
|
|
590
|
-
{ value: '50', label: '50' },
|
|
591
|
-
{ value: '100', label: '100' },
|
|
592
|
-
]}
|
|
633
|
+
value={String(searchParams.page_size ?? data?.meta.pageSize ?? 15)}
|
|
634
|
+
items={pageSizeItems}
|
|
593
635
|
onValueChange={handleOnPageSizeChange}
|
|
594
636
|
/>
|
|
595
637
|
<RouterPager
|
|
@@ -15,14 +15,21 @@ import {
|
|
|
15
15
|
getCollectionDefinition,
|
|
16
16
|
} from '@byline/core'
|
|
17
17
|
import { useTranslation } from '@byline/i18n/react'
|
|
18
|
+
import { z } from 'zod'
|
|
18
19
|
|
|
19
20
|
import { BreadcrumbsClient } from '../admin-shell/chrome/breadcrumbs/breadcrumbs-client.js'
|
|
20
21
|
import { CreateView } from '../admin-shell/collections/create.js'
|
|
21
22
|
import { getAdminRoutePath } from './admin-path.js'
|
|
22
23
|
|
|
24
|
+
const searchSchema = z.object({
|
|
25
|
+
/** URL-encoded list search state to return to on cancel — see list-return-state.ts. */
|
|
26
|
+
from: z.string().optional(),
|
|
27
|
+
})
|
|
28
|
+
|
|
23
29
|
export function createCollectionCreateRoute(path: string) {
|
|
24
30
|
// biome-ignore lint/suspicious/noExplicitAny: dynamic path bypasses route-tree typing
|
|
25
31
|
const Route: any = createFileRoute(path as never)({
|
|
32
|
+
validateSearch: searchSchema,
|
|
26
33
|
loader: async ({
|
|
27
34
|
params,
|
|
28
35
|
}: {
|
|
@@ -46,6 +53,7 @@ export function createCollectionCreateRoute(path: string) {
|
|
|
46
53
|
const collectionDef = getCollectionDefinition(collection) as CollectionDefinition
|
|
47
54
|
const adminConfig = getCollectionAdminConfig(collection)
|
|
48
55
|
const { t } = useTranslation('byline-admin')
|
|
56
|
+
const search = Route.useSearch() as z.infer<typeof searchSchema>
|
|
49
57
|
return (
|
|
50
58
|
<>
|
|
51
59
|
<BreadcrumbsClient
|
|
@@ -65,6 +73,7 @@ export function createCollectionCreateRoute(path: string) {
|
|
|
65
73
|
collectionDefinition={collectionDef}
|
|
66
74
|
adminConfig={adminConfig ?? undefined}
|
|
67
75
|
initialData={initialData}
|
|
76
|
+
from={search.from}
|
|
68
77
|
/>
|
|
69
78
|
</>
|
|
70
79
|
)
|
|
@@ -21,11 +21,14 @@ import { EditView } from '../admin-shell/collections/edit.js'
|
|
|
21
21
|
import { getCollectionDocument } from '../server-fns/collections/index.js'
|
|
22
22
|
import { getAdminRoutePath } from './admin-path.js'
|
|
23
23
|
import { getContentLocaleRouteConfig } from './get-content-locale-route-config.js'
|
|
24
|
+
import { decodeListReturnState } from './list-return-state.js'
|
|
24
25
|
|
|
25
26
|
const searchSchema = z.object({
|
|
26
27
|
locale: z.string().optional(),
|
|
27
28
|
/** Set by the create view's create → edit redirect; fires the created toast. */
|
|
28
29
|
action: z.enum(['created']).optional(),
|
|
30
|
+
/** URL-encoded list search state to return to on close — see list-return-state.ts. */
|
|
31
|
+
from: z.string().optional(),
|
|
29
32
|
})
|
|
30
33
|
|
|
31
34
|
export function createCollectionEditRoute(path: string) {
|
|
@@ -135,6 +138,7 @@ export function createCollectionEditRoute(path: string) {
|
|
|
135
138
|
locale={locale}
|
|
136
139
|
contentLocales={contentLocales}
|
|
137
140
|
defaultContentLocale={defaultContentLocale}
|
|
141
|
+
returnSearch={decodeListReturnState(search.from)}
|
|
138
142
|
/>
|
|
139
143
|
</>
|
|
140
144
|
)
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from '@byline/core'
|
|
19
19
|
import { useTranslation } from '@byline/i18n/react'
|
|
20
20
|
import { useToastManager } from '@byline/ui/react'
|
|
21
|
-
import { z } from 'zod'
|
|
21
|
+
import type { z } from 'zod'
|
|
22
22
|
|
|
23
23
|
import { BreadcrumbsClient } from '../admin-shell/chrome/breadcrumbs/breadcrumbs-client.js'
|
|
24
24
|
import { useNavigate } from '../admin-shell/chrome/loose-router.js'
|
|
@@ -31,19 +31,9 @@ import {
|
|
|
31
31
|
reorderCollectionDocument,
|
|
32
32
|
} from '../server-fns/collections/index.js'
|
|
33
33
|
import { getAdminRoutePath } from './admin-path.js'
|
|
34
|
+
import { collectionListSearchSchema as searchSchema } from './list-return-state.js'
|
|
34
35
|
import type { CollectionTreeRow } from '../server-fns/collections/tree.js'
|
|
35
36
|
|
|
36
|
-
const searchSchema = z.object({
|
|
37
|
-
page: z.coerce.number().min(1).optional(),
|
|
38
|
-
page_size: z.coerce.number().max(100).optional(),
|
|
39
|
-
order: z.string().optional(),
|
|
40
|
-
desc: z.coerce.boolean().optional(),
|
|
41
|
-
query: z.string().optional(),
|
|
42
|
-
locale: z.string().optional(),
|
|
43
|
-
status: z.string().optional(),
|
|
44
|
-
action: z.enum(['created']).optional(),
|
|
45
|
-
})
|
|
46
|
-
|
|
47
37
|
export function createCollectionListRoute(path: string) {
|
|
48
38
|
// biome-ignore lint/suspicious/noExplicitAny: dynamic path bypasses route-tree typing
|
|
49
39
|
const Route: any = createFileRoute(path as never)({
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from 'vitest'
|
|
10
|
+
|
|
11
|
+
import { decodeListReturnState, encodeListReturnState } from './list-return-state.js'
|
|
12
|
+
|
|
13
|
+
describe('encodeListReturnState / decodeListReturnState', () => {
|
|
14
|
+
it('round-trips a full list search state', () => {
|
|
15
|
+
const search = {
|
|
16
|
+
page: 7,
|
|
17
|
+
page_size: 50,
|
|
18
|
+
order: 'title',
|
|
19
|
+
desc: false,
|
|
20
|
+
query: 'harvest',
|
|
21
|
+
locale: 'fr',
|
|
22
|
+
status: 'draft',
|
|
23
|
+
}
|
|
24
|
+
const encoded = encodeListReturnState(search)
|
|
25
|
+
expect(typeof encoded).toBe('string')
|
|
26
|
+
expect(decodeListReturnState(encoded)).toEqual(search)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('returns undefined for an empty search (no point carrying a bare target)', () => {
|
|
30
|
+
expect(encodeListReturnState({})).toBeUndefined()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('never carries the transient action param', () => {
|
|
34
|
+
const encoded = encodeListReturnState({ page: 2, action: 'created' })
|
|
35
|
+
expect(decodeListReturnState(encoded)).toEqual({ page: 2 })
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('decodes desc=false as boolean false (not string-coerced truthiness)', () => {
|
|
39
|
+
const decoded = decodeListReturnState('order=title&desc=false')
|
|
40
|
+
expect(decoded).toEqual({ order: 'title', desc: false })
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('degrades malformed input to undefined instead of throwing', () => {
|
|
44
|
+
expect(decodeListReturnState(undefined)).toBeUndefined()
|
|
45
|
+
expect(decodeListReturnState('')).toBeUndefined()
|
|
46
|
+
expect(decodeListReturnState('page=notanumber')).toBeUndefined()
|
|
47
|
+
expect(decodeListReturnState('page=0')).toBeUndefined()
|
|
48
|
+
expect(decodeListReturnState('page_size=99999')).toBeUndefined()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('drops empty-string values at encode time', () => {
|
|
52
|
+
const encoded = encodeListReturnState({ page: 3, query: '' })
|
|
53
|
+
expect(decodeListReturnState(encoded)).toEqual({ page: 3 })
|
|
54
|
+
})
|
|
55
|
+
})
|