@byline/host-tanstack-start 4.18.0 → 4.19.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.
Files changed (45) hide show
  1. package/dist/admin-shell/collections/create.js +22 -5
  2. package/dist/admin-shell/collections/edit.js +85 -17
  3. package/dist/admin-shell/collections/restore-version-modal.js +14 -2
  4. package/dist/admin-shell/collections/save-navigation.test.d.ts +8 -0
  5. package/dist/admin-shell/singletons/view.js +10 -4
  6. package/dist/server-fns/collections/copy-to-locale.d.ts +2 -1
  7. package/dist/server-fns/collections/copy-to-locale.js +13 -6
  8. package/dist/server-fns/collections/create.d.ts +1 -1
  9. package/dist/server-fns/collections/create.js +15 -7
  10. package/dist/server-fns/collections/delete-locale.d.ts +2 -1
  11. package/dist/server-fns/collections/delete-locale.js +11 -4
  12. package/dist/server-fns/collections/duplicate.d.ts +2 -1
  13. package/dist/server-fns/collections/duplicate.js +10 -3
  14. package/dist/server-fns/collections/index.d.ts +1 -0
  15. package/dist/server-fns/collections/index.js +1 -0
  16. package/dist/server-fns/collections/restore-version.d.ts +1 -1
  17. package/dist/server-fns/collections/restore-version.js +17 -10
  18. package/dist/server-fns/collections/save-outcome.d.ts +23 -0
  19. package/dist/server-fns/collections/save-outcome.js +18 -0
  20. package/dist/server-fns/collections/update.d.ts +3 -1
  21. package/dist/server-fns/collections/update.js +17 -7
  22. package/dist/server-fns/singletons/copy-to-locale.js +14 -5
  23. package/dist/server-fns/singletons/restore-version.js +10 -1
  24. package/dist/server-fns/singletons/update.js +12 -5
  25. package/package.json +10 -10
  26. package/src/admin-shell/collections/create.tsx +22 -3
  27. package/src/admin-shell/collections/edit.tsx +63 -10
  28. package/src/admin-shell/collections/restore-version-modal.tsx +11 -2
  29. package/src/admin-shell/collections/save-navigation.test.tsx +384 -0
  30. package/src/admin-shell/singletons/history.test.tsx +43 -0
  31. package/src/admin-shell/singletons/view.test.tsx +27 -0
  32. package/src/admin-shell/singletons/view.tsx +35 -16
  33. package/src/server-fns/collections/copy-to-locale.ts +43 -29
  34. package/src/server-fns/collections/create.ts +16 -8
  35. package/src/server-fns/collections/delete-locale.ts +38 -24
  36. package/src/server-fns/collections/duplicate.ts +38 -24
  37. package/src/server-fns/collections/index.ts +1 -0
  38. package/src/server-fns/collections/restore-version.ts +16 -9
  39. package/src/server-fns/collections/save-outcome.test.node.ts +77 -0
  40. package/src/server-fns/collections/save-outcome.ts +55 -0
  41. package/src/server-fns/collections/update.ts +23 -8
  42. package/src/server-fns/singletons/copy-to-locale.ts +16 -9
  43. package/src/server-fns/singletons/restore-version.ts +12 -3
  44. package/src/server-fns/singletons/singletons.test.node.ts +34 -0
  45. package/src/server-fns/singletons/update.ts +12 -5
@@ -3,6 +3,7 @@ import { getAdminRequestContext } from "@byline/client/server";
3
3
  import { ERR_NOT_FOUND, getLogger, getServerConfig } from "@byline/core";
4
4
  import { updateDocumentSystemFields, updateDocumentWithPatches } from "@byline/core/services";
5
5
  import { ensureCollection } from "../../integrations/api-utils.js";
