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.
Files changed (53) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/CONTRIBUTING.md +38 -0
  3. package/LICENSE +21 -0
  4. package/README.md +186 -0
  5. package/admin/custom.d.ts +2 -0
  6. package/admin/src/components/DynamicZoneComponentDuplicateInjector.tsx +604 -0
  7. package/admin/src/components/DynamicZoneEditViewExtensions.tsx +7 -0
  8. package/admin/src/components/DynamicZoneToolsPanel.tsx +1027 -0
  9. package/admin/src/components/FillFromRecord.tsx +36 -0
  10. package/admin/src/components/Initializer.tsx +19 -0
  11. package/admin/src/components/PluginIcon.tsx +5 -0
  12. package/admin/src/index.ts +61 -0
  13. package/admin/src/pages/App.tsx +15 -0
  14. package/admin/src/pages/HomePage.tsx +16 -0
  15. package/admin/src/pluginId.ts +1 -0
  16. package/admin/src/translations/en.json +51 -0
  17. package/admin/src/utils/createRowActionButton.ts +57 -0
  18. package/admin/src/utils/createRowActionMenu.ts +276 -0
  19. package/admin/src/utils/dynamicZoneClipboard.ts +134 -0
  20. package/admin/src/utils/dynamicZonePaths.ts +236 -0
  21. package/admin/src/utils/getTranslation.ts +5 -0
  22. package/admin/src/utils/prepareDynamicZoneData.ts +625 -0
  23. package/admin/src/utils/relationQueryParams.ts +19 -0
  24. package/admin/tsconfig.build.json +10 -0
  25. package/admin/tsconfig.json +12 -0
  26. package/dist/admin/en-Ce0ZP0MJ.js +54 -0
  27. package/dist/admin/en-DrSdJbJW.mjs +54 -0
  28. package/dist/admin/index.js +2161 -0
  29. package/dist/admin/index.mjs +2159 -0
  30. package/dist/admin/src/index.d.ts +12 -0
  31. package/dist/server/index.js +137 -0
  32. package/dist/server/index.mjs +137 -0
  33. package/dist/server/src/index.d.ts +55 -0
  34. package/package.json +112 -0
  35. package/server/src/bootstrap.ts +18 -0
  36. package/server/src/config/index.ts +4 -0
  37. package/server/src/content-types/index.ts +1 -0
  38. package/server/src/controllers/controller.ts +85 -0
  39. package/server/src/controllers/index.ts +5 -0
  40. package/server/src/destroy.ts +7 -0
  41. package/server/src/index.ts +30 -0
  42. package/server/src/middlewares/index.ts +1 -0
  43. package/server/src/policies/index.ts +1 -0
  44. package/server/src/register.ts +7 -0
  45. package/server/src/routes/admin-api.ts +18 -0
  46. package/server/src/routes/content-api.ts +1 -0
  47. package/server/src/routes/index.ts +15 -0
  48. package/server/src/services/index.ts +5 -0
  49. package/server/src/services/service.ts +9 -0
  50. package/server/tsconfig.build.json +10 -0
  51. package/server/tsconfig.json +11 -0
  52. package/strapi-admin.js +3 -0
  53. package/strapi-server.js +3 -0
