@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.
Files changed (35) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +262 -0
  3. package/dist/_chunks/TransferPage-Bhp6vkZE.js +1 -0
  4. package/dist/_chunks/TransferPage-c8hdFQDQ.mjs +398 -0
  5. package/dist/admin/index.js +1 -0
  6. package/dist/admin/index.mjs +43 -0
  7. package/dist/admin/src/api.d.ts +114 -0
  8. package/dist/admin/src/components/ExportPanel.d.ts +7 -0
  9. package/dist/admin/src/components/ImportPanel.d.ts +15 -0
  10. package/dist/admin/src/components/ReportView.d.ts +6 -0
  11. package/dist/admin/src/index.d.ts +11 -0
  12. package/dist/admin/src/pages/TransferPage.d.ts +10 -0
  13. package/dist/admin/src/pluginId.d.ts +7 -0
  14. package/dist/server/index.js +6 -0
  15. package/dist/server/index.mjs +996 -0
  16. package/dist/server/src/config/index.d.ts +30 -0
  17. package/dist/server/src/controllers/index.d.ts +12 -0
  18. package/dist/server/src/controllers/transfer.d.ts +14 -0
  19. package/dist/server/src/index.d.ts +97 -0
  20. package/dist/server/src/policies/index.d.ts +5 -0
  21. package/dist/server/src/register.d.ts +12 -0
  22. package/dist/server/src/routes/admin.d.ts +12 -0
  23. package/dist/server/src/routes/index.d.ts +14 -0
  24. package/dist/server/src/services/csv.d.ts +59 -0
  25. package/dist/server/src/services/export.d.ts +24 -0
  26. package/dist/server/src/services/import.d.ts +78 -0
  27. package/dist/server/src/services/index.d.ts +54 -0
  28. package/dist/server/src/services/nested.d.ts +38 -0
  29. package/dist/server/src/services/parse.d.ts +25 -0
  30. package/dist/server/src/services/populate.d.ts +20 -0
  31. package/dist/server/src/services/schema.d.ts +81 -0
  32. package/dist/server/src/services/serialise.d.ts +14 -0
  33. package/dist/server/src/types.d.ts +145 -0
  34. package/dist/server/src/uids.d.ts +9 -0
  35. 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=_;