@byline/host-tanstack-start 4.5.0 → 4.6.1
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
|
@@ -25,8 +25,15 @@ export interface TableHeadingCellSortableProps extends TableHeadingCellProps {
|
|
|
25
25
|
*/
|
|
26
26
|
activeOrder?: string;
|
|
27
27
|
activeDesc?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Fired after a sort click navigates, with the new order/direction.
|
|
30
|
+
* The collection list uses this to persist the choice as a sticky
|
|
31
|
+
* per-user preference; surfaces that omit it (admin-users) are
|
|
32
|
+
* unaffected.
|
|
33
|
+
*/
|
|
34
|
+
onSort?: (order: string, desc: boolean) => void;
|
|
28
35
|
}
|
|
29
|
-
export declare function TableHeadingCellSortable({ path: _path, fieldName, label, sortable, align, className, activeOrder, activeDesc, ...rest }: TableHeadingCellSortableProps & {
|
|
36
|
+
export declare function TableHeadingCellSortable({ path: _path, fieldName, label, sortable, align, className, activeOrder, activeDesc, onSort, ...rest }: TableHeadingCellSortableProps & {
|
|
30
37
|
ref?: React.RefObject<HTMLTableCellElement>;
|
|
31
38
|
}): React.JSX.Element;
|
|
32
39
|
export {};
|
|
@@ -6,7 +6,7 @@ import classnames from "classnames";
|
|
|
6
6
|
import { useNavigate } from "./loose-router.js";
|
|
7
7
|
import { SortAscendingIcon, SortDescendingIcon, SortNeutralIcon } from "./sort-icons.js";
|
|
8
8
|
import th_sortable_module from "./th-sortable.module.js";
|
|
9
|
-
function TableHeadingCellSortable({ path: _path = '/', fieldName, label, sortable = false, align = 'left', className, activeOrder, activeDesc, ...rest }) {
|
|
9
|
+
function TableHeadingCellSortable({ path: _path = '/', fieldName, label, sortable = false, align = 'left', className, activeOrder, activeDesc, onSort, ...rest }) {
|
|
10
10
|
const navigate = useNavigate();
|
|
11
11
|
const location = useRouterState({
|
|
12
12
|
select: (s)=>s.location
|
|
@@ -23,6 +23,7 @@ function TableHeadingCellSortable({ path: _path = '/', fieldName, label, sortabl
|
|
|
23
23
|
to: location.pathname,
|
|
24
24
|
search: params
|
|
25
25
|
});
|
|
26
|
+
onSort?.(fieldName, descending);
|
|
26
27
|
}
|
|
27
28
|
};
|
|
28
29
|
useEffect(()=>{
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
import type { CollectionAdminConfig, CollectionDefinition } from '@byline/core';
|
|
9
|
-
export declare const CreateView: ({ collectionDefinition, adminConfig, initialData, }: {
|
|
9
|
+
export declare const CreateView: ({ collectionDefinition, adminConfig, initialData, from, }: {
|
|
10
10
|
collectionDefinition: CollectionDefinition;
|
|
11
11
|
adminConfig?: CollectionAdminConfig;
|
|
12
12
|
initialData?: Record<string, any>;
|
|
13
|
+
/** URL-encoded list search state to return to on cancel — see list-return-state.ts. */
|
|
14
|
+
from?: string;
|
|
13
15
|
}) => import("react").JSX.Element;
|
|
@@ -4,10 +4,11 @@ import { FormRenderer } from "@byline/admin/react";
|
|
|
4
4
|
import { useTranslation } from "@byline/i18n/react";
|
|
5
5
|
import { Container, Section, useToastManager } from "@byline/ui/react";
|
|
6
6
|
import { getAdminRoutePath } from "../../routes/admin-path.js";
|
|
7
|
+
import { decodeListReturnState } from "../../routes/list-return-state.js";
|
|
7
8
|
import { createCollectionDocument } from "../../server-fns/collections/index.js";
|
|
8
9
|
import { useNavigate } from "../chrome/loose-router.js";
|
|
9
10
|
import { useTanStackNavigationGuard } from "./tanstack-navigation-guard.js";
|
|
10
|
-
const CreateView = ({ collectionDefinition, adminConfig, initialData })=>{
|
|
11
|
+
const CreateView = ({ collectionDefinition, adminConfig, initialData, from })=>{
|
|
11
12
|
const toastManager = useToastManager();
|
|
12
13
|
const { t } = useTranslation('byline-admin');
|
|
13
14
|
const [_createState, setCreateState] = useState({
|
|
@@ -37,7 +38,8 @@ const CreateView = ({ collectionDefinition, adminConfig, initialData })=>{
|
|
|
37
38
|
id: result.documentId
|
|
38
39
|
},
|
|
39
40
|
search: {
|
|
40
|
-
action: 'created'
|
|
41
|
+
action: 'created',
|
|
42
|
+
from
|
|
41
43
|
}
|
|
42
44
|
}) : navigate({
|
|
43
45
|
to: getAdminRoutePath('collections', '$collection'),
|
|
@@ -45,6 +47,7 @@ const CreateView = ({ collectionDefinition, adminConfig, initialData })=>{
|
|
|
45
47
|
collection: path
|
|
46
48
|
},
|
|
47
49
|
search: {
|
|
50
|
+
...decodeListReturnState(from),
|
|
48
51
|
action: 'created'
|
|
49
52
|
}
|
|
50
53
|
});
|
|
@@ -88,7 +91,8 @@ const CreateView = ({ collectionDefinition, adminConfig, initialData })=>{
|
|
|
88
91
|
to: getAdminRoutePath('collections', '$collection'),
|
|
89
92
|
params: {
|
|
90
93
|
collection: path
|
|
91
|
-
}
|
|
94
|
+
},
|
|
95
|
+
search: decodeListReturnState(from)
|
|
92
96
|
}),
|
|
93
97
|
collectionPath: path
|
|
94
98
|
})
|
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
import type { CollectionAdminConfig, CollectionDefinition } from '@byline/core';
|
|
9
9
|
import type { AnyCollectionSchemaTypes } from '@byline/core/zod-schemas';
|
|
10
10
|
import type { ContentLocaleOption } from './view-menu.js';
|
|
11
|
-
export declare const EditView: ({ collectionDefinition, adminConfig, initialData, locale, contentLocales, defaultContentLocale, }: {
|
|
11
|
+
export declare const EditView: ({ collectionDefinition, adminConfig, initialData, locale, contentLocales, defaultContentLocale, returnSearch, }: {
|
|
12
12
|
collectionDefinition: CollectionDefinition;
|
|
13
13
|
adminConfig?: CollectionAdminConfig;
|
|
14
14
|
initialData: AnyCollectionSchemaTypes['UpdateType'];
|
|
15
15
|
locale?: string;
|
|
16
16
|
contentLocales: ReadonlyArray<ContentLocaleOption>;
|
|
17
17
|
defaultContentLocale: string;
|
|
18
|
+
/** URL-encoded list search state to return to on close — see list-return-state.ts. */
|
|
19
|
+
returnSearch?: Record<string, unknown>;
|
|
18
20
|
}) => import("react").JSX.Element;
|
|
@@ -9,7 +9,7 @@ import { copyDocumentToLocale, deleteDocument, deleteDocumentLocale, duplicateCo
|
|
|
9
9
|
import { useNavigate } from "../chrome/loose-router.js";
|
|
10
10
|
import { useTanStackNavigationGuard } from "./tanstack-navigation-guard.js";
|
|
11
11
|
import { ViewMenu } from "./view-menu.js";
|
|
12
|
-
const EditView = ({ collectionDefinition, adminConfig, initialData, locale, contentLocales, defaultContentLocale })=>{
|
|
12
|
+
const EditView = ({ collectionDefinition, adminConfig, initialData, locale, contentLocales, defaultContentLocale, returnSearch })=>{
|
|
13
13
|
const toastManager = useToastManager();
|
|
14
14
|
const { t } = useTranslation('byline-admin');
|
|
15
15
|
const [_editState, setEditState] = useState({
|
|
@@ -31,9 +31,10 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
|
|
|
31
31
|
collection: path,
|
|
32
32
|
id: String(initialData.id)
|
|
33
33
|
},
|
|
34
|
-
search: {
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
search: (prev)=>({
|
|
35
|
+
...prev,
|
|
36
|
+
locale: newLocale
|
|
37
|
+
})
|
|
37
38
|
});
|
|
38
39
|
};
|
|
39
40
|
const handleStatusChange = async (status)=>{
|
|
@@ -193,7 +194,10 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
|
|
|
193
194
|
params: {
|
|
194
195
|
collection: path,
|
|
195
196
|
id: result.documentId
|
|
196
|
-
}
|
|
197
|
+
},
|
|
198
|
+
search: (prev)=>({
|
|
199
|
+
...prev
|
|
200
|
+
})
|
|
197
201
|
});
|
|
198
202
|
} catch (err) {
|
|
199
203
|
console.error('Duplicate error:', err);
|
|
@@ -264,9 +268,10 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
|
|
|
264
268
|
collection: path,
|
|
265
269
|
id: String(initialData.id)
|
|
266
270
|
},
|
|
267
|
-
search: {
|
|
268
|
-
|
|
269
|
-
|
|
271
|
+
search: (prev)=>({
|
|
272
|
+
...prev,
|
|
273
|
+
locale: targetLocale
|
|
274
|
+
})
|
|
270
275
|
});
|
|
271
276
|
} catch (err) {
|
|
272
277
|
console.error('Copy to locale error:', err);
|
|
@@ -327,9 +332,10 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
|
|
|
327
332
|
collection: path,
|
|
328
333
|
id: String(initialData.id)
|
|
329
334
|
},
|
|
330
|
-
search: {
|
|
331
|
-
|
|
332
|
-
|
|
335
|
+
search: (prev)=>({
|
|
336
|
+
...prev,
|
|
337
|
+
locale: defaultContentLocale
|
|
338
|
+
})
|
|
333
339
|
});
|
|
334
340
|
} catch (err) {
|
|
335
341
|
console.error('Delete locale error:', err);
|
|
@@ -388,7 +394,8 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
|
|
|
388
394
|
to: getAdminRoutePath('collections', '$collection'),
|
|
389
395
|
params: {
|
|
390
396
|
collection: path
|
|
391
|
-
}
|
|
397
|
+
},
|
|
398
|
+
search: returnSearch
|
|
392
399
|
});
|
|
393
400
|
} catch (err) {
|
|
394
401
|
console.error('Delete error:', err);
|
|
@@ -530,7 +537,8 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
|
|
|
530
537
|
to: getAdminRoutePath('collections', '$collection'),
|
|
531
538
|
params: {
|
|
532
539
|
collection: path
|
|
533
|
-
}
|
|
540
|
+
},
|
|
541
|
+
search: returnSearch
|
|
534
542
|
}),
|
|
535
543
|
collectionPath: path
|
|
536
544
|
})
|
|
@@ -8,6 +8,8 @@ import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors } from
|
|
|
8
8
|
import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
9
9
|
import classnames from "classnames";
|
|
10
10
|
import { getAdminRoutePath } from "../../routes/admin-path.js";
|
|
11
|
+
import { encodeListReturnState } from "../../routes/list-return-state.js";
|
|
12
|
+
import { setListViewPreference } from "../../server-fns/collections/index.js";
|
|
11
13
|
import { Link, useNavigate } from "../chrome/loose-router.js";
|
|
12
14
|
import { RouterPager } from "../chrome/router-pager.js";
|
|
13
15
|
import { SortAscendingIcon } from "../chrome/sort-icons.js";
|
|
@@ -152,6 +154,24 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = fal
|
|
|
152
154
|
workflowStatuses,
|
|
153
155
|
t
|
|
154
156
|
]);
|
|
157
|
+
const pageSizeItems = useMemo(()=>[
|
|
158
|
+
{
|
|
159
|
+
value: '15',
|
|
160
|
+
label: '15'
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
value: '30',
|
|
164
|
+
label: '30'
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
value: '50',
|
|
168
|
+
label: '50'
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
value: '100',
|
|
172
|
+
label: '100'
|
|
173
|
+
}
|
|
174
|
+
], []);
|
|
155
175
|
const handleOnSearch = (query)=>{
|
|
156
176
|
if (null != query && query.length > 0) {
|
|
157
177
|
const params = structuredClone(location.search);
|
|
@@ -192,6 +212,16 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = fal
|
|
|
192
212
|
search: params
|
|
193
213
|
});
|
|
194
214
|
};
|
|
215
|
+
const persistListPreference = (value)=>{
|
|
216
|
+
setListViewPreference({
|
|
217
|
+
data: {
|
|
218
|
+
collection: data.included.collection.path,
|
|
219
|
+
value
|
|
220
|
+
}
|
|
221
|
+
}).catch((err)=>{
|
|
222
|
+
console.warn('list-view preference save failed:', err);
|
|
223
|
+
});
|
|
224
|
+
};
|
|
195
225
|
function handleOnPageSizeChange(value) {
|
|
196
226
|
if ('string' != typeof value || 0 === value.length) return;
|
|
197
227
|
const params = structuredClone(location.search);
|
|
@@ -204,6 +234,9 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = fal
|
|
|
204
234
|
},
|
|
205
235
|
search: params
|
|
206
236
|
});
|
|
237
|
+
persistListPreference({
|
|
238
|
+
page_size: Number.parseInt(value, 10)
|
|
239
|
+
});
|
|
207
240
|
}
|
|
208
241
|
return /*#__PURE__*/ jsx(Section, {
|
|
209
242
|
children: /*#__PURE__*/ jsxs(Container, {
|
|
@@ -227,6 +260,9 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = fal
|
|
|
227
260
|
to: getAdminRoutePath('collections', '$collection', 'create'),
|
|
228
261
|
params: {
|
|
229
262
|
collection: data.included.collection.path
|
|
263
|
+
},
|
|
264
|
+
search: {
|
|
265
|
+
from: encodeListReturnState(location.search)
|
|
230
266
|
}
|
|
231
267
|
}),
|
|
232
268
|
children: /*#__PURE__*/ jsx(PlusIcon, {
|
|
@@ -334,6 +370,9 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = fal
|
|
|
334
370
|
collection: data.included.collection.path,
|
|
335
371
|
id: document.id
|
|
336
372
|
},
|
|
373
|
+
search: {
|
|
374
|
+
from: encodeListReturnState(location.search)
|
|
375
|
+
},
|
|
337
376
|
children: column.formatter ? renderFormatted(getColumnValue(document, column.fieldName), document, column.formatter) : getColumnValue(document, column.fieldName) ?? '------'
|
|
338
377
|
}) : column.formatter ? renderFormatted(getColumnValue(document, column.fieldName), document, column.formatter) : 'status' === column.fieldName && workflowStatuses ? /*#__PURE__*/ jsx(StatusBadge, {
|
|
339
378
|
status: document.status,
|
|
@@ -358,7 +397,11 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = fal
|
|
|
358
397
|
align: column.align,
|
|
359
398
|
className: column.className,
|
|
360
399
|
activeOrder: activeOrder,
|
|
361
|
-
activeDesc: activeDesc
|
|
400
|
+
activeDesc: activeDesc,
|
|
401
|
+
onSort: (order, desc)=>persistListPreference({
|
|
402
|
+
order,
|
|
403
|
+
desc
|
|
404
|
+
})
|
|
362
405
|
}, String(column.fieldName)))
|
|
363
406
|
})
|
|
364
407
|
}),
|
|
@@ -377,6 +420,9 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = fal
|
|
|
377
420
|
collection: data.included.collection.path,
|
|
378
421
|
id: document.id
|
|
379
422
|
},
|
|
423
|
+
search: {
|
|
424
|
+
from: encodeListReturnState(location.search)
|
|
425
|
+
},
|
|
380
426
|
children: column.formatter ? renderFormatted(getColumnValue(document, column.fieldName), document, column.formatter) : getColumnValue(document, column.fieldName) ?? '------'
|
|
381
427
|
}) : column.formatter ? renderFormatted(getColumnValue(document, column.fieldName), document, column.formatter) : 'status' === column.fieldName && workflowStatuses ? /*#__PURE__*/ jsx(StatusBadge, {
|
|
382
428
|
status: document.status,
|
|
@@ -399,25 +445,8 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = fal
|
|
|
399
445
|
id: "page_size",
|
|
400
446
|
name: "page_size",
|
|
401
447
|
size: "sm",
|
|
402
|
-
|
|
403
|
-
items:
|
|
404
|
-
{
|
|
405
|
-
value: '15',
|
|
406
|
-
label: '15'
|
|
407
|
-
},
|
|
408
|
-
{
|
|
409
|
-
value: '30',
|
|
410
|
-
label: '30'
|
|
411
|
-
},
|
|
412
|
-
{
|
|
413
|
-
value: '50',
|
|
414
|
-
label: '50'
|
|
415
|
-
},
|
|
416
|
-
{
|
|
417
|
-
value: '100',
|
|
418
|
-
label: '100'
|
|
419
|
-
}
|
|
420
|
-
],
|
|
448
|
+
value: String(searchParams.page_size ?? data?.meta.pageSize ?? 15),
|
|
449
|
+
items: pageSizeItems,
|
|
421
450
|
onValueChange: handleOnPageSizeChange
|
|
422
451
|
}),
|
|
423
452
|
/*#__PURE__*/ jsx(RouterPager, {
|
|
@@ -2,11 +2,16 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { createFileRoute, notFound } from "@tanstack/react-router";
|
|
3
3
|
import { buildInitialDataFromFields, getCollectionAdminConfig, getCollectionDefinition } from "@byline/core";
|
|
4
4
|
import { useTranslation } from "@byline/i18n/react";
|
|
5
|
+
import { z } from "zod";
|
|
5
6
|
import { BreadcrumbsClient } from "../admin-shell/chrome/breadcrumbs/breadcrumbs-client.js";
|
|
6
7
|
import { CreateView } from "../admin-shell/collections/create.js";
|
|
7
8
|
import { getAdminRoutePath } from "./admin-path.js";
|
|
9
|
+
const searchSchema = z.object({
|
|
10
|
+
from: z.string().optional()
|
|
11
|
+
});
|
|
8
12
|
function createCollectionCreateRoute(path) {
|
|
9
13
|
const Route = createFileRoute(path)({
|
|
14
|
+
validateSearch: searchSchema,
|
|
10
15
|
loader: async ({ params })=>{
|
|
11
16
|
const collectionDef = getCollectionDefinition(params.collection);
|
|
12
17
|
if (!collectionDef) throw notFound();
|
|
@@ -24,6 +29,7 @@ function createCollectionCreateRoute(path) {
|
|
|
24
29
|
const collectionDef = getCollectionDefinition(collection);
|
|
25
30
|
const adminConfig = getCollectionAdminConfig(collection);
|
|
26
31
|
const { t } = useTranslation('byline-admin');
|
|
32
|
+
const search = Route.useSearch();
|
|
27
33
|
return /*#__PURE__*/ jsxs(Fragment, {
|
|
28
34
|
children: [
|
|
29
35
|
/*#__PURE__*/ jsx(BreadcrumbsClient, {
|
|
@@ -45,7 +51,8 @@ function createCollectionCreateRoute(path) {
|
|
|
45
51
|
/*#__PURE__*/ jsx(CreateView, {
|
|
46
52
|
collectionDefinition: collectionDef,
|
|
47
53
|
adminConfig: adminConfig ?? void 0,
|
|
48
|
-
initialData: initialData
|
|
54
|
+
initialData: initialData,
|
|
55
|
+
from: search.from
|
|
49
56
|
})
|
|
50
57
|
]
|
|
51
58
|
});
|
|
@@ -11,11 +11,13 @@ import { EditView } from "../admin-shell/collections/edit.js";
|
|
|
11
11
|
import { getCollectionDocument } from "../server-fns/collections/index.js";
|
|
12
12
|
import { getAdminRoutePath } from "./admin-path.js";
|
|
13
13
|
import { getContentLocaleRouteConfig } from "./get-content-locale-route-config.js";
|
|
14
|
+
import { decodeListReturnState } from "./list-return-state.js";
|
|
14
15
|
const searchSchema = z.object({
|
|
15
16
|
locale: z.string().optional(),
|
|
16
17
|
action: z["enum"]([
|
|
17
18
|
'created'
|
|
18
|
-
]).optional()
|
|
19
|
+
]).optional(),
|
|
20
|
+
from: z.string().optional()
|
|
19
21
|
});
|
|
20
22
|
function createCollectionEditRoute(path) {
|
|
21
23
|
const Route = createFileRoute(path)({
|
|
@@ -105,7 +107,8 @@ function createCollectionEditRoute(path) {
|
|
|
105
107
|
initialData: data,
|
|
106
108
|
locale: locale,
|
|
107
109
|
contentLocales: contentLocales,
|
|
108
|
-
defaultContentLocale: defaultContentLocale
|
|
110
|
+
defaultContentLocale: defaultContentLocale,
|
|
111
|
+
returnSearch: decodeListReturnState(search.from)
|
|
109
112
|
})
|
|
110
113
|
]
|
|
111
114
|
});
|
|
@@ -5,28 +5,16 @@ import { applyRelationColumnFormatters } from "@byline/admin/react";
|
|
|
5
5
|
import { getCollectionAdminConfig, getCollectionDefinition, getWorkflowStatuses } from "@byline/core";
|
|
6
6
|
import { useTranslation } from "@byline/i18n/react";
|
|
7
7
|
import { useToastManager } from "@byline/ui/react";
|
|
8
|
-
import { z } from "zod";
|
|
9
8
|
import { BreadcrumbsClient } from "../admin-shell/chrome/breadcrumbs/breadcrumbs-client.js";
|
|
10
9
|
import { useNavigate } from "../admin-shell/chrome/loose-router.js";
|
|
11
10
|
import { ListView } from "../admin-shell/collections/list.js";
|
|
12
11
|
import { TreeListView } from "../admin-shell/collections/tree-list.js";
|
|
13
12
|
import { getCollectionDocuments, getCollectionTree, placeTreeNode, reorderCollectionDocument } from "../server-fns/collections/index.js";
|
|
14
13
|
import { getAdminRoutePath } from "./admin-path.js";
|
|
15
|
-
|
|
16
|
-
page: z.coerce.number().min(1).optional(),
|
|
17
|
-
page_size: z.coerce.number().max(100).optional(),
|
|
18
|
-
order: z.string().optional(),
|
|
19
|
-
desc: z.coerce.boolean().optional(),
|
|
20
|
-
query: z.string().optional(),
|
|
21
|
-
locale: z.string().optional(),
|
|
22
|
-
status: z.string().optional(),
|
|
23
|
-
action: z["enum"]([
|
|
24
|
-
'created'
|
|
25
|
-
]).optional()
|
|
26
|
-
});
|
|
14
|
+
import { collectionListSearchSchema } from "./list-return-state.js";
|
|
27
15
|
function createCollectionListRoute(path) {
|
|
28
16
|
const Route = createFileRoute(path)({
|
|
29
|
-
validateSearch:
|
|
17
|
+
validateSearch: collectionListSearchSchema,
|
|
30
18
|
loaderDeps: ({ search })=>({
|
|
31
19
|
page: search.page,
|
|
32
20
|
page_size: search.page_size,
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
* Return-to-list state for the collection editor (issue #17).
|
|
10
|
+
*
|
|
11
|
+
* The list route holds its complete view state in URL search params, so
|
|
12
|
+
* "come back to page 7 of the filtered list" needs no storage: the list
|
|
13
|
+
* encodes its current search into a single `from` param on the editor
|
|
14
|
+
* link, the editor threads it through its own navigations, and
|
|
15
|
+
* close/delete decode it back into list search params. Malformed or
|
|
16
|
+
* absent state degrades to the bare list — never an error.
|
|
17
|
+
*
|
|
18
|
+
* The list route's search schema lives here (rather than in the route
|
|
19
|
+
* factory) so the helpers and the factory share one definition without
|
|
20
|
+
* a circular import.
|
|
21
|
+
*/
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
export declare const collectionListSearchSchema: z.ZodObject<{
|
|
24
|
+
page: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
|
25
|
+
page_size: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
|
26
|
+
order: z.ZodOptional<z.ZodString>;
|
|
27
|
+
desc: z.ZodOptional<z.ZodCoercedBoolean<unknown>>;
|
|
28
|
+
query: z.ZodOptional<z.ZodString>;
|
|
29
|
+
locale: z.ZodOptional<z.ZodString>;
|
|
30
|
+
status: z.ZodOptional<z.ZodString>;
|
|
31
|
+
action: z.ZodOptional<z.ZodEnum<{
|
|
32
|
+
created: "created";
|
|
33
|
+
}>>;
|
|
34
|
+
}, z.core.$strip>;
|
|
35
|
+
export type CollectionListSearch = z.infer<typeof collectionListSearchSchema>;
|
|
36
|
+
/** `undefined` when there is nothing worth carrying (bare list). */
|
|
37
|
+
export declare function encodeListReturnState(search: Record<string, unknown>): string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Parse a `from` param back into list search params. Runs the decoded
|
|
40
|
+
* pairs through the list search schema; anything malformed returns
|
|
41
|
+
* `undefined` (→ bare list). `desc` needs explicit normalisation
|
|
42
|
+
* because `z.coerce.boolean()` would coerce the string `'false'` to
|
|
43
|
+
* `true`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function decodeListReturnState(from: string | undefined): Record<string, unknown> | undefined;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const collectionListSearchSchema = z.object({
|
|
3
|
+
page: z.coerce.number().min(1).optional(),
|
|
4
|
+
page_size: z.coerce.number().max(100).optional(),
|
|
5
|
+
order: z.string().optional(),
|
|
6
|
+
desc: z.coerce.boolean().optional(),
|
|
7
|
+
query: z.string().optional(),
|
|
8
|
+
locale: z.string().optional(),
|
|
9
|
+
status: z.string().optional(),
|
|
10
|
+
action: z["enum"]([
|
|
11
|
+
'created'
|
|
12
|
+
]).optional()
|
|
13
|
+
});
|
|
14
|
+
const RETURN_KEYS = [
|
|
15
|
+
'page',
|
|
16
|
+
'page_size',
|
|
17
|
+
'order',
|
|
18
|
+
'desc',
|
|
19
|
+
'query',
|
|
20
|
+
'locale',
|
|
21
|
+
'status'
|
|
22
|
+
];
|
|
23
|
+
function encodeListReturnState(search) {
|
|
24
|
+
const params = new URLSearchParams();
|
|
25
|
+
for (const key of RETURN_KEYS){
|
|
26
|
+
const value = search[key];
|
|
27
|
+
if (null != value && '' !== value) params.set(key, String(value));
|
|
28
|
+
}
|
|
29
|
+
const encoded = params.toString();
|
|
30
|
+
return encoded.length > 0 ? encoded : void 0;
|
|
31
|
+
}
|
|
32
|
+
function decodeListReturnState(from) {
|
|
33
|
+
if (null == from || 0 === from.length) return;
|
|
34
|
+
const raw = Object.fromEntries(new URLSearchParams(from));
|
|
35
|
+
delete raw.action;
|
|
36
|
+
if ('string' == typeof raw.desc) raw.desc = 'true' === raw.desc;
|
|
37
|
+
const parsed = collectionListSearchSchema.safeParse(raw);
|
|
38
|
+
if (!parsed.success) return;
|
|
39
|
+
const result = {};
|
|
40
|
+
for (const [key, value] of Object.entries(parsed.data))if (void 0 !== value) result[key] = value;
|
|
41
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
42
|
+
}
|
|
43
|
+
export { collectionListSearchSchema, decodeListReturnState, encodeListReturnState };
|
|
@@ -11,6 +11,7 @@ export * from "./list.js";
|
|
|
11
11
|
export * from "./reindex.js";
|
|
12
12
|
export * from "./reorder.js";
|
|
13
13
|
export * from "./restore-version.js";
|
|
14
|
+
export * from "./set-list-view-preference.js";
|
|
14
15
|
export * from "./stats.js";
|
|
15
16
|
export * from "./status.js";
|
|
16
17
|
export * from "./tree.js";
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
* The field names a stored sort preference may legitimately name: fields
|
|
10
|
+
* whose store type carries an orderable scalar column, plus the document
|
|
11
|
+
* system columns.
|
|
12
|
+
*/
|
|
13
|
+
export declare function sortableFieldNames(fields: Array<{
|
|
14
|
+
name: string;
|
|
15
|
+
type: string;
|
|
16
|
+
}>): string[];
|
|
17
|
+
export interface ListViewPreferenceValue {
|
|
18
|
+
page_size?: number;
|
|
19
|
+
order?: string;
|
|
20
|
+
desc?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface ResolveListViewStateInput {
|
|
23
|
+
params: {
|
|
24
|
+
page_size?: number;
|
|
25
|
+
order?: string;
|
|
26
|
+
desc?: boolean;
|
|
27
|
+
};
|
|
28
|
+
preference: ListViewPreferenceValue | null;
|
|
29
|
+
orderable: boolean;
|
|
30
|
+
/** Field names valid as a sort column (collection fields + system columns). */
|
|
31
|
+
sortableFields: string[];
|
|
32
|
+
configuredSort?: {
|
|
33
|
+
order: string;
|
|
34
|
+
desc: boolean;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export interface ResolvedListViewState {
|
|
38
|
+
pageSize: number;
|
|
39
|
+
/** Sort spec for `CollectionHandle.find`; undefined → storage default. */
|
|
40
|
+
sort?: Record<string, 'asc' | 'desc'>;
|
|
41
|
+
/** Effective sort echoed through `meta.order` / `meta.desc` for the header indicator. */
|
|
42
|
+
metaOrder?: string;
|
|
43
|
+
metaDesc?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function resolveListViewState(input: ResolveListViewStateInput): ResolvedListViewState;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { fieldTypeToStore } from "@byline/core";
|
|
2
|
+
const SORTABLE_STORE_TYPES = new Set([
|
|
3
|
+
'text',
|
|
4
|
+
'numeric',
|
|
5
|
+
'boolean',
|
|
6
|
+
'datetime'
|
|
7
|
+
]);
|
|
8
|
+
function sortableFieldNames(fields) {
|
|
9
|
+
return [
|
|
10
|
+
...fields.filter((f)=>SORTABLE_STORE_TYPES.has(fieldTypeToStore[f.type]?.storeType ?? '')).map((f)=>f.name),
|
|
11
|
+
'created_at',
|
|
12
|
+
'updated_at'
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
function resolveListViewState(input) {
|
|
16
|
+
const { params, preference, orderable, sortableFields, configuredSort } = input;
|
|
17
|
+
const rawPrefPageSize = preference?.page_size;
|
|
18
|
+
const prefPageSize = 'number' == typeof rawPrefPageSize && Number.isInteger(rawPrefPageSize) && rawPrefPageSize >= 1 && rawPrefPageSize <= 100 ? rawPrefPageSize : void 0;
|
|
19
|
+
const pageSize = params.page_size ?? prefPageSize ?? 15;
|
|
20
|
+
if (params.order) return {
|
|
21
|
+
pageSize,
|
|
22
|
+
sort: {
|
|
23
|
+
[params.order]: false === params.desc ? 'asc' : 'desc'
|
|
24
|
+
},
|
|
25
|
+
metaOrder: params.order,
|
|
26
|
+
metaDesc: params.desc
|
|
27
|
+
};
|
|
28
|
+
if (orderable) return {
|
|
29
|
+
pageSize,
|
|
30
|
+
sort: {
|
|
31
|
+
order_key: 'asc'
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const rawPrefOrder = preference?.order;
|
|
35
|
+
const prefOrder = 'string' == typeof rawPrefOrder && sortableFields.includes(rawPrefOrder) ? rawPrefOrder : void 0;
|
|
36
|
+
if (null != prefOrder) {
|
|
37
|
+
const desc = preference?.desc === true;
|
|
38
|
+
return {
|
|
39
|
+
pageSize,
|
|
40
|
+
sort: {
|
|
41
|
+
[prefOrder]: desc ? 'desc' : 'asc'
|
|
42
|
+
},
|
|
43
|
+
metaOrder: prefOrder,
|
|
44
|
+
metaDesc: desc
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (null != configuredSort) return {
|
|
48
|
+
pageSize,
|
|
49
|
+
sort: {
|
|
50
|
+
[configuredSort.order]: configuredSort.desc ? 'desc' : 'asc'
|
|
51
|
+
},
|
|
52
|
+
metaOrder: configuredSort.order,
|
|
53
|
+
metaDesc: configuredSort.desc
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
pageSize
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export { resolveListViewState, sortableFieldNames };
|