strapi-plugin-dynamic-zone-tools 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/CHANGELOG.md +12 -0
- package/CONTRIBUTING.md +38 -0
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/admin/custom.d.ts +2 -0
- package/admin/src/components/DynamicZoneComponentDuplicateInjector.tsx +604 -0
- package/admin/src/components/DynamicZoneEditViewExtensions.tsx +7 -0
- package/admin/src/components/DynamicZoneToolsPanel.tsx +1027 -0
- package/admin/src/components/FillFromRecord.tsx +36 -0
- package/admin/src/components/Initializer.tsx +19 -0
- package/admin/src/components/PluginIcon.tsx +5 -0
- package/admin/src/index.ts +61 -0
- package/admin/src/pages/App.tsx +15 -0
- package/admin/src/pages/HomePage.tsx +16 -0
- package/admin/src/pluginId.ts +1 -0
- package/admin/src/translations/en.json +51 -0
- package/admin/src/utils/createRowActionButton.ts +57 -0
- package/admin/src/utils/createRowActionMenu.ts +276 -0
- package/admin/src/utils/dynamicZoneClipboard.ts +134 -0
- package/admin/src/utils/dynamicZonePaths.ts +236 -0
- package/admin/src/utils/getTranslation.ts +5 -0
- package/admin/src/utils/prepareDynamicZoneData.ts +625 -0
- package/admin/src/utils/relationQueryParams.ts +19 -0
- package/admin/tsconfig.build.json +10 -0
- package/admin/tsconfig.json +12 -0
- package/dist/admin/en-Ce0ZP0MJ.js +54 -0
- package/dist/admin/en-DrSdJbJW.mjs +54 -0
- package/dist/admin/index.js +2161 -0
- package/dist/admin/index.mjs +2159 -0
- package/dist/admin/src/index.d.ts +12 -0
- package/dist/server/index.js +137 -0
- package/dist/server/index.mjs +137 -0
- package/dist/server/src/index.d.ts +55 -0
- package/package.json +112 -0
- package/server/src/bootstrap.ts +18 -0
- package/server/src/config/index.ts +4 -0
- package/server/src/content-types/index.ts +1 -0
- package/server/src/controllers/controller.ts +85 -0
- package/server/src/controllers/index.ts +5 -0
- package/server/src/destroy.ts +7 -0
- package/server/src/index.ts +30 -0
- package/server/src/middlewares/index.ts +1 -0
- package/server/src/policies/index.ts +1 -0
- package/server/src/register.ts +7 -0
- package/server/src/routes/admin-api.ts +18 -0
- package/server/src/routes/content-api.ts +1 -0
- package/server/src/routes/index.ts +15 -0
- package/server/src/services/index.ts +5 -0
- package/server/src/services/service.ts +9 -0
- package/server/tsconfig.build.json +10 -0
- package/server/tsconfig.json +11 -0
- package/strapi-admin.js +3 -0
- package/strapi-server.js +3 -0
|
@@ -0,0 +1,1027 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Dialog,
|
|
5
|
+
SingleSelect,
|
|
6
|
+
SingleSelectOption,
|
|
7
|
+
Typography,
|
|
8
|
+
Flex,
|
|
9
|
+
Alert,
|
|
10
|
+
Box,
|
|
11
|
+
Checkbox,
|
|
12
|
+
} from '@strapi/design-system';
|
|
13
|
+
import { useFetchClient, useForm, useNotification } from '@strapi/strapi/admin';
|
|
14
|
+
import { useIntl } from 'react-intl';
|
|
15
|
+
import { Duplicate, WarningCircle } from '@strapi/icons';
|
|
16
|
+
import { PLUGIN_ID } from '../pluginId';
|
|
17
|
+
import {
|
|
18
|
+
filterAllowedDynamicZoneEntries,
|
|
19
|
+
getComponentDisplayName,
|
|
20
|
+
getDynamicZoneEntryLabel,
|
|
21
|
+
loadComponentSchemas,
|
|
22
|
+
prepareZoneData,
|
|
23
|
+
prepareZoneDataForInsert,
|
|
24
|
+
} from '../utils/prepareDynamicZoneData';
|
|
25
|
+
import { getDynamicZoneFields } from '../utils/dynamicZonePaths';
|
|
26
|
+
|
|
27
|
+
const TOOLS_DIALOG_STYLE_ID = `${PLUGIN_ID}-dialog-layout-styles`;
|
|
28
|
+
|
|
29
|
+
function ensureToolsDialogStyles() {
|
|
30
|
+
const dom = globalThis.document;
|
|
31
|
+
|
|
32
|
+
if (!dom || dom.getElementById(TOOLS_DIALOG_STYLE_ID)) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const style = dom.createElement('style');
|
|
37
|
+
style.id = TOOLS_DIALOG_STYLE_ID;
|
|
38
|
+
style.textContent = `
|
|
39
|
+
[role='alertdialog'][data-dz-tools-dialog='true'] {
|
|
40
|
+
max-height: calc(100dvh - 2rem) !important;
|
|
41
|
+
height: auto !important;
|
|
42
|
+
overflow: hidden !important;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
[data-dz-tools-dialog='true'] [data-dz-tools-shell='true'] {
|
|
46
|
+
display: flex;
|
|
47
|
+
flex: 1 1 auto;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
min-height: 0;
|
|
50
|
+
overflow: hidden;
|
|
51
|
+
width: 100%;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
[data-dz-tools-dialog='true'] [data-dz-tools-body='true'] {
|
|
55
|
+
flex: 1 1 auto !important;
|
|
56
|
+
min-height: 0 !important;
|
|
57
|
+
overflow-y: auto !important;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
[data-dz-tools-dialog='true'] [data-dz-tools-footer='true'] {
|
|
61
|
+
flex: 0 0 auto !important;
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
dom.head.appendChild(style);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type FillMode = 'replace' | 'append';
|
|
69
|
+
|
|
70
|
+
interface ContentType {
|
|
71
|
+
uid: string;
|
|
72
|
+
displayName: string;
|
|
73
|
+
draftAndPublish: boolean;
|
|
74
|
+
localized: boolean;
|
|
75
|
+
dynamicZones: DynamicZoneInfo[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface SourceRecord {
|
|
79
|
+
id: number;
|
|
80
|
+
documentId?: string;
|
|
81
|
+
status?: 'draft' | 'published' | 'modified';
|
|
82
|
+
[key: string]: any;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface DynamicZoneInfo {
|
|
86
|
+
fieldName: string;
|
|
87
|
+
displayName: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface ConfiguredLocale {
|
|
91
|
+
code: string;
|
|
92
|
+
name: string;
|
|
93
|
+
isDefault?: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface DynamicZoneToolsDialogProps {
|
|
97
|
+
document: any;
|
|
98
|
+
documentId?: string;
|
|
99
|
+
model: string;
|
|
100
|
+
collectionType: string;
|
|
101
|
+
activeTab?: 'draft' | 'published' | null;
|
|
102
|
+
schemaAttributes?: { [key: string]: any } | null;
|
|
103
|
+
onClose: () => void;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const DynamicZoneToolsHeaderAction = ({
|
|
107
|
+
document,
|
|
108
|
+
documentId,
|
|
109
|
+
meta,
|
|
110
|
+
model,
|
|
111
|
+
collectionType,
|
|
112
|
+
activeTab,
|
|
113
|
+
}: any) => {
|
|
114
|
+
const schemaAttributes = meta?.schema?.attributes || meta?.attributes || null;
|
|
115
|
+
if (schemaAttributes) {
|
|
116
|
+
const hasDynamicZone = Object.values(schemaAttributes).some(
|
|
117
|
+
(attr: any) => attr?.type === 'dynamiczone'
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (!hasDynamicZone) return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
type: 'icon',
|
|
125
|
+
icon: <Duplicate />,
|
|
126
|
+
label: 'Copy dynamic zone data',
|
|
127
|
+
dialog: {
|
|
128
|
+
type: 'dialog',
|
|
129
|
+
title: 'Dynamic Zone Tools',
|
|
130
|
+
content: ({ onClose }: { onClose: () => void }) => (
|
|
131
|
+
<>
|
|
132
|
+
<DynamicZoneToolsDialog
|
|
133
|
+
document={document}
|
|
134
|
+
documentId={documentId}
|
|
135
|
+
model={model}
|
|
136
|
+
collectionType={collectionType}
|
|
137
|
+
activeTab={activeTab}
|
|
138
|
+
schemaAttributes={schemaAttributes}
|
|
139
|
+
onClose={onClose}
|
|
140
|
+
/>
|
|
141
|
+
</>
|
|
142
|
+
),
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const DynamicZoneToolsDialog: React.FC<DynamicZoneToolsDialogProps> = ({
|
|
148
|
+
document,
|
|
149
|
+
documentId,
|
|
150
|
+
model,
|
|
151
|
+
collectionType,
|
|
152
|
+
activeTab,
|
|
153
|
+
schemaAttributes,
|
|
154
|
+
onClose,
|
|
155
|
+
}) => {
|
|
156
|
+
const { formatMessage } = useIntl();
|
|
157
|
+
const { get } = useFetchClient();
|
|
158
|
+
const { setValues, values } = useForm('DynamicZoneTools', (state) => ({
|
|
159
|
+
setValues: state.setValues,
|
|
160
|
+
values: state.values,
|
|
161
|
+
}));
|
|
162
|
+
const { toggleNotification } = useNotification();
|
|
163
|
+
|
|
164
|
+
const parsePayloadData = (payload: any) => {
|
|
165
|
+
if (payload?.data && typeof payload.data === 'object') return payload.data;
|
|
166
|
+
if (payload && typeof payload === 'object') return payload;
|
|
167
|
+
return null;
|
|
168
|
+
};
|
|
169
|
+
const getDynamicZonesFromAttributes = (attributes?: { [key: string]: any } | null) =>
|
|
170
|
+
getDynamicZoneFields(attributes);
|
|
171
|
+
|
|
172
|
+
// State management
|
|
173
|
+
const [targetDynamicZones, setTargetDynamicZones] = useState<DynamicZoneInfo[]>([]);
|
|
174
|
+
const [selectedTargetZone, setSelectedTargetZone] = useState<string>('');
|
|
175
|
+
const [contentTypes, setContentTypes] = useState<ContentType[]>([]);
|
|
176
|
+
const [selectedContentType, setSelectedContentType] = useState<string>('');
|
|
177
|
+
const [sourceDynamicZones, setSourceDynamicZones] = useState<DynamicZoneInfo[]>([]);
|
|
178
|
+
const [selectedSourceZone, setSelectedSourceZone] = useState<string>('');
|
|
179
|
+
const [records, setRecords] = useState<SourceRecord[]>([]);
|
|
180
|
+
const [selectedRecord, setSelectedRecord] = useState<string>('');
|
|
181
|
+
const [configuredLocales, setConfiguredLocales] = useState<ConfiguredLocale[]>([]);
|
|
182
|
+
const [selectedLocale, setSelectedLocale] = useState<string>('');
|
|
183
|
+
const [selectedVersion, setSelectedVersion] = useState<'draft' | 'published'>('draft');
|
|
184
|
+
const [recordsLoading, setRecordsLoading] = useState(false);
|
|
185
|
+
const [loading, setLoading] = useState(false);
|
|
186
|
+
const [error, setError] = useState<string>('');
|
|
187
|
+
const [fillMode, setFillMode] = useState<FillMode>('replace');
|
|
188
|
+
const [sourcePreviewLoading, setSourcePreviewLoading] = useState(false);
|
|
189
|
+
const [sourceZoneEntries, setSourceZoneEntries] = useState<any[]>([]);
|
|
190
|
+
const [selectedBlockIndexes, setSelectedBlockIndexes] = useState<Set<number>>(new Set());
|
|
191
|
+
|
|
192
|
+
const sourceContentType = contentTypes.find((ct) => ct.uid === selectedContentType);
|
|
193
|
+
const sourceHasDraftAndPublish = sourceContentType?.draftAndPublish ?? false;
|
|
194
|
+
const sourceIsLocalized = (sourceContentType?.localized ?? false) && configuredLocales.length > 0;
|
|
195
|
+
const selectedRecordEntry = records.find(
|
|
196
|
+
(record) => (record.documentId || record.id.toString()) === selectedRecord
|
|
197
|
+
);
|
|
198
|
+
// The record list is locale-scoped and entries are draft representations;
|
|
199
|
+
// their status field tells us whether a published version exists.
|
|
200
|
+
const selectedRecordHasPublished =
|
|
201
|
+
selectedRecordEntry?.status === 'published' || selectedRecordEntry?.status === 'modified';
|
|
202
|
+
|
|
203
|
+
// Resolve target dynamic zones for the current model
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
if (schemaAttributes) {
|
|
206
|
+
setTargetDynamicZones(getDynamicZonesFromAttributes(schemaAttributes));
|
|
207
|
+
} else {
|
|
208
|
+
const currentType = contentTypes.find((ct) => ct.uid === model);
|
|
209
|
+
setTargetDynamicZones(currentType?.dynamicZones || []);
|
|
210
|
+
}
|
|
211
|
+
}, [schemaAttributes, contentTypes, model]);
|
|
212
|
+
|
|
213
|
+
// Fetch content types with their dynamic zones
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
const fetchContentTypes = async () => {
|
|
216
|
+
try {
|
|
217
|
+
const response = await get('/content-manager/content-types');
|
|
218
|
+
let contentTypesData = response.data || response;
|
|
219
|
+
|
|
220
|
+
if (!Array.isArray(contentTypesData)) {
|
|
221
|
+
if (contentTypesData.data && Array.isArray(contentTypesData.data)) {
|
|
222
|
+
contentTypesData = contentTypesData.data;
|
|
223
|
+
} else if (contentTypesData.results && Array.isArray(contentTypesData.results)) {
|
|
224
|
+
contentTypesData = contentTypesData.results;
|
|
225
|
+
} else {
|
|
226
|
+
contentTypesData = [];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const typesWithDynamicZones = contentTypesData
|
|
231
|
+
.map((ct: any) => {
|
|
232
|
+
const attributes = ct.attributes || ct.schema?.attributes;
|
|
233
|
+
const dynamicZones = getDynamicZonesFromAttributes(attributes);
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
uid: ct.uid,
|
|
237
|
+
displayName: ct.info?.displayName || ct.schema?.displayName || ct.uid,
|
|
238
|
+
draftAndPublish: Boolean(
|
|
239
|
+
ct.options?.draftAndPublish ?? ct.schema?.options?.draftAndPublish
|
|
240
|
+
),
|
|
241
|
+
localized: Boolean(
|
|
242
|
+
ct.pluginOptions?.i18n?.localized ?? ct.schema?.pluginOptions?.i18n?.localized
|
|
243
|
+
),
|
|
244
|
+
dynamicZones,
|
|
245
|
+
};
|
|
246
|
+
})
|
|
247
|
+
.filter((ct: ContentType) => ct.dynamicZones.length > 0);
|
|
248
|
+
|
|
249
|
+
setContentTypes(typesWithDynamicZones);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
setError('Failed to load content types');
|
|
252
|
+
console.error('Error loading content types:', err);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
fetchContentTypes();
|
|
257
|
+
}, [get]);
|
|
258
|
+
|
|
259
|
+
React.useLayoutEffect(() => {
|
|
260
|
+
ensureToolsDialogStyles();
|
|
261
|
+
}, []);
|
|
262
|
+
|
|
263
|
+
const dialogLayoutRef = React.useCallback((node: HTMLDivElement | null) => {
|
|
264
|
+
const markDialog = () => {
|
|
265
|
+
const dialogContent = node?.closest('[role="alertdialog"]');
|
|
266
|
+
|
|
267
|
+
if (dialogContent instanceof HTMLElement) {
|
|
268
|
+
dialogContent.setAttribute('data-dz-tools-dialog', 'true');
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
if (!node) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
markDialog();
|
|
277
|
+
requestAnimationFrame(markDialog);
|
|
278
|
+
}, []);
|
|
279
|
+
|
|
280
|
+
React.useLayoutEffect(() => {
|
|
281
|
+
return () => {
|
|
282
|
+
const dom = globalThis.document;
|
|
283
|
+
|
|
284
|
+
if (!dom) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
dom
|
|
289
|
+
.querySelectorAll('[data-dz-tools-dialog="true"]')
|
|
290
|
+
.forEach((element: Element) => {
|
|
291
|
+
element.removeAttribute('data-dz-tools-dialog');
|
|
292
|
+
});
|
|
293
|
+
};
|
|
294
|
+
}, []);
|
|
295
|
+
|
|
296
|
+
// Update source dynamic zones when content type is selected
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
if (!selectedContentType) {
|
|
299
|
+
setSourceDynamicZones([]);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const selectedType = contentTypes.find((ct) => ct.uid === selectedContentType);
|
|
304
|
+
if (!selectedType) return;
|
|
305
|
+
|
|
306
|
+
setSourceDynamicZones(selectedType.dynamicZones);
|
|
307
|
+
}, [selectedContentType, contentTypes]);
|
|
308
|
+
|
|
309
|
+
// Fetch records for the selected content type (scoped to the source locale
|
|
310
|
+
// for localized types, so only records existing in that locale are listed)
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
const fetchRecords = async () => {
|
|
313
|
+
if (!selectedContentType || (sourceIsLocalized && !selectedLocale)) {
|
|
314
|
+
setRecords([]);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
setRecordsLoading(true);
|
|
320
|
+
// No status filter: list all records; the version select decides
|
|
321
|
+
// whether the draft or published state gets copied.
|
|
322
|
+
const requestParams = new URLSearchParams({
|
|
323
|
+
page: '1',
|
|
324
|
+
pageSize: '500',
|
|
325
|
+
});
|
|
326
|
+
if (sourceIsLocalized) requestParams.set('locale', selectedLocale);
|
|
327
|
+
const response = await get(
|
|
328
|
+
`/content-manager/collection-types/${encodeURIComponent(
|
|
329
|
+
selectedContentType
|
|
330
|
+
)}?${requestParams.toString()}`
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
let recordsData = [];
|
|
334
|
+
if (response.data) {
|
|
335
|
+
if (Array.isArray(response.data)) {
|
|
336
|
+
recordsData = response.data;
|
|
337
|
+
} else if (response.data.results && Array.isArray(response.data.results)) {
|
|
338
|
+
recordsData = response.data.results;
|
|
339
|
+
} else if (response.data.data && Array.isArray(response.data.data)) {
|
|
340
|
+
recordsData = response.data.data;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
setRecords(recordsData);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
setError('Failed to load records');
|
|
347
|
+
console.error('Error loading records:', err);
|
|
348
|
+
} finally {
|
|
349
|
+
setRecordsLoading(false);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
fetchRecords();
|
|
354
|
+
}, [selectedContentType, sourceIsLocalized, selectedLocale, get]);
|
|
355
|
+
|
|
356
|
+
// Configured locales, best-effort (i18n plugin may not be installed)
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
const fetchLocales = async () => {
|
|
359
|
+
try {
|
|
360
|
+
const response = await get('/i18n/locales');
|
|
361
|
+
const locales = Array.isArray(response.data) ? response.data : (response.data?.data ?? []);
|
|
362
|
+
setConfiguredLocales(
|
|
363
|
+
locales
|
|
364
|
+
.filter((locale: any) => locale?.code)
|
|
365
|
+
.map((locale: any) => ({
|
|
366
|
+
code: locale.code,
|
|
367
|
+
name: locale.name || locale.code,
|
|
368
|
+
isDefault: Boolean(locale.isDefault),
|
|
369
|
+
}))
|
|
370
|
+
);
|
|
371
|
+
} catch {
|
|
372
|
+
// Codes/locale picker simply won't be shown
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
fetchLocales();
|
|
377
|
+
}, [get]);
|
|
378
|
+
|
|
379
|
+
// Default the source locale when a localized content type is selected:
|
|
380
|
+
// prefer the locale currently being edited, then Strapi's default locale.
|
|
381
|
+
useEffect(() => {
|
|
382
|
+
if (!selectedContentType || !sourceIsLocalized) {
|
|
383
|
+
setSelectedLocale('');
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const currentLocale = document?.locale ?? null;
|
|
388
|
+
const defaultLocale =
|
|
389
|
+
configuredLocales.find((locale) => locale.code === currentLocale)?.code ??
|
|
390
|
+
configuredLocales.find((locale) => locale.isDefault)?.code ??
|
|
391
|
+
configuredLocales[0]?.code ??
|
|
392
|
+
'';
|
|
393
|
+
setSelectedLocale(defaultLocale);
|
|
394
|
+
}, [selectedContentType, sourceIsLocalized, configuredLocales, document?.locale]);
|
|
395
|
+
|
|
396
|
+
// Component schemas (uid -> attributes), needed to transform copied data the
|
|
397
|
+
// same way the content manager does when it loads a document into the form.
|
|
398
|
+
const [componentSchemas, setComponentSchemas] = useState<Record<string, any> | null>(null);
|
|
399
|
+
|
|
400
|
+
const fetchComponentSchemas = async (): Promise<Record<string, any>> => {
|
|
401
|
+
if (componentSchemas) return componentSchemas;
|
|
402
|
+
|
|
403
|
+
const map = await loadComponentSchemas(get);
|
|
404
|
+
setComponentSchemas(map);
|
|
405
|
+
return map;
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const getRecordLabel = (record: SourceRecord) => {
|
|
409
|
+
const rawLabel = record.title || record.name || `Document ${record.documentId || record.id}`;
|
|
410
|
+
const maxLength = 60;
|
|
411
|
+
|
|
412
|
+
return rawLabel.length > maxLength ? `${rawLabel.substring(0, maxLength)}...` : rawLabel;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const getLocaleLabel = (code: string) => {
|
|
416
|
+
const name = configuredLocales.find((locale) => locale.code === code)?.name;
|
|
417
|
+
if (!name) return code;
|
|
418
|
+
// Strapi locale names usually already include the code, e.g. "German (de)"
|
|
419
|
+
return name.includes(`(${code})`) ? name : `${name} (${code})`;
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const getSelectString = (value: string | number) => String(value);
|
|
423
|
+
|
|
424
|
+
const canFetchSourcePreview =
|
|
425
|
+
Boolean(selectedContentType && selectedSourceZone && selectedRecord) &&
|
|
426
|
+
(!sourceIsLocalized || Boolean(selectedLocale));
|
|
427
|
+
|
|
428
|
+
const buildSourceDocumentQuery = () => {
|
|
429
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
430
|
+
const urlLocale = urlParams.get('plugins[i18n][locale]');
|
|
431
|
+
const locale = selectedLocale || urlLocale;
|
|
432
|
+
const requestParams = new URLSearchParams();
|
|
433
|
+
|
|
434
|
+
if (locale) requestParams.set('locale', locale);
|
|
435
|
+
if (sourceHasDraftAndPublish) requestParams.set('status', selectedVersion);
|
|
436
|
+
|
|
437
|
+
return requestParams.toString() ? `?${requestParams.toString()}` : '';
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// Prefetch source zone blocks for preview and partial selection
|
|
441
|
+
useEffect(() => {
|
|
442
|
+
if (!canFetchSourcePreview) {
|
|
443
|
+
setSourceZoneEntries([]);
|
|
444
|
+
setSelectedBlockIndexes(new Set());
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
let active = true;
|
|
449
|
+
|
|
450
|
+
const fetchSourcePreview = async () => {
|
|
451
|
+
try {
|
|
452
|
+
setSourcePreviewLoading(true);
|
|
453
|
+
setError('');
|
|
454
|
+
|
|
455
|
+
const localeQuery = buildSourceDocumentQuery();
|
|
456
|
+
const response = await get(
|
|
457
|
+
`/${PLUGIN_ID}/source-document/${encodeURIComponent(
|
|
458
|
+
selectedContentType
|
|
459
|
+
)}/${selectedRecord}${localeQuery}`
|
|
460
|
+
);
|
|
461
|
+
const sourceDocument = parsePayloadData(response.data ?? response);
|
|
462
|
+
const zoneData = sourceDocument?.[selectedSourceZone];
|
|
463
|
+
|
|
464
|
+
if (!active) return;
|
|
465
|
+
|
|
466
|
+
if (!Array.isArray(zoneData) || zoneData.length === 0) {
|
|
467
|
+
setSourceZoneEntries([]);
|
|
468
|
+
setSelectedBlockIndexes(new Set());
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
setSourceZoneEntries(zoneData);
|
|
473
|
+
setSelectedBlockIndexes(new Set(zoneData.map((_, index) => index)));
|
|
474
|
+
} catch {
|
|
475
|
+
if (active) {
|
|
476
|
+
setSourceZoneEntries([]);
|
|
477
|
+
setSelectedBlockIndexes(new Set());
|
|
478
|
+
}
|
|
479
|
+
} finally {
|
|
480
|
+
if (active) {
|
|
481
|
+
setSourcePreviewLoading(false);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
void fetchSourcePreview();
|
|
487
|
+
|
|
488
|
+
return () => {
|
|
489
|
+
active = false;
|
|
490
|
+
};
|
|
491
|
+
}, [
|
|
492
|
+
canFetchSourcePreview,
|
|
493
|
+
get,
|
|
494
|
+
selectedContentType,
|
|
495
|
+
selectedRecord,
|
|
496
|
+
selectedSourceZone,
|
|
497
|
+
selectedLocale,
|
|
498
|
+
selectedVersion,
|
|
499
|
+
sourceHasDraftAndPublish,
|
|
500
|
+
]);
|
|
501
|
+
|
|
502
|
+
const selectAllBlocks = () => {
|
|
503
|
+
setSelectedBlockIndexes(new Set(sourceZoneEntries.map((_, index) => index)));
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const clearAllBlocks = () => {
|
|
507
|
+
setSelectedBlockIndexes(new Set());
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
const handleConfirmFill = async () => {
|
|
511
|
+
if (!selectedTargetZone || !selectedContentType || !selectedSourceZone || !selectedRecord)
|
|
512
|
+
return;
|
|
513
|
+
|
|
514
|
+
if (selectedBlockIndexes.size === 0) {
|
|
515
|
+
setError('Select at least one block to copy');
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
setLoading(true);
|
|
521
|
+
setError('');
|
|
522
|
+
|
|
523
|
+
const schemas = await fetchComponentSchemas();
|
|
524
|
+
const sourceData =
|
|
525
|
+
sourceZoneEntries.length > 0
|
|
526
|
+
? sourceZoneEntries
|
|
527
|
+
: (() => {
|
|
528
|
+
throw new Error('Source blocks are not loaded yet');
|
|
529
|
+
})();
|
|
530
|
+
|
|
531
|
+
const selectedEntries = sourceData.filter((_, index) => selectedBlockIndexes.has(index));
|
|
532
|
+
|
|
533
|
+
const allowedComponents: string[] | undefined =
|
|
534
|
+
schemaAttributes?.[selectedTargetZone]?.components;
|
|
535
|
+
const { allowed: allowedData, skippedCount } = filterAllowedDynamicZoneEntries(
|
|
536
|
+
selectedEntries,
|
|
537
|
+
allowedComponents
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
if (allowedData.length === 0) {
|
|
541
|
+
setError('None of the selected components are allowed in the target dynamic zone');
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const currentZone = Array.isArray(values?.[selectedTargetZone])
|
|
546
|
+
? (values[selectedTargetZone] as any[])
|
|
547
|
+
: [];
|
|
548
|
+
|
|
549
|
+
const cleanedData =
|
|
550
|
+
fillMode === 'append'
|
|
551
|
+
? prepareZoneDataForInsert(allowedData, schemas, {
|
|
552
|
+
previousKey: currentZone.at(-1)?.__temp_key__ ?? null,
|
|
553
|
+
nextKey: null,
|
|
554
|
+
})
|
|
555
|
+
: prepareZoneData(allowedData, schemas);
|
|
556
|
+
|
|
557
|
+
setValues({
|
|
558
|
+
...values,
|
|
559
|
+
[selectedTargetZone]:
|
|
560
|
+
fillMode === 'append' ? [...currentZone, ...cleanedData] : cleanedData,
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
toggleNotification({
|
|
564
|
+
type: skippedCount > 0 ? 'warning' : 'success',
|
|
565
|
+
message:
|
|
566
|
+
skippedCount > 0
|
|
567
|
+
? formatMessage(
|
|
568
|
+
{
|
|
569
|
+
id: `${PLUGIN_ID}.notification.partial`,
|
|
570
|
+
defaultMessage:
|
|
571
|
+
'Content copied, but {count} component(s) were skipped because the target zone does not allow them.',
|
|
572
|
+
},
|
|
573
|
+
{ count: skippedCount }
|
|
574
|
+
)
|
|
575
|
+
: formatMessage({
|
|
576
|
+
id:
|
|
577
|
+
fillMode === 'append'
|
|
578
|
+
? `${PLUGIN_ID}.notification.append-success`
|
|
579
|
+
: `${PLUGIN_ID}.notification.success`,
|
|
580
|
+
defaultMessage:
|
|
581
|
+
fillMode === 'append'
|
|
582
|
+
? 'Selected blocks appended successfully!'
|
|
583
|
+
: 'Dynamic zone content copied successfully!',
|
|
584
|
+
}),
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
setSelectedTargetZone('');
|
|
588
|
+
setSelectedContentType('');
|
|
589
|
+
setSelectedSourceZone('');
|
|
590
|
+
setSelectedRecord('');
|
|
591
|
+
setSelectedVersion('draft');
|
|
592
|
+
setFillMode('replace');
|
|
593
|
+
setSourceZoneEntries([]);
|
|
594
|
+
setSelectedBlockIndexes(new Set());
|
|
595
|
+
|
|
596
|
+
onClose();
|
|
597
|
+
} catch (err) {
|
|
598
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to fill in data';
|
|
599
|
+
setError(errorMessage);
|
|
600
|
+
toggleNotification({
|
|
601
|
+
type: 'warning',
|
|
602
|
+
message: errorMessage,
|
|
603
|
+
});
|
|
604
|
+
} finally {
|
|
605
|
+
setLoading(false);
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
const isFormValid =
|
|
610
|
+
selectedTargetZone &&
|
|
611
|
+
selectedContentType &&
|
|
612
|
+
selectedSourceZone &&
|
|
613
|
+
selectedRecord &&
|
|
614
|
+
(!sourceIsLocalized || selectedLocale) &&
|
|
615
|
+
selectedBlockIndexes.size > 0 &&
|
|
616
|
+
!sourcePreviewLoading;
|
|
617
|
+
|
|
618
|
+
return (
|
|
619
|
+
<Flex
|
|
620
|
+
ref={dialogLayoutRef}
|
|
621
|
+
data-dz-tools-shell="true"
|
|
622
|
+
direction="column"
|
|
623
|
+
alignItems="stretch"
|
|
624
|
+
width="100%"
|
|
625
|
+
>
|
|
626
|
+
<Dialog.Body data-dz-tools-body="true">
|
|
627
|
+
<Flex direction="column" gap={4} width="100%" alignItems="stretch">
|
|
628
|
+
<Flex
|
|
629
|
+
gap={3}
|
|
630
|
+
padding={4}
|
|
631
|
+
background={fillMode === 'replace' ? 'danger100' : 'primary100'}
|
|
632
|
+
hasRadius
|
|
633
|
+
alignItems="flex-start"
|
|
634
|
+
width="100%"
|
|
635
|
+
>
|
|
636
|
+
<Box style={{ flexShrink: 0, display: 'flex' }} paddingTop={1}>
|
|
637
|
+
<WarningCircle fill={fillMode === 'replace' ? 'danger600' : 'primary600'} />
|
|
638
|
+
</Box>
|
|
639
|
+
<Flex direction="column" gap={1} alignItems="flex-start">
|
|
640
|
+
<Typography
|
|
641
|
+
fontWeight="semiBold"
|
|
642
|
+
textColor={fillMode === 'replace' ? 'danger700' : 'primary700'}
|
|
643
|
+
>
|
|
644
|
+
{fillMode === 'replace'
|
|
645
|
+
? formatMessage({
|
|
646
|
+
id: `${PLUGIN_ID}.modal.warning`,
|
|
647
|
+
defaultMessage:
|
|
648
|
+
'This will replace the current content of the target dynamic zone.',
|
|
649
|
+
})
|
|
650
|
+
: formatMessage({
|
|
651
|
+
id: `${PLUGIN_ID}.modal.append-info`,
|
|
652
|
+
defaultMessage:
|
|
653
|
+
'Selected blocks will be appended to the end of the target dynamic zone.',
|
|
654
|
+
})}
|
|
655
|
+
</Typography>
|
|
656
|
+
<Typography
|
|
657
|
+
variant="pi"
|
|
658
|
+
textColor={fillMode === 'replace' ? 'danger600' : 'primary600'}
|
|
659
|
+
>
|
|
660
|
+
{formatMessage({
|
|
661
|
+
id: `${PLUGIN_ID}.modal.warning.helper`,
|
|
662
|
+
defaultMessage:
|
|
663
|
+
'Nothing is saved yet — review the result and save the document to keep it.',
|
|
664
|
+
})}
|
|
665
|
+
</Typography>
|
|
666
|
+
</Flex>
|
|
667
|
+
</Flex>
|
|
668
|
+
|
|
669
|
+
{error && (
|
|
670
|
+
<Alert closeLabel="Close" title="Error" variant="danger" onClose={() => setError('')}>
|
|
671
|
+
{error}
|
|
672
|
+
</Alert>
|
|
673
|
+
)}
|
|
674
|
+
|
|
675
|
+
{/* Fill mode */}
|
|
676
|
+
<Flex direction="column" gap={1} width="100%">
|
|
677
|
+
<Typography variant="pi" fontWeight="bold" textColor="neutral800">
|
|
678
|
+
{formatMessage({
|
|
679
|
+
id: `${PLUGIN_ID}.fill-mode.label`,
|
|
680
|
+
defaultMessage: 'Copy mode',
|
|
681
|
+
})}
|
|
682
|
+
</Typography>
|
|
683
|
+
<SingleSelect
|
|
684
|
+
value={fillMode}
|
|
685
|
+
onChange={(value: string | number) => setFillMode(value === 'append' ? 'append' : 'replace')}
|
|
686
|
+
disabled={loading}
|
|
687
|
+
size="S"
|
|
688
|
+
>
|
|
689
|
+
<SingleSelectOption value="replace">
|
|
690
|
+
{formatMessage({
|
|
691
|
+
id: `${PLUGIN_ID}.fill-mode.replace`,
|
|
692
|
+
defaultMessage: 'Replace current zone',
|
|
693
|
+
})}
|
|
694
|
+
</SingleSelectOption>
|
|
695
|
+
<SingleSelectOption value="append">
|
|
696
|
+
{formatMessage({
|
|
697
|
+
id: `${PLUGIN_ID}.fill-mode.append`,
|
|
698
|
+
defaultMessage: 'Append to current zone',
|
|
699
|
+
})}
|
|
700
|
+
</SingleSelectOption>
|
|
701
|
+
</SingleSelect>
|
|
702
|
+
</Flex>
|
|
703
|
+
|
|
704
|
+
{/* Target Dynamic Zone Selection */}
|
|
705
|
+
<Flex direction="column" gap={1} width="100%">
|
|
706
|
+
<Typography variant="pi" fontWeight="bold" textColor="neutral800">
|
|
707
|
+
{formatMessage({
|
|
708
|
+
id: `${PLUGIN_ID}.target-zone.label`,
|
|
709
|
+
defaultMessage: 'Target Dynamic Zone',
|
|
710
|
+
})}
|
|
711
|
+
</Typography>
|
|
712
|
+
<SingleSelect
|
|
713
|
+
placeholder={formatMessage({
|
|
714
|
+
id: `${PLUGIN_ID}.target-zone.placeholder`,
|
|
715
|
+
defaultMessage: 'Select zone to fill',
|
|
716
|
+
})}
|
|
717
|
+
value={selectedTargetZone}
|
|
718
|
+
onChange={(value: string | number) => {
|
|
719
|
+
setSelectedTargetZone(getSelectString(value));
|
|
720
|
+
setSelectedContentType('');
|
|
721
|
+
setSelectedSourceZone('');
|
|
722
|
+
setSelectedRecord('');
|
|
723
|
+
setSelectedVersion('draft');
|
|
724
|
+
}}
|
|
725
|
+
disabled={loading}
|
|
726
|
+
size="S"
|
|
727
|
+
>
|
|
728
|
+
{targetDynamicZones.map((zone) => (
|
|
729
|
+
<SingleSelectOption key={zone.fieldName} value={zone.fieldName}>
|
|
730
|
+
{zone.displayName}
|
|
731
|
+
</SingleSelectOption>
|
|
732
|
+
))}
|
|
733
|
+
</SingleSelect>
|
|
734
|
+
</Flex>
|
|
735
|
+
|
|
736
|
+
{/* Source Collection Selection */}
|
|
737
|
+
<Flex direction="column" gap={1} width="100%">
|
|
738
|
+
<Typography variant="pi" fontWeight="bold" textColor="neutral800">
|
|
739
|
+
{formatMessage({
|
|
740
|
+
id: `${PLUGIN_ID}.source-collection.label`,
|
|
741
|
+
defaultMessage: 'Source Collection',
|
|
742
|
+
})}
|
|
743
|
+
</Typography>
|
|
744
|
+
<SingleSelect
|
|
745
|
+
placeholder={formatMessage({
|
|
746
|
+
id: `${PLUGIN_ID}.source-collection.placeholder`,
|
|
747
|
+
defaultMessage: 'Select source collection',
|
|
748
|
+
})}
|
|
749
|
+
value={selectedContentType}
|
|
750
|
+
onChange={(value: string | number) => {
|
|
751
|
+
setSelectedContentType(getSelectString(value));
|
|
752
|
+
setSelectedSourceZone('');
|
|
753
|
+
setSelectedRecord('');
|
|
754
|
+
setSelectedVersion('draft');
|
|
755
|
+
}}
|
|
756
|
+
disabled={!selectedTargetZone || loading}
|
|
757
|
+
size="S"
|
|
758
|
+
>
|
|
759
|
+
{contentTypes.map((ct) => (
|
|
760
|
+
<SingleSelectOption key={ct.uid} value={ct.uid}>
|
|
761
|
+
{ct.displayName}
|
|
762
|
+
</SingleSelectOption>
|
|
763
|
+
))}
|
|
764
|
+
</SingleSelect>
|
|
765
|
+
</Flex>
|
|
766
|
+
|
|
767
|
+
{/* Source Dynamic Zone Selection */}
|
|
768
|
+
<Flex direction="column" gap={1} width="100%">
|
|
769
|
+
<Typography variant="pi" fontWeight="bold" textColor="neutral800">
|
|
770
|
+
{formatMessage({
|
|
771
|
+
id: `${PLUGIN_ID}.source-zone.label`,
|
|
772
|
+
defaultMessage: 'Source Dynamic Zone',
|
|
773
|
+
})}
|
|
774
|
+
</Typography>
|
|
775
|
+
<SingleSelect
|
|
776
|
+
placeholder={formatMessage({
|
|
777
|
+
id: `${PLUGIN_ID}.source-zone.placeholder`,
|
|
778
|
+
defaultMessage: 'Select zone to copy from',
|
|
779
|
+
})}
|
|
780
|
+
value={selectedSourceZone}
|
|
781
|
+
onChange={(value: string | number) => {
|
|
782
|
+
setSelectedSourceZone(getSelectString(value));
|
|
783
|
+
setSelectedRecord('');
|
|
784
|
+
setSelectedVersion('draft');
|
|
785
|
+
}}
|
|
786
|
+
disabled={!selectedContentType || loading}
|
|
787
|
+
size="S"
|
|
788
|
+
>
|
|
789
|
+
{sourceDynamicZones.map((zone) => (
|
|
790
|
+
<SingleSelectOption key={zone.fieldName} value={zone.fieldName}>
|
|
791
|
+
{zone.displayName}
|
|
792
|
+
</SingleSelectOption>
|
|
793
|
+
))}
|
|
794
|
+
</SingleSelect>
|
|
795
|
+
</Flex>
|
|
796
|
+
|
|
797
|
+
{/* Source Locale Selection (localized content types only) */}
|
|
798
|
+
{sourceIsLocalized && (
|
|
799
|
+
<Flex direction="column" gap={1} width="100%">
|
|
800
|
+
<Typography variant="pi" fontWeight="bold" textColor="neutral800">
|
|
801
|
+
{formatMessage({
|
|
802
|
+
id: `${PLUGIN_ID}.source-locale.label`,
|
|
803
|
+
defaultMessage: 'Source Locale',
|
|
804
|
+
})}
|
|
805
|
+
</Typography>
|
|
806
|
+
<SingleSelect
|
|
807
|
+
value={selectedLocale}
|
|
808
|
+
onChange={(value: string | number) => {
|
|
809
|
+
setSelectedLocale(getSelectString(value));
|
|
810
|
+
setSelectedRecord('');
|
|
811
|
+
setSelectedVersion('draft');
|
|
812
|
+
}}
|
|
813
|
+
disabled={!selectedSourceZone || loading}
|
|
814
|
+
size="S"
|
|
815
|
+
>
|
|
816
|
+
{configuredLocales.map((locale) => (
|
|
817
|
+
<SingleSelectOption key={locale.code} value={locale.code}>
|
|
818
|
+
{getLocaleLabel(locale.code)}
|
|
819
|
+
</SingleSelectOption>
|
|
820
|
+
))}
|
|
821
|
+
</SingleSelect>
|
|
822
|
+
</Flex>
|
|
823
|
+
)}
|
|
824
|
+
|
|
825
|
+
{/* Source Record Selection */}
|
|
826
|
+
<Flex direction="column" gap={1} width="100%">
|
|
827
|
+
<Typography variant="pi" fontWeight="bold" textColor="neutral800">
|
|
828
|
+
{formatMessage({
|
|
829
|
+
id: `${PLUGIN_ID}.source-record.label`,
|
|
830
|
+
defaultMessage: 'Source Record',
|
|
831
|
+
})}
|
|
832
|
+
</Typography>
|
|
833
|
+
<SingleSelect
|
|
834
|
+
placeholder={
|
|
835
|
+
recordsLoading
|
|
836
|
+
? formatMessage({
|
|
837
|
+
id: `${PLUGIN_ID}.source-record.loading`,
|
|
838
|
+
defaultMessage: 'Loading records...',
|
|
839
|
+
})
|
|
840
|
+
: formatMessage({
|
|
841
|
+
id: `${PLUGIN_ID}.source-record.placeholder`,
|
|
842
|
+
defaultMessage: 'Select record to copy from',
|
|
843
|
+
})
|
|
844
|
+
}
|
|
845
|
+
value={selectedRecord}
|
|
846
|
+
onChange={(value: string | number) => {
|
|
847
|
+
setSelectedRecord(getSelectString(value));
|
|
848
|
+
setSelectedVersion('draft');
|
|
849
|
+
}}
|
|
850
|
+
disabled={
|
|
851
|
+
!selectedSourceZone ||
|
|
852
|
+
recordsLoading ||
|
|
853
|
+
loading ||
|
|
854
|
+
(sourceIsLocalized && !selectedLocale)
|
|
855
|
+
}
|
|
856
|
+
size="S"
|
|
857
|
+
>
|
|
858
|
+
{records.map((record) => (
|
|
859
|
+
<SingleSelectOption
|
|
860
|
+
key={record.documentId || record.id}
|
|
861
|
+
value={record.documentId || record.id.toString()}
|
|
862
|
+
>
|
|
863
|
+
{getRecordLabel(record)}
|
|
864
|
+
</SingleSelectOption>
|
|
865
|
+
))}
|
|
866
|
+
</SingleSelect>
|
|
867
|
+
</Flex>
|
|
868
|
+
|
|
869
|
+
{/* Source Version Selection (draft & publish content types only) */}
|
|
870
|
+
{sourceHasDraftAndPublish && (
|
|
871
|
+
<Flex direction="column" gap={1} width="100%">
|
|
872
|
+
<Typography variant="pi" fontWeight="bold" textColor="neutral800">
|
|
873
|
+
{formatMessage({
|
|
874
|
+
id: `${PLUGIN_ID}.source-version.label`,
|
|
875
|
+
defaultMessage: 'Source Version',
|
|
876
|
+
})}
|
|
877
|
+
</Typography>
|
|
878
|
+
<SingleSelect
|
|
879
|
+
value={selectedVersion}
|
|
880
|
+
onChange={(value: string | number) =>
|
|
881
|
+
setSelectedVersion(value === 'published' ? 'published' : 'draft')
|
|
882
|
+
}
|
|
883
|
+
disabled={!selectedRecord || loading}
|
|
884
|
+
size="S"
|
|
885
|
+
>
|
|
886
|
+
<SingleSelectOption value="draft">
|
|
887
|
+
{formatMessage({
|
|
888
|
+
id: `${PLUGIN_ID}.source-version.draft`,
|
|
889
|
+
defaultMessage: 'Draft',
|
|
890
|
+
})}
|
|
891
|
+
</SingleSelectOption>
|
|
892
|
+
<SingleSelectOption value="published" disabled={!selectedRecordHasPublished}>
|
|
893
|
+
{selectedRecord && !selectedRecordHasPublished
|
|
894
|
+
? formatMessage({
|
|
895
|
+
id: `${PLUGIN_ID}.source-version.published-unavailable`,
|
|
896
|
+
defaultMessage: 'Published (not available)',
|
|
897
|
+
})
|
|
898
|
+
: formatMessage({
|
|
899
|
+
id: `${PLUGIN_ID}.source-version.published`,
|
|
900
|
+
defaultMessage: 'Published',
|
|
901
|
+
})}
|
|
902
|
+
</SingleSelectOption>
|
|
903
|
+
</SingleSelect>
|
|
904
|
+
</Flex>
|
|
905
|
+
)}
|
|
906
|
+
|
|
907
|
+
{/* Source blocks preview */}
|
|
908
|
+
{canFetchSourcePreview && (
|
|
909
|
+
<Flex direction="column" gap={2} width="100%">
|
|
910
|
+
<Flex justifyContent="space-between" alignItems="center" width="100%">
|
|
911
|
+
<Typography variant="pi" fontWeight="bold" textColor="neutral800">
|
|
912
|
+
{formatMessage({
|
|
913
|
+
id: `${PLUGIN_ID}.source-blocks.label`,
|
|
914
|
+
defaultMessage: 'Blocks to copy',
|
|
915
|
+
})}
|
|
916
|
+
</Typography>
|
|
917
|
+
<Flex gap={2}>
|
|
918
|
+
<Button variant="tertiary" size="S" onClick={selectAllBlocks} disabled={loading}>
|
|
919
|
+
{formatMessage({
|
|
920
|
+
id: `${PLUGIN_ID}.source-blocks.select-all`,
|
|
921
|
+
defaultMessage: 'Select all',
|
|
922
|
+
})}
|
|
923
|
+
</Button>
|
|
924
|
+
<Button variant="tertiary" size="S" onClick={clearAllBlocks} disabled={loading}>
|
|
925
|
+
{formatMessage({
|
|
926
|
+
id: `${PLUGIN_ID}.source-blocks.clear-all`,
|
|
927
|
+
defaultMessage: 'Clear all',
|
|
928
|
+
})}
|
|
929
|
+
</Button>
|
|
930
|
+
</Flex>
|
|
931
|
+
</Flex>
|
|
932
|
+
|
|
933
|
+
{sourcePreviewLoading ? (
|
|
934
|
+
<Typography variant="pi" textColor="neutral600">
|
|
935
|
+
{formatMessage({
|
|
936
|
+
id: `${PLUGIN_ID}.source-blocks.loading`,
|
|
937
|
+
defaultMessage: 'Loading source blocks...',
|
|
938
|
+
})}
|
|
939
|
+
</Typography>
|
|
940
|
+
) : sourceZoneEntries.length === 0 ? (
|
|
941
|
+
<Typography variant="pi" textColor="neutral600">
|
|
942
|
+
{formatMessage({
|
|
943
|
+
id: `${PLUGIN_ID}.source-blocks.empty`,
|
|
944
|
+
defaultMessage: 'No blocks found in the selected source zone.',
|
|
945
|
+
})}
|
|
946
|
+
</Typography>
|
|
947
|
+
) : (
|
|
948
|
+
<Flex
|
|
949
|
+
direction="column"
|
|
950
|
+
gap={2}
|
|
951
|
+
width="100%"
|
|
952
|
+
padding={3}
|
|
953
|
+
background="neutral100"
|
|
954
|
+
hasRadius
|
|
955
|
+
>
|
|
956
|
+
{sourceZoneEntries.map((entry, index) => {
|
|
957
|
+
const componentUid = entry?.__component ?? '';
|
|
958
|
+
const label = getDynamicZoneEntryLabel(entry);
|
|
959
|
+
const componentName = getComponentDisplayName(componentUid);
|
|
960
|
+
const allowedComponents: string[] | undefined =
|
|
961
|
+
schemaAttributes?.[selectedTargetZone]?.components;
|
|
962
|
+
const isAllowed =
|
|
963
|
+
!Array.isArray(allowedComponents) ||
|
|
964
|
+
allowedComponents.includes(componentUid);
|
|
965
|
+
|
|
966
|
+
return (
|
|
967
|
+
<Checkbox
|
|
968
|
+
key={`${componentUid}-${index}`}
|
|
969
|
+
checked={selectedBlockIndexes.has(index)}
|
|
970
|
+
onCheckedChange={(checked: boolean | 'indeterminate') => {
|
|
971
|
+
setSelectedBlockIndexes((current) => {
|
|
972
|
+
const next = new Set(current);
|
|
973
|
+
|
|
974
|
+
if (checked === true) {
|
|
975
|
+
next.add(index);
|
|
976
|
+
} else {
|
|
977
|
+
next.delete(index);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
return next;
|
|
981
|
+
});
|
|
982
|
+
}}
|
|
983
|
+
disabled={loading || !isAllowed}
|
|
984
|
+
>
|
|
985
|
+
{componentName}
|
|
986
|
+
{label ? ` · ${label}` : ''}
|
|
987
|
+
{!isAllowed
|
|
988
|
+
? ` (${formatMessage({
|
|
989
|
+
id: `${PLUGIN_ID}.source-blocks.not-allowed`,
|
|
990
|
+
defaultMessage: 'not allowed in target zone',
|
|
991
|
+
})})`
|
|
992
|
+
: ''}
|
|
993
|
+
</Checkbox>
|
|
994
|
+
);
|
|
995
|
+
})}
|
|
996
|
+
</Flex>
|
|
997
|
+
)}
|
|
998
|
+
</Flex>
|
|
999
|
+
)}
|
|
1000
|
+
</Flex>
|
|
1001
|
+
</Dialog.Body>
|
|
1002
|
+
|
|
1003
|
+
<Dialog.Footer data-dz-tools-footer="true">
|
|
1004
|
+
<Button variant="tertiary" fullWidth onClick={onClose} disabled={loading}>
|
|
1005
|
+
{formatMessage({
|
|
1006
|
+
id: `${PLUGIN_ID}.modal.cancel`,
|
|
1007
|
+
defaultMessage: 'No, cancel',
|
|
1008
|
+
})}
|
|
1009
|
+
</Button>
|
|
1010
|
+
<Button
|
|
1011
|
+
variant="default"
|
|
1012
|
+
fullWidth
|
|
1013
|
+
onClick={handleConfirmFill}
|
|
1014
|
+
loading={loading}
|
|
1015
|
+
disabled={!isFormValid}
|
|
1016
|
+
>
|
|
1017
|
+
{formatMessage({
|
|
1018
|
+
id: `${PLUGIN_ID}.modal.confirm`,
|
|
1019
|
+
defaultMessage: 'Yes, fill in',
|
|
1020
|
+
})}
|
|
1021
|
+
</Button>
|
|
1022
|
+
</Dialog.Footer>
|
|
1023
|
+
</Flex>
|
|
1024
|
+
);
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
export { DynamicZoneToolsHeaderAction };
|