@byline/host-tanstack-start 3.20.4 → 4.0.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 (140) hide show
  1. package/dist/admin-shell/admin-activity/list.js +15 -5
  2. package/dist/admin-shell/admin-roles/delete.js +2 -1
  3. package/dist/admin-shell/admin-roles/list.js +2 -1
  4. package/dist/admin-shell/admin-users/delete.js +2 -1
  5. package/dist/admin-shell/admin-users/list.js +4 -7
  6. package/dist/admin-shell/chrome/admin-app-bar.js +2 -1
  7. package/dist/admin-shell/chrome/admin-menu-drawer.js +9 -8
  8. package/dist/admin-shell/chrome/admin-menu-provider.js +2 -1
  9. package/dist/admin-shell/chrome/dashboard.js +4 -3
  10. package/dist/admin-shell/chrome/sign-in-page.d.ts +2 -2
  11. package/dist/admin-shell/chrome/sign-in-page.js +2 -2
  12. package/dist/admin-shell/collections/create.js +4 -3
  13. package/dist/admin-shell/collections/document-history.js +6 -1
  14. package/dist/admin-shell/collections/edit.js +18 -14
  15. package/dist/admin-shell/collections/history.js +5 -4
  16. package/dist/admin-shell/collections/list.js +8 -7
  17. package/dist/admin-shell/collections/restore-version-modal.js +2 -1
  18. package/dist/admin-shell/collections/tree-list.js +53 -17
  19. package/dist/admin-shell/collections/view-menu.js +9 -8
  20. package/dist/auth/auth-context.d.ts +13 -0
  21. package/dist/auth/auth-context.js +4 -0
  22. package/dist/auth/request-scope.d.ts +16 -0
  23. package/dist/auth/request-scope.js +24 -0
  24. package/dist/integrations/abilities.js +8 -7
  25. package/dist/integrations/byline-public-client.d.ts +38 -0
  26. package/dist/integrations/byline-public-client.js +8 -4
  27. package/dist/integrations/byline-viewer-client.d.ts +6 -0
  28. package/dist/integrations/byline-viewer-client.js +22 -17
  29. package/dist/integrations/start-errors.js +2 -1
  30. package/dist/routes/admin-path.d.ts +22 -0
  31. package/dist/routes/admin-path.js +35 -0
  32. package/dist/routes/create-admin-account-route.js +3 -2
  33. package/dist/routes/create-admin-activity-route.js +3 -2
  34. package/dist/routes/create-admin-dashboard-route.js +2 -1
  35. package/dist/routes/create-admin-layout-route.d.ts +1 -1
  36. package/dist/routes/create-admin-layout-route.js +3 -1
  37. package/dist/routes/create-admin-permissions-route.js +3 -2
  38. package/dist/routes/create-admin-role-edit-route.js +4 -3
  39. package/dist/routes/create-admin-roles-list-route.js +3 -2
  40. package/dist/routes/create-admin-user-edit-route.js +4 -3
  41. package/dist/routes/create-admin-users-list-route.js +3 -2
  42. package/dist/routes/create-collection-api-route.d.ts +1 -7
  43. package/dist/routes/create-collection-api-route.js +12 -8
  44. package/dist/routes/create-collection-create-route.js +4 -3
  45. package/dist/routes/create-collection-edit-route.d.ts +1 -7
  46. package/dist/routes/create-collection-edit-route.js +9 -6
  47. package/dist/routes/create-collection-history-route.d.ts +1 -7
  48. package/dist/routes/create-collection-history-route.js +10 -7
  49. package/dist/routes/create-collection-list-route.js +3 -2
  50. package/dist/routes/create-sign-in-route.js +3 -2
  51. package/dist/routes/get-content-locale-route-config.d.ts +12 -0
  52. package/dist/routes/get-content-locale-route-config.js +16 -0
  53. package/dist/routes/index.d.ts +3 -0
  54. package/dist/routes/index.js +2 -0
  55. package/dist/routes/sign-in-path.d.ts +13 -0
  56. package/dist/routes/sign-in-path.js +19 -0
  57. package/dist/server-fns/collections/delete-outcome.d.ts +30 -0
  58. package/dist/server-fns/collections/delete-outcome.js +34 -0
  59. package/dist/server-fns/collections/delete.d.ts +1 -4
  60. package/dist/server-fns/collections/delete.js +2 -4
  61. package/dist/server-fns/collections/index.d.ts +1 -0
  62. package/dist/server-fns/collections/index.js +1 -0
  63. package/dist/server-fns/collections/tree-mutation.d.ts +46 -0
  64. package/dist/server-fns/collections/tree-mutation.js +109 -0
  65. package/dist/server-fns/collections/tree-read.d.ts +6 -0
  66. package/dist/server-fns/collections/tree-read.js +15 -0
  67. package/dist/server-fns/collections/tree.d.ts +5 -4
  68. package/dist/server-fns/collections/tree.js +15 -12
  69. package/dist/server-fns/collections/update.js +2 -1
  70. package/dist/vite/client-hook-boundary.d.ts +19 -0
  71. package/dist/vite/client-hook-boundary.js +20 -0
  72. package/dist/vite/index.d.ts +8 -0
  73. package/dist/vite/index.js +1 -0
  74. package/package.json +15 -9
  75. package/src/admin-shell/admin-activity/list.tsx +13 -3
  76. package/src/admin-shell/admin-roles/delete.tsx +2 -1
  77. package/src/admin-shell/admin-roles/list.tsx +2 -1
  78. package/src/admin-shell/admin-users/delete.tsx +3 -2
  79. package/src/admin-shell/admin-users/list.tsx +9 -8
  80. package/src/admin-shell/chrome/admin-app-bar.tsx +2 -1
  81. package/src/admin-shell/chrome/admin-menu-drawer.tsx +13 -8
  82. package/src/admin-shell/chrome/admin-menu-provider.tsx +2 -1
  83. package/src/admin-shell/chrome/dashboard.tsx +4 -3
  84. package/src/admin-shell/chrome/sign-in-page.tsx +3 -3
  85. package/src/admin-shell/collections/create.tsx +4 -3
  86. package/src/admin-shell/collections/document-history.tsx +5 -0
  87. package/src/admin-shell/collections/edit.tsx +20 -13
  88. package/src/admin-shell/collections/history.tsx +6 -5
  89. package/src/admin-shell/collections/list.tsx +8 -7
  90. package/src/admin-shell/collections/restore-version-modal.tsx +2 -1
  91. package/src/admin-shell/collections/tree-list.tsx +78 -22
  92. package/src/admin-shell/collections/view-menu.tsx +9 -8
  93. package/src/auth/auth-context.test.node.ts +87 -0
  94. package/src/auth/auth-context.ts +18 -0
  95. package/src/auth/request-scope.test.node.ts +93 -0
  96. package/src/auth/request-scope.ts +62 -0
  97. package/src/integrations/abilities.tsx +14 -4
  98. package/src/integrations/byline-client.ts +6 -4
  99. package/src/integrations/byline-public-client.ts +18 -4
  100. package/src/integrations/byline-viewer-client.ts +30 -18
  101. package/src/integrations/request-context-contract.test.node.ts +131 -0
  102. package/src/integrations/request-context-factories.test.node.ts +140 -0
  103. package/src/integrations/start-errors.test.node.ts +22 -0
  104. package/src/integrations/start-errors.ts +1 -0
  105. package/src/routes/admin-path.test.node.ts +129 -0
  106. package/src/routes/admin-path.ts +66 -0
  107. package/src/routes/auth-routes.test.node.ts +39 -0
  108. package/src/routes/create-admin-account-route.tsx +3 -2
  109. package/src/routes/create-admin-activity-route.tsx +3 -2
  110. package/src/routes/create-admin-dashboard-route.tsx +2 -1
  111. package/src/routes/create-admin-layout-route.tsx +4 -4
  112. package/src/routes/create-admin-permissions-route.tsx +3 -2
  113. package/src/routes/create-admin-role-edit-route.tsx +7 -3
  114. package/src/routes/create-admin-roles-list-route.tsx +3 -2
  115. package/src/routes/create-admin-user-edit-route.tsx +7 -3
  116. package/src/routes/create-admin-users-list-route.tsx +3 -2
  117. package/src/routes/create-collection-api-route.tsx +15 -14
  118. package/src/routes/create-collection-create-route.tsx +7 -3
  119. package/src/routes/create-collection-edit-route.tsx +12 -12
  120. package/src/routes/create-collection-history-route.tsx +13 -13
  121. package/src/routes/create-collection-list-route.tsx +3 -2
  122. package/src/routes/create-sign-in-route.tsx +9 -3
  123. package/src/routes/get-content-locale-route-config.ts +26 -0
  124. package/src/routes/index.ts +11 -0
  125. package/src/routes/sign-in-path.test.node.ts +79 -0
  126. package/src/routes/sign-in-path.ts +36 -0
  127. package/src/server-fns/collections/delete-outcome.test.node.ts +72 -0
  128. package/src/server-fns/collections/delete-outcome.ts +80 -0
  129. package/src/server-fns/collections/delete.ts +2 -1
  130. package/src/server-fns/collections/get.ts +2 -0
  131. package/src/server-fns/collections/index.ts +1 -0
  132. package/src/server-fns/collections/tree-mutation.test.node.ts +225 -0
  133. package/src/server-fns/collections/tree-mutation.ts +165 -0
  134. package/src/server-fns/collections/tree-read.test.node.ts +46 -0
  135. package/src/server-fns/collections/tree-read.ts +26 -0
  136. package/src/server-fns/collections/tree.ts +26 -10
  137. package/src/server-fns/collections/update.ts +3 -0
  138. package/src/vite/client-hook-boundary.test.node.ts +50 -0
  139. package/src/vite/client-hook-boundary.ts +45 -0
  140. package/src/vite/index.ts +9 -0
