@qkix/strapi-plugin-blueprint 0.1.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/LICENSE +21 -0
- package/README.md +151 -0
- package/dist/_chunks/DiagramPage-F0OgJkQF.mjs +275 -0
- package/dist/_chunks/DiagramPage-LORj1QlF.js +1 -0
- package/dist/admin/index.js +1 -0
- package/dist/admin/index.mjs +43 -0
- package/dist/admin/src/api.d.ts +50 -0
- package/dist/admin/src/components/Canvas.d.ts +46 -0
- package/dist/admin/src/index.d.ts +11 -0
- package/dist/admin/src/pages/DiagramPage.d.ts +3 -0
- package/dist/admin/src/pluginId.d.ts +6 -0
- package/dist/server/index.js +35 -0
- package/dist/server/index.mjs +438 -0
- package/dist/server/src/config/index.d.ts +21 -0
- package/dist/server/src/controllers/diagram.d.ts +27 -0
- package/dist/server/src/controllers/index.d.ts +11 -0
- package/dist/server/src/index.d.ts +54 -0
- package/dist/server/src/policies/index.d.ts +4 -0
- package/dist/server/src/register.d.ts +12 -0
- package/dist/server/src/routes/admin.d.ts +12 -0
- package/dist/server/src/routes/index.d.ts +14 -0
- package/dist/server/src/services/graph.d.ts +69 -0
- package/dist/server/src/services/index.d.ts +15 -0
- package/dist/server/src/services/layout.d.ts +42 -0
- package/dist/server/src/services/svg.d.ts +23 -0
- package/dist/server/src/types.d.ts +83 -0
- package/dist/server/src/uids.d.ts +7 -0
- package/package.json +121 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 qkix
|
|
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,151 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/qkix/strapi-plugins/main/packages/strapi-plugin-blueprint/docs/logo.png" alt="Blueprint" width="120" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">Blueprint for Strapi</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">A diagram of your content types — <strong>including the components and dynamic zones everything else leaves out</strong>.</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://www.npmjs.com/package/@qkix/strapi-plugin-blueprint">
|
|
11
|
+
<img alt="npm version" src="https://img.shields.io/npm/v/@qkix/strapi-plugin-blueprint.svg" />
|
|
12
|
+
</a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/@qkix/strapi-plugin-blueprint">
|
|
14
|
+
<img alt="npm downloads" src="https://img.shields.io/npm/dm/@qkix/strapi-plugin-blueprint.svg" />
|
|
15
|
+
</a>
|
|
16
|
+
<a href="https://github.com/qkix/strapi-plugins/blob/main/LICENSE">
|
|
17
|
+
<img alt="license" src="https://img.shields.io/npm/l/@qkix/strapi-plugin-blueprint.svg" />
|
|
18
|
+
</a>
|
|
19
|
+
<a href="https://buymeacoffee.com/qkix">
|
|
20
|
+
<img alt="Buy Me a Coffee" src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-support-FFDD00?logo=buymeacoffee&logoColor=black" />
|
|
21
|
+
</a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="https://raw.githubusercontent.com/qkix/strapi-plugins/main/packages/strapi-plugin-blueprint/docs/diagram.png" alt="A schema diagram: Page linked to Article by a relation, to Seo and Link as components, and to Quote and Link through a dynamic zone" width="900" />
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
## Why another one of these
|
|
29
|
+
|
|
30
|
+
Every schema diagram for Strapi draws **relations only**. That is fine for a
|
|
31
|
+
schema built out of relations, and useless for the way most Strapi projects are
|
|
32
|
+
actually built — a handful of content types, and the real structure living in
|
|
33
|
+
components and dynamic zones. Draw relations only and a modern schema comes out
|
|
34
|
+
as a scattering of disconnected boxes.
|
|
35
|
+
|
|
36
|
+
Blueprint draws all three, and tells them apart:
|
|
37
|
+
|
|
38
|
+
| Line | Means |
|
|
39
|
+
| ------ | -------------------------- |
|
|
40
|
+
| solid | a relation |
|
|
41
|
+
| dashed | a component |
|
|
42
|
+
| dotted | a member of a dynamic zone |
|
|
43
|
+
|
|
44
|
+
A component is followed into whatever it contains, so a component of a component
|
|
45
|
+
is on the picture too.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
npm install @qkix/strapi-plugin-blueprint
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// config/plugins.ts
|
|
55
|
+
export default {
|
|
56
|
+
blueprint: {
|
|
57
|
+
enabled: true,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Then open **Blueprint** in the main menu. There is nothing to configure and
|
|
63
|
+
nothing to switch on per content type.
|
|
64
|
+
|
|
65
|
+
## What you get
|
|
66
|
+
|
|
67
|
+
**Drag to pan, scroll to zoom, click a box** to open it in the Content-Type
|
|
68
|
+
Builder. The view fits the whole diagram when it opens and whenever what is
|
|
69
|
+
drawn changes.
|
|
70
|
+
|
|
71
|
+
Boxes that have nowhere to go do not pretend otherwise: a plugin's internal
|
|
72
|
+
tables — Rewind's versions, upload's folders, anything under `admin::` — belong
|
|
73
|
+
on the diagram because they are part of the schema, but the Content-Type Builder
|
|
74
|
+
has no page for them, so they take no pointer and no hover.
|
|
75
|
+
|
|
76
|
+
**Toggles, remembered between visits**: components and dynamic zones, Strapi's
|
|
77
|
+
own `createdAt`-style fields, and the content types belonging to Strapi and to
|
|
78
|
+
other plugins. Layout runs top-to-bottom or left-to-right.
|
|
79
|
+
|
|
80
|
+
**Download SVG** — an actual vector, not a screenshot of one. It is a standalone
|
|
81
|
+
document with its styles and arrowheads inline and nothing fetched from
|
|
82
|
+
anywhere, so it works pasted into a README, opened from a downloads folder, or
|
|
83
|
+
embedded in a docs site, and it stays sharp at any size.
|
|
84
|
+
|
|
85
|
+
## Permissions
|
|
86
|
+
|
|
87
|
+
One permission, `blueprint.read`, under **Settings → Administration panel →
|
|
88
|
+
Roles**. There is nothing else to guard: the plugin only reads, and everything
|
|
89
|
+
it shows is already visible to anyone who can open the Content-Type Builder.
|
|
90
|
+
|
|
91
|
+
## Drawn on the server
|
|
92
|
+
|
|
93
|
+
The diagram is laid out and rendered to SVG by the server, and the admin panel
|
|
94
|
+
displays what it is given. Two things follow from that.
|
|
95
|
+
|
|
96
|
+
The first is that **anything that can make an HTTP request can have the
|
|
97
|
+
diagram** — a docs build, a CI job, a script that commits the current schema
|
|
98
|
+
next to your code:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
GET /blueprint/diagram.svg?components=true&direction=LR
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The same options as the toolbar, as query parameters: `components`, `foreign`,
|
|
105
|
+
`defaultFields`, `direction` (`TB` or `LR`) and `exclude`. `GET /blueprint/graph`
|
|
106
|
+
returns the graph as JSON if you would rather draw it yourself. Both are admin
|
|
107
|
+
routes, so they need an admin session.
|
|
108
|
+
|
|
109
|
+
The second is that there is **no graph library in your admin panel**. Panning,
|
|
110
|
+
zooming and clicking are about a hundred lines over an SVG. The plugins this
|
|
111
|
+
replaces all reached for a React canvas library, and their issue trackers are
|
|
112
|
+
what happened next: `styled-components` version errors, engine warnings, a Node
|
|
113
|
+
20 incompatibility. A layout algorithm on the server has no opinion about your
|
|
114
|
+
front end.
|
|
115
|
+
|
|
116
|
+
## Configuration
|
|
117
|
+
|
|
118
|
+
| Option | Default | What it does |
|
|
119
|
+
| --------- | ------- | --------------------------------------------- |
|
|
120
|
+
| `exclude` | `[]` | Uids never to draw, whatever the toolbar says |
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
blueprint: {
|
|
124
|
+
enabled: true,
|
|
125
|
+
config: {
|
|
126
|
+
exclude: ['api::import-log.import-log'],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The toolbar can hide things too. The difference is that this survives a reload
|
|
132
|
+
and applies to the API as well — it is the answer to "this never belongs on the
|
|
133
|
+
picture", not to "not right now".
|
|
134
|
+
|
|
135
|
+
## Known limitations
|
|
136
|
+
|
|
137
|
+
**Boxes stop at twelve fields** and count the rest as `+n more`. Strapi's own
|
|
138
|
+
`File` has fifteen, and one box four times the height of its neighbours turns a
|
|
139
|
+
diagram into a wall. The full list is a click away in the Content-Type Builder.
|
|
140
|
+
|
|
141
|
+
**Layout is automatic, and cannot be nudged.** Dragging a box to a better spot
|
|
142
|
+
and having it stay there is a real request on the plugins this replaces, and it
|
|
143
|
+
is not built yet.
|
|
144
|
+
|
|
145
|
+
**Polymorphic relations are not drawn.** A `morphToMany` has no single target to
|
|
146
|
+
point at, so there is no honest line to draw for it. The field is still listed on
|
|
147
|
+
its box.
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
MIT © [qkix](https://github.com/qkix)
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { jsx as o, jsxs as f } from "react/jsx-runtime";
|
|
2
|
+
import * as c from "react";
|
|
3
|
+
import { useFetchClient as Y, Page as D, Layouts as O } from "@strapi/admin/strapi-admin";
|
|
4
|
+
import { Box as L, Button as v, Flex as C, Checkbox as B, SingleSelect as F, SingleSelectOption as $, Loader as X, Typography as S } from "@strapi/design-system";
|
|
5
|
+
import { Download as j } from "@strapi/icons";
|
|
6
|
+
import { useNavigate as N } from "react-router-dom";
|
|
7
|
+
import { useTheme as _ } from "styled-components";
|
|
8
|
+
import { PLUGIN_ID as U } from "../admin/index.mjs";
|
|
9
|
+
const W = 0.15, z = 3, P = { x: 0, y: 0, scale: 1 }, G = (n, s, r = 24) => {
|
|
10
|
+
if (n.width <= 0 || n.height <= 0) return P;
|
|
11
|
+
const a = Math.min(
|
|
12
|
+
1,
|
|
13
|
+
(s.width - r * 2) / n.width,
|
|
14
|
+
(s.height - r * 2) / n.height
|
|
15
|
+
);
|
|
16
|
+
return {
|
|
17
|
+
scale: a,
|
|
18
|
+
x: (s.width - n.width * a) / 2,
|
|
19
|
+
y: (s.height - n.height * a) / 2
|
|
20
|
+
};
|
|
21
|
+
}, H = ({
|
|
22
|
+
svg: n,
|
|
23
|
+
onOpen: s,
|
|
24
|
+
view: r,
|
|
25
|
+
onViewChange: a,
|
|
26
|
+
fitTo: i,
|
|
27
|
+
refit: w
|
|
28
|
+
}) => {
|
|
29
|
+
const u = c.useRef(null), l = c.useRef(
|
|
30
|
+
null
|
|
31
|
+
), g = _(), y = c.useMemo(() => {
|
|
32
|
+
const t = g?.colors ?? {};
|
|
33
|
+
return {
|
|
34
|
+
"--bp-surface": t.neutral0,
|
|
35
|
+
"--bp-box": t.neutral0,
|
|
36
|
+
"--bp-box-component": t.neutral100,
|
|
37
|
+
"--bp-border": t.neutral200,
|
|
38
|
+
"--bp-accent": t.primary600,
|
|
39
|
+
"--bp-accent-component": t.success600,
|
|
40
|
+
"--bp-accent-single": t.warning600,
|
|
41
|
+
"--bp-title": t.neutral800,
|
|
42
|
+
"--bp-field": t.neutral800,
|
|
43
|
+
"--bp-type": t.neutral600,
|
|
44
|
+
"--bp-edge": t.neutral500 ?? t.neutral600,
|
|
45
|
+
"--bp-hover": t.primary100
|
|
46
|
+
};
|
|
47
|
+
}, [g]), p = c.useCallback(
|
|
48
|
+
(t) => {
|
|
49
|
+
if (!u.current) return;
|
|
50
|
+
t.preventDefault();
|
|
51
|
+
const e = u.current.getBoundingClientRect(), d = t.clientX - e.left, m = t.clientY - e.top, x = Math.exp(-t.deltaY / 400), T = Math.min(z, Math.max(W, r.scale * x)), E = T / r.scale;
|
|
52
|
+
a({
|
|
53
|
+
scale: T,
|
|
54
|
+
x: d - (d - r.x) * E,
|
|
55
|
+
y: m - (m - r.y) * E
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
[r, a]
|
|
59
|
+
);
|
|
60
|
+
c.useEffect(() => {
|
|
61
|
+
const t = u.current;
|
|
62
|
+
if (t)
|
|
63
|
+
return t.addEventListener("wheel", p, { passive: !1 }), () => t.removeEventListener("wheel", p);
|
|
64
|
+
}, [p]);
|
|
65
|
+
const R = `${i.width}x${i.height}x${w}`;
|
|
66
|
+
c.useEffect(() => {
|
|
67
|
+
const t = u.current;
|
|
68
|
+
if (!t) return;
|
|
69
|
+
const e = t.getBoundingClientRect();
|
|
70
|
+
a(G(i, { width: e.width, height: e.height }));
|
|
71
|
+
}, [R]);
|
|
72
|
+
const I = (t) => {
|
|
73
|
+
t.button === 0 && (l.current = { x: r.x, y: r.y, startX: t.clientX, startY: t.clientY }, t.target.setPointerCapture?.(t.pointerId));
|
|
74
|
+
}, k = (t) => {
|
|
75
|
+
const e = l.current;
|
|
76
|
+
e && a({
|
|
77
|
+
scale: r.scale,
|
|
78
|
+
x: e.x + (t.clientX - e.startX),
|
|
79
|
+
y: e.y + (t.clientY - e.startY)
|
|
80
|
+
});
|
|
81
|
+
}, h = (t) => {
|
|
82
|
+
const e = l.current;
|
|
83
|
+
if (l.current = null, !e || Math.abs(t.clientX - e.startX) > 3 || Math.abs(t.clientY - e.startY) > 3) return;
|
|
84
|
+
const m = t.target.closest?.("[data-uid]"), x = m?.getAttribute("data-uid");
|
|
85
|
+
x && m?.getAttribute("data-navigable") === "true" && s(x);
|
|
86
|
+
};
|
|
87
|
+
return /* @__PURE__ */ o(
|
|
88
|
+
L,
|
|
89
|
+
{
|
|
90
|
+
ref: u,
|
|
91
|
+
background: "neutral100",
|
|
92
|
+
hasRadius: !0,
|
|
93
|
+
overflow: "hidden",
|
|
94
|
+
position: "relative",
|
|
95
|
+
style: {
|
|
96
|
+
height: "100%",
|
|
97
|
+
cursor: l.current ? "grabbing" : "grab",
|
|
98
|
+
touchAction: "none"
|
|
99
|
+
},
|
|
100
|
+
onPointerDown: I,
|
|
101
|
+
onPointerMove: k,
|
|
102
|
+
onPointerUp: h,
|
|
103
|
+
onPointerLeave: () => {
|
|
104
|
+
l.current = null;
|
|
105
|
+
},
|
|
106
|
+
children: /* @__PURE__ */ o(
|
|
107
|
+
"div",
|
|
108
|
+
{
|
|
109
|
+
style: {
|
|
110
|
+
...y,
|
|
111
|
+
transform: `translate(${r.x}px, ${r.y}px) scale(${r.scale})`,
|
|
112
|
+
transformOrigin: "0 0",
|
|
113
|
+
width: "max-content"
|
|
114
|
+
},
|
|
115
|
+
dangerouslySetInnerHTML: { __html: n }
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
}, J = P, b = {
|
|
121
|
+
foreign: !1,
|
|
122
|
+
components: !0,
|
|
123
|
+
defaultFields: !1,
|
|
124
|
+
direction: "TB",
|
|
125
|
+
exclude: []
|
|
126
|
+
}, K = {
|
|
127
|
+
diagram: `/${U}/diagram`
|
|
128
|
+
}, V = (n) => ({
|
|
129
|
+
foreign: String(n.foreign),
|
|
130
|
+
components: String(n.components),
|
|
131
|
+
defaultFields: String(n.defaultFields),
|
|
132
|
+
direction: n.direction,
|
|
133
|
+
...n.exclude.length > 0 ? { exclude: n.exclude.join(",") } : {}
|
|
134
|
+
}), A = "blueprint:options", q = () => {
|
|
135
|
+
try {
|
|
136
|
+
const n = window.localStorage.getItem(A);
|
|
137
|
+
return n ? { ...b, ...JSON.parse(n) } : b;
|
|
138
|
+
} catch {
|
|
139
|
+
return b;
|
|
140
|
+
}
|
|
141
|
+
}, M = (n) => {
|
|
142
|
+
try {
|
|
143
|
+
window.localStorage.setItem(A, JSON.stringify(n));
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
}, Q = (n, s = "blueprint.svg") => {
|
|
147
|
+
const r = new Blob([n], { type: "image/svg+xml;charset=utf-8" }), a = URL.createObjectURL(r), i = document.createElement("a");
|
|
148
|
+
i.href = a, i.download = s, document.body.appendChild(i), i.click(), i.remove(), URL.revokeObjectURL(a);
|
|
149
|
+
}, Z = (n) => {
|
|
150
|
+
if (n.includes("::")) return `/plugins/content-type-builder/content-types/${n}`;
|
|
151
|
+
const [s] = n.split(".");
|
|
152
|
+
return `/plugins/content-type-builder/component-categories/${s}/${n}`;
|
|
153
|
+
}, ct = () => {
|
|
154
|
+
const { get: n } = Y(), s = N(), [r, a] = c.useState(q), [i, w] = c.useState(null), [u, l] = c.useState(!0), [g, y] = c.useState(null), [p, R] = c.useState(J), [I, k] = c.useState(0);
|
|
155
|
+
c.useEffect(() => {
|
|
156
|
+
let e = !0;
|
|
157
|
+
return l(!0), y(null), n(K.diagram, { params: V(r) }).then(({ data: d }) => {
|
|
158
|
+
e && w(d);
|
|
159
|
+
}).catch(() => e && y("Could not build the diagram.")).finally(() => e && l(!1)), () => {
|
|
160
|
+
e = !1;
|
|
161
|
+
};
|
|
162
|
+
}, [n, r]);
|
|
163
|
+
const h = (e) => {
|
|
164
|
+
const d = { ...r, ...e };
|
|
165
|
+
a(d), M(d);
|
|
166
|
+
}, t = i?.counts;
|
|
167
|
+
return /* @__PURE__ */ f(D.Main, { children: [
|
|
168
|
+
/* @__PURE__ */ o(
|
|
169
|
+
O.Header,
|
|
170
|
+
{
|
|
171
|
+
title: "Blueprint",
|
|
172
|
+
subtitle: t ? `${t.nodes} types, ${t.components} components, ${t.edges} relations` : "Your content types and how they fit together",
|
|
173
|
+
primaryAction: /* @__PURE__ */ o(
|
|
174
|
+
v,
|
|
175
|
+
{
|
|
176
|
+
startIcon: /* @__PURE__ */ o(j, {}),
|
|
177
|
+
variant: "secondary",
|
|
178
|
+
disabled: !i,
|
|
179
|
+
onClick: () => i && Q(i.svg),
|
|
180
|
+
children: "Download SVG"
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
),
|
|
185
|
+
/* @__PURE__ */ f(O.Content, { children: [
|
|
186
|
+
/* @__PURE__ */ f(C, { gap: 4, wrap: "wrap", paddingBottom: 4, alignItems: "center", children: [
|
|
187
|
+
/* @__PURE__ */ o(
|
|
188
|
+
B,
|
|
189
|
+
{
|
|
190
|
+
checked: r.components,
|
|
191
|
+
onCheckedChange: (e) => h({ components: !!e }),
|
|
192
|
+
children: "Components & dynamic zones"
|
|
193
|
+
}
|
|
194
|
+
),
|
|
195
|
+
/* @__PURE__ */ o(
|
|
196
|
+
B,
|
|
197
|
+
{
|
|
198
|
+
checked: r.defaultFields,
|
|
199
|
+
onCheckedChange: (e) => h({ defaultFields: !!e }),
|
|
200
|
+
children: "Strapi's own fields"
|
|
201
|
+
}
|
|
202
|
+
),
|
|
203
|
+
/* @__PURE__ */ o(
|
|
204
|
+
B,
|
|
205
|
+
{
|
|
206
|
+
checked: r.foreign,
|
|
207
|
+
onCheckedChange: (e) => h({ foreign: !!e }),
|
|
208
|
+
children: "Plugin content types"
|
|
209
|
+
}
|
|
210
|
+
),
|
|
211
|
+
/* @__PURE__ */ o(L, { minWidth: "180px", children: /* @__PURE__ */ f(
|
|
212
|
+
F,
|
|
213
|
+
{
|
|
214
|
+
"aria-label": "Layout direction",
|
|
215
|
+
value: r.direction,
|
|
216
|
+
onChange: (e) => h({ direction: e === "LR" ? "LR" : "TB" }),
|
|
217
|
+
children: [
|
|
218
|
+
/* @__PURE__ */ o($, { value: "TB", children: "Top to bottom" }),
|
|
219
|
+
/* @__PURE__ */ o($, { value: "LR", children: "Left to right" })
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
) }),
|
|
223
|
+
/* @__PURE__ */ o(v, { variant: "tertiary", onClick: () => k((e) => e + 1), children: "Fit to screen" }),
|
|
224
|
+
/* @__PURE__ */ o(
|
|
225
|
+
v,
|
|
226
|
+
{
|
|
227
|
+
variant: "tertiary",
|
|
228
|
+
onClick: () => {
|
|
229
|
+
a(b), M(b);
|
|
230
|
+
},
|
|
231
|
+
children: "Reset options"
|
|
232
|
+
}
|
|
233
|
+
)
|
|
234
|
+
] }),
|
|
235
|
+
/* @__PURE__ */ o(
|
|
236
|
+
L,
|
|
237
|
+
{
|
|
238
|
+
hasRadius: !0,
|
|
239
|
+
borderColor: "neutral200",
|
|
240
|
+
borderWidth: "1px",
|
|
241
|
+
borderStyle: "solid",
|
|
242
|
+
style: { height: "70vh" },
|
|
243
|
+
children: u ? /* @__PURE__ */ o(C, { justifyContent: "center", alignItems: "center", height: "100%", children: /* @__PURE__ */ o(X, { small: !0, children: "Building the diagram" }) }) : g ? /* @__PURE__ */ o(C, { justifyContent: "center", alignItems: "center", height: "100%", children: /* @__PURE__ */ o(S, { textColor: "danger600", children: g }) }) : i && i.counts.nodes > 0 ? /* @__PURE__ */ o(
|
|
244
|
+
H,
|
|
245
|
+
{
|
|
246
|
+
svg: i.svg,
|
|
247
|
+
view: p,
|
|
248
|
+
onViewChange: R,
|
|
249
|
+
fitTo: { width: i.width, height: i.height },
|
|
250
|
+
refit: I,
|
|
251
|
+
onOpen: (e) => s(Z(e))
|
|
252
|
+
}
|
|
253
|
+
) : /* @__PURE__ */ f(
|
|
254
|
+
C,
|
|
255
|
+
{
|
|
256
|
+
justifyContent: "center",
|
|
257
|
+
alignItems: "center",
|
|
258
|
+
height: "100%",
|
|
259
|
+
direction: "column",
|
|
260
|
+
gap: 2,
|
|
261
|
+
children: [
|
|
262
|
+
/* @__PURE__ */ o(S, { variant: "delta", children: "Nothing to draw yet" }),
|
|
263
|
+
/* @__PURE__ */ o(S, { textColor: "neutral600", children: "Create a content type, or switch on plugin content types above." })
|
|
264
|
+
]
|
|
265
|
+
}
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
),
|
|
269
|
+
/* @__PURE__ */ o(L, { paddingTop: 2, children: /* @__PURE__ */ o(S, { variant: "pi", textColor: "neutral600", children: "Drag to pan, scroll to zoom, click a box to open it in the Content-Type Builder. Solid lines are relations, dashed are components, dotted are dynamic zones." }) })
|
|
270
|
+
] })
|
|
271
|
+
] });
|
|
272
|
+
};
|
|
273
|
+
export {
|
|
274
|
+
ct as DiagramPage
|
|
275
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const o=require("react/jsx-runtime"),v=require("react"),j=require("@strapi/admin/strapi-admin"),s=require("@strapi/design-system"),I=require("@strapi/icons"),P=require("react-router-dom"),M=require("styled-components"),D=require("../admin/index.js");function E(t){if(t&&t.__esModule)return t;const a=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t){for(const r in t)if(r!=="default"){const c=Object.getOwnPropertyDescriptor(t,r);Object.defineProperty(a,r,c.get?c:{enumerable:!0,get:()=>t[r]})}}return a.default=t,Object.freeze(a)}const l=E(v),_=.15,$=3,k={x:0,y:0,scale:1},A=(t,a,r=24)=>{if(t.width<=0||t.height<=0)return k;const c=Math.min(1,(a.width-r*2)/t.width,(a.height-r*2)/t.height);return{scale:c,x:(a.width-t.width*c)/2,y:(a.height-t.height*c)/2}},F=({svg:t,onOpen:a,view:r,onViewChange:c,fitTo:i,refit:S})=>{const h=l.useRef(null),d=l.useRef(null),p=M.useTheme(),y=l.useMemo(()=>{const e=p?.colors??{};return{"--bp-surface":e.neutral0,"--bp-box":e.neutral0,"--bp-box-component":e.neutral100,"--bp-border":e.neutral200,"--bp-accent":e.primary600,"--bp-accent-component":e.success600,"--bp-accent-single":e.warning600,"--bp-title":e.neutral800,"--bp-field":e.neutral800,"--bp-type":e.neutral600,"--bp-edge":e.neutral500??e.neutral600,"--bp-hover":e.primary100}},[p]),x=l.useCallback(e=>{if(!h.current)return;e.preventDefault();const n=h.current.getBoundingClientRect(),u=e.clientX-n.left,m=e.clientY-n.top,b=Math.exp(-e.deltaY/400),R=Math.min($,Math.max(_,r.scale*b)),B=R/r.scale;c({scale:R,x:u-(u-r.x)*B,y:m-(m-r.y)*B})},[r,c]);l.useEffect(()=>{const e=h.current;if(e)return e.addEventListener("wheel",x,{passive:!1}),()=>e.removeEventListener("wheel",x)},[x]);const C=`${i.width}x${i.height}x${S}`;l.useEffect(()=>{const e=h.current;if(!e)return;const n=e.getBoundingClientRect();c(A(i,{width:n.width,height:n.height}))},[C]);const L=e=>{e.button===0&&(d.current={x:r.x,y:r.y,startX:e.clientX,startY:e.clientY},e.target.setPointerCapture?.(e.pointerId))},w=e=>{const n=d.current;n&&c({scale:r.scale,x:n.x+(e.clientX-n.startX),y:n.y+(e.clientY-n.startY)})},g=e=>{const n=d.current;if(d.current=null,!n||Math.abs(e.clientX-n.startX)>3||Math.abs(e.clientY-n.startY)>3)return;const m=e.target.closest?.("[data-uid]"),b=m?.getAttribute("data-uid");b&&m?.getAttribute("data-navigable")==="true"&&a(b)};return o.jsx(s.Box,{ref:h,background:"neutral100",hasRadius:!0,overflow:"hidden",position:"relative",style:{height:"100%",cursor:d.current?"grabbing":"grab",touchAction:"none"},onPointerDown:L,onPointerMove:w,onPointerUp:g,onPointerLeave:()=>{d.current=null},children:o.jsx("div",{style:{...y,transform:`translate(${r.x}px, ${r.y}px) scale(${r.scale})`,transformOrigin:"0 0",width:"max-content"},dangerouslySetInnerHTML:{__html:t}})})},Y=k,f={foreign:!1,components:!0,defaultFields:!1,direction:"TB",exclude:[]},N={diagram:`/${D.PLUGIN_ID}/diagram`},X=t=>({foreign:String(t.foreign),components:String(t.components),defaultFields:String(t.defaultFields),direction:t.direction,...t.exclude.length>0?{exclude:t.exclude.join(",")}:{}}),T="blueprint:options",q=()=>{try{const t=window.localStorage.getItem(T);return t?{...f,...JSON.parse(t)}:f}catch{return f}},O=t=>{try{window.localStorage.setItem(T,JSON.stringify(t))}catch{}},U=(t,a="blueprint.svg")=>{const r=new Blob([t],{type:"image/svg+xml;charset=utf-8"}),c=URL.createObjectURL(r),i=document.createElement("a");i.href=c,i.download=a,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(c)},z=t=>{if(t.includes("::"))return`/plugins/content-type-builder/content-types/${t}`;const[a]=t.split(".");return`/plugins/content-type-builder/component-categories/${a}/${t}`},W=()=>{const{get:t}=j.useFetchClient(),a=P.useNavigate(),[r,c]=l.useState(q),[i,S]=l.useState(null),[h,d]=l.useState(!0),[p,y]=l.useState(null),[x,C]=l.useState(Y),[L,w]=l.useState(0);l.useEffect(()=>{let n=!0;return d(!0),y(null),t(N.diagram,{params:X(r)}).then(({data:u})=>{n&&S(u)}).catch(()=>n&&y("Could not build the diagram.")).finally(()=>n&&d(!1)),()=>{n=!1}},[t,r]);const g=n=>{const u={...r,...n};c(u),O(u)},e=i?.counts;return o.jsxs(j.Page.Main,{children:[o.jsx(j.Layouts.Header,{title:"Blueprint",subtitle:e?`${e.nodes} types, ${e.components} components, ${e.edges} relations`:"Your content types and how they fit together",primaryAction:o.jsx(s.Button,{startIcon:o.jsx(I.Download,{}),variant:"secondary",disabled:!i,onClick:()=>i&&U(i.svg),children:"Download SVG"})}),o.jsxs(j.Layouts.Content,{children:[o.jsxs(s.Flex,{gap:4,wrap:"wrap",paddingBottom:4,alignItems:"center",children:[o.jsx(s.Checkbox,{checked:r.components,onCheckedChange:n=>g({components:!!n}),children:"Components & dynamic zones"}),o.jsx(s.Checkbox,{checked:r.defaultFields,onCheckedChange:n=>g({defaultFields:!!n}),children:"Strapi's own fields"}),o.jsx(s.Checkbox,{checked:r.foreign,onCheckedChange:n=>g({foreign:!!n}),children:"Plugin content types"}),o.jsx(s.Box,{minWidth:"180px",children:o.jsxs(s.SingleSelect,{"aria-label":"Layout direction",value:r.direction,onChange:n=>g({direction:n==="LR"?"LR":"TB"}),children:[o.jsx(s.SingleSelectOption,{value:"TB",children:"Top to bottom"}),o.jsx(s.SingleSelectOption,{value:"LR",children:"Left to right"})]})}),o.jsx(s.Button,{variant:"tertiary",onClick:()=>w(n=>n+1),children:"Fit to screen"}),o.jsx(s.Button,{variant:"tertiary",onClick:()=>{c(f),O(f)},children:"Reset options"})]}),o.jsx(s.Box,{hasRadius:!0,borderColor:"neutral200",borderWidth:"1px",borderStyle:"solid",style:{height:"70vh"},children:h?o.jsx(s.Flex,{justifyContent:"center",alignItems:"center",height:"100%",children:o.jsx(s.Loader,{small:!0,children:"Building the diagram"})}):p?o.jsx(s.Flex,{justifyContent:"center",alignItems:"center",height:"100%",children:o.jsx(s.Typography,{textColor:"danger600",children:p})}):i&&i.counts.nodes>0?o.jsx(F,{svg:i.svg,view:x,onViewChange:C,fitTo:{width:i.width,height:i.height},refit:L,onOpen:n=>a(z(n))}):o.jsxs(s.Flex,{justifyContent:"center",alignItems:"center",height:"100%",direction:"column",gap:2,children:[o.jsx(s.Typography,{variant:"delta",children:"Nothing to draw yet"}),o.jsx(s.Typography,{textColor:"neutral600",children:"Create a content type, or switch on plugin content types above."})]})}),o.jsx(s.Box,{paddingTop:2,children:o.jsx(s.Typography,{variant:"pi",textColor:"neutral600",children:"Drag to pan, scroll to zoom, click a box to open it in the Content-Type Builder. Solid lines are relations, dashed are components, dotted are dynamic zones."})})]})]})};exports.DiagramPage=W;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const a=require("@strapi/icons"),o=(n,e,i)=>{const r=n[e];return r?typeof r=="function"?r():Promise.resolve(r):new Promise((l,s)=>{(typeof queueMicrotask=="function"?queueMicrotask:setTimeout)(s.bind(null,new Error("Unknown variable dynamic import: "+e+(e.split("/").length!==i?". Note that variables only represent file names one level deep.":""))))})},t="blueprint",u={register(n){n.registerPlugin({id:t,name:t,isReady:!0}),n.addMenuLink({to:`/plugins/${t}`,icon:a.Graph,intlLabel:{id:`${t}.menu.label`,defaultMessage:"Blueprint"},Component:async()=>{const{DiagramPage:e}=await Promise.resolve().then(()=>require("../_chunks/DiagramPage-LORj1QlF.js"));return{default:e}},permissions:[{action:`plugin::${t}.read`,subject:null}]})},async registerTrads({locales:n}){return Promise.all(n.map(async e=>{try{const{default:i}=await o(Object.assign({}),`./translations/${e}.json`,3);return{data:i,locale:e}}catch{return{data:{},locale:e}}}))}};exports.PLUGIN_ID=t;exports.default=u;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Graph as s } from "@strapi/icons";
|
|
2
|
+
const o = (n, e, i) => {
|
|
3
|
+
const r = n[e];
|
|
4
|
+
return r ? typeof r == "function" ? r() : Promise.resolve(r) : new Promise((u, a) => {
|
|
5
|
+
(typeof queueMicrotask == "function" ? queueMicrotask : setTimeout)(
|
|
6
|
+
a.bind(
|
|
7
|
+
null,
|
|
8
|
+
new Error(
|
|
9
|
+
"Unknown variable dynamic import: " + e + (e.split("/").length !== i ? ". Note that variables only represent file names one level deep." : "")
|
|
10
|
+
)
|
|
11
|
+
)
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
}, t = "blueprint", c = {
|
|
15
|
+
register(n) {
|
|
16
|
+
n.registerPlugin({ id: t, name: t, isReady: !0 }), n.addMenuLink({
|
|
17
|
+
to: `/plugins/${t}`,
|
|
18
|
+
icon: s,
|
|
19
|
+
intlLabel: { id: `${t}.menu.label`, defaultMessage: "Blueprint" },
|
|
20
|
+
Component: async () => {
|
|
21
|
+
const { DiagramPage: e } = await import("../_chunks/DiagramPage-F0OgJkQF.mjs");
|
|
22
|
+
return { default: e };
|
|
23
|
+
},
|
|
24
|
+
permissions: [{ action: `plugin::${t}.read`, subject: null }]
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
async registerTrads({ locales: n }) {
|
|
28
|
+
return Promise.all(
|
|
29
|
+
n.map(async (e) => {
|
|
30
|
+
try {
|
|
31
|
+
const { default: i } = await o(/* @__PURE__ */ Object.assign({}), `./translations/${e}.json`, 3);
|
|
32
|
+
return { data: i, locale: e };
|
|
33
|
+
} catch {
|
|
34
|
+
return { data: {}, locale: e };
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
export {
|
|
41
|
+
t as PLUGIN_ID,
|
|
42
|
+
c as default
|
|
43
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface DiagramOptions {
|
|
2
|
+
/** Strapi's and other plugins' content types. */
|
|
3
|
+
foreign: boolean;
|
|
4
|
+
/** Components and dynamic zones. */
|
|
5
|
+
components: boolean;
|
|
6
|
+
/** `createdAt` and the rest of Strapi's bookkeeping. */
|
|
7
|
+
defaultFields: boolean;
|
|
8
|
+
/** Top-to-bottom, or left-to-right. */
|
|
9
|
+
direction: 'TB' | 'LR';
|
|
10
|
+
exclude: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare const DEFAULT_OPTIONS: DiagramOptions;
|
|
13
|
+
export interface DiagramResponse {
|
|
14
|
+
svg: string;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
counts: {
|
|
18
|
+
nodes: number;
|
|
19
|
+
edges: number;
|
|
20
|
+
components: number;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export interface CatalogueEntry {
|
|
24
|
+
uid: string;
|
|
25
|
+
label: string;
|
|
26
|
+
kind: string;
|
|
27
|
+
}
|
|
28
|
+
export declare const routes: {
|
|
29
|
+
diagram: string;
|
|
30
|
+
contentTypes: string;
|
|
31
|
+
};
|
|
32
|
+
/** The options as the server's query string expects them. */
|
|
33
|
+
export declare const toParams: (options: DiagramOptions) => Record<string, string>;
|
|
34
|
+
/**
|
|
35
|
+
* The toolbar state, kept across visits.
|
|
36
|
+
*
|
|
37
|
+
* "Persist arrangement" is one of the open requests on both of the abandoned
|
|
38
|
+
* plugins this replaces. Re-picking the same four toggles on every visit is the
|
|
39
|
+
* kind of small friction that stops people using a tool they otherwise like.
|
|
40
|
+
*/
|
|
41
|
+
export declare const loadOptions: () => DiagramOptions;
|
|
42
|
+
export declare const saveOptions: (options: DiagramOptions) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Hands the browser the SVG already on screen.
|
|
45
|
+
*
|
|
46
|
+
* Deliberately not a link to the server's download route: an admin route needs
|
|
47
|
+
* the session's Authorization header, which a plain navigation cannot send. The
|
|
48
|
+
* string is already here, and it is the same string the server would return.
|
|
49
|
+
*/
|
|
50
|
+
export declare const downloadSvg: (svg: string, filename?: string) => void;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
interface View {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
scale: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The view that shows the whole diagram, centred.
|
|
9
|
+
*
|
|
10
|
+
* A schema of any size is bigger than the panel, so opening at 1:1 in the
|
|
11
|
+
* top-left corner shows a corner of one box and reads as broken. Never zooms
|
|
12
|
+
* *in* past 1:1 — a two-box schema blown up to fill the screen looks like a
|
|
13
|
+
* mistake too.
|
|
14
|
+
*/
|
|
15
|
+
export declare const fitView: (content: {
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
}, viewport: {
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
}, padding?: number) => View;
|
|
22
|
+
/**
|
|
23
|
+
* Pan and zoom over a diagram the server drew.
|
|
24
|
+
*
|
|
25
|
+
* The whole of the interactivity, in one file and with no graph library. That
|
|
26
|
+
* is the point: every plugin this replaces pulled in a React canvas library to
|
|
27
|
+
* get dragging, and then broke when React, styled-components or Strapi moved
|
|
28
|
+
* under it. Panning is a translate, zooming is a scale about the pointer, and
|
|
29
|
+
* clicking a box is one delegated listener reading the `data-uid` the renderer
|
|
30
|
+
* already puts on every box.
|
|
31
|
+
*/
|
|
32
|
+
export declare const Canvas: ({ svg, onOpen, view, onViewChange, fitTo, refit, }: {
|
|
33
|
+
svg: string;
|
|
34
|
+
onOpen: (uid: string) => void;
|
|
35
|
+
view: View;
|
|
36
|
+
onViewChange: (view: View) => void;
|
|
37
|
+
/** Diagram extent. When it changes, the view is fitted to it. */
|
|
38
|
+
fitTo: {
|
|
39
|
+
width: number;
|
|
40
|
+
height: number;
|
|
41
|
+
};
|
|
42
|
+
/** Bump to refit on demand — what the "Reset view" button changes. */
|
|
43
|
+
refit: number;
|
|
44
|
+
}) => React.JSX.Element;
|
|
45
|
+
export declare const INITIAL_VIEW: View;
|
|
46
|
+
export type { View };
|