Merge pull request #11 from ShahriarKh/dev

Bunch of new things
main
Shahriar 2024-01-20 01:22:56 +03:30 committed by GitHub
commit 5deaa35ef3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 4672 additions and 5560 deletions

View File

@ -1,11 +0,0 @@
import { request } from "@strapi/helper-plugin";
const explorerRequests = {
getContentTypes: async () => {
return await request("/content-type-explorer/get-content-types", {
method: "GET",
});
},
};
export default explorerRequests;

View File

@ -5,14 +5,20 @@
*/
import React from "react";
import { Badge, Box, Divider, Tooltip, Typography } from "@strapi/design-system";
import {
Badge,
Box,
Divider,
Tooltip,
Typography,
} from "@strapi/design-system";
import { useTheme } from "styled-components";
import { Handle } from "reactflow";
import { RelationIcon } from "./RelationIcon";
import { getIcon } from "../utils/themeUtils";
import "./CustomNode.css";
export default function CustomNode({ data }) {
export function CustomNode({ data }) {
let attributesToShow = Object.entries(data.attributes);
if (data.options.showRelationsOnly) {
@ -34,13 +40,28 @@ export default function CustomNode({ data }) {
const theme = useTheme();
return (
<Box background="neutral0" shadow="tableShadow" hasRadius padding="16px 24px" className="cte-plugin-box">
<Typography fontWeight="bold" textColor="buttonPrimary500" padding="16px" className="cte-plugin-header nodrag">
<Box
background="neutral0"
shadow="tableShadow"
hasRadius
padding="16px 24px"
className="cte-plugin-box"
>
<Typography
fontWeight="bold"
textColor="buttonPrimary500"
padding="16px"
className="cte-plugin-header nodrag"
>
{data.info.displayName}
</Typography>
<br />
<Typography textColor="neutral400" padding="16px" className="cte-plugin-header nodrag">
<Typography
textColor="neutral400"
padding="16px"
className="cte-plugin-header nodrag"
>
{data.key}
<Handle
type="target"
@ -61,7 +82,11 @@ export default function CustomNode({ data }) {
<p className="cte-plugin-line nodrag">{attr[0]}</p>
{data.options.showTypes && (
<Badge size="M" backgroundColor="neutral0" textColor="neutral400">
<Badge
size="M"
backgroundColor="neutral0"
textColor="neutral400"
>
{attr[1].type}
</Badge>
)}
@ -70,9 +95,16 @@ export default function CustomNode({ data }) {
{attr[1].type === "relation" && (
<>
<Tooltip description={attr[1].relation}>
<RelationIcon theme={theme}>{getIcon(attr[1].relation)}</RelationIcon>
<RelationIcon theme={theme}>
{getIcon(attr[1].relation)}
</RelationIcon>
</Tooltip>
<Handle type="source" id={attr[0]} position="right" className="cte-plugin-handle" />
<Handle
type="source"
id={attr[0]}
position="right"
className="cte-plugin-handle"
/>
</>
)}
</div>

View File

@ -0,0 +1,129 @@
import {
ModalLayout,
ModalHeader,
ModalFooter,
ModalBody,
Button,
Typography,
SingleSelect,
SingleSelectOption,
NumberInput,
} from "@strapi/design-system";
import { toJpeg, toPng, toSvg } from "html-to-image";
import { useCallback, useState } from "react";
import { useDigramStore } from "../store";
import { useTheme } from "styled-components";
export function ExportModal({ imageRef }) {
const theme = useTheme();
const { setShowModal } = useDigramStore();
const [format, setFormat] = useState("png");
const [quality, setQuality] = useState(0.95);
function downloadImage(dataUrl, fileExtension) {
const link = document.createElement("a");
link.download = `strapi-diagram-${new Date()
.toISOString()
.replace(/[-:T.]/g, "")
.slice(0, -5)}.${fileExtension}`;
link.href = dataUrl;
link.click();
}
const exportDiagram = useCallback(() => {
if (imageRef.current === null) {
return;
}
const filter = (node) => {
const exclusionClasses = ["cte-plugin-controls"];
return !exclusionClasses.some((classname) =>
node.classList?.contains(classname)
);
};
if (format == "png") {
toPng(imageRef.current, {
cacheBust: true,
filter: filter,
style: {
background: theme.colors.neutral100,
},
})
.then((dataUrl) => {
downloadImage(dataUrl, "png");
})
.catch((err) => {
console.log(err);
});
} else if (format == "svg") {
toSvg(imageRef.current, {
cacheBust: true,
filter: filter,
style: {
background: theme.colors.neutral100,
},
})
.then((dataUrl) => {
downloadImage(dataUrl, "svg");
})
.catch((err) => {
console.log(err);
});
} else if (format == "jpeg") {
toJpeg(imageRef.current, {
cacheBust: true,
filter: filter,
quality: quality,
style: {
background: theme.colors.neutral100,
},
})
.then((dataUrl) => {
downloadImage(dataUrl, "jpeg");
})
.catch((err) => {
console.log(err);
});
}
}, [imageRef, quality, format]);
return (
<ModalLayout onClose={() => setShowModal(false)}>
<ModalHeader>
<Typography fontWeight="bold" textColor="neutral800" as="h2" id="title">
Export Diagram
</Typography>
</ModalHeader>
<ModalBody>
<SingleSelect
label="Format"
onClear={() => {
setFormat(undefined);
}}
value={format}
onChange={setFormat}
>
<SingleSelectOption value="svg">SVG</SingleSelectOption>
<SingleSelectOption value="png">PNG</SingleSelectOption>
<SingleSelectOption value="jpeg">JPEG</SingleSelectOption>
</SingleSelect>
<span style={{ height: "16px", display: "block" }} />
{format == "jpeg" && (
<NumberInput
label="Quality"
name="quality"
hint="0.0 - 1.0"
onValueChange={(value) => setQuality(value)}
value={quality}
/>
)}
</ModalBody>
<ModalFooter
endActions={<Button onClick={exportDiagram}>Export</Button>}
/>
</ModalLayout>
);
}

View File

@ -4,14 +4,16 @@ import {
SingleSelectOption,
} from "@strapi/design-system";
import React from "react";
import { useDigramStore } from "../store";
export default function OptionsBar({ options, toggleOption }) {
export function OptionsBar() {
const { options, toggleOption } = useDigramStore();
return (
<div
style={{
display: "flex",
justifyContent: "flex-start",
alignItems: "center",
alignItems: "flex-start",
padding: "0 56px 24px",
gap: "24px",
}}
@ -23,14 +25,14 @@ export default function OptionsBar({ options, toggleOption }) {
}}
value={options.showTypes}
>
Fields Data Types
Data Types
</Checkbox>
<Checkbox
name="show-icons"
onValueChange={() => toggleOption("showIcons")}
value={options.showIcons}
>
Fields Icons
Data Type Icons
</Checkbox>
<Checkbox
name="show-default-fields"

View File

@ -1,48 +1,49 @@
/*
*
* HomePage
*
*/
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { HeaderLayout, Icon, LinkButton } from "@strapi/design-system";
import explorerRequests from "../../api/explorer-api";
import { Question, Search, Drag } from "@strapi/icons";
import { useTheme } from "styled-components";
import { SmartBezierEdge, SmartStepEdge, SmartStraightEdge } from "@tisoap/react-flow-smart-edge";
import CustomNode from "../../components/CustomNode";
import { createEdegs, createNodes, updateEdges, updateNodes } from "../../utils/dataUtils";
import { Background, ControlButton, Controls, ReactFlow, useEdgesState, useNodesState } from "reactflow";
import { getBackgroundColor } from "../../utils/themeUtils";
import "reactflow/dist/style.css";
import OptionsBar from "../../components/OptionsBar";
import "./styles.css";
import React, { useEffect, useMemo, useRef } from "react";
import { useFetchClient } from "@strapi/helper-plugin";
import { HeaderLayout, Icon, Button } from "@strapi/design-system";
import { Search, Drag, Download, Refresh } from "@strapi/icons";
import { useTheme } from "styled-components";
import {
SmartBezierEdge,
SmartStepEdge,
SmartStraightEdge,
} from "@tisoap/react-flow-smart-edge";
import { Background, ControlButton, Controls, ReactFlow } from "reactflow";
import { getBackgroundColor } from "../../utils/themeUtils";
import { useDigramStore } from "../../store";
import { CustomNode } from "../../components/CustomNode";
import { OptionsBar } from "../../components/OptionsBar";
import { ExportModal } from "../../components/ExportModal";
const useEffectSkipInitial = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
if (didMount.current) func();
else didMount.current = true;
}, deps);
};
const HomePage = () => {
const theme = useTheme();
const [contentTypes, setContentTypes] = useState([]);
const [options, setOptions] = useState({
snapToGrid: false,
showTypes: true,
showIcons: true,
showRelationsOnly: false,
showAdminTypes: false,
showDefaultFields: false,
showPluginTypes: false,
showEdges: false,
scrollMode: true,
edgeType: "smartbezier",
backgroundPattern: "dots",
});
function toggleOption(optionName, optionValue = null) {
setOptions({
...options,
[optionName]: optionValue || !options[optionName],
});
}
const { get } = useFetchClient();
const {
nodes,
redrawEdges,
edges,
onNodesChange,
onEdgesChange,
onConnect,
redrawNodes,
drawDiagram,
toggleOption,
options,
setData,
showModal,
setShowModal,
} = useDigramStore();
const nodeTypes = useMemo(() => ({ special: CustomNode }), []);
const edgeTypes = useMemo(
@ -54,66 +55,59 @@ const HomePage = () => {
[]
);
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const regenrate = async () => {
const { data } = await get(`/strapi-content-type-explorer/get-types`);
setData(data);
drawDiagram();
};
const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
// get (and filter) content-types
useEffect(() => {
const fetchData = async () => {
let allTypes = await explorerRequests.getContentTypes();
if (!options.showAdminTypes) {
allTypes = allTypes.filter((x) => !x.name.startsWith("admin::"));
}
if (!options.showPluginTypes) {
allTypes = allTypes.filter((x) => !x.name.startsWith("plugin::"));
}
setContentTypes(allTypes);
};
fetchData();
useEffectSkipInitial(() => {
regenrate();
}, [options.showAdminTypes, options.showPluginTypes]);
// create nodes & edges
useEffect(() => {
if (contentTypes.length > 0) {
let newNodes = createNodes(contentTypes, options);
setNodes(newNodes);
let newEdges = createEdegs(contentTypes, options);
setEdges(newEdges);
}
}, [contentTypes]);
redrawEdges();
}, [options.edgeType, options.showEdges]);
useEffect(() => {
let newEdges = updateEdges(edges, options);
setEdges(newEdges);
}, [setEdges, options.edgeType, options.showEdges]);
redrawNodes();
}, [
options.showTypes,
options.showIcons,
options.showRelationsOnly,
options.showDefaultFields,
]);
useEffect(() => {
let newNodes = updateNodes(nodes, options);
setNodes(newNodes);
}, [setNodes, options.showTypes, options.showIcons, options.showRelationsOnly, options.showDefaultFields]);
const ref = useRef(null);
return (
<>
<div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
<HeaderLayout
title="Content-Type Explorer"
// primaryAction={<Button>Download as Image</Button>}
secondaryAction={
<LinkButton
variant="secondary"
startIcon={<Question />}
href="https://github.com/shahriarkh/strapi-content-type-explorer"
primaryAction={
<Button
variant="primary"
startIcon={<Download />}
onClick={() => setShowModal(true)}
>
Help
</LinkButton>
Export Diagram
</Button>
}
secondaryAction={
<Button
variant="secondary"
startIcon={<Refresh />}
onClick={regenrate}
>
Regenrate
</Button>
}
/>
<OptionsBar options={options} toggleOption={toggleOption} />
<OptionsBar />
<div
ref={ref}
style={{
height: "100vh",
height: "100%",
borderTop: `1px solid ${theme.colors.neutral150}`,
}}
>
@ -144,8 +138,14 @@ const HomePage = () => {
"--button-hover": theme.colors.buttonPrimary500,
}}
>
<ControlButton onClick={() => toggleOption("scrollMode")} title="Toggle Mouse Wheel Behavior (Zoom/Scroll)">
<Icon color="neutral1000" as={options.scrollMode ? Drag : Search} />
<ControlButton
onClick={() => toggleOption("scrollMode")}
title="Toggle Mouse Wheel Behavior (Zoom/Scroll)"
>
<Icon
color="neutral1000"
as={options.scrollMode ? Drag : Search}
/>
</ControlButton>
</Controls>
<Background
@ -153,8 +153,9 @@ const HomePage = () => {
color={getBackgroundColor(options.backgroundPattern, theme)}
/>
</ReactFlow>
{showModal && <ExportModal imageRef={ref} />}
</div>
</>
</div>
);
};

102
admin/src/store/index.js Normal file
View File

@ -0,0 +1,102 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { addEdge, applyNodeChanges, applyEdgeChanges } from "reactflow";
import {
createEdegs,
createNodes,
updateEdges,
updateNodes,
} from "../utils/dataUtils";
export const useDigramStore = create(
persist(
(set, get) => ({
nodes: [],
edges: [],
data: [],
showModal: false,
options: {
snapToGrid: false,
showTypes: true,
showIcons: true,
showRelationsOnly: false,
showAdminTypes: false,
showDefaultFields: false,
showPluginTypes: false,
showEdges: false,
scrollMode: true,
edgeType: "smartbezier",
backgroundPattern: "dots",
},
setData: (contentTypesData) => {
set({
data: contentTypesData,
});
},
setShowModal: (bool) => {
set({
showModal: bool,
});
},
toggleOption: (optionName, optionValue = null) => {
let newOptions = {
...get().options,
[optionName]: optionValue || !get().options[optionName],
};
set({
options: newOptions,
});
},
onNodesChange: (changes) => {
set({
nodes: applyNodeChanges(changes, get().nodes),
});
},
onEdgesChange: (changes) => {
set({
edges: applyEdgeChanges(changes, get().edges),
});
},
onConnect: (connection) => {
set({
edges: addEdge(connection, get().edges),
});
},
drawDiagram: () => {
const options = get().options;
let typesToDraw = get().data;
if (!options.showAdminTypes) {
typesToDraw = typesToDraw.filter(
(x) => !x.name.startsWith("admin::")
);
}
if (!options.showPluginTypes) {
typesToDraw = typesToDraw.filter(
(x) => !x.name.startsWith("plugin::")
);
}
let newNodes = createNodes(typesToDraw, options);
let newEdges = createEdegs(typesToDraw, options);
set({
nodes: newNodes,
edges: newEdges,
});
},
redrawNodes: () => {
let newNodes = updateNodes(get().nodes, get().options);
set({
nodes: newNodes,
});
},
redrawEdges: () => {
let newEdges = updateEdges(get().edges, get().options);
set({
edges: newEdges,
});
},
}),
{
name: "strapi-content-type-explorer",
}
)
);

View File

@ -10,7 +10,9 @@ export function createNodes(contentTypes, options) {
id: node.key,
position: {
x: (index % CARDS_PER_ROW) * 320,
y: ((index - (index % CARDS_PER_ROW)) / CARDS_PER_ROW) * 560,
y:
((index - (index % CARDS_PER_ROW)) / CARDS_PER_ROW) * 560 +
(index % 2) * 48,
},
type: "special",
@ -38,7 +40,11 @@ export function createEdegs(contentTypes, options) {
Object.keys(contentType.attributes).map((attr) => {
if (contentType.attributes[attr].type == "relation") {
// only add edge if target node is not excluded (not hidden)
if (contentTypes.some((node) => node.key === contentType.attributes[attr].target)) {
if (
contentTypes.some(
(node) => node.key === contentType.attributes[attr].target
)
) {
newEdges = [
...newEdges,
{

View File

@ -11,6 +11,7 @@ import {
Media,
Boolean,
Json,
Blocks,
Relation,
Uid,
OneToMany,
@ -67,6 +68,8 @@ export function getIcon(attrType) {
return <RichText />;
case "media":
return <Media />;
case "blocks":
return <Blocks />;
case "onetomany": //
return <OneToMany />;

9672
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,12 +13,10 @@
"url": "https://github.com/shahriarkh/strapi-content-type-explorer.git"
},
"dependencies": {
"@strapi/design-system": "^1.6.3",
"@strapi/helper-plugin": "^4.6.0",
"@strapi/icons": "^1.6.3",
"prop-types": "^15.7.2",
"reactflow": "^11.7.4",
"@tisoap/react-flow-smart-edge": "^3.0.0"
"@tisoap/react-flow-smart-edge": "^3.0.0",
"html-to-image": "^1.11.11",
"reactflow": "^11.10.2",
"zustand": "^4.4.7"
},
"peerDependencies": {
"@strapi/strapi": "^4.0.0"

View File

@ -1,20 +1,11 @@
"use strict";
module.exports = ({ strapi }) => ({
getContentTypes(ctx) {
const data = strapi
.plugin("content-type-explorer")
.service("explorerService")
async getTypes(ctx) {
const contentTypes = await strapi
.service("plugin::strapi-content-type-explorer.explorerService")
.getContentTypes();
let neededData = Object.keys(data).map((key) => ({
name: key,
attributes: data[key]["attributes"],
info: data[key]["info"],
// kind: data[key]["kind"],
key: data[key]["uid"],
}));
ctx.body = neededData;
ctx.body = contentTypes;
},
});

View File

@ -1,15 +1,15 @@
'use strict';
"use strict";
const register = require('./register');
const bootstrap = require('./bootstrap');
const destroy = require('./destroy');
const config = require('./config');
const contentTypes = require('./content-types');
const controllers = require('./controllers');
const routes = require('./routes');
const middlewares = require('./middlewares');
const policies = require('./policies');
const services = require('./services');
const register = require("./register");
const bootstrap = require("./bootstrap");
const destroy = require("./destroy");
const config = require("./config");
const contentTypes = require("./content-types");
const controllers = require("./controllers");
const routes = require("./routes");
const middlewares = require("./middlewares");
const policies = require("./policies");
const services = require("./services");
module.exports = {
register,

View File

@ -1,10 +1,11 @@
module.exports = [
{
method: "GET",
path: "/get-content-types",
handler: "explorerController.getContentTypes",
path: "/get-types",
handler: "explorerController.getTypes",
config: {
policies: [],
auth: false,
},
},
];

View File

@ -1,7 +1,17 @@
"use strict";
module.exports = ({ strapi }) => ({
getContentTypes() {
return strapi.contentTypes;
async getContentTypes() {
const types = strapi.contentTypes;
let formattedTypes = Object.keys(types).map((key) => ({
name: key,
attributes: types[key]["attributes"],
info: types[key]["info"],
// kind: data[key]["kind"],
key: types[key]["uid"],
}));
return formattedTypes;
},
});

View File

@ -1,3 +1,3 @@
'use strict';
"use strict";
module.exports = require('./admin/src').default;
module.exports = require("./admin/src").default;