@@ -0,0 +1,34 @@
1
+ import { ErrorCodes } from "@byline/core";
2
+ function sanitizePhase(phase) {
3
+ if ('storageCleanup' === phase || 'afterTreeChange' === phase || 'afterDelete' === phase) return phase;
4
+ return 'unknown';
5
+ }
6
+ function sanitizeCode(code) {
7
+ return code === ErrorCodes.STORAGE ? ErrorCodes.STORAGE : ErrorCodes.UNHANDLED;
8
+ }
9
+ function sanitizeFailure({ phase, code }) {
10
+ return {
11
+ phase: sanitizePhase(phase),
12
+ code: sanitizeCode(code)
13
+ };
14
+ }
15
+ function toDeleteDocumentResponse(result) {
16
+ if ('committed' === result.outcome) return {
17
+ status: 'ok',
18
+ ...result
19
+ };
20
+ const [firstFailure, ...remainingFailures] = result.sideEffectFailures;
21
+ return {
22
+ status: 'ok',
23
+ deletedVersionCount: result.deletedVersionCount,
24
+ outcome: result.outcome,
25
+ sideEffectFailures: [
26
+ sanitizeFailure(firstFailure),
27
+ ...remainingFailures.map(sanitizeFailure)
28
+ ]
29
+ };
30
+ }
31
+ function hasDeleteSideEffectFailures(result) {
32
+ return 'committed-with-side-effect-failures' === result.outcome;
33
+ }
34
+ export { hasDeleteSideEffectFailures, toDeleteDocumentResponse };
@@ -11,7 +11,4 @@ export declare const deleteDocument: import("@tanstack/react-start").RequiredFet
11
11
  }) => {
12
12
  collection: string;
13
13
  id: string;
14
- }, Promise<{
15
- status: 'ok';
16
- deletedVersionCount: number;
17
- }>>;
14
+ }, Promise<import("./delete-outcome.js").DeleteDocumentResponse>>;
@@ -3,6 +3,7 @@ import { ERR_NOT_FOUND, getLogger, getServerConfig, getUploadFields } from "@byl
3
3
  import { deleteDocument } from "@byline/core/services";