@@ -0,0 +1,625 @@
1
+ import { generateKeyBetween, generateNKeysBetween } from 'fractional-indexing';
2
+
3
+ export type ComponentSchemas = Record<string, Record<string, any>>;
4
+
5
+ export interface PrepareZoneDataInsertOptions {
6
+ /** Fractional key of the last existing block when appending. */
7
+ previousKey?: string | null;
8
+ /** Fractional key of the block after the insertion range, if any. */
9
+ nextKey?: string | null;
10
+ }
11
+
12
+ const TRANSIENT_ENTRY_KEYS = new Set(['id', 'documentId', '__temp_key__']);
13
+
14
+ const ENTRY_LABEL_KEYS = ['title', 'heading', 'name', 'label', 'displayName', 'question', 'slug'];
15
+
16
+ /** Human-readable label for a dynamic zone block (outline, checklists). */
17
+ export function getDynamicZoneEntryLabel(entry: Record<string, any> | null | undefined): string {
18
+ if (!entry || typeof entry !== 'object') {
19
+ return '';
20
+ }
21
+
22
+ for (const key of ENTRY_LABEL_KEYS) {
23
+ const value = entry[key];
24
+
25
+ if (typeof value === 'string' && value.trim()) {
26
+ return value.trim();
27
+ }
28
+ }
29
+
30
+ const componentUid = entry.__component;
31
+
32
+ if (typeof componentUid === 'string') {
33
+ const shortName = componentUid.split('.').pop() || componentUid;
34
+ return shortName;
35
+ }
36
+
37
+ return '';
38
+ }
39
+
40
+ /** Short display name for a component UID (e.g. sections.hero-v3 → hero-v3). */
41
+ export function getComponentDisplayName(componentUid: string): string {
42
+ return componentUid.split('.').pop() || componentUid;
43
+ }
44
+
45
+ export interface FilterAllowedResult {
46
+ allowed: any[];
47
+ skippedCount: number;
48
+ }
49
+
50
+ /** Keeps only entries whose __component is allowed in the target dynamic zone. */
51
+ export function filterAllowedDynamicZoneEntries(
52
+ entries: any[],
53
+ allowedComponents?: string[] | null
54
+ ): FilterAllowedResult {
55
+ if (!Array.isArray(entries)) {
56
+ return { allowed: [], skippedCount: 0 };
57
+ }
58
+
59
+ if (!Array.isArray(allowedComponents)) {
60
+ return { allowed: entries, skippedCount: 0 };
61
+ }
62
+
63
+ const allowed = entries.filter((entry) => allowedComponents.includes(entry?.__component));
64
+ return { allowed, skippedCount: entries.length - allowed.length };
65
+ }
66
+
67
+ /**
68
+ * Loads component attribute maps from the Content Manager init endpoint.
69
+ */
70
+ export async function loadComponentSchemas(
71
+ get: (url: string) => Promise<{ data?: any }>
72
+ ): Promise<ComponentSchemas> {
73
+ const response = await get('/content-manager/init');
74
+ const payload = response?.data?.data ?? response?.data ?? {};
75
+ const map: ComponentSchemas = {};
76
+
77
+ (payload.components ?? []).forEach((component: any) => {
78
+ map[component.uid] = component.attributes ?? component.schema?.attributes ?? {};
79
+ });
80
+
81
+ return map;
82
+ }
83
+
84
+ /**
85
+ * Transforms deep-populated API dynamic zone entries into Content Manager form shape.
86
+ * Used when copying from another record via the fill-from-record workflow.
87
+ */
88
+ export function prepareZoneData(
89
+ entries: any[],
90
+ schemas: ComponentSchemas,
91
+ insertOptions?: PrepareZoneDataInsertOptions
92
+ ): any[] {
93
+ const buildConnectItems = (value: any, attribute: any) => {
94
+ const items = (Array.isArray(value) ? value : [value]).filter(
95
+ (rel) => rel && typeof rel === 'object' && rel.documentId
96
+ );
97
+ const keys = generateNKeysBetween(undefined, undefined, items.length);
98
+
99
+ return items.map((rel: any, index: number) => ({
100
+ id: rel.id,
101
+ apiData: {
102
+ id: rel.id,
103
+ documentId: rel.documentId,
104
+ locale: rel.locale ?? undefined,
105
+ isTemporary: true,
106
+ },
107
+ status: rel.status ?? (rel.publishedAt ? 'published' : 'draft'),
108
+ label: rel.title ?? rel.name ?? rel.label ?? rel.documentId,
109
+ href: `../collection-types/${attribute.target}/${rel.documentId}${
110
+ rel.locale ? `?plugins[i18n][locale]=${rel.locale}` : ''
111
+ }`,
112
+ __temp_key__: keys[index],
113
+ }));
114
+ };
115
+
116
+ const transformComponent = (data: any, attributes: Record<string, any>): any => {
117
+ const out: Record<string, any> = {};
118
+
119
+ Object.entries(data ?? {}).forEach(([key, value]) => {
120
+ if (key === 'id') return;
121
+ if (key === '__component') {
122
+ out[key] = value;
123
+ return;
124
+ }
125
+
126
+ const attribute = attributes?.[key];
127
+ if (!attribute) return;
128
+
129
+ if (value === null || value === undefined) {
130
+ if (attribute.type === 'boolean') out[key] = value;
131
+ return;
132
+ }
133
+
134
+ switch (attribute.type) {
135
+ case 'component': {
136
+ const nestedAttributes = schemas[attribute.component] ?? {};
137
+ if (attribute.repeatable) {
138
+ const items = Array.isArray(value) ? value : [];
139
+ const keys = generateNKeysBetween(undefined, undefined, items.length);
140
+ out[key] = items.map((item: any, index: number) => ({
141
+ ...transformComponent(item, nestedAttributes),
142
+ __temp_key__: keys[index],
143
+ }));
144
+ } else {
145
+ out[key] = transformComponent(value, nestedAttributes);
146
+ }
147
+ break;
148
+ }
149
+ case 'relation': {
150
+ if (attribute.target) {
151
+ out[key] = { connect: buildConnectItems(value, attribute), disconnect: [] };
152
+ }
153
+ break;
154
+ }
155
+ default:
156
+ out[key] = value;
157
+ }
158
+ });
159
+
160
+ return out;
161
+ };
162
+
163
+ const { previousKey = null, nextKey = null } = insertOptions ?? {};
164
+ const keys =
165
+ insertOptions !== undefined
166
+ ? generateNKeysBetween(previousKey ?? null, nextKey ?? null, entries.length)
167
+ : generateNKeysBetween(undefined, undefined, entries.length);
168
+
169
+ return entries.map((entry: any, index: number) => ({
170
+ ...transformComponent(entry, schemas[entry.__component] ?? {}),
171
+ __component: entry.__component,
172
+ __temp_key__: keys[index],
173
+ }));
174
+ }
175
+
176
+ /** Transforms entries for append mode with stable keys after existing zone blocks. */
177
+ export function prepareZoneDataForInsert(
178
+ entries: any[],
179
+ schemas: ComponentSchemas,
180
+ insertOptions: PrepareZoneDataInsertOptions
181
+ ): any[] {
182
+ return prepareZoneData(entries, schemas, insertOptions);
183
+ }
184
+
185
+ /** Prepares a single API-sourced dynamic zone entry for the edit form. */
186
+ export function prepareDynamicZoneEntry(entry: any, schemas: ComponentSchemas): any {
187
+ return prepareZoneData([entry], schemas)[0];
188
+ }
189
+
190
+ function clonePlainValue<T>(value: T): T {
191
+ if (typeof structuredClone === 'function') {
192
+ return structuredClone(value);
193
+ }
194
+
195
+ return JSON.parse(JSON.stringify(value));
196
+ }
197
+
198
+ const COLLECTION_TYPES = 'collection-types';
199
+ const SINGLE_TYPES = 'single-types';
200
+
201
+ const ONE_WAY_RELATIONS = new Set([
202
+ 'oneWay',
203
+ 'oneToOne',
204
+ 'manyToOne',
205
+ 'oneToManyMorph',
206
+ 'oneToOneMorph',
207
+ ]);
208
+
209
+ export type ContentTypeSchema = { uid?: string; kind?: string };
210
+
211
+ export type FetchRelationItems = (
212
+ model: string,
213
+ entityId: string | number,
214
+ fieldName: string
215
+ ) => Promise<any[]>;
216
+
217
+ export interface CloneFormDynamicZoneEntryOptions {
218
+ schemas: ComponentSchemas;
219
+ contentTypes?: ContentTypeSchema[];
220
+ fetchRelationItems?: FetchRelationItems;
221
+ }
222
+
223
+ interface RelationSourceContext {
224
+ model?: string;
225
+ id?: string | number;
226
+ }
227
+
228
+ function isPlainObject(value: unknown): value is Record<string, any> {
229
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
230
+ }
231
+
232
+ function getRelationIdentity(relation: any): string | null {
233
+ if (!isPlainObject(relation)) {
234
+ return null;
235
+ }
236
+
237
+ const documentId = relation.documentId ?? relation.apiData?.documentId;
238
+ const locale = relation.locale ?? relation.apiData?.locale ?? '';
239
+ const id = relation.id ?? relation.apiData?.id;
240
+
241
+ if (documentId) {
242
+ return `${documentId}::${locale}`;
243
+ }
244
+
245
+ if (id !== null && id !== undefined) {
246
+ return `id:${id}`;
247
+ }
248
+
249
+ return null;
250
+ }
251
+
252
+ function getRelationDisplayValue(relation: Record<string, any>): string {
253
+ for (const key of ['label', 'title', 'name', 'displayName', 'question', 'heading', 'slug', 'documentId', 'id']) {
254
+ const value = relation[key];
255
+
256
+ if (typeof value === 'string' && value.trim()) {
257
+ return value;
258
+ }
259
+
260
+ if (typeof value === 'number') {
261
+ return String(value);
262
+ }
263
+ }
264
+
265
+ return '';
266
+ }
267
+
268
+ function getRelationCollectionType(targetModel: string, contentTypes?: ContentTypeSchema[]): string {
269
+ const targetSchema = contentTypes?.find((schema) => schema?.uid === targetModel);
270
+
271
+ return targetSchema?.kind === 'singleType' ? SINGLE_TYPES : COLLECTION_TYPES;
272
+ }
273
+
274
+ function getRelationHref(
275
+ targetModel: string | undefined,
276
+ contentTypes: ContentTypeSchema[] | undefined,
277
+ documentId: string | undefined,
278
+ locale?: string | null
279
+ ): string | undefined {
280
+ if (!targetModel || !documentId) {
281
+ return undefined;
282
+ }
283
+
284
+ const collectionType = getRelationCollectionType(targetModel, contentTypes);
285
+ const basePath =
286
+ collectionType === SINGLE_TYPES
287
+ ? `../${SINGLE_TYPES}/${targetModel}`
288
+ : `../${COLLECTION_TYPES}/${targetModel}/${documentId}`;
289
+
290
+ return locale ? `${basePath}?plugins[i18n][locale]=${locale}` : basePath;
291
+ }
292
+
293
+ function normalizeFetchedRelations(value: unknown): any[] {
294
+ if (Array.isArray(value)) {
295
+ return value.filter(isPlainObject);
296
+ }
297
+
298
+ if (isPlainObject(value) && Array.isArray(value.results)) {
299
+ return value.results.filter(isPlainObject);
300
+ }
301
+
302
+ if (isPlainObject(value)) {
303
+ return [value];
304
+ }
305
+
306
+ return [];
307
+ }
308
+
309
+ function toRelationConnectEntry(
310
+ relation: Record<string, any>,
311
+ attribute: Record<string, any>,
312
+ contentTypes: ContentTypeSchema[] | undefined,
313
+ tempKey: string
314
+ ) {
315
+ const id = relation.id ?? relation.apiData?.id;
316
+ const documentId = relation.documentId ?? relation.apiData?.documentId;
317
+ const locale = relation.locale ?? relation.apiData?.locale ?? null;
318
+ const label = relation.label ?? getRelationDisplayValue(relation);
319
+ const href = relation.href ?? getRelationHref(attribute.target, contentTypes, documentId, locale);
320
+
321
+ if (id === undefined && !documentId) {
322
+ return null;
323
+ }
324
+
325
+ const entry: Record<string, any> = {
326
+ id,
327
+ documentId,
328
+ locale,
329
+ href,
330
+ label: label || documentId || (id !== undefined && id !== null ? String(id) : ''),
331
+ __temp_key__: tempKey,
332
+ apiData: {
333
+ id,
334
+ documentId,
335
+ locale,
336
+ isTemporary: true,
337
+ },
338
+ };
339
+
340
+ if (relation.status !== undefined) {
341
+ entry.status = relation.status;
342
+ } else if (typeof relation.publishedAt === 'string') {
343
+ entry.status = 'published';
344
+ }
345
+
346
+ return entry;
347
+ }
348
+
349
+ async function sanitizeRelationValue(
350
+ attribute: Record<string, any>,
351
+ value: any,
352
+ fieldName: string,
353
+ sourceContext: RelationSourceContext | undefined,
354
+ contentTypes: ContentTypeSchema[] | undefined,
355
+ fetchRelationItems: FetchRelationItems | undefined,
356
+ createTempKey: () => string
357
+ ) {
358
+ const currentConnect = Array.isArray(value?.connect) ? value.connect : [];
359
+ const currentDisconnect = Array.isArray(value?.disconnect) ? value.disconnect : [];
360
+ const fetchedRelations =
361
+ fetchRelationItems && sourceContext?.model && sourceContext?.id
362
+ ? await fetchRelationItems(sourceContext.model, sourceContext.id, fieldName)
363
+ : [];
364
+
365
+ const connectedByIdentity = new Map<string, Record<string, any>>();
366
+
367
+ for (const relation of currentConnect) {
368
+ const entry = toRelationConnectEntry(relation, attribute, contentTypes, createTempKey());
369
+ const identity = getRelationIdentity(entry);
370
+
371
+ if (entry && identity) {
372
+ connectedByIdentity.set(identity, entry);
373
+ }
374
+ }
375
+
376
+ const disconnectedIdentities = new Set(
377
+ currentDisconnect.map(getRelationIdentity).filter(Boolean) as string[]
378
+ );
379
+
380
+ const effectiveRelations: Record<string, any>[] = [];
381
+
382
+ for (const relation of fetchedRelations) {
383
+ const entry = toRelationConnectEntry(relation, attribute, contentTypes, createTempKey());
384
+ const identity = getRelationIdentity(entry);
385
+
386
+ if (!entry || !identity || disconnectedIdentities.has(identity)) {
387
+ continue;
388
+ }
389
+
390
+ if (connectedByIdentity.has(identity)) {
391
+ effectiveRelations.push(connectedByIdentity.get(identity)!);
392
+ connectedByIdentity.delete(identity);
393
+ continue;
394
+ }
395
+
396
+ effectiveRelations.push(entry);
397
+ }
398
+
399
+ for (const relation of connectedByIdentity.values()) {
400
+ effectiveRelations.push(relation);
401
+ }
402
+
403
+ const connect = ONE_WAY_RELATIONS.has(attribute.relation)
404
+ ? effectiveRelations.slice(-1)
405
+ : effectiveRelations;
406
+
407
+ return {
408
+ connect,
409
+ disconnect: [],
410
+ };
411
+ }
412
+
413
+ async function cloneComponentFields(
414
+ data: any,
415
+ componentUid: string,
416
+ options: CloneFormDynamicZoneEntryOptions,
417
+ sourceContext?: RelationSourceContext
418
+ ): Promise<Record<string, any>> {
419
+ const { schemas, contentTypes, fetchRelationItems } = options;
420
+ const attributes = schemas[componentUid] ?? {};
421
+ const out: Record<string, any> = { __component: data.__component };
422
+ const tempKeys = generateNKeysBetween(undefined, undefined, Object.keys(data).length + 10);
423
+ let tempKeyIndex = 0;
424
+ const createTempKey = () => tempKeys[tempKeyIndex++] ?? generateKeyBetween(null, null);
425
+
426
+ const context: RelationSourceContext = {
427
+ model: componentUid,
428
+ id: sourceContext?.id ?? data.id,
429
+ };
430
+
431
+ for (const [key, rawValue] of Object.entries(data)) {
432
+ const value = rawValue as any;
433
+ if (TRANSIENT_ENTRY_KEYS.has(key) || key === '__component') {
434
+ continue;
435
+ }
436
+
437
+ const attribute = attributes[key];
438
+
439
+ if (!attribute) {
440
+ if (value !== undefined) {
441
+ out[key] = clonePlainValue(value);
442
+ }
443
+ continue;
444
+ }
445
+
446
+ if (value === null || value === undefined) {
447
+ if (attribute.type === 'boolean') {
448
+ out[key] = value;
449
+ }
450
+ continue;
451
+ }
452
+
453
+ switch (attribute.type) {
454
+ case 'component': {
455
+ if (attribute.repeatable) {
456
+ const items = Array.isArray(value) ? value : [];
457
+ const keys = generateNKeysBetween(undefined, undefined, items.length);
458
+ out[key] = await Promise.all(
459
+ items.map(async (item: any, index: number) => ({
460
+ ...(await cloneComponentFields(item, attribute.component, options, {
461
+ model: attribute.component,
462
+ id: item?.id,
463
+ })),
464
+ __temp_key__: keys[index],
465
+ }))
466
+ );
467
+ } else {
468
+ out[key] = await cloneComponentFields(value, attribute.component, options, {
469
+ model: attribute.component,
470
+ id: value?.id,
471
+ });
472
+ }
473
+ break;
474
+ }
475
+ case 'dynamiczone': {
476
+ if (!Array.isArray(value)) {
477
+ out[key] = [];
478
+ break;
479
+ }
480
+
481
+ const keys = generateNKeysBetween(undefined, undefined, value.length);
482
+ out[key] = await Promise.all(
483
+ value.map(async (item: any, index: number) => ({
484
+ ...(await cloneFormDynamicZoneEntry(item, options)),
485
+ __temp_key__: keys[index],
486
+ }))
487
+ );
488
+ break;
489
+ }
490
+ case 'relation':
491
+ if (!attribute.target) {
492
+ out[key] = { connect: [], disconnect: [] };
493
+ break;
494
+ }
495
+
496
+ out[key] = await sanitizeRelationValue(
497
+ attribute,
498
+ value,
499
+ key,
500
+ context,
501
+ contentTypes,
502
+ fetchRelationItems,
503
+ createTempKey
504
+ );
505
+ break;
506
+ default:
507
+ out[key] = clonePlainValue(value);
508
+ }
509
+ }
510
+
511
+ return out;
512
+ }
513
+
514
+ /**
515
+ * Clones an in-form dynamic zone entry for in-place duplication.
516
+ * Strips persisted ids/temp keys, assigns fresh ordering keys, and
517
+ * fetches persisted relations from the content-manager relations API.
518
+ */
519
+ export async function cloneFormDynamicZoneEntry(
520
+ entry: any,
521
+ options: CloneFormDynamicZoneEntryOptions
522
+ ): Promise<any> {
523
+ if (!entry || typeof entry.__component !== 'string') {
524
+ return entry;
525
+ }
526
+
527
+ const cloned = await cloneComponentFields(entry, entry.__component, options, {
528
+ model: entry.__component,
529
+ id: entry.id,
530
+ });
531
+
532
+ return cloned;
533
+ }
534
+
535
+ /** Creates a cached fetcher for component relation fields. */
536
+ export function createFetchRelationItems(
537
+ get: (url: string, config?: { params?: Record<string, unknown> }) => Promise<{ data?: any }>,
538
+ relationQueryParams: Record<string, unknown> = {}
539
+ ): FetchRelationItems {
540
+ const cache = new Map<string, Promise<any[]>>();
541
+
542
+ return async (relationModel, relationId, fieldName) => {
543
+ if (!relationModel || relationId === undefined || relationId === null || !fieldName) {
544
+ return [];
545
+ }
546
+
547
+ const cacheKey = JSON.stringify({ relationModel, relationId, fieldName, relationQueryParams });
548
+ const cached = cache.get(cacheKey);
549
+
550
+ if (cached) {
551
+ return cached;
552
+ }
553
+
554
+ const request = (async () => {
555
+ let page = 1;
556
+ let totalPages = 1;
557
+ const relations: any[] = [];
558
+
559
+ while (page <= totalPages) {
560
+ const response = await get(
561
+ `/content-manager/relations/${relationModel}/${relationId}/${fieldName}`,
562
+ {
563
+ params: {
564
+ ...relationQueryParams,
565
+ page,
566
+ pageSize: 100,
567
+ },
568
+ }
569
+ );
570
+ const payload = response?.data ?? {};
571
+ const pageResults = normalizeFetchedRelations(payload.results).reverse();
572
+
573
+ relations.push(...pageResults);
574
+
575
+ const pagination = payload?.pagination;
576
+ const pageCount =
577
+ typeof pagination?.pageCount === 'number'
578
+ ? pagination.pageCount
579
+ : Math.max(1, Math.ceil((pagination?.total ?? pageResults.length) / 100));
580
+
581
+ totalPages = pageCount;
582
+ page += 1;
583
+ }
584
+
585
+ return relations;
586
+ })();
587
+
588
+ cache.set(cacheKey, request);
589
+
590
+ try {
591
+ return await request;
592
+ } catch {
593
+ cache.delete(cacheKey);
594
+ throw new Error('Failed to fetch relations');
595
+ }
596
+ };
597
+ }
598
+
599
+ /** Inserts a cloned entry immediately after `sourceIndex` with a stable temp key. */
600
+ export function insertDynamicZoneClone(
601
+ zone: any[],
602
+ sourceIndex: number,
603
+ clonedEntry: any
604
+ ): any[] {
605
+ const prevKey = zone[sourceIndex]?.__temp_key__ ?? null;
606
+ const nextKey = zone[sourceIndex + 1]?.__temp_key__ ?? null;
607
+
608
+ return [
609
+ ...zone.slice(0, sourceIndex + 1),
610
+ { ...clonedEntry, __temp_key__: generateKeyBetween(prevKey, nextKey) },
611
+ ...zone.slice(sourceIndex + 1),
612
+ ];
613
+ }
614
+
615
+ /** Prepare a copied block for insertion at a target index with a fresh temp key. */
616
+ export function prepareEntryForZoneInsert(zone: any[], targetIndex: number, entry: any): any {
617
+ const prevKey = targetIndex > 0 ? zone[targetIndex - 1]?.__temp_key__ ?? null : null;
618
+ const nextKey = targetIndex < zone.length ? zone[targetIndex]?.__temp_key__ ?? null : null;
619
+ const { id, documentId, __temp_key__, ...rest } = entry;
620
+
621
+ return {
622
+ ...rest,
623
+ __temp_key__: generateKeyBetween(prevKey, nextKey),
624
+ };
625
+ }
@@ -0,0 +1,19 @@
1
+ /** Extract locale/status for content-manager relation requests from URL query params. */
2
+ export function getRelationQueryParams(query: Record<string, unknown> | undefined) {
3
+ if (!query) {
4
+ return {};
5
+ }
6
+
7
+ const { plugins, ...rest } = query as Record<string, any>;
8
+ const pluginParams = Object.values(plugins ?? {}).reduce<Record<string, unknown>>(
9
+ (acc, current) => Object.assign(acc, current as Record<string, unknown>),
10
+ {}
11
+ );
12
+
13
+ const params = { ...rest, ...pluginParams };
14
+
15
+ return {
16
+ locale: params.locale,
17
+ status: params.status,
18
+ };
19
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "./tsconfig",
3
+ "include": ["./src", "./custom.d.ts"],
4
+ "exclude": ["**/*.test.ts", "**/*.test.tsx"],
5
+ "compilerOptions": {
6
+ "rootDir": "../",
7
+ "baseUrl": ".",
8
+ "outDir": "./dist"
9
+ }
10
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "@strapi/typescript-utils/tsconfigs/admin",
3
+ "include": [
4
+ "./src",
5
+ "./custom.d.ts"
6
+ ],
7
+ "compilerOptions": {
8
+ "rootDir": "../",
9
+ "baseUrl": ".",
10
+ "tsBuildInfoFile": "./.tsbuildinfo"
11
+ }
12
+ }