@devx-labs/strapi-preview 1.0.3
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/LICENSE +21 -0
- package/README.md +77 -0
- package/admin/src/components/ComponentPreviewPanel.tsx +206 -0
- package/admin/src/components/PreviewImageInput.tsx +31 -0
- package/admin/src/index.tsx +47 -0
- package/package.json +61 -0
- package/server/src/controllers/options.ts +27 -0
- package/server/src/index.ts +43 -0
- package/server/src/routes/index.ts +10 -0
- package/strapi-admin.js +1 -0
- package/strapi-server.js +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pratham Bhatia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @devx-labs/strapi-preview
|
|
2
|
+
|
|
3
|
+
A Strapi 5 plugin that adds a **preview image URL** field to any component and renders all component previews as a side panel in the edit view.
|
|
4
|
+
|
|
5
|
+
Component authors set the image URL once in the Content-Type Builder (no Media Library required — any HTTPS URL works), and the panel displays the matching image live for every component on the entry. Dynamic-zone reorders are reflected immediately, and components without a configured preview are simply skipped.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @devx-labs/strapi-preview
|
|
11
|
+
# or
|
|
12
|
+
yarn add @devx-labs/strapi-preview
|
|
13
|
+
# or
|
|
14
|
+
bun add @devx-labs/strapi-preview
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Register the plugin in `config/plugins.ts` (Strapi only auto-discovers packages whose names start with `strapi-plugin-` — this one uses a scope, so an explicit entry is needed):
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
export default () => ({
|
|
21
|
+
'component-preview-image': {
|
|
22
|
+
enabled: true,
|
|
23
|
+
resolve: '@devx-labs/strapi-preview',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The plugin's internal id is `component-preview-image` (that's why the key in the config object is `component-preview-image`, not `strapi-preview`). Don't change that key.
|
|
29
|
+
|
|
30
|
+
Rebuild the admin:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm run build
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Add the preview field to your components
|
|
37
|
+
|
|
38
|
+
In Strapi's Content-Type Builder, edit a component → **Add another field** → **Custom** → pick **"Preview Image"**. Name the field exactly `preview` (any name works, but `preview` is the convention).
|
|
39
|
+
|
|
40
|
+
In the field's **Base settings**, fill in **Preview Image URL** with a direct image URL (Shopify CDN, S3, Cloudinary, picsum, anything reachable by the browser).
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
1. Configure the `preview` field on each component you want to preview (URL is set once per component schema in CTB).
|
|
45
|
+
2. Open any entry that uses your components — for example a Page with a dynamic zone of blocks.
|
|
46
|
+
3. The **Component previews** side panel on the right shows every component on the entry, with its preview image, in the current dynamic-zone order.
|
|
47
|
+
4. The custom field itself renders a small **Preview ↗** button inside each component instance so editors can quickly open the full image in a new tab.
|
|
48
|
+
|
|
49
|
+
### Behavior
|
|
50
|
+
|
|
51
|
+
- Dynamic zone: panel items appear in the current order. Reorder components → panel reorders live (no save required).
|
|
52
|
+
- Components without a configured `preview` URL are silently skipped.
|
|
53
|
+
- Regular (non-dynamic-zone) component fields are also rendered, including repeatable components.
|
|
54
|
+
- No Media Library coupling: URLs are plain strings, so previews can live on any CDN — useful when your component thumbnails live separately from your production media (e.g. a private design-system bucket).
|
|
55
|
+
|
|
56
|
+
## CSP note
|
|
57
|
+
|
|
58
|
+
If your preview URLs point to external hosts (Shopify CDN, etc.), make sure your CSP `img-src` permits them. The simplest broad allow is `"https:"` in `config/middlewares.ts`:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
'img-src': ["'self'", 'data:', 'blob:', 'https:', 'market-assets.strapi.io'],
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## What the plugin registers
|
|
65
|
+
|
|
66
|
+
- Custom field type: `plugin::component-preview-image.preview-image` (stored as `string`)
|
|
67
|
+
- Custom field UI: a compact **Preview ↗** link button rendered inside each component instance
|
|
68
|
+
- Content-Manager edit-view side panel: **Component previews**
|
|
69
|
+
- Content-API route: `GET /api/component-preview-image/options` returning a `{ [componentUid]: { name, url } }` map (used internally by the side panel)
|
|
70
|
+
|
|
71
|
+
## Supported Strapi versions
|
|
72
|
+
|
|
73
|
+
Strapi 5.x
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
|
@@ -0,0 +1,206 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@devx-labs/strapi-preview",
|
|
3
|
+
"version": "1.0.3",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"strapi",
|
|
7
|
+
"plugin",
|
|
8
|
+
"strapi-plugin",
|
|
9
|
+
"component-preview",
|
|
10
|
+
"preview-image",
|
|
11
|
+
"custom-field",
|
|
12
|
+
"side-panel"
|
|
13
|
+
],
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Pratham Bhatia"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/prathambdevx/strapi-component-preview.git"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/prathambdevx/strapi-component-preview#readme",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/prathambdevx/strapi-component-preview/issues"
|
|
25
|
+
},
|
|
26
|
+
"main": "./strapi-server.js",
|
|
27
|
+
"exports": {
|
|
28
|
+
"./package.json": "./package.json",
|
|
29
|
+
"./strapi-admin": {
|
|
30
|
+
"import": "./strapi-admin.js",
|
|
31
|
+
"default": "./strapi-admin.js"
|
|
32
|
+
},
|
|
33
|
+
"./strapi-server": {
|
|
34
|
+
"require": "./strapi-server.js",
|
|
35
|
+
"default": "./strapi-server.js"
|
|
36
|
+
},
|
|
37
|
+
".": "./strapi-server.js"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"admin",
|
|
41
|
+
"server",
|
|
42
|
+
"strapi-admin.js",
|
|
43
|
+
"strapi-server.js",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
47
|
+
"strapi": {
|
|
48
|
+
"name": "component-preview-image",
|
|
49
|
+
"displayName": "Strapi Preview",
|
|
50
|
+
"description": "Adds a preview-image custom field (URL configured in CTB) and an edit-view side panel that displays those images.",
|
|
51
|
+
"kind": "plugin"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@strapi/content-manager": ">=5.0.0",
|
|
55
|
+
"@strapi/design-system": ">=2.0.0-0",
|
|
56
|
+
"@strapi/icons": ">=2.0.0-0",
|
|
57
|
+
"@strapi/strapi": ">=5.0.0",
|
|
58
|
+
"react": "^17.0.0 || ^18.0.0",
|
|
59
|
+
"react-dom": "^17.0.0 || ^18.0.0"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
};
|
package/strapi-admin.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './admin/src/index';
|
package/strapi-server.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
};
|