@byline/host-tanstack-start 6.2.1 → 6.4.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 (39) hide show
  1. package/dist/admin-shell/chrome/breadcrumbs/breadcrumbs-provider.js +13 -2
  2. package/dist/admin-shell/chrome/breadcrumbs/breadcrumbs.js +2 -6
  3. package/dist/admin-shell/chrome/breadcrumbs/breadcrumbs.test.d.ts +8 -0
  4. package/dist/admin-shell/collections/api.js +3 -2
  5. package/dist/admin-shell/collections/edit.js +3 -2
  6. package/dist/admin-shell/collections/restore-version-modal.js +19 -0
  7. package/dist/admin-shell/describe-mutation-failure.d.ts +18 -0
  8. package/dist/admin-shell/describe-mutation-failure.js +9 -0
  9. package/dist/admin-shell/singletons/view.js +2 -1
  10. package/dist/integrations/byline-field-services.d.ts +19 -0
  11. package/dist/integrations/byline-field-services.js +18 -2
  12. package/dist/routes/create-admin-layout-route.js +6 -3
  13. package/dist/server-fns/collections/create.js +4 -2
  14. package/dist/server-fns/collections/restore-version.d.ts +1 -0
  15. package/dist/server-fns/collections/restore-version.js +4 -1
  16. package/dist/server-fns/collections/update.js +3 -2
  17. package/dist/server-fns/document-mutation-errors.js +7 -1
  18. package/dist/server-fns/document-write-input.d.ts +9 -0
  19. package/dist/server-fns/document-write-input.js +44 -0
  20. package/dist/server-fns/singletons/restore-version.d.ts +1 -1
  21. package/package.json +16 -16
  22. package/src/admin-shell/chrome/breadcrumbs/breadcrumbs-provider.tsx +34 -4
  23. package/src/admin-shell/chrome/breadcrumbs/breadcrumbs.test.tsx +274 -0
  24. package/src/admin-shell/chrome/breadcrumbs/breadcrumbs.tsx +7 -7
  25. package/src/admin-shell/collections/api.tsx +17 -2
  26. package/src/admin-shell/collections/edit.tsx +3 -2
  27. package/src/admin-shell/collections/restore-version-modal.tsx +26 -0
  28. package/src/admin-shell/describe-mutation-failure.ts +30 -0
  29. package/src/admin-shell/singletons/view.tsx +2 -1
  30. package/src/integrations/byline-field-services.test.node.ts +129 -0
  31. package/src/integrations/byline-field-services.ts +37 -0
  32. package/src/routes/create-admin-layout-route.tsx +17 -6
  33. package/src/server-fns/collections/create.ts +5 -2
  34. package/src/server-fns/collections/restore-version.ts +4 -0
  35. package/src/server-fns/collections/update.ts +3 -2
  36. package/src/server-fns/document-mutation-errors.ts +8 -0
  37. package/src/server-fns/document-mutation-transport.test.node.ts +41 -0
  38. package/src/server-fns/document-write-input.test.node.ts +48 -0
  39. package/src/server-fns/document-write-input.ts +67 -0
@@ -1,16 +1,27 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { createContext, useContext, useState } from "react";
2
+ import { createContext, useCallback, useContext, useState } from "react";
3
3
  const BreadcrumbsContext = /*#__PURE__*/ createContext(void 0);
