strapi-plugin-copy-any-component 1.0.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.
@@ -0,0 +1,2 @@
1
+ export const PLUGIN_ID = "copy-any-component";
2
+
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "strapi-plugin-copy-any-component",
3
+ "version": "1.0.0",
4
+ "description": "A powerful Strapi plugin that allows you to copy and reorder components (sections) between pages using an intuitive drag-and-drop interface. Works with any content type and dynamic zone - no code required!",
5
+ "keywords": [
6
+ "strapi",
7
+ "strapi-plugin",
8
+ "component",
9
+ "copy",
10
+ "drag-and-drop",
11
+ "dynamic-zone",
12
+ "content-management",
13
+ "cms",
14
+ "strapi-v5"
15
+ ],
16
+ "strapi": {
17
+ "name": "copy-any-component",
18
+ "description": "A powerful Strapi plugin that allows you to copy and reorder components between pages",
19
+ "kind": "plugin",
20
+ "displayName": "Copy Any Component"
21
+ },
22
+ "main": "./strapi-server.js",
23
+ "exports": {
24
+ "./strapi-admin": "./strapi-admin.js",
25
+ "./strapi-server": "./strapi-server.js"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/metehankasapp/strapi-plugin-copy-any-component.git"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/metehankasapp/strapi-plugin-copy-any-component/issues"
33
+ },
34
+ "homepage": "https://github.com/metehankasapp/strapi-plugin-copy-any-component#readme",
35
+ "files": [
36
+ "strapi-admin.js",
37
+ "strapi-server.js",
38
+ "admin",
39
+ "server",
40
+ "README.md",
41
+ "INSTALLATION.md",
42
+ "USAGE.md",
43
+ "QUICK_START.md",
44
+ "TESTING.md"
45
+ ],
46
+ "scripts": {
47
+ "test": "echo \"Error: no test specified\" && exit 1"
48
+ },
49
+ "dependencies": {},
50
+ "peerDependencies": {
51
+ "@strapi/strapi": "^5.0.0"
52
+ },
53
+ "author": {
54
+ "name": "Your Name",
55
+ "email": "your.email@example.com"
56
+ },
57
+ "license": "MIT",
58
+ "engines": {
59
+ "node": ">=18.0.0 <=20.x.x",
60
+ "npm": ">=6.0.0"
61
+ }
62
+ }
@@ -0,0 +1,22 @@
1
+ import type { Core } from "@strapi/strapi";
2
+
3
+ const controller = ({ strapi }: { strapi: Core.Strapi }) => ({
4
+ async hello(ctx) {
5
+ ctx.body = {
6
+ message: "Hello from my plugin! 👋",
7
+ timestamp: new Date().toISOString(),
8
+ plugin: "copy-any-component",
9
+ };
10
+ },
11
+
12
+ async greet(ctx) {
13
+ const { name } = ctx.params;
14
+ ctx.body = {
15
+ message: `Merhaba, ${name}! 🎉`,
16
+ timestamp: new Date().toISOString(),
17
+ };
18
+ },
19
+ });
20
+
21
+ export default controller;
22
+
@@ -0,0 +1,5 @@
1
+ import controller from "./controller";
2
+
3
+ export default {
4
+ controller,
5
+ };
@@ -0,0 +1,13 @@
1
+ import controllers from "./controllers";
2
+ import routes from "./routes";
3
+
4
+ export default {
5
+ register({ strapi }) {
6
+ // Register plugin
7
+ },
8
+ bootstrap({ strapi }) {
9
+ strapi.log.info("🚀 My Simple Plugin loaded successfully!");
10
+ },
11
+ controllers,
12
+ routes,
13
+ };
@@ -0,0 +1,11 @@
1
+ export default [
2
+ {
3
+ method: "GET",
4
+ path: "/hello",
5
+ handler: "controller.hello",
6
+ config: {
7
+ policies: [],
8
+ },
9
+ },
10
+ ];
11
+
@@ -0,0 +1,19 @@
1
+ export default [
2
+ {
3
+ method: "GET",
4
+ path: "/hello",
5
+ handler: "controller.hello",
6
+ config: {
7
+ policies: [],
8
+ },
9
+ },
10
+ {
11
+ method: "GET",
12
+ path: "/greet/:name",
13
+ handler: "controller.greet",
14
+ config: {
15
+ policies: [],
16
+ },
17
+ },
18
+ ];
19
+
@@ -0,0 +1,13 @@
1
+ import contentApi from "./content-api";
2
+ import admin from "./admin";
3
+
4
+ export default {
5
+ "content-api": {
6
+ type: "content-api",
7
+ routes: [...contentApi],
8
+ },
9
+ admin: {
10
+ type: "admin",
11
+ routes: [...admin],
12
+ },
13
+ };
@@ -0,0 +1,439 @@
1
+ "use strict";
2
+
3
+ const service = ({ strapi }) => {
4
+ // Get plugin configuration with defaults
5
+ const getConfig = () => {
6
+ const pluginConfig = strapi.config.get('plugin::copy-any-component') || {};
7
+ return {
8
+ contentType: pluginConfig.contentType || 'api::page.page',
9
+ dynamicZoneField: pluginConfig.dynamicZoneField || 'sections',
10
+ };
11
+ };
12
+
13
+ const deepCloneSections = (sections) => {
14
+ return JSON.parse(JSON.stringify(sections));
15
+ };
16
+
17
+ const isMediaField = (obj) => {
18
+ if (obj && typeof obj === 'object') {
19
+ // If it has __component property, it's a component, not media
20
+ if (obj.__component !== undefined) {
21
+ return false;
22
+ }
23
+ // Media fields have specific properties that distinguish them
24
+ return (
25
+ obj.mime !== undefined ||
26
+ obj.url !== undefined ||
27
+ obj.formats !== undefined ||
28
+ obj.provider !== undefined ||
29
+ (obj.id !== undefined && obj.hash !== undefined && (obj.name !== undefined || obj.alternativeText !== undefined))
30
+ );
31
+ }
32
+ return false;
33
+ };
34
+
35
+ const cleanComponent = (component) => {
36
+ if (!component || typeof component !== 'object') {
37
+ return component;
38
+ }
39
+
40
+ const cleaned = {};
41
+
42
+ if (component.__component) {
43
+ cleaned.__component = component.__component;
44
+ }
45
+
46
+ const fieldsToRemove = ['id', 'documentId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy', 'publishedAt', 'locale'];
47
+
48
+ for (const key in component) {
49
+ if (fieldsToRemove.includes(key) || key === '__component') {
50
+ continue;
51
+ }
52
+
53
+ const value = component[key];
54
+
55
+ if (Array.isArray(value)) {
56
+ cleaned[key] = value.map(item => {
57
+ if (item && typeof item === 'object') {
58
+ // Check if it's a component first (components always have __component property)
59
+ if (item.__component !== undefined) {
60
+ return cleanComponent(item);
61
+ }
62
+ // If it's a media field, keep it as is (just remove entity-specific fields)
63
+ if (isMediaField(item)) {
64
+ const cleanedMedia = {};
65
+ if (item.id !== undefined) cleanedMedia.id = item.id;
66
+ if (item.name !== undefined) cleanedMedia.name = item.name;
67
+ if (item.alternativeText !== undefined) cleanedMedia.alternativeText = item.alternativeText;
68
+ if (item.caption !== undefined) cleanedMedia.caption = item.caption;
69
+ if (item.width !== undefined) cleanedMedia.width = item.width;
70
+ if (item.height !== undefined) cleanedMedia.height = item.height;
71
+ if (item.formats !== undefined) cleanedMedia.formats = item.formats;
72
+ if (item.hash !== undefined) cleanedMedia.hash = item.hash;
73
+ if (item.ext !== undefined) cleanedMedia.ext = item.ext;
74
+ if (item.mime !== undefined) cleanedMedia.mime = item.mime;
75
+ if (item.size !== undefined) cleanedMedia.size = item.size;
76
+ if (item.url !== undefined) cleanedMedia.url = item.url;
77
+ if (item.previewUrl !== undefined) cleanedMedia.previewUrl = item.previewUrl;
78
+ if (item.provider !== undefined) cleanedMedia.provider = item.provider;
79
+ if (item.provider_metadata !== undefined) cleanedMedia.provider_metadata = item.provider_metadata;
80
+ return cleanedMedia;
81
+ }
82
+ // Plain object - just clean it recursively
83
+ return cleanComponent(item);
84
+ }
85
+ return item;
86
+ });
87
+ } else if (value && typeof value === 'object') {
88
+ // Check if it's a component first (components always have __component property)
89
+ if (value.__component !== undefined) {
90
+ cleaned[key] = cleanComponent(value);
91
+ } else if (isMediaField(value)) {
92
+ const cleanedMedia = {};
93
+ if (value.id !== undefined) cleanedMedia.id = value.id;
94
+ if (value.name !== undefined) cleanedMedia.name = value.name;
95
+ if (value.alternativeText !== undefined) cleanedMedia.alternativeText = value.alternativeText;
96
+ if (value.caption !== undefined) cleanedMedia.caption = value.caption;
97
+ if (value.width !== undefined) cleanedMedia.width = value.width;
98
+ if (value.height !== undefined) cleanedMedia.height = value.height;
99
+ if (value.formats !== undefined) cleanedMedia.formats = value.formats;
100
+ if (value.hash !== undefined) cleanedMedia.hash = value.hash;
101
+ if (value.ext !== undefined) cleanedMedia.ext = value.ext;
102
+ if (value.mime !== undefined) cleanedMedia.mime = value.mime;
103
+ if (value.size !== undefined) cleanedMedia.size = value.size;
104
+ if (value.url !== undefined) cleanedMedia.url = value.url;
105
+ if (value.previewUrl !== undefined) cleanedMedia.previewUrl = value.previewUrl;
106
+ if (value.provider !== undefined) cleanedMedia.provider = value.provider;
107
+ if (value.provider_metadata !== undefined) cleanedMedia.provider_metadata = value.provider_metadata;
108
+ cleaned[key] = cleanedMedia;
109
+ } else {
110
+ // Plain object - just clean it recursively
111
+ cleaned[key] = cleanComponent(value);
112
+ }
113
+ } else {
114
+ cleaned[key] = value;
115
+ }
116
+ }
117
+
118
+ return cleaned;
119
+ };
120
+
121
+ // Deep populate helper - Strapi için doğru format
122
+ const buildDeepPopulate = () => {
123
+ // Strapi'de dynamic zone için en etkili populate
124
+ return {
125
+ populate: '*'
126
+ };
127
+ };
128
+
129
+ // Content type'dan display field'ı bul (title, name, heading vb.)
130
+ const getDisplayField = (page) => {
131
+ // Öncelik sırasına göre display field'ları kontrol et
132
+ const possibleFields = ['title', 'name', 'heading', 'label', 'displayName', 'slug'];
133
+ for (const field of possibleFields) {
134
+ if (page[field]) return page[field];
135
+ }
136
+ return `ID: ${page.id}`;
137
+ };
138
+
139
+ const getPageSections = async (pageId) => {
140
+ try {
141
+ const config = getConfig();
142
+ const page = await strapi.entityService.findOne(config.contentType, pageId, {
143
+ populate: {
144
+ [config.dynamicZoneField]: buildDeepPopulate(),
145
+ },
146
+ });
147
+
148
+ if (!page) {
149
+ return { error: "Page not found", data: null };
150
+ }
151
+
152
+ return {
153
+ error: null,
154
+ data: {
155
+ pageId: page.id,
156
+ pageTitle: getDisplayField(page),
157
+ sections: page[config.dynamicZoneField] || [],
158
+ },
159
+ };
160
+ } catch (error) {
161
+ return { error: error.message, data: null };
162
+ }
163
+ };
164
+
165
+ const analyzeComponentFields = (originalComponent, cleanedComponent) => {
166
+ const fields = [];
167
+ const mediaFields = [];
168
+ const removedFields = [];
169
+
170
+ const fieldsToRemove = ['id', 'documentId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy', 'publishedAt', 'locale'];
171
+
172
+ // Check for removed fields at root level
173
+ for (const field of fieldsToRemove) {
174
+ if (originalComponent[field] !== undefined && cleanedComponent[field] === undefined) {
175
+ removedFields.push({
176
+ path: field,
177
+ type: typeof originalComponent[field],
178
+ value: originalComponent[field],
179
+ reason: 'System field (automatically removed)'
180
+ });
181
+ }
182
+ }
183
+
184
+ const traverse = (original, cleaned, path = '') => {
185
+ if (!original || typeof original !== 'object') {
186
+ return;
187
+ }
188
+
189
+ for (const key in original) {
190
+ if (key === '__component' || key.startsWith('_')) {
191
+ continue;
192
+ }
193
+
194
+ const currentPath = path ? `${path}.${key}` : key;
195
+ const originalValue = original[key];
196
+ const cleanedValue = cleaned && typeof cleaned === 'object' ? cleaned[key] : undefined;
197
+
198
+ if (originalValue === null || originalValue === undefined) {
199
+ continue;
200
+ }
201
+
202
+ // Check if field was removed
203
+ if (cleanedValue === undefined && originalValue !== undefined) {
204
+ removedFields.push({
205
+ path: currentPath,
206
+ type: typeof originalValue,
207
+ value: Array.isArray(originalValue) ? `Array(${originalValue.length})` :
208
+ typeof originalValue === 'object' ? 'Object' : originalValue,
209
+ reason: 'Field removed during copy'
210
+ });
211
+ continue;
212
+ }
213
+
214
+ if (Array.isArray(originalValue)) {
215
+ if (originalValue.length > 0) {
216
+ if (originalValue[0] && typeof originalValue[0] === 'object' && originalValue[0].id !== undefined) {
217
+ if (originalValue[0].mime || originalValue[0].url || originalValue[0].formats) {
218
+ mediaFields.push({
219
+ path: currentPath,
220
+ count: originalValue.length,
221
+ items: originalValue.map(item => ({
222
+ id: item.id,
223
+ name: item.name || item.alternativeText || 'Media',
224
+ mime: item.mime,
225
+ url: item.url
226
+ }))
227
+ });
228
+ } else {
229
+ fields.push({ path: currentPath, type: 'array', count: originalValue.length });
230
+ if (cleanedValue && Array.isArray(cleanedValue)) {
231
+ originalValue.forEach((item, idx) => {
232
+ if (item && typeof item === 'object' && cleanedValue[idx]) {
233
+ traverse(item, cleanedValue[idx], `${currentPath}[${idx}]`);
234
+ }
235
+ });
236
+ }
237
+ }
238
+ } else {
239
+ fields.push({ path: currentPath, type: 'array', count: originalValue.length, sample: originalValue[0] });
240
+ }
241
+ }
242
+ } else if (typeof originalValue === 'object') {
243
+ if (originalValue.id !== undefined) {
244
+ if (originalValue.mime || originalValue.url || originalValue.formats) {
245
+ mediaFields.push({
246
+ path: currentPath,
247
+ count: 1,
248
+ items: [{
249
+ id: originalValue.id,
250
+ name: originalValue.name || originalValue.alternativeText || 'Media',
251
+ mime: originalValue.mime,
252
+ url: originalValue.url
253
+ }]
254
+ });
255
+ } else {
256
+ fields.push({ path: currentPath, type: 'object', value: originalValue.id });
257
+ if (cleanedValue && typeof cleanedValue === 'object') {
258
+ traverse(originalValue, cleanedValue, currentPath);
259
+ }
260
+ }
261
+ } else {
262
+ fields.push({ path: currentPath, type: 'object' });
263
+ if (cleanedValue && typeof cleanedValue === 'object') {
264
+ traverse(originalValue, cleanedValue, currentPath);
265
+ }
266
+ }
267
+ } else {
268
+ const type = typeof originalValue;
269
+ fields.push({
270
+ path: currentPath,
271
+ type: type,
272
+ value: type === 'string' && originalValue.length > 50 ? originalValue.substring(0, 50) + '...' : originalValue
273
+ });
274
+ }
275
+ }
276
+ };
277
+
278
+ traverse(originalComponent, cleanedComponent);
279
+
280
+ return { fields, mediaFields, removedFields };
281
+ };
282
+
283
+ const copySectionsToPage = async (sourcePageId, targetPageId, sectionIndices = null, insertIndex = null) => {
284
+ try {
285
+ const sourceResult = await getPageSections(sourcePageId);
286
+ if (sourceResult.error) {
287
+ return sourceResult;
288
+ }
289
+
290
+ const sourceSections = sourceResult.data.sections || [];
291
+
292
+ if (sourceSections.length === 0) {
293
+ return { error: "No sections found in source page", data: null };
294
+ }
295
+
296
+ let sectionsToCopy = sourceSections;
297
+ if (sectionIndices && Array.isArray(sectionIndices) && sectionIndices.length > 0) {
298
+ sectionsToCopy = sectionIndices
299
+ .map((index) => sourceSections[index])
300
+ .filter((section) => section !== undefined);
301
+
302
+ if (sectionsToCopy.length === 0) {
303
+ return { error: "Selected sections not found", data: null };
304
+ }
305
+ }
306
+
307
+ const clonedSections = deepCloneSections(sectionsToCopy);
308
+ const cleanedSections = clonedSections.map(section => cleanComponent(section));
309
+
310
+ // Analyze copied sections - compare original with cleaned
311
+ const copiedDetails = cleanedSections.map((section, idx) => {
312
+ const originalSection = sectionsToCopy[idx];
313
+ const analysis = analyzeComponentFields(originalSection, section);
314
+ return {
315
+ index: sectionIndices ? sectionIndices[idx] : idx,
316
+ componentType: section.__component || 'unknown',
317
+ fields: analysis.fields,
318
+ mediaFields: analysis.mediaFields,
319
+ removedFields: analysis.removedFields,
320
+ totalFields: analysis.fields.length,
321
+ totalMedia: analysis.mediaFields.reduce((sum, m) => sum + m.count, 0),
322
+ totalRemoved: analysis.removedFields.length
323
+ };
324
+ });
325
+
326
+ const config = getConfig();
327
+ const targetPage = await strapi.entityService.findOne(
328
+ config.contentType,
329
+ targetPageId,
330
+ {
331
+ populate: {
332
+ [config.dynamicZoneField]: buildDeepPopulate(),
333
+ },
334
+ }
335
+ );
336
+
337
+ if (!targetPage) {
338
+ return { error: "Target page not found", data: null };
339
+ }
340
+
341
+ const existingSections = targetPage[config.dynamicZoneField] || [];
342
+
343
+ // If insertIndex is specified and valid, insert at that position
344
+ // Otherwise, append to the end
345
+ let updatedSections;
346
+ if (insertIndex !== null && insertIndex !== undefined && typeof insertIndex === 'number' && insertIndex >= 0 && insertIndex <= existingSections.length) {
347
+ updatedSections = [...existingSections];
348
+ updatedSections.splice(insertIndex, 0, ...cleanedSections);
349
+ } else {
350
+ updatedSections = [...existingSections, ...cleanedSections];
351
+ }
352
+
353
+ const updatedPage = await strapi.entityService.update(
354
+ config.contentType,
355
+ targetPageId,
356
+ {
357
+ data: {
358
+ [config.dynamicZoneField]: updatedSections,
359
+ },
360
+ }
361
+ );
362
+
363
+ return {
364
+ error: null,
365
+ data: {
366
+ targetPageId: updatedPage.id,
367
+ targetPageTitle: getDisplayField(updatedPage),
368
+ copiedSectionsCount: cleanedSections.length,
369
+ totalSections: updatedSections.length,
370
+ copiedDetails: copiedDetails,
371
+ },
372
+ };
373
+ } catch (error) {
374
+ return { error: error.message || "Unknown error", data: null };
375
+ }
376
+ };
377
+
378
+ const moveSectionsToPage = async (sourcePageId, targetPageId, sectionIndices = null) => {
379
+ try {
380
+ const copyResult = await copySectionsToPage(
381
+ sourcePageId,
382
+ targetPageId,
383
+ sectionIndices
384
+ );
385
+
386
+ if (copyResult.error) {
387
+ return copyResult;
388
+ }
389
+
390
+ const moveConfig = getConfig();
391
+ const sourcePage = await strapi.entityService.findOne(
392
+ moveConfig.contentType,
393
+ sourcePageId,
394
+ {
395
+ populate: [moveConfig.dynamicZoneField],
396
+ }
397
+ );
398
+
399
+ if (!sourcePage) {
400
+ return { error: "Source page not found", data: null };
401
+ }
402
+
403
+ let updatedSections = sourcePage[moveConfig.dynamicZoneField] || [];
404
+
405
+ if (sectionIndices && Array.isArray(sectionIndices)) {
406
+ const sortedIndices = [...sectionIndices].sort((a, b) => b - a);
407
+ sortedIndices.forEach((index) => {
408
+ updatedSections.splice(index, 1);
409
+ });
410
+ } else {
411
+ updatedSections = [];
412
+ }
413
+
414
+ await strapi.entityService.update(moveConfig.contentType, sourcePageId, {
415
+ data: {
416
+ [moveConfig.dynamicZoneField]: updatedSections,
417
+ },
418
+ });
419
+
420
+ return {
421
+ error: null,
422
+ data: {
423
+ ...copyResult.data,
424
+ moved: true,
425
+ },
426
+ };
427
+ } catch (error) {
428
+ return { error: error.message, data: null };
429
+ }
430
+ };
431
+
432
+ return {
433
+ getPageSections,
434
+ copySectionsToPage,
435
+ moveSectionsToPage,
436
+ };
437
+ };
438
+
439
+ module.exports = service;
@@ -0,0 +1,2 @@
1
+ export { default } from "./admin/src";
2
+