4
4
  import { getAdminRequestContext } from "../../auth/auth-context.js";
5
5
  import { ensureCollection } from "../../integrations/api-utils.js";
6
+ import { toDeleteDocumentResponse } from "./delete-outcome.js";
6
7
  const delete_deleteDocument = createServerFn({
7
8
  method: 'POST'
8
9
  }).validator((input)=>input).handler(async ({ data: input })=>{
@@ -36,9 +37,6 @@ const delete_deleteDocument = createServerFn({
36
37
  const result = await deleteDocument(ctx, {
37
38
  documentId: id
38
39
  });
39
- return {
40
- status: 'ok',
41
- deletedVersionCount: result.deletedVersionCount
42
- };
40
+ return toDeleteDocumentResponse(result);
43
41
  });
44
42
  export { delete_deleteDocument as deleteDocument };
@@ -9,6 +9,7 @@ export * from './copy-to-locale';
9
9
  export * from './create';
10
10
  export * from './delete';
11
11
  export * from './delete-locale';
12
+ export * from './delete-outcome';
12
13
  export * from './duplicate';
13
14
  export * from './get';
14
15
  export * from './history';
@@ -3,6 +3,7 @@ export * from "./copy-to-locale.js";
3
3
  export * from "./create.js";
4
4
  export * from "./delete.js";
5
5
  export * from "./delete-locale.js";
6
+ export * from "./delete-outcome.js";
6
7
  export * from "./duplicate.js";
7
8
  export * from "./get.js";
8
9
  export * from "./history.js";
@@ -0,0 +1,46 @@
1
+ import type { CollectionHandle, PlaceTreeNodeOptions, RemoveFromTreeOptions } from '@byline/client';
2
+ /** Prefer the code; the marker survives transports that reconstruct a code-less Error. */
3
+ export declare function isTreeMutationConflict(error: unknown): boolean;
4
+ /** Detect when tree/audit committed even if transport reconstruction dropped the code. */
5
+ export declare function isCommittedTreeHookFailure(error: unknown): boolean;
6
+ export type AdminTreeMoveOutcome = {
7
+ status: 'ok';
8
+ } | {
9
+ status: 'suppressed';
10
+ } | {
11
+ status: 'mutation-failed';
12
+ error: unknown;
13
+ refreshError?: unknown;
14
+ } | {
15
+ status: 'committed-hook-failed';
16
+ error: unknown;
17
+ refreshError?: unknown;
18
+ } | {
19
+ status: 'refresh-failed';
20
+ error: unknown;
21
+ };
22
+ export interface AdminTreeMoveUiEffects {
23
+ rollback: boolean;
24
+ preserveOptimistic: boolean;
25
+ structuralFailure: 'conflict' | 'generic' | null;
26
+ committedHookWarning: boolean;
27
+ refreshWarning: boolean;
28
+ }
29
+ /** Pure UI policy for every mutation/refresh outcome. */
30
+ export declare function getAdminTreeMoveUiEffects(outcome: AdminTreeMoveOutcome): AdminTreeMoveUiEffects;
31
+ /** Preserve a committed optimistic move until a later loader returns new row data. */
32
+ export declare function shouldSyncAdminTreeRows(rows: unknown, preservedRows: unknown, isMoving: boolean): boolean;
33
+ /** Keep mutation and canonical-refresh failures distinct for optimistic tree UIs. */
34
+ export declare function executeAdminTreeMove(mutate: () => Promise<unknown>, refresh: () => Promise<unknown>): Promise<AdminTreeMoveOutcome>;
35
+ export interface AdminTreeMoveController {
36
+ isMoving(): boolean;
37
+ execute(mutate: () => Promise<unknown>, refresh: () => Promise<unknown>): Promise<AdminTreeMoveOutcome>;
38
+ }
39
+ /** Single-flight controller shared by pointer and keyboard tree moves. */
40
+ export declare function createAdminTreeMoveController(): AdminTreeMoveController;
41
+ /** Admin retries reconcile post-commit tree hooks unless explicitly disabled. */
42
+ export declare function placeAdminTreeNode(handle: Pick<CollectionHandle, 'placeTreeNode'>, documentId: string, options: PlaceTreeNodeOptions): Promise<{
43
+ orderKey: string;
44
+ }>;
45
+ /** Admin retries reconcile post-commit tree hooks unless explicitly disabled. */
46
+ export declare function removeAdminTreeNode(handle: Pick<CollectionHandle, 'removeFromTree'>, documentId: string, options?: RemoveFromTreeOptions): Promise<void>;
@@ -0,0 +1,109 @@
1
+ import { ErrorCodes, TREE_HOOK_COMMITTED_MARKER, TREE_PLACEMENT_STALE_MARKER } from "@byline/core";
2
+ function hasErrorCode(error, code) {
3
+ return 'object' == typeof error && null != error && 'code' in error && error.code === code;
4
+ }
5
+ function hasMessageMarker(error, marker) {
6
+ return error instanceof Error && error.message.startsWith(marker);
7
+ }
8
+ function isTreeMutationConflict(error) {
9
+ if (hasErrorCode(error, ErrorCodes.CONFLICT)) return true;
10
+ return hasMessageMarker(error, TREE_PLACEMENT_STALE_MARKER);
11
+ }
12
+ function isCommittedTreeHookFailure(error) {
13
+ if (hasErrorCode(error, ErrorCodes.TREE_HOOK_COMMITTED)) return true;
14
+ return hasMessageMarker(error, TREE_HOOK_COMMITTED_MARKER);
15
+ }
16
+ function getAdminTreeMoveUiEffects(outcome) {
17
+ if ('mutation-failed' === outcome.status) return {
18
+ rollback: true,
19
+ preserveOptimistic: false,
20
+ structuralFailure: isTreeMutationConflict(outcome.error) ? 'conflict' : 'generic',
21
+ committedHookWarning: false,
22
+ refreshWarning: null != outcome.refreshError
23
+ };
24
+ if ('committed-hook-failed' === outcome.status) return {
25
+ rollback: false,
26
+ preserveOptimistic: null != outcome.refreshError,
27
+ structuralFailure: null,
28
+ committedHookWarning: true,
29
+ refreshWarning: null != outcome.refreshError
30
+ };
31
+ if ('refresh-failed' === outcome.status) return {
32
+ rollback: false,
33
+ preserveOptimistic: true,
34
+ structuralFailure: null,
35
+ committedHookWarning: false,
36
+ refreshWarning: true
37
+ };
38
+ return {
39
+ rollback: 'suppressed' === outcome.status,
40
+ preserveOptimistic: false,
41
+ structuralFailure: null,
42
+ committedHookWarning: false,
43
+ refreshWarning: false
44
+ };
45
+ }
46
+ function shouldSyncAdminTreeRows(rows, preservedRows, isMoving) {
47
+ return !isMoving && rows !== preservedRows;
48
+ }
49
+ async function executeAdminTreeMove(mutate, refresh) {
50
+ try {
51
+ await mutate();
52
+ } catch (error) {
53
+ const status = isCommittedTreeHookFailure(error) ? 'committed-hook-failed' : 'mutation-failed';
54
+ try {
55
+ await refresh();
56
+ return {
57
+ status,
58
+ error
59
+ };
60
+ } catch (refreshError) {
61
+ return {
62
+ status,
63
+ error,
64
+ refreshError
65
+ };
66
+ }
67
+ }
68
+ try {
69
+ await refresh();
70
+ return {
71
+ status: 'ok'
72
+ };
73
+ } catch (error) {
74
+ return {
75
+ status: 'refresh-failed',
76
+ error
77
+ };
78
+ }
79
+ }
80
+ function createAdminTreeMoveController() {
81
+ let moving = false;
82
+ return {
83
+ isMoving: ()=>moving,
84
+ execute: async (mutate, refresh)=>{
85
+ if (moving) return {
86
+ status: 'suppressed'
87
+ };
88
+ moving = true;
89
+ try {
90
+ return await executeAdminTreeMove(mutate, refresh);
91
+ } finally{
92
+ moving = false;
93
+ }
94
+ }
95
+ };
96
+ }
97
+ function placeAdminTreeNode(handle, documentId, options) {
98
+ return handle.placeTreeNode(documentId, {
99
+ ...options,
100
+ reconcile: options.reconcile ?? true
101
+ });
102
+ }
103
+ function removeAdminTreeNode(handle, documentId, options = {}) {
104
+ return handle.removeFromTree(documentId, {
105
+ ...options,
106
+ reconcile: options.reconcile ?? true
107
+ });
108
+ }
109
+ export { createAdminTreeMoveController, executeAdminTreeMove, getAdminTreeMoveUiEffects, isCommittedTreeHookFailure, isTreeMutationConflict, placeAdminTreeNode, removeAdminTreeNode, shouldSyncAdminTreeRows };
@@ -0,0 +1,6 @@
1
+ import type { ClientDocument, CollectionHandle, TreeParentResult } from '@byline/client';
2
+ import type { ReadContext } from '@byline/core';
3
+ /** Admin tree placement reads must include drafts. */
4
+ export declare function getAdminTreeParent(handle: Pick<CollectionHandle, 'getTreeParent'>, documentId: string): Promise<TreeParentResult>;
5
+ /** Find unplaced nodes through the same row scope as the structural tree read. */
6
+ export declare function getAdminUnplacedTreeDocuments(handle: Pick<CollectionHandle, 'find'>, placed: ReadonlySet<string>, readContext: ReadContext, locale?: string): Promise<ClientDocument[]>;
@@ -0,0 +1,15 @@
1
+ function getAdminTreeParent(handle, documentId) {
2
+ return handle.getTreeParent(documentId, {
3
+ status: 'any'
4
+ });
5
+ }
6
+ async function getAdminUnplacedTreeDocuments(handle, placed, readContext, locale) {
7
+ const all = await handle.find({
8
+ status: 'any',
9
+ locale,
10
+ pageSize: 1000,
11
+ _readContext: readContext
12
+ });
13
+ return all.docs.filter((doc)=>!placed.has(doc.id));
14
+ }
15
+ export { getAdminTreeParent, getAdminUnplacedTreeDocuments };
@@ -11,12 +11,14 @@ export declare const placeTreeNode: import("@tanstack/react-start").RequiredFetc
11
11
  parentDocumentId: string | null;
12
12
  beforeDocumentId?: string | null;
13
13
  afterDocumentId?: string | null;
14
+ reconcile?: boolean;
14
15
  }) => {
15
16
  collection: string;
16
17
  documentId: string;
17
18
  parentDocumentId: string | null;
18
19
  beforeDocumentId?: string | null;
19
20
  afterDocumentId?: string | null;
21
+ reconcile?: boolean;
20
22
  }, Promise<{
21
23
  status: 'ok';
22
24
  orderKey: string;
@@ -24,9 +26,11 @@ export declare const placeTreeNode: import("@tanstack/react-start").RequiredFetc
24
26
  export declare const removeFromTree: import("@tanstack/react-start").RequiredFetcher<undefined, (input: {
25
27
  collection: string;
26
28
  documentId: string;
29
+ reconcile?: boolean;
27
30
  }) => {
28
31
  collection: string;
29
32
  documentId: string;
33
+ reconcile?: boolean;
30
34
  }, Promise<{
31
35
  status: 'ok';
32
36
  }>>;
@@ -47,10 +51,7 @@ export declare const getTreeParent: import("@tanstack/react-start").RequiredFetc
47
51
  }) => {
48
52
  collection: string;
49
53
  documentId: string;
50
- }, Promise<{
51
- placed: boolean;
52
- parentDocumentId: string | null;
53
- }>>;
54
+ }, Promise<import("@byline/client").TreeParentResult>>;
54
55
  export interface CollectionTreeRow {
55
56
  id: string;
56
57
  /** Parent document id, or `null` for a root or unplaced node. Drives the
@@ -1,7 +1,9 @@
1
1
  import { createServerFn } from "@tanstack/react-start";
2
- import { ERR_NOT_FOUND, getLogger } from "@byline/core";
2
+ import { ERR_NOT_FOUND, createReadContext, getLogger } from "@byline/core";
3
3
  import { ensureCollection } from "../../integrations/api-utils.js";
4
4
  import { getAdminBylineClient } from "../../integrations/byline-client.js";
5
+ import { placeAdminTreeNode, removeAdminTreeNode } from "./tree-mutation.js";
6
+ import { getAdminTreeParent, getAdminUnplacedTreeDocuments } from "./tree-read.js";
5
7
  const placeTreeNode = createServerFn({
6
8
  method: 'POST'
7
9
  }).validator((input)=>input).handler(async ({ data })=>{
@@ -14,10 +16,11 @@ const placeTreeNode = createServerFn({
14
16
  }
15
17
  }).log(getLogger());
16
18
  const handle = getAdminBylineClient().collection(path);
17
- const result = await handle.placeTreeNode(documentId, {
19
+ const result = await placeAdminTreeNode(handle, documentId, {
18
20
  parentDocumentId: data.parentDocumentId,
19
21
  beforeDocumentId: data.beforeDocumentId ?? null,
20
- afterDocumentId: data.afterDocumentId ?? null
22
+ afterDocumentId: data.afterDocumentId ?? null,
23
+ reconcile: data.reconcile
21
24
  });
22
25
  return {
23
26
  status: 'ok',
@@ -35,7 +38,9 @@ const removeFromTree = createServerFn({
35
38
  collectionPath: path
36
39
  }
37
40
  }).log(getLogger());
38
- await getAdminBylineClient().collection(path).removeFromTree(documentId);
41
+ await removeAdminTreeNode(getAdminBylineClient().collection(path), documentId, {
42
+ reconcile: data.reconcile
43
+ });
39
44
  return {
40
45
  status: 'ok'
41
46
  };
@@ -76,7 +81,7 @@ const getTreeParent = createServerFn({
76
81
  collectionPath: path
77
82
  }
78
83
  }).log(getLogger());
79
- return getAdminBylineClient().collection(path).getTreeParent(documentId);
84
+ return getAdminTreeParent(getAdminBylineClient().collection(path), documentId);
80
85
  });
81
86
  const getCollectionTree = createServerFn({
82
87
  method: 'GET'
@@ -90,9 +95,11 @@ const getCollectionTree = createServerFn({
90
95
  }
91
96
  }).log(getLogger());
92
97
  const handle = getAdminBylineClient().collection(path);
98
+ const readContext = createReadContext();
93
99
  const forest = await handle.getSubtree({
94
100
  status: 'any',
95
- locale: data.locale
101
+ locale: data.locale,
102
+ _readContext: readContext
96
103
  });
97
104
  const rows = [];
98
105
  const placed = new Set();
@@ -115,12 +122,8 @@ const getCollectionTree = createServerFn({
115
122
  }
116
123
  };
117
124
  walk(forest, 0, null);
118
- const all = await handle.find({
119
- status: 'any',
120
- pageSize: 1000,
121
- _bypassBeforeRead: true
122
- });
123
- for (const doc of all.docs)if (!placed.has(doc.id)) rows.push({
125
+ const unplaced = await getAdminUnplacedTreeDocuments(handle, placed, readContext, data.locale);
126
+ for (const doc of unplaced)rows.push({
124
127
  id: doc.id,
125
128
  parentId: null,
126
129
  depth: 0,
@@ -65,7 +65,8 @@ const updateCollectionDocumentSystemFields = createServerFn({
65
65
  documentId: id,
66
66
  locale: locale ?? serverConfig.i18n.content.defaultLocale,
67
67
  path: explicitPath,
68
- availableLocales
68
+ availableLocales,
69
+ reconcile: true
69
70
  });
70
71
  return {
71
72
  status: 'ok'
@@ -0,0 +1,19 @@
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
+ * Production client-bundle guard for server-only Byline lifecycle modules.
10
+ *
11
+ * Collection schemas are isomorphic, but registered lifecycle hooks may
12
+ * import Node APIs, caches, storage SDKs, and other server-only dependencies.
13
+ * This Vite plugin inspects emitted client chunks and fails the build if a
14
+ * hook implementation, server hook registry, or shared lifecycle helper
15
+ * becomes reachable from the browser bundle.
16
+ */
17
+ import type { Plugin } from 'vite';
18
+ export declare function findServerHookModules(moduleIds: Iterable<string>): string[];
19
+ export declare function bylineClientHookBoundary(): Plugin;
@@ -0,0 +1,20 @@
1
+ const SERVER_HOOK_MODULE = /\/byline\/collections\/(?:.+\/)?(?:hooks|[^/]+-hooks|[^/]+-lifecycle-hooks|[^/]+-side-effects)\.[cm]?[jt]sx?$/;
2
+ function findServerHookModules(moduleIds) {
3
+ return [
4
+ ...moduleIds
5
+ ].filter((moduleId)=>{
6
+ const normalized = moduleId.replaceAll('\\', '/').split('?', 1)[0];
7
+ return SERVER_HOOK_MODULE.test(normalized);
8
+ });
9
+ }
10
+ function bylineClientHookBoundary() {
11
+ return {
12
+ name: 'byline:client-hook-boundary',
13
+ generateBundle (_options, bundle) {
14
+ if (this.environment?.name !== 'client') return;
15
+ const leaked = findServerHookModules(Object.values(bundle).flatMap((output)=>'chunk' === output.type ? Object.keys(output.modules) : []));
16
+ if (leaked.length > 0) this.error(`server lifecycle modules entered the client bundle:\n${leaked.join('\n')}`);
17
+ }
18
+ };
19
+ }
20
+ export { bylineClientHookBoundary, findServerHookModules };
@@ -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 { bylineClientHookBoundary } from './client-hook-boundary.js';
@@ -0,0 +1 @@
1
+ export { bylineClientHookBoundary } from "./client-hook-boundary.js";
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "private": false,
4
4
  "type": "module",
5
5
  "license": "MPL-2.0",
6
- "version": "3.20.4",
6
+ "version": "4.0.0",
7
7
  "engines": {
8
8
  "node": ">=20.9.0"
9
9
  },
@@ -108,6 +108,10 @@
108
108
  "./routes/*": {
109
109
  "types": "./dist/routes/*.d.ts",
110
110
  "import": "./dist/routes/*.js"
111
+ },
112
+ "./vite": {
113
+ "types": "./dist/vite/index.d.ts",
114
+ "import": "./dist/vite/index.js"
111
115
  }
112
116
  },
