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.
- package/INSTALLATION.md +179 -0
- package/QUICK_START.md +88 -0
- package/README.md +165 -0
- package/TESTING.md +193 -0
- package/USAGE.md +121 -0
- package/admin/src/components/ComponentCopyField.jsx +3 -0
- package/admin/src/components/Initializer.jsx +22 -0
- package/admin/src/components/PluginIcon.jsx +22 -0
- package/admin/src/index.js +27 -0
- package/admin/src/pages/HomePage.jsx +995 -0
- package/admin/src/pluginId.js +2 -0
- package/package.json +62 -0
- package/server/src/controllers/controller.ts +22 -0
- package/server/src/controllers/index.ts +5 -0
- package/server/src/index.ts +13 -0
- package/server/src/routes/admin.ts +11 -0
- package/server/src/routes/content-api.ts +19 -0
- package/server/src/routes/index.ts +13 -0
- package/server/src/services/component-copy.js +439 -0
- package/strapi-admin.js +2 -0
- package/strapi-server.js +685 -0
|
@@ -0,0 +1,995 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Main,
|
|
4
|
+
Box,
|
|
5
|
+
SingleSelect,
|
|
6
|
+
SingleSelectOption,
|
|
7
|
+
Typography,
|
|
8
|
+
Alert,
|
|
9
|
+
Flex,
|
|
10
|
+
Badge,
|
|
11
|
+
Button,
|
|
12
|
+
Card,
|
|
13
|
+
Grid,
|
|
14
|
+
IconButton,
|
|
15
|
+
} from "@strapi/design-system";
|
|
16
|
+
import { Drag, Cross } from "@strapi/icons";
|
|
17
|
+
import { Page } from "@strapi/strapi/admin";
|
|
18
|
+
import { useFetchClient } from "@strapi/strapi/admin";
|
|
19
|
+
import { PLUGIN_ID } from "../pluginId";
|
|
20
|
+
|
|
21
|
+
const HomePage = () => {
|
|
22
|
+
const [pages, setPages] = useState([]);
|
|
23
|
+
const [sourcePageId, setSourcePageId] = useState("");
|
|
24
|
+
const [targetPageId, setTargetPageId] = useState("");
|
|
25
|
+
const [sourceSections, setSourceSections] = useState([]);
|
|
26
|
+
const [targetSections, setTargetSections] = useState([]);
|
|
27
|
+
const [loading, setLoading] = useState(false);
|
|
28
|
+
const [loadingSourceSections, setLoadingSourceSections] = useState(false);
|
|
29
|
+
const [loadingTargetSections, setLoadingTargetSections] = useState(false);
|
|
30
|
+
const [message, setMessage] = useState(null);
|
|
31
|
+
const [draggedIndex, setDraggedIndex] = useState(null);
|
|
32
|
+
const [draggedSource, setDraggedSource] = useState(null);
|
|
33
|
+
const [isDragOver, setIsDragOver] = useState(false);
|
|
34
|
+
const [dragOverTargetIndex, setDragOverTargetIndex] = useState(null);
|
|
35
|
+
const [copyDetails, setCopyDetails] = useState(null);
|
|
36
|
+
const [showDetailsModal, setShowDetailsModal] = useState(false);
|
|
37
|
+
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
38
|
+
const [publishing, setPublishing] = useState(false);
|
|
39
|
+
|
|
40
|
+
// Content Type yapılandırması
|
|
41
|
+
const [contentTypes, setContentTypes] = useState([]);
|
|
42
|
+
const [currentConfig, setCurrentConfig] = useState({ contentType: '', dynamicZoneField: '' });
|
|
43
|
+
const [selectedContentType, setSelectedContentType] = useState('');
|
|
44
|
+
const [selectedDynamicZone, setSelectedDynamicZone] = useState('');
|
|
45
|
+
const [showSettings, setShowSettings] = useState(false);
|
|
46
|
+
const [configLoading, setConfigLoading] = useState(false);
|
|
47
|
+
|
|
48
|
+
const { get, post, put } = useFetchClient();
|
|
49
|
+
|
|
50
|
+
// Content type'ları yükle
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const fetchContentTypes = async () => {
|
|
53
|
+
try {
|
|
54
|
+
const { data } = await get(`/${PLUGIN_ID}/content-types`);
|
|
55
|
+
if (data?.data) {
|
|
56
|
+
setContentTypes(data.data.contentTypes || []);
|
|
57
|
+
setCurrentConfig(data.data.currentConfig || {});
|
|
58
|
+
setSelectedContentType(data.data.currentConfig?.contentType || '');
|
|
59
|
+
setSelectedDynamicZone(data.data.currentConfig?.dynamicZoneField || '');
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error("Failed to load content types:", error);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
fetchContentTypes();
|
|
66
|
+
}, [get]);
|
|
67
|
+
|
|
68
|
+
// Sayfaları yükle
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const fetchPages = async () => {
|
|
71
|
+
try {
|
|
72
|
+
const { data } = await get(`/${PLUGIN_ID}/pages`);
|
|
73
|
+
setPages(data?.data || []);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
setMessage({ type: "danger", text: "Failed to load pages: " + (error.message || "Unknown error") });
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
fetchPages();
|
|
79
|
+
}, [get, currentConfig]);
|
|
80
|
+
|
|
81
|
+
// Content type yapılandırmasını güncelle
|
|
82
|
+
const handleConfigUpdate = async () => {
|
|
83
|
+
if (!selectedContentType || !selectedDynamicZone) {
|
|
84
|
+
setMessage({ type: "warning", text: "Lütfen content type ve dynamic zone seçin" });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setConfigLoading(true);
|
|
89
|
+
try {
|
|
90
|
+
const { data } = await put(`/${PLUGIN_ID}/config`, {
|
|
91
|
+
contentType: selectedContentType,
|
|
92
|
+
dynamicZoneField: selectedDynamicZone,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (data?.data) {
|
|
96
|
+
setCurrentConfig({
|
|
97
|
+
contentType: selectedContentType,
|
|
98
|
+
dynamicZoneField: selectedDynamicZone,
|
|
99
|
+
});
|
|
100
|
+
setMessage({ type: "success", text: "Yapılandırma güncellendi! Sayfalar yeniden yükleniyor..." });
|
|
101
|
+
|
|
102
|
+
// Sayfaları yeniden yükle
|
|
103
|
+
const pagesRes = await get(`/${PLUGIN_ID}/pages`);
|
|
104
|
+
setPages(pagesRes.data?.data || []);
|
|
105
|
+
setSourcePageId("");
|
|
106
|
+
setTargetPageId("");
|
|
107
|
+
setSourceSections([]);
|
|
108
|
+
setTargetSections([]);
|
|
109
|
+
setShowSettings(false);
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
setMessage({ type: "danger", text: "Yapılandırma güncellenemedi: " + (error.message || "Bilinmeyen hata") });
|
|
113
|
+
} finally {
|
|
114
|
+
setConfigLoading(false);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Seçilen content type'a göre dynamic zone'ları getir
|
|
119
|
+
const getAvailableDynamicZones = () => {
|
|
120
|
+
const ct = contentTypes.find(c => c.uid === selectedContentType);
|
|
121
|
+
return ct?.dynamicZones || [];
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (sourcePageId) {
|
|
126
|
+
loadSourceSections();
|
|
127
|
+
} else {
|
|
128
|
+
setSourceSections([]);
|
|
129
|
+
}
|
|
130
|
+
}, [sourcePageId]);
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (targetPageId) {
|
|
134
|
+
loadTargetSections();
|
|
135
|
+
setHasUnsavedChanges(false);
|
|
136
|
+
} else {
|
|
137
|
+
setTargetSections([]);
|
|
138
|
+
setHasUnsavedChanges(false);
|
|
139
|
+
}
|
|
140
|
+
}, [targetPageId]);
|
|
141
|
+
|
|
142
|
+
const loadSourceSections = useCallback(async () => {
|
|
143
|
+
setLoadingSourceSections(true);
|
|
144
|
+
try {
|
|
145
|
+
const { data } = await get(
|
|
146
|
+
`/${PLUGIN_ID}/pages/${encodeURIComponent(sourcePageId)}/sections`
|
|
147
|
+
);
|
|
148
|
+
if (data.error) {
|
|
149
|
+
setMessage({ type: "danger", text: data.error });
|
|
150
|
+
setSourceSections([]);
|
|
151
|
+
} else {
|
|
152
|
+
setSourceSections(data.data.sections || []);
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
setMessage({ type: "danger", text: "Failed to load sections: " + (error.message || "Unknown error") });
|
|
156
|
+
setSourceSections([]);
|
|
157
|
+
} finally {
|
|
158
|
+
setLoadingSourceSections(false);
|
|
159
|
+
}
|
|
160
|
+
}, [sourcePageId, get]);
|
|
161
|
+
|
|
162
|
+
const loadTargetSections = useCallback(async () => {
|
|
163
|
+
setLoadingTargetSections(true);
|
|
164
|
+
try {
|
|
165
|
+
const { data } = await get(
|
|
166
|
+
`/${PLUGIN_ID}/pages/${encodeURIComponent(targetPageId)}/sections`
|
|
167
|
+
);
|
|
168
|
+
if (data.error) {
|
|
169
|
+
setTargetSections([]);
|
|
170
|
+
} else {
|
|
171
|
+
setTargetSections(data.data.sections || []);
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
setTargetSections([]);
|
|
175
|
+
} finally {
|
|
176
|
+
setLoadingTargetSections(false);
|
|
177
|
+
}
|
|
178
|
+
}, [targetPageId, get]);
|
|
179
|
+
|
|
180
|
+
const handleDragStart = useCallback((e, index, source = 'source') => {
|
|
181
|
+
setDraggedIndex(index);
|
|
182
|
+
setDraggedSource(source);
|
|
183
|
+
e.dataTransfer.effectAllowed = source === 'source' ? "copy" : "move";
|
|
184
|
+
e.dataTransfer.setData("text/plain", JSON.stringify({ index, source }));
|
|
185
|
+
|
|
186
|
+
const dragImage = e.target.cloneNode(true);
|
|
187
|
+
dragImage.style.opacity = "0.8";
|
|
188
|
+
dragImage.style.position = "absolute";
|
|
189
|
+
dragImage.style.top = "-1000px";
|
|
190
|
+
document.body.appendChild(dragImage);
|
|
191
|
+
e.dataTransfer.setDragImage(dragImage, 0, 0);
|
|
192
|
+
setTimeout(() => document.body.removeChild(dragImage), 0);
|
|
193
|
+
}, []);
|
|
194
|
+
|
|
195
|
+
const handleDragEnd = useCallback(() => {
|
|
196
|
+
setDraggedIndex(null);
|
|
197
|
+
setDraggedSource(null);
|
|
198
|
+
setIsDragOver(false);
|
|
199
|
+
setDragOverTargetIndex(null);
|
|
200
|
+
}, []);
|
|
201
|
+
|
|
202
|
+
const handleDragOver = useCallback((e, targetIndex = null) => {
|
|
203
|
+
e.preventDefault();
|
|
204
|
+
e.stopPropagation();
|
|
205
|
+
e.dataTransfer.dropEffect = draggedSource === 'source' ? "copy" : "move";
|
|
206
|
+
setIsDragOver(true);
|
|
207
|
+
if (targetIndex !== null) {
|
|
208
|
+
setDragOverTargetIndex(targetIndex);
|
|
209
|
+
}
|
|
210
|
+
}, [draggedSource]);
|
|
211
|
+
|
|
212
|
+
const handleDragLeave = useCallback((e) => {
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
e.stopPropagation();
|
|
215
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
216
|
+
const x = e.clientX;
|
|
217
|
+
const y = e.clientY;
|
|
218
|
+
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) {
|
|
219
|
+
setIsDragOver(false);
|
|
220
|
+
setDragOverTargetIndex(null);
|
|
221
|
+
}
|
|
222
|
+
}, []);
|
|
223
|
+
|
|
224
|
+
const handleDrop = useCallback(async (e, dropIndex = null) => {
|
|
225
|
+
e.preventDefault();
|
|
226
|
+
e.stopPropagation();
|
|
227
|
+
setIsDragOver(false);
|
|
228
|
+
setDragOverTargetIndex(null);
|
|
229
|
+
|
|
230
|
+
const dataStr = e.dataTransfer.getData("text/plain");
|
|
231
|
+
let dragData;
|
|
232
|
+
try {
|
|
233
|
+
dragData = JSON.parse(dataStr);
|
|
234
|
+
} catch {
|
|
235
|
+
const index = parseInt(dataStr, 10);
|
|
236
|
+
if (isNaN(index)) return;
|
|
237
|
+
dragData = { index, source: 'source' };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const { index, source } = dragData;
|
|
241
|
+
|
|
242
|
+
if (source === 'target' && dropIndex !== null && targetPageId) {
|
|
243
|
+
if (index === dropIndex) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const newSections = [...targetSections];
|
|
248
|
+
const [movedSection] = newSections.splice(index, 1);
|
|
249
|
+
newSections.splice(dropIndex, 0, movedSection);
|
|
250
|
+
|
|
251
|
+
setTargetSections(newSections);
|
|
252
|
+
setHasUnsavedChanges(true);
|
|
253
|
+
setDraggedIndex(null);
|
|
254
|
+
setDraggedSource(null);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!sourcePageId || !targetPageId) {
|
|
259
|
+
setMessage({ type: "danger", text: "Please select both source and target pages" });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
setLoading(true);
|
|
264
|
+
setMessage(null);
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const { data } = await post(
|
|
268
|
+
`/${PLUGIN_ID}/pages/${encodeURIComponent(sourcePageId)}/copy-to/${encodeURIComponent(targetPageId)}`,
|
|
269
|
+
{ sectionIndices: [index], insertIndex: dropIndex }
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
if (data.error) {
|
|
273
|
+
setMessage({ type: "danger", text: data.error });
|
|
274
|
+
setCopyDetails(null);
|
|
275
|
+
} else {
|
|
276
|
+
const isDuplicate = sourcePageId === targetPageId;
|
|
277
|
+
setMessage({
|
|
278
|
+
type: "success",
|
|
279
|
+
text: `Successfully ${isDuplicate ? 'duplicated' : 'copied'} ${data.data.copiedSectionsCount} section(s)! Click to view details.`,
|
|
280
|
+
});
|
|
281
|
+
setCopyDetails(data.data.copiedDetails || []);
|
|
282
|
+
setHasUnsavedChanges(true);
|
|
283
|
+
setShowDetailsModal(true);
|
|
284
|
+
|
|
285
|
+
await loadTargetSections();
|
|
286
|
+
if (isDuplicate) {
|
|
287
|
+
await loadSourceSections();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} catch (error) {
|
|
291
|
+
setMessage({
|
|
292
|
+
type: "danger",
|
|
293
|
+
text: error.message || "An error occurred",
|
|
294
|
+
});
|
|
295
|
+
} finally {
|
|
296
|
+
setLoading(false);
|
|
297
|
+
setDraggedIndex(null);
|
|
298
|
+
setDraggedSource(null);
|
|
299
|
+
}
|
|
300
|
+
}, [sourcePageId, targetPageId, targetSections, post, put, loadTargetSections, loadSourceSections]);
|
|
301
|
+
|
|
302
|
+
const getPageTitle = (page) => {
|
|
303
|
+
// Farklı content type'larda farklı field adları olabilir
|
|
304
|
+
return page.attributes?.title || page.title ||
|
|
305
|
+
page.attributes?.name || page.name ||
|
|
306
|
+
page.attributes?.heading || page.heading ||
|
|
307
|
+
page.attributes?.label || page.label ||
|
|
308
|
+
page.attributes?.slug || page.slug ||
|
|
309
|
+
`ID: ${page.id}`;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const handlePublish = useCallback(async () => {
|
|
313
|
+
if (!targetPageId || !hasUnsavedChanges) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
setPublishing(true);
|
|
318
|
+
setMessage(null);
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
const { data: updateData } = await put(
|
|
322
|
+
`/${PLUGIN_ID}/pages/${encodeURIComponent(targetPageId)}/sections`,
|
|
323
|
+
{ sections: targetSections }
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
if (updateData.error) {
|
|
327
|
+
setMessage({ type: "danger", text: updateData.error });
|
|
328
|
+
setPublishing(false);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const { data: publishData } = await post(
|
|
333
|
+
`/${PLUGIN_ID}/pages/${encodeURIComponent(targetPageId)}/publish`
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
if (publishData.error) {
|
|
337
|
+
setMessage({ type: "danger", text: publishData.error });
|
|
338
|
+
} else {
|
|
339
|
+
setMessage({
|
|
340
|
+
type: "success",
|
|
341
|
+
text: "Page published successfully!",
|
|
342
|
+
});
|
|
343
|
+
setHasUnsavedChanges(false);
|
|
344
|
+
await loadTargetSections();
|
|
345
|
+
}
|
|
346
|
+
} catch (error) {
|
|
347
|
+
setMessage({
|
|
348
|
+
type: "danger",
|
|
349
|
+
text: error.message || "An error occurred while publishing",
|
|
350
|
+
});
|
|
351
|
+
} finally {
|
|
352
|
+
setPublishing(false);
|
|
353
|
+
}
|
|
354
|
+
}, [targetPageId, targetSections, hasUnsavedChanges, put, post, loadTargetSections]);
|
|
355
|
+
|
|
356
|
+
const getComponentName = (section) => {
|
|
357
|
+
const componentType = section.__component || "";
|
|
358
|
+
return componentType.replace("sections.", "").replace(/-/g, " ").replace(/\b\w/g, l => l.toUpperCase());
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const getComponentPreview = (section) => {
|
|
362
|
+
return section.title ||
|
|
363
|
+
section.heading ||
|
|
364
|
+
section.text?.substring(0, 40) ||
|
|
365
|
+
section.content?.substring(0, 40) ||
|
|
366
|
+
"No content";
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
return (
|
|
370
|
+
<Page.Main>
|
|
371
|
+
<Page.Title>Copy Any Component 🎨</Page.Title>
|
|
372
|
+
<Main>
|
|
373
|
+
<Box padding={8}>
|
|
374
|
+
<Flex justifyContent="space-between" alignItems="flex-start" marginBottom={6}>
|
|
375
|
+
<Box>
|
|
376
|
+
<Typography variant="alpha" tag="h1">
|
|
377
|
+
Copy Any Component 🎨
|
|
378
|
+
</Typography>
|
|
379
|
+
<Typography variant="omega" textColor="neutral600" marginTop={2}>
|
|
380
|
+
Drag any component from source page to target page - Works with all component types!
|
|
381
|
+
</Typography>
|
|
382
|
+
{currentConfig.contentType && (
|
|
383
|
+
<Flex gap={2} marginTop={2}>
|
|
384
|
+
<Badge>
|
|
385
|
+
{contentTypes.find(c => c.uid === currentConfig.contentType)?.displayName || currentConfig.contentType}
|
|
386
|
+
</Badge>
|
|
387
|
+
<Badge variant="secondary">
|
|
388
|
+
{currentConfig.dynamicZoneField}
|
|
389
|
+
</Badge>
|
|
390
|
+
</Flex>
|
|
391
|
+
)}
|
|
392
|
+
</Box>
|
|
393
|
+
<Button
|
|
394
|
+
variant={showSettings ? "secondary" : "tertiary"}
|
|
395
|
+
onClick={() => setShowSettings(!showSettings)}
|
|
396
|
+
size="S"
|
|
397
|
+
>
|
|
398
|
+
⚙️ {showSettings ? "Ayarları Kapat" : "Content Type Ayarları"}
|
|
399
|
+
</Button>
|
|
400
|
+
</Flex>
|
|
401
|
+
|
|
402
|
+
{/* Content Type Ayarları */}
|
|
403
|
+
{showSettings && (
|
|
404
|
+
<Box marginBottom={6} padding={4} background="neutral100" hasRadius>
|
|
405
|
+
<Typography variant="beta" tag="h3" marginBottom={4}>
|
|
406
|
+
📋 Content Type Yapılandırması
|
|
407
|
+
</Typography>
|
|
408
|
+
<Typography variant="omega" textColor="neutral600" marginBottom={4}>
|
|
409
|
+
Farklı bir content type veya dynamic zone kullanmak için aşağıdan seçim yapın.
|
|
410
|
+
Ayarlarınız <strong>otomatik olarak kaydedilir</strong> ve Strapi yeniden başlatıldığında da geçerli olur.
|
|
411
|
+
Kod düzenlemeye gerek yok! ✨
|
|
412
|
+
</Typography>
|
|
413
|
+
|
|
414
|
+
{contentTypes.length === 0 ? (
|
|
415
|
+
<Alert variant="warning" title="Uyarı">
|
|
416
|
+
Dynamic zone içeren content type bulunamadı. Lütfen önce bir content type oluşturun.
|
|
417
|
+
</Alert>
|
|
418
|
+
) : (
|
|
419
|
+
<Grid.Root gap={4}>
|
|
420
|
+
<Grid.Item col={5} s={12}>
|
|
421
|
+
<Box>
|
|
422
|
+
<Typography variant="sigma" textColor="neutral700" marginBottom={2}>
|
|
423
|
+
Content Type
|
|
424
|
+
</Typography>
|
|
425
|
+
<SingleSelect
|
|
426
|
+
value={selectedContentType}
|
|
427
|
+
onChange={val => {
|
|
428
|
+
setSelectedContentType(val);
|
|
429
|
+
// İlk dynamic zone'u otomatik seç
|
|
430
|
+
const ct = contentTypes.find(c => c.uid === val);
|
|
431
|
+
if (ct?.dynamicZones?.length > 0) {
|
|
432
|
+
setSelectedDynamicZone(ct.dynamicZones[0].name);
|
|
433
|
+
} else {
|
|
434
|
+
setSelectedDynamicZone('');
|
|
435
|
+
}
|
|
436
|
+
}}
|
|
437
|
+
placeholder="Content type seçin..."
|
|
438
|
+
>
|
|
439
|
+
{contentTypes.map(ct => (
|
|
440
|
+
<SingleSelectOption key={ct.uid} value={ct.uid}>
|
|
441
|
+
{ct.displayName} ({ct.kind === 'collectionType' ? 'Collection' : 'Single'})
|
|
442
|
+
</SingleSelectOption>
|
|
443
|
+
))}
|
|
444
|
+
</SingleSelect>
|
|
445
|
+
</Box>
|
|
446
|
+
</Grid.Item>
|
|
447
|
+
|
|
448
|
+
<Grid.Item col={4} s={12}>
|
|
449
|
+
<Box>
|
|
450
|
+
<Typography variant="sigma" textColor="neutral700" marginBottom={2}>
|
|
451
|
+
Dynamic Zone Field
|
|
452
|
+
</Typography>
|
|
453
|
+
<SingleSelect
|
|
454
|
+
value={selectedDynamicZone}
|
|
455
|
+
onChange={setSelectedDynamicZone}
|
|
456
|
+
placeholder="Dynamic zone seçin..."
|
|
457
|
+
disabled={!selectedContentType}
|
|
458
|
+
>
|
|
459
|
+
{getAvailableDynamicZones().map(dz => (
|
|
460
|
+
<SingleSelectOption key={dz.name} value={dz.name}>
|
|
461
|
+
{dz.name} ({dz.components.length} component)
|
|
462
|
+
</SingleSelectOption>
|
|
463
|
+
))}
|
|
464
|
+
</SingleSelect>
|
|
465
|
+
</Box>
|
|
466
|
+
</Grid.Item>
|
|
467
|
+
|
|
468
|
+
<Grid.Item col={3} s={12}>
|
|
469
|
+
<Box>
|
|
470
|
+
<Typography variant="sigma" textColor="neutral700" marginBottom={2}>
|
|
471
|
+
|
|
472
|
+
</Typography>
|
|
473
|
+
<Button
|
|
474
|
+
onClick={handleConfigUpdate}
|
|
475
|
+
loading={configLoading}
|
|
476
|
+
disabled={!selectedContentType || !selectedDynamicZone ||
|
|
477
|
+
(selectedContentType === currentConfig.contentType &&
|
|
478
|
+
selectedDynamicZone === currentConfig.dynamicZoneField)}
|
|
479
|
+
fullWidth
|
|
480
|
+
>
|
|
481
|
+
💾 Kaydet
|
|
482
|
+
</Button>
|
|
483
|
+
</Box>
|
|
484
|
+
</Grid.Item>
|
|
485
|
+
</Grid.Root>
|
|
486
|
+
)}
|
|
487
|
+
|
|
488
|
+
{contentTypes.length > 0 && (
|
|
489
|
+
<Box marginTop={4} padding={3} background="neutral0" hasRadius>
|
|
490
|
+
<Typography variant="sigma" textColor="neutral700" marginBottom={2}>
|
|
491
|
+
💡 Mevcut Content Type'lar ve Dynamic Zone'ları:
|
|
492
|
+
</Typography>
|
|
493
|
+
<Box style={{ maxHeight: '150px', overflowY: 'auto' }}>
|
|
494
|
+
{contentTypes.map(ct => (
|
|
495
|
+
<Flex key={ct.uid} gap={2} marginBottom={1} alignItems="center">
|
|
496
|
+
<Badge variant={ct.uid === currentConfig.contentType ? "success" : "secondary"}>
|
|
497
|
+
{ct.displayName}
|
|
498
|
+
</Badge>
|
|
499
|
+
<Typography variant="omega" textColor="neutral600">
|
|
500
|
+
→ {ct.dynamicZones.map(dz => dz.name).join(', ')}
|
|
501
|
+
</Typography>
|
|
502
|
+
</Flex>
|
|
503
|
+
))}
|
|
504
|
+
</Box>
|
|
505
|
+
</Box>
|
|
506
|
+
)}
|
|
507
|
+
</Box>
|
|
508
|
+
)}
|
|
509
|
+
|
|
510
|
+
{message && (
|
|
511
|
+
<Box marginBottom={6}>
|
|
512
|
+
<Alert
|
|
513
|
+
closeLabel="Close"
|
|
514
|
+
title={message.type === "success" ? "Success" : "Error"}
|
|
515
|
+
variant={message.type === "success" ? "success" : "danger"}
|
|
516
|
+
onClose={() => {
|
|
517
|
+
setMessage(null);
|
|
518
|
+
setCopyDetails(null);
|
|
519
|
+
}}
|
|
520
|
+
onClick={() => {
|
|
521
|
+
if (message.type === "success" && copyDetails) {
|
|
522
|
+
setShowDetailsModal(true);
|
|
523
|
+
}
|
|
524
|
+
}}
|
|
525
|
+
style={{ cursor: message.type === "success" && copyDetails ? "pointer" : "default" }}
|
|
526
|
+
>
|
|
527
|
+
{message.text}
|
|
528
|
+
</Alert>
|
|
529
|
+
</Box>
|
|
530
|
+
)}
|
|
531
|
+
|
|
532
|
+
{showDetailsModal && copyDetails && (
|
|
533
|
+
<div
|
|
534
|
+
style={{
|
|
535
|
+
position: "fixed",
|
|
536
|
+
top: 0,
|
|
537
|
+
left: 0,
|
|
538
|
+
right: 0,
|
|
539
|
+
bottom: 0,
|
|
540
|
+
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
541
|
+
zIndex: 1000,
|
|
542
|
+
display: "flex",
|
|
543
|
+
alignItems: "center",
|
|
544
|
+
justifyContent: "center",
|
|
545
|
+
padding: "20px",
|
|
546
|
+
}}
|
|
547
|
+
onClick={() => setShowDetailsModal(false)}
|
|
548
|
+
>
|
|
549
|
+
<Card
|
|
550
|
+
style={{
|
|
551
|
+
maxWidth: "800px",
|
|
552
|
+
width: "100%",
|
|
553
|
+
maxHeight: "90vh",
|
|
554
|
+
display: "flex",
|
|
555
|
+
flexDirection: "column",
|
|
556
|
+
}}
|
|
557
|
+
onClick={(e) => e.stopPropagation()}
|
|
558
|
+
>
|
|
559
|
+
<Box
|
|
560
|
+
padding={4}
|
|
561
|
+
borderColor="neutral200"
|
|
562
|
+
style={{
|
|
563
|
+
borderBottom: "1px solid",
|
|
564
|
+
display: "flex",
|
|
565
|
+
justifyContent: "space-between",
|
|
566
|
+
alignItems: "center",
|
|
567
|
+
}}
|
|
568
|
+
>
|
|
569
|
+
<Typography fontWeight="bold" variant="beta" as="h2">
|
|
570
|
+
Copy Details
|
|
571
|
+
</Typography>
|
|
572
|
+
<IconButton
|
|
573
|
+
variant="ghost"
|
|
574
|
+
onClick={() => setShowDetailsModal(false)}
|
|
575
|
+
label="Close"
|
|
576
|
+
>
|
|
577
|
+
<Cross />
|
|
578
|
+
</IconButton>
|
|
579
|
+
</Box>
|
|
580
|
+
<Box
|
|
581
|
+
padding={4}
|
|
582
|
+
style={{
|
|
583
|
+
overflowY: "auto",
|
|
584
|
+
flex: 1,
|
|
585
|
+
}}
|
|
586
|
+
>
|
|
587
|
+
{copyDetails.map((detail, idx) => (
|
|
588
|
+
<Box key={idx} marginBottom={6}>
|
|
589
|
+
<Card>
|
|
590
|
+
<Box padding={4}>
|
|
591
|
+
<Flex justifyContent="space-between" alignItems="center" marginBottom={4}>
|
|
592
|
+
<Typography variant="beta" fontWeight="bold">
|
|
593
|
+
{detail.componentType.replace("sections.", "").replace(/-/g, " ").replace(/\b\w/g, l => l.toUpperCase())}
|
|
594
|
+
</Typography>
|
|
595
|
+
<Flex gap={2}>
|
|
596
|
+
<Badge>
|
|
597
|
+
{detail.totalFields} fields • {detail.totalMedia} media
|
|
598
|
+
</Badge>
|
|
599
|
+
{detail.totalRemoved > 0 && (
|
|
600
|
+
<Badge variant="secondary">
|
|
601
|
+
{detail.totalRemoved} system
|
|
602
|
+
</Badge>
|
|
603
|
+
)}
|
|
604
|
+
</Flex>
|
|
605
|
+
</Flex>
|
|
606
|
+
|
|
607
|
+
{detail.mediaFields.length > 0 && (
|
|
608
|
+
<Box marginBottom={4}>
|
|
609
|
+
<Typography variant="sigma" textColor="neutral700" marginBottom={2}>
|
|
610
|
+
📷 Media Files ({detail.totalMedia}):
|
|
611
|
+
</Typography>
|
|
612
|
+
{detail.mediaFields.map((media, mIdx) => (
|
|
613
|
+
<Box key={mIdx} padding={3} marginBottom={2} background="neutral100" hasRadius>
|
|
614
|
+
<Typography variant="omega" fontWeight="bold" marginBottom={1}>
|
|
615
|
+
{media.path}:
|
|
616
|
+
</Typography>
|
|
617
|
+
{media.items.map((item, iIdx) => (
|
|
618
|
+
<Box key={iIdx} padding={2} marginTop={1} background="neutral0" hasRadius>
|
|
619
|
+
<Flex alignItems="center" gap={2}>
|
|
620
|
+
<Typography variant="omega" textColor="success600">
|
|
621
|
+
✓
|
|
622
|
+
</Typography>
|
|
623
|
+
<Box>
|
|
624
|
+
<Typography variant="omega" fontWeight="semiBold">
|
|
625
|
+
{item.name}
|
|
626
|
+
</Typography>
|
|
627
|
+
<Typography variant="omega" textColor="neutral600" fontSize={1}>
|
|
628
|
+
{item.mime} • ID: {item.id}
|
|
629
|
+
</Typography>
|
|
630
|
+
</Box>
|
|
631
|
+
</Flex>
|
|
632
|
+
</Box>
|
|
633
|
+
))}
|
|
634
|
+
</Box>
|
|
635
|
+
))}
|
|
636
|
+
</Box>
|
|
637
|
+
)}
|
|
638
|
+
|
|
639
|
+
{detail.fields.length > 0 && (
|
|
640
|
+
<Box marginBottom={4}>
|
|
641
|
+
<Typography variant="sigma" textColor="neutral700" marginBottom={2}>
|
|
642
|
+
📝 Fields ({detail.totalFields}):
|
|
643
|
+
</Typography>
|
|
644
|
+
<Box style={{ maxHeight: "300px", overflowY: "auto" }}>
|
|
645
|
+
{detail.fields.slice(0, 20).map((field, fIdx) => (
|
|
646
|
+
<Box key={fIdx} padding={2} marginBottom={1} background="neutral0" hasRadius>
|
|
647
|
+
<Flex alignItems="center" gap={2}>
|
|
648
|
+
<Typography variant="omega" textColor="success600">
|
|
649
|
+
✓
|
|
650
|
+
</Typography>
|
|
651
|
+
<Box style={{ flex: 1 }}>
|
|
652
|
+
<Typography variant="omega" fontWeight="semiBold">
|
|
653
|
+
{field.path}
|
|
654
|
+
</Typography>
|
|
655
|
+
<Typography variant="omega" textColor="neutral600" fontSize={1}>
|
|
656
|
+
Type: {field.type}
|
|
657
|
+
{field.value !== undefined && ` • Value: ${String(field.value).substring(0, 50)}`}
|
|
658
|
+
{field.count !== undefined && ` • Count: ${field.count}`}
|
|
659
|
+
</Typography>
|
|
660
|
+
</Box>
|
|
661
|
+
</Flex>
|
|
662
|
+
</Box>
|
|
663
|
+
))}
|
|
664
|
+
{detail.fields.length > 20 && (
|
|
665
|
+
<Typography variant="omega" textColor="neutral600" marginTop={2}>
|
|
666
|
+
... and {detail.fields.length - 20} more fields
|
|
667
|
+
</Typography>
|
|
668
|
+
)}
|
|
669
|
+
</Box>
|
|
670
|
+
</Box>
|
|
671
|
+
)}
|
|
672
|
+
|
|
673
|
+
{detail.removedFields && detail.removedFields.length > 0 && (
|
|
674
|
+
<Box>
|
|
675
|
+
<Flex alignItems="center" gap={2} marginBottom={2}>
|
|
676
|
+
<Typography variant="sigma" textColor="neutral600">
|
|
677
|
+
ℹ️ System Fields ({detail.totalRemoved}):
|
|
678
|
+
</Typography>
|
|
679
|
+
<Badge variant="secondary" size="S">Auto-handled</Badge>
|
|
680
|
+
</Flex>
|
|
681
|
+
<Box padding={3} background="neutral100" hasRadius marginBottom={2}>
|
|
682
|
+
<Typography variant="omega" textColor="neutral600" style={{ fontSize: '12px' }}>
|
|
683
|
+
💡 Aşağıdaki alanlar otomatik olarak yönetiliyor. ID'ler silinir ve Strapi yeni benzersiz ID'ler oluşturur. Bu normal bir davranıştır ve veri kaybına neden olmaz.
|
|
684
|
+
</Typography>
|
|
685
|
+
</Box>
|
|
686
|
+
<Box style={{ maxHeight: "150px", overflowY: "auto" }}>
|
|
687
|
+
{detail.removedFields.map((field, rIdx) => (
|
|
688
|
+
<Box key={rIdx} padding={2} marginBottom={1} background="neutral100" hasRadius>
|
|
689
|
+
<Flex alignItems="center" gap={2}>
|
|
690
|
+
<Typography variant="omega" textColor="neutral500">
|
|
691
|
+
→
|
|
692
|
+
</Typography>
|
|
693
|
+
<Box style={{ flex: 1 }}>
|
|
694
|
+
<Typography variant="omega" fontWeight="semiBold" textColor="neutral700">
|
|
695
|
+
{field.path}
|
|
696
|
+
</Typography>
|
|
697
|
+
<Typography variant="omega" textColor="neutral500" style={{ fontSize: '11px' }}>
|
|
698
|
+
{field.reason === 'System field (automatically removed)'
|
|
699
|
+
? 'Sistem alanı - Yeni ID atanacak'
|
|
700
|
+
: 'Nested ID - Otomatik oluşturulacak'}
|
|
701
|
+
</Typography>
|
|
702
|
+
</Box>
|
|
703
|
+
</Flex>
|
|
704
|
+
</Box>
|
|
705
|
+
))}
|
|
706
|
+
</Box>
|
|
707
|
+
</Box>
|
|
708
|
+
)}
|
|
709
|
+
|
|
710
|
+
{detail.fields.length === 0 && detail.mediaFields.length === 0 && (
|
|
711
|
+
<Typography variant="omega" textColor="neutral600">
|
|
712
|
+
No fields detected in this component
|
|
713
|
+
</Typography>
|
|
714
|
+
)}
|
|
715
|
+
</Box>
|
|
716
|
+
</Card>
|
|
717
|
+
</Box>
|
|
718
|
+
))}
|
|
719
|
+
</Box>
|
|
720
|
+
<Box
|
|
721
|
+
padding={4}
|
|
722
|
+
borderColor="neutral200"
|
|
723
|
+
style={{
|
|
724
|
+
borderTop: "1px solid",
|
|
725
|
+
display: "flex",
|
|
726
|
+
justifyContent: "flex-end",
|
|
727
|
+
}}
|
|
728
|
+
>
|
|
729
|
+
<Button onClick={() => setShowDetailsModal(false)} variant="tertiary">
|
|
730
|
+
Close
|
|
731
|
+
</Button>
|
|
732
|
+
</Box>
|
|
733
|
+
</Card>
|
|
734
|
+
</div>
|
|
735
|
+
)}
|
|
736
|
+
|
|
737
|
+
<Grid.Root gap={4}>
|
|
738
|
+
<Grid.Item col={6} xs={12}>
|
|
739
|
+
<Card style={{ height: "100%",width: "100%" }}>
|
|
740
|
+
<Box padding={4}>
|
|
741
|
+
<Typography variant="delta" tag="h2" marginBottom={4}>
|
|
742
|
+
Source Page
|
|
743
|
+
</Typography>
|
|
744
|
+
|
|
745
|
+
<Box marginBottom={4}>
|
|
746
|
+
<SingleSelect
|
|
747
|
+
label="Select source page"
|
|
748
|
+
placeholder="Choose page..."
|
|
749
|
+
value={sourcePageId}
|
|
750
|
+
onChange={setSourcePageId}
|
|
751
|
+
>
|
|
752
|
+
{pages.map((page) => {
|
|
753
|
+
const pageId = page.documentId || page.id;
|
|
754
|
+
return (
|
|
755
|
+
<SingleSelectOption key={pageId} value={pageId}>
|
|
756
|
+
{getPageTitle(page)}
|
|
757
|
+
</SingleSelectOption>
|
|
758
|
+
);
|
|
759
|
+
})}
|
|
760
|
+
</SingleSelect>
|
|
761
|
+
</Box>
|
|
762
|
+
|
|
763
|
+
<Box
|
|
764
|
+
style={{
|
|
765
|
+
minHeight: "400px",
|
|
766
|
+
maxHeight: "600px",
|
|
767
|
+
overflowY: "auto",
|
|
768
|
+
}}
|
|
769
|
+
padding={3}
|
|
770
|
+
background="neutral100"
|
|
771
|
+
hasRadius
|
|
772
|
+
>
|
|
773
|
+
{loadingSourceSections ? (
|
|
774
|
+
<Typography>Loading...</Typography>
|
|
775
|
+
) : sourceSections.length > 0 ? (
|
|
776
|
+
<>
|
|
777
|
+
<Typography variant="sigma" textColor="neutral600" marginBottom={3}>
|
|
778
|
+
{sourceSections.length} SECTION(S) • DRAG TO COPY →
|
|
779
|
+
</Typography>
|
|
780
|
+
{sourceSections.map((section, index) => (
|
|
781
|
+
<Card
|
|
782
|
+
key={index}
|
|
783
|
+
style={{
|
|
784
|
+
cursor: "grab",
|
|
785
|
+
opacity: draggedIndex === index && draggedSource === 'source' ? 0.5 : 1,
|
|
786
|
+
marginBottom: 3,
|
|
787
|
+
}}
|
|
788
|
+
draggable
|
|
789
|
+
onDragStart={(e) => handleDragStart(e, index, 'source')}
|
|
790
|
+
onDragEnd={handleDragEnd}
|
|
791
|
+
>
|
|
792
|
+
<Box padding={3}>
|
|
793
|
+
<Flex justifyContent="space-between" alignItems="center">
|
|
794
|
+
<Box style={{ flex: 1 }}>
|
|
795
|
+
<Badge>
|
|
796
|
+
{getComponentName(section)}
|
|
797
|
+
</Badge>
|
|
798
|
+
<Typography variant="omega" marginTop={2} tag="p">
|
|
799
|
+
{getComponentPreview(section)}
|
|
800
|
+
</Typography>
|
|
801
|
+
</Box>
|
|
802
|
+
<IconButton
|
|
803
|
+
variant="ghost"
|
|
804
|
+
onClick={(e) => e.preventDefault()}
|
|
805
|
+
label="Drag"
|
|
806
|
+
noBorder
|
|
807
|
+
>
|
|
808
|
+
<Drag />
|
|
809
|
+
</IconButton>
|
|
810
|
+
</Flex>
|
|
811
|
+
</Box>
|
|
812
|
+
</Card>
|
|
813
|
+
))}
|
|
814
|
+
</>
|
|
815
|
+
) : sourcePageId ? (
|
|
816
|
+
<Typography textColor="neutral600">No sections in this page</Typography>
|
|
817
|
+
) : (
|
|
818
|
+
<Typography textColor="neutral600">Select a source page</Typography>
|
|
819
|
+
)}
|
|
820
|
+
</Box>
|
|
821
|
+
</Box>
|
|
822
|
+
</Card>
|
|
823
|
+
</Grid.Item>
|
|
824
|
+
|
|
825
|
+
<Grid.Item col={6} xs={12}>
|
|
826
|
+
<Card style={{ height: "100%",width: "100%" }}>
|
|
827
|
+
<Box padding={4}>
|
|
828
|
+
<Flex justifyContent="space-between" alignItems="center" marginBottom={4}>
|
|
829
|
+
<Typography variant="delta" tag="h2">
|
|
830
|
+
Target Page
|
|
831
|
+
</Typography>
|
|
832
|
+
{hasUnsavedChanges && targetPageId && (
|
|
833
|
+
<Button
|
|
834
|
+
onClick={handlePublish}
|
|
835
|
+
loading={publishing}
|
|
836
|
+
variant="default"
|
|
837
|
+
size="S"
|
|
838
|
+
>
|
|
839
|
+
Publish
|
|
840
|
+
</Button>
|
|
841
|
+
)}
|
|
842
|
+
</Flex>
|
|
843
|
+
|
|
844
|
+
{hasUnsavedChanges && (
|
|
845
|
+
<Box marginBottom={4} padding={3} background="warning100" hasRadius>
|
|
846
|
+
<Typography variant="omega" textColor="warning700">
|
|
847
|
+
⚠️ You have unsaved changes. Click Publish to save and publish.
|
|
848
|
+
</Typography>
|
|
849
|
+
</Box>
|
|
850
|
+
)}
|
|
851
|
+
|
|
852
|
+
<Box marginBottom={4}>
|
|
853
|
+
<SingleSelect
|
|
854
|
+
label="Select target page"
|
|
855
|
+
placeholder="Choose page..."
|
|
856
|
+
value={targetPageId}
|
|
857
|
+
onChange={setTargetPageId}
|
|
858
|
+
>
|
|
859
|
+
{pages.map((page) => {
|
|
860
|
+
const pageId = page.documentId || page.id;
|
|
861
|
+
return (
|
|
862
|
+
<SingleSelectOption key={pageId} value={pageId}>
|
|
863
|
+
{getPageTitle(page)}
|
|
864
|
+
</SingleSelectOption>
|
|
865
|
+
);
|
|
866
|
+
})}
|
|
867
|
+
</SingleSelect>
|
|
868
|
+
</Box>
|
|
869
|
+
|
|
870
|
+
<Box
|
|
871
|
+
padding={3}
|
|
872
|
+
background={isDragOver ? "primary100" : "neutral100"}
|
|
873
|
+
hasRadius
|
|
874
|
+
style={{
|
|
875
|
+
minHeight: "400px",
|
|
876
|
+
maxHeight: "600px",
|
|
877
|
+
overflowY: "auto",
|
|
878
|
+
border: isDragOver ? "2px dashed" : "2px dashed transparent",
|
|
879
|
+
borderColor: isDragOver ? "primary500" : "transparent",
|
|
880
|
+
}}
|
|
881
|
+
onDragOver={(e) => {
|
|
882
|
+
if (draggedSource === 'source') {
|
|
883
|
+
handleDragOver(e);
|
|
884
|
+
}
|
|
885
|
+
}}
|
|
886
|
+
onDragLeave={handleDragLeave}
|
|
887
|
+
onDrop={(e) => {
|
|
888
|
+
if (draggedSource === 'source') {
|
|
889
|
+
handleDrop(e, targetSections.length);
|
|
890
|
+
}
|
|
891
|
+
}}
|
|
892
|
+
>
|
|
893
|
+
{loadingTargetSections ? (
|
|
894
|
+
<Typography>Loading...</Typography>
|
|
895
|
+
) : (
|
|
896
|
+
<>
|
|
897
|
+
{targetSections.length > 0 && (
|
|
898
|
+
<Typography variant="sigma" textColor="neutral600" marginBottom={3}>
|
|
899
|
+
{targetSections.length} SECTION(S) • Drag to reorder
|
|
900
|
+
</Typography>
|
|
901
|
+
)}
|
|
902
|
+
|
|
903
|
+
{targetSections.map((section, index) => (
|
|
904
|
+
<React.Fragment key={index}>
|
|
905
|
+
{dragOverTargetIndex === index && (
|
|
906
|
+
<Box
|
|
907
|
+
style={{
|
|
908
|
+
height: "4px",
|
|
909
|
+
backgroundColor: "primary500",
|
|
910
|
+
marginBottom: 2,
|
|
911
|
+
borderRadius: "2px",
|
|
912
|
+
}}
|
|
913
|
+
/>
|
|
914
|
+
)}
|
|
915
|
+
<Card
|
|
916
|
+
style={{
|
|
917
|
+
cursor: draggedSource === 'target' ? "move" : "default",
|
|
918
|
+
opacity: draggedIndex === index && draggedSource === 'target' ? 0.5 : 1,
|
|
919
|
+
marginBottom: 3,
|
|
920
|
+
}}
|
|
921
|
+
draggable={true}
|
|
922
|
+
onDragStart={(e) => handleDragStart(e, index, 'target')}
|
|
923
|
+
onDragEnd={handleDragEnd}
|
|
924
|
+
onDragOver={(e) => handleDragOver(e, index)}
|
|
925
|
+
onDragLeave={handleDragLeave}
|
|
926
|
+
onDrop={(e) => handleDrop(e, index)}
|
|
927
|
+
>
|
|
928
|
+
<Box padding={3}>
|
|
929
|
+
<Flex justifyContent="space-between" alignItems="flex-start">
|
|
930
|
+
<Box style={{ flex: 1 }}>
|
|
931
|
+
<Badge variant="secondary">
|
|
932
|
+
{getComponentName(section)}
|
|
933
|
+
</Badge>
|
|
934
|
+
<Typography variant="omega" marginTop={2} tag="p">
|
|
935
|
+
{getComponentPreview(section)}
|
|
936
|
+
</Typography>
|
|
937
|
+
</Box>
|
|
938
|
+
<IconButton
|
|
939
|
+
variant="ghost"
|
|
940
|
+
onClick={(e) => e.preventDefault()}
|
|
941
|
+
label="Drag"
|
|
942
|
+
noBorder
|
|
943
|
+
>
|
|
944
|
+
<Drag />
|
|
945
|
+
</IconButton>
|
|
946
|
+
</Flex>
|
|
947
|
+
</Box>
|
|
948
|
+
</Card>
|
|
949
|
+
</React.Fragment>
|
|
950
|
+
))}
|
|
951
|
+
|
|
952
|
+
{dragOverTargetIndex === targetSections.length && (
|
|
953
|
+
<Box
|
|
954
|
+
style={{
|
|
955
|
+
height: "4px",
|
|
956
|
+
backgroundColor: "primary500",
|
|
957
|
+
marginTop: 2,
|
|
958
|
+
borderRadius: "2px",
|
|
959
|
+
}}
|
|
960
|
+
/>
|
|
961
|
+
)}
|
|
962
|
+
|
|
963
|
+
{isDragOver && targetSections.length === 0 && (
|
|
964
|
+
<Box padding={8} textAlign="center">
|
|
965
|
+
<Typography variant="omega" textColor="primary600">
|
|
966
|
+
↓ Drop here to copy section ↓
|
|
967
|
+
</Typography>
|
|
968
|
+
</Box>
|
|
969
|
+
)}
|
|
970
|
+
|
|
971
|
+
{!isDragOver && targetSections.length === 0 && targetPageId && (
|
|
972
|
+
<Box padding={8} textAlign="center">
|
|
973
|
+
<Typography variant="omega" textColor="neutral600">
|
|
974
|
+
No sections yet. Drag from source to add.
|
|
975
|
+
</Typography>
|
|
976
|
+
</Box>
|
|
977
|
+
)}
|
|
978
|
+
|
|
979
|
+
{!targetPageId && (
|
|
980
|
+
<Typography textColor="neutral600">Select a target page</Typography>
|
|
981
|
+
)}
|
|
982
|
+
</>
|
|
983
|
+
)}
|
|
984
|
+
</Box>
|
|
985
|
+
</Box>
|
|
986
|
+
</Card>
|
|
987
|
+
</Grid.Item>
|
|
988
|
+
</Grid.Root>
|
|
989
|
+
</Box>
|
|
990
|
+
</Main>
|
|
991
|
+
</Page.Main>
|
|
992
|
+
);
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
export default HomePage;
|