@byline/host-tanstack-start 3.13.3 → 3.15.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/collections/list.d.ts +6 -3
- package/dist/admin-shell/collections/list.js +4 -1
- package/dist/admin-shell/collections/reindex-button.d.ts +25 -0
- package/dist/admin-shell/collections/reindex-button.js +55 -0
- package/dist/admin-shell/collections/tree-list.d.ts +4 -2
- package/dist/admin-shell/collections/tree-list.js +4 -1
- package/dist/integrations/byline-client.d.ts +14 -11
- package/dist/integrations/byline-client.js +13 -1
- package/dist/routes/create-collection-list-route.js +4 -1
- package/dist/server-fns/collections/index.d.ts +1 -0
- package/dist/server-fns/collections/index.js +1 -0
- package/dist/server-fns/collections/list.js +10 -1
- package/dist/server-fns/collections/reindex.d.ts +14 -0
- package/dist/server-fns/collections/reindex.js +18 -0
- package/package.json +8 -8
- package/src/admin-shell/collections/list.tsx +9 -1
- package/src/admin-shell/collections/reindex-button.tsx +69 -0
- package/src/admin-shell/collections/tree-list.tsx +8 -1
- package/src/integrations/byline-client.ts +26 -0
- package/src/routes/create-collection-edit-route.tsx +1 -1
- package/src/routes/create-collection-list-route.tsx +7 -1
- package/src/server-fns/collections/index.ts +1 -0
- package/src/server-fns/collections/list.ts +29 -0
- package/src/server-fns/collections/reindex.ts +47 -0
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
import type
|
|
8
|
+
import type React from 'react';
|
|
9
|
+
import type { ColumnDefinition, ListActionComponentProps, WorkflowStatus } from '@byline/core';
|
|
9
10
|
import type { AnyCollectionSchemaTypes } from '@byline/core/zod-schemas';
|
|
10
11
|
type ReorderFn = (params: {
|
|
11
12
|
documentId: string;
|
|
12
13
|
beforeDocumentId: string | null;
|
|
13
14
|
afterDocumentId: string | null;
|
|
14
15
|
}) => Promise<unknown>;
|
|
15
|
-
export declare const ListView: ({ data, columns, workflowStatuses, useAsTitle, orderable, onReorder, }: {
|
|
16
|
+
export declare const ListView: ({ data, columns, workflowStatuses, useAsTitle, orderable, onReorder, listActions, }: {
|
|
16
17
|
data: AnyCollectionSchemaTypes["ListType"];
|
|
17
18
|
columns: ColumnDefinition[];
|
|
18
19
|
workflowStatuses?: WorkflowStatus[];
|
|
@@ -21,5 +22,7 @@ export declare const ListView: ({ data, columns, workflowStatuses, useAsTitle, o
|
|
|
21
22
|
orderable?: boolean;
|
|
22
23
|
/** Persists a single-row reorder via the host's reorder server fn. */
|
|
23
24
|
onReorder?: ReorderFn;
|
|
24
|
-
|
|
25
|
+
/** Header action components (`CollectionAdminConfig.listActions`). */
|
|
26
|
+
listActions?: Array<(props: ListActionComponentProps) => React.ReactNode>;
|
|
27
|
+
}) => React.JSX.Element;
|
|
25
28
|
export {};
|
|
@@ -70,7 +70,7 @@ function SortableTableRow({ id, disabled, t, children }) {
|
|
|
70
70
|
]
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
|
-
const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = false, onReorder })=>{
|
|
73
|
+
const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = false, onReorder, listActions })=>{
|
|
74
74
|
const navigate = useNavigate();
|
|
75
75
|
const router = useRouter();
|
|
76
76
|
const toastManager = useToastManager();
|
|
@@ -215,6 +215,9 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = fal
|
|
|
215
215
|
/*#__PURE__*/ jsx(Stats, {
|
|
216
216
|
total: data?.meta.total
|
|
217
217
|
}),
|
|
218
|
+
listActions?.map((Action, i)=>/*#__PURE__*/ jsx(Action, {
|
|
219
|
+
collectionPath: data.included.collection.path
|
|
220
|
+
}, i)),
|
|
218
221
|
/*#__PURE__*/ jsx(IconButton, {
|
|
219
222
|
"aria-label": t('collections.list.createAriaLabel'),
|
|
220
223
|
render: /*#__PURE__*/ jsx(Link, {
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
* A `CollectionAdminConfig.listActions` component that rebuilds a collection's
|
|
10
|
+
* search index. Register it on a searchable collection's admin config:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { ReindexButton } from '@byline/host-tanstack-start/admin-shell/collections/reindex-button'
|
|
14
|
+
* // CollectionAdminConfig:
|
|
15
|
+
* listActions: [ReindexButton],
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Permission-gated both ends: hidden unless the actor holds
|
|
19
|
+
* `collections.<path>.reindex` (the same ability the server fn re-asserts).
|
|
20
|
+
* Synchronous — fine for small/medium collections; a large corpus wants a
|
|
21
|
+
* backgrounded job (see docs/05-reading-and-delivery/07-search.md).
|
|
22
|
+
*/
|
|
23
|
+
import type React from 'react';
|
|
24
|
+
import type { ListActionComponentProps } from '@byline/core';
|
|
25
|
+
export declare function ReindexButton({ collectionPath, }: ListActionComponentProps): React.JSX.Element | null;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { useTranslation } from "@byline/i18n/react";
|
|
4
|
+
import { Button, useToastManager } from "@byline/ui/react";
|
|
5
|
+
import { useAbility } from "../../integrations/abilities.js";
|
|
6
|
+
import { reindexCollection } from "../../server-fns/collections/reindex.js";
|
|
7
|
+
function ReindexButton({ collectionPath }) {
|
|
8
|
+
const { t } = useTranslation('byline-admin');
|
|
9
|
+
const toastManager = useToastManager();
|
|
10
|
+
const canReindex = useAbility(`collections.${collectionPath}.reindex`);
|
|
11
|
+
const [pending, setPending] = useState(false);
|
|
12
|
+
if (!canReindex) return null;
|
|
13
|
+
const onClick = async ()=>{
|
|
14
|
+
setPending(true);
|
|
15
|
+
try {
|
|
16
|
+
const report = await reindexCollection({
|
|
17
|
+
data: {
|
|
18
|
+
collection: collectionPath
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
toastManager.add({
|
|
22
|
+
title: t('collections.list.reindexDoneTitle'),
|
|
23
|
+
description: t("collections.list.reindexDoneDescription", {
|
|
24
|
+
count: report.documents
|
|
25
|
+
}),
|
|
26
|
+
data: {
|
|
27
|
+
intent: 'success',
|
|
28
|
+
iconType: 'success',
|
|
29
|
+
icon: true,
|
|
30
|
+
close: true
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
} catch {
|
|
34
|
+
toastManager.add({
|
|
35
|
+
title: t('collections.list.reindexFailedTitle'),
|
|
36
|
+
data: {
|
|
37
|
+
intent: 'danger',
|
|
38
|
+
iconType: 'danger',
|
|
39
|
+
icon: true,
|
|
40
|
+
close: true
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
} finally{
|
|
44
|
+
setPending(false);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
return /*#__PURE__*/ jsx(Button, {
|
|
48
|
+
size: "sm",
|
|
49
|
+
variant: "outlined",
|
|
50
|
+
disabled: pending,
|
|
51
|
+
onClick: onClick,
|
|
52
|
+
children: pending ? t('collections.list.reindexPending') : t('collections.list.reindexLabel')
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export { ReindexButton };
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
import type React from 'react';
|
|
9
|
-
import type { ColumnDefinition, WorkflowStatus } from '@byline/core';
|
|
9
|
+
import type { ColumnDefinition, ListActionComponentProps, WorkflowStatus } from '@byline/core';
|
|
10
10
|
import type { CollectionTreeRow } from '../../server-fns/collections/tree.js';
|
|
11
11
|
export type TreeMoveFn = (params: {
|
|
12
12
|
documentId: string;
|
|
@@ -14,7 +14,7 @@ export type TreeMoveFn = (params: {
|
|
|
14
14
|
beforeDocumentId: string | null;
|
|
15
15
|
afterDocumentId: string | null;
|
|
16
16
|
}) => Promise<unknown>;
|
|
17
|
-
export declare const TreeListView: ({ rows, columns, workflowStatuses, useAsTitle, collection, collectionLabels, onMove, }: {
|
|
17
|
+
export declare const TreeListView: ({ rows, columns, workflowStatuses, useAsTitle, collection, collectionLabels, onMove, listActions, }: {
|
|
18
18
|
rows: CollectionTreeRow[];
|
|
19
19
|
columns: ColumnDefinition[];
|
|
20
20
|
workflowStatuses?: WorkflowStatus[];
|
|
@@ -26,4 +26,6 @@ export declare const TreeListView: ({ rows, columns, workflowStatuses, useAsTitl
|
|
|
26
26
|
};
|
|
27
27
|
/** When provided, the placed tree becomes drag-to-reorder / re-parent. */
|
|
28
28
|
onMove?: TreeMoveFn;
|
|
29
|
+
/** Header action components (`CollectionAdminConfig.listActions`). */
|
|
30
|
+
listActions?: Array<(props: ListActionComponentProps) => React.ReactNode>;
|
|
29
31
|
}) => React.JSX.Element;
|
|
@@ -50,7 +50,7 @@ function SortableTreeRow({ id, depth, dragging, dragHandleLabel, children }) {
|
|
|
50
50
|
]
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
|
-
const TreeListView = ({ rows, columns, workflowStatuses, useAsTitle, collection, collectionLabels, onMove })=>{
|
|
53
|
+
const TreeListView = ({ rows, columns, workflowStatuses, useAsTitle, collection, collectionLabels, onMove, listActions })=>{
|
|
54
54
|
const { t } = useTranslation('byline-admin');
|
|
55
55
|
const router = useRouter();
|
|
56
56
|
const toastManager = useToastManager();
|
|
@@ -177,6 +177,9 @@ const TreeListView = ({ rows, columns, workflowStatuses, useAsTitle, collection,
|
|
|
177
177
|
className: classnames('byline-coll-list-stats', tree_list_module.stats),
|
|
178
178
|
children: formatNumber(rows.length, 0)
|
|
179
179
|
}),
|
|
180
|
+
listActions?.map((Action, i)=>/*#__PURE__*/ jsx(Action, {
|
|
181
|
+
collectionPath: collection
|
|
182
|
+
}, i)),
|
|
180
183
|
/*#__PURE__*/ jsx(IconButton, {
|
|
181
184
|
"aria-label": t('collections.list.createAriaLabel'),
|
|
182
185
|
render: /*#__PURE__*/ jsx(Link, {
|
|
@@ -5,17 +5,20 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
+
import { type BylineClient } from '@byline/client';
|
|
9
|
+
export declare function getAdminBylineClient(): BylineClient;
|
|
8
10
|
/**
|
|
9
|
-
*
|
|
11
|
+
* A `BylineClient` bound to an explicit super-admin context — for
|
|
12
|
+
* **system / background** work that is not scoped to an HTTP request:
|
|
13
|
+
* lifecycle-hook search indexing, maintenance scripts, seeds, migrations.
|
|
10
14
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* `
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* Unlike {@link getAdminBylineClient}, this does **not** read session
|
|
16
|
+
* cookies, so it works outside the TanStack Start server runtime (a bare
|
|
17
|
+
* `tsx` script, a seed, a test). Reaching for the request-scoped client in
|
|
18
|
+
* a lifecycle hook couples that hook to the live server and throws
|
|
19
|
+
* `No StartEvent found in AsyncLocalStorage` from any out-of-band write
|
|
20
|
+
* path. Indexing is maintenance, not a user action — it reads the published
|
|
21
|
+
* view and bypasses `beforeRead` — so the super-admin context is both
|
|
22
|
+
* correct and runtime-agnostic. The context is auditable by its stable id.
|
|
19
23
|
*/
|
|
20
|
-
|
|
21
|
-
export declare function getAdminBylineClient(): BylineClient;
|
|
24
|
+
export declare function getSystemBylineClient(): BylineClient;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createSuperAdminContext } from "@byline/auth";
|
|
1
2
|
import { createBylineClient } from "@byline/client";
|
|
2
3
|
import { getServerConfig } from "@byline/core";
|
|
3
4
|
import { getAdminRequestContext } from "../auth/auth-context.js";
|
|
@@ -10,4 +11,15 @@ function getAdminBylineClient() {
|
|
|
10
11
|
});
|
|
11
12
|
return cachedClient;
|
|
12
13
|
}
|
|
13
|
-
|
|
14
|
+
let cachedSystemClient;
|
|
15
|
+
function getSystemBylineClient() {
|
|
16
|
+
if (cachedSystemClient) return cachedSystemClient;
|
|
17
|
+
cachedSystemClient = createBylineClient({
|
|
18
|
+
config: getServerConfig(),
|
|
19
|
+
requestContext: createSuperAdminContext({
|
|
20
|
+
id: 'byline-system-client'
|
|
21
|
+
})
|
|
22
|
+
});
|
|
23
|
+
return cachedSystemClient;
|
|
24
|
+
}
|
|
25
|
+
export { getAdminBylineClient, getSystemBylineClient };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
3
|
import { createFileRoute, notFound } from "@tanstack/react-router";
|
|
4
|
+
import { applyRelationColumnFormatters } from "@byline/admin/react";
|
|
4
5
|
import { getCollectionAdminConfig, getCollectionDefinition, getWorkflowStatuses } from "@byline/core";
|
|
5
6
|
import { useTranslation } from "@byline/i18n/react";
|
|
6
7
|
import { useToastManager } from "@byline/ui/react";
|
|
@@ -71,7 +72,7 @@ function createCollectionListRoute(path) {
|
|
|
71
72
|
const navigate = useNavigate();
|
|
72
73
|
const collectionDef = getCollectionDefinition(collection);
|
|
73
74
|
const adminConfig = getCollectionAdminConfig(collection);
|
|
74
|
-
const columns = adminConfig?.columns || [];
|
|
75
|
+
const columns = applyRelationColumnFormatters(adminConfig?.columns || [], collectionDef);
|
|
75
76
|
const workflowStatuses = getWorkflowStatuses(collectionDef);
|
|
76
77
|
const createdToastFiredRef = useRef(false);
|
|
77
78
|
useEffect(()=>{
|
|
@@ -132,6 +133,7 @@ function createCollectionListRoute(path) {
|
|
|
132
133
|
useAsTitle: collectionDef.useAsTitle,
|
|
133
134
|
collection: collection,
|
|
134
135
|
collectionLabels: data.included.collection.labels,
|
|
136
|
+
listActions: adminConfig?.listActions,
|
|
135
137
|
onMove: async ({ documentId, parentDocumentId, beforeDocumentId, afterDocumentId })=>{
|
|
136
138
|
await placeTreeNode({
|
|
137
139
|
data: {
|
|
@@ -152,6 +154,7 @@ function createCollectionListRoute(path) {
|
|
|
152
154
|
workflowStatuses: workflowStatuses,
|
|
153
155
|
useAsTitle: collectionDef.useAsTitle,
|
|
154
156
|
orderable: true === collectionDef.orderable,
|
|
157
|
+
listActions: adminConfig?.listActions,
|
|
155
158
|
onReorder: async ({ documentId, beforeDocumentId, afterDocumentId })=>{
|
|
156
159
|
await reorderCollectionDocument({
|
|
157
160
|
data: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
-
import { ERR_NOT_FOUND, getCollectionSchemasForPath, getLogger, getServerConfig } from "@byline/core";
|
|
2
|
+
import { ERR_NOT_FOUND, buildRelationSummaryPopulateMap, getCollectionAdminConfig, getCollectionDefinition, getCollectionSchemasForPath, getLogger, getServerConfig } from "@byline/core";
|
|
3
3
|
import { ensureCollection } from "../../integrations/api-utils.js";
|
|
4
4
|
import { getAdminBylineClient } from "../../integrations/byline-client.js";
|
|
5
5
|
import { serialise } from "./utils.js";
|
|
@@ -26,6 +26,12 @@ const getCollectionDocuments = createServerFn({
|
|
|
26
26
|
const sortSpec = params.order ? {
|
|
27
27
|
[params.order]: false === params.desc ? 'asc' : 'desc'
|
|
28
28
|
} : defaultSort;
|
|
29
|
+
const populateMap = buildRelationSummaryPopulateMap(config.definition.fields, (targetPath)=>({
|
|
30
|
+
def: getCollectionDefinition(targetPath),
|
|
31
|
+
admin: getCollectionAdminConfig(targetPath)
|
|
32
|
+
}));
|
|
33
|
+
const hasRelations = Object.keys(populateMap).length > 0;
|
|
34
|
+
const populate = hasRelations ? populateMap : void 0;
|
|
29
35
|
const result = await handle.find({
|
|
30
36
|
where: Object.keys(where).length > 0 ? where : void 0,
|
|
31
37
|
sort: sortSpec,
|
|
@@ -33,6 +39,8 @@ const getCollectionDocuments = createServerFn({
|
|
|
33
39
|
page: params.page,
|
|
34
40
|
pageSize,
|
|
35
41
|
select: params.fields,
|
|
42
|
+
populate,
|
|
43
|
+
depth: hasRelations ? 1 : void 0,
|
|
36
44
|
status: 'any',
|
|
37
45
|
onMissingLocale: 'empty'
|
|
38
46
|
});
|
|
@@ -65,6 +73,7 @@ const getCollectionDocuments = createServerFn({
|
|
|
65
73
|
};
|
|
66
74
|
const serialised = serialise(response);
|
|
67
75
|
const { list } = getCollectionSchemasForPath(path);
|
|
76
|
+
if (hasRelations) return serialised;
|
|
68
77
|
return list.parse(serialised);
|
|
69
78
|
});
|
|
70
79
|
export { getCollectionDocuments };
|
|
@@ -0,0 +1,14 @@
|
|
|
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 { ReindexResult } from '@byline/client';
|
|
9
|
+
export type { ReindexResult };
|
|
10
|
+
export declare const reindexCollection: import("@tanstack/react-start").RequiredFetcher<undefined, (input: {
|
|
11
|
+
collection: string;
|
|
12
|
+
}) => {
|
|
13
|
+
collection: string;
|
|
14
|
+
}, Promise<ReindexResult>>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
+
import { ERR_NOT_FOUND, getLogger } from "@byline/core";
|
|
3
|
+
import { ensureCollection } from "../../integrations/api-utils.js";
|
|
4
|
+
import { getAdminBylineClient } from "../../integrations/byline-client.js";
|
|
5
|
+
const reindexCollection = createServerFn({
|
|
6
|
+
method: 'POST'
|
|
7
|
+
}).validator((input)=>input).handler(async ({ data })=>{
|
|
8
|
+
const { collection } = data;
|
|
9
|
+
const config = await ensureCollection(collection);
|
|
10
|
+
if (!config) throw ERR_NOT_FOUND({
|
|
11
|
+
message: 'Collection not found',
|
|
12
|
+
details: {
|
|
13
|
+
collectionPath: collection
|
|
14
|
+
}
|
|
15
|
+
}).log(getLogger());
|
|
16
|
+
return getAdminBylineClient().collection(collection).reindex();
|
|
17
|
+
});
|
|
18
|
+
export { reindexCollection };
|
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.
|
|
6
|
+
"version": "3.15.0",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=20.9.0"
|
|
9
9
|
},
|
|
@@ -119,13 +119,13 @@
|
|
|
119
119
|
"react-swipeable": "^7.0.2",
|
|
120
120
|
"uuid": "^14.0.0",
|
|
121
121
|
"zod": "^4.4.3",
|
|
122
|
-
"@byline/
|
|
123
|
-
"@byline/
|
|
124
|
-
"@byline/
|
|
125
|
-
"@byline/
|
|
126
|
-
"@byline/
|
|
127
|
-
"@byline/
|
|
128
|
-
"@byline/
|
|
122
|
+
"@byline/admin": "3.15.0",
|
|
123
|
+
"@byline/auth": "3.15.0",
|
|
124
|
+
"@byline/ai": "3.15.0",
|
|
125
|
+
"@byline/i18n": "3.15.0",
|
|
126
|
+
"@byline/client": "3.15.0",
|
|
127
|
+
"@byline/core": "3.15.0",
|
|
128
|
+
"@byline/ui": "3.15.0"
|
|
129
129
|
},
|
|
130
130
|
"peerDependencies": {
|
|
131
131
|
"@tanstack/react-router": "^1.167.0",
|
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import type React from 'react'
|
|
9
10
|
import { useEffect, useMemo, useState } from 'react'
|
|
10
11
|
import { useRouter, useRouterState } from '@tanstack/react-router'
|
|
11
12
|
|
|
12
13
|
import { renderFormatted, StatusBadge } from '@byline/admin/react'
|
|
13
|
-
import type { ColumnDefinition, WorkflowStatus } from '@byline/core'
|
|
14
|
+
import type { ColumnDefinition, ListActionComponentProps, WorkflowStatus } from '@byline/core'
|
|
14
15
|
import type { AnyCollectionSchemaTypes } from '@byline/core/zod-schemas'
|
|
15
16
|
import type { UseTranslationReturn } from '@byline/i18n/react'
|
|
16
17
|
import { useTranslation } from '@byline/i18n/react'
|
|
@@ -158,6 +159,7 @@ export const ListView = ({
|
|
|
158
159
|
useAsTitle,
|
|
159
160
|
orderable = false,
|
|
160
161
|
onReorder,
|
|
162
|
+
listActions,
|
|
161
163
|
}: {
|
|
162
164
|
data: AnyCollectionSchemaTypes['ListType']
|
|
163
165
|
columns: ColumnDefinition[]
|
|
@@ -167,6 +169,8 @@ export const ListView = ({
|
|
|
167
169
|
orderable?: boolean
|
|
168
170
|
/** Persists a single-row reorder via the host's reorder server fn. */
|
|
169
171
|
onReorder?: ReorderFn
|
|
172
|
+
/** Header action components (`CollectionAdminConfig.listActions`). */
|
|
173
|
+
listActions?: Array<(props: ListActionComponentProps) => React.ReactNode>
|
|
170
174
|
}) => {
|
|
171
175
|
const navigate = useNavigate()
|
|
172
176
|
const router = useRouter()
|
|
@@ -316,6 +320,10 @@ export const ListView = ({
|
|
|
316
320
|
{data.included.collection.labels.plural as string}
|
|
317
321
|
</h1>
|
|
318
322
|
<Stats total={data?.meta.total} />
|
|
323
|
+
{listActions?.map((Action, i) => (
|
|
324
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: static config order
|
|
325
|
+
<Action key={i} collectionPath={data.included.collection.path as string} />
|
|
326
|
+
))}
|
|
319
327
|
<IconButton
|
|
320
328
|
aria-label={t('collections.list.createAriaLabel')}
|
|
321
329
|
render={
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A `CollectionAdminConfig.listActions` component that rebuilds a collection's
|
|
11
|
+
* search index. Register it on a searchable collection's admin config:
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { ReindexButton } from '@byline/host-tanstack-start/admin-shell/collections/reindex-button'
|
|
15
|
+
* // CollectionAdminConfig:
|
|
16
|
+
* listActions: [ReindexButton],
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Permission-gated both ends: hidden unless the actor holds
|
|
20
|
+
* `collections.<path>.reindex` (the same ability the server fn re-asserts).
|
|
21
|
+
* Synchronous — fine for small/medium collections; a large corpus wants a
|
|
22
|
+
* backgrounded job (see docs/05-reading-and-delivery/07-search.md).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type React from 'react'
|
|
26
|
+
import { useState } from 'react'
|
|
27
|
+
|
|
28
|
+
import type { ListActionComponentProps } from '@byline/core'
|
|
29
|
+
import { useTranslation } from '@byline/i18n/react'
|
|
30
|
+
import { Button, useToastManager } from '@byline/ui/react'
|
|
31
|
+
|
|
32
|
+
import { useAbility } from '../../integrations/abilities.js'
|
|
33
|
+
import { reindexCollection } from '../../server-fns/collections/reindex.js'
|
|
34
|
+
|
|
35
|
+
export function ReindexButton({
|
|
36
|
+
collectionPath,
|
|
37
|
+
}: ListActionComponentProps): React.JSX.Element | null {
|
|
38
|
+
const { t } = useTranslation('byline-admin')
|
|
39
|
+
const toastManager = useToastManager()
|
|
40
|
+
const canReindex = useAbility(`collections.${collectionPath}.reindex`)
|
|
41
|
+
const [pending, setPending] = useState(false)
|
|
42
|
+
|
|
43
|
+
if (!canReindex) return null
|
|
44
|
+
|
|
45
|
+
const onClick = async (): Promise<void> => {
|
|
46
|
+
setPending(true)
|
|
47
|
+
try {
|
|
48
|
+
const report = await reindexCollection({ data: { collection: collectionPath } })
|
|
49
|
+
toastManager.add({
|
|
50
|
+
title: t('collections.list.reindexDoneTitle'),
|
|
51
|
+
description: t('collections.list.reindexDoneDescription', { count: report.documents }),
|
|
52
|
+
data: { intent: 'success', iconType: 'success', icon: true, close: true },
|
|
53
|
+
})
|
|
54
|
+
} catch {
|
|
55
|
+
toastManager.add({
|
|
56
|
+
title: t('collections.list.reindexFailedTitle'),
|
|
57
|
+
data: { intent: 'danger', iconType: 'danger', icon: true, close: true },
|
|
58
|
+
})
|
|
59
|
+
} finally {
|
|
60
|
+
setPending(false)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Button size="sm" variant="outlined" disabled={pending} onClick={onClick}>
|
|
66
|
+
{pending ? t('collections.list.reindexPending') : t('collections.list.reindexLabel')}
|
|
67
|
+
</Button>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -11,7 +11,7 @@ import { useEffect, useMemo, useState } from 'react'
|
|
|
11
11
|
import { useRouter } from '@tanstack/react-router'
|
|
12
12
|
|
|
13
13
|
import { renderFormatted, StatusBadge } from '@byline/admin/react'
|
|
14
|
-
import type { ColumnDefinition, WorkflowStatus } from '@byline/core'
|
|
14
|
+
import type { ColumnDefinition, ListActionComponentProps, WorkflowStatus } from '@byline/core'
|
|
15
15
|
import { useTranslation } from '@byline/i18n/react'
|
|
16
16
|
import {
|
|
17
17
|
Container,
|
|
@@ -124,6 +124,7 @@ export const TreeListView = ({
|
|
|
124
124
|
collection,
|
|
125
125
|
collectionLabels,
|
|
126
126
|
onMove,
|
|
127
|
+
listActions,
|
|
127
128
|
}: {
|
|
128
129
|
rows: CollectionTreeRow[]
|
|
129
130
|
columns: ColumnDefinition[]
|
|
@@ -133,6 +134,8 @@ export const TreeListView = ({
|
|
|
133
134
|
collectionLabels: { singular: string; plural: string }
|
|
134
135
|
/** When provided, the placed tree becomes drag-to-reorder / re-parent. */
|
|
135
136
|
onMove?: TreeMoveFn
|
|
137
|
+
/** Header action components (`CollectionAdminConfig.listActions`). */
|
|
138
|
+
listActions?: Array<(props: ListActionComponentProps) => React.ReactNode>
|
|
136
139
|
}) => {
|
|
137
140
|
const { t } = useTranslation('byline-admin')
|
|
138
141
|
const router = useRouter()
|
|
@@ -267,6 +270,10 @@ export const TreeListView = ({
|
|
|
267
270
|
<span className={cx('byline-coll-list-stats', styles.stats)}>
|
|
268
271
|
{formatNumber(rows.length, 0)}
|
|
269
272
|
</span>
|
|
273
|
+
{listActions?.map((Action, i) => (
|
|
274
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: static config order
|
|
275
|
+
<Action key={i} collectionPath={collection} />
|
|
276
|
+
))}
|
|
270
277
|
<IconButton
|
|
271
278
|
aria-label={t('collections.list.createAriaLabel')}
|
|
272
279
|
render={
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* adapter wiring.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import { createSuperAdminContext } from '@byline/auth'
|
|
22
23
|
import { type BylineClient, createBylineClient } from '@byline/client'
|
|
23
24
|
import { getServerConfig } from '@byline/core'
|
|
24
25
|
|
|
@@ -38,3 +39,28 @@ export function getAdminBylineClient(): BylineClient {
|
|
|
38
39
|
})
|
|
39
40
|
return cachedClient
|
|
40
41
|
}
|
|
42
|
+
|
|
43
|
+
let cachedSystemClient: BylineClient | undefined
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A `BylineClient` bound to an explicit super-admin context — for
|
|
47
|
+
* **system / background** work that is not scoped to an HTTP request:
|
|
48
|
+
* lifecycle-hook search indexing, maintenance scripts, seeds, migrations.
|
|
49
|
+
*
|
|
50
|
+
* Unlike {@link getAdminBylineClient}, this does **not** read session
|
|
51
|
+
* cookies, so it works outside the TanStack Start server runtime (a bare
|
|
52
|
+
* `tsx` script, a seed, a test). Reaching for the request-scoped client in
|
|
53
|
+
* a lifecycle hook couples that hook to the live server and throws
|
|
54
|
+
* `No StartEvent found in AsyncLocalStorage` from any out-of-band write
|
|
55
|
+
* path. Indexing is maintenance, not a user action — it reads the published
|
|
56
|
+
* view and bypasses `beforeRead` — so the super-admin context is both
|
|
57
|
+
* correct and runtime-agnostic. The context is auditable by its stable id.
|
|
58
|
+
*/
|
|
59
|
+
export function getSystemBylineClient(): BylineClient {
|
|
60
|
+
if (cachedSystemClient) return cachedSystemClient
|
|
61
|
+
cachedSystemClient = createBylineClient({
|
|
62
|
+
config: getServerConfig(),
|
|
63
|
+
requestContext: createSuperAdminContext({ id: 'byline-system-client' }),
|
|
64
|
+
})
|
|
65
|
+
return cachedSystemClient
|
|
66
|
+
}
|
|
@@ -54,7 +54,7 @@ export function createCollectionEditRoute(path: string, opts: CollectionEditOpts
|
|
|
54
54
|
// Auto-populate direct relation fields (depth 1) so the edit form's
|
|
55
55
|
// relation-summary tiles render with target data (category name, media
|
|
56
56
|
// thumbnail, etc.) on first paint. Projection is derived from each
|
|
57
|
-
// target's `CollectionAdminConfig.
|
|
57
|
+
// target's `CollectionAdminConfig.itemView` columns.
|
|
58
58
|
const data = await getCollectionDocument(
|
|
59
59
|
params.collection,
|
|
60
60
|
params.id,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { useEffect, useRef } from 'react'
|
|
10
10
|
import { createFileRoute, notFound } from '@tanstack/react-router'
|
|
11
11
|
|
|
12
|
+
import { applyRelationColumnFormatters } from '@byline/admin/react'
|
|
12
13
|
import type { CollectionDefinition } from '@byline/core'
|
|
13
14
|
import {
|
|
14
15
|
getCollectionAdminConfig,
|
|
@@ -118,7 +119,10 @@ export function createCollectionListRoute(path: string) {
|
|
|
118
119
|
const navigate = useNavigate()
|
|
119
120
|
const collectionDef = getCollectionDefinition(collection) as CollectionDefinition
|
|
120
121
|
const adminConfig = getCollectionAdminConfig(collection)
|
|
121
|
-
|
|
122
|
+
// Apply the built-in relation formatter to any relation column without an
|
|
123
|
+
// explicit one, so relation cells render the target's title (the list
|
|
124
|
+
// read populates relation columns to depth 1).
|
|
125
|
+
const columns = applyRelationColumnFormatters(adminConfig?.columns || [], collectionDef)
|
|
122
126
|
const workflowStatuses = getWorkflowStatuses(collectionDef)
|
|
123
127
|
|
|
124
128
|
// Ref-guarded so the post-create toast fires exactly once per arrival with
|
|
@@ -178,6 +182,7 @@ export function createCollectionListRoute(path: string) {
|
|
|
178
182
|
useAsTitle={collectionDef.useAsTitle}
|
|
179
183
|
collection={collection}
|
|
180
184
|
collectionLabels={data.included.collection.labels}
|
|
185
|
+
listActions={adminConfig?.listActions}
|
|
181
186
|
onMove={async ({
|
|
182
187
|
documentId,
|
|
183
188
|
parentDocumentId,
|
|
@@ -204,6 +209,7 @@ export function createCollectionListRoute(path: string) {
|
|
|
204
209
|
workflowStatuses={workflowStatuses}
|
|
205
210
|
useAsTitle={collectionDef.useAsTitle}
|
|
206
211
|
orderable={collectionDef.orderable === true}
|
|
212
|
+
listActions={adminConfig?.listActions}
|
|
207
213
|
onReorder={async ({ documentId, beforeDocumentId, afterDocumentId }) => {
|
|
208
214
|
await reorderCollectionDocument({
|
|
209
215
|
data: {
|
|
@@ -9,10 +9,14 @@
|
|
|
9
9
|
import { createServerFn } from '@tanstack/react-start'
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
+
buildRelationSummaryPopulateMap,
|
|
12
13
|
ERR_NOT_FOUND,
|
|
14
|
+
getCollectionAdminConfig,
|
|
15
|
+
getCollectionDefinition,
|
|
13
16
|
getCollectionSchemasForPath,
|
|
14
17
|
getLogger,
|
|
15
18
|
getServerConfig,
|
|
19
|
+
type PopulateSpec,
|
|
16
20
|
type QueryPredicate,
|
|
17
21
|
} from '@byline/core'
|
|
18
22
|
|
|
@@ -74,6 +78,19 @@ export const getCollectionDocuments = createServerFn({ method: 'GET' })
|
|
|
74
78
|
? { [params.order]: params.desc === false ? 'asc' : 'desc' }
|
|
75
79
|
: defaultSort
|
|
76
80
|
|
|
81
|
+
// Auto-populate relation columns (depth 1) so the list renders each
|
|
82
|
+
// target's title (via `relationColumnFormatter`) rather than a raw
|
|
83
|
+
// document id. Projection follows each target's `itemView` columns +
|
|
84
|
+
// `useAsTitle` — the same map the edit view uses. Populate only fires for
|
|
85
|
+
// relation fields actually loaded (i.e. selected as columns), so building
|
|
86
|
+
// the full map is harmless for collections whose relations aren't shown.
|
|
87
|
+
const populateMap = buildRelationSummaryPopulateMap(config.definition.fields, (targetPath) => ({
|
|
88
|
+
def: getCollectionDefinition(targetPath),
|
|
89
|
+
admin: getCollectionAdminConfig(targetPath),
|
|
90
|
+
}))
|
|
91
|
+
const hasRelations = Object.keys(populateMap).length > 0
|
|
92
|
+
const populate: PopulateSpec | undefined = hasRelations ? populateMap : undefined
|
|
93
|
+
|
|
77
94
|
const result = await handle.find({
|
|
78
95
|
where: Object.keys(where).length > 0 ? where : undefined,
|
|
79
96
|
sort: sortSpec,
|
|
@@ -81,6 +98,8 @@ export const getCollectionDocuments = createServerFn({ method: 'GET' })
|
|
|
81
98
|
page: params.page,
|
|
82
99
|
pageSize,
|
|
83
100
|
select: params.fields,
|
|
101
|
+
populate,
|
|
102
|
+
depth: hasRelations ? 1 : undefined,
|
|
84
103
|
status: 'any',
|
|
85
104
|
// Admin list: show the raw per-locale state (untranslated docs render
|
|
86
105
|
// empty in the active locale's columns) rather than falling back to the
|
|
@@ -131,5 +150,15 @@ export const getCollectionDocuments = createServerFn({ method: 'GET' })
|
|
|
131
150
|
|
|
132
151
|
// Validate with schema for runtime type safety and field normalisation.
|
|
133
152
|
const { list } = getCollectionSchemasForPath(path)
|
|
153
|
+
|
|
154
|
+
// Skip the per-locale Zod parse when relations were populated — the tree
|
|
155
|
+
// then carries nested populated documents that don't match the raw
|
|
156
|
+
// relation-ref shape the list schema expects, and a strict parse would
|
|
157
|
+
// strip the `_resolved` / `document` envelope keys the relation formatter
|
|
158
|
+
// reads. Mirrors the edit-route (`get.ts`) populated-tree handling.
|
|
159
|
+
if (hasRelations) {
|
|
160
|
+
return serialised as unknown as ReturnType<typeof list.parse>
|
|
161
|
+
}
|
|
162
|
+
|
|
134
163
|
return list.parse(serialised)
|
|
135
164
|
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Admin server fn: rebuild a collection's search index. Thin boundary over
|
|
11
|
+
* `CollectionHandle.reindex()` — which clears the index slice and re-indexes
|
|
12
|
+
* every published document. Routes through `getAdminBylineClient()` so the
|
|
13
|
+
* request's admin actor is used; `reindex()` asserts the
|
|
14
|
+
* `collections.<path>.reindex` ability, so authorization is enforced here even
|
|
15
|
+
* though the button is also permission-gated in the UI.
|
|
16
|
+
*
|
|
17
|
+
* Synchronous today (fine for small/medium collections). A large corpus wants
|
|
18
|
+
* this backgrounded with progress — see docs/05-reading-and-delivery/07-search.md
|
|
19
|
+
* ("reindex cost").
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
23
|
+
|
|
24
|
+
import type { ReindexResult } from '@byline/client'
|
|
25
|
+
import { ERR_NOT_FOUND, getLogger } from '@byline/core'
|
|
26
|
+
|
|
27
|
+
import { ensureCollection } from '../../integrations/api-utils.js'
|
|
28
|
+
import { getAdminBylineClient } from '../../integrations/byline-client.js'
|
|
29
|
+
|
|
30
|
+
export type { ReindexResult }
|
|
31
|
+
|
|
32
|
+
export const reindexCollection = createServerFn({ method: 'POST' })
|
|
33
|
+
.validator((input: { collection: string }) => input)
|
|
34
|
+
.handler(async ({ data }): Promise<ReindexResult> => {
|
|
35
|
+
const { collection } = data
|
|
36
|
+
const config = await ensureCollection(collection)
|
|
37
|
+
if (!config) {
|
|
38
|
+
throw ERR_NOT_FOUND({
|
|
39
|
+
message: 'Collection not found',
|
|
40
|
+
details: { collectionPath: collection },
|
|
41
|
+
}).log(getLogger())
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// `reindex()` asserts `collections.<collection>.reindex` against the
|
|
45
|
+
// request's admin actor before doing any work.
|
|
46
|
+
return getAdminBylineClient().collection(collection).reindex()
|
|
47
|
+
})
|