@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.
- package/dist/admin-shell/admin-activity/list.js +15 -5
- package/dist/admin-shell/admin-roles/delete.js +2 -1
- package/dist/admin-shell/admin-roles/list.js +2 -1
- package/dist/admin-shell/admin-users/delete.js +2 -1
- package/dist/admin-shell/admin-users/list.js +4 -7
- package/dist/admin-shell/chrome/admin-app-bar.js +2 -1
- package/dist/admin-shell/chrome/admin-menu-drawer.js +9 -8
- package/dist/admin-shell/chrome/admin-menu-provider.js +2 -1
- package/dist/admin-shell/chrome/dashboard.js +4 -3
- package/dist/admin-shell/chrome/sign-in-page.d.ts +2 -2
- package/dist/admin-shell/chrome/sign-in-page.js +2 -2
- package/dist/admin-shell/collections/create.js +4 -3
- package/dist/admin-shell/collections/document-history.js +6 -1
- package/dist/admin-shell/collections/edit.js +18 -14
- package/dist/admin-shell/collections/history.js +5 -4
- package/dist/admin-shell/collections/list.js +8 -7
- package/dist/admin-shell/collections/restore-version-modal.js +2 -1
- package/dist/admin-shell/collections/tree-list.js +53 -17
- package/dist/admin-shell/collections/view-menu.js +9 -8
- package/dist/auth/auth-context.d.ts +13 -0
- package/dist/auth/auth-context.js +4 -0
- package/dist/auth/request-scope.d.ts +16 -0
- package/dist/auth/request-scope.js +24 -0
- package/dist/integrations/abilities.js +8 -7
- package/dist/integrations/byline-public-client.d.ts +38 -0
- package/dist/integrations/byline-public-client.js +8 -4
- package/dist/integrations/byline-viewer-client.d.ts +6 -0
- package/dist/integrations/byline-viewer-client.js +22 -17
- package/dist/integrations/start-errors.js +2 -1
- package/dist/routes/admin-path.d.ts +22 -0
- package/dist/routes/admin-path.js +35 -0
- package/dist/routes/create-admin-account-route.js +3 -2
- package/dist/routes/create-admin-activity-route.js +3 -2
- package/dist/routes/create-admin-dashboard-route.js +2 -1
- package/dist/routes/create-admin-layout-route.d.ts +1 -1
- package/dist/routes/create-admin-layout-route.js +3 -1
- package/dist/routes/create-admin-permissions-route.js +3 -2
- package/dist/routes/create-admin-role-edit-route.js +4 -3
- package/dist/routes/create-admin-roles-list-route.js +3 -2
- package/dist/routes/create-admin-user-edit-route.js +4 -3
- package/dist/routes/create-admin-users-list-route.js +3 -2
- package/dist/routes/create-collection-api-route.d.ts +1 -7
- package/dist/routes/create-collection-api-route.js +12 -8
- package/dist/routes/create-collection-create-route.js +4 -3
- package/dist/routes/create-collection-edit-route.d.ts +1 -7
- package/dist/routes/create-collection-edit-route.js +9 -6
- package/dist/routes/create-collection-history-route.d.ts +1 -7
- package/dist/routes/create-collection-history-route.js +10 -7
- package/dist/routes/create-collection-list-route.js +3 -2
- package/dist/routes/create-sign-in-route.js +3 -2
- package/dist/routes/get-content-locale-route-config.d.ts +12 -0
- package/dist/routes/get-content-locale-route-config.js +16 -0
- package/dist/routes/index.d.ts +3 -0
- package/dist/routes/index.js +2 -0
- package/dist/routes/sign-in-path.d.ts +13 -0
- package/dist/routes/sign-in-path.js +19 -0
- package/dist/server-fns/collections/delete-outcome.d.ts +30 -0
- package/dist/server-fns/collections/delete-outcome.js +34 -0
- package/dist/server-fns/collections/delete.d.ts +1 -4
- package/dist/server-fns/collections/delete.js +2 -4
- package/dist/server-fns/collections/index.d.ts +1 -0
- package/dist/server-fns/collections/index.js +1 -0
- package/dist/server-fns/collections/tree-mutation.d.ts +46 -0
- package/dist/server-fns/collections/tree-mutation.js +109 -0
- package/dist/server-fns/collections/tree-read.d.ts +6 -0
- package/dist/server-fns/collections/tree-read.js +15 -0
- package/dist/server-fns/collections/tree.d.ts +5 -4
- package/dist/server-fns/collections/tree.js +15 -12
- package/dist/server-fns/collections/update.js +2 -1
- package/dist/vite/client-hook-boundary.d.ts +19 -0
- package/dist/vite/client-hook-boundary.js +20 -0
- package/dist/vite/index.d.ts +8 -0
- package/dist/vite/index.js +1 -0
- package/package.json +15 -9
- package/src/admin-shell/admin-activity/list.tsx +13 -3
- package/src/admin-shell/admin-roles/delete.tsx +2 -1
- package/src/admin-shell/admin-roles/list.tsx +2 -1
- package/src/admin-shell/admin-users/delete.tsx +3 -2
- package/src/admin-shell/admin-users/list.tsx +9 -8
- package/src/admin-shell/chrome/admin-app-bar.tsx +2 -1
- package/src/admin-shell/chrome/admin-menu-drawer.tsx +13 -8
- package/src/admin-shell/chrome/admin-menu-provider.tsx +2 -1
- package/src/admin-shell/chrome/dashboard.tsx +4 -3
- package/src/admin-shell/chrome/sign-in-page.tsx +3 -3
- package/src/admin-shell/collections/create.tsx +4 -3
- package/src/admin-shell/collections/document-history.tsx +5 -0
- package/src/admin-shell/collections/edit.tsx +20 -13
- package/src/admin-shell/collections/history.tsx +6 -5
- package/src/admin-shell/collections/list.tsx +8 -7
- package/src/admin-shell/collections/restore-version-modal.tsx +2 -1
- package/src/admin-shell/collections/tree-list.tsx +78 -22
- package/src/admin-shell/collections/view-menu.tsx +9 -8
- package/src/auth/auth-context.test.node.ts +87 -0
- package/src/auth/auth-context.ts +18 -0
- package/src/auth/request-scope.test.node.ts +93 -0
- package/src/auth/request-scope.ts +62 -0
- package/src/integrations/abilities.tsx +14 -4
- package/src/integrations/byline-client.ts +6 -4
- package/src/integrations/byline-public-client.ts +18 -4
- package/src/integrations/byline-viewer-client.ts +30 -18
- package/src/integrations/request-context-contract.test.node.ts +131 -0
- package/src/integrations/request-context-factories.test.node.ts +140 -0
- package/src/integrations/start-errors.test.node.ts +22 -0
- package/src/integrations/start-errors.ts +1 -0
- package/src/routes/admin-path.test.node.ts +129 -0
- package/src/routes/admin-path.ts +66 -0
- package/src/routes/auth-routes.test.node.ts +39 -0
- package/src/routes/create-admin-account-route.tsx +3 -2
- package/src/routes/create-admin-activity-route.tsx +3 -2
- package/src/routes/create-admin-dashboard-route.tsx +2 -1
- package/src/routes/create-admin-layout-route.tsx +4 -4
- package/src/routes/create-admin-permissions-route.tsx +3 -2
- package/src/routes/create-admin-role-edit-route.tsx +7 -3
- package/src/routes/create-admin-roles-list-route.tsx +3 -2
- package/src/routes/create-admin-user-edit-route.tsx +7 -3
- package/src/routes/create-admin-users-list-route.tsx +3 -2
- package/src/routes/create-collection-api-route.tsx +15 -14
- package/src/routes/create-collection-create-route.tsx +7 -3
- package/src/routes/create-collection-edit-route.tsx +12 -12
- package/src/routes/create-collection-history-route.tsx +13 -13
- package/src/routes/create-collection-list-route.tsx +3 -2
- package/src/routes/create-sign-in-route.tsx +9 -3
- package/src/routes/get-content-locale-route-config.ts +26 -0
- package/src/routes/index.ts +11 -0
- package/src/routes/sign-in-path.test.node.ts +79 -0
- package/src/routes/sign-in-path.ts +36 -0
- package/src/server-fns/collections/delete-outcome.test.node.ts +72 -0
- package/src/server-fns/collections/delete-outcome.ts +80 -0
- package/src/server-fns/collections/delete.ts +2 -1
- package/src/server-fns/collections/get.ts +2 -0
- package/src/server-fns/collections/index.ts +1 -0
- package/src/server-fns/collections/tree-mutation.test.node.ts +225 -0
- package/src/server-fns/collections/tree-mutation.ts +165 -0
- package/src/server-fns/collections/tree-read.test.node.ts +46 -0
- package/src/server-fns/collections/tree-read.ts +26 -0
- package/src/server-fns/collections/tree.ts +26 -10
- package/src/server-fns/collections/update.ts +3 -0
- package/src/vite/client-hook-boundary.test.node.ts +50 -0
- package/src/vite/client-hook-boundary.ts +45 -0
- package/src/vite/index.ts +9 -0
|
@@ -7,6 +7,7 @@ import { Container, GripperVerticalIcon, IconButton, LoaderRing, PlusIcon, Searc
|
|
|
7
7
|
import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
|
|
8
8
|
import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
9
9
|
import classnames from "classnames";
|
|
10
|
+
import { getAdminRoutePath } from "../../routes/admin-path.js";
|
|
10
11
|
import { Link, useNavigate } from "../chrome/loose-router.js";
|
|
11
12
|
import { RouterPager } from "../chrome/router-pager.js";
|
|
12
13
|
import { SortAscendingIcon } from "../chrome/sort-icons.js";
|
|
@@ -157,7 +158,7 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = fal
|
|
|
157
158
|
delete params.page;
|
|
158
159
|
params.query = query;
|
|
159
160
|
navigate({
|
|
160
|
-
to: '
|
|
161
|
+
to: getAdminRoutePath('collections', '$collection'),
|
|
161
162
|
params: {
|
|
162
163
|
collection: data.included.collection.path
|
|
163
164
|
},
|
|
@@ -170,7 +171,7 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = fal
|
|
|
170
171
|
delete params.page;
|
|
171
172
|
delete params.query;
|
|
172
173
|
navigate({
|
|
173
|
-
to: '
|
|
174
|
+
to: getAdminRoutePath('collections', '$collection'),
|
|
174
175
|
params: {
|
|
175
176
|
collection: data.included.collection.path
|
|
176
177
|
},
|
|
@@ -184,7 +185,7 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = fal
|
|
|
184
185
|
if ('_all' === value) delete params.status;
|
|
185
186
|
else params.status = value;
|
|
186
187
|
navigate({
|
|
187
|
-
to: '
|
|
188
|
+
to: getAdminRoutePath('collections', '$collection'),
|
|
188
189
|
params: {
|
|
189
190
|
collection: data.included.collection.path
|
|
190
191
|
},
|
|
@@ -197,7 +198,7 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = fal
|
|
|
197
198
|
delete params.page;
|
|
198
199
|
params.page_size = Number.parseInt(value, 10);
|
|
199
200
|
navigate({
|
|
200
|
-
to: '
|
|
201
|
+
to: getAdminRoutePath('collections', '$collection'),
|
|
201
202
|
params: {
|
|
202
203
|
collection: data.included.collection.path
|
|
203
204
|
},
|
|
@@ -223,7 +224,7 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = fal
|
|
|
223
224
|
/*#__PURE__*/ jsx(IconButton, {
|
|
224
225
|
"aria-label": t('collections.list.createAriaLabel'),
|
|
225
226
|
render: /*#__PURE__*/ jsx(Link, {
|
|
226
|
-
to: '
|
|
227
|
+
to: getAdminRoutePath('collections', '$collection', 'create'),
|
|
227
228
|
params: {
|
|
228
229
|
collection: data.included.collection.path
|
|
229
230
|
}
|
|
@@ -328,7 +329,7 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = fal
|
|
|
328
329
|
[list_module.cellCenter]: 'center' === column.align
|
|
329
330
|
}),
|
|
330
331
|
children: useAsTitle && column.fieldName === useAsTitle ? /*#__PURE__*/ jsx(Link, {
|
|
331
|
-
to: '
|
|
332
|
+
to: getAdminRoutePath('collections', '$collection', '$id'),
|
|
332
333
|
params: {
|
|
333
334
|
collection: data.included.collection.path,
|
|
334
335
|
id: document.id
|
|
@@ -371,7 +372,7 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = fal
|
|
|
371
372
|
[list_module.cellCenter]: 'center' === column.align
|
|
372
373
|
}),
|
|
373
374
|
children: useAsTitle && column.fieldName === useAsTitle ? /*#__PURE__*/ jsx(Link, {
|
|
374
|
-
to: '
|
|
375
|
+
to: getAdminRoutePath('collections', '$collection', '$id'),
|
|
375
376
|
params: {
|
|
376
377
|
collection: data.included.collection.path,
|
|
377
378
|
id: document.id
|
|
@@ -5,6 +5,7 @@ import { useRouter } from "@tanstack/react-router";
|
|
|
5
5
|
import { useTranslation } from "@byline/i18n/react";
|
|
6
6
|
import { Alert, Button, LoaderEllipsis, Modal } from "@byline/ui/react";
|
|
7
7
|
import classnames from "classnames";
|
|
8
|
+
import { getAdminRoutePath } from "../../routes/admin-path.js";
|
|
8
9
|
import { restoreDocumentVersion } from "../../server-fns/collections/index.js";
|
|
9
10
|
import { useNavigate } from "../chrome/loose-router.js";
|
|
10
11
|
import restore_version_modal_module from "./restore-version-modal.module.js";
|
|
@@ -29,7 +30,7 @@ function RestoreVersionModal({ collection, documentId, versionId, versionLabel,
|
|
|
29
30
|
onClose();
|
|
30
31
|
await router.invalidate();
|
|
31
32
|
navigate({
|
|
32
|
-
to: '
|
|
33
|
+
to: getAdminRoutePath('collections', '$collection', '$id'),
|
|
33
34
|
params: {
|
|
34
35
|
collection,
|
|
35
36
|
id: documentId
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { useRouter } from "@tanstack/react-router";
|
|
4
4
|
import { StatusBadge, renderFormatted } from "@byline/admin/react";
|
|
5
5
|
import { useTranslation } from "@byline/i18n/react";
|
|
@@ -7,6 +7,8 @@ import { Container, GripperVerticalIcon, IconButton, PlusIcon, Section, Table, u
|
|
|
7
7
|
import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
|
|
8
8
|
import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
9
9
|
import classnames from "classnames";
|
|
10
|
+
import { getAdminRoutePath } from "../../routes/admin-path.js";
|
|
11
|
+
import { createAdminTreeMoveController, getAdminTreeMoveUiEffects, shouldSyncAdminTreeRows } from "../../server-fns/collections/tree-mutation.js";
|
|
10
12
|
import { Link } from "../chrome/loose-router.js";
|
|
11
13
|
import { TableHeadingCellSortable } from "../chrome/th-sortable.js";
|
|
12
14
|
import { formatNumber } from "../chrome/utils.js";
|
|
@@ -17,9 +19,10 @@ function getColumnValue(row, fieldName) {
|
|
|
17
19
|
if (row.fields && fieldName in row.fields) return row.fields[fieldName];
|
|
18
20
|
return row[fieldName];
|
|
19
21
|
}
|
|
20
|
-
function SortableTreeRow({ id,
|
|
22
|
+
function SortableTreeRow({ id, dragging, disabled, dragHandleLabel, children }) {
|
|
21
23
|
const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
|
|
22
|
-
id
|
|
24
|
+
id,
|
|
25
|
+
disabled
|
|
23
26
|
});
|
|
24
27
|
const style = {
|
|
25
28
|
transform: transform ? `translate3d(0, ${transform.y}px, 0)` : void 0,
|
|
@@ -39,6 +42,7 @@ function SortableTreeRow({ id, depth, dragging, dragHandleLabel, children }) {
|
|
|
39
42
|
className: classnames('byline-tree-list-drag-cell', tree_list_module.dragCell),
|
|
40
43
|
children: /*#__PURE__*/ jsx("button", {
|
|
41
44
|
type: "button",
|
|
45
|
+
disabled: disabled,
|
|
42
46
|
className: classnames('byline-tree-list-drag-handle', tree_list_module.dragHandle),
|
|
43
47
|
"aria-label": dragHandleLabel,
|
|
44
48
|
...attributes,
|
|
@@ -59,8 +63,12 @@ const TreeListView = ({ rows, columns, workflowStatuses, useAsTitle, collection,
|
|
|
59
63
|
]);
|
|
60
64
|
const [localPlaced, setLocalPlaced] = useState(()=>rows.filter((r)=>!r.unplaced));
|
|
61
65
|
const [isMoving, setIsMoving] = useState(false);
|
|
66
|
+
const [moveController] = useState(()=>createAdminTreeMoveController());
|
|
67
|
+
const preserveOptimisticForRows = useRef(null);
|
|
62
68
|
useEffect(()=>{
|
|
63
|
-
if (!
|
|
69
|
+
if (!shouldSyncAdminTreeRows(rows, preserveOptimisticForRows.current, isMoving)) return;
|
|
70
|
+
preserveOptimisticForRows.current = null;
|
|
71
|
+
setLocalPlaced(rows.filter((r)=>!r.unplaced));
|
|
64
72
|
}, [
|
|
65
73
|
rows,
|
|
66
74
|
isMoving
|
|
@@ -89,10 +97,13 @@ const TreeListView = ({ rows, columns, workflowStatuses, useAsTitle, collection,
|
|
|
89
97
|
setOverId(null);
|
|
90
98
|
setOffsetLeft(0);
|
|
91
99
|
};
|
|
92
|
-
const handleDragStart = ({ active })=>
|
|
100
|
+
const handleDragStart = ({ active })=>{
|
|
101
|
+
if (!moveController.isMoving()) setActiveId(String(active.id));
|
|
102
|
+
};
|
|
93
103
|
const handleDragMove = ({ delta })=>setOffsetLeft(delta.x);
|
|
94
104
|
const handleDragOver = ({ over })=>setOverId(over ? String(over.id) : null);
|
|
95
105
|
const handleDragEnd = async ({ active, over })=>{
|
|
106
|
+
if (moveController.isMoving()) return void resetDnd();
|
|
96
107
|
const proj = null != over ? getTreeProjection(localPlaced, String(active.id), String(over.id), offsetLeft, INDENT) : null;
|
|
97
108
|
resetDnd();
|
|
98
109
|
if (!onMove || null == proj) return;
|
|
@@ -103,19 +114,24 @@ const TreeListView = ({ rows, columns, workflowStatuses, useAsTitle, collection,
|
|
|
103
114
|
if (unchanged) return;
|
|
104
115
|
setLocalPlaced(next);
|
|
105
116
|
setIsMoving(true);
|
|
106
|
-
|
|
107
|
-
await onMove({
|
|
117
|
+
const outcome = await moveController.execute(()=>onMove({
|
|
108
118
|
documentId,
|
|
109
119
|
parentDocumentId: proj.parentId,
|
|
110
120
|
beforeDocumentId: proj.beforeId,
|
|
111
121
|
afterDocumentId: proj.afterId
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
} catch {
|
|
122
|
+
}), ()=>router.invalidate());
|
|
123
|
+
if ('suppressed' === outcome.status) {
|
|
115
124
|
setLocalPlaced(current);
|
|
125
|
+
setIsMoving(moveController.isMoving());
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const effects = getAdminTreeMoveUiEffects(outcome);
|
|
129
|
+
if (effects.rollback) setLocalPlaced(current);
|
|
130
|
+
if (null != effects.structuralFailure) {
|
|
131
|
+
const conflict = 'conflict' === effects.structuralFailure;
|
|
116
132
|
toastManager.add({
|
|
117
|
-
title: t('collections.list.reorderFailedToast'),
|
|
118
|
-
description: t("collections.list.reorderFailedDescription"),
|
|
133
|
+
title: t(conflict ? 'collections.list.treeConflictToast' : 'collections.list.reorderFailedToast'),
|
|
134
|
+
description: t(conflict ? "collections.list.treeConflictDescription" : "collections.list.reorderFailedDescription"),
|
|
119
135
|
data: {
|
|
120
136
|
intent: 'danger',
|
|
121
137
|
iconType: 'danger',
|
|
@@ -123,9 +139,29 @@ const TreeListView = ({ rows, columns, workflowStatuses, useAsTitle, collection,
|
|
|
123
139
|
close: true
|
|
124
140
|
}
|
|
125
141
|
});
|
|
126
|
-
} finally{
|
|
127
|
-
setIsMoving(false);
|
|
128
142
|
}
|
|
143
|
+
if (effects.committedHookWarning) toastManager.add({
|
|
144
|
+
title: t('collections.list.treeHookFailedToast'),
|
|
145
|
+
description: t("collections.list.treeHookFailedDescription"),
|
|
146
|
+
data: {
|
|
147
|
+
intent: 'warning',
|
|
148
|
+
iconType: 'warning',
|
|
149
|
+
icon: true,
|
|
150
|
+
close: true
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
if (effects.refreshWarning) toastManager.add({
|
|
154
|
+
title: t('collections.list.treeRefreshFailedToast'),
|
|
155
|
+
description: t("collections.list.treeRefreshFailedDescription"),
|
|
156
|
+
data: {
|
|
157
|
+
intent: 'warning',
|
|
158
|
+
iconType: 'warning',
|
|
159
|
+
icon: true,
|
|
160
|
+
close: true
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
if (effects.preserveOptimistic) preserveOptimisticForRows.current = rows;
|
|
164
|
+
setIsMoving(false);
|
|
129
165
|
};
|
|
130
166
|
const renderCells = (row, depth)=>columns.map((column)=>{
|
|
131
167
|
const fieldName = column.fieldName;
|
|
@@ -147,7 +183,7 @@ const TreeListView = ({ rows, columns, workflowStatuses, useAsTitle, collection,
|
|
|
147
183
|
children: "└─"
|
|
148
184
|
}),
|
|
149
185
|
/*#__PURE__*/ jsx(Link, {
|
|
150
|
-
to: '
|
|
186
|
+
to: getAdminRoutePath('collections', '$collection', '$id'),
|
|
151
187
|
params: {
|
|
152
188
|
collection,
|
|
153
189
|
id: row.id
|
|
@@ -183,7 +219,7 @@ const TreeListView = ({ rows, columns, workflowStatuses, useAsTitle, collection,
|
|
|
183
219
|
/*#__PURE__*/ jsx(IconButton, {
|
|
184
220
|
"aria-label": t('collections.list.createAriaLabel'),
|
|
185
221
|
render: /*#__PURE__*/ jsx(Link, {
|
|
186
|
-
to: '
|
|
222
|
+
to: getAdminRoutePath('collections', '$collection', 'create'),
|
|
187
223
|
params: {
|
|
188
224
|
collection
|
|
189
225
|
}
|
|
@@ -234,8 +270,8 @@ const TreeListView = ({ rows, columns, workflowStatuses, useAsTitle, collection,
|
|
|
234
270
|
const depth = row.id === activeId && projection ? projection.depth : row.depth;
|
|
235
271
|
return dndEnabled ? /*#__PURE__*/ jsx(SortableTreeRow, {
|
|
236
272
|
id: row.id,
|
|
237
|
-
depth: depth,
|
|
238
273
|
dragging: row.id === activeId,
|
|
274
|
+
disabled: isMoving,
|
|
239
275
|
dragHandleLabel: t('collections.list.dragHandleAriaLabel'),
|
|
240
276
|
children: renderCells(row, depth)
|
|
241
277
|
}, row.id) : /*#__PURE__*/ jsx(Table.Row, {
|
|
@@ -3,6 +3,7 @@ import { useEffect } from "react";
|
|
|
3
3
|
import { useTranslation } from "@byline/i18n/react";
|
|
4
4
|
import { Button, HistoryIcon, IconButton, Label, Select } from "@byline/ui/react";
|
|
5
5
|
import classnames from "classnames";
|
|
6
|
+
import { getAdminRoutePath } from "../../routes/admin-path.js";
|
|
6
7
|
import { useNavigate } from "../chrome/loose-router.js";
|
|
7
8
|
import { PreviewLink } from "./preview-link.js";
|
|
8
9
|
import view_menu_module from "./view-menu.module.js";
|
|
@@ -11,7 +12,7 @@ const ViewMenu = ({ collection, documentId, activeView, locale, depth, contentLo
|
|
|
11
12
|
const { t } = useTranslation('byline-admin');
|
|
12
13
|
useEffect(()=>{
|
|
13
14
|
if ('edit' === activeView && 'all' === locale) navigate({
|
|
14
|
-
to: '
|
|
15
|
+
to: getAdminRoutePath('collections', '$collection', '$id'),
|
|
15
16
|
params: {
|
|
16
17
|
collection,
|
|
17
18
|
id: documentId
|
|
@@ -36,7 +37,7 @@ const ViewMenu = ({ collection, documentId, activeView, locale, depth, contentLo
|
|
|
36
37
|
locale: value
|
|
37
38
|
});
|
|
38
39
|
if ('api' === activeView) navigate({
|
|
39
|
-
to: '
|
|
40
|
+
to: getAdminRoutePath('collections', '$collection', '$id', 'api'),
|
|
40
41
|
params: {
|
|
41
42
|
collection,
|
|
42
43
|
id: documentId
|
|
@@ -44,14 +45,14 @@ const ViewMenu = ({ collection, documentId, activeView, locale, depth, contentLo
|
|
|
44
45
|
search: search
|
|
45
46
|
});
|
|
46
47
|
else 'history' === activeView ? navigate({
|
|
47
|
-
to: '
|
|
48
|
+
to: getAdminRoutePath('collections', '$collection', '$id', 'history'),
|
|
48
49
|
params: {
|
|
49
50
|
collection,
|
|
50
51
|
id: documentId
|
|
51
52
|
},
|
|
52
53
|
search: search
|
|
53
54
|
}) : navigate({
|
|
54
|
-
to: '
|
|
55
|
+
to: getAdminRoutePath('collections', '$collection', '$id'),
|
|
55
56
|
params: {
|
|
56
57
|
collection,
|
|
57
58
|
id: documentId
|
|
@@ -64,7 +65,7 @@ const ViewMenu = ({ collection, documentId, activeView, locale, depth, contentLo
|
|
|
64
65
|
const n = Number.parseInt(value, 10);
|
|
65
66
|
const nextDepth = Number.isFinite(n) ? Math.max(0, Math.min(3, n)) : 0;
|
|
66
67
|
navigate({
|
|
67
|
-
to: '
|
|
68
|
+
to: getAdminRoutePath('collections', '$collection', '$id', 'api'),
|
|
68
69
|
params: {
|
|
69
70
|
collection,
|
|
70
71
|
id: documentId
|
|
@@ -154,7 +155,7 @@ const ViewMenu = ({ collection, documentId, activeView, locale, depth, contentLo
|
|
|
154
155
|
size: "xs",
|
|
155
156
|
variant: 'history' === activeView ? 'filled' : 'text',
|
|
156
157
|
onClick: ()=>navigate({
|
|
157
|
-
to: '
|
|
158
|
+
to: getAdminRoutePath('collections', '$collection', '$id', 'history'),
|
|
158
159
|
params: {
|
|
159
160
|
collection,
|
|
160
161
|
id: documentId
|
|
@@ -172,7 +173,7 @@ const ViewMenu = ({ collection, documentId, activeView, locale, depth, contentLo
|
|
|
172
173
|
variant: 'edit' === activeView ? 'filled' : 'outlined',
|
|
173
174
|
className: classnames('byline-view-menu-button', view_menu_module.button),
|
|
174
175
|
onClick: ()=>navigate({
|
|
175
|
-
to: '
|
|
176
|
+
to: getAdminRoutePath('collections', '$collection', '$id'),
|
|
176
177
|
params: {
|
|
177
178
|
collection,
|
|
178
179
|
id: documentId
|
|
@@ -188,7 +189,7 @@ const ViewMenu = ({ collection, documentId, activeView, locale, depth, contentLo
|
|
|
188
189
|
variant: 'api' === activeView ? 'filled' : 'outlined',
|
|
189
190
|
className: classnames('byline-view-menu-button', view_menu_module.button),
|
|
190
191
|
onClick: ()=>navigate({
|
|
191
|
-
to: '
|
|
192
|
+
to: getAdminRoutePath('collections', '$collection', '$id', 'api'),
|
|
192
193
|
params: {
|
|
193
194
|
collection,
|
|
194
195
|
id: documentId
|
|
@@ -32,6 +32,19 @@
|
|
|
32
32
|
* Burns a refresh-token rotation only when the access token actually
|
|
33
33
|
* fails verification — not on every request. The "one session" UX is
|
|
34
34
|
* the consequence of this helper working invisibly behind each call.
|
|
35
|
+
*
|
|
36
|
+
* Resolution is memoized per request (`oncePerRequest`): every call within
|
|
37
|
+
* one logical request returns the same `RequestContext` instance — same
|
|
38
|
+
* actor snapshot, same `requestId`. Two invariants depend on this:
|
|
39
|
+
*
|
|
40
|
+
* - The read-authorization layer binds each `ReadContext` to one
|
|
41
|
+
* immutable authority token that includes `requestId`; a fresh id per
|
|
42
|
+
* call makes any two reads sharing a `ReadContext` (the admin tree
|
|
43
|
+
* view, hook-threaded nested reads) throw
|
|
44
|
+
* 'ReadContext cannot be reused across request authorities'.
|
|
45
|
+
* - The refresh path rotates the refresh token; a second unmemoized call
|
|
46
|
+
* in the same request would re-run the dance against the
|
|
47
|
+
* already-rotated cookie and sign the admin out.
|
|
35
48
|
*/
|
|
36
49
|
import { type RequestContext } from '@byline/auth';
|
|
37
50
|
export declare function getAdminRequestContext(): Promise<RequestContext>;
|
|
@@ -2,12 +2,16 @@ import { ERR_UNAUTHENTICATED } from "@byline/auth";
|
|
|
2
2
|
import { getServerConfig } from "@byline/core";
|
|
3
3
|
import { v7 } from "uuid";
|
|
4
4
|
import { clearSessionCookies, readAccessTokenCookie, readRefreshTokenCookie, setSessionCookies } from "./auth-cookies.js";
|
|
5
|
+
import { oncePerRequest } from "./request-scope.js";
|
|
5
6
|
function requireSessionProvider() {
|
|
6
7
|
const provider = getServerConfig().sessionProvider;
|
|
7
8
|
if (!provider) throw new Error("No sessionProvider configured on ServerConfig. Construct a JwtSessionProvider in your server config and pass it to initBylineCore().");
|
|
8
9
|
return provider;
|
|
9
10
|
}
|
|
10
11
|
async function getAdminRequestContext() {
|
|
12
|
+
return oncePerRequest('byline:admin-request-context', resolveAdminRequestContext);
|
|
13
|
+
}
|
|
14
|
+
async function resolveAdminRequestContext() {
|
|
11
15
|
const provider = requireSessionProvider();
|
|
12
16
|
const accessToken = readAccessTokenCookie();
|
|
13
17
|
if (accessToken) try {
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
* Resolve `factory` at most once per HTTP request for a given `key`. The
|
|
10
|
+
* returned promise is memoized on the current Start request — rejections
|
|
11
|
+
* included, since a request's cookies cannot change mid-flight and retrying
|
|
12
|
+
* a failed refresh would burn a second rotation on an already-rotated
|
|
13
|
+
* token. Outside the Start runtime (seed scripts, unit tests, background
|
|
14
|
+
* jobs) there is no request to key on and the factory runs unmemoized.
|
|
15
|
+
*/
|
|
16
|
+
export declare function oncePerRequest<T>(key: string, factory: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getRequest } from "@tanstack/react-start/server";
|
|
2
|
+
const requestScopes = new WeakMap();
|
|
3
|
+
function oncePerRequest(key, factory) {
|
|
4
|
+
const request = currentRequest();
|
|
5
|
+
if (!request) return factory();
|
|
6
|
+
let scope = requestScopes.get(request);
|
|
7
|
+
if (!scope) {
|
|
8
|
+
scope = new Map();
|
|
9
|
+
requestScopes.set(request, scope);
|
|
10
|
+
}
|
|
11
|
+
const existing = scope.get(key);
|
|
12
|
+
if (existing) return existing;
|
|
13
|
+
const pending = factory();
|
|
14
|
+
scope.set(key, pending);
|
|
15
|
+
return pending;
|
|
16
|
+
}
|
|
17
|
+
function currentRequest() {
|
|
18
|
+
try {
|
|
19
|
+
return getRequest();
|
|
20
|
+
} catch {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export { oncePerRequest };
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { useRouteContext } from "@tanstack/react-router";
|
|
2
|
-
|
|
3
|
-
function
|
|
4
|
-
|
|
5
|
-
from:
|
|
2
|
+
import { getAdminRouteId } from "../routes/admin-path.js";
|
|
3
|
+
function useAbilityRouteContext() {
|
|
4
|
+
return useRouteContext({
|
|
5
|
+
from: getAdminRouteId()
|
|
6
6
|
});
|
|
7
|
+
}
|
|
8
|
+
function useAbility(ability) {
|
|
9
|
+
const { user } = useAbilityRouteContext();
|
|
7
10
|
if (user.is_super_admin) return true;
|
|
8
11
|
return user.abilities.includes(ability);
|
|
9
12
|
}
|
|
10
13
|
function useAbilities() {
|
|
11
|
-
const { user } =
|
|
12
|
-
from: ADMIN_ROUTE_ID
|
|
13
|
-
});
|
|
14
|
+
const { user } = useAbilityRouteContext();
|
|
14
15
|
const has = (ability)=>user.is_super_admin || user.abilities.includes(ability);
|
|
15
16
|
const hasAny = (abilities)=>user.is_super_admin || abilities.some((key)=>user.abilities.includes(key));
|
|
16
17
|
return {
|
|
@@ -5,5 +5,43 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* The host's *public-read* `BylineClient` singleton — the strict
|
|
10
|
+
* "preview-can-never-apply" sibling of `getViewerBylineClient`.
|
|
11
|
+
*
|
|
12
|
+
* Configured with `actor: null` and `readMode: 'published'`, full stop.
|
|
13
|
+
* The `requestContext` factory does not consult the `byline_preview`
|
|
14
|
+
* cookie or attempt any admin-session resolution, so an admin browsing
|
|
15
|
+
* with preview mode on cannot accidentally elevate a read that flows
|
|
16
|
+
* through this client. `assertActorCanPerform` permits the null actor
|
|
17
|
+
* exactly on `read` with `readMode === 'published'`, which is what the
|
|
18
|
+
* factory always returns.
|
|
19
|
+
*
|
|
20
|
+
* **Use this client for:**
|
|
21
|
+
* - RSS feeds, Atom feeds, sitemaps
|
|
22
|
+
* - JSON or other endpoints exposed to third-party consumers
|
|
23
|
+
* - any response that an upstream CDN / cache might serve to multiple
|
|
24
|
+
* visitors without keying off the `byline_preview` cookie
|
|
25
|
+
*
|
|
26
|
+
* **Use `getViewerBylineClient` instead for:**
|
|
27
|
+
* - user-facing public pages where an admin's preview-mode session
|
|
28
|
+
* should be honoured
|
|
29
|
+
*
|
|
30
|
+
* Both clients share the same module-scoped singleton pattern so the
|
|
31
|
+
* SDK's per-instance `collectionRecordCache` is amortised across the
|
|
32
|
+
* process lifetime.
|
|
33
|
+
*
|
|
34
|
+
* Companion: `getAdminBylineClient` in `./byline-client.ts` for the
|
|
35
|
+
* admin webapp's authenticated reads (resolves the `RequestContext`
|
|
36
|
+
* from session cookies, memoized per request).
|
|
37
|
+
*/
|
|
38
|
+
import { type RequestContext } from '@byline/auth';
|
|
8
39
|
import { type BylineClient } from '@byline/client';
|
|
40
|
+
/**
|
|
41
|
+
* Anonymous, published-only context — memoized per request so the
|
|
42
|
+
* context (and its requestId) is stable across every read in one
|
|
43
|
+
* request: reads sharing a ReadContext must bind a single request
|
|
44
|
+
* authority.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolvePublicRequestContext(): Promise<RequestContext>;
|
|
9
47
|
export declare function getPublicBylineClient(): BylineClient;
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { createRequestContext } from "@byline/auth";
|
|
2
2
|
import { createBylineClient } from "@byline/client";
|
|
3
3
|
import { getServerConfig } from "@byline/core";
|
|
4
|
+
import { oncePerRequest } from "../auth/request-scope.js";
|
|
5
|
+
function resolvePublicRequestContext() {
|
|
6
|
+
return oncePerRequest('byline:public-request-context', async ()=>createRequestContext({
|
|
7
|
+
readMode: 'published'
|
|
8
|
+
}));
|
|
9
|
+
}
|
|
4
10
|
let cachedClient;
|
|
5
11
|
function getPublicBylineClient() {
|
|
6
12
|
if (cachedClient) return cachedClient;
|
|
7
13
|
cachedClient = createBylineClient({
|
|
8
14
|
config: getServerConfig(),
|
|
9
|
-
requestContext:
|
|
10
|
-
readMode: 'published'
|
|
11
|
-
})
|
|
15
|
+
requestContext: resolvePublicRequestContext
|
|
12
16
|
});
|
|
13
17
|
return cachedClient;
|
|
14
18
|
}
|
|
15
|
-
export { getPublicBylineClient };
|
|
19
|
+
export { getPublicBylineClient, resolvePublicRequestContext };
|
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { type BylineClient } from '@byline/client';
|
|
9
9
|
export declare function getViewerBylineClient(): BylineClient;
|
|
10
|
+
/**
|
|
11
|
+
* Preview-aware viewer context — memoized per request so every read in
|
|
12
|
+
* one request binds the same context instance (and requestId): reads
|
|
13
|
+
* sharing a ReadContext must resolve a single request authority.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveViewerRequestContext(): Promise<import("@byline/auth").RequestContext>;
|
|
10
16
|
/**
|
|
11
17
|
* Resolve whether the current request should surface non-published
|
|
12
18
|
* versions. True iff the preview cookie is set **and** a valid admin
|
|
@@ -3,30 +3,35 @@ import { createBylineClient } from "@byline/client";
|
|
|
3
3
|
import { getServerConfig } from "@byline/core";
|
|
4
4
|
import { getAdminRequestContext } from "../auth/auth-context.js";
|
|
5
5
|
import { readPreviewCookie } from "../auth/preview-cookies.js";
|
|
6
|
+
import { oncePerRequest } from "../auth/request-scope.js";
|
|
6
7
|
let cachedClient;
|
|
7
8
|
function getViewerBylineClient() {
|
|
8
9
|
if (cachedClient) return cachedClient;
|
|
9
10
|
cachedClient = createBylineClient({
|
|
10
11
|
config: getServerConfig(),
|
|
11
|
-
requestContext:
|
|
12
|
-
if (!readPreviewCookie()) return createRequestContext({
|
|
13
|
-
readMode: 'published'
|
|
14
|
-
});
|
|
15
|
-
try {
|
|
16
|
-
const ctx = await getAdminRequestContext();
|
|
17
|
-
return {
|
|
18
|
-
...ctx,
|
|
19
|
-
readMode: 'any'
|
|
20
|
-
};
|
|
21
|
-
} catch {
|
|
22
|
-
return createRequestContext({
|
|
23
|
-
readMode: 'published'
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
}
|
|
12
|
+
requestContext: resolveViewerRequestContext
|
|
27
13
|
});
|
|
28
14
|
return cachedClient;
|
|
29
15
|
}
|
|
16
|
+
function resolveViewerRequestContext() {
|
|
17
|
+
return oncePerRequest('byline:viewer-request-context', resolveViewerContext);
|
|
18
|
+
}
|
|
19
|
+
async function resolveViewerContext() {
|
|
20
|
+
if (!readPreviewCookie()) return createRequestContext({
|
|
21
|
+
readMode: 'published'
|
|
22
|
+
});
|
|
23
|
+
try {
|
|
24
|
+
const ctx = await getAdminRequestContext();
|
|
25
|
+
return {
|
|
26
|
+
...ctx,
|
|
27
|
+
readMode: 'any'
|
|
28
|
+
};
|
|
29
|
+
} catch {
|
|
30
|
+
return createRequestContext({
|
|
31
|
+
readMode: 'published'
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
30
35
|
async function isPreviewActive() {
|
|
31
36
|
if (!readPreviewCookie()) return false;
|
|
32
37
|
try {
|
|
@@ -36,4 +41,4 @@ async function isPreviewActive() {
|
|
|
36
41
|
return false;
|
|
37
42
|
}
|
|
38
43
|
}
|
|
39
|
-
export { getViewerBylineClient, isPreviewActive };
|
|
44
|
+
export { getViewerBylineClient, isPreviewActive, resolveViewerRequestContext };
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
/** Build an admin URL or parameterized TanStack route from client-safe config. */
|
|
9
|
+
export declare function getAdminRoutePath(...segments: ReadonlyArray<string | number>): string;
|
|
10
|
+
/** Build the matching pathless-group route ID used by generated host route trees. */
|
|
11
|
+
export declare function getAdminRouteId(...segments: ReadonlyArray<string | number>): string;
|
|
12
|
+
/** True when a pathname is the target route or one of its segment-delimited descendants. */
|
|
13
|
+
export declare function isRoutePathWithin(pathname: string, target: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Accept an untrusted callback only when it is an unencoded, same-origin path
|
|
16
|
+
* inside the configured admin mount.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveAdminCallbackPath(value: unknown): string | undefined;
|
|
19
|
+
/** Resolve an untrusted callback or fall back to the configured admin dashboard. */
|
|
20
|
+
export declare function resolveAdminSignInRedirect(value: unknown): string;
|
|
21
|
+
/** Match the admin root exactly and admin child entries by pathname prefix. */
|
|
22
|
+
export declare function isAdminRoutePathActive(pathname: string, ...segments: ReadonlyArray<string>): boolean;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getClientConfig, normalizeRootRelativeRedirect } from "@byline/core";
|
|
2
|
+
function getAdminRoutePath(...segments) {
|
|
3
|
+
const configuredBase = getClientConfig().routes.admin;
|
|
4
|
+
const normalizedBase = configuredBase.replace(/^\/+|\/+$/g, '');
|
|
5
|
+
const base = normalizedBase ? `/${normalizedBase}` : '/';
|
|
6
|
+
const suffix = segments.map((segment)=>String(segment).replace(/^\/+|\/+$/g, '')).filter(Boolean).join('/');
|
|
7
|
+
if (!suffix) return base;
|
|
8
|
+
return '/' === base ? `/${suffix}` : `${base}/${suffix}`;
|
|
9
|
+
}
|
|
10
|
+
function getAdminRouteId(...segments) {
|
|
11
|
+
return `/_byline${getAdminRoutePath(...segments)}`;
|
|
12
|
+
}
|
|
13
|
+
function isRoutePathWithin(pathname, target) {
|
|
14
|
+
const current = pathname.length > 1 ? pathname.replace(/\/+$/, '') : pathname;
|
|
15
|
+
const base = target.length > 1 ? target.replace(/\/+$/, '') : target;
|
|
16
|
+
return current === base || ('/' === base ? current.startsWith('/') : current.startsWith(`${base}/`));
|
|
17
|
+
}
|
|
18
|
+
function resolveAdminCallbackPath(value) {
|
|
19
|
+
if ('string' != typeof value) return;
|
|
20
|
+
const normalized = normalizeRootRelativeRedirect(value);
|
|
21
|
+
if (!normalized) return;
|
|
22
|
+
const url = new URL(normalized, 'https://byline.invalid');
|
|
23
|
+
if (!isRoutePathWithin(url.pathname, getAdminRoutePath())) return;
|
|
24
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
25
|
+
}
|
|
26
|
+
function resolveAdminSignInRedirect(value) {
|
|
27
|
+
return resolveAdminCallbackPath(value) ?? getAdminRoutePath();
|
|
28
|
+
}
|
|
29
|
+
function isAdminRoutePathActive(pathname, ...segments) {
|
|
30
|
+
const target = getAdminRoutePath(...segments);
|
|
31
|
+
const current = pathname.length > 1 ? pathname.replace(/\/+$/, '') : pathname;
|
|
32
|
+
if (0 === segments.length) return current === target;
|
|
33
|
+
return isRoutePathWithin(current, target);
|
|
34
|
+
}
|
|
35
|
+
export { getAdminRouteId, getAdminRoutePath, isAdminRoutePathActive, isRoutePathWithin, resolveAdminCallbackPath, resolveAdminSignInRedirect };
|