@byline/host-tanstack-start 1.9.0 → 1.10.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/edit.js +106 -1
- package/dist/server-fns/collections/copy-to-locale.d.ts +30 -0
- package/dist/server-fns/collections/copy-to-locale.js +37 -0
- package/dist/server-fns/collections/duplicate.d.ts +26 -0
- package/dist/server-fns/collections/duplicate.js +34 -0
- package/dist/server-fns/collections/index.d.ts +2 -0
- package/dist/server-fns/collections/index.js +2 -0
- package/package.json +7 -7
- package/src/admin-shell/collections/edit.tsx +114 -0
- package/src/server-fns/collections/copy-to-locale.ts +71 -0
- package/src/server-fns/collections/duplicate.ts +60 -0
- package/src/server-fns/collections/index.ts +2 -0
|
@@ -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';
|
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.
|
|
6
|
+
"version": "1.10.0",
|
|
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/admin": "1.
|
|
109
|
-
"@byline/
|
|
110
|
-
"@byline/
|
|
111
|
-
"@byline/
|
|
112
|
-
"@byline/
|
|
113
|
-
"@byline/ui": "1.
|
|
108
|
+
"@byline/admin": "1.10.0",
|
|
109
|
+
"@byline/auth": "1.10.0",
|
|
110
|
+
"@byline/core": "1.10.0",
|
|
111
|
+
"@byline/client": "1.10.0",
|
|
112
|
+
"@byline/ai": "1.10.0",
|
|
113
|
+
"@byline/ui": "1.10.0"
|
|
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
|
+
})
|