6
+ import { toCommittedDocumentHookFailureResponse } from "./save-outcome.js";
6
7
  const updateCollectionDocumentWithPatches = createServerFn({
7
8
  method: 'POST'
8
9
  }).validator((input)=>input).handler(async ({ data: input })=>{
@@ -27,14 +28,23 @@ const updateCollectionDocumentWithPatches = createServerFn({
27
28
  slugifier: serverConfig.slugifier,
28
29
  requestContext: await getAdminRequestContext()
29
30
  };
30
- await updateDocumentWithPatches(ctx, {
31
- documentId: id,
32
- patches,
33
- documentVersionId: versionId,
34
- locale: locale ?? serverConfig.i18n.content.defaultLocale
35
- });
31
+ let result;
32
+ try {
33
+ result = await updateDocumentWithPatches(ctx, {
34
+ documentId: id,
35
+ patches,
36
+ documentVersionId: versionId,
37
+ locale: locale ?? serverConfig.i18n.content.defaultLocale
38
+ });
39
+ } catch (error) {
40
+ const committedFailure = toCommittedDocumentHookFailureResponse(error);
41
+ if (null != committedFailure) return committedFailure;
42
+ throw error;
43
+ }
36
44
  return {
37
- status: 'ok'
45
+ status: 'ok',
46
+ documentId: result.documentId,
47
+ documentVersionId: result.documentVersionId
38
48
  };
39
49
  });
40
50
  const updateCollectionDocumentSystemFields = createServerFn({
@@ -1,11 +1,20 @@
1
1
  import { createServerFn } from "@tanstack/react-start";
2
2
  import { getAdminBylineClient } from "@byline/client/server";
3
+ import { toCommittedDocumentHookFailureResponse } from "../collections/save-outcome.js";
3
4
  import { serialise } from "../serialise.js";
4
5
  const copySingletonToLocale = createServerFn({
5
6
  method: 'POST'
6
- }).validator((input)=>input).handler(async ({ data })=>serialise(await getAdminBylineClient().singleton(data.singleton).copyToLocale({
7
- sourceLocale: data.sourceLocale,
8
- targetLocale: data.targetLocale,
9
- overwrite: data.overwrite
10
- })));
7
+ }).validator((input)=>input).handler(async ({ data })=>{
8
+ try {
9
+ return serialise(await getAdminBylineClient().singleton(data.singleton).copyToLocale({
10
+ sourceLocale: data.sourceLocale,
11
+ targetLocale: data.targetLocale,
12
+ overwrite: data.overwrite
13
+ }));
14
+ } catch (error) {
15
+ const committedFailure = toCommittedDocumentHookFailureResponse(error);
16
+ if (null != committedFailure) return committedFailure;
17
+ throw error;
18
+ }
19
+ });
11
20
  export { copySingletonToLocale };
@@ -1,7 +1,16 @@
1
1
  import { createServerFn } from "@tanstack/react-start";
2
2
  import { getAdminBylineClient } from "@byline/client/server";
3
+ import { toCommittedDocumentHookFailureResponse } from "../collections/save-outcome.js";
3
4
  import { serialise } from "../serialise.js";
4
5
  const restoreSingletonVersion = createServerFn({
5
6
  method: 'POST'
6
- }).validator((input)=>input).handler(async ({ data })=>serialise(await getAdminBylineClient().singleton(data.singleton).restoreVersion(data.versionId)));
7
+ }).validator((input)=>input).handler(async ({ data })=>{
8
+ try {
9
+ return serialise(await getAdminBylineClient().singleton(data.singleton).restoreVersion(data.versionId));
10
+ } catch (error) {
11
+ const committedFailure = toCommittedDocumentHookFailureResponse(error);
12
+ if (null != committedFailure) return committedFailure;
13
+ throw error;
14
+ }
15
+ });
7
16
  export { restoreSingletonVersion };
@@ -1,14 +1,21 @@
1
1
  import { createServerFn } from "@tanstack/react-start";
2
2
  import { getAdminBylineClient } from "@byline/client/server";
3
+ import { toCommittedDocumentHookFailureResponse } from "../collections/save-outcome.js";
3
4
  import { serialise } from "../serialise.js";
4
5
  const updateSingleton = createServerFn({
5
6
  method: 'POST'
6
7
  }).validator((input)=>input).handler(async ({ data })=>{
7
8
  const { singleton, data: fields, locale, expectedVersionId } = data;
8
- const result = await getAdminBylineClient().singleton(singleton).update(fields, {
9
- locale,
10
- expectedVersionId
11
- });
12
- return serialise(result);
9
+ try {
10
+ const result = await getAdminBylineClient().singleton(singleton).update(fields, {
11
+ locale,
12
+ expectedVersionId
13
+ });
14
+ return serialise(result);
15
+ } catch (error) {
16
+ const committedFailure = toCommittedDocumentHookFailureResponse(error);
17
+ if (null != committedFailure) return committedFailure;
18
+ throw error;
19
+ }
13
20
  });
