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,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
|
|
3
|
+
const bootstrap = ({ strapi }) => {
|
|
4
|
+
const actions = [
|
|
5
|
+
{
|
|
6
|
+
section: "plugins",
|
|
7
|
+
displayName: "Fill Dynamic Zone",
|
|
8
|
+
uid: "fill",
|
|
9
|
+
pluginName: "dynamic-zone-tools"
|
|
10
|
+
}
|
|
11
|
+
];
|
|
12
|
+
strapi.admin.services.permission.actionProvider.registerMany(actions);
|
|
13
|
+
};
|
|
14
|
+
const destroy = ({ strapi }) => {
|
|
15
|
+
};
|
|
16
|
+
const register = ({ strapi }) => {
|
|
17
|
+
};
|
|
18
|
+
const config = {
|
|
19
|
+
default: {},
|
|
20
|
+
validator() {
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const contentTypes = {};
|
|
24
|
+
const MAX_POPULATE_DEPTH = 8;
|
|
25
|
+
const buildDeepPopulate = (strapi, uid, level = 1) => {
|
|
26
|
+
if (level > MAX_POPULATE_DEPTH) return true;
|
|
27
|
+
const model = strapi.getModel(uid);
|
|
28
|
+
if (!model) return true;
|
|
29
|
+
const populate = {};
|
|
30
|
+
for (const [name, attribute] of Object.entries(model.attributes)) {
|
|
31
|
+
switch (attribute.type) {
|
|
32
|
+
case "media":
|
|
33
|
+
populate[name] = true;
|
|
34
|
+
break;
|
|
35
|
+
case "relation":
|
|
36
|
+
if (attribute.target) populate[name] = true;
|
|
37
|
+
break;
|
|
38
|
+
case "component": {
|
|
39
|
+
const nested = buildDeepPopulate(strapi, attribute.component, level + 1);
|
|
40
|
+
populate[name] = nested === true || Object.keys(nested).length === 0 ? true : { populate: nested };
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
case "dynamiczone": {
|
|
44
|
+
const on = {};
|
|
45
|
+
for (const componentUid of attribute.components ?? []) {
|
|
46
|
+
const nested = buildDeepPopulate(strapi, componentUid, level + 1);
|
|
47
|
+
on[componentUid] = nested === true || Object.keys(nested).length === 0 ? true : { populate: nested };
|
|
48
|
+
}
|
|
49
|
+
populate[name] = { on };
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return populate;
|
|
55
|
+
};
|
|
56
|
+
const controller = ({ strapi }) => ({
|
|
57
|
+
async getDynamicZoneData(ctx) {
|
|
58
|
+
const { contentType, id } = ctx.params;
|
|
59
|
+
const { locale, status } = ctx.query;
|
|
60
|
+
const model = strapi.getModel(contentType);
|
|
61
|
+
if (!model) {
|
|
62
|
+
return ctx.badRequest(`Unknown content type: ${contentType}`);
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const populate = buildDeepPopulate(strapi, contentType);
|
|
66
|
+
const hasDraftAndPublish = Boolean(model.options?.draftAndPublish);
|
|
67
|
+
const isLocalized = Boolean(model.pluginOptions?.i18n?.localized);
|
|
68
|
+
const document = await strapi.documents(contentType).findOne({
|
|
69
|
+
documentId: id,
|
|
70
|
+
populate: populate === true ? "*" : populate,
|
|
71
|
+
locale: isLocalized && locale ? locale : void 0,
|
|
72
|
+
status: hasDraftAndPublish ? status === "published" ? "published" : "draft" : void 0
|
|
73
|
+
});
|
|
74
|
+
if (!document) {
|
|
75
|
+
return ctx.notFound("Document not found");
|
|
76
|
+
}
|
|
77
|
+
return ctx.send({ data: document });
|
|
78
|
+
} catch (error) {
|
|
79
|
+
strapi.log.error("dynamic-zone-tools: failed to fetch source document", error);
|
|
80
|
+
return ctx.internalServerError("Failed to fetch dynamic zone data");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
const controllers = {
|
|
85
|
+
controller
|
|
86
|
+
};
|
|
87
|
+
const middlewares = {};
|
|
88
|
+
const policies$1 = {};
|
|
89
|
+
const contentAPIRoutes = [];
|
|
90
|
+
const policies = [
|
|
91
|
+
"admin::isAuthenticatedAdmin",
|
|
92
|
+
{
|
|
93
|
+
name: "admin::hasPermissions",
|
|
94
|
+
config: {
|
|
95
|
+
actions: ["plugin::dynamic-zone-tools.fill"]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
];
|
|
99
|
+
const adminAPIRoutes = [
|
|
100
|
+
{
|
|
101
|
+
method: "GET",
|
|
102
|
+
path: "/source-document/:contentType/:id",
|
|
103
|
+
handler: "controller.getDynamicZoneData",
|
|
104
|
+
config: { policies }
|
|
105
|
+
}
|
|
106
|
+
];
|
|
107
|
+
const routes = {
|
|
108
|
+
"content-api": {
|
|
109
|
+
type: "content-api",
|
|
110
|
+
routes: contentAPIRoutes
|
|
111
|
+
},
|
|
112
|
+
"admin": {
|
|
113
|
+
type: "admin",
|
|
114
|
+
routes: adminAPIRoutes
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const service = ({ strapi }) => ({
|
|
118
|
+
getWelcomeMessage() {
|
|
119
|
+
return "Welcome to Strapi 🚀";
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
const services = {
|
|
123
|
+
service
|
|
124
|
+
};
|
|
125
|
+
const index = {
|
|
126
|
+
register,
|
|
127
|
+
bootstrap,
|
|
128
|
+
destroy,
|
|
129
|
+
config,
|
|
130
|
+
controllers,
|
|
131
|
+
routes,
|
|
132
|
+
services,
|
|
133
|
+
contentTypes,
|
|
134
|
+
policies: policies$1,
|
|
135
|
+
middlewares
|
|
136
|
+
};
|
|
137
|
+
exports.default = index;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const bootstrap = ({ strapi }) => {
|
|
2
|
+
const actions = [
|
|
3
|
+
{
|
|
4
|
+
section: "plugins",
|
|
5
|
+
displayName: "Fill Dynamic Zone",
|
|
6
|
+
uid: "fill",
|
|
7
|
+
pluginName: "dynamic-zone-tools"
|
|
8
|
+
}
|
|
9
|
+
];
|
|
10
|
+
strapi.admin.services.permission.actionProvider.registerMany(actions);
|
|
11
|
+
};
|
|
12
|
+
const destroy = ({ strapi }) => {
|
|
13
|
+
};
|
|
14
|
+
const register = ({ strapi }) => {
|
|
15
|
+
};
|
|
16
|
+
const config = {
|
|
17
|
+
default: {},
|
|
18
|
+
validator() {
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const contentTypes = {};
|
|
22
|
+
const MAX_POPULATE_DEPTH = 8;
|
|
23
|
+
const buildDeepPopulate = (strapi, uid, level = 1) => {
|
|
24
|
+
if (level > MAX_POPULATE_DEPTH) return true;
|
|
25
|
+
const model = strapi.getModel(uid);
|
|
26
|
+
if (!model) return true;
|
|
27
|
+
const populate = {};
|
|
28
|
+
for (const [name, attribute] of Object.entries(model.attributes)) {
|
|
29
|
+
switch (attribute.type) {
|
|
30
|
+
case "media":
|
|
31
|
+
populate[name] = true;
|
|
32
|
+
break;
|
|
33
|
+
case "relation":
|
|
34
|
+
if (attribute.target) populate[name] = true;
|
|
35
|
+
break;
|
|
36
|
+
case "component": {
|
|
37
|
+
const nested = buildDeepPopulate(strapi, attribute.component, level + 1);
|
|
38
|
+
populate[name] = nested === true || Object.keys(nested).length === 0 ? true : { populate: nested };
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
case "dynamiczone": {
|
|
42
|
+
const on = {};
|
|
43
|
+
for (const componentUid of attribute.components ?? []) {
|
|
44
|
+
const nested = buildDeepPopulate(strapi, componentUid, level + 1);
|
|
45
|
+
on[componentUid] = nested === true || Object.keys(nested).length === 0 ? true : { populate: nested };
|
|
46
|
+
}
|
|
47
|
+
populate[name] = { on };
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return populate;
|
|
53
|
+
};
|
|
54
|
+
const controller = ({ strapi }) => ({
|
|
55
|
+
async getDynamicZoneData(ctx) {
|
|
56
|
+
const { contentType, id } = ctx.params;
|
|
57
|
+
const { locale, status } = ctx.query;
|
|
58
|
+
const model = strapi.getModel(contentType);
|
|
59
|
+
if (!model) {
|
|
60
|
+
return ctx.badRequest(`Unknown content type: ${contentType}`);
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const populate = buildDeepPopulate(strapi, contentType);
|
|
64
|
+
const hasDraftAndPublish = Boolean(model.options?.draftAndPublish);
|
|
65
|
+
const isLocalized = Boolean(model.pluginOptions?.i18n?.localized);
|
|
66
|
+
const document = await strapi.documents(contentType).findOne({
|
|
67
|
+
documentId: id,
|
|
68
|
+
populate: populate === true ? "*" : populate,
|
|
69
|
+
locale: isLocalized && locale ? locale : void 0,
|
|
70
|
+
status: hasDraftAndPublish ? status === "published" ? "published" : "draft" : void 0
|
|
71
|
+
});
|
|
72
|
+
if (!document) {
|
|
73
|
+
return ctx.notFound("Document not found");
|
|
74
|
+
}
|
|
75
|
+
return ctx.send({ data: document });
|
|
76
|
+
} catch (error) {
|
|
77
|
+
strapi.log.error("dynamic-zone-tools: failed to fetch source document", error);
|
|
78
|
+
return ctx.internalServerError("Failed to fetch dynamic zone data");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
const controllers = {
|
|
83
|
+
controller
|
|
84
|
+
};
|
|
85
|
+
const middlewares = {};
|
|
86
|
+
const policies$1 = {};
|
|
87
|
+
const contentAPIRoutes = [];
|
|
88
|
+
const policies = [
|
|
89
|
+
"admin::isAuthenticatedAdmin",
|
|
90
|
+
{
|
|
91
|
+
name: "admin::hasPermissions",
|
|
92
|
+
config: {
|
|
93
|
+
actions: ["plugin::dynamic-zone-tools.fill"]
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
];
|
|
97
|
+
const adminAPIRoutes = [
|
|
98
|
+
{
|
|
99
|
+
method: "GET",
|
|
100
|
+
path: "/source-document/:contentType/:id",
|
|
101
|
+
handler: "controller.getDynamicZoneData",
|
|
102
|
+
config: { policies }
|
|
103
|
+
}
|
|
104
|
+
];
|
|
105
|
+
const routes = {
|
|
106
|
+
"content-api": {
|
|
107
|
+
type: "content-api",
|
|
108
|
+
routes: contentAPIRoutes
|
|
109
|
+
},
|
|
110
|
+
"admin": {
|
|
111
|
+
type: "admin",
|
|
112
|
+
routes: adminAPIRoutes
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const service = ({ strapi }) => ({
|
|
116
|
+
getWelcomeMessage() {
|
|
117
|
+
return "Welcome to Strapi 🚀";
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
const services = {
|
|
121
|
+
service
|
|
122
|
+
};
|
|
123
|
+
const index = {
|
|
124
|
+
register,
|
|
125
|
+
bootstrap,
|
|
126
|
+
destroy,
|
|
127
|
+
config,
|
|
128
|
+
controllers,
|
|
129
|
+
routes,
|
|
130
|
+
services,
|
|
131
|
+
contentTypes,
|
|
132
|
+
policies: policies$1,
|
|
133
|
+
middlewares
|
|
134
|
+
};
|
|
135
|
+
export {
|
|
136
|
+
index as default
|
|
137
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
register: ({ strapi }: {
|
|
3
|
+
strapi: import('@strapi/types/dist/core').Strapi;
|
|
4
|
+
}) => void;
|
|
5
|
+
bootstrap: ({ strapi }: {
|
|
6
|
+
strapi: import('@strapi/types/dist/core').Strapi;
|
|
7
|
+
}) => void;
|
|
8
|
+
destroy: ({ strapi }: {
|
|
9
|
+
strapi: import('@strapi/types/dist/core').Strapi;
|
|
10
|
+
}) => void;
|
|
11
|
+
config: {
|
|
12
|
+
default: {};
|
|
13
|
+
validator(): void;
|
|
14
|
+
};
|
|
15
|
+
controllers: {
|
|
16
|
+
controller: ({ strapi }: {
|
|
17
|
+
strapi: import('@strapi/types/dist/core').Strapi;
|
|
18
|
+
}) => {
|
|
19
|
+
getDynamicZoneData(ctx: any): Promise<any>;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
routes: {
|
|
23
|
+
'content-api': {
|
|
24
|
+
type: string;
|
|
25
|
+
routes: any[];
|
|
26
|
+
};
|
|
27
|
+
admin: {
|
|
28
|
+
type: string;
|
|
29
|
+
routes: {
|
|
30
|
+
method: string;
|
|
31
|
+
path: string;
|
|
32
|
+
handler: string;
|
|
33
|
+
config: {
|
|
34
|
+
policies: (string | {
|
|
35
|
+
name: string;
|
|
36
|
+
config: {
|
|
37
|
+
actions: string[];
|
|
38
|
+
};
|
|
39
|
+
})[];
|
|
40
|
+
};
|
|
41
|
+
}[];
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
services: {
|
|
45
|
+
service: ({ strapi }: {
|
|
46
|
+
strapi: import('@strapi/types/dist/core').Strapi;
|
|
47
|
+
}) => {
|
|
48
|
+
getWelcomeMessage(): string;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
contentTypes: {};
|
|
52
|
+
policies: {};
|
|
53
|
+
middlewares: {};
|
|
54
|
+
};
|
|
55
|
+
export default _default;
|
package/package.json
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "strapi-plugin-dynamic-zone-tools",
|
|
3
|
+
"description": "Dynamic zone productivity tools for Strapi 5: copy dynamic zones from other records and duplicate, copy, or insert individual component blocks.",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"strapi",
|
|
8
|
+
"strapi5",
|
|
9
|
+
"strapi-plugin",
|
|
10
|
+
"plugin",
|
|
11
|
+
"dynamic-zone",
|
|
12
|
+
"dynamic-zones",
|
|
13
|
+
"content-manager",
|
|
14
|
+
"copy",
|
|
15
|
+
"duplicate",
|
|
16
|
+
"cms"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://github.com/dimitrisganotis/strapi-plugin-dynamic-zone-tools#readme",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/dimitrisganotis/strapi-plugin-dynamic-zone-tools/issues"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/dimitrisganotis/strapi-plugin-dynamic-zone-tools.git"
|
|
25
|
+
},
|
|
26
|
+
"author": {
|
|
27
|
+
"name": "Dimitris Ganotis",
|
|
28
|
+
"email": "hello@dganotis.dev",
|
|
29
|
+
"url": "https://www.dganotis.dev"
|
|
30
|
+
},
|
|
31
|
+
"type": "commonjs",
|
|
32
|
+
"strapi": {
|
|
33
|
+
"kind": "plugin",
|
|
34
|
+
"name": "dynamic-zone-tools",
|
|
35
|
+
"displayName": "Dynamic Zone Tools",
|
|
36
|
+
"description": "Copy dynamic zone content between records and duplicate, copy, or insert individual component blocks in Strapi 5."
|
|
37
|
+
},
|
|
38
|
+
"main": "./strapi-server.js",
|
|
39
|
+
"files": [
|
|
40
|
+
"admin/src",
|
|
41
|
+
"admin/custom.d.ts",
|
|
42
|
+
"admin/tsconfig.json",
|
|
43
|
+
"admin/tsconfig.build.json",
|
|
44
|
+
"server/src",
|
|
45
|
+
"server/tsconfig.json",
|
|
46
|
+
"server/tsconfig.build.json",
|
|
47
|
+
"dist",
|
|
48
|
+
"strapi-admin.js",
|
|
49
|
+
"strapi-server.js",
|
|
50
|
+
"package.json",
|
|
51
|
+
"README.md",
|
|
52
|
+
"CHANGELOG.md",
|
|
53
|
+
"CONTRIBUTING.md",
|
|
54
|
+
"LICENSE"
|
|
55
|
+
],
|
|
56
|
+
"exports": {
|
|
57
|
+
"./strapi-admin": {
|
|
58
|
+
"types": "./dist/admin/src/index.d.ts",
|
|
59
|
+
"source": "./admin/src/index.ts",
|
|
60
|
+
"import": "./dist/admin/index.mjs",
|
|
61
|
+
"require": "./dist/admin/index.js",
|
|
62
|
+
"default": "./dist/admin/index.js"
|
|
63
|
+
},
|
|
64
|
+
"./strapi-server": {
|
|
65
|
+
"types": "./dist/server/src/index.d.ts",
|
|
66
|
+
"source": "./server/src/index.ts",
|
|
67
|
+
"import": "./dist/server/index.mjs",
|
|
68
|
+
"require": "./dist/server/index.js",
|
|
69
|
+
"default": "./dist/server/index.js"
|
|
70
|
+
},
|
|
71
|
+
"./package.json": "./package.json"
|
|
72
|
+
},
|
|
73
|
+
"scripts": {
|
|
74
|
+
"build": "strapi-plugin build",
|
|
75
|
+
"watch": "strapi-plugin watch",
|
|
76
|
+
"watch:link": "strapi-plugin watch:link",
|
|
77
|
+
"verify": "strapi-plugin verify",
|
|
78
|
+
"prepack": "npm run build",
|
|
79
|
+
"test:ts:front": "tsc -p admin/tsconfig.json --noEmit",
|
|
80
|
+
"test:ts:back": "tsc -p server/tsconfig.json --noEmit"
|
|
81
|
+
},
|
|
82
|
+
"dependencies": {
|
|
83
|
+
"fractional-indexing": "^3.2.0",
|
|
84
|
+
"react-intl": "^7.1.14"
|
|
85
|
+
},
|
|
86
|
+
"devDependencies": {
|
|
87
|
+
"@strapi/strapi": "^5.41.0",
|
|
88
|
+
"@strapi/sdk-plugin": "^5.3.2",
|
|
89
|
+
"prettier": "^3.7.4",
|
|
90
|
+
"react": "^18.3.1",
|
|
91
|
+
"react-dom": "^18.3.1",
|
|
92
|
+
"react-router-dom": "^6.30.2",
|
|
93
|
+
"styled-components": "^6.1.19",
|
|
94
|
+
"@types/react": "^18",
|
|
95
|
+
"@types/react-dom": "^18",
|
|
96
|
+
"@strapi/typescript-utils": "^5.41.0",
|
|
97
|
+
"typescript": "^5"
|
|
98
|
+
},
|
|
99
|
+
"peerDependencies": {
|
|
100
|
+
"@strapi/design-system": "^2.0.0",
|
|
101
|
+
"@strapi/icons": "^2.0.0",
|
|
102
|
+
"@strapi/strapi": "^5.41.0",
|
|
103
|
+
"react": "^18.0.0",
|
|
104
|
+
"react-dom": "^18.0.0",
|
|
105
|
+
"react-router-dom": "^6.0.0",
|
|
106
|
+
"styled-components": "^6.0.0"
|
|
107
|
+
},
|
|
108
|
+
"engines": {
|
|
109
|
+
"node": ">=18.0.0 <=22.x.x",
|
|
110
|
+
"npm": ">=6.0.0"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Core } from '@strapi/strapi';
|
|
2
|
+
|
|
3
|
+
const bootstrap = ({ strapi }: { strapi: Core.Strapi }) => {
|
|
4
|
+
// Register permissions for the plugin
|
|
5
|
+
const actions = [
|
|
6
|
+
{
|
|
7
|
+
section: 'plugins',
|
|
8
|
+
displayName: 'Fill Dynamic Zone',
|
|
9
|
+
uid: 'fill',
|
|
10
|
+
pluginName: 'dynamic-zone-tools',
|
|
11
|
+
},
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
// Register permissions immediately
|
|
15
|
+
strapi.admin.services.permission.actionProvider.registerMany(actions);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default bootstrap;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Core } from '@strapi/strapi';
|
|
2
|
+
|
|
3
|
+
const MAX_POPULATE_DEPTH = 8;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds a populate object that fully populates media, relations, components
|
|
7
|
+
* and dynamic zones (recursively). The content-manager admin endpoint only
|
|
8
|
+
* returns relation counts, which is why the tools need its own fetch.
|
|
9
|
+
*/
|
|
10
|
+
const buildDeepPopulate = (strapi: Core.Strapi, uid: string, level = 1): Record<string, any> | true => {
|
|
11
|
+
if (level > MAX_POPULATE_DEPTH) return true;
|
|
12
|
+
|
|
13
|
+
const model = strapi.getModel(uid as any);
|
|
14
|
+
if (!model) return true;
|
|
15
|
+
|
|
16
|
+
const populate: Record<string, any> = {};
|
|
17
|
+
|
|
18
|
+
for (const [name, attribute] of Object.entries<any>(model.attributes)) {
|
|
19
|
+
switch (attribute.type) {
|
|
20
|
+
case 'media':
|
|
21
|
+
populate[name] = true;
|
|
22
|
+
break;
|
|
23
|
+
case 'relation':
|
|
24
|
+
// Polymorphic relations have no single target; skip them
|
|
25
|
+
if (attribute.target) populate[name] = true;
|
|
26
|
+
break;
|
|
27
|
+
case 'component': {
|
|
28
|
+
const nested = buildDeepPopulate(strapi, attribute.component, level + 1);
|
|
29
|
+
populate[name] = nested === true || Object.keys(nested).length === 0 ? true : { populate: nested };
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
case 'dynamiczone': {
|
|
33
|
+
const on: Record<string, any> = {};
|
|
34
|
+
for (const componentUid of attribute.components ?? []) {
|
|
35
|
+
const nested = buildDeepPopulate(strapi, componentUid, level + 1);
|
|
36
|
+
on[componentUid] =
|
|
37
|
+
nested === true || Object.keys(nested).length === 0 ? true : { populate: nested };
|
|
38
|
+
}
|
|
39
|
+
populate[name] = { on };
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
default:
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return populate;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const controller = ({ strapi }: { strapi: Core.Strapi }) => ({
|
|
51
|
+
async getDynamicZoneData(ctx) {
|
|
52
|
+
const { contentType, id } = ctx.params;
|
|
53
|
+
const { locale, status } = ctx.query;
|
|
54
|
+
|
|
55
|
+
const model = strapi.getModel(contentType);
|
|
56
|
+
if (!model) {
|
|
57
|
+
return ctx.badRequest(`Unknown content type: ${contentType}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const populate = buildDeepPopulate(strapi, contentType);
|
|
62
|
+
|
|
63
|
+
const hasDraftAndPublish = Boolean((model.options as any)?.draftAndPublish);
|
|
64
|
+
const isLocalized = Boolean((model.pluginOptions as any)?.i18n?.localized);
|
|
65
|
+
|
|
66
|
+
const document = await strapi.documents(contentType).findOne({
|
|
67
|
+
documentId: id,
|
|
68
|
+
populate: populate === true ? '*' : populate,
|
|
69
|
+
locale: isLocalized && locale ? locale : undefined,
|
|
70
|
+
status: hasDraftAndPublish ? (status === 'published' ? 'published' : 'draft') : undefined,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (!document) {
|
|
74
|
+
return ctx.notFound('Document not found');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return ctx.send({ data: document });
|
|
78
|
+
} catch (error) {
|
|
79
|
+
strapi.log.error('dynamic-zone-tools: failed to fetch source document', error);
|
|
80
|
+
return ctx.internalServerError('Failed to fetch dynamic zone data');
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export default controller;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application methods
|
|
3
|
+
*/
|
|
4
|
+
import bootstrap from './bootstrap';
|
|
5
|
+
import destroy from './destroy';
|
|
6
|
+
import register from './register';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Plugin server methods
|
|
10
|
+
*/
|
|
11
|
+
import config from './config';
|
|
12
|
+
import contentTypes from './content-types';
|
|
13
|
+
import controllers from './controllers';
|
|
14
|
+
import middlewares from './middlewares';
|
|
15
|
+
import policies from './policies';
|
|
16
|
+
import routes from './routes';
|
|
17
|
+
import services from './services';
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
register,
|
|
21
|
+
bootstrap,
|
|
22
|
+
destroy,
|
|
23
|
+
config,
|
|
24
|
+
controllers,
|
|
25
|
+
routes,
|
|
26
|
+
services,
|
|
27
|
+
contentTypes,
|
|
28
|
+
policies,
|
|
29
|
+
middlewares,
|
|
30
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const policies = [
|
|
2
|
+
'admin::isAuthenticatedAdmin',
|
|
3
|
+
{
|
|
4
|
+
name: 'admin::hasPermissions',
|
|
5
|
+
config: {
|
|
6
|
+
actions: ['plugin::dynamic-zone-tools.fill'],
|
|
7
|
+
},
|
|
8
|
+
},
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export default [
|
|
12
|
+
{
|
|
13
|
+
method: 'GET',
|
|
14
|
+
path: '/source-document/:contentType/:id',
|
|
15
|
+
handler: 'controller.getDynamicZoneData',
|
|
16
|
+
config: { policies },
|
|
17
|
+
},
|
|
18
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default [];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import contentAPIRoutes from './content-api';
|
|
2
|
+
import adminAPIRoutes from './admin-api';
|
|
3
|
+
|
|
4
|
+
const routes = {
|
|
5
|
+
'content-api': {
|
|
6
|
+
type: 'content-api',
|
|
7
|
+
routes: contentAPIRoutes,
|
|
8
|
+
},
|
|
9
|
+
'admin': {
|
|
10
|
+
type: 'admin',
|
|
11
|
+
routes: adminAPIRoutes,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default routes;
|