@qkix/strapi-plugin-ferry 0.1.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/LICENSE +21 -0
- package/README.md +262 -0
- package/dist/_chunks/TransferPage-Bhp6vkZE.js +1 -0
- package/dist/_chunks/TransferPage-c8hdFQDQ.mjs +398 -0
- package/dist/admin/index.js +1 -0
- package/dist/admin/index.mjs +43 -0
- package/dist/admin/src/api.d.ts +114 -0
- package/dist/admin/src/components/ExportPanel.d.ts +7 -0
- package/dist/admin/src/components/ImportPanel.d.ts +15 -0
- package/dist/admin/src/components/ReportView.d.ts +6 -0
- package/dist/admin/src/index.d.ts +11 -0
- package/dist/admin/src/pages/TransferPage.d.ts +10 -0
- package/dist/admin/src/pluginId.d.ts +7 -0
- package/dist/server/index.js +6 -0
- package/dist/server/index.mjs +996 -0
- package/dist/server/src/config/index.d.ts +30 -0
- package/dist/server/src/controllers/index.d.ts +12 -0
- package/dist/server/src/controllers/transfer.d.ts +14 -0
- package/dist/server/src/index.d.ts +97 -0
- package/dist/server/src/policies/index.d.ts +5 -0
- package/dist/server/src/register.d.ts +12 -0
- package/dist/server/src/routes/admin.d.ts +12 -0
- package/dist/server/src/routes/index.d.ts +14 -0
- package/dist/server/src/services/csv.d.ts +59 -0
- package/dist/server/src/services/export.d.ts +24 -0
- package/dist/server/src/services/import.d.ts +78 -0
- package/dist/server/src/services/index.d.ts +54 -0
- package/dist/server/src/services/nested.d.ts +38 -0
- package/dist/server/src/services/parse.d.ts +25 -0
- package/dist/server/src/services/populate.d.ts +20 -0
- package/dist/server/src/services/schema.d.ts +81 -0
- package/dist/server/src/services/serialise.d.ts +14 -0
- package/dist/server/src/types.d.ts +145 -0
- package/dist/server/src/uids.d.ts +9 -0
- package/package.json +118 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 qkix
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/qkix/strapi-plugins/main/packages/strapi-plugin-ferry/docs/logo.png" alt="Ferry" width="120" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">Ferry for Strapi</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">Import and export content as JSON or CSV - <strong>with relations, components and dynamic zones that actually survive the trip</strong>.</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://www.npmjs.com/package/@qkix/strapi-plugin-ferry">
|
|
11
|
+
<img alt="npm version" src="https://img.shields.io/npm/v/@qkix/strapi-plugin-ferry.svg" />
|
|
12
|
+
</a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/@qkix/strapi-plugin-ferry">
|
|
14
|
+
<img alt="npm downloads" src="https://img.shields.io/npm/dm/@qkix/strapi-plugin-ferry.svg" />
|
|
15
|
+
</a>
|
|
16
|
+
<a href="https://github.com/qkix/strapi-plugins/blob/main/LICENSE">
|
|
17
|
+
<img alt="license" src="https://img.shields.io/npm/l/@qkix/strapi-plugin-ferry.svg" />
|
|
18
|
+
</a>
|
|
19
|
+
<a href="https://buymeacoffee.com/qkix">
|
|
20
|
+
<img alt="Buy Me a Coffee" src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-support-FFDD00?logo=buymeacoffee&logoColor=black" />
|
|
21
|
+
</a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="https://raw.githubusercontent.com/qkix/strapi-plugins/main/packages/strapi-plugin-ferry/docs/dry-run.png" alt="The Ferry panel: an Export card on the left and an Import card on the right, with a dry run reporting 5 documents in the file, 3 to create and 2 to update, and nothing written yet" width="900" />
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
## Why another one of these
|
|
29
|
+
|
|
30
|
+
Import and export is the most-wanted thing in the Strapi ecosystem that nobody
|
|
31
|
+
maintains. The original plugin was last published in December 2023 and requires
|
|
32
|
+
Strapi 4, so it does not run at all on Strapi 5. The Strapi 5 fork of it was
|
|
33
|
+
written over two days in October 2024, released as `0.0.1`, and never touched
|
|
34
|
+
again. Between them they still take around 780 downloads a day, and the fork has
|
|
35
|
+
24 forks of its own, which is what "I had to patch it myself" looks like from the
|
|
36
|
+
outside.
|
|
37
|
+
|
|
38
|
+
Its open issues say what is broken, and the two that matter are not details:
|
|
39
|
+
|
|
40
|
+
- **relations do not import**, because the code carries Strapi 4's numeric `id`
|
|
41
|
+
and a numeric id means a different row in every environment
|
|
42
|
+
- **components do not export**, because `populate: '*'` stops one level down and
|
|
43
|
+
a component inside a component comes back empty
|
|
44
|
+
|
|
45
|
+
Ferry is built around those two, and around the thing neither plugin has: it
|
|
46
|
+
tells you what it is going to do before it does it.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
npm install @qkix/strapi-plugin-ferry
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
// config/plugins.ts
|
|
56
|
+
export default {
|
|
57
|
+
ferry: {
|
|
58
|
+
enabled: true,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Then open **Ferry** in the main menu.
|
|
64
|
+
|
|
65
|
+
## The dry run
|
|
66
|
+
|
|
67
|
+
Nothing is written until you have read a report. Upload a file, and Ferry
|
|
68
|
+
resolves the whole thing against the database first:
|
|
69
|
+
|
|
70
|
+
- how many documents it will **create**, **update** and **skip**
|
|
71
|
+
- every relation that points at a document which is **neither in the file nor in
|
|
72
|
+
this environment**, named by row and by field, including
|
|
73
|
+
`seo.owner`-style paths inside a component
|
|
74
|
+
- which columns of a CSV it did not recognise, which media it could not find,
|
|
75
|
+
which fields the format cannot carry
|
|
76
|
+
|
|
77
|
+
Then you press the button. It is the same request either way with one flag
|
|
78
|
+
changed, so the preview cannot drift from what the import does.
|
|
79
|
+
|
|
80
|
+
## Relations that arrive
|
|
81
|
+
|
|
82
|
+
Ferry keys everything on **`documentId`**, which is the one identifier that means
|
|
83
|
+
the same thing in your laptop's database and in production. That is what makes an
|
|
84
|
+
import an upsert rather than a paste: run the same file twice and the second run
|
|
85
|
+
updates in place instead of duplicating the content.
|
|
86
|
+
|
|
87
|
+
The import runs in **two passes** - every document is written first, and the
|
|
88
|
+
links between them second. There is no ordering of a single pass that works,
|
|
89
|
+
because two documents that refer to each other cannot both be written second. A
|
|
90
|
+
missing target is reported rather than discovered: Strapi refuses an entire row
|
|
91
|
+
when one relation points at nothing, so an importer that just tries it loses
|
|
92
|
+
content it never mentioned.
|
|
93
|
+
|
|
94
|
+
An import is **all or nothing** by default. If a row fails, the whole thing rolls
|
|
95
|
+
back and the database is exactly as it was, because a half-applied import is
|
|
96
|
+
worse than a refused one - nobody knows which half. You can opt into committing
|
|
97
|
+
the rest.
|
|
98
|
+
|
|
99
|
+
## Components, dynamic zones and media
|
|
100
|
+
|
|
101
|
+
Components are exported and imported whole, including **a component inside a
|
|
102
|
+
component**, and including **a relation inside a component**. Component ids are
|
|
103
|
+
dropped, because a component row belongs to one document and its id means nothing
|
|
104
|
+
in another database. A dynamic zone keeps its order, because in a dynamic zone
|
|
105
|
+
the order is the content.
|
|
106
|
+
|
|
107
|
+
Media travels as a **reference, not as bytes**. Ferry matches by the file's
|
|
108
|
+
`hash`, which is derived from its contents, so the same image already uploaded to
|
|
109
|
+
the target environment is reconnected even though its id and filename differ.
|
|
110
|
+
When it is not there, the field is left empty and the report says how many.
|
|
111
|
+
|
|
112
|
+
## A file you can commit
|
|
113
|
+
|
|
114
|
+
An export has **no timestamp in it**, and documents come out sorted by
|
|
115
|
+
`documentId` with a to-many relation sorted too. Two exports of the same content
|
|
116
|
+
are byte-for-byte identical, so the file can live in a repository and a diff
|
|
117
|
+
means something changed. Git already knows when the file arrived.
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"ferry": 1,
|
|
122
|
+
"contentType": "api::article.article",
|
|
123
|
+
"status": "draft",
|
|
124
|
+
"count": 1,
|
|
125
|
+
"documents": [
|
|
126
|
+
{
|
|
127
|
+
"documentId": "k2n4xk1t9m0q8v7w",
|
|
128
|
+
"title": "The first one",
|
|
129
|
+
"author": "a91xbq2r7t4m0p3z",
|
|
130
|
+
"tags": ["c7h2k9m1p4r8t0w3", "d1f5j8n2q6s9v3y7"],
|
|
131
|
+
"seo": {
|
|
132
|
+
"metaTitle": "Meta title",
|
|
133
|
+
"canonical": { "href": "https://example.com/one", "label": "Canonical" }
|
|
134
|
+
},
|
|
135
|
+
"blocks": [
|
|
136
|
+
{ "__component": "shared.quote", "body": "A quote" },
|
|
137
|
+
{ "__component": "shared.seo", "metaTitle": "Inside a zone" }
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## CSV
|
|
145
|
+
|
|
146
|
+
CSV is a real implementation of RFC 4180, not a split on commas: a comma inside a
|
|
147
|
+
title, a quote inside a body, a line break inside a paragraph all survive the
|
|
148
|
+
round trip. It reads a **semicolon** file, which is what a spreadsheet saves in
|
|
149
|
+
every locale that uses a comma for decimals, and writes a byte order mark so
|
|
150
|
+
Excel opens UTF-8 as UTF-8.
|
|
151
|
+
|
|
152
|
+
CSV carries flat fields and relation keys. Components and dynamic zones are left
|
|
153
|
+
out, and the export says which ones by name rather than writing a JSON blob into
|
|
154
|
+
a spreadsheet cell that no one can edit. Use JSON when you want everything.
|
|
155
|
+
|
|
156
|
+
One rule worth knowing: **a blank cell means "leave this alone"**, not "empty
|
|
157
|
+
this field". CSV cannot tell the two apart, and the safe reading is the one that
|
|
158
|
+
does not let a spreadsheet quietly erase content that simply was not in it.
|
|
159
|
+
|
|
160
|
+
## Permissions
|
|
161
|
+
|
|
162
|
+
Two, under **Settings → Administration panel → Roles**:
|
|
163
|
+
|
|
164
|
+
| Permission | What it allows |
|
|
165
|
+
| -------------- | ------------------------------------------------- |
|
|
166
|
+
| `ferry.export` | reading content out, and seeing the list of types |
|
|
167
|
+
| `ferry.import` | the dry run and the import itself |
|
|
168
|
+
|
|
169
|
+
They are separate because they are different risks. An export is a copy; an
|
|
170
|
+
import replaces what is there. A role that may take a copy away has no automatic
|
|
171
|
+
claim on replacing production.
|
|
172
|
+
|
|
173
|
+
## From a script
|
|
174
|
+
|
|
175
|
+
Both directions are plain admin endpoints, so anything with an admin session can
|
|
176
|
+
drive them - a CI job, a seeding script, a migration between environments:
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
# Take a copy
|
|
180
|
+
curl -X POST http://localhost:1337/ferry/export \
|
|
181
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
182
|
+
-H "Content-Type: application/json" \
|
|
183
|
+
-d '{"uid":"api::article.article","format":"json"}' \
|
|
184
|
+
-o articles.json
|
|
185
|
+
|
|
186
|
+
# See what importing it would do, without doing it
|
|
187
|
+
curl -X POST http://localhost:1337/ferry/import/preview \
|
|
188
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
189
|
+
-F uid=api::article.article \
|
|
190
|
+
-F file=@articles.json
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The import endpoints take a multipart upload, which keeps a large file clear of
|
|
194
|
+
the JSON body limit, or a `content` field with the file inline for scripting
|
|
195
|
+
convenience.
|
|
196
|
+
|
|
197
|
+
`POST /ferry/export` returns the file itself, which is what you want piped into
|
|
198
|
+
`-o`. Pass `"envelope": true` to get it wrapped in JSON instead, as
|
|
199
|
+
`{ filename, mime, body, count, warnings }` - useful when you want the warnings
|
|
200
|
+
alongside the content rather than in the `X-Ferry-Warnings` header, and what the
|
|
201
|
+
admin panel itself uses.
|
|
202
|
+
|
|
203
|
+
| Route | Method | Does |
|
|
204
|
+
| ----------------------- | ------ | ------------------------------------- |
|
|
205
|
+
| `/ferry/content-types` | GET | what can be carried |
|
|
206
|
+
| `/ferry/plan/:uid` | GET | which fields travel, and which do not |
|
|
207
|
+
| `/ferry/export` | POST | the file |
|
|
208
|
+
| `/ferry/import/preview` | POST | the dry run |
|
|
209
|
+
| `/ferry/import` | POST | the import |
|
|
210
|
+
|
|
211
|
+
## Configuration
|
|
212
|
+
|
|
213
|
+
| Option | Default | What it does |
|
|
214
|
+
| ----------- | ------- | ------------------------------------------------------- |
|
|
215
|
+
| `exclude` | `[]` | Content types Ferry will not carry, in either direction |
|
|
216
|
+
| `maxExport` | `10000` | The most documents one export will read |
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
export default {
|
|
220
|
+
ferry: {
|
|
221
|
+
enabled: true,
|
|
222
|
+
config: {
|
|
223
|
+
exclude: ['api::invoice.invoice'],
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
An excluded type is refused by the endpoints as well as hidden in the picker: a
|
|
230
|
+
hidden thing that can still be fetched by typing its uid is not hidden.
|
|
231
|
+
|
|
232
|
+
A `password` field is never exported, whatever the configuration says. A hash is
|
|
233
|
+
still a credential, and files get mailed around and committed.
|
|
234
|
+
|
|
235
|
+
## What it does not do
|
|
236
|
+
|
|
237
|
+
Said plainly, because finding out later is worse:
|
|
238
|
+
|
|
239
|
+
- **It does not carry files.** Media travels as a reference and is reconnected by
|
|
240
|
+
hash when the upload already exists in the target.
|
|
241
|
+
- **One content type per file.** Export the types a relation points at first, and
|
|
242
|
+
import them in that order. Links within a single file work in any order.
|
|
243
|
+
- **One locale per file.** Pass `locale` to move a specific one.
|
|
244
|
+
- **No XLSX.** CSV opens in the same places.
|
|
245
|
+
|
|
246
|
+
## Tested against a real Strapi
|
|
247
|
+
|
|
248
|
+
The test suite boots an actual Strapi against a throwaway database rather than
|
|
249
|
+
mocking one, and the headline test is the thing people actually do: export a
|
|
250
|
+
whole environment, delete its content, import the files back, and assert that a
|
|
251
|
+
second export is **byte-for-byte the first**.
|
|
252
|
+
|
|
253
|
+
Nine of the tests are about Strapi rather than about Ferry. They pin down the
|
|
254
|
+
behaviour the design rests on, so that a Strapi upgrade that changes it fails a
|
|
255
|
+
test here instead of quietly corrupting an import - that `documentId` can be
|
|
256
|
+
assigned on create, that `create` is not an upsert, that a missing relation
|
|
257
|
+
target takes the whole row down, and that a draft is allowed to be incomplete so
|
|
258
|
+
`required` is no guide to what will import.
|
|
259
|
+
|
|
260
|
+
## Licence
|
|
261
|
+
|
|
262
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("react/jsx-runtime"),B=require("react"),y=require("@strapi/admin/strapi-admin"),n=require("@strapi/design-system"),O=require("@strapi/icons"),F=require("../admin/index.js");function L(t){if(t&&t.__esModule)return t;const s=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t){for(const i in t)if(i!=="default"){const o=Object.getOwnPropertyDescriptor(t,i);Object.defineProperty(s,i,o.get?o:{enumerable:!0,get:()=>t[i]})}}return s.default=t,Object.freeze(s)}const d=L(B),k={contentTypes:`/${F.PLUGIN_ID}/content-types`,plan:t=>`/${F.PLUGIN_ID}/plan/${t}`,export:`/${F.PLUGIN_ID}/export`,preview:`/${F.PLUGIN_ID}/import/preview`,apply:`/${F.PLUGIN_ID}/import`},E={format:"json",status:"draft",relations:!0,media:!0},P={status:"draft",onExisting:"update",onMissingRelation:"skip",publish:!0,continueOnError:!1},$=(t,s,i)=>{const o=URL.createObjectURL(new Blob([t],{type:i})),c=document.createElement("a");c.href=o,c.download=s,document.body.appendChild(c),c.click(),c.remove(),URL.revokeObjectURL(o)},D=t=>[t.created>0?`${t.created} to create`:null,t.updated>0?`${t.updated} to update`:null,t.skipped>0?`${t.skipped} skipped`:null,t.errored>0?`${t.errored} failed`:null].filter(Boolean).join(", ")||"nothing to do",W=({contentTypes:t})=>{const{get:s,post:i}=y.useFetchClient(),[o,c]=d.useState(t[0]?.uid??""),[h,p]=d.useState(E),[r,u]=d.useState(null),[j,f]=d.useState(!1),[R,S]=d.useState([]),[v,b]=d.useState(null),[m,C]=d.useState(null);d.useEffect(()=>{if(!o)return;let l=!0;return s(k.plan(o)).then(({data:w})=>l&&u(w)).catch(()=>l&&u(null)),()=>{l=!1}},[s,o]);const a=l=>p(w=>({...w,...l})),x=async()=>{f(!0),b(null),S([]),C(null);try{const{data:l}=await i(k.export,{uid:o,...h,envelope:!0});$(l.body,l.filename,l.mime),S(l.warnings??[]),C(`${l.filename} (${l.count} document${l.count===1?"":"s"})`)}catch(l){b(l?.response?.data?.error?.message??"The export failed.")}finally{f(!1)}},g=r?[`${r.scalars.length} field(s)`,h.relations&&r.relations.length>0?`${r.relations.length} relation(s)`:null,r.components.length>0?`${r.components.length} component(s)`:null,r.dynamicZones.length>0?`${r.dynamicZones.length} dynamic zone(s)`:null,h.media&&r.media.length>0?`${r.media.length} media field(s)`:null].filter(Boolean):[];return e.jsxs(n.Flex,{direction:"column",gap:4,alignItems:"stretch",children:[e.jsx(n.Typography,{variant:"delta",children:"Export"}),e.jsxs(n.Field.Root,{name:"export-uid",children:[e.jsx(n.Field.Label,{children:"Content type"}),e.jsx(n.SingleSelect,{value:o,onChange:l=>c(String(l)),children:t.map(l=>e.jsx(n.SingleSelectOption,{value:l.uid,children:l.label},l.uid))})]}),e.jsxs(n.Flex,{gap:4,wrap:"wrap",alignItems:"flex-end",children:[e.jsx(n.Box,{minWidth:"160px",children:e.jsxs(n.Field.Root,{name:"export-format",children:[e.jsx(n.Field.Label,{children:"Format"}),e.jsxs(n.SingleSelect,{value:h.format,onChange:l=>a({format:l}),children:[e.jsx(n.SingleSelectOption,{value:"json",children:"JSON - everything"}),e.jsx(n.SingleSelectOption,{value:"csv",children:"CSV - flat fields"})]})]})}),e.jsx(n.Box,{minWidth:"160px",children:e.jsxs(n.Field.Root,{name:"export-status",children:[e.jsx(n.Field.Label,{children:"Version"}),e.jsxs(n.SingleSelect,{value:h.status,onChange:l=>a({status:l}),disabled:r?!r.draftAndPublish:!1,children:[e.jsx(n.SingleSelectOption,{value:"draft",children:"Draft"}),e.jsx(n.SingleSelectOption,{value:"published",children:"Published"})]})]})})]}),e.jsxs(n.Flex,{gap:4,wrap:"wrap",children:[e.jsx(n.Checkbox,{checked:h.relations,onCheckedChange:l=>a({relations:!!l}),children:"Relations"}),e.jsx(n.Checkbox,{checked:h.media,onCheckedChange:l=>a({media:!!l}),children:"Media references"})]}),g.length>0&&e.jsxs(n.Typography,{variant:"pi",textColor:"neutral600",children:["Carries ",g.join(", "),".",h.format==="csv"&&(r?.components.length||r?.dynamicZones.length)?" CSV leaves components and dynamic zones behind - use JSON to keep them.":""]}),e.jsx(n.Box,{children:e.jsx(n.Button,{startIcon:e.jsx(O.Download,{}),onClick:x,loading:j,disabled:!o,children:"Export"})}),m&&e.jsxs(n.Typography,{variant:"pi",textColor:"success600",children:["Saved ",m,"."]}),R.map((l,w)=>e.jsx(n.Typography,{variant:"pi",textColor:"warning700",children:l},w)),v&&e.jsx(n.Typography,{variant:"pi",textColor:"danger600",children:v})]})},T=({label:t,value:s,tone:i})=>e.jsxs(n.Flex,{direction:"column",gap:1,alignItems:"flex-start",children:[e.jsx(n.Typography,{variant:"beta",textColor:s>0?i:"neutral500",children:s}),e.jsx(n.Typography,{variant:"pi",textColor:"neutral600",children:t})]}),U=({report:t})=>{const s=t.rows.filter(i=>i.outcome==="error");return e.jsxs(n.Flex,{direction:"column",gap:4,alignItems:"stretch",children:[e.jsxs(n.Box,{padding:4,hasRadius:!0,background:"neutral0",borderColor:"neutral200",borderWidth:"1px",borderStyle:"solid",children:[e.jsxs(n.Flex,{justifyContent:"space-between",alignItems:"center",paddingBottom:3,children:[e.jsx(n.Typography,{variant:"delta",children:t.applied?"Imported":"Dry run - nothing written yet"}),e.jsx(n.Badge,{children:D(t)})]}),e.jsxs(n.Flex,{gap:8,wrap:"wrap",children:[e.jsx(T,{label:"in the file",value:t.total,tone:"neutral800"}),e.jsx(T,{label:"to create",value:t.created,tone:"success600"}),e.jsx(T,{label:"to update",value:t.updated,tone:"primary600"}),e.jsx(T,{label:"skipped",value:t.skipped,tone:"neutral600"}),e.jsx(T,{label:"failed",value:t.errored,tone:"danger600"})]})]}),t.warnings.map((i,o)=>e.jsx(n.Box,{padding:3,hasRadius:!0,background:"warning100",borderColor:"warning200",borderWidth:"1px",borderStyle:"solid",children:e.jsx(n.Typography,{variant:"pi",textColor:"warning700",children:i})},o)),t.unresolved.length>0&&e.jsxs(n.Box,{padding:4,hasRadius:!0,background:"neutral0",borderColor:"warning200",borderWidth:"1px",borderStyle:"solid",children:[e.jsx(n.Typography,{variant:"delta",children:"Relations that point at nothing"}),e.jsx(n.Box,{paddingTop:1,paddingBottom:3,children:e.jsx(n.Typography,{variant:"pi",textColor:"neutral600",children:'These targets are neither in the file nor in this environment. With "drop the link" the rows still import without them; with "refuse the import" nothing is written at all.'})}),e.jsxs(n.Flex,{direction:"column",gap:2,alignItems:"stretch",children:[t.unresolved.slice(0,25).map((i,o)=>e.jsxs(n.Flex,{gap:3,alignItems:"baseline",wrap:"wrap",children:[e.jsxs(n.Badge,{children:["row ",i.row]}),e.jsx(n.Typography,{variant:"omega",fontWeight:"bold",children:i.field}),e.jsx(n.Typography,{variant:"pi",textColor:"neutral600",children:i.target}),e.jsx(n.Typography,{variant:"pi",textColor:"danger600",children:i.keys.join(", ")})]},o)),t.unresolved.length>25&&e.jsxs(n.Typography,{variant:"pi",textColor:"neutral600",children:["and ",t.unresolved.length-25," more"]})]})]}),s.length>0&&e.jsxs(n.Box,{padding:4,hasRadius:!0,background:"neutral0",borderColor:"danger200",borderWidth:"1px",borderStyle:"solid",children:[e.jsx(n.Typography,{variant:"delta",textColor:"danger600",children:"Rows that failed"}),e.jsx(n.Box,{paddingTop:2,children:e.jsx(n.Flex,{direction:"column",gap:2,alignItems:"stretch",children:s.slice(0,25).map(i=>e.jsxs(n.Flex,{gap:3,alignItems:"baseline",wrap:"wrap",children:[e.jsxs(n.Badge,{children:["row ",i.row]}),e.jsx(n.Typography,{variant:"pi",textColor:"neutral600",children:i.documentId??"no documentId"}),e.jsx(n.Typography,{variant:"pi",textColor:"danger600",children:i.message})]},i.row))})})]})]})},N=({contentTypes:t})=>{const{post:s}=y.useFetchClient(),[i,o]=d.useState(t[0]?.uid??""),[c,h]=d.useState(P),[p,r]=d.useState(null),[u,j]=d.useState(null),[f,R]=d.useState(!1),[S,v]=d.useState(null),b=d.useRef(null),m=a=>{h(x=>({...x,...a})),j(null)},C=async a=>{if(!p)return;R(!0),v(null);const x=new FormData;x.append("file",p),x.append("uid",i),Object.entries(c).forEach(([g,l])=>x.append(g,String(l)));try{const{data:g}=await s(a?k.apply:k.preview,x);j(g)}catch(g){v(g?.response?.data?.error?.message??"The file could not be read."),j(null)}finally{R(!1)}};return e.jsxs(n.Flex,{direction:"column",gap:4,alignItems:"stretch",children:[e.jsx(n.Typography,{variant:"delta",children:"Import"}),e.jsxs(n.Field.Root,{name:"import-uid",children:[e.jsx(n.Field.Label,{children:"Content type"}),e.jsx(n.SingleSelect,{value:i,onChange:a=>{o(String(a)),j(null)},children:t.map(a=>e.jsx(n.SingleSelectOption,{value:a.uid,children:a.label},a.uid))})]}),e.jsxs(n.Field.Root,{name:"import-file",children:[e.jsx(n.Field.Label,{children:"File"}),e.jsxs(n.Flex,{gap:2,alignItems:"center",children:[e.jsx(n.Button,{variant:"secondary",onClick:()=>b.current?.click(),children:"Choose a file"}),e.jsx(n.Typography,{variant:"pi",textColor:p?"neutral800":"neutral500",children:p?p.name:"JSON or CSV"})]}),e.jsx("input",{ref:b,type:"file",accept:".json,.csv,application/json,text/csv",style:{display:"none"},onChange:a=>{r(a.target.files?.[0]??null),j(null)}})]}),e.jsxs(n.Flex,{gap:4,wrap:"wrap",alignItems:"flex-end",children:[e.jsx(n.Box,{minWidth:"200px",children:e.jsxs(n.Field.Root,{name:"import-existing",children:[e.jsx(n.Field.Label,{children:"When it is already there"}),e.jsxs(n.SingleSelect,{value:c.onExisting,onChange:a=>m({onExisting:a==="skip"?"skip":"update"}),children:[e.jsx(n.SingleSelectOption,{value:"update",children:"Update it"}),e.jsx(n.SingleSelectOption,{value:"skip",children:"Leave it alone"})]})]})}),e.jsx(n.Box,{minWidth:"240px",children:e.jsxs(n.Field.Root,{name:"import-missing",children:[e.jsx(n.Field.Label,{children:"When a relation points at nothing"}),e.jsxs(n.SingleSelect,{value:c.onMissingRelation,onChange:a=>m({onMissingRelation:a==="fail"?"fail":"skip"}),children:[e.jsx(n.SingleSelectOption,{value:"skip",children:"Drop the link, keep the row"}),e.jsx(n.SingleSelectOption,{value:"fail",children:"Refuse the import"})]})]})}),e.jsx(n.Box,{minWidth:"160px",children:e.jsxs(n.Field.Root,{name:"import-status",children:[e.jsx(n.Field.Label,{children:"Import as"}),e.jsxs(n.SingleSelect,{value:c.status,onChange:a=>m({status:a}),children:[e.jsx(n.SingleSelectOption,{value:"draft",children:"Draft"}),e.jsx(n.SingleSelectOption,{value:"published",children:"Published"})]})]})})]}),e.jsx(n.Checkbox,{checked:c.continueOnError,onCheckedChange:a=>m({continueOnError:!!a}),children:"Keep going past a row that fails"}),e.jsx(n.Typography,{variant:"pi",textColor:"neutral600",children:c.continueOnError?"Rows that fail are reported and the rest are committed.":"All or nothing: if any row fails, the whole import is rolled back and nothing changes."}),e.jsxs(n.Flex,{gap:2,children:[e.jsx(n.Button,{variant:"secondary",startIcon:e.jsx(O.Upload,{}),onClick:()=>C(!1),loading:f,disabled:!p||!i,children:"Dry run"}),e.jsx(n.Button,{onClick:()=>C(!0),loading:f,disabled:!p||!i||!u||u.applied,children:"Import for real"})]}),!u&&p&&e.jsx(n.Typography,{variant:"pi",textColor:"neutral600",children:"Run the dry run first. Nothing is written until you have seen what it would do."}),S&&e.jsx(n.Typography,{variant:"pi",textColor:"danger600",children:S}),u&&e.jsx(U,{report:u})]})},I=({children:t})=>e.jsx(n.Box,{padding:6,hasRadius:!0,background:"neutral0",borderColor:"neutral200",borderWidth:"1px",borderStyle:"solid",flex:"1",minWidth:"380px",children:t}),_=()=>{const{get:t}=y.useFetchClient(),[s,i]=d.useState([]),[o,c]=d.useState(!0),[h,p]=d.useState(null);return d.useEffect(()=>{let r=!0;return t(k.contentTypes).then(({data:u})=>r&&i(u.contentTypes)).catch(()=>r&&p("Could not read the list of content types.")).finally(()=>r&&c(!1)),()=>{r=!1}},[t]),e.jsxs(y.Page.Main,{children:[e.jsx(y.Layouts.Header,{title:"Ferry",subtitle:"Move content in and out as JSON or CSV, with relations and components intact"}),e.jsx(y.Layouts.Content,{children:o?e.jsx(n.Flex,{justifyContent:"center",padding:10,children:e.jsx(n.Loader,{small:!0,children:"Reading the schema"})}):h?e.jsx(n.Typography,{textColor:"danger600",children:h}):s.length===0?e.jsxs(n.Flex,{direction:"column",gap:2,padding:10,alignItems:"center",children:[e.jsx(n.Typography,{variant:"delta",children:"Nothing to carry yet"}),e.jsx(n.Typography,{textColor:"neutral600",children:"Create a content type, and it will show up here."})]}):e.jsxs(n.Flex,{gap:6,alignItems:"flex-start",wrap:"wrap",children:[e.jsx(I,{children:e.jsx(W,{contentTypes:s})}),e.jsx(I,{children:e.jsx(N,{contentTypes:s})})]})})]})};exports.TransferPage=_;
|