@byline/host-tanstack-start 1.9.1 → 1.10.1

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.
@@ -2,7 +2,7 @@ import { jsx } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
3
  import { getDefaultStatus, getWorkflowStatuses } from "@byline/core";
4
4
  import { Container, FormRenderer, Section, useToastManager } from "@byline/ui/react";
5
- import { deleteDocument, unpublishDocument, updateCollectionDocumentWithPatches, updateDocumentStatus } from "../../server-fns/collections/index.js";
5
+ import { copyDocumentToLocale, deleteDocument, duplicateCollectionDocument, unpublishDocument, updateCollectionDocumentWithPatches, updateDocumentStatus } from "../../server-fns/collections/index.js";
6
6
  import { useNavigate } from "../chrome/loose-router.js";
7
7
  import { useTanStackNavigationGuard } from "./tanstack-navigation-guard.js";
8
8
  import { ViewMenu } from "./view-menu.js";
@@ -133,6 +133,108 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
133
133
  });
134
134
  }
135
135
  };
136
+ const handleDuplicate = async ()=>{
137
+ try {
138
+ const result = await duplicateCollectionDocument({
139
+ data: {
140
+ collection: path,
141
+ id: String(initialData.id)
142
+ }
143
+ });
144
+ toastManager.add({
145
+ title: `${labels.singular} Duplicated`,
146
+ description: result.pathRetried ? `Created with auto-generated path "${result.newPath}" (the preferred slug was already in use).` : `Created with path "${result.newPath}". Update the title and path in the new document.`,
147
+ data: {
148
+ intent: 'success',
149
+ iconType: 'success',
150
+ icon: true,
151
+ close: true
152
+ }
153
+ });
154
+ setEditState({
155
+ status: 'success',
156
+ message: `${labels.singular} duplicated.`
157
+ });
158
+ navigate({
159
+ to: '/admin/collections/$collection/$id',
160
+ params: {
161
+ collection: path,
162
+ id: result.documentId
163
+ }
164
+ });
165
+ } catch (err) {
166
+ console.error('Duplicate error:', err);
167
+ toastManager.add({
168
+ title: `${labels.singular} Duplicate`,
169
+ description: `Failed to duplicate: ${err.message}`,
170
+ data: {
171
+ intent: 'danger',
172
+ iconType: 'danger',
173
+ icon: true,
174
+ close: true
175
+ }
176
+ });
177
+ setEditState({
178
+ status: 'failed',
179
+ message: `Failed to duplicate: ${err.message}`
180
+ });
181
+ }
182
+ };
183
+ const handleCopyToLocale = async ({ targetLocale, overwrite })=>{
184
+ try {
185
+ const result = await copyDocumentToLocale({
186
+ data: {
187
+ collection: path,
188
+ id: String(initialData.id),
189
+ sourceLocale: locale ?? defaultContentLocale,
190
+ targetLocale,
191
+ overwrite
192
+ }
193
+ });
194
+ const sourceLabel = contentLocales.find((l)=>l.code === result.sourceLocale)?.label ?? result.sourceLocale;
195
+ const targetLabel = contentLocales.find((l)=>l.code === result.targetLocale)?.label ?? result.targetLocale;
196
+ toastManager.add({
197
+ title: `${labels.singular} Copy to Locale`,
198
+ description: result.fieldsUpdated > 0 ? `Copied ${result.fieldsUpdated} field${1 === result.fieldsUpdated ? '' : 's'} from ${sourceLabel} to ${targetLabel}.` : `No fields needed copying from ${sourceLabel} to ${targetLabel} under the current rule.`,
199
+ data: {
200
+ intent: 'success',
201
+ iconType: 'success',
202
+ icon: true,
203
+ close: true
204
+ }
205
+ });
206
+ setEditState({
207
+ status: 'success',
208
+ message: `Copied ${sourceLabel} → ${targetLabel}.`
209
+ });
210
+ navigate({
211
+ to: '/admin/collections/$collection/$id',
212
+ params: {
213
+ collection: path,
214
+ id: String(initialData.id)
215
+ },
216
+ search: {
217
+ locale: targetLocale
218
+ }
219
+ });
220
+ } catch (err) {
221
+ console.error('Copy to locale error:', err);
222
+ toastManager.add({
223
+ title: `${labels.singular} Copy to Locale`,
224
+ description: `Failed to copy: ${err.message}`,
225
+ data: {
226
+ intent: 'danger',
227
+ iconType: 'danger',
228
+ icon: true,
229
+ close: true
230
+ }
231
+ });
232
+ setEditState({
233
+ status: 'failed',
234
+ message: `Failed to copy: ${err.message}`
235
+ });
236
+ }
237
+ };
136
238
  const handleDelete = async ()=>{
137
239
  try {
138
240
  await deleteDocument({
@@ -262,6 +364,9 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
262
364
  onStatusChange: handleStatusChange,
263
365
  onUnpublish: publishedVersion ? handleUnpublish : void 0,
264
366
  onDelete: handleDelete,
367
+ onDuplicate: handleDuplicate,
368
+ onCopyToLocale: handleCopyToLocale,
369
+ contentLocales: contentLocales,
265
370
  publishedVersion: publishedVersion,
266
371
  restoreWarnings: restoreWarnings,
267
372
  nextStatus: nextStatus,
@@ -0,0 +1,30 @@
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 { CopyToLocaleResult } from '@byline/core/services';
9
+ /**
10
+ * Copy a document's content from `sourceLocale` into `targetLocale` on
11
+ * the same document. Mirrors the thin-wrapper pattern of the other
12
+ * collection server fns: resolve the request context, build the
13
+ * lifecycle context, delegate to the `copyToLocale` service.
14
+ *
15
+ * `assertActorCanPerform('update')` runs inside the service; auth
16
+ * failures propagate to TanStack Start's transport layer.
17
+ */
18
+ export declare const copyDocumentToLocale: import("@tanstack/react-start").RequiredFetcher<undefined, (input: {
19
+ collection: string;
20
+ id: string;
21
+ sourceLocale: string;
22
+ targetLocale: string;
23
+ overwrite: boolean;
24
+ }) => {
25
+ collection: string;
26
+ id: string;
27
+ sourceLocale: string;
28
+ targetLocale: string;
29
+ overwrite: boolean;
30
+ }, Promise<CopyToLocaleResult>>;
@@ -0,0 +1,37 @@
1
+ import { createServerFn } from "@tanstack/react-start";
2
+ import { ERR_NOT_FOUND, getLogger, getServerConfig } from "@byline/core";
3
+ import { copyToLocale } from "@byline/core/services";
4
+ import { getAdminRequestContext } from "../../auth/auth-context.js";
5
+ import { ensureCollection } from "../../integrations/api-utils.js";
6
+ const copyDocumentToLocale = createServerFn({
7
+ method: 'POST'
8
+ }).inputValidator((input)=>input).handler(async ({ data: input })=>{
9
+ const { collection: path, id, sourceLocale, targetLocale, overwrite } = input;
10
+ const logger = getLogger();
11
+ const config = await ensureCollection(path);
12
+ if (!config) throw ERR_NOT_FOUND({
13
+ message: 'Collection not found',
14
+ details: {
15
+ collectionPath: path
16
+ }
17
+ }).log(logger);
18
+ const serverConfig = getServerConfig();
19
+ const ctx = {
20
+ db: serverConfig.db,
21
+ definition: config.definition,
22
+ collectionId: config.collection.id,
23
+ collectionVersion: config.collection.version,
24
+ collectionPath: path,
25
+ logger,
26
+ defaultLocale: serverConfig.i18n.content.defaultLocale,
27
+ slugifier: serverConfig.slugifier,
28
+ requestContext: await getAdminRequestContext()
29
+ };
30
+ return copyToLocale(ctx, {
31
+ documentId: id,
32
+ sourceLocale,
33
+ targetLocale,
34
+ overwrite
35
+ });
36
+ });
37
+ export { copyDocumentToLocale };
@@ -0,0 +1,26 @@
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 { DuplicateDocumentResult } from '@byline/core/services';
9
+ /**
10
+ * Duplicate an existing document — clones all locales into a brand-new
11
+ * document in one atomic write. Returns the new document's id so the UI
12
+ * can navigate straight to its edit view.
13
+ *
14
+ * Mirrors the thin-wrapper pattern of the other collection server fns:
15
+ * resolve the request context from session cookies, build the lifecycle
16
+ * context, delegate to the `duplicateDocument` service. The service runs
17
+ * `assertActorCanPerform(..., 'create')`; any auth failure propagates to
18
+ * TanStack Start's transport layer for the client to branch on.
19
+ */
20
+ export declare const duplicateCollectionDocument: import("@tanstack/react-start").RequiredFetcher<undefined, (input: {
21
+ collection: string;
22
+ id: string;
23
+ }) => {
24
+ collection: string;
25
+ id: string;
26
+ }, Promise<DuplicateDocumentResult>>;
@@ -0,0 +1,34 @@
1
+ import { createServerFn } from "@tanstack/react-start";
2
+ import { ERR_NOT_FOUND, getLogger, getServerConfig } from "@byline/core";
3
+ import { duplicateDocument } from "@byline/core/services";
4
+ import { getAdminRequestContext } from "../../auth/auth-context.js";
5
+ import { ensureCollection } from "../../integrations/api-utils.js";
6
+ const duplicateCollectionDocument = createServerFn({
7
+ method: 'POST'
8
+ }).inputValidator((input)=>input).handler(async ({ data: input })=>{
9
+ const { collection: path, id: sourceDocumentId } = input;
10
+ const logger = getLogger();
11
+ const config = await ensureCollection(path);
12
+ if (!config) throw ERR_NOT_FOUND({
13
+ message: 'Collection not found',
14
+ details: {
15
+ collectionPath: path
16
+ }
17
+ }).log(logger);
18
+ const serverConfig = getServerConfig();
19
+ const ctx = {
20
+ db: serverConfig.db,
21
+ definition: config.definition,
22
+ collectionId: config.collection.id,
23
+ collectionVersion: config.collection.version,
24
+ collectionPath: path,
25
+ logger,
26
+ defaultLocale: serverConfig.i18n.content.defaultLocale,
27
+ slugifier: serverConfig.slugifier,
28
+ requestContext: await getAdminRequestContext()
29
+ };
30
+ return duplicateDocument(ctx, {
31
+ sourceDocumentId
32
+ });
33
+ });
34
+ export { duplicateCollectionDocument };
@@ -4,8 +4,10 @@
4
4
  * Each module is self-contained: it defines the TanStack Start server
5
5
  * function and exports a clean public API.
6
6
  */
7
+ export * from './copy-to-locale';
7
8
  export * from './create';
8
9
  export * from './delete';
10
+ export * from './duplicate';
9
11
  export * from './get';
10
12
  export * from './history';
11
13
  export * from './list';
@@ -1,5 +1,7 @@
1
+ export * from "./copy-to-locale.js";
1
2
  export * from "./create.js";
2
3
  export * from "./delete.js";
4
+ export * from "./duplicate.js";
3
5
  export * from "./get.js";
4
6
  export * from "./history.js";
5
7
  export * from "./list.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": "1.9.1",
6
+ "version": "1.10.1",
7
7
  "engines": {
8
8
  "node": ">=20.9.0"
9
9
  },
@@ -105,12 +105,12 @@
105
105
  "react-swipeable": "^7.0.2",
106
106
  "uuid": "^14.0.0",
107
107
  "zod": "^4.4.2",
108
- "@byline/auth": "1.9.1",
109
- "@byline/admin": "1.9.1",
110
- "@byline/client": "1.9.1",
111
- "@byline/core": "1.9.1",
112
- "@byline/ai": "1.9.1",
113
- "@byline/ui": "1.9.1"
108
+ "@byline/admin": "1.10.1",
109
+ "@byline/core": "1.10.1",
110
+ "@byline/auth": "1.10.1",
111
+ "@byline/ui": "1.10.1",
112
+ "@byline/client": "1.10.1",
113
+ "@byline/ai": "1.10.1"
114
114
  },
115
115
  "peerDependencies": {
116
116
  "@tanstack/react-router": "^1.167.0",
@@ -14,7 +14,9 @@ import type { AnyCollectionSchemaTypes } from '@byline/core/zod-schemas'
14
14
  import { Container, FormRenderer, Section, useToastManager } from '@byline/ui/react'
15
15
 
16
16
  import {
17
+ copyDocumentToLocale,
17
18
  deleteDocument,
19
+ duplicateCollectionDocument,
18
20
  unpublishDocument,
19
21
  updateCollectionDocumentWithPatches,
20
22
  updateDocumentStatus,
@@ -167,6 +169,115 @@ export const EditView = ({
167
169
  }
168
170
  }
169
171
 
172
+ const handleDuplicate = async () => {
173
+ try {
174
+ const result = await duplicateCollectionDocument({
175
+ data: { collection: path, id: String(initialData.id) },
176
+ })
177
+ toastManager.add({
178
+ title: `${labels.singular} Duplicated`,
179
+ description: result.pathRetried
180
+ ? `Created with auto-generated path "${result.newPath}" (the preferred slug was already in use).`
181
+ : `Created with path "${result.newPath}". Update the title and path in the new document.`,
182
+ data: {
183
+ intent: 'success',
184
+ iconType: 'success',
185
+ icon: true,
186
+ close: true,
187
+ },
188
+ })
189
+ setEditState({
190
+ status: 'success',
191
+ message: `${labels.singular} duplicated.`,
192
+ })
193
+ // Navigate to the new document's edit view.
194
+ navigate({
195
+ to: '/admin/collections/$collection/$id' as never,
196
+ params: { collection: path, id: result.documentId },
197
+ })
198
+ } catch (err) {
199
+ console.error('Duplicate error:', err)
200
+ toastManager.add({
201
+ title: `${labels.singular} Duplicate`,
202
+ description: `Failed to duplicate: ${(err as Error).message}`,
203
+ data: {
204
+ intent: 'danger',
205
+ iconType: 'danger',
206
+ icon: true,
207
+ close: true,
208
+ },
209
+ })
210
+ setEditState({
211
+ status: 'failed',
212
+ message: `Failed to duplicate: ${(err as Error).message}`,
213
+ })
214
+ }
215
+ }
216
+
217
+ const handleCopyToLocale = async ({
218
+ targetLocale,
219
+ overwrite,
220
+ }: {
221
+ targetLocale: string
222
+ overwrite: boolean
223
+ }) => {
224
+ try {
225
+ const result = await copyDocumentToLocale({
226
+ data: {
227
+ collection: path,
228
+ id: String(initialData.id),
229
+ sourceLocale: locale ?? defaultContentLocale,
230
+ targetLocale,
231
+ overwrite,
232
+ },
233
+ })
234
+ const sourceLabel =
235
+ contentLocales.find((l) => l.code === result.sourceLocale)?.label ?? result.sourceLocale
236
+ const targetLabel =
237
+ contentLocales.find((l) => l.code === result.targetLocale)?.label ?? result.targetLocale
238
+ toastManager.add({
239
+ title: `${labels.singular} Copy to Locale`,
240
+ description:
241
+ result.fieldsUpdated > 0
242
+ ? `Copied ${result.fieldsUpdated} field${result.fieldsUpdated === 1 ? '' : 's'} from ${sourceLabel} to ${targetLabel}.`
243
+ : `No fields needed copying from ${sourceLabel} to ${targetLabel} under the current rule.`,
244
+ data: {
245
+ intent: 'success',
246
+ iconType: 'success',
247
+ icon: true,
248
+ close: true,
249
+ },
250
+ })
251
+ setEditState({
252
+ status: 'success',
253
+ message: `Copied ${sourceLabel} → ${targetLabel}.`,
254
+ })
255
+ // Switch the form to the target locale so the editor sees the
256
+ // copied content immediately.
257
+ navigate({
258
+ to: '/admin/collections/$collection/$id' as never,
259
+ params: { collection: path, id: String(initialData.id) },
260
+ search: { locale: targetLocale },
261
+ })
262
+ } catch (err) {
263
+ console.error('Copy to locale error:', err)
264
+ toastManager.add({
265
+ title: `${labels.singular} Copy to Locale`,
266
+ description: `Failed to copy: ${(err as Error).message}`,
267
+ data: {
268
+ intent: 'danger',
269
+ iconType: 'danger',
270
+ icon: true,
271
+ close: true,
272
+ },
273
+ })
274
+ setEditState({
275
+ status: 'failed',
276
+ message: `Failed to copy: ${(err as Error).message}`,
277
+ })
278
+ }
279
+ }
280
+
170
281
  const handleDelete = async () => {
171
282
  try {
172
283
  await deleteDocument({ data: { collection: path, id: String(initialData.id) } })
@@ -308,6 +419,9 @@ export const EditView = ({
308
419
  onStatusChange={handleStatusChange}
309
420
  onUnpublish={publishedVersion ? handleUnpublish : undefined}
310
421
  onDelete={handleDelete}
422
+ onDuplicate={handleDuplicate}
423
+ onCopyToLocale={handleCopyToLocale}
424
+ contentLocales={contentLocales}
311
425
  publishedVersion={publishedVersion}
312
426
  restoreWarnings={restoreWarnings}
313
427
  nextStatus={nextStatus}
@@ -0,0 +1,71 @@
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
+ import { createServerFn } from '@tanstack/react-start'
10
+
11
+ import { ERR_NOT_FOUND, getLogger, getServerConfig } from '@byline/core'
12
+ import type { CopyToLocaleResult, DocumentLifecycleContext } from '@byline/core/services'
13
+ import { copyToLocale } from '@byline/core/services'
14
+
15
+ import { getAdminRequestContext } from '../../auth/auth-context.js'
16
+ import { ensureCollection } from '../../integrations/api-utils.js'
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Copy document content from one locale into another
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * Copy a document's content from `sourceLocale` into `targetLocale` on
24
+ * the same document. Mirrors the thin-wrapper pattern of the other
25
+ * collection server fns: resolve the request context, build the
26
+ * lifecycle context, delegate to the `copyToLocale` service.
27
+ *
28
+ * `assertActorCanPerform('update')` runs inside the service; auth
29
+ * failures propagate to TanStack Start's transport layer.
30
+ */
31
+ export const copyDocumentToLocale = createServerFn({ method: 'POST' })
32
+ .inputValidator(
33
+ (input: {
34
+ collection: string
35
+ id: string
36
+ sourceLocale: string
37
+ targetLocale: string
38
+ overwrite: boolean
39
+ }) => input
40
+ )
41
+ .handler(async ({ data: input }): Promise<CopyToLocaleResult> => {
42
+ const { collection: path, id, sourceLocale, targetLocale, overwrite } = input
43
+ const logger = getLogger()
44
+ const config = await ensureCollection(path)
45
+ if (!config) {
46
+ throw ERR_NOT_FOUND({
47
+ message: 'Collection not found',
48
+ details: { collectionPath: path },
49
+ }).log(logger)
50
+ }
51
+
52
+ const serverConfig = getServerConfig()
53
+ const ctx: DocumentLifecycleContext = {
54
+ db: serverConfig.db,
55
+ definition: config.definition,
56
+ collectionId: config.collection.id,
57
+ collectionVersion: config.collection.version,
58
+ collectionPath: path,
59
+ logger,
60
+ defaultLocale: serverConfig.i18n.content.defaultLocale,
61
+ slugifier: serverConfig.slugifier,
62
+ requestContext: await getAdminRequestContext(),
63
+ }
64
+
65
+ return copyToLocale(ctx, {
66
+ documentId: id,
67
+ sourceLocale,
68
+ targetLocale,
69
+ overwrite,
70
+ })
71
+ })
@@ -0,0 +1,60 @@
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
+ import { createServerFn } from '@tanstack/react-start'
10
+
11
+ import { ERR_NOT_FOUND, getLogger, getServerConfig } from '@byline/core'
12
+ import type { DocumentLifecycleContext, DuplicateDocumentResult } from '@byline/core/services'
13
+ import { duplicateDocument } from '@byline/core/services'
14
+
15
+ import { getAdminRequestContext } from '../../auth/auth-context.js'
16
+ import { ensureCollection } from '../../integrations/api-utils.js'
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Duplicate document
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * Duplicate an existing document — clones all locales into a brand-new
24
+ * document in one atomic write. Returns the new document's id so the UI
25
+ * can navigate straight to its edit view.
26
+ *
27
+ * Mirrors the thin-wrapper pattern of the other collection server fns:
28
+ * resolve the request context from session cookies, build the lifecycle
29
+ * context, delegate to the `duplicateDocument` service. The service runs
30
+ * `assertActorCanPerform(..., 'create')`; any auth failure propagates to
31
+ * TanStack Start's transport layer for the client to branch on.
32
+ */
33
+ export const duplicateCollectionDocument = createServerFn({ method: 'POST' })
34
+ .inputValidator((input: { collection: string; id: string }) => input)
35
+ .handler(async ({ data: input }): Promise<DuplicateDocumentResult> => {
36
+ const { collection: path, id: sourceDocumentId } = input
37
+ const logger = getLogger()
38
+ const config = await ensureCollection(path)
39
+ if (!config) {
40
+ throw ERR_NOT_FOUND({
41
+ message: 'Collection not found',
42
+ details: { collectionPath: path },
43
+ }).log(logger)
44
+ }
45
+
46
+ const serverConfig = getServerConfig()
47
+ const ctx: DocumentLifecycleContext = {
48
+ db: serverConfig.db,
49
+ definition: config.definition,
50
+ collectionId: config.collection.id,
51
+ collectionVersion: config.collection.version,
52
+ collectionPath: path,
53
+ logger,
54
+ defaultLocale: serverConfig.i18n.content.defaultLocale,
55
+ slugifier: serverConfig.slugifier,
56
+ requestContext: await getAdminRequestContext(),
57
+ }
58
+
59
+ return duplicateDocument(ctx, { sourceDocumentId })
60
+ })
@@ -5,8 +5,10 @@
5
5
  * function and exports a clean public API.
6
6
  */
7
7
 
8
+ export * from './copy-to-locale'
8
9
  export * from './create'
9
10
  export * from './delete'
11
+ export * from './duplicate'
10
12
  export * from './get'
11
13
  export * from './history'
12
14
  export * from './list'