@devx-labs/strapi-preview 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { Flex, LinkButton } from "@strapi/design-system";
3
+ import { ExternalLink } from "@strapi/icons";
4
+ const PreviewImageInput = ({ attribute }) => {
5
+ const url = attribute?.options?.url;
6
+ if (!url) return null;
7
+ return /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsx(
8
+ LinkButton,
9
+ {
10
+ href: url,
11
+ target: "_blank",
12
+ rel: "noreferrer",
13
+ variant: "tertiary",
14
+ size: "S",
15
+ endIcon: /* @__PURE__ */ jsx(ExternalLink, {}),
16
+ children: "Preview"
17
+ }
18
+ ) });
19
+ };
20
+ export {
21
+ PreviewImageInput
22
+ };
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const designSystem = require("@strapi/design-system");
5
+ const icons = require("@strapi/icons");
6
+ const PreviewImageInput = ({ attribute }) => {
7
+ const url = attribute?.options?.url;
8
+ if (!url) return null;
9
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxRuntime.jsx(
10
+ designSystem.LinkButton,
11
+ {
12
+ href: url,
13
+ target: "_blank",
14
+ rel: "noreferrer",
15
+ variant: "tertiary",
16
+ size: "S",
17
+ endIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.ExternalLink, {}),
18
+ children: "Preview"
19
+ }
20
+ ) });
21
+ };
22
+ exports.PreviewImageInput = PreviewImageInput;
@@ -0,0 +1,203 @@
1
+ "use strict";
2
+ const jsxRuntime = require("react/jsx-runtime");
3
+ const react = require("react");
4
+ const designSystem = require("@strapi/design-system");
5
+ const strapiAdmin = require("@strapi/content-manager/strapi-admin");
6
+ const admin = require("@strapi/strapi/admin");
7
+ const icons = require("@strapi/icons");
8
+ const collectPreviewItems = (value, attributes, componentSchemas, optionsMap) => {
9
+ const items = [];
10
+ if (!value || !attributes || typeof value !== "object") return items;
11
+ const pushItem = (componentUid, tempKey) => {
12
+ const opts = optionsMap[componentUid];
13
+ if (!opts) return;
14
+ const schema = componentSchemas[componentUid];
15
+ items.push({
16
+ uid: componentUid,
17
+ displayName: schema?.info?.displayName ?? componentUid,
18
+ previewUrl: opts.url,
19
+ previewName: opts.name,
20
+ count: 1,
21
+ tempKey
22
+ });
23
+ };
24
+ for (const [attributeName, attribute] of Object.entries(attributes)) {
25
+ const attributeValue = value[attributeName];
26
+ if (!attributeValue) continue;
27
+ if (attribute.type === "dynamiczone" && Array.isArray(attributeValue)) {
28
+ for (const item of attributeValue) {
29
+ if (!item || typeof item !== "object") continue;
30
+ const componentUid = item.__component;
31
+ const tempKey = item.__temp_key__;
32
+ if (componentUid) pushItem(componentUid, tempKey);
33
+ }
34
+ continue;
35
+ }
36
+ if (attribute.type === "component" && attribute.component) {
37
+ const componentUid = attribute.component;
38
+ if (attribute.repeatable && Array.isArray(attributeValue)) {
39
+ for (let i = 0; i < attributeValue.length; i++) {
40
+ const item = attributeValue[i];
41
+ pushItem(componentUid, item?.__temp_key__);
42
+ }
43
+ } else {
44
+ pushItem(componentUid);
45
+ }
46
+ }
47
+ }
48
+ return items;
49
+ };
50
+ const ComponentPreviewPanel = () => {
51
+ const { components, contentType, isCreatingEntry } = strapiAdmin.unstable_useContentManagerContext();
52
+ const values = admin.useForm("ComponentPreviewPanel", (state) => state.values);
53
+ const { get } = admin.useFetchClient();
54
+ const [optionsMap, setOptionsMap] = react.useState({});
55
+ react.useEffect(() => {
56
+ let cancelled = false;
57
+ get("/api/component-preview-image/options").then(({ data }) => {
58
+ if (!cancelled) setOptionsMap(data ?? {});
59
+ }).catch(() => {
60
+ if (!cancelled) setOptionsMap({});
61
+ });
62
+ return () => {
63
+ cancelled = true;
64
+ };
65
+ }, []);
66
+ const previewItems = collectPreviewItems(
67
+ values,
68
+ contentType?.attributes,
69
+ components,
70
+ optionsMap
71
+ );
72
+ if (isCreatingEntry) {
73
+ return {
74
+ title: "Component previews",
75
+ content: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", textColor: "neutral600", children: "Save this entry once to load component previews." })
76
+ };
77
+ }
78
+ if (previewItems.length === 0) {
79
+ return {
80
+ title: "Component previews",
81
+ content: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", textColor: "neutral600", children: "No component previews are available for this entry yet." })
82
+ };
83
+ }
84
+ return {
85
+ title: "Component previews",
86
+ content: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 4, alignItems: "stretch", children: previewItems.map((item, index2) => /* @__PURE__ */ jsxRuntime.jsx(
87
+ designSystem.Box,
88
+ {
89
+ borderColor: "neutral200",
90
+ background: "neutral0",
91
+ hasRadius: true,
92
+ padding: 3,
93
+ shadow: "tableShadow",
94
+ width: "100%",
95
+ overflow: "hidden",
96
+ style: { boxSizing: "border-box" },
97
+ children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 3, alignItems: "stretch", children: [
98
+ /* @__PURE__ */ jsxRuntime.jsx(
99
+ "img",
100
+ {
101
+ src: item.previewUrl,
102
+ alt: item.previewName || item.displayName,
103
+ style: {
104
+ width: "100%",
105
+ maxWidth: "100%",
106
+ display: "block",
107
+ borderRadius: "8px",
108
+ border: "1px solid #dcdce4",
109
+ objectFit: "cover",
110
+ boxSizing: "border-box"
111
+ }
112
+ }
113
+ ),
114
+ /* @__PURE__ */ jsxRuntime.jsxs(
115
+ designSystem.Flex,
116
+ {
117
+ justifyContent: "space-between",
118
+ alignItems: "flex-start",
119
+ gap: 2,
120
+ width: "100%",
121
+ children: [
122
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { style: { minWidth: 0, flex: 1 }, children: [
123
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "neutral800", children: item.previewName || item.displayName }),
124
+ /* @__PURE__ */ jsxRuntime.jsx(
125
+ designSystem.Typography,
126
+ {
127
+ variant: "pi",
128
+ textColor: "neutral600",
129
+ style: {
130
+ display: "block",
131
+ overflowWrap: "anywhere",
132
+ wordBreak: "break-word"
133
+ },
134
+ children: item.uid
135
+ }
136
+ )
137
+ ] }),
138
+ /* @__PURE__ */ jsxRuntime.jsx(
139
+ designSystem.Button,
140
+ {
141
+ variant: "tertiary",
142
+ size: "S",
143
+ tag: "a",
144
+ href: item.previewUrl,
145
+ target: "_blank",
146
+ rel: "noreferrer",
147
+ endIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.ExternalLink, {}),
148
+ style: { flexShrink: 0 },
149
+ children: "Open"
150
+ }
151
+ )
152
+ ]
153
+ }
154
+ )
155
+ ] })
156
+ },
157
+ item.tempKey ?? `${index2}-${item.uid}`
158
+ )) })
159
+ };
160
+ };
161
+ const index = {
162
+ register(app) {
163
+ app.customFields.register({
164
+ name: "preview-image",
165
+ pluginId: "component-preview-image",
166
+ type: "string",
167
+ inputSize: { default: 12, isResizable: false },
168
+ intlLabel: {
169
+ id: "component-preview-image.preview-image.label",
170
+ defaultMessage: "Preview Image"
171
+ },
172
+ intlDescription: {
173
+ id: "component-preview-image.preview-image.description",
174
+ defaultMessage: "Schema-level preview image — set the image URL once in the Content-Type Builder, shown in the edit-view side panel"
175
+ },
176
+ components: {
177
+ Input: async () => Promise.resolve().then(() => require("../_chunks/PreviewImageInput-Bha74a3E.js")).then((mod) => ({
178
+ default: mod.PreviewImageInput
179
+ }))
180
+ },
181
+ options: {
182
+ base: [
183
+ {
184
+ name: "options.url",
185
+ type: "text",
186
+ intlLabel: {
187
+ id: "component-preview-image.options.url.label",
188
+ defaultMessage: "Preview Image URL"
189
+ },
190
+ description: {
191
+ id: "component-preview-image.options.url.description",
192
+ defaultMessage: "Direct URL of the image to display in the preview panel"
193
+ }
194
+ }
195
+ ]
196
+ }
197
+ });
198
+ },
199
+ bootstrap(app) {
200
+ app.getPlugin("content-manager").apis.addEditViewSidePanel([ComponentPreviewPanel]);
201
+ }
202
+ };
203
+ module.exports = index;
@@ -0,0 +1,204 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import { Typography, Flex, Box, Button } from "@strapi/design-system";
4
+ import { unstable_useContentManagerContext } from "@strapi/content-manager/strapi-admin";
5
+ import { useForm, useFetchClient } from "@strapi/strapi/admin";
6
+ import { ExternalLink } from "@strapi/icons";
7
+ const collectPreviewItems = (value, attributes, componentSchemas, optionsMap) => {
8
+ const items = [];
9
+ if (!value || !attributes || typeof value !== "object") return items;
10
+ const pushItem = (componentUid, tempKey) => {
11
+ const opts = optionsMap[componentUid];
12
+ if (!opts) return;
13
+ const schema = componentSchemas[componentUid];
14
+ items.push({
15
+ uid: componentUid,
16
+ displayName: schema?.info?.displayName ?? componentUid,
17
+ previewUrl: opts.url,
18
+ previewName: opts.name,
19
+ count: 1,
20
+ tempKey
21
+ });
22
+ };
23
+ for (const [attributeName, attribute] of Object.entries(attributes)) {
24
+ const attributeValue = value[attributeName];
25
+ if (!attributeValue) continue;
26
+ if (attribute.type === "dynamiczone" && Array.isArray(attributeValue)) {
27
+ for (const item of attributeValue) {
28
+ if (!item || typeof item !== "object") continue;
29
+ const componentUid = item.__component;
30
+ const tempKey = item.__temp_key__;
31
+ if (componentUid) pushItem(componentUid, tempKey);
32
+ }
33
+ continue;
34
+ }
35
+ if (attribute.type === "component" && attribute.component) {
36
+ const componentUid = attribute.component;
37
+ if (attribute.repeatable && Array.isArray(attributeValue)) {
38
+ for (let i = 0; i < attributeValue.length; i++) {
39
+ const item = attributeValue[i];
40
+ pushItem(componentUid, item?.__temp_key__);
41
+ }
42
+ } else {
43
+ pushItem(componentUid);
44
+ }
45
+ }
46
+ }
47
+ return items;
48
+ };
49
+ const ComponentPreviewPanel = () => {
50
+ const { components, contentType, isCreatingEntry } = unstable_useContentManagerContext();
51
+ const values = useForm("ComponentPreviewPanel", (state) => state.values);
52
+ const { get } = useFetchClient();
53
+ const [optionsMap, setOptionsMap] = useState({});
54
+ useEffect(() => {
55
+ let cancelled = false;
56
+ get("/api/component-preview-image/options").then(({ data }) => {
57
+ if (!cancelled) setOptionsMap(data ?? {});
58
+ }).catch(() => {
59
+ if (!cancelled) setOptionsMap({});
60
+ });
61
+ return () => {
62
+ cancelled = true;
63
+ };
64
+ }, []);
65
+ const previewItems = collectPreviewItems(
66
+ values,
67
+ contentType?.attributes,
68
+ components,
69
+ optionsMap
70
+ );
71
+ if (isCreatingEntry) {
72
+ return {
73
+ title: "Component previews",
74
+ content: /* @__PURE__ */ jsx(Typography, { variant: "omega", textColor: "neutral600", children: "Save this entry once to load component previews." })
75
+ };
76
+ }
77
+ if (previewItems.length === 0) {
78
+ return {
79
+ title: "Component previews",
80
+ content: /* @__PURE__ */ jsx(Typography, { variant: "omega", textColor: "neutral600", children: "No component previews are available for this entry yet." })
81
+ };
82
+ }
83
+ return {
84
+ title: "Component previews",
85
+ content: /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 4, alignItems: "stretch", children: previewItems.map((item, index2) => /* @__PURE__ */ jsx(
86
+ Box,
87
+ {
88
+ borderColor: "neutral200",
89
+ background: "neutral0",
90
+ hasRadius: true,
91
+ padding: 3,
92
+ shadow: "tableShadow",
93
+ width: "100%",
94
+ overflow: "hidden",
95
+ style: { boxSizing: "border-box" },
96
+ children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, alignItems: "stretch", children: [
97
+ /* @__PURE__ */ jsx(
98
+ "img",
99
+ {
100
+ src: item.previewUrl,
101
+ alt: item.previewName || item.displayName,
102
+ style: {
103
+ width: "100%",
104
+ maxWidth: "100%",
105
+ display: "block",
106
+ borderRadius: "8px",
107
+ border: "1px solid #dcdce4",
108
+ objectFit: "cover",
109
+ boxSizing: "border-box"
110
+ }
111
+ }
112
+ ),
113
+ /* @__PURE__ */ jsxs(
114
+ Flex,
115
+ {
116
+ justifyContent: "space-between",
117
+ alignItems: "flex-start",
118
+ gap: 2,
119
+ width: "100%",
120
+ children: [
121
+ /* @__PURE__ */ jsxs(Box, { style: { minWidth: 0, flex: 1 }, children: [
122
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral800", children: item.previewName || item.displayName }),
123
+ /* @__PURE__ */ jsx(
124
+ Typography,
125
+ {
126
+ variant: "pi",
127
+ textColor: "neutral600",
128
+ style: {
129
+ display: "block",
130
+ overflowWrap: "anywhere",
131
+ wordBreak: "break-word"
132
+ },
133
+ children: item.uid
134
+ }
135
+ )
136
+ ] }),
137
+ /* @__PURE__ */ jsx(
138
+ Button,
139
+ {
140
+ variant: "tertiary",
141
+ size: "S",
142
+ tag: "a",
143
+ href: item.previewUrl,
144
+ target: "_blank",
145
+ rel: "noreferrer",
146
+ endIcon: /* @__PURE__ */ jsx(ExternalLink, {}),
147
+ style: { flexShrink: 0 },
148
+ children: "Open"
149
+ }
150
+ )
151
+ ]
152
+ }
153
+ )
154
+ ] })
155
+ },
156
+ item.tempKey ?? `${index2}-${item.uid}`
157
+ )) })
158
+ };
159
+ };
160
+ const index = {
161
+ register(app) {
162
+ app.customFields.register({
163
+ name: "preview-image",
164
+ pluginId: "component-preview-image",
165
+ type: "string",
166
+ inputSize: { default: 12, isResizable: false },
167
+ intlLabel: {
168
+ id: "component-preview-image.preview-image.label",
169
+ defaultMessage: "Preview Image"
170
+ },
171
+ intlDescription: {
172
+ id: "component-preview-image.preview-image.description",
173
+ defaultMessage: "Schema-level preview image — set the image URL once in the Content-Type Builder, shown in the edit-view side panel"
174
+ },
175
+ components: {
176
+ Input: async () => import("../_chunks/PreviewImageInput-BcPJkyKS.mjs").then((mod) => ({
177
+ default: mod.PreviewImageInput
178
+ }))
179
+ },
180
+ options: {
181
+ base: [
182
+ {
183
+ name: "options.url",
184
+ type: "text",
185
+ intlLabel: {
186
+ id: "component-preview-image.options.url.label",
187
+ defaultMessage: "Preview Image URL"
188
+ },
189
+ description: {
190
+ id: "component-preview-image.options.url.description",
191
+ defaultMessage: "Direct URL of the image to display in the preview panel"
192
+ }
193
+ }
194
+ ]
195
+ }
196
+ });
197
+ },
198
+ bootstrap(app) {
199
+ app.getPlugin("content-manager").apis.addEditViewSidePanel([ComponentPreviewPanel]);
200
+ }
201
+ };
202
+ export {
203
+ index as default
204
+ };
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ const CUSTOM_FIELD_KEY = "plugin::component-preview-image.preview-image";
3
+ const optionsController = ({ strapi }) => ({
4
+ async getOptions(ctx) {
5
+ const result = {};
6
+ for (const [uid, schema] of Object.entries(strapi.components)) {
7
+ for (const attr of Object.values(schema.attributes)) {
8
+ if (attr.type === "customField" && attr.customField === CUSTOM_FIELD_KEY && attr.options?.url) {
9
+ result[uid] = {
10
+ name: schema.info?.displayName || uid,
11
+ url: attr.options.url
12
+ };
13
+ break;
14
+ }
15
+ }
16
+ }
17
+ ctx.body = result;
18
+ }
19
+ });
20
+ const PLUGIN_NAME = "component-preview-image";
21
+ const index = {
22
+ register({ strapi }) {
23
+ strapi.customFields.register({
24
+ name: "preview-image",
25
+ plugin: PLUGIN_NAME,
26
+ type: "string"
27
+ });
28
+ },
29
+ bootstrap() {
30
+ },
31
+ destroy() {
32
+ },
33
+ config: {},
34
+ routes: {
35
+ "content-api": {
36
+ type: "content-api",
37
+ routes: [
38
+ {
39
+ method: "GET",
40
+ path: "/options",
41
+ handler: "options.getOptions",
42
+ config: {
43
+ auth: false
44
+ }
45
+ }
46
+ ]
47
+ }
48
+ },
49
+ controllers: {
50
+ options: optionsController
51
+ },
52
+ services: {},
53
+ policies: {},
54
+ middlewares: {}
55
+ };
56
+ module.exports = index;
@@ -0,0 +1,57 @@
1
+ const CUSTOM_FIELD_KEY = "plugin::component-preview-image.preview-image";
2
+ const optionsController = ({ strapi }) => ({
3
+ async getOptions(ctx) {
4
+ const result = {};
5
+ for (const [uid, schema] of Object.entries(strapi.components)) {
6
+ for (const attr of Object.values(schema.attributes)) {
7
+ if (attr.type === "customField" && attr.customField === CUSTOM_FIELD_KEY && attr.options?.url) {
8
+ result[uid] = {
9
+ name: schema.info?.displayName || uid,
10
+ url: attr.options.url
11
+ };
12
+ break;
13
+ }
14
+ }
15
+ }
16
+ ctx.body = result;
17
+ }
18
+ });
19
+ const PLUGIN_NAME = "component-preview-image";
20
+ const index = {
21
+ register({ strapi }) {
22
+ strapi.customFields.register({
23
+ name: "preview-image",
24
+ plugin: PLUGIN_NAME,
25
+ type: "string"
26
+ });
27
+ },
28
+ bootstrap() {
29
+ },
30
+ destroy() {
31
+ },
32
+ config: {},
33
+ routes: {
34
+ "content-api": {
35
+ type: "content-api",
36
+ routes: [
37
+ {
38
+ method: "GET",
39
+ path: "/options",
40
+ handler: "options.getOptions",
41
+ config: {
42
+ auth: false
43
+ }
44
+ }
45
+ ]
46
+ }
47
+ },
48
+ controllers: {
49
+ options: optionsController
50
+ },
51
+ services: {},
52
+ policies: {},
53
+ middlewares: {}
54
+ };
55
+ export {
56
+ index as default
57
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devx-labs/strapi-preview",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Strapi 5 plugin that adds a preview-image custom field (configured per component in the Content-Type Builder with a direct image URL) and renders a side panel in the edit view showing each component's preview image in dynamic-zone order.",
5
5
  "keywords": [
6
6
  "strapi",
@@ -11,9 +11,8 @@
11
11
  "custom-field",
12
12
  "side-panel"
13
13
  ],
14
- "author": {
15
- "name": "Pratham Bhatia"
16
- },
14
+ "type": "commonjs",
15
+ "author": "Pratham Bhatia",
17
16
  "license": "MIT",
18
17
  "repository": {
19
18
  "type": "git",
@@ -23,33 +22,51 @@
23
22
  "bugs": {
24
23
  "url": "https://github.com/prathambdevx/strapi-component-preview/issues"
25
24
  },
26
- "main": "./strapi-server.js",
27
25
  "exports": {
28
26
  "./package.json": "./package.json",
29
27
  "./strapi-admin": {
30
- "import": "./strapi-admin.js",
31
- "default": "./strapi-admin.js"
28
+ "source": "./admin/src/index.tsx",
29
+ "import": "./dist/admin/index.mjs",
30
+ "require": "./dist/admin/index.js",
31
+ "default": "./dist/admin/index.js"
32
32
  },
33
33
  "./strapi-server": {
34
- "require": "./strapi-server.js",
35
- "default": "./strapi-server.js"
36
- },
37
- ".": "./strapi-server.js"
34
+ "source": "./server/src/index.ts",
35
+ "import": "./dist/server/index.mjs",
36
+ "require": "./dist/server/index.js",
37
+ "default": "./dist/server/index.js"
38
+ }
38
39
  },
39
40
  "files": [
40
- "admin",
41
- "server",
42
- "strapi-admin.js",
43
- "strapi-server.js",
41
+ "dist",
44
42
  "README.md",
45
43
  "LICENSE"
46
44
  ],
45
+ "scripts": {
46
+ "build": "strapi-plugin build",
47
+ "watch": "strapi-plugin watch",
48
+ "verify": "strapi-plugin verify"
49
+ },
47
50
  "strapi": {
48
51
  "name": "component-preview-image",
49
52
  "displayName": "Strapi Preview",
50
53
  "description": "Adds a preview-image custom field (URL configured in CTB) and an edit-view side panel that displays those images.",
51
54
  "kind": "plugin"
52
55
  },
56
+ "dependencies": {},
57
+ "devDependencies": {
58
+ "@strapi/sdk-plugin": "^5.3.2",
59
+ "@strapi/strapi": "^5.0.0",
60
+ "@strapi/design-system": "^2.0.0-rc.21",
61
+ "@strapi/icons": "^2.0.0-rc.21",
62
+ "@types/react": "^18.0.0",
63
+ "@types/react-dom": "^18.0.0",
64
+ "react": "^18.0.0",
65
+ "react-dom": "^18.0.0",
66
+ "react-router-dom": "^6.0.0",
67
+ "styled-components": "^6.0.0",
68
+ "typescript": "^5.0.0"
69
+ },
53
70
  "peerDependencies": {
54
71
  "@strapi/content-manager": ">=5.0.0",
55
72
  "@strapi/design-system": ">=2.0.0-0",
@@ -1,206 +0,0 @@
1
- import { useEffect, useState } from 'react';
2
- import { Box, Button, Flex, Typography } from '@strapi/design-system';
3
- import type { PanelComponent } from '@strapi/content-manager/strapi-admin';
4
- import { unstable_useContentManagerContext } from '@strapi/content-manager/strapi-admin';
5
- import { useForm, useFetchClient } from '@strapi/strapi/admin';
6
- import { ExternalLink } from '@strapi/icons';
7
-
8
- type SchemaAttribute = {
9
- type?: string;
10
- component?: string;
11
- repeatable?: boolean;
12
- };
13
-
14
- type SchemaDefinition = {
15
- attributes?: Record<string, SchemaAttribute>;
16
- info?: { displayName?: string };
17
- };
18
-
19
- type PreviewOption = { name: string; url: string };
20
-
21
- type PreviewItem = {
22
- uid: string;
23
- displayName: string;
24
- previewUrl: string;
25
- previewName: string;
26
- count: number;
27
- tempKey?: string;
28
- };
29
-
30
- const collectPreviewItems = (
31
- value: unknown,
32
- attributes: Record<string, SchemaAttribute> | undefined,
33
- componentSchemas: Record<string, SchemaDefinition>,
34
- optionsMap: Record<string, PreviewOption>
35
- ): PreviewItem[] => {
36
- const items: PreviewItem[] = [];
37
-
38
- if (!value || !attributes || typeof value !== 'object') return items;
39
-
40
- const pushItem = (componentUid: string, tempKey?: string) => {
41
- const opts = optionsMap[componentUid];
42
- if (!opts) return;
43
- const schema = componentSchemas[componentUid];
44
- items.push({
45
- uid: componentUid,
46
- displayName: schema?.info?.displayName ?? componentUid,
47
- previewUrl: opts.url,
48
- previewName: opts.name,
49
- count: 1,
50
- tempKey,
51
- });
52
- };
53
-
54
- for (const [attributeName, attribute] of Object.entries(attributes)) {
55
- const attributeValue = (value as Record<string, unknown>)[attributeName];
56
- if (!attributeValue) continue;
57
-
58
- if (attribute.type === 'dynamiczone' && Array.isArray(attributeValue)) {
59
- for (const item of attributeValue) {
60
- if (!item || typeof item !== 'object') continue;
61
- const componentUid = (item as { __component?: string }).__component;
62
- const tempKey = (item as { __temp_key__?: string }).__temp_key__;
63
- if (componentUid) pushItem(componentUid, tempKey);
64
- }
65
- continue;
66
- }
67
-
68
- if (attribute.type === 'component' && attribute.component) {
69
- const componentUid = attribute.component;
70
- if (attribute.repeatable && Array.isArray(attributeValue)) {
71
- for (let i = 0; i < attributeValue.length; i++) {
72
- const item = attributeValue[i] as { __temp_key__?: string } | undefined;
73
- pushItem(componentUid, item?.__temp_key__);
74
- }
75
- } else {
76
- pushItem(componentUid);
77
- }
78
- }
79
- }
80
-
81
- return items;
82
- };
83
-
84
- export const ComponentPreviewPanel: PanelComponent = () => {
85
- const { components, contentType, isCreatingEntry } = unstable_useContentManagerContext();
86
- const values = useForm('ComponentPreviewPanel', (state) => state.values);
87
- const { get } = useFetchClient();
88
- const [optionsMap, setOptionsMap] = useState<Record<string, PreviewOption>>({});
89
-
90
- useEffect(() => {
91
- let cancelled = false;
92
-
93
- get('/api/component-preview-image/options')
94
- .then(({ data }: { data: Record<string, PreviewOption> }) => {
95
- if (!cancelled) setOptionsMap(data ?? {});
96
- })
97
- .catch(() => {
98
- if (!cancelled) setOptionsMap({});
99
- });
100
-
101
- return () => {
102
- cancelled = true;
103
- };
104
- }, []);
105
-
106
- const previewItems = collectPreviewItems(
107
- values,
108
- contentType?.attributes,
109
- components as Record<string, SchemaDefinition>,
110
- optionsMap
111
- );
112
-
113
- if (isCreatingEntry) {
114
- return {
115
- title: 'Component previews',
116
- content: (
117
- <Typography variant="omega" textColor="neutral600">
118
- Save this entry once to load component previews.
119
- </Typography>
120
- ),
121
- };
122
- }
123
-
124
- if (previewItems.length === 0) {
125
- return {
126
- title: 'Component previews',
127
- content: (
128
- <Typography variant="omega" textColor="neutral600">
129
- No component previews are available for this entry yet.
130
- </Typography>
131
- ),
132
- };
133
- }
134
-
135
- return {
136
- title: 'Component previews',
137
- content: (
138
- <Flex direction="column" gap={4} alignItems="stretch">
139
- {previewItems.map((item, index) => (
140
- <Box
141
- key={item.tempKey ?? `${index}-${item.uid}`}
142
- borderColor="neutral200"
143
- background="neutral0"
144
- hasRadius
145
- padding={3}
146
- shadow="tableShadow"
147
- width="100%"
148
- overflow="hidden"
149
- style={{ boxSizing: 'border-box' }}
150
- >
151
- <Flex direction="column" gap={3} alignItems="stretch">
152
- <img
153
- src={item.previewUrl}
154
- alt={item.previewName || item.displayName}
155
- style={{
156
- width: '100%',
157
- maxWidth: '100%',
158
- display: 'block',
159
- borderRadius: '8px',
160
- border: '1px solid #dcdce4',
161
- objectFit: 'cover',
162
- boxSizing: 'border-box',
163
- }}
164
- />
165
- <Flex
166
- justifyContent="space-between"
167
- alignItems="flex-start"
168
- gap={2}
169
- width="100%"
170
- >
171
- <Box style={{ minWidth: 0, flex: 1 }}>
172
- <Typography variant="sigma" textColor="neutral800">
173
- {item.previewName || item.displayName}
174
- </Typography>
175
- <Typography
176
- variant="pi"
177
- textColor="neutral600"
178
- style={{
179
- display: 'block',
180
- overflowWrap: 'anywhere',
181
- wordBreak: 'break-word',
182
- }}
183
- >
184
- {item.uid}
185
- </Typography>
186
- </Box>
187
- <Button
188
- variant="tertiary"
189
- size="S"
190
- tag="a"
191
- href={item.previewUrl}
192
- target="_blank"
193
- rel="noreferrer"
194
- endIcon={<ExternalLink />}
195
- style={{ flexShrink: 0 }}
196
- >
197
- Open
198
- </Button>
199
- </Flex>
200
- </Flex>
201
- </Box>
202
- ))}
203
- </Flex>
204
- ),
205
- };
206
- };
@@ -1,31 +0,0 @@
1
- import { Flex, LinkButton } from '@strapi/design-system';
2
- import { ExternalLink } from '@strapi/icons';
3
-
4
- type PreviewImageInputProps = {
5
- attribute?: {
6
- options?: {
7
- url?: string;
8
- };
9
- };
10
- };
11
-
12
- export const PreviewImageInput = ({ attribute }: PreviewImageInputProps) => {
13
- const url = attribute?.options?.url;
14
-
15
- if (!url) return null;
16
-
17
- return (
18
- <Flex justifyContent="flex-end">
19
- <LinkButton
20
- href={url}
21
- target="_blank"
22
- rel="noreferrer"
23
- variant="tertiary"
24
- size="S"
25
- endIcon={<ExternalLink />}
26
- >
27
- Preview
28
- </LinkButton>
29
- </Flex>
30
- );
31
- };
@@ -1,47 +0,0 @@
1
- import { ComponentPreviewPanel } from './components/ComponentPreviewPanel';
2
-
3
- export default {
4
- register(app: any) {
5
- app.customFields.register({
6
- name: 'preview-image',
7
- pluginId: 'component-preview-image',
8
- type: 'string',
9
- inputSize: { default: 12, isResizable: false },
10
- intlLabel: {
11
- id: 'component-preview-image.preview-image.label',
12
- defaultMessage: 'Preview Image',
13
- },
14
- intlDescription: {
15
- id: 'component-preview-image.preview-image.description',
16
- defaultMessage:
17
- 'Schema-level preview image — set the image URL once in the Content-Type Builder, shown in the edit-view side panel',
18
- },
19
- components: {
20
- Input: async () =>
21
- import('./components/PreviewImageInput').then((mod) => ({
22
- default: mod.PreviewImageInput,
23
- })),
24
- },
25
- options: {
26
- base: [
27
- {
28
- name: 'options.url',
29
- type: 'text',
30
- intlLabel: {
31
- id: 'component-preview-image.options.url.label',
32
- defaultMessage: 'Preview Image URL',
33
- },
34
- description: {
35
- id: 'component-preview-image.options.url.description',
36
- defaultMessage: 'Direct URL of the image to display in the preview panel',
37
- },
38
- },
39
- ],
40
- },
41
- });
42
- },
43
-
44
- bootstrap(app: any) {
45
- app.getPlugin('content-manager').apis.addEditViewSidePanel([ComponentPreviewPanel]);
46
- },
47
- };
@@ -1,27 +0,0 @@
1
- import type { Core } from '@strapi/strapi';
2
-
3
- const CUSTOM_FIELD_KEY = 'plugin::component-preview-image.preview-image';
4
-
5
- export default ({ strapi }: { strapi: Core.Strapi }) => ({
6
- async getOptions(ctx: any) {
7
- const result: Record<string, { name: string; url: string }> = {};
8
-
9
- for (const [uid, schema] of Object.entries(strapi.components as Record<string, any>)) {
10
- for (const attr of Object.values(schema.attributes as Record<string, any>)) {
11
- if (
12
- attr.type === 'customField' &&
13
- attr.customField === CUSTOM_FIELD_KEY &&
14
- attr.options?.url
15
- ) {
16
- result[uid] = {
17
- name: schema.info?.displayName || uid,
18
- url: attr.options.url,
19
- };
20
- break;
21
- }
22
- }
23
- }
24
-
25
- ctx.body = result;
26
- },
27
- });
@@ -1,43 +0,0 @@
1
- import type { Core } from '@strapi/strapi';
2
- import optionsController from './controllers/options';
3
-
4
- const PLUGIN_NAME = 'component-preview-image';
5
-
6
- export default {
7
- register({ strapi }: { strapi: Core.Strapi }) {
8
- strapi.customFields.register({
9
- name: 'preview-image',
10
- plugin: PLUGIN_NAME,
11
- type: 'string',
12
- });
13
- },
14
-
15
- bootstrap() {},
16
- destroy() {},
17
-
18
- config: {},
19
-
20
- routes: {
21
- 'content-api': {
22
- type: 'content-api',
23
- routes: [
24
- {
25
- method: 'GET',
26
- path: '/options',
27
- handler: 'options.getOptions',
28
- config: {
29
- auth: false,
30
- },
31
- },
32
- ],
33
- },
34
- },
35
-
36
- controllers: {
37
- options: optionsController,
38
- },
39
-
40
- services: {},
41
- policies: {},
42
- middlewares: {},
43
- };
@@ -1,10 +0,0 @@
1
- export default [
2
- {
3
- method: 'GET',
4
- path: '/options',
5
- handler: 'options.getOptions',
6
- config: {
7
- auth: false,
8
- },
9
- },
10
- ];
package/strapi-admin.js DELETED
@@ -1 +0,0 @@
1
- export { default } from './admin/src/index';
package/strapi-server.js DELETED
@@ -1,61 +0,0 @@
1
- 'use strict';
2
-
3
- const CUSTOM_FIELD_KEY = 'plugin::component-preview-image.preview-image';
4
-
5
- module.exports = {
6
- register({ strapi }) {
7
- strapi.customFields.register({
8
- name: 'preview-image',
9
- plugin: 'component-preview-image',
10
- type: 'string',
11
- inputSize: { default: 12, isResizable: false },
12
- });
13
- },
14
-
15
- bootstrap() {},
16
- destroy() {},
17
- config: {},
18
-
19
- routes: {
20
- 'content-api': {
21
- type: 'content-api',
22
- routes: [
23
- {
24
- method: 'GET',
25
- path: '/options',
26
- handler: 'options.getOptions',
27
- config: { auth: false },
28
- },
29
- ],
30
- },
31
- },
32
-
33
- controllers: {
34
- options: ({ strapi }) => ({
35
- async getOptions(ctx) {
36
- const result = {};
37
-
38
- for (const [uid, schema] of Object.entries(strapi.components || {})) {
39
- for (const attr of Object.values(schema.attributes || {})) {
40
- if (
41
- attr.customField === CUSTOM_FIELD_KEY &&
42
- attr.options?.url
43
- ) {
44
- result[uid] = {
45
- name: schema.info?.displayName || uid,
46
- url: attr.options.url,
47
- };
48
- break;
49
- }
50
- }
51
- }
52
-
53
- ctx.body = result;
54
- },
55
- }),
56
- },
57
-
58
- services: {},
59
- policies: {},
60
- middlewares: {},
61
- };