113
117
  "dependencies": {
@@ -119,19 +123,20 @@
119
123
  "react-swipeable": "^7.0.2",
120
124
  "uuid": "^14.0.1",
121
125
  "zod": "^4.4.3",
122
- "@byline/admin": "3.20.4",
123
- "@byline/auth": "3.20.4",
124
- "@byline/ai": "3.20.4",
125
- "@byline/i18n": "3.20.4",
126
- "@byline/ui": "3.20.4",
127
- "@byline/client": "3.20.4",
128
- "@byline/core": "3.20.4"
126
+ "@byline/admin": "4.0.0",
127
+ "@byline/auth": "4.0.0",
128
+ "@byline/ui": "4.0.0",
129
+ "@byline/i18n": "4.0.0",
130
+ "@byline/ai": "4.0.0",
131
+ "@byline/client": "4.0.0",
132
+ "@byline/core": "4.0.0"
129
133
  },
130
134
  "peerDependencies": {
131
135
  "@tanstack/react-router": "^1.167.0",
132
136
  "@tanstack/react-start": "^1.167.0",
133
137
  "react": "^19.0.0",
134
- "react-dom": "^19.0.0"
138
+ "react-dom": "^19.0.0",
139
+ "vite": "^8.0.0"
135
140
  },
136
141
  "devDependencies": {
137
142
  "@biomejs/biome": "2.5.2",
@@ -148,6 +153,7 @@
148
153
  "rimraf": "^6.1.3",
149
154
  "typescript": "^7.0.2",
150
155
  "typescript-plugin-css-modules": "^5.2.0",
156
+ "vite": "^8.1.3",
151
157
  "vitest": "^4.1.10"
152
158
  },
