@qkix/strapi-plugin-ferry 0.1.0 → 0.2.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/README.md CHANGED
@@ -62,6 +62,24 @@ export default {
62
62
 
63
63
  Then open **Ferry** in the main menu.
64
64
 
65
+ ## Exporting from the list view
66
+
67
+ <img src="https://raw.githubusercontent.com/qkix/strapi-plugins/main/packages/strapi-plugin-ferry/docs/list-view.png" alt="A Content Manager list view with two rows ticked: an Export action between Publish and Delete, and a second Export button beside the view settings" width="900" />
68
+
69
+ Ferry's own page carries whole content types. The Content Manager is where you
70
+ usually know which entries you actually want, so the export is there too, in the
71
+ two shapes that question comes in:
72
+
73
+ - **Tick some rows** and _Export_ appears beside _Delete_. This is the "a copy of
74
+ the twelve articles I just fixed" case.
75
+ - **Narrow the list with filters** and the _Export_ button next to the view
76
+ settings takes everything that matches - including what is on the pages you
77
+ cannot see, which is why ticking rows cannot answer it.
78
+
79
+ Either way you pick the format and the version, and the file downloads. Both
80
+ respect the `plugin::ferry.export` permission and appear only on the project's
81
+ own content types, so nothing offers an export the server would refuse.
82
+
65
83
  ## The dry run
66
84
 
67
85
  Nothing is written until you have read a report. Upload a file, and Ferry
@@ -257,6 +275,11 @@ assigned on create, that `create` is not an upsert, that a missing relation
257
275
  target takes the whole row down, and that a draft is allowed to be incomplete so
258
276
  `required` is no guide to what will import.
259
277
 
260
- ## Licence
278
+ ## Roadmap
279
+
280
+ What is worth building next, what is deliberately not planned, and why:
281
+ [ROADMAP.md](https://github.com/qkix/strapi-plugins/blob/main/packages/strapi-plugin-ferry/ROADMAP.md).
282
+
283
+ ## License
261
284
 
262
285
  MIT
@@ -0,0 +1,373 @@
1
+ import { jsxs as n, jsx as e } from "react/jsx-runtime";
2
+ import * as d from "react";
3
+ import { useFetchClient as j, Page as N, Layouts as $ } from "@strapi/admin/strapi-admin";
4
+ import { Flex as s, Typography as r, Field as c, SingleSelect as S, SingleSelectOption as f, Box as g, Checkbox as P, Button as D, Badge as F, Loader as U } from "@strapi/design-system";
5
+ import { Download as V, Upload as A } from "@strapi/icons";
6
+ import { D as J, r as T, s as Z, a as z, b as _ } from "./index-C3-QXVsq.mjs";
7
+ const H = ({ contentTypes: a }) => {
8
+ const { get: h, post: i } = j(), [u, v] = d.useState(a[0]?.uid ?? ""), [p, m] = d.useState(J), [o, C] = d.useState(null), [w, k] = d.useState(!1), [B, R] = d.useState([]), [I, E] = d.useState(null), [y, W] = d.useState(null);
9
+ d.useEffect(() => {
10
+ if (!u) return;
11
+ let t = !0;
12
+ return h(T.plan(u)).then(({ data: L }) => t && C(L)).catch(() => t && C(null)), () => {
13
+ t = !1;
14
+ };
15
+ }, [h, u]);
16
+ const l = (t) => m((L) => ({ ...L, ...t })), b = async () => {
17
+ k(!0), E(null), R([]), W(null);
18
+ try {
19
+ const { data: t } = await i(T.export, {
20
+ uid: u,
21
+ ...p,
22
+ envelope: !0
23
+ });
24
+ Z(t.body, t.filename, t.mime), R(t.warnings ?? []), W(`${t.filename} (${t.count} document${t.count === 1 ? "" : "s"})`);
25
+ } catch (t) {
26
+ E(t?.response?.data?.error?.message ?? "The export failed.");
27
+ } finally {
28
+ k(!1);
29
+ }
30
+ }, x = o ? [
31
+ `${o.scalars.length} field(s)`,
32
+ p.relations && o.relations.length > 0 ? `${o.relations.length} relation(s)` : null,
33
+ o.components.length > 0 ? `${o.components.length} component(s)` : null,
34
+ o.dynamicZones.length > 0 ? `${o.dynamicZones.length} dynamic zone(s)` : null,
35
+ p.media && o.media.length > 0 ? `${o.media.length} media field(s)` : null
36
+ ].filter(Boolean) : [];
37
+ return /* @__PURE__ */ n(s, { direction: "column", gap: 4, alignItems: "stretch", children: [
38
+ /* @__PURE__ */ e(r, { variant: "delta", children: "Export" }),
39
+ /* @__PURE__ */ n(c.Root, { name: "export-uid", children: [
40
+ /* @__PURE__ */ e(c.Label, { children: "Content type" }),
41
+ /* @__PURE__ */ e(S, { value: u, onChange: (t) => v(String(t)), children: a.map((t) => /* @__PURE__ */ e(f, { value: t.uid, children: t.label }, t.uid)) })
42
+ ] }),
43
+ /* @__PURE__ */ n(s, { gap: 4, wrap: "wrap", alignItems: "flex-end", children: [
44
+ /* @__PURE__ */ e(g, { minWidth: "160px", children: /* @__PURE__ */ n(c.Root, { name: "export-format", children: [
45
+ /* @__PURE__ */ e(c.Label, { children: "Format" }),
46
+ /* @__PURE__ */ n(
47
+ S,
48
+ {
49
+ value: p.format,
50
+ onChange: (t) => l({ format: t }),
51
+ children: [
52
+ /* @__PURE__ */ e(f, { value: "json", children: "JSON - everything" }),
53
+ /* @__PURE__ */ e(f, { value: "csv", children: "CSV - flat fields" })
54
+ ]
55
+ }
56
+ )
57
+ ] }) }),
58
+ /* @__PURE__ */ e(g, { minWidth: "160px", children: /* @__PURE__ */ n(c.Root, { name: "export-status", children: [
59
+ /* @__PURE__ */ e(c.Label, { children: "Version" }),
60
+ /* @__PURE__ */ n(
61
+ S,
62
+ {
63
+ value: p.status,
64
+ onChange: (t) => l({ status: t }),
65
+ disabled: o ? !o.draftAndPublish : !1,
66
+ children: [
67
+ /* @__PURE__ */ e(f, { value: "draft", children: "Draft" }),
68
+ /* @__PURE__ */ e(f, { value: "published", children: "Published" })
69
+ ]
70
+ }
71
+ )
72
+ ] }) })
73
+ ] }),
74
+ /* @__PURE__ */ n(s, { gap: 4, wrap: "wrap", children: [
75
+ /* @__PURE__ */ e(
76
+ P,
77
+ {
78
+ checked: p.relations,
79
+ onCheckedChange: (t) => l({ relations: !!t }),
80
+ children: "Relations"
81
+ }
82
+ ),
83
+ /* @__PURE__ */ e(
84
+ P,
85
+ {
86
+ checked: p.media,
87
+ onCheckedChange: (t) => l({ media: !!t }),
88
+ children: "Media references"
89
+ }
90
+ )
91
+ ] }),
92
+ x.length > 0 && /* @__PURE__ */ n(r, { variant: "pi", textColor: "neutral600", children: [
93
+ "Carries ",
94
+ x.join(", "),
95
+ ".",
96
+ p.format === "csv" && (o?.components.length || o?.dynamicZones.length) ? " CSV leaves components and dynamic zones behind - use JSON to keep them." : ""
97
+ ] }),
98
+ /* @__PURE__ */ e(g, { children: /* @__PURE__ */ e(D, { startIcon: /* @__PURE__ */ e(V, {}), onClick: b, loading: w, disabled: !u, children: "Export" }) }),
99
+ y && /* @__PURE__ */ n(r, { variant: "pi", textColor: "success600", children: [
100
+ "Saved ",
101
+ y,
102
+ "."
103
+ ] }),
104
+ B.map((t, L) => /* @__PURE__ */ e(r, { variant: "pi", textColor: "warning700", children: t }, L)),
105
+ I && /* @__PURE__ */ e(r, { variant: "pi", textColor: "danger600", children: I })
106
+ ] });
107
+ }, O = ({ label: a, value: h, tone: i }) => /* @__PURE__ */ n(s, { direction: "column", gap: 1, alignItems: "flex-start", children: [
108
+ /* @__PURE__ */ e(r, { variant: "beta", textColor: h > 0 ? i : "neutral500", children: h }),
109
+ /* @__PURE__ */ e(r, { variant: "pi", textColor: "neutral600", children: a })
110
+ ] }), K = ({ report: a }) => {
111
+ const h = a.rows.filter((i) => i.outcome === "error");
112
+ return /* @__PURE__ */ n(s, { direction: "column", gap: 4, alignItems: "stretch", children: [
113
+ /* @__PURE__ */ n(
114
+ g,
115
+ {
116
+ padding: 4,
117
+ hasRadius: !0,
118
+ background: "neutral0",
119
+ borderColor: "neutral200",
120
+ borderWidth: "1px",
121
+ borderStyle: "solid",
122
+ children: [
123
+ /* @__PURE__ */ n(s, { justifyContent: "space-between", alignItems: "center", paddingBottom: 3, children: [
124
+ /* @__PURE__ */ e(r, { variant: "delta", children: a.applied ? "Imported" : "Dry run - nothing written yet" }),
125
+ /* @__PURE__ */ e(F, { children: z(a) })
126
+ ] }),
127
+ /* @__PURE__ */ n(s, { gap: 8, wrap: "wrap", children: [
128
+ /* @__PURE__ */ e(O, { label: "in the file", value: a.total, tone: "neutral800" }),
129
+ /* @__PURE__ */ e(O, { label: "to create", value: a.created, tone: "success600" }),
130
+ /* @__PURE__ */ e(O, { label: "to update", value: a.updated, tone: "primary600" }),
131
+ /* @__PURE__ */ e(O, { label: "skipped", value: a.skipped, tone: "neutral600" }),
132
+ /* @__PURE__ */ e(O, { label: "failed", value: a.errored, tone: "danger600" })
133
+ ] })
134
+ ]
135
+ }
136
+ ),
137
+ a.warnings.map((i, u) => /* @__PURE__ */ e(
138
+ g,
139
+ {
140
+ padding: 3,
141
+ hasRadius: !0,
142
+ background: "warning100",
143
+ borderColor: "warning200",
144
+ borderWidth: "1px",
145
+ borderStyle: "solid",
146
+ children: /* @__PURE__ */ e(r, { variant: "pi", textColor: "warning700", children: i })
147
+ },
148
+ u
149
+ )),
150
+ a.unresolved.length > 0 && /* @__PURE__ */ n(
151
+ g,
152
+ {
153
+ padding: 4,
154
+ hasRadius: !0,
155
+ background: "neutral0",
156
+ borderColor: "warning200",
157
+ borderWidth: "1px",
158
+ borderStyle: "solid",
159
+ children: [
160
+ /* @__PURE__ */ e(r, { variant: "delta", children: "Relations that point at nothing" }),
161
+ /* @__PURE__ */ e(g, { paddingTop: 1, paddingBottom: 3, children: /* @__PURE__ */ e(r, { 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.' }) }),
162
+ /* @__PURE__ */ n(s, { direction: "column", gap: 2, alignItems: "stretch", children: [
163
+ a.unresolved.slice(0, 25).map((i, u) => /* @__PURE__ */ n(s, { gap: 3, alignItems: "baseline", wrap: "wrap", children: [
164
+ /* @__PURE__ */ n(F, { children: [
165
+ "row ",
166
+ i.row
167
+ ] }),
168
+ /* @__PURE__ */ e(r, { variant: "omega", fontWeight: "bold", children: i.field }),
169
+ /* @__PURE__ */ e(r, { variant: "pi", textColor: "neutral600", children: i.target }),
170
+ /* @__PURE__ */ e(r, { variant: "pi", textColor: "danger600", children: i.keys.join(", ") })
171
+ ] }, u)),
172
+ a.unresolved.length > 25 && /* @__PURE__ */ n(r, { variant: "pi", textColor: "neutral600", children: [
173
+ "and ",
174
+ a.unresolved.length - 25,
175
+ " more"
176
+ ] })
177
+ ] })
178
+ ]
179
+ }
180
+ ),
181
+ h.length > 0 && /* @__PURE__ */ n(
182
+ g,
183
+ {
184
+ padding: 4,
185
+ hasRadius: !0,
186
+ background: "neutral0",
187
+ borderColor: "danger200",
188
+ borderWidth: "1px",
189
+ borderStyle: "solid",
190
+ children: [
191
+ /* @__PURE__ */ e(r, { variant: "delta", textColor: "danger600", children: "Rows that failed" }),
192
+ /* @__PURE__ */ e(g, { paddingTop: 2, children: /* @__PURE__ */ e(s, { direction: "column", gap: 2, alignItems: "stretch", children: h.slice(0, 25).map((i) => /* @__PURE__ */ n(s, { gap: 3, alignItems: "baseline", wrap: "wrap", children: [
193
+ /* @__PURE__ */ n(F, { children: [
194
+ "row ",
195
+ i.row
196
+ ] }),
197
+ /* @__PURE__ */ e(r, { variant: "pi", textColor: "neutral600", children: i.documentId ?? "no documentId" }),
198
+ /* @__PURE__ */ e(r, { variant: "pi", textColor: "danger600", children: i.message })
199
+ ] }, i.row)) }) })
200
+ ]
201
+ }
202
+ )
203
+ ] });
204
+ }, X = ({ contentTypes: a }) => {
205
+ const { post: h } = j(), [i, u] = d.useState(a[0]?.uid ?? ""), [v, p] = d.useState(_), [m, o] = d.useState(null), [C, w] = d.useState(null), [k, B] = d.useState(!1), [R, I] = d.useState(null), E = d.useRef(null), y = (l) => {
206
+ p((b) => ({ ...b, ...l })), w(null);
207
+ }, W = async (l) => {
208
+ if (!m) return;
209
+ B(!0), I(null);
210
+ const b = new FormData();
211
+ b.append("file", m), b.append("uid", i), Object.entries(v).forEach(([x, t]) => b.append(x, String(t)));
212
+ try {
213
+ const { data: x } = await h(l ? T.apply : T.preview, b);
214
+ w(x);
215
+ } catch (x) {
216
+ I(x?.response?.data?.error?.message ?? "The file could not be read."), w(null);
217
+ } finally {
218
+ B(!1);
219
+ }
220
+ };
221
+ return /* @__PURE__ */ n(s, { direction: "column", gap: 4, alignItems: "stretch", children: [
222
+ /* @__PURE__ */ e(r, { variant: "delta", children: "Import" }),
223
+ /* @__PURE__ */ n(c.Root, { name: "import-uid", children: [
224
+ /* @__PURE__ */ e(c.Label, { children: "Content type" }),
225
+ /* @__PURE__ */ e(
226
+ S,
227
+ {
228
+ value: i,
229
+ onChange: (l) => {
230
+ u(String(l)), w(null);
231
+ },
232
+ children: a.map((l) => /* @__PURE__ */ e(f, { value: l.uid, children: l.label }, l.uid))
233
+ }
234
+ )
235
+ ] }),
236
+ /* @__PURE__ */ n(c.Root, { name: "import-file", children: [
237
+ /* @__PURE__ */ e(c.Label, { children: "File" }),
238
+ /* @__PURE__ */ n(s, { gap: 2, alignItems: "center", children: [
239
+ /* @__PURE__ */ e(D, { variant: "secondary", onClick: () => E.current?.click(), children: "Choose a file" }),
240
+ /* @__PURE__ */ e(r, { variant: "pi", textColor: m ? "neutral800" : "neutral500", children: m ? m.name : "JSON or CSV" })
241
+ ] }),
242
+ /* @__PURE__ */ e(
243
+ "input",
244
+ {
245
+ ref: E,
246
+ type: "file",
247
+ accept: ".json,.csv,application/json,text/csv",
248
+ style: { display: "none" },
249
+ onChange: (l) => {
250
+ o(l.target.files?.[0] ?? null), w(null);
251
+ }
252
+ }
253
+ )
254
+ ] }),
255
+ /* @__PURE__ */ n(s, { gap: 4, wrap: "wrap", alignItems: "flex-end", children: [
256
+ /* @__PURE__ */ e(g, { minWidth: "200px", children: /* @__PURE__ */ n(c.Root, { name: "import-existing", children: [
257
+ /* @__PURE__ */ e(c.Label, { children: "When it is already there" }),
258
+ /* @__PURE__ */ n(
259
+ S,
260
+ {
261
+ value: v.onExisting,
262
+ onChange: (l) => y({ onExisting: l === "skip" ? "skip" : "update" }),
263
+ children: [
264
+ /* @__PURE__ */ e(f, { value: "update", children: "Update it" }),
265
+ /* @__PURE__ */ e(f, { value: "skip", children: "Leave it alone" })
266
+ ]
267
+ }
268
+ )
269
+ ] }) }),
270
+ /* @__PURE__ */ e(g, { minWidth: "240px", children: /* @__PURE__ */ n(c.Root, { name: "import-missing", children: [
271
+ /* @__PURE__ */ e(c.Label, { children: "When a relation points at nothing" }),
272
+ /* @__PURE__ */ n(
273
+ S,
274
+ {
275
+ value: v.onMissingRelation,
276
+ onChange: (l) => y({ onMissingRelation: l === "fail" ? "fail" : "skip" }),
277
+ children: [
278
+ /* @__PURE__ */ e(f, { value: "skip", children: "Drop the link, keep the row" }),
279
+ /* @__PURE__ */ e(f, { value: "fail", children: "Refuse the import" })
280
+ ]
281
+ }
282
+ )
283
+ ] }) }),
284
+ /* @__PURE__ */ e(g, { minWidth: "160px", children: /* @__PURE__ */ n(c.Root, { name: "import-status", children: [
285
+ /* @__PURE__ */ e(c.Label, { children: "Import as" }),
286
+ /* @__PURE__ */ n(
287
+ S,
288
+ {
289
+ value: v.status,
290
+ onChange: (l) => y({ status: l }),
291
+ children: [
292
+ /* @__PURE__ */ e(f, { value: "draft", children: "Draft" }),
293
+ /* @__PURE__ */ e(f, { value: "published", children: "Published" })
294
+ ]
295
+ }
296
+ )
297
+ ] }) })
298
+ ] }),
299
+ /* @__PURE__ */ e(
300
+ P,
301
+ {
302
+ checked: v.continueOnError,
303
+ onCheckedChange: (l) => y({ continueOnError: !!l }),
304
+ children: "Keep going past a row that fails"
305
+ }
306
+ ),
307
+ /* @__PURE__ */ e(r, { variant: "pi", textColor: "neutral600", children: v.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." }),
308
+ /* @__PURE__ */ n(s, { gap: 2, children: [
309
+ /* @__PURE__ */ e(
310
+ D,
311
+ {
312
+ variant: "secondary",
313
+ startIcon: /* @__PURE__ */ e(A, {}),
314
+ onClick: () => W(!1),
315
+ loading: k,
316
+ disabled: !m || !i,
317
+ children: "Dry run"
318
+ }
319
+ ),
320
+ /* @__PURE__ */ e(
321
+ D,
322
+ {
323
+ onClick: () => W(!0),
324
+ loading: k,
325
+ disabled: !m || !i || !C || C.applied,
326
+ children: "Import for real"
327
+ }
328
+ )
329
+ ] }),
330
+ !C && m && /* @__PURE__ */ e(r, { variant: "pi", textColor: "neutral600", children: "Run the dry run first. Nothing is written until you have seen what it would do." }),
331
+ R && /* @__PURE__ */ e(r, { variant: "pi", textColor: "danger600", children: R }),
332
+ C && /* @__PURE__ */ e(K, { report: C })
333
+ ] });
334
+ }, M = ({ children: a }) => /* @__PURE__ */ e(
335
+ g,
336
+ {
337
+ padding: 6,
338
+ hasRadius: !0,
339
+ background: "neutral0",
340
+ borderColor: "neutral200",
341
+ borderWidth: "1px",
342
+ borderStyle: "solid",
343
+ flex: "1",
344
+ minWidth: "380px",
345
+ children: a
346
+ }
347
+ ), ne = () => {
348
+ const { get: a } = j(), [h, i] = d.useState([]), [u, v] = d.useState(!0), [p, m] = d.useState(null);
349
+ return d.useEffect(() => {
350
+ let o = !0;
351
+ return a(T.contentTypes).then(({ data: C }) => o && i(C.contentTypes)).catch(() => o && m("Could not read the list of content types.")).finally(() => o && v(!1)), () => {
352
+ o = !1;
353
+ };
354
+ }, [a]), /* @__PURE__ */ n(N.Main, { children: [
355
+ /* @__PURE__ */ e(
356
+ $.Header,
357
+ {
358
+ title: "Ferry",
359
+ subtitle: "Move content in and out as JSON or CSV, with relations and components intact"
360
+ }
361
+ ),
362
+ /* @__PURE__ */ e($.Content, { children: u ? /* @__PURE__ */ e(s, { justifyContent: "center", padding: 10, children: /* @__PURE__ */ e(U, { small: !0, children: "Reading the schema" }) }) : p ? /* @__PURE__ */ e(r, { textColor: "danger600", children: p }) : h.length === 0 ? /* @__PURE__ */ n(s, { direction: "column", gap: 2, padding: 10, alignItems: "center", children: [
363
+ /* @__PURE__ */ e(r, { variant: "delta", children: "Nothing to carry yet" }),
364
+ /* @__PURE__ */ e(r, { textColor: "neutral600", children: "Create a content type, and it will show up here." })
365
+ ] }) : /* @__PURE__ */ n(s, { gap: 6, alignItems: "flex-start", wrap: "wrap", children: [
366
+ /* @__PURE__ */ e(M, { children: /* @__PURE__ */ e(H, { contentTypes: h }) }),
367
+ /* @__PURE__ */ e(M, { children: /* @__PURE__ */ e(X, { contentTypes: h }) })
368
+ ] }) })
369
+ ] });
370
+ };
371
+ export {
372
+ ne as TransferPage
373
+ };
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("react/jsx-runtime"),B=require("react"),f=require("@strapi/admin/strapi-admin"),n=require("@strapi/design-system"),R=require("@strapi/icons"),x=require("./index-9EKjguHv.js");function I(t){if(t&&t.__esModule)return t;const o=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t){for(const i in t)if(i!=="default"){const d=Object.getOwnPropertyDescriptor(t,i);Object.defineProperty(o,i,d.get?d:{enumerable:!0,get:()=>t[i]})}}return o.default=t,Object.freeze(o)}const s=I(B),E=({contentTypes:t})=>{const{get:o,post:i}=f.useFetchClient(),[d,p]=s.useState(t[0]?.uid??""),[c,h]=s.useState(x.DEFAULT_EXPORT),[a,u]=s.useState(null),[m,S]=s.useState(!1),[k,v]=s.useState([]),[b,C]=s.useState(null),[y,w]=s.useState(null);s.useEffect(()=>{if(!d)return;let l=!0;return o(x.routes.plan(d)).then(({data:F})=>l&&u(F)).catch(()=>l&&u(null)),()=>{l=!1}},[o,d]);const r=l=>h(F=>({...F,...l})),g=async()=>{S(!0),C(null),v([]),w(null);try{const{data:l}=await i(x.routes.export,{uid:d,...c,envelope:!0});x.saveFile(l.body,l.filename,l.mime),v(l.warnings??[]),w(`${l.filename} (${l.count} document${l.count===1?"":"s"})`)}catch(l){C(l?.response?.data?.error?.message??"The export failed.")}finally{S(!1)}},j=a?[`${a.scalars.length} field(s)`,c.relations&&a.relations.length>0?`${a.relations.length} relation(s)`:null,a.components.length>0?`${a.components.length} component(s)`:null,a.dynamicZones.length>0?`${a.dynamicZones.length} dynamic zone(s)`:null,c.media&&a.media.length>0?`${a.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:d,onChange:l=>p(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:c.format,onChange:l=>r({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:c.status,onChange:l=>r({status:l}),disabled:a?!a.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:c.relations,onCheckedChange:l=>r({relations:!!l}),children:"Relations"}),e.jsx(n.Checkbox,{checked:c.media,onCheckedChange:l=>r({media:!!l}),children:"Media references"})]}),j.length>0&&e.jsxs(n.Typography,{variant:"pi",textColor:"neutral600",children:["Carries ",j.join(", "),".",c.format==="csv"&&(a?.components.length||a?.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(R.Download,{}),onClick:g,loading:m,disabled:!d,children:"Export"})}),y&&e.jsxs(n.Typography,{variant:"pi",textColor:"success600",children:["Saved ",y,"."]}),k.map((l,F)=>e.jsx(n.Typography,{variant:"pi",textColor:"warning700",children:l},F)),b&&e.jsx(n.Typography,{variant:"pi",textColor:"danger600",children:b})]})},T=({label:t,value:o,tone:i})=>e.jsxs(n.Flex,{direction:"column",gap:1,alignItems:"flex-start",children:[e.jsx(n.Typography,{variant:"beta",textColor:o>0?i:"neutral500",children:o}),e.jsx(n.Typography,{variant:"pi",textColor:"neutral600",children:t})]}),W=({report:t})=>{const o=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:x.summarise(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,d)=>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})},d)),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,d)=>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(", ")})]},d)),t.unresolved.length>25&&e.jsxs(n.Typography,{variant:"pi",textColor:"neutral600",children:["and ",t.unresolved.length-25," more"]})]})]}),o.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:o.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))})})]})]})},L=({contentTypes:t})=>{const{post:o}=f.useFetchClient(),[i,d]=s.useState(t[0]?.uid??""),[p,c]=s.useState(x.DEFAULT_IMPORT),[h,a]=s.useState(null),[u,m]=s.useState(null),[S,k]=s.useState(!1),[v,b]=s.useState(null),C=s.useRef(null),y=r=>{c(g=>({...g,...r})),m(null)},w=async r=>{if(!h)return;k(!0),b(null);const g=new FormData;g.append("file",h),g.append("uid",i),Object.entries(p).forEach(([j,l])=>g.append(j,String(l)));try{const{data:j}=await o(r?x.routes.apply:x.routes.preview,g);m(j)}catch(j){b(j?.response?.data?.error?.message??"The file could not be read."),m(null)}finally{k(!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:r=>{d(String(r)),m(null)},children:t.map(r=>e.jsx(n.SingleSelectOption,{value:r.uid,children:r.label},r.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:()=>C.current?.click(),children:"Choose a file"}),e.jsx(n.Typography,{variant:"pi",textColor:h?"neutral800":"neutral500",children:h?h.name:"JSON or CSV"})]}),e.jsx("input",{ref:C,type:"file",accept:".json,.csv,application/json,text/csv",style:{display:"none"},onChange:r=>{a(r.target.files?.[0]??null),m(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:p.onExisting,onChange:r=>y({onExisting:r==="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:p.onMissingRelation,onChange:r=>y({onMissingRelation:r==="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:p.status,onChange:r=>y({status:r}),children:[e.jsx(n.SingleSelectOption,{value:"draft",children:"Draft"}),e.jsx(n.SingleSelectOption,{value:"published",children:"Published"})]})]})})]}),e.jsx(n.Checkbox,{checked:p.continueOnError,onCheckedChange:r=>y({continueOnError:!!r}),children:"Keep going past a row that fails"}),e.jsx(n.Typography,{variant:"pi",textColor:"neutral600",children:p.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(R.Upload,{}),onClick:()=>w(!1),loading:S,disabled:!h||!i,children:"Dry run"}),e.jsx(n.Button,{onClick:()=>w(!0),loading:S,disabled:!h||!i||!u||u.applied,children:"Import for real"})]}),!u&&h&&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."}),v&&e.jsx(n.Typography,{variant:"pi",textColor:"danger600",children:v}),u&&e.jsx(W,{report:u})]})},O=({children:t})=>e.jsx(n.Box,{padding:6,hasRadius:!0,background:"neutral0",borderColor:"neutral200",borderWidth:"1px",borderStyle:"solid",flex:"1",minWidth:"380px",children:t}),P=()=>{const{get:t}=f.useFetchClient(),[o,i]=s.useState([]),[d,p]=s.useState(!0),[c,h]=s.useState(null);return s.useEffect(()=>{let a=!0;return t(x.routes.contentTypes).then(({data:u})=>a&&i(u.contentTypes)).catch(()=>a&&h("Could not read the list of content types.")).finally(()=>a&&p(!1)),()=>{a=!1}},[t]),e.jsxs(f.Page.Main,{children:[e.jsx(f.Layouts.Header,{title:"Ferry",subtitle:"Move content in and out as JSON or CSV, with relations and components intact"}),e.jsx(f.Layouts.Content,{children:d?e.jsx(n.Flex,{justifyContent:"center",padding:10,children:e.jsx(n.Loader,{small:!0,children:"Reading the schema"})}):c?e.jsx(n.Typography,{textColor:"danger600",children:c}):o.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(O,{children:e.jsx(E,{contentTypes:o})}),e.jsx(O,{children:e.jsx(L,{contentTypes:o})})]})})]})};exports.TransferPage=P;
@@ -0,0 +1 @@
1
+ "use strict";const y=require("@strapi/icons"),t=require("react/jsx-runtime"),F=require("react"),g=require("@strapi/admin/strapi-admin"),r=require("@strapi/design-system");function k(e){if(e&&e.__esModule)return e;const n=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(e){for(const o in e)if(o!=="default"){const s=Object.getOwnPropertyDescriptor(e,o);Object.defineProperty(n,o,s.get?s:{enumerable:!0,get:()=>e[o]})}}return n.default=e,Object.freeze(n)}const p=k(F),$=(e,n,o)=>{const s=e[n];return s?typeof s=="function"?s():Promise.resolve(s):new Promise((i,a)=>{(typeof queueMicrotask=="function"?queueMicrotask:setTimeout)(a.bind(null,new Error("Unknown variable dynamic import: "+n+(n.split("/").length!==o?". Note that variables only represent file names one level deep.":""))))})},c="ferry",S={contentTypes:`/${c}/content-types`,plan:e=>`/${c}/plan/${e}`,export:`/${c}/export`,preview:`/${c}/import/preview`,apply:`/${c}/import`},h={format:"json",status:"draft",relations:!0,media:!0},M={status:"draft",onExisting:"update",onMissingRelation:"skip",publish:!0,continueOnError:!1},E=(e,n,o)=>{const s=URL.createObjectURL(new Blob([e],{type:o})),i=document.createElement("a");i.href=s,i.download=n,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(s)},I=e=>[e.created>0?`${e.created} to create`:null,e.updated>0?`${e.updated} to update`:null,e.skipped>0?`${e.skipped} skipped`:null,e.errored>0?`${e.errored} failed`:null].filter(Boolean).join(", ")||"nothing to do",O=({scope:e,request:n,uid:o,onClose:s})=>{const{post:i}=g.useFetchClient(),[a,d]=p.useState(h.format),[u,x]=p.useState(h.status),[m,j]=p.useState(!1),[b,v]=p.useState(null),[f,w]=p.useState([]),P=async()=>{j(!0),v(null),w([]);try{const{data:l}=await i(S.export,{uid:o,...h,...n,format:a,status:u,envelope:!0});if(E(l.body,l.filename,l.mime),l.warnings?.length){w(l.warnings);return}s()}catch(l){v(l?.response?.data?.error?.message??"The export failed.")}finally{j(!1)}};return t.jsxs(t.Fragment,{children:[t.jsx(r.Modal.Body,{children:t.jsxs(r.Flex,{direction:"column",alignItems:"stretch",gap:4,children:[t.jsxs(r.Typography,{textColor:"neutral700",children:["Exporting ",e,"."]}),t.jsxs(r.Field.Root,{name:"ferry-format",children:[t.jsx(r.Field.Label,{children:"Format"}),t.jsxs(r.SingleSelect,{value:a,onChange:l=>d(String(l)),disabled:m,children:[t.jsx(r.SingleSelectOption,{value:"json",children:"JSON"}),t.jsx(r.SingleSelectOption,{value:"csv",children:"CSV"})]})]}),t.jsxs(r.Field.Root,{name:"ferry-status",children:[t.jsx(r.Field.Label,{children:"Version"}),t.jsxs(r.SingleSelect,{value:u,onChange:l=>x(String(l)),disabled:m,children:[t.jsx(r.SingleSelectOption,{value:"draft",children:"Draft"}),t.jsx(r.SingleSelectOption,{value:"published",children:"Published"})]})]}),f.length>0?t.jsxs(r.Flex,{direction:"column",alignItems:"flex-start",gap:1,children:[t.jsx(r.Typography,{variant:"pi",textColor:"warning600",fontWeight:"bold",children:"The file was saved, with warnings:"}),f.map(l=>t.jsx(r.Typography,{variant:"pi",textColor:"warning600",children:l},l))]}):null,b?t.jsx(r.Typography,{textColor:"danger600",children:b}):null]})}),t.jsxs(r.Modal.Footer,{children:[t.jsx(r.Button,{variant:"tertiary",onClick:s,disabled:m,children:f.length>0?"Close":"Cancel"}),t.jsx(r.Button,{onClick:P,loading:m,children:"Export"})]})]})},C=e=>e.startsWith("api::"),R=[{action:`plugin::${c}.export`,subject:null}],T=e=>e.plugins?.i18n?.locale,L=({documents:e,model:n})=>{const{allowedActions:o,isLoading:s}=g.useRBAC({export:R}),[{query:i}]=g.useQueryParams();if(s||!o.canExport||!C(n))return null;const a=e.map(u=>u.documentId).filter(Boolean);if(a.length===0)return null;const d=a.length;return{label:"Export",icon:t.jsx(y.Download,{}),dialog:{type:"modal",title:"Export",content:({onClose:u})=>t.jsx(O,{uid:n,scope:`${d} selected ${d===1?"entry":"entries"}`,request:{documentIds:a,locale:T(i)},onClose:u})}}},_=()=>{const{allowedActions:e,isLoading:n}=g.useRBAC({export:R}),[{query:o}]=g.useQueryParams(),[s,i]=p.useState(!1),a=window.location.pathname.match(/\/content-manager\/collection-types\/([^/?#]+)/),d=a?decodeURIComponent(a[1]):null;if(n||!e.canExport||!d||!C(d))return null;const u=o.filters,x=!!(u&&Object.keys(u).length>0);return t.jsxs(t.Fragment,{children:[t.jsx(r.Button,{variant:"tertiary",startIcon:t.jsx(y.Download,{}),onClick:()=>i(!0),children:"Export"}),s?t.jsx(r.Modal.Root,{open:!0,onOpenChange:()=>i(!1),children:t.jsxs(r.Modal.Content,{children:[t.jsx(r.Modal.Header,{children:t.jsx(r.Modal.Title,{children:"Export"})}),t.jsx(O,{uid:d,scope:x?"everything matching this view":"every entry of this content type",request:{filters:u,sort:o.sort,locale:T(o)},onClose:()=>i(!1)})]})}):null]})},A={register(e){e.registerPlugin({id:c,name:c,isReady:!0}),e.addMenuLink({to:`/plugins/${c}`,icon:y.ArrowsHorizontal,intlLabel:{id:`${c}.menu.label`,defaultMessage:"Ferry"},Component:async()=>{const{TransferPage:n}=await Promise.resolve().then(()=>require("./TransferPage-DPZr7PFx.js"));return{default:n}},permissions:[{action:`plugin::${c}.export`,subject:null}]})},bootstrap(e){const n=e.getPlugin("content-manager");n.apis.addBulkAction(o=>{const s=o.findIndex(a=>a.type==="delete"),i=s===-1?o.length:s;return[...o.slice(0,i),L,...o.slice(i)]}),n.injectComponent("listView","actions",{name:`${c}-export-view`,Component:_})},async registerTrads({locales:e}){return Promise.all(e.map(async n=>{try{const{default:o}=await $(Object.assign({}),`./translations/${n}.json`,3);return{data:o,locale:n}}catch{return{data:{},locale:n}}}))}};exports.DEFAULT_EXPORT=h;exports.DEFAULT_IMPORT=M;exports.PLUGIN_ID=c;exports.index=A;exports.routes=S;exports.saveFile=E;exports.summarise=I;
@@ -0,0 +1,207 @@
1
+ import { Download as O, ArrowsHorizontal as M } from "@strapi/icons";
2
+ import { jsxs as c, Fragment as P, jsx as n } from "react/jsx-runtime";
3
+ import * as p from "react";
4
+ import { useFetchClient as U, useRBAC as j, useQueryParams as I } from "@strapi/admin/strapi-admin";
5
+ import { Modal as m, Flex as k, Typography as g, Field as f, SingleSelect as R, SingleSelectOption as y, Button as w } from "@strapi/design-system";
6
+ const D = (e, t, o) => {
7
+ const r = e[t];
8
+ return r ? typeof r == "function" ? r() : Promise.resolve(r) : new Promise((a, l) => {
9
+ (typeof queueMicrotask == "function" ? queueMicrotask : setTimeout)(
10
+ l.bind(
11
+ null,
12
+ new Error(
13
+ "Unknown variable dynamic import: " + t + (t.split("/").length !== o ? ". Note that variables only represent file names one level deep." : "")
14
+ )
15
+ )
16
+ );
17
+ });
18
+ }, s = "ferry", _ = {
19
+ contentTypes: `/${s}/content-types`,
20
+ plan: (e) => `/${s}/plan/${e}`,
21
+ export: `/${s}/export`,
22
+ preview: `/${s}/import/preview`,
23
+ apply: `/${s}/import`
24
+ }, v = {
25
+ format: "json",
26
+ status: "draft",
27
+ relations: !0,
28
+ media: !0
29
+ }, G = {
30
+ status: "draft",
31
+ onExisting: "update",
32
+ onMissingRelation: "skip",
33
+ publish: !0,
34
+ continueOnError: !1
35
+ }, q = (e, t, o) => {
36
+ const r = URL.createObjectURL(new Blob([e], { type: o })), a = document.createElement("a");
37
+ a.href = r, a.download = t, document.body.appendChild(a), a.click(), a.remove(), URL.revokeObjectURL(r);
38
+ }, J = (e) => [
39
+ e.created > 0 ? `${e.created} to create` : null,
40
+ e.updated > 0 ? `${e.updated} to update` : null,
41
+ e.skipped > 0 ? `${e.skipped} skipped` : null,
42
+ e.errored > 0 ? `${e.errored} failed` : null
43
+ ].filter(Boolean).join(", ") || "nothing to do", L = ({
44
+ /** What is being exported, in words, e.g. "3 selected entries". */
45
+ scope: e,
46
+ /** The scope as request keys - `documentIds`, or `filters` and `locale`. */
47
+ request: t,
48
+ uid: o,
49
+ onClose: r
50
+ }) => {
51
+ const { post: a } = U(), [l, d] = p.useState(v.format), [u, x] = p.useState(v.status), [h, C] = p.useState(!1), [E, S] = p.useState(null), [b, $] = p.useState([]), A = async () => {
52
+ C(!0), S(null), $([]);
53
+ try {
54
+ const { data: i } = await a(_.export, {
55
+ uid: o,
56
+ ...v,
57
+ ...t,
58
+ format: l,
59
+ status: u,
60
+ envelope: !0
61
+ });
62
+ if (q(i.body, i.filename, i.mime), i.warnings?.length) {
63
+ $(i.warnings);
64
+ return;
65
+ }
66
+ r();
67
+ } catch (i) {
68
+ S(i?.response?.data?.error?.message ?? "The export failed.");
69
+ } finally {
70
+ C(!1);
71
+ }
72
+ };
73
+ return /* @__PURE__ */ c(P, { children: [
74
+ /* @__PURE__ */ n(m.Body, { children: /* @__PURE__ */ c(k, { direction: "column", alignItems: "stretch", gap: 4, children: [
75
+ /* @__PURE__ */ c(g, { textColor: "neutral700", children: [
76
+ "Exporting ",
77
+ e,
78
+ "."
79
+ ] }),
80
+ /* @__PURE__ */ c(f.Root, { name: "ferry-format", children: [
81
+ /* @__PURE__ */ n(f.Label, { children: "Format" }),
82
+ /* @__PURE__ */ c(
83
+ R,
84
+ {
85
+ value: l,
86
+ onChange: (i) => d(String(i)),
87
+ disabled: h,
88
+ children: [
89
+ /* @__PURE__ */ n(y, { value: "json", children: "JSON" }),
90
+ /* @__PURE__ */ n(y, { value: "csv", children: "CSV" })
91
+ ]
92
+ }
93
+ )
94
+ ] }),
95
+ /* @__PURE__ */ c(f.Root, { name: "ferry-status", children: [
96
+ /* @__PURE__ */ n(f.Label, { children: "Version" }),
97
+ /* @__PURE__ */ c(
98
+ R,
99
+ {
100
+ value: u,
101
+ onChange: (i) => x(String(i)),
102
+ disabled: h,
103
+ children: [
104
+ /* @__PURE__ */ n(y, { value: "draft", children: "Draft" }),
105
+ /* @__PURE__ */ n(y, { value: "published", children: "Published" })
106
+ ]
107
+ }
108
+ )
109
+ ] }),
110
+ b.length > 0 ? /* @__PURE__ */ c(k, { direction: "column", alignItems: "flex-start", gap: 1, children: [
111
+ /* @__PURE__ */ n(g, { variant: "pi", textColor: "warning600", fontWeight: "bold", children: "The file was saved, with warnings:" }),
112
+ b.map((i) => /* @__PURE__ */ n(g, { variant: "pi", textColor: "warning600", children: i }, i))
113
+ ] }) : null,
114
+ E ? /* @__PURE__ */ n(g, { textColor: "danger600", children: E }) : null
115
+ ] }) }),
116
+ /* @__PURE__ */ c(m.Footer, { children: [
117
+ /* @__PURE__ */ n(w, { variant: "tertiary", onClick: r, disabled: h, children: b.length > 0 ? "Close" : "Cancel" }),
118
+ /* @__PURE__ */ n(w, { onClick: A, loading: h, children: "Export" })
119
+ ] })
120
+ ] });
121
+ }, T = (e) => e.startsWith("api::"), F = [{ action: `plugin::${s}.export`, subject: null }], B = (e) => e.plugins?.i18n?.locale, N = ({ documents: e, model: t }) => {
122
+ const { allowedActions: o, isLoading: r } = j({ export: F }), [{ query: a }] = I();
123
+ if (r || !o.canExport || !T(t)) return null;
124
+ const l = e.map((u) => u.documentId).filter(Boolean);
125
+ if (l.length === 0) return null;
126
+ const d = l.length;
127
+ return {
128
+ label: "Export",
129
+ icon: /* @__PURE__ */ n(O, {}),
130
+ dialog: {
131
+ type: "modal",
132
+ title: "Export",
133
+ content: ({ onClose: u }) => /* @__PURE__ */ n(
134
+ L,
135
+ {
136
+ uid: t,
137
+ scope: `${d} selected ${d === 1 ? "entry" : "entries"}`,
138
+ request: { documentIds: l, locale: B(a) },
139
+ onClose: u
140
+ }
141
+ )
142
+ }
143
+ };
144
+ }, V = () => {
145
+ const { allowedActions: e, isLoading: t } = j({ export: F }), [{ query: o }] = I(), [r, a] = p.useState(!1), l = window.location.pathname.match(/\/content-manager\/collection-types\/([^/?#]+)/), d = l ? decodeURIComponent(l[1]) : null;
146
+ if (t || !e.canExport || !d || !T(d)) return null;
147
+ const u = o.filters, x = !!(u && Object.keys(u).length > 0);
148
+ return /* @__PURE__ */ c(P, { children: [
149
+ /* @__PURE__ */ n(w, { variant: "tertiary", startIcon: /* @__PURE__ */ n(O, {}), onClick: () => a(!0), children: "Export" }),
150
+ r ? /* @__PURE__ */ n(m.Root, { open: !0, onOpenChange: () => a(!1), children: /* @__PURE__ */ c(m.Content, { children: [
151
+ /* @__PURE__ */ n(m.Header, { children: /* @__PURE__ */ n(m.Title, { children: "Export" }) }),
152
+ /* @__PURE__ */ n(
153
+ L,
154
+ {
155
+ uid: d,
156
+ scope: x ? "everything matching this view" : "every entry of this content type",
157
+ request: { filters: u, sort: o.sort, locale: B(o) },
158
+ onClose: () => a(!1)
159
+ }
160
+ )
161
+ ] }) }) : null
162
+ ] });
163
+ }, Q = {
164
+ register(e) {
165
+ e.registerPlugin({ id: s, name: s, isReady: !0 }), e.addMenuLink({
166
+ to: `/plugins/${s}`,
167
+ icon: M,
168
+ intlLabel: { id: `${s}.menu.label`, defaultMessage: "Ferry" },
169
+ Component: async () => {
170
+ const { TransferPage: t } = await import("./TransferPage-B0Le6wyG.mjs");
171
+ return { default: t };
172
+ },
173
+ permissions: [{ action: `plugin::${s}.export`, subject: null }]
174
+ });
175
+ },
176
+ bootstrap(e) {
177
+ const t = e.getPlugin("content-manager");
178
+ t.apis.addBulkAction((o) => {
179
+ const r = o.findIndex((l) => l.type === "delete"), a = r === -1 ? o.length : r;
180
+ return [...o.slice(0, a), N, ...o.slice(a)];
181
+ }), t.injectComponent("listView", "actions", {
182
+ name: `${s}-export-view`,
183
+ Component: V
184
+ });
185
+ },
186
+ async registerTrads({ locales: e }) {
187
+ return Promise.all(
188
+ e.map(async (t) => {
189
+ try {
190
+ const { default: o } = await D(/* @__PURE__ */ Object.assign({}), `./translations/${t}.json`, 3);
191
+ return { data: o, locale: t };
192
+ } catch {
193
+ return { data: {}, locale: t };
194
+ }
195
+ })
196
+ );
197
+ }
198
+ };
199
+ export {
200
+ v as D,
201
+ s as P,
202
+ J as a,
203
+ G as b,
204
+ Q as i,
205
+ _ as r,
206
+ q as s
207
+ };
@@ -1 +1 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const o=require("@strapi/icons"),a=(r,e,i)=>{const t=r[e];return t?typeof t=="function"?t():Promise.resolve(t):new Promise((l,s)=>{(typeof queueMicrotask=="function"?queueMicrotask:setTimeout)(s.bind(null,new Error("Unknown variable dynamic import: "+e+(e.split("/").length!==i?". Note that variables only represent file names one level deep.":""))))})},n="ferry",u={register(r){r.registerPlugin({id:n,name:n,isReady:!0}),r.addMenuLink({to:`/plugins/${n}`,icon:o.ArrowsHorizontal,intlLabel:{id:`${n}.menu.label`,defaultMessage:"Ferry"},Component:async()=>{const{TransferPage:e}=await Promise.resolve().then(()=>require("../_chunks/TransferPage-Bhp6vkZE.js"));return{default:e}},permissions:[{action:`plugin::${n}.export`,subject:null}]})},async registerTrads({locales:r}){return Promise.all(r.map(async e=>{try{const{default:i}=await a(Object.assign({}),`./translations/${e}.json`,3);return{data:i,locale:e}}catch{return{data:{},locale:e}}}))}};exports.PLUGIN_ID=n;exports.default=u;
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("../_chunks/index-9EKjguHv.js");require("@strapi/icons");exports.PLUGIN_ID=e.PLUGIN_ID;exports.default=e.index;
@@ -1,43 +1,6 @@
1
- import { ArrowsHorizontal as s } from "@strapi/icons";
2
- const o = (n, e, i) => {
3
- const t = n[e];
4
- return t ? typeof t == "function" ? t() : Promise.resolve(t) : new Promise((u, a) => {
5
- (typeof queueMicrotask == "function" ? queueMicrotask : setTimeout)(
6
- a.bind(
7
- null,
8
- new Error(
9
- "Unknown variable dynamic import: " + e + (e.split("/").length !== i ? ". Note that variables only represent file names one level deep." : "")
10
- )
11
- )
12
- );
13
- });
14
- }, r = "ferry", c = {
15
- register(n) {
16
- n.registerPlugin({ id: r, name: r, isReady: !0 }), n.addMenuLink({
17
- to: `/plugins/${r}`,
18
- icon: s,
19
- intlLabel: { id: `${r}.menu.label`, defaultMessage: "Ferry" },
20
- Component: async () => {
21
- const { TransferPage: e } = await import("../_chunks/TransferPage-c8hdFQDQ.mjs");
22
- return { default: e };
23
- },
24
- permissions: [{ action: `plugin::${r}.export`, subject: null }]
25
- });
26
- },
27
- async registerTrads({ locales: n }) {
28
- return Promise.all(
29
- n.map(async (e) => {
30
- try {
31
- const { default: i } = await o(/* @__PURE__ */ Object.assign({}), `./translations/${e}.json`, 3);
32
- return { data: i, locale: e };
33
- } catch {
34
- return { data: {}, locale: e };
35
- }
36
- })
37
- );
38
- }
39
- };
1
+ import { P as t, i as e } from "../_chunks/index-C3-QXVsq.mjs";
2
+ import "@strapi/icons";
40
3
  export {
41
- r as PLUGIN_ID,
42
- c as default
4
+ t as PLUGIN_ID,
5
+ e as default
43
6
  };
@@ -75,7 +75,17 @@ export interface ExportRequest {
75
75
  status: Status;
76
76
  relations: boolean;
77
77
  media: boolean;
78
+ /** Export exactly these documents - the Content Manager's selected rows. */
78
79
  documentIds?: string[];
80
+ /**
81
+ * Strapi filters, passed through untouched - the Content Manager's current
82
+ * view. Ignored when `documentIds` is set, which the server resolves by
83
+ * overwriting `filters.documentId`.
84
+ */
85
+ filters?: Record<string, unknown>;
86
+ /** Which locale to take, when the content type has more than one. */
87
+ locale?: string;
88
+ sort?: string;
79
89
  }
80
90
  export interface ImportRequest {
81
91
  uid: string;
@@ -0,0 +1,21 @@
1
+ import * as React from 'react';
2
+ import { type ExportRequest } from '../api';
3
+ /**
4
+ * The export dialog the Content Manager opens, for either scope.
5
+ *
6
+ * One component for "the rows I ticked" and "everything in this view", because
7
+ * the two differ only in which keys of the request are filled in. What they
8
+ * must not differ in is the answer to "what am I about to get" - a dialog that
9
+ * asks the same questions but means something different each time is worse
10
+ * than two dialogs.
11
+ *
12
+ * Only format and status are offered. The full page keeps the rest: relations
13
+ * and media are defaults nobody changes per export, and a list view is not the
14
+ * place to relearn them.
15
+ */
16
+ export declare const ExportScopeDialog: ({ scope, request, uid, onClose, }: {
17
+ scope: string;
18
+ request: Partial<ExportRequest>;
19
+ uid: string;
20
+ onClose: () => void;
21
+ }) => React.JSX.Element;
@@ -0,0 +1,35 @@
1
+ import * as React from 'react';
2
+ /**
3
+ * Export the ticked rows.
4
+ *
5
+ * A bulk action, so it appears only once something is selected, next to Delete
6
+ * and Publish. That is the shape of the request it answers: "a copy of the
7
+ * twelve articles I just fixed".
8
+ */
9
+ export declare const ExportBulkAction: ({ documents, model }: {
10
+ documents: any[];
11
+ model: string;
12
+ }) => {
13
+ label: string;
14
+ icon: React.JSX.Element;
15
+ dialog: {
16
+ type: "modal";
17
+ title: string;
18
+ content: ({ onClose }: {
19
+ onClose: () => void;
20
+ }) => React.JSX.Element;
21
+ };
22
+ } | null;
23
+ /**
24
+ * Export everything the current filters match.
25
+ *
26
+ * The other half of the same job, and it cannot be the same control: a bulk
27
+ * action exists only while rows are ticked, and ticking is per page. Narrowing
28
+ * a list to three hundred entries and wanting all of them is the case this
29
+ * covers, and there is no way to tick them.
30
+ *
31
+ * Lives in the `listView.actions` injection zone, beside the view settings -
32
+ * always visible, because the filters it follows always exist, even when they
33
+ * are "no filters at all".
34
+ */
35
+ export declare const ExportViewAction: () => React.JSX.Element | null;
@@ -1,5 +1,6 @@
1
1
  declare const _default: {
2
2
  register(app: any): void;
3
+ bootstrap(app: any): void;
3
4
  registerTrads({ locales }: {
4
5
  locales: string[];
5
6
  }): Promise<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qkix/strapi-plugin-ferry",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Import and export Strapi v5 content as JSON or CSV - relations, components and dynamic zones included, with a dry run before anything is written",
5
5
  "strapi": {
6
6
  "kind": "plugin",
@@ -58,8 +58,8 @@
58
58
  "typescript": "^5.8.3",
59
59
  "vitest": "^3.2.4",
60
60
  "@qkix/eslint-config": "0.0.0",
61
- "@qkix/tsconfig": "0.0.0",
62
- "@qkix/strapi-test-harness": "0.0.0"
61
+ "@qkix/strapi-test-harness": "0.0.0",
62
+ "@qkix/tsconfig": "0.0.0"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "@strapi/admin": "^5.0.0",
@@ -1 +0,0 @@
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=_;
@@ -1,398 +0,0 @@
1
- import { jsxs as t, jsx as e } from "react/jsx-runtime";
2
- import * as d from "react";
3
- import { useFetchClient as F, Page as N, Layouts as U } from "@strapi/admin/strapi-admin";
4
- import { Flex as s, Typography as i, Field as p, SingleSelect as k, SingleSelectOption as v, Box as m, Checkbox as D, Button as B, Badge as P, Loader as V } from "@strapi/design-system";
5
- import { Download as A, Upload as J } from "@strapi/icons";
6
- import { PLUGIN_ID as O } from "../admin/index.mjs";
7
- const j = {
8
- contentTypes: `/${O}/content-types`,
9
- plan: (n) => `/${O}/plan/${n}`,
10
- export: `/${O}/export`,
11
- preview: `/${O}/import/preview`,
12
- apply: `/${O}/import`
13
- }, Z = {
14
- format: "json",
15
- status: "draft",
16
- relations: !0,
17
- media: !0
18
- }, _ = {
19
- status: "draft",
20
- onExisting: "update",
21
- onMissingRelation: "skip",
22
- publish: !0,
23
- continueOnError: !1
24
- }, z = (n, h, r) => {
25
- const c = URL.createObjectURL(new Blob([n], { type: r })), u = document.createElement("a");
26
- u.href = c, u.download = h, document.body.appendChild(u), u.click(), u.remove(), URL.revokeObjectURL(c);
27
- }, G = (n) => [
28
- n.created > 0 ? `${n.created} to create` : null,
29
- n.updated > 0 ? `${n.updated} to update` : null,
30
- n.skipped > 0 ? `${n.skipped} skipped` : null,
31
- n.errored > 0 ? `${n.errored} failed` : null
32
- ].filter(Boolean).join(", ") || "nothing to do", H = ({ contentTypes: n }) => {
33
- const { get: h, post: r } = F(), [c, u] = d.useState(n[0]?.uid ?? ""), [g, f] = d.useState(Z), [o, b] = d.useState(null), [w, S] = d.useState(!1), [T, R] = d.useState([]), [I, E] = d.useState(null), [y, L] = d.useState(null);
34
- d.useEffect(() => {
35
- if (!c) return;
36
- let a = !0;
37
- return h(j.plan(c)).then(({ data: $ }) => a && b($)).catch(() => a && b(null)), () => {
38
- a = !1;
39
- };
40
- }, [h, c]);
41
- const l = (a) => f(($) => ({ ...$, ...a })), C = async () => {
42
- S(!0), E(null), R([]), L(null);
43
- try {
44
- const { data: a } = await r(j.export, {
45
- uid: c,
46
- ...g,
47
- envelope: !0
48
- });
49
- z(a.body, a.filename, a.mime), R(a.warnings ?? []), L(`${a.filename} (${a.count} document${a.count === 1 ? "" : "s"})`);
50
- } catch (a) {
51
- E(a?.response?.data?.error?.message ?? "The export failed.");
52
- } finally {
53
- S(!1);
54
- }
55
- }, x = o ? [
56
- `${o.scalars.length} field(s)`,
57
- g.relations && o.relations.length > 0 ? `${o.relations.length} relation(s)` : null,
58
- o.components.length > 0 ? `${o.components.length} component(s)` : null,
59
- o.dynamicZones.length > 0 ? `${o.dynamicZones.length} dynamic zone(s)` : null,
60
- g.media && o.media.length > 0 ? `${o.media.length} media field(s)` : null
61
- ].filter(Boolean) : [];
62
- return /* @__PURE__ */ t(s, { direction: "column", gap: 4, alignItems: "stretch", children: [
63
- /* @__PURE__ */ e(i, { variant: "delta", children: "Export" }),
64
- /* @__PURE__ */ t(p.Root, { name: "export-uid", children: [
65
- /* @__PURE__ */ e(p.Label, { children: "Content type" }),
66
- /* @__PURE__ */ e(k, { value: c, onChange: (a) => u(String(a)), children: n.map((a) => /* @__PURE__ */ e(v, { value: a.uid, children: a.label }, a.uid)) })
67
- ] }),
68
- /* @__PURE__ */ t(s, { gap: 4, wrap: "wrap", alignItems: "flex-end", children: [
69
- /* @__PURE__ */ e(m, { minWidth: "160px", children: /* @__PURE__ */ t(p.Root, { name: "export-format", children: [
70
- /* @__PURE__ */ e(p.Label, { children: "Format" }),
71
- /* @__PURE__ */ t(
72
- k,
73
- {
74
- value: g.format,
75
- onChange: (a) => l({ format: a }),
76
- children: [
77
- /* @__PURE__ */ e(v, { value: "json", children: "JSON - everything" }),
78
- /* @__PURE__ */ e(v, { value: "csv", children: "CSV - flat fields" })
79
- ]
80
- }
81
- )
82
- ] }) }),
83
- /* @__PURE__ */ e(m, { minWidth: "160px", children: /* @__PURE__ */ t(p.Root, { name: "export-status", children: [
84
- /* @__PURE__ */ e(p.Label, { children: "Version" }),
85
- /* @__PURE__ */ t(
86
- k,
87
- {
88
- value: g.status,
89
- onChange: (a) => l({ status: a }),
90
- disabled: o ? !o.draftAndPublish : !1,
91
- children: [
92
- /* @__PURE__ */ e(v, { value: "draft", children: "Draft" }),
93
- /* @__PURE__ */ e(v, { value: "published", children: "Published" })
94
- ]
95
- }
96
- )
97
- ] }) })
98
- ] }),
99
- /* @__PURE__ */ t(s, { gap: 4, wrap: "wrap", children: [
100
- /* @__PURE__ */ e(
101
- D,
102
- {
103
- checked: g.relations,
104
- onCheckedChange: (a) => l({ relations: !!a }),
105
- children: "Relations"
106
- }
107
- ),
108
- /* @__PURE__ */ e(
109
- D,
110
- {
111
- checked: g.media,
112
- onCheckedChange: (a) => l({ media: !!a }),
113
- children: "Media references"
114
- }
115
- )
116
- ] }),
117
- x.length > 0 && /* @__PURE__ */ t(i, { variant: "pi", textColor: "neutral600", children: [
118
- "Carries ",
119
- x.join(", "),
120
- ".",
121
- g.format === "csv" && (o?.components.length || o?.dynamicZones.length) ? " CSV leaves components and dynamic zones behind - use JSON to keep them." : ""
122
- ] }),
123
- /* @__PURE__ */ e(m, { children: /* @__PURE__ */ e(B, { startIcon: /* @__PURE__ */ e(A, {}), onClick: C, loading: w, disabled: !c, children: "Export" }) }),
124
- y && /* @__PURE__ */ t(i, { variant: "pi", textColor: "success600", children: [
125
- "Saved ",
126
- y,
127
- "."
128
- ] }),
129
- T.map((a, $) => /* @__PURE__ */ e(i, { variant: "pi", textColor: "warning700", children: a }, $)),
130
- I && /* @__PURE__ */ e(i, { variant: "pi", textColor: "danger600", children: I })
131
- ] });
132
- }, W = ({ label: n, value: h, tone: r }) => /* @__PURE__ */ t(s, { direction: "column", gap: 1, alignItems: "flex-start", children: [
133
- /* @__PURE__ */ e(i, { variant: "beta", textColor: h > 0 ? r : "neutral500", children: h }),
134
- /* @__PURE__ */ e(i, { variant: "pi", textColor: "neutral600", children: n })
135
- ] }), K = ({ report: n }) => {
136
- const h = n.rows.filter((r) => r.outcome === "error");
137
- return /* @__PURE__ */ t(s, { direction: "column", gap: 4, alignItems: "stretch", children: [
138
- /* @__PURE__ */ t(
139
- m,
140
- {
141
- padding: 4,
142
- hasRadius: !0,
143
- background: "neutral0",
144
- borderColor: "neutral200",
145
- borderWidth: "1px",
146
- borderStyle: "solid",
147
- children: [
148
- /* @__PURE__ */ t(s, { justifyContent: "space-between", alignItems: "center", paddingBottom: 3, children: [
149
- /* @__PURE__ */ e(i, { variant: "delta", children: n.applied ? "Imported" : "Dry run - nothing written yet" }),
150
- /* @__PURE__ */ e(P, { children: G(n) })
151
- ] }),
152
- /* @__PURE__ */ t(s, { gap: 8, wrap: "wrap", children: [
153
- /* @__PURE__ */ e(W, { label: "in the file", value: n.total, tone: "neutral800" }),
154
- /* @__PURE__ */ e(W, { label: "to create", value: n.created, tone: "success600" }),
155
- /* @__PURE__ */ e(W, { label: "to update", value: n.updated, tone: "primary600" }),
156
- /* @__PURE__ */ e(W, { label: "skipped", value: n.skipped, tone: "neutral600" }),
157
- /* @__PURE__ */ e(W, { label: "failed", value: n.errored, tone: "danger600" })
158
- ] })
159
- ]
160
- }
161
- ),
162
- n.warnings.map((r, c) => /* @__PURE__ */ e(
163
- m,
164
- {
165
- padding: 3,
166
- hasRadius: !0,
167
- background: "warning100",
168
- borderColor: "warning200",
169
- borderWidth: "1px",
170
- borderStyle: "solid",
171
- children: /* @__PURE__ */ e(i, { variant: "pi", textColor: "warning700", children: r })
172
- },
173
- c
174
- )),
175
- n.unresolved.length > 0 && /* @__PURE__ */ t(
176
- m,
177
- {
178
- padding: 4,
179
- hasRadius: !0,
180
- background: "neutral0",
181
- borderColor: "warning200",
182
- borderWidth: "1px",
183
- borderStyle: "solid",
184
- children: [
185
- /* @__PURE__ */ e(i, { variant: "delta", children: "Relations that point at nothing" }),
186
- /* @__PURE__ */ e(m, { paddingTop: 1, paddingBottom: 3, children: /* @__PURE__ */ e(i, { 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.' }) }),
187
- /* @__PURE__ */ t(s, { direction: "column", gap: 2, alignItems: "stretch", children: [
188
- n.unresolved.slice(0, 25).map((r, c) => /* @__PURE__ */ t(s, { gap: 3, alignItems: "baseline", wrap: "wrap", children: [
189
- /* @__PURE__ */ t(P, { children: [
190
- "row ",
191
- r.row
192
- ] }),
193
- /* @__PURE__ */ e(i, { variant: "omega", fontWeight: "bold", children: r.field }),
194
- /* @__PURE__ */ e(i, { variant: "pi", textColor: "neutral600", children: r.target }),
195
- /* @__PURE__ */ e(i, { variant: "pi", textColor: "danger600", children: r.keys.join(", ") })
196
- ] }, c)),
197
- n.unresolved.length > 25 && /* @__PURE__ */ t(i, { variant: "pi", textColor: "neutral600", children: [
198
- "and ",
199
- n.unresolved.length - 25,
200
- " more"
201
- ] })
202
- ] })
203
- ]
204
- }
205
- ),
206
- h.length > 0 && /* @__PURE__ */ t(
207
- m,
208
- {
209
- padding: 4,
210
- hasRadius: !0,
211
- background: "neutral0",
212
- borderColor: "danger200",
213
- borderWidth: "1px",
214
- borderStyle: "solid",
215
- children: [
216
- /* @__PURE__ */ e(i, { variant: "delta", textColor: "danger600", children: "Rows that failed" }),
217
- /* @__PURE__ */ e(m, { paddingTop: 2, children: /* @__PURE__ */ e(s, { direction: "column", gap: 2, alignItems: "stretch", children: h.slice(0, 25).map((r) => /* @__PURE__ */ t(s, { gap: 3, alignItems: "baseline", wrap: "wrap", children: [
218
- /* @__PURE__ */ t(P, { children: [
219
- "row ",
220
- r.row
221
- ] }),
222
- /* @__PURE__ */ e(i, { variant: "pi", textColor: "neutral600", children: r.documentId ?? "no documentId" }),
223
- /* @__PURE__ */ e(i, { variant: "pi", textColor: "danger600", children: r.message })
224
- ] }, r.row)) }) })
225
- ]
226
- }
227
- )
228
- ] });
229
- }, X = ({ contentTypes: n }) => {
230
- const { post: h } = F(), [r, c] = d.useState(n[0]?.uid ?? ""), [u, g] = d.useState(_), [f, o] = d.useState(null), [b, w] = d.useState(null), [S, T] = d.useState(!1), [R, I] = d.useState(null), E = d.useRef(null), y = (l) => {
231
- g((C) => ({ ...C, ...l })), w(null);
232
- }, L = async (l) => {
233
- if (!f) return;
234
- T(!0), I(null);
235
- const C = new FormData();
236
- C.append("file", f), C.append("uid", r), Object.entries(u).forEach(([x, a]) => C.append(x, String(a)));
237
- try {
238
- const { data: x } = await h(l ? j.apply : j.preview, C);
239
- w(x);
240
- } catch (x) {
241
- I(x?.response?.data?.error?.message ?? "The file could not be read."), w(null);
242
- } finally {
243
- T(!1);
244
- }
245
- };
246
- return /* @__PURE__ */ t(s, { direction: "column", gap: 4, alignItems: "stretch", children: [
247
- /* @__PURE__ */ e(i, { variant: "delta", children: "Import" }),
248
- /* @__PURE__ */ t(p.Root, { name: "import-uid", children: [
249
- /* @__PURE__ */ e(p.Label, { children: "Content type" }),
250
- /* @__PURE__ */ e(
251
- k,
252
- {
253
- value: r,
254
- onChange: (l) => {
255
- c(String(l)), w(null);
256
- },
257
- children: n.map((l) => /* @__PURE__ */ e(v, { value: l.uid, children: l.label }, l.uid))
258
- }
259
- )
260
- ] }),
261
- /* @__PURE__ */ t(p.Root, { name: "import-file", children: [
262
- /* @__PURE__ */ e(p.Label, { children: "File" }),
263
- /* @__PURE__ */ t(s, { gap: 2, alignItems: "center", children: [
264
- /* @__PURE__ */ e(B, { variant: "secondary", onClick: () => E.current?.click(), children: "Choose a file" }),
265
- /* @__PURE__ */ e(i, { variant: "pi", textColor: f ? "neutral800" : "neutral500", children: f ? f.name : "JSON or CSV" })
266
- ] }),
267
- /* @__PURE__ */ e(
268
- "input",
269
- {
270
- ref: E,
271
- type: "file",
272
- accept: ".json,.csv,application/json,text/csv",
273
- style: { display: "none" },
274
- onChange: (l) => {
275
- o(l.target.files?.[0] ?? null), w(null);
276
- }
277
- }
278
- )
279
- ] }),
280
- /* @__PURE__ */ t(s, { gap: 4, wrap: "wrap", alignItems: "flex-end", children: [
281
- /* @__PURE__ */ e(m, { minWidth: "200px", children: /* @__PURE__ */ t(p.Root, { name: "import-existing", children: [
282
- /* @__PURE__ */ e(p.Label, { children: "When it is already there" }),
283
- /* @__PURE__ */ t(
284
- k,
285
- {
286
- value: u.onExisting,
287
- onChange: (l) => y({ onExisting: l === "skip" ? "skip" : "update" }),
288
- children: [
289
- /* @__PURE__ */ e(v, { value: "update", children: "Update it" }),
290
- /* @__PURE__ */ e(v, { value: "skip", children: "Leave it alone" })
291
- ]
292
- }
293
- )
294
- ] }) }),
295
- /* @__PURE__ */ e(m, { minWidth: "240px", children: /* @__PURE__ */ t(p.Root, { name: "import-missing", children: [
296
- /* @__PURE__ */ e(p.Label, { children: "When a relation points at nothing" }),
297
- /* @__PURE__ */ t(
298
- k,
299
- {
300
- value: u.onMissingRelation,
301
- onChange: (l) => y({ onMissingRelation: l === "fail" ? "fail" : "skip" }),
302
- children: [
303
- /* @__PURE__ */ e(v, { value: "skip", children: "Drop the link, keep the row" }),
304
- /* @__PURE__ */ e(v, { value: "fail", children: "Refuse the import" })
305
- ]
306
- }
307
- )
308
- ] }) }),
309
- /* @__PURE__ */ e(m, { minWidth: "160px", children: /* @__PURE__ */ t(p.Root, { name: "import-status", children: [
310
- /* @__PURE__ */ e(p.Label, { children: "Import as" }),
311
- /* @__PURE__ */ t(
312
- k,
313
- {
314
- value: u.status,
315
- onChange: (l) => y({ status: l }),
316
- children: [
317
- /* @__PURE__ */ e(v, { value: "draft", children: "Draft" }),
318
- /* @__PURE__ */ e(v, { value: "published", children: "Published" })
319
- ]
320
- }
321
- )
322
- ] }) })
323
- ] }),
324
- /* @__PURE__ */ e(
325
- D,
326
- {
327
- checked: u.continueOnError,
328
- onCheckedChange: (l) => y({ continueOnError: !!l }),
329
- children: "Keep going past a row that fails"
330
- }
331
- ),
332
- /* @__PURE__ */ e(i, { variant: "pi", textColor: "neutral600", children: u.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." }),
333
- /* @__PURE__ */ t(s, { gap: 2, children: [
334
- /* @__PURE__ */ e(
335
- B,
336
- {
337
- variant: "secondary",
338
- startIcon: /* @__PURE__ */ e(J, {}),
339
- onClick: () => L(!1),
340
- loading: S,
341
- disabled: !f || !r,
342
- children: "Dry run"
343
- }
344
- ),
345
- /* @__PURE__ */ e(
346
- B,
347
- {
348
- onClick: () => L(!0),
349
- loading: S,
350
- disabled: !f || !r || !b || b.applied,
351
- children: "Import for real"
352
- }
353
- )
354
- ] }),
355
- !b && f && /* @__PURE__ */ e(i, { variant: "pi", textColor: "neutral600", children: "Run the dry run first. Nothing is written until you have seen what it would do." }),
356
- R && /* @__PURE__ */ e(i, { variant: "pi", textColor: "danger600", children: R }),
357
- b && /* @__PURE__ */ e(K, { report: b })
358
- ] });
359
- }, M = ({ children: n }) => /* @__PURE__ */ e(
360
- m,
361
- {
362
- padding: 6,
363
- hasRadius: !0,
364
- background: "neutral0",
365
- borderColor: "neutral200",
366
- borderWidth: "1px",
367
- borderStyle: "solid",
368
- flex: "1",
369
- minWidth: "380px",
370
- children: n
371
- }
372
- ), te = () => {
373
- const { get: n } = F(), [h, r] = d.useState([]), [c, u] = d.useState(!0), [g, f] = d.useState(null);
374
- return d.useEffect(() => {
375
- let o = !0;
376
- return n(j.contentTypes).then(({ data: b }) => o && r(b.contentTypes)).catch(() => o && f("Could not read the list of content types.")).finally(() => o && u(!1)), () => {
377
- o = !1;
378
- };
379
- }, [n]), /* @__PURE__ */ t(N.Main, { children: [
380
- /* @__PURE__ */ e(
381
- U.Header,
382
- {
383
- title: "Ferry",
384
- subtitle: "Move content in and out as JSON or CSV, with relations and components intact"
385
- }
386
- ),
387
- /* @__PURE__ */ e(U.Content, { children: c ? /* @__PURE__ */ e(s, { justifyContent: "center", padding: 10, children: /* @__PURE__ */ e(V, { small: !0, children: "Reading the schema" }) }) : g ? /* @__PURE__ */ e(i, { textColor: "danger600", children: g }) : h.length === 0 ? /* @__PURE__ */ t(s, { direction: "column", gap: 2, padding: 10, alignItems: "center", children: [
388
- /* @__PURE__ */ e(i, { variant: "delta", children: "Nothing to carry yet" }),
389
- /* @__PURE__ */ e(i, { textColor: "neutral600", children: "Create a content type, and it will show up here." })
390
- ] }) : /* @__PURE__ */ t(s, { gap: 6, alignItems: "flex-start", wrap: "wrap", children: [
391
- /* @__PURE__ */ e(M, { children: /* @__PURE__ */ e(H, { contentTypes: h }) }),
392
- /* @__PURE__ */ e(M, { children: /* @__PURE__ */ e(X, { contentTypes: h }) })
393
- ] }) })
394
- ] });
395
- };
396
- export {
397
- te as TransferPage
398
- };