14
21
  export { updateSingleton };
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.18.0",
6
+ "version": "4.19.0",
7
7
  "engines": {
8
8
  "node": ">=20.9.0"
9
9
  },
@@ -125,15 +125,15 @@
125
125
  "react-json-view-lite": "^2.5.0",
126
126
  "react-swipeable": "^7.0.2",
127
127
  "zod": "^4.4.3",
128
- "@byline/admin": "4.18.0",
129
- "@byline/ai": "4.18.0",
130
- "@byline/analytics-agent": "4.18.0",
131
- "@byline/client": "4.18.0",
132
- "@byline/auth": "4.18.0",
133
- "@byline/analytics": "4.18.0",
134
- "@byline/i18n": "4.18.0",
135
- "@byline/core": "4.18.0",
136
- "@byline/ui": "4.18.0"
128
+ "@byline/admin": "4.19.0",
129
+ "@byline/analytics": "4.19.0",
130
+ "@byline/analytics-agent": "4.19.0",
131
+ "@byline/ai": "4.19.0",
132
+ "@byline/core": "4.19.0",
133
+ "@byline/client": "4.19.0",
134
+ "@byline/auth": "4.19.0",
135
+ "@byline/i18n": "4.19.0",
136
+ "@byline/ui": "4.19.0"
137
137
  },
138
138
  "peerDependencies": {
139
139
  "@tanstack/react-router": "^1.167.0",
@@ -15,7 +15,10 @@ import { Container, Section, useToastManager } from '@byline/ui/react'
15
15
 
16
16
  import { getAdminRoutePath } from '../../routes/admin-path.js'
17
17
  import { decodeListReturnState } from '../../routes/list-return-state.js'
18
- import { createCollectionDocument } from '../../server-fns/collections/index.js'
18
+ import {
19
+ createCollectionDocument,
20
+ hasCommittedDocumentHookFailure,
21
+ } from '../../server-fns/collections/index.js'
19
22
  import { useNavigate } from '../chrome/loose-router.js'
20
23
  import { useTanStackNavigationGuard } from './tanstack-navigation-guard.js'
21
24
 
@@ -65,6 +68,14 @@ export const CreateView = ({
65
68
  ...(systemAvailableLocales ? { availableLocales: systemAvailableLocales } : {}),
66
69
  },
67
70
  })
71
+ const hookFailed = hasCommittedDocumentHookFailure(result)
72
+ if (hookFailed) {
73
+ toastManager.add({
74
+ title: t('collections.save.hookFailedToast'),
75
+ description: t('collections.save.hookFailedDescription'),
76
+ data: { intent: 'warning', iconType: 'warning', icon: true, close: true },
77
+ })
78
+ }
68
79
  // Create → edit: land the editor on the new document, where the rest
69
80
  // of the work (content, status, relations) happens. `from` threads
70
81
  // forward so closing the brand-new document also returns to the
