@byline/host-tanstack-start 4.6.0 → 4.6.2

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.
@@ -5,6 +5,7 @@ import { getDefaultStatus, getWorkflowStatuses } from "@byline/core";
5
5
  import { useTranslation } from "@byline/i18n/react";
6
6
  import { Container, Section, useToastManager } from "@byline/ui/react";
7
7
  import { getAdminRoutePath } from "../../routes/admin-path.js";
8
+ import { clearListReturnState } from "../../routes/list-return-storage.js";
8
9
  import { copyDocumentToLocale, deleteDocument, deleteDocumentLocale, duplicateCollectionDocument, hasDeleteSideEffectFailures, unpublishDocument, updateCollectionDocumentSystemFields, updateCollectionDocumentWithPatches, updateDocumentStatus } from "../../server-fns/collections/index.js";
9
10
  import { useNavigate } from "../chrome/loose-router.js";
10
11
  import { useTanStackNavigationGuard } from "./tanstack-navigation-guard.js";
@@ -390,6 +391,7 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
390
391
  status: 'success',
391
392
  message: description
392
393
  });
394
+ clearListReturnState(path, String(initialData.id));
393
395
  navigate({
394
396
  to: getAdminRoutePath('collections', '$collection'),
395
397
  params: {
@@ -533,13 +535,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
533
535
  restoreWarnings: restoreWarnings,
534
536
  nextStatus: nextStatus,
535
537
  workflowStatuses: workflowStatuses,
536
- onCancel: ()=>navigate({
538
+ onCancel: ()=>{
539
+ clearListReturnState(path, String(initialData.id));
540
+ navigate({
537
541
  to: getAdminRoutePath('collections', '$collection'),
538
542
  params: {
539
543
  collection: path
540
544
  },
541
545
  search: returnSearch
542
- }),
546
+ });
547
+ },
543
548
  collectionPath: path
544
549
  })
545
550
  })
@@ -1,5 +1,5 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef } from "react";
2
+ import { useEffect, useRef, useState } from "react";
3
3
  import { createFileRoute, notFound } from "@tanstack/react-router";
4
4
  import { getCollectionAdminConfig, getCollectionDefinition } from "@byline/core";
5
5
  import { useTranslation } from "@byline/i18n/react";
@@ -12,6 +12,7 @@ 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
14
  import { decodeListReturnState } from "./list-return-state.js";