153
159
  "publishConfig": {
@@ -15,6 +15,7 @@ import { useTranslation } from '@byline/i18n/react'
15
15
  import { Button, Container, Section, Select, Table } from '@byline/ui/react'
16
16
  import cx from 'classnames'
17
17
 
18
+ import { getAdminRoutePath } from '../../routes/admin-path.js'
18
19
  import { Link, useNavigate } from '../chrome/loose-router.js'
19
20
  import { RouterPager } from '../chrome/router-pager.js'
20
21
  import styles from './list.module.css'
@@ -33,6 +34,10 @@ const ACTION_KEYS: Record<string, string> = {
33
34
  'document.locales.changed': 'activity.actions.localesChanged',
34
35
  'document.status.changed': 'activity.actions.statusChanged',
35
36
  'document.deleted': 'activity.actions.deleted',
37
+ 'document.tree.placed': 'activity.actions.treePlaced',
38
+ 'document.tree.reparented': 'activity.actions.treeReparented',
39
+ 'document.tree.reordered': 'activity.actions.treeReordered',
40
+ 'document.tree.removed': 'activity.actions.treeRemoved',
36
41
  }
37
42
 
38
43
  /** The selectable action types, in display order. `_all` clears the filter. */
@@ -43,12 +48,17 @@ const ACTION_FILTER_VALUES = [
43
48
  'document.path.changed',
44
49
  'document.locales.changed',
45
50
  'document.deleted',
51
+ 'document.tree.placed',
52
+ 'document.tree.reparented',
53
+ 'document.tree.reordered',
54
+ 'document.tree.removed',
46
55
  ] as const
47
56
 
48
57
  /** Render an audit before/after value inline: arrays comma-join, nullish → em-dash. */
49
58
  function formatAuditValue(value: unknown): string {
50
59
  if (value == null) return '—'
51
60
  if (Array.isArray(value)) return value.length > 0 ? value.join(', ') : '—'
61
+ if (typeof value === 'object') return JSON.stringify(value)
52
62
  return String(value)
53
63
  }
54
64
 
@@ -81,7 +91,7 @@ export const ActivitySystemView = ({ data }: { data: SystemActivityResponse }) =
81
91
  } else {
82
92
  params[key] = value
83
93
  }