@@ -75,13 +86,21 @@ export const CreateView = ({
75
86
  navigate({
76
87
  to: getAdminRoutePath('collections', '$collection', '$id'),
77
88
  params: { collection: path, id: result.documentId } as never,
78
- search: { action: 'created', from },
89
+ search: { ...(hookFailed ? {} : { action: 'created' as const }), from },
90
+ // Persistence has succeeded, but FormRenderer cannot commit its
91
+ // clean baseline until this handler returns. Do not let that brief
92
+ // dirty-state overlap block the intentional create → edit redirect.
93
+ ignoreBlocker: true,
79
94
  })
80
95
  } else {
81
96
  navigate({
82
97
  to: getAdminRoutePath('collections', '$collection'),
83
98
  params: { collection: path },
84
- search: { ...decodeListReturnState(from), action: 'created' },
99
+ search: {
100
+ ...decodeListReturnState(from),
101
+ ...(hookFailed ? {} : { action: 'created' as const }),
102
+ },
103
+ ignoreBlocker: true,
85
104
  })
86
105
  }
87
106
  } catch (err) {
@@ -29,6 +29,7 @@ import {
29
29
  deleteDocument,
30
30
  deleteDocumentLocale,
31
31
  duplicateCollectionDocument,
32
+ hasCommittedDocumentHookFailure,
32
33
  hasDeleteSideEffectFailures,
33
34
  scheduleCollectionDocumentPublish,
34
35
  unpublishDocument,
@@ -43,7 +44,7 @@ import type { SerializedDocumentPublishSchedule } from '../../server-fns/collect
43
44
  import type { ContentLocaleOption } from './view-menu.js'
44
45
 
45
46
  type EditState = {
46
- status: 'success' | 'failed' | 'busy' | 'idle'
47
+ status: 'success' | 'warning' | 'failed' | 'busy' | 'idle'
47
48
  message: string
48
49
  }
49
50
 
@@ -110,6 +111,16 @@ export const EditView = ({
110
111
  })
111
112
  }
112
113
 
114
+ const notifyCommittedHookFailure = () => {
115
+ const description = t('collections.save.hookFailedDescription')
116
+ toastManager.add({
117
+ title: t('collections.save.hookFailedToast'),
118
+ description,
119
+ data: { intent: 'warning', iconType: 'warning', icon: true, close: true },
120
+ })
121
+ setEditState({ status: 'warning', message: description })
122
+ }
123
+
113
124
  const handleLocaleChange = (newLocale: string) => {
114
125
  navigate({
115
126
  to: getAdminRoutePath('collections', '$collection', '$id'),
@@ -306,6 +317,15 @@ export const EditView = ({
306
317
  const result = await duplicateCollectionDocument({
307
318
  data: { collection: path, id: String(initialData.id) },
308
319
  })
320
+ if (hasCommittedDocumentHookFailure(result)) {
321
+ notifyCommittedHookFailure()
322
+ navigate({
323
+ to: getAdminRoutePath('collections', '$collection', '$id'),
324
+ params: { collection: path, id: result.documentId },
325
+ search: (prev: Record<string, unknown>) => ({ ...prev }),
326
+ })
327
+ return
328
+ }
309
329
  const description = result.pathRetried
310
330
  ? t('collections.edit.duplicatedAutoPathDescription', { path: result.newPath })
311
331
  : t('collections.edit.duplicatedPathDescription', { path: result.newPath })
@@ -367,6 +387,16 @@ export const EditView = ({
367
387
  overwrite,
368
388
  },
369
389
  })
390
+ if (hasCommittedDocumentHookFailure(result)) {
391
+ notifyCommittedHookFailure()
392
+ notifyScheduleSuspended()
393
+ navigate({
394
+ to: getAdminRoutePath('collections', '$collection', '$id'),
395
+ params: { collection: path, id: String(initialData.id) },
396
+ search: (prev: Record<string, unknown>) => ({ ...prev, locale: targetLocale }),
397
+ })
398
+ return
399
+ }
370
400
  const sourceLabel =
371
401
  contentLocales.find((l) => l.code === result.sourceLocale)?.label ?? result.sourceLocale
372
402
  const targetLabel =
@@ -435,6 +465,16 @@ export const EditView = ({
435
465
  locale: targetLocale,
436
466
  },
437
467
  })
468
+ if (hasCommittedDocumentHookFailure(result)) {
469
+ notifyCommittedHookFailure()
470
+ notifyScheduleSuspended()
471
+ navigate({
472
+ to: getAdminRoutePath('collections', '$collection', '$id'),
473
+ params: { collection: path, id: String(initialData.id) },
474
+ search: (prev: Record<string, unknown>) => ({ ...prev, locale: defaultContentLocale }),
475
+ })
476
+ return
477
+ }
438
478
  const targetLabel =
439
479
  contentLocales.find((l) => l.code === result.locale)?.label ?? result.locale
440
480
  const description = t('collections.edit.deletedLocaleDescription', {
@@ -498,7 +538,10 @@ export const EditView = ({
498
538
  close: true,
499
539
  },
500
540
  })
501
- setEditState({ status: 'success', message: description })
541
+ setEditState({
542
+ status: hasSideEffectFailures ? 'warning' : 'success',
543
+ message: description,
544
+ })
502
545
  // Navigate back to the collection list after deletion. The return
503
546
  // target has now been consumed — clear the stored fallback so a later
504
547
  // visit doesn't resurrect a stale list position.
@@ -550,6 +593,7 @@ export const EditView = ({
550
593
  systemAvailableLocales?: string[]
551
594
  }) => {
552
595
  try {
596
+ let hookFailed = false
553
597
  // Document-grain system fields write first via their own non-versioned
554
598
  // path — so a path conflict surfaces before we mint a content version,
555
599
  // and these immediate writes never reset workflow status. See
@@ -571,7 +615,7 @@ export const EditView = ({
571
615
  // fields changed, so a path/advertising edit never creates an empty
572
616
  // content version.
573
617
  if (contentDirty) {
574
- await updateCollectionDocumentWithPatches({
618
+ const result = await updateCollectionDocumentWithPatches({
575
619
  data: {
576
620
  collection: path,
577
621
  id: String(initialData.id),
@@ -580,33 +624,42 @@ export const EditView = ({
580
624
  locale,
581
625
  },
582
626
  })
627
+ hookFailed = hasCommittedDocumentHookFailure(result)
583
628
  }
584
629
 
585
630
  if (contentDirty && scheduledPublication?.state === 'armed') {
586
631
  notifyScheduleSuspended()
587
632
  }
588
633
 
589
- const description = t('collections.edit.updatedDescription', { label: singularLower })
634
+ const description = hookFailed
635
+ ? t('collections.save.hookFailedDescription')
636
+ : t('collections.edit.updatedDescription', { label: singularLower })
590
637
  toastManager.add({
591
- title: t('collections.edit.updateTitle', { label: singular }),
638
+ title: hookFailed
639
+ ? t('collections.save.hookFailedToast')
640
+ : t('collections.edit.updateTitle', { label: singular }),
592
641
  description,
593
642
  data: {
594
- intent: 'success',
595
- iconType: 'success',
643
+ intent: hookFailed ? 'warning' : 'success',
644
+ iconType: hookFailed ? 'warning' : 'success',
596
645
  icon: true,
597
646
  close: true,
598
647
  },
599
648
  })
600
649
 
601
- setEditState({ status: 'success', message: description })
650
+ setEditState({ status: hookFailed ? 'warning' : 'success', message: description })
602
651
 
603
652
  // Re-navigate to the same route so the loader re-fetches the document.
604
- // The new version will have a fresh version ID, draft status, and
605
- // updated publishedVersion metadata.
653
+ // The new version will have a fresh version ID, the collection's default
654
+ // workflow status, and updated publishedVersion metadata.
606
655
  navigate({
607
656
  to: getAdminRoutePath('collections', '$collection', '$id'),
608
657
  params: { collection: path, id: String(initialData.id) },
609
658
  search: (prev: Record<string, unknown>) => ({ ...prev }),
659
+ // Every write above has succeeded. FormRenderer clears its dirty
660
+ // baseline after this handler returns, so bypass the guard during the
661
+ // intentional post-save refresh rather than racing that state update.
662
+ ignoreBlocker: true,
610
663
  })
611
664
  } catch (err) {
612
665
  console.error('Network error:', err)
@@ -19,9 +19,10 @@
19
19
  import { useState } from 'react'
20
20
 
21
21
  import { useTranslation } from '@byline/i18n/react'
22
- import { Alert, Button, LoaderEllipsis, Modal } from '@byline/ui/react'
22
+ import { Alert, Button, LoaderEllipsis, Modal, useToastManager } from '@byline/ui/react'
23
23
  import cx from 'clsx'
24
24
 
25
+ import { hasCommittedDocumentHookFailure } from '../../server-fns/collections/save-outcome.js'
25
26
  import styles from './restore-version-modal.module.css'
26
27
 
27
28
  interface RestoreVersionModalProps {
@@ -42,6 +43,7 @@ export function RestoreVersionModal({
42
43
  onRestoreComplete,
43
44
  }: RestoreVersionModalProps) {
44
45
  const { t } = useTranslation('byline-admin')
46
+ const toastManager = useToastManager()
45
47
  const [error, setError] = useState<string | null>(null)
46
48
  const [pending, setPending] = useState(false)
47
49
 
@@ -50,7 +52,14 @@ export function RestoreVersionModal({
50
52
  setPending(true)
51
53
  setError(null)
52
54
  try {
53
- await restoreVersion()
55
+ const result = await restoreVersion()
56
+ if (hasCommittedDocumentHookFailure(result)) {
57
+ toastManager.add({
58
+ title: t('collections.save.hookFailedToast'),
59
+ description: t('collections.save.hookFailedDescription'),
60
+ data: { intent: 'warning', iconType: 'warning', icon: true, close: true },
61
+ })
62
+ }
54
63
  onClose()
55
64
  await onRestoreComplete()
56
65
  } catch (err) {