4
+ function sameBreadcrumbs(a, b) {
5
+ if (a === b) return true;
6
+ if (a.length !== b.length) return false;
7
+ return a.every((crumb, index)=>crumb.label === b[index].label && crumb.href === b[index].href && crumb.active === b[index].active);
8
+ }
9
+ function sameSettings(a, b) {
10
+ return a.homeLabel === b.homeLabel && a.homePath === b.homePath && sameBreadcrumbs(a.breadcrumbs, b.breadcrumbs);
11
+ }
4
12
  function BreadcrumbsProvider({ children }) {
5
13
  const [breadcrumbSettings, setBreadcrumbSettings] = useState({
6
14
  homeLabel: 'Home',
7
15
  homePath: '/',
8
16
  breadcrumbs: []
9
17
  });
18
+ const setBreadcrumbs = useCallback((settings)=>{
19
+ setBreadcrumbSettings((previous)=>sameSettings(previous, settings) ? previous : settings);
20
+ }, []);
10
21
  return /*#__PURE__*/ jsx(BreadcrumbsContext, {
11
22
  value: {
12
23
  breadCrumbSettings: breadcrumbSettings,
13
- setBreadcrumbs: setBreadcrumbSettings
24
+ setBreadcrumbs
14
25
  },
15
26
  children: children
16
27
  });
@@ -18,11 +18,6 @@ function Breadcrumbs({ breadcrumbs, className, homeLabel = 'Home', homePath = '/
18
18
  const navRef = useRef(null);
19
19
  const measureRef = useRef(null);
20
20
  const [visibleIndices, setVisibleIndices] = useState(()=>breadcrumbs.map((_, i)=>i));
21
- useIsoLayoutEffect(()=>{
22
- setVisibleIndices(breadcrumbs.map((_, i)=>i));
23
- }, [
24
- breadcrumbs
25
- ]);
26
21
  useIsoLayoutEffect(()=>{
27
22
  const nav = navRef.current;
28
23
  const measure = measureRef.current;
@@ -71,7 +66,8 @@ function Breadcrumbs({ breadcrumbs, className, homeLabel = 'Home', homePath = '/
71
66
  ro.observe(nav);
72
67
  return ()=>ro.disconnect();
73
68
  }, [
74
- breadcrumbs
69
+ breadcrumbs,
70
+ homeLabel
75
71
  ]);
76
72
  const overflowed = useMemo(()=>{
77
73
  const visible = new Set(visibleIndices);
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ export {};
@@ -2,10 +2,11 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useTranslation } from "@byline/i18n/react";
3
3
  import { Container, Section } from "@byline/ui/react";
4
4
  import clsx from "clsx";
5
- import { JsonView, allExpanded, darkStyles } from "react-json-view-lite";
5
+ import { JsonView, darkStyles } from "react-json-view-lite";
6
6
  import api_module from "./api.module.js";
7
7
  import { ViewMenu } from "./view-menu.js";
8
8
  import "react-json-view-lite/dist/index.css";
9
+ const expandToFirstLevel = (level)=>level < 2;
9
10
  const ApiView = ({ collectionDefinition, initialData, locale, depth, contentLocales, defaultContentLocale })=>{
10
11
  const { labels, path } = collectionDefinition;
11
12
  const { t } = useTranslation('byline-admin');
@@ -38,7 +39,7 @@ const ApiView = ({ collectionDefinition, initialData, locale, depth, contentLoca
38
39
  className: clsx('byline-api-viewer', api_module.viewer),
39
40
  children: /*#__PURE__*/ jsx(JsonView, {
40
41
  data: initialData,
41
- shouldExpandNode: allExpanded,
42
+ shouldExpandNode: expandToFirstLevel,
42
43
  style: {
43
44
  ...darkStyles,
44
45
  container: 'api-json-view'
@@ -8,6 +8,7 @@ import { getAdminRoutePath } from "../../routes/admin-path.js";
8
8
  import { clearListReturnState } from "../../routes/list-return-storage.js";
9
9
  import { cancelCollectionDocumentScheduledPublish, confirmCollectionDocumentScheduledPublish, copyDocumentToLocale, deleteDocument, deleteDocumentLocale, duplicateCollectionDocument, hasCommittedDocumentHookFailure, hasDeleteSideEffectFailures, scheduleCollectionDocumentPublish, unpublishDocument, updateCollectionDocumentWithPatches, updateDocumentStatus } from "../../server-fns/collections/index.js";
10
10
  import { useNavigate } from "../chrome/loose-router.js";
11
+ import { describeMutationFailure } from "../describe-mutation-failure.js";
11
12
  import { useDocumentMutationState } from "../document-mutation-state.js";
12
13
  import { useTanStackNavigationGuard } from "./tanstack-navigation-guard.js";
13
14
  import { ViewMenu } from "./view-menu.js";
@@ -432,7 +433,7 @@ const EditView = ({ collectionDefinition, adminConfig, initialData: loadedData,
432
433
  }
433
434
  console.error('Duplicate error:', err);
434
435
  const description = t("collections.edit.duplicateFailedDescription", {
435
- message: t('documentConcurrency.failed')
436
+ message: describeMutationFailure(err, t)
436
437
  });
437
438
  toastManager.add({
438
439
  title: t('collections.edit.duplicateTitle', {
@@ -533,7 +534,7 @@ const EditView = ({ collectionDefinition, adminConfig, initialData: loadedData,
533
534
  }
534
535
  console.error('Copy to locale error:', err);
535
536
  const description = t("collections.edit.copyFailedDescription", {
536
- message: t('documentConcurrency.failed')
537
+ message: describeMutationFailure(err, t)
537
538
  });
538
539
  toastManager.add({
539
540
  title: t('collections.edit.copyToLocaleTitle', {
@@ -17,6 +17,19 @@ function RestoreVersionModal({ disabled = false, onMutationError, versionLabel,
17
17
  setError(null);
18
18
  try {
19
19
  const result = await restoreVersion();
20
+ const issues = getRestoreValidationIssues(result);
21
+ if (issues.length > 0) toastManager.add({
22
+ title: t('collections.restore.validationIssuesToast'),
23
+ description: t("collections.restore.validationIssuesDescription", {
24
+ fields: issues.join(', ')
25
+ }),
26
+ data: {
27
+ intent: 'warning',
28
+ iconType: 'warning',
29
+ icon: true,
30
+ close: true
31
+ }
32
+ });
20
33
  if (hasCommittedDocumentHookFailure(result)) toastManager.add({
21
34
  title: t('collections.save.hookFailedToast'),
22
35
  description: t("collections.save.hookFailedDescription"),
@@ -126,6 +139,12 @@ function RestoreVersionModal({ disabled = false, onMutationError, versionLabel,
126
139
  ]
127
140
  });
128
141
  }
142
+ function getRestoreValidationIssues(result) {
143
+ if (null == result || 'object' != typeof result) return [];
144
+ const issues = result.validationIssues;
145
+ if (!Array.isArray(issues)) return [];
146
+ return issues.map((issue)=>null != issue && 'object' == typeof issue && 'string' == typeof issue.field ? issue.field : '').filter((field)=>field.length > 0);
147
+ }
129
148
  function getErrorCode(err) {
130
149
  return 'string' == typeof err?.code ? err.code : null;
131
150
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type { TranslationValues } from '@byline/i18n';
9
+ /**
10
+ * Describe a failed document mutation for the editor.
11
+ *
12
+ * Content writes that are not restores — create, update, patch saves,
13
+ * duplication and locale copies — keep the field-validation gate, because each
14
+ * reads or authors content the editor can open and correct. Naming the
15
+ * offending fields is what makes that correction actionable; without it the
16
+ * editor sees only a generic failure.
17
+ */
18
+ export declare function describeMutationFailure(err: unknown, t: (key: string, values?: TranslationValues) => string): string;
@@ -0,0 +1,9 @@
1
+ import { getDocumentFieldValidationDetails } from "@byline/core";
2
+ function describeMutationFailure(err, t) {
3
+ const validation = getDocumentFieldValidationDetails(err);
4
+ if (null == validation) return t('documentConcurrency.failed');
5
+ return t('collections.edit.invalidFieldsMessage', {
6
+ fields: validation.issues.map((issue)=>issue.field || '?').join(', ')
7
+ });
8
+ }
9
+ export { describeMutationFailure };
@@ -10,6 +10,7 @@ import { hasCommittedDocumentHookFailure } from "../../server-fns/collections/in
10
10
  import { cancelSingletonScheduledPublish, changeSingletonStatus, confirmSingletonScheduledPublish, copySingletonToLocale, scheduleSingletonPublish, unpublishSingleton, updateSingleton } from "../../server-fns/singletons/index.js";
11
11
  import { useNavigate } from "../chrome/loose-router.js";
12
12
  import { useTanStackNavigationGuard } from "../collections/tanstack-navigation-guard.js";
13
+ import { describeMutationFailure } from "../describe-mutation-failure.js";
13
14
  import { useDocumentMutationState } from "../document-mutation-state.js";
14
15
  import { SingletonViewMenu } from "./view-menu.js";
15
16
  function SingletonView({ singletonDefinition, adminConfig, document: loadedDocument, initialData, locale, contentLocales, defaultContentLocale }) {
@@ -201,7 +202,7 @@ function SingletonView({ singletonDefinition, adminConfig, document: loadedDocum
201
202
  toast(t('collections.edit.copyToLocaleTitle', {
202
203
  label
203
204
  }), t("collections.edit.copyFailedDescription", {
204
- message: t('documentConcurrency.failed')
205
+ message: describeMutationFailure(err, t)
205
206
  }), 'danger');
206
207
  throw err;
207
208
  }
@@ -16,3 +16,22 @@
16
16
  */
17
17
  import type { BylineFieldServices } from '@byline/admin/react';
18
18
  export declare const bylineFieldServices: BylineFieldServices;
19
+ /** The subset of the admin session the field services need. */
20
+ export interface FieldServicesViewer {
21
+ is_super_admin: boolean;
22
+ abilities: ReadonlyArray<string>;
23
+ }
24
+ /**
25
+ * Content-addressed memo key for the services object.
26
+ *
27
+ * Avoids notifying field-service context consumers when the host supplies an
28
+ * equivalent viewer object or abilities array. Existing service functions retain
29
+ * their module-level identity even when the containing object is rebuilt.
30
+ * Ability order is not meaningful, so sort a copy before building the key.
31
+ */
32
+ export declare function abilityFingerprint(viewer: FieldServicesViewer): string;
33
+ /**
34
+ * Field services for one viewer. Call inside `useMemo`, keyed on
35
+ * `abilityFingerprint(viewer)`.
36
+ */
37
+ export declare function buildBylineFieldServices(viewer: FieldServicesViewer): BylineFieldServices;
@@ -1,3 +1,4 @@
1
+ import { getAdminRoutePath } from "../routes/admin-path.js";
1
2
  import { getCollectionDocuments } from "../server-fns/collections/list.js";
2
3
  import { getTreeAncestors, getTreeParent, placeTreeNode, removeFromTree } from "../server-fns/collections/tree.js";
3
4
  import { uploadField } from "../server-fns/collections/upload.js";
@@ -20,12 +21,27 @@ const byline_field_services_getTreeAncestors = (input)=>getTreeAncestors({
20
21
  const byline_field_services_getTreeParent = (input)=>getTreeParent({
21
22
  data: input
22
23
  });
24
+ const getCreateDocumentUrl = (collectionPath)=>getAdminRoutePath('collections', collectionPath, 'create');
23
25
  const bylineFieldServices = {
24
26
  getCollectionDocuments: byline_field_services_getCollectionDocuments,
25
27
  uploadField: byline_field_services_uploadField,
26
28
  placeTreeNode: byline_field_services_placeTreeNode,
27
29
  removeFromTree: byline_field_services_removeFromTree,
28
30
  getTreeAncestors: byline_field_services_getTreeAncestors,
29
- getTreeParent: byline_field_services_getTreeParent
31
+ getTreeParent: byline_field_services_getTreeParent,
32
+ getCreateDocumentUrl: getCreateDocumentUrl
30
33
  };
31
- export { bylineFieldServices };
34
+ function abilityFingerprint(viewer) {
35
+ if (viewer.is_super_admin) return 'super-admin';
36
+ return [
37
+ ...viewer.abilities
38
+ ].sort().join(' ');
39
+ }
40
+ function buildBylineFieldServices(viewer) {
41
+ const canCreateInCollection = (collectionPath)=>viewer.is_super_admin || viewer.abilities.includes(`collections.${collectionPath}.create`);
42
+ return {
43
+ ...bylineFieldServices,
44
+ canCreateInCollection
45
+ };
46
+ }
47
+ export { abilityFingerprint, buildBylineFieldServices, bylineFieldServices };
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { useEffect } from "react";
2
+ import { useEffect, useMemo } from "react";
3
3
  import { Outlet, createFileRoute } from "@tanstack/react-router";
4
4
  import { applyStoredTheme } from "@byline/admin/admin-account/components/theme";
5
5
  import { BylineFieldServicesProvider } from "@byline/admin/react";
@@ -18,7 +18,7 @@ import { SessionChangeBoundary } from "../admin-shell/chrome/session-change-boun
18
18
  import { buildLocaleDefinitions } from "../i18n/locale-definitions.js";
19
19
  import { bylineAdminServices } from "../integrations/byline-admin-services.js";
20
20
  import { BylineAiAdminProvider } from "../integrations/byline-ai.js";
21
- import { bylineFieldServices } from "../integrations/byline-field-services.js";
21
+ import { abilityFingerprint, buildBylineFieldServices } from "../integrations/byline-field-services.js";
22
22
  import { observeSession, sessionIsChanged } from "../integrations/session-coordination.js";
23
23
  import { renewExpectedSession } from "../integrations/session-renewal.js";
24
24
  import { getAnalyticsRuntime } from "../server-fns/analytics/index.js";
@@ -58,6 +58,9 @@ function createAdminLayoutRoute(path) {
58
58
  useEffect(()=>{
59
59
  applyStoredTheme();
60
60
  }, []);
61
+ const fieldServices = useMemo(()=>buildBylineFieldServices(user), [
62
+ abilityFingerprint(user)
63
+ ]);
61
64
  const handleSetLocale = async (next)=>{
62
65
  await setAdminLocaleFn({
63
66
  data: {
@@ -77,7 +80,7 @@ function createAdminLayoutRoute(path) {
77
80
  children: /*#__PURE__*/ jsx(BylineAdminServicesProvider, {
78
81
  services: bylineAdminServices,
79
82
  children: /*#__PURE__*/ jsx(BylineFieldServicesProvider, {
80
- services: bylineFieldServices,
83
+ services: fieldServices,
81
84
  children: /*#__PURE__*/ jsx(BylineAiAdminProvider, {
82
85
  children: /*#__PURE__*/ jsxs(AdminMenuProvider, {
83
86
  children: [
@@ -2,15 +2,17 @@ import { createServerFn } from "@tanstack/react-start";
2
2
  import { getAdminRequestContext } from "@byline/client/server";
3
3
  import { ERR_NOT_FOUND, getLogger, getServerConfig } from "@byline/core";
4
4
  import { createDocument } from "@byline/core/services";
5
+ import { getDefaultStatus } from "@byline/core/workflow";
5
6
  import { ensureCollection } from "../../integrations/api-utils.js";
6
7
  import { adminSessionMiddleware } from "../../integrations/session-middleware.js";
7
8
  import { withDocumentMutationErrors } from "../document-mutation-errors.js";
9
+ import { validateDocumentWriteInput } from "../document-write-input.js";
8
10
  import { toCommittedDocumentHookFailureResponse } from "./save-outcome.js";
9
11
  const createCollectionDocument = createServerFn({
10
12
  method: 'POST'
11
13
  }).middleware([
12
14
  adminSessionMiddleware
13
- ]).validator((input)=>input).handler(withDocumentMutationErrors(async ({ data: input })=>{
15
+ ]).validator((input)=>validateDocumentWriteInput(input, 'create')).handler(withDocumentMutationErrors(async ({ data: input })=>{
14
16
  const { collection: path, data: documentData, locale, path: explicitPath, availableLocales } = input;
15
17
  const logger = getLogger();
16
18
  const config = await ensureCollection(path);
@@ -36,7 +38,7 @@ const createCollectionDocument = createServerFn({
36
38
  try {
37
39
  result = await createDocument(ctx, {
38
40
  data: structuredClone(documentData),
39
- status: documentData.status,
41
+ status: getDefaultStatus(config.definition),
40
42
  locale: locale ?? serverConfig.i18n.content.defaultLocale,
41
43
  path: explicitPath,
42
44
  availableLocales
@@ -23,4 +23,5 @@ export declare const restoreDocumentVersion: import("@tanstack/react-start").Req
23
23
  revision: number;
24
24
  documentVersionId: string;
25
25
  sourceVersionId: string;
26
+ validationIssues?: import("@byline/core").DocumentFieldIssue[] | undefined;
26
27
  }>>;
@@ -43,7 +43,10 @@ const restore_version_restoreDocumentVersion = createServerFn({
43
43
  documentId: result.documentId,
44
44
  revision: result.revision,
45
45
  documentVersionId: result.documentVersionId,
46
- sourceVersionId: result.sourceVersionId
46
+ sourceVersionId: result.sourceVersionId,
47
+ ...result.validationIssues ? {
48
+ validationIssues: result.validationIssues
49
+ } : {}
47
50
  };
48
51
  } catch (error) {
49
52
  const committedFailure = toCommittedDocumentHookFailureResponse(error);
@@ -5,12 +5,13 @@ import { saveDocument, updateDocumentSystemFields } from "@byline/core/services"
5
5
  import { ensureCollection } from "../../integrations/api-utils.js";
6
6
  import { adminSessionMiddleware } from "../../integrations/session-middleware.js";
7
7
  import { withDocumentMutationErrors } from "../document-mutation-errors.js";
8
+ import { validateDocumentWriteInput } from "../document-write-input.js";
8
9
  import { toCommittedDocumentHookFailureResponse } from "./save-outcome.js";
9
10
  const updateCollectionDocumentWithPatches = createServerFn({
10
11
  method: 'POST'
11
12
  }).middleware([
12
13
  adminSessionMiddleware
13
- ]).validator((input)=>input).handler(withDocumentMutationErrors(async ({ data: input })=>{
14
+ ]).validator((input)=>validateDocumentWriteInput(input, 'patch')).handler(withDocumentMutationErrors(async ({ data: input })=>{
14
15
  const { collection: path, id, patches, expectedRevision, locale, path: explicitPath, availableLocales } = input;
15
16
  const logger = getLogger();
16
17
  const config = await ensureCollection(path);
@@ -60,7 +61,7 @@ const updateCollectionDocumentSystemFields = createServerFn({
60
61
  method: 'POST'
61
62
  }).middleware([
62
63
  adminSessionMiddleware
63
- ]).validator((input)=>input).handler(withDocumentMutationErrors(async ({ data: input })=>{
64
+ ]).validator((input)=>validateDocumentWriteInput(input, 'metadata')).handler(withDocumentMutationErrors(async ({ data: input })=>{
64
65
  const { collection: path, id, expectedRevision, locale, path: explicitPath, availableLocales } = input;
65
66
  const logger = getLogger();
66
67
  const config = await ensureCollection(path);
@@ -1,4 +1,4 @@
1
- import { BylineError, ErrorCodes, getDocumentRevisionValidationDetails, getDocumentStaleDetails, getLockConflictDetails } from "@byline/core";
1
+ import { BylineError, ErrorCodes, getDocumentFieldValidationDetails, getDocumentRevisionValidationDetails, getDocumentStaleDetails, getLockConflictDetails } from "@byline/core";
2
2
  import { getDocumentHookCommittedDetails } from "@byline/core/services";
3
3
  import { committedTreeReceipt } from "./structural-receipt.js";
4
4
  function documentMutationError(error) {
@@ -14,6 +14,12 @@ function documentMutationError(error) {
14
14
  message: 'Reload this document before making changes.',
15
15
  details: validation
16
16
  };
17
+ const fields = getDocumentFieldValidationDetails(error);
18
+ if (fields) return {
19
+ code: ErrorCodes.VALIDATION,
20
+ message: 'Some document fields are invalid.',
21
+ details: fields
22
+ };
17
23
  const lock = getLockConflictDetails(error);
18
24
  if (lock) return {
19
25
  code: ErrorCodes.LOCK_CONFLICT,
@@ -0,0 +1,9 @@
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
+ /** Shape checks complement lifecycle authorization, revision and field validation. */
9
+ export declare function validateDocumentWriteInput<T>(input: T, mode: 'create' | 'patch' | 'metadata'): T;
@@ -0,0 +1,44 @@
1
+ import { ERR_VALIDATION, parseInstancePath } from "@byline/core";
2
+ const object = (value)=>null !== value && 'object' == typeof value && !Array.isArray(value);
3
+ const document_write_input_text = (value)=>'string' == typeof value && value.trim().length > 0;
4
+ const index = (value)=>Number.isSafeInteger(value) && value >= 0;
5
+ const path = (value)=>document_write_input_text(value) && parseInstancePath(value).ok;
6
+ function patches(value) {
7
+ return Array.isArray(value) && value.every((patch)=>{
8
+ if (!object(patch) || !path(patch.path)) return false;
9
+ if (void 0 !== patch.opId && !document_write_input_text(patch.opId)) return false;
10
+ if (void 0 !== patch.timestamp && !document_write_input_text(patch.timestamp)) return false;
11
+ switch(patch.kind){
12
+ case 'field.set':
13
+ return 'value' in patch;
14
+ case 'field.clear':
15
+ return true;
16
+ case 'array.insert':
17
+ return 'item' in patch && (void 0 === patch.index || index(patch.index));
18
+ case 'array.move':
19
+ return document_write_input_text(patch.itemId) && index(patch.toIndex);
20
+ case 'array.remove':
21
+ return document_write_input_text(patch.itemId);
22
+ case 'array.updateItem':
23
+ return document_write_input_text(patch.itemId) && patches(patch.patches);
24
+ case 'block.add':
25
+ return document_write_input_text(patch.blockType) && (void 0 === patch.index || index(patch.index));
26
+ case 'block.move':
27
+ return document_write_input_text(patch.blockId) && index(patch.toIndex);
28
+ case 'block.remove':
29
+ return document_write_input_text(patch.blockId);
30
+ case 'block.updateField':
31
+ return document_write_input_text(patch.blockId) && path(patch.fieldPath) && 'value' in patch;
32
+ default:
33
+ return false;
34
+ }
35
+ });
36
+ }
37
+ function validateDocumentWriteInput(input, mode) {
38
+ const valid = object(input) && document_write_input_text(input.collection) && ('create' === mode ? object(input.data) : document_write_input_text(input.id)) && (void 0 === input.locale || document_write_input_text(input.locale)) && (null == input.path || 'string' == typeof input.path) && (void 0 === input.availableLocales || Array.isArray(input.availableLocales) && input.availableLocales.every(document_write_input_text)) && ('patch' !== mode || patches(input.patches));
39
+ if (!valid) throw ERR_VALIDATION({
40
+ message: 'Invalid document write request.'
41
+ });
42
+ return input;
43
+ }
44
+ export { validateDocumentWriteInput };
@@ -15,4 +15,4 @@ export declare const restoreSingletonVersion: import("@tanstack/react-start").Re
15
15
  expectedRevision: number;
16
16
  singleton: string;
17
17
  versionId: string;
18
- }, Promise<import("@byline/core").SingletonSaveResult>>;
18
+ }, Promise<import("../collections/save-outcome.js").CollectionDocumentCommittedHookFailureResponse | import("@byline/core").RestoreSingletonVersionResult>>;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "private": false,
4
4
  "type": "module",
5
5
  "license": "MPL-2.0",
6
- "version": "6.2.1",
6
+ "version": "6.4.0",
7
7
  "engines": {
8
8
  "node": ">=20.9.0"
9
9
  },
@@ -124,16 +124,16 @@
124
124
  "clsx": "^2.1.1",
125
125
  "react-json-view-lite": "^2.5.0",
126
126
  "react-swipeable": "^7.0.2",
127
- "zod": "^4.6.2",
128
- "@byline/admin": "6.2.1",
129
- "@byline/analytics-agent": "6.2.1",
130
- "@byline/analytics": "6.2.1",
131
- "@byline/ai": "6.2.1",
132
- "@byline/client": "6.2.1",
133
- "@byline/core": "6.2.1",
134
- "@byline/i18n": "6.2.1",
135
- "@byline/ui": "6.2.1",
136
- "@byline/auth": "6.2.1"
127
+ "zod": "^4.6.5",
128
+ "@byline/ai": "6.4.0",
129
+ "@byline/analytics": "6.4.0",
130
+ "@byline/analytics-agent": "6.4.0",
131
+ "@byline/client": "6.4.0",
132
+ "@byline/ui": "6.4.0",
133
+ "@byline/admin": "6.4.0",
134
+ "@byline/auth": "6.4.0",
135
+ "@byline/core": "6.4.0",
136
+ "@byline/i18n": "6.4.0"
137
137
  },
138
138
  "peerDependencies": {
139
139
  "@tanstack/react-router": "^1.167.0",
@@ -145,10 +145,10 @@
145
145
  "devDependencies": {
146
146
  "@biomejs/biome": "2.5.13",
147
147
  "@rsbuild/plugin-react": "^2.1.0",
148
- "@rslib/core": "^1.0.0",
149
- "@tanstack/react-router": "^1.170.35",
150
- "@tanstack/react-start": "^1.168.52",
151
- "@types/node": "^26.5.1",
148
+ "@rslib/core": "^1.0.1",
149
+ "@tanstack/react-router": "^1.170.36",
150
+ "@tanstack/react-start": "^1.168.54",
151
+ "@types/node": "^26.6.1",
152
152
  "@types/react": "19.3.0",
153
153
  "@types/react-dom": "19.3.0",
154
154
  "@vitejs/plugin-react": "^6.1.1",
@@ -159,7 +159,7 @@
159
159
  "typescript": "^7.0.2",
160
160
  "typescript-plugin-css-modules": "^5.2.0",
161
161
  "vite": "^8.3.0",
162
- "vitest": "^5.0.0"
162
+ "vitest": "^5.0.1"
163
163
  },
164
164
  "publishConfig": {
165
165
  "access": "public",
@@ -6,7 +6,7 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
 
9
- import { createContext, type ReactNode, useContext, useState } from 'react'
9
+ import { createContext, type ReactNode, useCallback, useContext, useState } from 'react'
10
10
 
11
11
  import type { Breadcrumb } from './@types.js'
12
12
 
@@ -23,6 +23,32 @@ interface BreadcrumbsContextType {
23
23
 
24
24
  const BreadcrumbsContext = createContext<BreadcrumbsContextType | undefined>(undefined)
25
25
 
26
+ /**
27
+ * Route components build their breadcrumb trail from an inline array literal,
28
+ * so every render submits a new array that is almost always value-identical to
29
+ * the last one. Comparing by value lets the provider hand back the previous
30
+ * state, which keeps the array reference — and therefore the measurement
31
+ * effect in `Breadcrumbs` — stable across unrelated renders.
32
+ */
33
+ function sameBreadcrumbs(a: Breadcrumb[], b: Breadcrumb[]): boolean {
34
+ if (a === b) return true
35
+ if (a.length !== b.length) return false
36
+ return a.every(
37
+ (crumb, index) =>
38
+ crumb.label === b[index].label &&
39
+ crumb.href === b[index].href &&
40
+ crumb.active === b[index].active
41
+ )
42
+ }
43
+
44
+ function sameSettings(a: BreadcrumbsSettings, b: BreadcrumbsSettings): boolean {
45
+ return (
46
+ a.homeLabel === b.homeLabel &&
47
+ a.homePath === b.homePath &&
48
+ sameBreadcrumbs(a.breadcrumbs, b.breadcrumbs)
49
+ )
50
+ }
51
+
26
52
  export function BreadcrumbsProvider({ children }: { children: ReactNode }) {
27
53
  const [breadcrumbSettings, setBreadcrumbSettings] = useState<BreadcrumbsSettings>({
28
54
  homeLabel: 'Home',
@@ -30,10 +56,14 @@ export function BreadcrumbsProvider({ children }: { children: ReactNode }) {
30
56
  breadcrumbs: [],
31
57
  })
32
58
 
59
+ // `BreadcrumbsClient` lists this setter in its effect dependencies, so it
60
+ // has to stay referentially stable or the effect refires on every render.
61
+ const setBreadcrumbs = useCallback((settings: BreadcrumbsSettings) => {
62
+ setBreadcrumbSettings((previous) => (sameSettings(previous, settings) ? previous : settings))
63
+ }, [])
64
+
33
65
  return (
34
- <BreadcrumbsContext
35
- value={{ breadCrumbSettings: breadcrumbSettings, setBreadcrumbs: setBreadcrumbSettings }}
36
- >
66
+ <BreadcrumbsContext value={{ breadCrumbSettings: breadcrumbSettings, setBreadcrumbs }}>
37
67
  {children}
38
68
  </BreadcrumbsContext>
39
69
  )