84
- navigate({ to: '/admin/activity' as never, search: params })
94
+ navigate({ to: getAdminRoutePath('activity'), search: params })
85
95
  }
86
96
 
87
97
  const collectionItems = [
@@ -134,7 +144,7 @@ export const ActivitySystemView = ({ data }: { data: SystemActivityResponse }) =
134
144
  <Button
135
145
  size="sm"
136
146
  variant="text"
137
- onClick={() => navigate({ to: '/admin/activity' as never, search: {} })}
147
+ onClick={() => navigate({ to: getAdminRoutePath('activity'), search: {} })}
138
148
  >
139
149
  {t('activity.filters.clear')}
140
150
  </Button>
@@ -190,7 +200,7 @@ export const ActivitySystemView = ({ data }: { data: SystemActivityResponse }) =
190
200
  <Table.Cell>
191
201
  {col != null && entry.documentId != null ? (
192
202
  <Link
193
- to={'/admin/collections/$collection/$id' as never}
203
+ to={getAdminRoutePath('collections', '$collection', '$id')}
194
204
  params={{ collection: col.path, id: entry.documentId }}
195
205
  >
196
206
  {collectionLabel}
@@ -24,6 +24,7 @@ import { useTranslation } from '@byline/i18n/react'
24
24
  import { Alert, Button, LoaderEllipsis, Modal } from '@byline/ui/react'
25
25
  import cx from 'classnames'
26
26
 
27
+ import { getAdminRoutePath } from '../../routes/admin-path.js'
27
28
  import { type AdminRoleResponse, deleteAdminRole } from '../../server-fns/admin-roles/index.js'
28
29
  import { useNavigate } from '../chrome/loose-router.js'
29
30
  import styles from './delete.module.css'
@@ -52,7 +53,7 @@ export function DeleteRole({ role, onClose }: DeleteRoleProps) {
52
53
  // forced to re-fetch — without this, TanStack Router can serve
53
54
  // the cached pre-delete list.
54
55
  await router.invalidate()
55
- navigate({ to: '/admin/roles' as never })
56
+ navigate({ to: getAdminRoutePath('roles') })
56
57
  } catch (err) {
57
58
  const code = getErrorCode(err)
58
59
  if (code === 'admin.roles.versionConflict') {
@@ -46,6 +46,7 @@ import {
46
46
  } from '@byline/ui/react'
47
47
  import cx from 'classnames'
48
48
 
49
+ import { getAdminRoutePath } from '../../routes/admin-path.js'
49
50
  import { reorderAdminRoles } from '../../server-fns/admin-roles/reorder.js'
50
51
  import { Link } from '../chrome/loose-router.js'
51
52
  import { formatNumber } from '../chrome/utils.js'
@@ -97,7 +98,7 @@ const DraggableRow: React.FC<DraggableRowProps> = ({ item, disabled }) => {
97
98
  </div>
98
99
  </Table.Cell>
99
100
  <Table.Cell>
100
- <Link to={'/admin/roles/$id' as never} params={{ id: item.id }}>
101
+ <Link to={getAdminRoutePath('roles', '$id')} params={{ id: item.id }}>
101
102
  {item.name}
102
103
  </Link>
103
104
  </Table.Cell>
@@ -15,7 +15,7 @@
15
15
  * component only lays out the content + action row — the modal header
16
16
  * and dismiss button are already drawn by the container.
17
17
  *
18
- * On success we navigate away to `/admin/users`; the record that hosts
18
+ * On success we navigate back to the configured admin users list; the record that hosts
19
19
  * this view no longer exists. The `onSuccess` callback on `PanelProps`
20
20
  * is unused here because `delete` produces no updated user.
21
21
  */
@@ -27,6 +27,7 @@ import { useTranslation } from '@byline/i18n/react'
27
27
  import { Alert, Button, LoaderEllipsis, Modal } from '@byline/ui/react'
28
28
  import cx from 'classnames'
29
29
 
30
+ import { getAdminRoutePath } from '../../routes/admin-path.js'
30
31
  import { type AdminUserResponse, deleteAdminUser } from '../../server-fns/admin-users/index.js'
31
32
  import { useNavigate } from '../chrome/loose-router.js'
32
33
  import styles from './delete.module.css'
@@ -62,7 +63,7 @@ export function DeleteUser({ user, onClose }: DeleteUserProps) {
62
63
  // forced to re-fetch — without this, TanStack Router can serve
63
64
  // the cached pre-delete list.
64
65
  await router.invalidate()
65
- navigate({ to: '/admin/users' as never })
66
+ navigate({ to: getAdminRoutePath('users') })
66
67
  } catch (err) {
67
68
  const code = getErrorCode(err)
68
69
  if (code === 'admin.users.selfDeleteForbidden') {