@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.
Files changed (34) hide show
  1. package/dist/admin-shell/chrome/th-sortable.d.ts +8 -1
  2. package/dist/admin-shell/chrome/th-sortable.js +2 -1
  3. package/dist/admin-shell/collections/create.d.ts +3 -1
  4. package/dist/admin-shell/collections/create.js +7 -3
  5. package/dist/admin-shell/collections/edit.d.ts +3 -1
  6. package/dist/admin-shell/collections/edit.js +21 -13
  7. package/dist/admin-shell/collections/list.js +49 -20
  8. package/dist/routes/create-collection-create-route.js +8 -1
  9. package/dist/routes/create-collection-edit-route.js +5 -2
  10. package/dist/routes/create-collection-list-route.js +2 -14
  11. package/dist/routes/list-return-state.d.ts +45 -0
  12. package/dist/routes/list-return-state.js +43 -0
  13. package/dist/server-fns/collections/index.d.ts +1 -0
  14. package/dist/server-fns/collections/index.js +1 -0
  15. package/dist/server-fns/collections/list-view-state.d.ts +45 -0
  16. package/dist/server-fns/collections/list-view-state.js +59 -0
  17. package/dist/server-fns/collections/list.js +37 -14
  18. package/dist/server-fns/collections/set-list-view-preference.d.ts +18 -0
  19. package/dist/server-fns/collections/set-list-view-preference.js +32 -0
  20. package/package.json +8 -8
  21. package/src/admin-shell/chrome/th-sortable.tsx +9 -0
  22. package/src/admin-shell/collections/create.tsx +12 -5
  23. package/src/admin-shell/collections/edit.tsx +12 -4
  24. package/src/admin-shell/collections/list.tsx +49 -7
  25. package/src/routes/create-collection-create-route.tsx +9 -0
  26. package/src/routes/create-collection-edit-route.tsx +4 -0
  27. package/src/routes/create-collection-list-route.tsx +2 -12
  28. package/src/routes/list-return-state.test.node.ts +55 -0
  29. package/src/routes/list-return-state.ts +77 -0
  30. package/src/server-fns/collections/index.ts +1 -0
  31. package/src/server-fns/collections/list-view-state.test.node.ts +141 -0
  32. package/src/server-fns/collections/list-view-state.ts +137 -0
  33. package/src/server-fns/collections/list.ts +51 -26
  34. 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
- locale: newLocale
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
- locale: targetLocale
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
- locale: defaultContentLocale
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
- defaultValue: "15",
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
- const searchSchema = z.object({
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: searchSchema,
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 };
@@ -17,6 +17,7 @@ export * from './list';
17
17
  export * from './reindex';
18
18
  export * from './reorder';
19
19
  export * from './restore-version';
20
+ export * from './set-list-view-preference';
20
21
  export * from './stats';
21
22
  export * from './status';
22
23
  export * from './tree';
@@ -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 };