15
+ import { persistListReturnState, readListReturnState } from "./list-return-storage.js";
15
16
  const searchSchema = z.object({
16
17
  locale: z.string().optional(),
17
18
  action: z["enum"]([
@@ -46,6 +47,17 @@ function createCollectionEditRoute(path) {
46
47
  const toastManager = useToastManager();
47
48
  const navigate = useNavigate();
48
49
  const { contentLocales, defaultContentLocale } = getContentLocaleRouteConfig();
50
+ const [returnSearch, setReturnSearch] = useState(()=>decodeListReturnState(search.from));
51
+ useEffect(()=>{
52
+ if (null != search.from && search.from.length > 0) {
53
+ persistListReturnState(collection, id, search.from);
54
+ setReturnSearch(decodeListReturnState(search.from));
55
+ } else setReturnSearch(decodeListReturnState(readListReturnState(collection, id)));
56
+ }, [
57
+ search.from,
58
+ collection,
59
+ id
60
+ ]);
49
61
  const createdToastFiredRef = useRef(false);
50
62
  useEffect(()=>{
51
63
  if ('created' !== search.action) {
@@ -108,7 +120,7 @@ function createCollectionEditRoute(path) {
108
120
  locale: locale,
109
121
  contentLocales: contentLocales,
110
122
  defaultContentLocale: defaultContentLocale,
111
- returnSearch: decodeListReturnState(search.from)
123
+ returnSearch: returnSearch
112
124
  })
113
125
  ]
114
126
  });
@@ -0,0 +1,13 @@
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
+ /** Persist the encoded `from` state for one document. No-op when unavailable. */
9
+ export declare function persistListReturnState(collection: string, id: string, from: string): void;
10
+ /** Read the stored `from` state for one document, or `undefined`. */
11
+ export declare function readListReturnState(collection: string, id: string): string | undefined;
12
+ /** Clear the stored `from` state for one document (on close / delete). */
13
+ export declare function clearListReturnState(collection: string, id: string): void;
@@ -0,0 +1,36 @@
1
+ const KEY_PREFIX = 'byline:list-return:';
2
+ function storage() {
3
+ try {
4
+ return globalThis.sessionStorage ?? void 0;
5
+ } catch {
6
+ return;
7
+ }
8
+ }
9
+ function keyFor(collection, id) {
10
+ return `${KEY_PREFIX}${collection}:${id}`;
11
+ }
12
+ function persistListReturnState(collection, id, from) {
13
+ if (0 === from.length) return;
14
+ const store = storage();
15
+ if (null == store) return;
16
+ try {
17
+ store.setItem(keyFor(collection, id), from);
18
+ } catch {}
19
+ }
20
+ function readListReturnState(collection, id) {
21
+ const store = storage();
22
+ if (null == store) return;
23
+ try {
24
+ return store.getItem(keyFor(collection, id)) ?? void 0;
25
+ } catch {
26
+ return;
27
+ }
28
+ }
29
+ function clearListReturnState(collection, id) {
30
+ const store = storage();
31
+ if (null == store) return;
32
+ try {
33
+ store.removeItem(keyFor(collection, id));
34
+ } catch {}
35
+ }
36
+ export { clearListReturnState, persistListReturnState, readListReturnState };
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.0",
6
+ "version": "4.6.2",
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.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"
121
+ "@byline/auth": "4.6.2",
122
+ "@byline/ai": "4.6.2",
123
+ "@byline/admin": "4.6.2",
124
+ "@byline/client": "4.6.2",
125
+ "@byline/core": "4.6.2",
126
+ "@byline/i18n": "4.6.2",
127
+ "@byline/ui": "4.6.2"
128
128
  },
129
129
  "peerDependencies": {
130
130
  "@tanstack/react-router": "^1.167.0",
@@ -16,6 +16,7 @@ import { useTranslation } from '@byline/i18n/react'
16
16
  import { Container, Section, useToastManager } from '@byline/ui/react'
17
17
 
18
18
  import { getAdminRoutePath } from '../../routes/admin-path.js'
19
+ import { clearListReturnState } from '../../routes/list-return-storage.js'
19
20
  import {
20
21
  copyDocumentToLocale,
21
22
  deleteDocument,
@@ -373,7 +374,10 @@ export const EditView = ({
373
374
  },
374
375
  })
375
376
  setEditState({ status: 'success', message: description })
376
- // Navigate back to the collection list after deletion.
377
+ // Navigate back to the collection list after deletion. The return
378
+ // target has now been consumed — clear the stored fallback so a later
379
+ // visit doesn't resurrect a stale list position.
380
+ clearListReturnState(path, String(initialData.id))
377
381
  navigate({
378
382
  to: getAdminRoutePath('collections', '$collection'),
379
383
  params: { collection: path },
@@ -536,13 +540,17 @@ export const EditView = ({
536
540
  restoreWarnings={restoreWarnings}
537
541
  nextStatus={nextStatus}
538
542
  workflowStatuses={workflowStatuses}
539
- onCancel={() =>
543
+ onCancel={() => {
544
+ // Close consumes the return target — clear the stored fallback so
545
+ // re-opening this document later degrades to the bare list unless
546
+ // a fresh `from` is supplied.
547
+ clearListReturnState(path, String(initialData.id))
540
548
  navigate({
541
549
  to: getAdminRoutePath('collections', '$collection'),
542
550
  params: { collection: path },
543
551
  search: returnSearch,
544
552
  })
545
- }
553
+ }}
546
554
  collectionPath={path}
547
555
  />
548
556
  </Container>
@@ -6,7 +6,7 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
 
9
- import { useEffect, useRef } from 'react'
9
+ import { useEffect, useRef, useState } from 'react'
10
10
  import { createFileRoute, notFound } from '@tanstack/react-router'
11
11
 
12
12
  import type { CollectionDefinition } from '@byline/core'
@@ -22,6 +22,7 @@ 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
24
  import { decodeListReturnState } from './list-return-state.js'
25
+ import { persistListReturnState, readListReturnState } from './list-return-storage.js'
25
26
 
26
27
  const searchSchema = z.object({
27
28
  locale: z.string().optional(),
@@ -83,6 +84,24 @@ export function createCollectionEditRoute(path: string) {
83
84
  const navigate = useNavigate()
84
85
  const { contentLocales, defaultContentLocale } = getContentLocaleRouteConfig()
85
86
 
87
+ // Return-to-list state. The URL `from` param is the primary carrier;
88
+ // when present we also mirror it into tab-scoped sessionStorage so the
89
+ // full-page preview round-trip (which discards the admin URL) can
90
+ // recover it. When the URL has no `from` — e.g. the editor was
91
+ // re-opened from the public preview bar's Edit link — we fall back to
92
+ // the stored value. See list-return-storage.ts.
93
+ const [returnSearch, setReturnSearch] = useState<Record<string, unknown> | undefined>(() =>
94
+ decodeListReturnState(search.from)
95
+ )
96
+ useEffect(() => {
97
+ if (search.from != null && search.from.length > 0) {
98
+ persistListReturnState(collection, id, search.from)
99
+ setReturnSearch(decodeListReturnState(search.from))
100
+ } else {
101
+ setReturnSearch(decodeListReturnState(readListReturnState(collection, id)))
102
+ }
103
+ }, [search.from, collection, id])
104
+
86
105
  // Post-create toast for the create → edit redirect (?action=created).
87
106
  // Ref-guarded for the same reason as the list route's created toast:
88
107
  // `toastManager` changes identity on every add, so the effect depends
@@ -138,7 +157,7 @@ export function createCollectionEditRoute(path: string) {
138
157
  locale={locale}
139
158
  contentLocales={contentLocales}
140
159
  defaultContentLocale={defaultContentLocale}
141
- returnSearch={decodeListReturnState(search.from)}
160
+ returnSearch={returnSearch}
142
161
  />
143
162
  </>
144
163
  )
@@ -0,0 +1,77 @@
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 { afterEach, describe, expect, it } from 'vitest'
10
+
11
+ import {
12
+ clearListReturnState,
13
+ persistListReturnState,
14
+ readListReturnState,
15
+ } from './list-return-storage.js'
16
+
17
+ /** Minimal Map-backed `Storage` stand-in — node mode has no sessionStorage. */
18
+ function mockSessionStorage(): Storage {
19
+ const map = new Map<string, string>()
20
+ return {
21
+ get length() {
22
+ return map.size
23
+ },
24
+ clear: () => map.clear(),
25
+ getItem: (k: string) => (map.has(k) ? (map.get(k) as string) : null),
26
+ key: (i: number) => Array.from(map.keys())[i] ?? null,
27
+ removeItem: (k: string) => {
28
+ map.delete(k)
29
+ },
30
+ setItem: (k: string, v: string) => {
31
+ map.set(k, v)
32
+ },
33
+ }
34
+ }
35
+
36
+ function stub(): void {
37
+ ;(globalThis as { sessionStorage?: Storage }).sessionStorage = mockSessionStorage()
38
+ }
39
+
40
+ describe('list-return-storage', () => {
41
+ afterEach(() => {
42
+ delete (globalThis as { sessionStorage?: Storage }).sessionStorage
43
+ })
44
+
45
+ it('persists and reads a value keyed by collection + id', () => {
46
+ stub()
47
+ persistListReturnState('news', '42', 'page=6&order=title')
48
+ expect(readListReturnState('news', '42')).toBe('page=6&order=title')
49
+ })
50
+
51
+ it('scopes stored state by collection + id', () => {
52
+ stub()
53
+ persistListReturnState('news', '42', 'page=6')
54
+ expect(readListReturnState('news', '99')).toBeUndefined()
55
+ expect(readListReturnState('pages', '42')).toBeUndefined()
56
+ })
57
+
58
+ it('clears a stored value on consume', () => {
59
+ stub()
60
+ persistListReturnState('news', '42', 'page=6')
61
+ clearListReturnState('news', '42')
62
+ expect(readListReturnState('news', '42')).toBeUndefined()
63
+ })
64
+
65
+ it('ignores an empty from value (nothing worth carrying)', () => {
66
+ stub()
67
+ persistListReturnState('news', '42', '')
68
+ expect(readListReturnState('news', '42')).toBeUndefined()
69
+ })
70
+
71
+ it('degrades silently when sessionStorage is unavailable', () => {
72
+ // No stub installed — mirrors SSR / privacy-mode contexts.
73
+ expect(() => persistListReturnState('news', '42', 'page=6')).not.toThrow()
74
+ expect(readListReturnState('news', '42')).toBeUndefined()
75
+ expect(() => clearListReturnState('news', '42')).not.toThrow()
76
+ })
77
+ })
@@ -0,0 +1,80 @@
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
+ /**
10
+ * Tab-scoped fallback for the collection editor's return-to-list state.
11
+ *
12
+ * The primary carrier is the `from` URL param (see list-return-state.ts) —
13
+ * pure, shareable, and enough for every in-app navigation. But the preview
14
+ * affordance does a full-page `window.location.assign()` to the public site,
15
+ * which discards the admin URL (and its `from`). When the editor is re-opened
16
+ * from the public preview bar's "Edit" link, that link carries only the
17
+ * collection and document id — no `from` — so the URL alone can no longer say
18
+ * which list page / filter to return to on close.
19
+ *
20
+ * sessionStorage bridges that gap: it survives a full-page navigation within
21
+ * the same tab, is scoped to the tab, and never leaks into shareable URLs. The
22
+ * editor persists `from` whenever it renders with one, keyed by collection +
23
+ * document id, and reads it back only when the URL carries no `from`. Closing
24
+ * or deleting the document consumes (clears) the entry, so a later visit to
25
+ * the same document with no context degrades to the bare list rather than a
26
+ * stale page.
27
+ *
28
+ * Every access is guarded: an absent `sessionStorage` (SSR, privacy modes,
29
+ * quota failures) degrades silently to "no stored state" — never an error.
30
+ */
31
+
32
+ const KEY_PREFIX = 'byline:list-return:'
33
+
34
+ /** Resolve `sessionStorage` defensively — absent under SSR, may throw in some
35
+ * sandboxed / privacy contexts. */
36
+ function storage(): Storage | undefined {
37
+ try {
38
+ return globalThis.sessionStorage ?? undefined
39
+ } catch {
40
+ return undefined
41
+ }
42
+ }
43
+
44
+ function keyFor(collection: string, id: string): string {
45
+ return `${KEY_PREFIX}${collection}:${id}`
46
+ }
47
+
48
+ /** Persist the encoded `from` state for one document. No-op when unavailable. */
49
+ export function persistListReturnState(collection: string, id: string, from: string): void {
50
+ if (from.length === 0) return
51
+ const store = storage()
52
+ if (store == null) return
53
+ try {
54
+ store.setItem(keyFor(collection, id), from)
55
+ } catch {
56
+ // Quota exceeded / storage disabled — persistence is best-effort only.
57
+ }
58
+ }
59
+
60
+ /** Read the stored `from` state for one document, or `undefined`. */
61
+ export function readListReturnState(collection: string, id: string): string | undefined {
62
+ const store = storage()
63
+ if (store == null) return undefined
64
+ try {
65
+ return store.getItem(keyFor(collection, id)) ?? undefined
66
+ } catch {
67
+ return undefined
68
+ }
69
+ }
70
+
71
+ /** Clear the stored `from` state for one document (on close / delete). */
72
+ export function clearListReturnState(collection: string, id: string): void {
73
+ const store = storage()
74
+ if (store == null) return
75
+ try {
76
+ store.removeItem(keyFor(collection, id))
77
+ } catch {
78
+ // Best-effort — nothing to recover if removal fails.
79
+ }
80
+ }