Add files via upload

pull/5/head
Shahriar 2023-07-14 02:44:38 +03:30 committed by GitHub
parent 672757694c
commit e19bd0a7ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 917 additions and 0 deletions

View File

@ -0,0 +1,3 @@
# Strapi plugin content-type-explorer
A quick description of content-type-explorer.

View File

@ -0,0 +1,11 @@
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

@ -0,0 +1,35 @@
.cte-plugin-box {
position: relative;
min-width: 280px;
}
.cte-plugin-header {
user-select: text;
cursor: auto;
}
.cte-plugin-line {
margin-right: auto;
padding-right: 12px; /*minimum gap*/
margin-top: -2px;
user-select: text;
cursor: auto;
}
.cte-plugin-handle {
position: absolute;
right: -27px;
top: 6px;
bottom: 0;
margin: auto;
visibility: hidden;
position: absolute;
padding: 0;
}
.cte-plugin-field {
display: flex;
align-items: center;
margin-bottom: 6px;
position: relative;
}

View File

@ -0,0 +1,116 @@
/**
*
* PluginIcon
*
*/
import React from "react";
import {
Badge,
Box,
Divider,
Tooltip,
Typography,
} from "@strapi/design-system";
import { useTheme } from "styled-components";
import { Handle } from "reactflow";
import { RelationIndicator } from "./RelationIndicator";
import { getIcon } from "../utils/themeUtils";
import "./CustomNode.css";
export default function CustomNode({ data }) {
let attributesToShow = Object.entries(data.attributes);
if (data.options.showRelationsOnly) {
attributesToShow = attributesToShow.filter((x) => x[1].type === "relation");
}
if (!data.options.showDefaultFields) {
attributesToShow = attributesToShow.filter(
(x) =>
!(
x[0] === "updatedAt" ||
x[0] === "createdAt" ||
x[0] === "updatedBy" ||
x[0] === "createdBy" ||
x[0] === "publishedAt"
)
);
}
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"
>
{data.info.displayName}
</Typography>
<br />
<Typography
textColor="neutral400"
padding="16px"
className="cte-plugin-header nodrag"
>
{data.key}
<Handle
type="target"
position="top"
style={{
borderColor: theme.colors.neutral200,
background: theme.colors.neutral0,
}}
/>
</Typography>
<Divider style={{ margin: "8px 0" }} />
{attributesToShow.map((attr) => {
return (
<Typography key={attr[0]}>
<div className="cte-plugin-field">
<p className="cte-plugin-line nodrag">{attr[0]}</p>
{data.options.showTypes && (
<Badge
size="M"
backgroundColor="neutral0"
textColor="neutral400"
>
{attr[1].type}
</Badge>
)}
{data.options.showIcons && getIcon(attr[1].type)}
{attr[1].type === "relation" && (
<>
<Tooltip description={attr[1].relation}>
<RelationIndicator theme={theme}>
{getIcon(attr[1].relation)}
</RelationIndicator>
</Tooltip>
<Handle
type="source"
id={attr[0]}
position="right"
className="cte-plugin-handle"
/>
</>
)}
</div>
</Typography>
);
})}
</Box>
);
}

View File

@ -0,0 +1,26 @@
/**
*
* Initializer
*
*/
import { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import pluginId from '../../pluginId';
const Initializer = ({ setPlugin }) => {
const ref = useRef();
ref.current = setPlugin;
useEffect(() => {
ref.current(pluginId);
}, []);
return null;
};
Initializer.propTypes = {
setPlugin: PropTypes.func.isRequired,
};
export default Initializer;

View File

@ -0,0 +1,110 @@
import {
Checkbox,
SingleSelect,
SingleSelectOption,
} from "@strapi/design-system";
import React from "react";
export default function OptionsBar({ options, toggleOption }) {
return (
<div
style={{
display: "flex",
justifyContent: "flex-start",
alignItems: "center",
padding: "0 56px 24px",
gap: "24px",
}}
>
<Checkbox
name="show-type-names"
onValueChange={() => {
toggleOption("showTypes");
}}
value={options.showTypes}
>
Fields Data Types
</Checkbox>
<Checkbox
name="show-icons"
onValueChange={() => toggleOption("showIcons")}
value={options.showIcons}
>
Fields Icons
</Checkbox>
<Checkbox
name="show-default-fields"
onValueChange={() => toggleOption("showDefaultFields")}
value={options.showDefaultFields}
>
Default Fields
</Checkbox>
<Checkbox
name="show-relations-only"
onValueChange={() => toggleOption("showRelationsOnly")}
value={options.showRelationsOnly}
>
Relational Fields Only
</Checkbox>
<Checkbox
name="show-admin-types"
onValueChange={() => toggleOption("showAdminTypes")}
value={options.showAdminTypes}
>
admin:: Types
</Checkbox>
<Checkbox
name="show-plugin-types"
onValueChange={() => toggleOption("showPluginTypes")}
value={options.showPluginTypes}
>
plugin:: Types
</Checkbox>
<Checkbox
name="show-edges"
onValueChange={() => toggleOption("showEdges")}
value={options.showEdges}
>
Edges
</Checkbox>
<Checkbox
name="snap-to-grid"
onValueChange={() => toggleOption("snapToGrid")}
value={options.snapToGrid}
>
Snap To Grid
</Checkbox>
<div style={{ flexGrow: 1 }} />
<SingleSelect
// label="Edge Type"
value={options.edgeType}
onChange={(type) => toggleOption("edgeType", type)}
>
<SingleSelectOption value="smartbezier">
Smart Bezier
</SingleSelectOption>
<SingleSelectOption value="smartstraight">
Smart Straight
</SingleSelectOption>
<SingleSelectOption value="smartstep">Smart Step</SingleSelectOption>
<SingleSelectOption value="default">Bezier</SingleSelectOption>
<SingleSelectOption value="simplebezier">
Simple Bezier
</SingleSelectOption>
<SingleSelectOption value="straight">Straight</SingleSelectOption>
<SingleSelectOption value="step">Step</SingleSelectOption>
<SingleSelectOption value="smoothstep">Smooth Step</SingleSelectOption>
</SingleSelect>
<SingleSelect
// label="Background"
value={options.backgroundPattern}
onChange={(pattern) => toggleOption("backgroundPattern", pattern)}
>
<SingleSelectOption value="dots">Dots</SingleSelectOption>
<SingleSelectOption value="lines">Lines</SingleSelectOption>
<SingleSelectOption value="cross">Cross</SingleSelectOption>
<SingleSelectOption value="none">None</SingleSelectOption>
</SingleSelect>
</div>
);
}

View File

@ -0,0 +1,12 @@
/**
*
* PluginIcon
*
*/
import React from "react";
import { OneToMany } from "@strapi/icons";
const PluginIcon = () => <OneToMany />;
export default PluginIcon;

View File

@ -0,0 +1,21 @@
import styled from "styled-components";
export const RelationIndicator = styled.span`
height: 20px;
width: 20px;
font-size: 12px;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
background: ${(props) => props.theme.colors.neutral0};
position: absolute;
right: -32px;
top: 0;
bottom: 0;
margin: auto;
svg path {
fill: ${(props) => props.theme.colors.buttonPrimary500};
}
`;

View File

@ -0,0 +1,65 @@
import { prefixPluginTranslations } from "@strapi/helper-plugin";
import pluginPkg from "../../package.json";
import pluginId from "./pluginId";
import Initializer from "./components/Initializer";
import PluginIcon from "./components/PluginIcon";
const name = pluginPkg.strapi.name;
export default {
register(app) {
app.addMenuLink({
to: `/plugins/${pluginId}`,
icon: PluginIcon,
intlLabel: {
id: `${pluginId}.plugin.name`,
defaultMessage: "Content-Type Explorer",
},
Component: async () => {
const component = await import(
/* webpackChunkName: "[request]" */ "./pages/App"
);
return component;
},
permissions: [
// Uncomment to set the permissions of the plugin here
// {
// action: '', // the action name should be plugin::plugin-name.actionType
// subject: null,
// },
],
});
app.registerPlugin({
id: pluginId,
initializer: Initializer,
isReady: false,
name,
});
},
bootstrap(app) {},
async registerTrads({ locales }) {
const importedTrads = await Promise.all(
locales.map((locale) => {
return import(
/* webpackChunkName: "translation-[request]" */ `./translations/${locale}.json`
)
.then(({ default: data }) => {
return {
data: prefixPluginTranslations(data, pluginId),
locale,
};
})
.catch(() => {
return {
data: {},
locale,
};
});
})
);
return Promise.resolve(importedTrads);
},
};

View File

@ -0,0 +1,25 @@
/**
*
* This component is the skeleton around the actual pages, and should only
* contain code that should be seen on all pages. (e.g. navigation bar)
*
*/
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import { AnErrorOccurred } from '@strapi/helper-plugin';
import pluginId from '../../pluginId';
import HomePage from '../HomePage';
const App = () => {
return (
<div>
<Switch>
<Route path={`/plugins/${pluginId}`} component={HomePage} exact />
<Route component={AnErrorOccurred} />
</Switch>
</div>
);
};
export default App;

View File

@ -0,0 +1,175 @@
/*
*
* HomePage
*
*/
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Button, 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 } 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";
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 nodeTypes = useMemo(() => ({ special: CustomNode }), []);
const edgeTypes = useMemo(
() => ({
smartbezier: SmartBezierEdge,
smartstep: SmartStepEdge,
smartstraight: SmartStraightEdge,
}),
[]
);
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[setEdges]
);
// Show/hide content types on options change
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();
}, [options.showAdminTypes, options.showPluginTypes]);
// Create/update nodes & edges
useEffect(() => {
if (contentTypes.length > 0) {
let newNodes = createNodes(contentTypes, options);
setNodes(newNodes);
if (options.showEdges) {
let newEdges = createEdegs(contentTypes, options);
setEdges(newEdges);
} else {
setEdges([]);
}
}
}, [contentTypes, options]);
return (
<>
<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"
>
Help
</LinkButton>
}
/>
<OptionsBar options={options} toggleOption={toggleOption} />
<div
style={{
height: "100vh",
borderTop: `1px solid ${theme.colors.neutral150}`,
}}
>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
fitView
minZoom={0}
preventScrolling={!options.scrollMode}
snapGrid={[20, 20]}
snapToGrid={options.snapToGrid}
fitViewOptions={{
maxZoom: 1,
}}
>
<Controls
position="top-left"
showInteractive={false}
className="cte-plugin-controls"
style={{
"--button-background": theme.colors.neutral150,
"--button-foreground": theme.colors.neutral1000,
"--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>
</Controls>
<Background
variant={options.backgroundPattern}
color={getBackgroundColor(options.backgroundPattern, theme)}
/>
</ReactFlow>
</div>
</>
);
};
export default HomePage;

View File

@ -0,0 +1,12 @@
.cte-plugin-controls button {
background-color: var(--button-background);
border: none;
}
.cte-plugin-controls button:hover {
background-color: var(--button-hover);
}
.cte-plugin-controls button svg {
fill: var(--button-foreground);
}

View File

@ -0,0 +1,5 @@
import pluginPkg from '../../package.json';
const pluginId = pluginPkg.name.replace(/^(@[^-,.][\w,-]+\/|strapi-)plugin-/i, '');
export default pluginId;

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,54 @@
const CARDS_PER_ROW = 6;
export function createNodes(contentTypes, options) {
let newNodes = [];
contentTypes.map(
(node, index) =>
(newNodes = [
...newNodes,
{
id: node.key,
position: {
x: (index % CARDS_PER_ROW) * 320,
y: ((index - (index % CARDS_PER_ROW)) / CARDS_PER_ROW) * 560,
},
type: "special",
data: {
...node,
options: options,
},
},
])
);
return newNodes;
}
export function createEdegs(contentTypes, options) {
let newEdges = [];
contentTypes.map((contentType) => {
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
)
) {
newEdges = [
...newEdges,
{
id: `${contentType.attributes[attr].target}-${contentType.key}.${attr}`,
source: contentType.key,
target: contentType.attributes[attr].target,
type: options.edgeType,
sourceHandle: attr,
},
];
}
}
});
});
return newEdges;
}

View File

@ -0,0 +1,5 @@
import pluginId from '../pluginId';
const getTrad = (id) => `${pluginId}.${id}`;
export default getTrad;

View File

@ -0,0 +1,86 @@
import React from "react";
import { darkTheme } from "@strapi/design-system";
import {
Text,
Email,
Password,
Number,
Enumeration,
Date,
Media,
Boolean,
Json,
Relation,
Uid,
OneToMany,
OneToOne,
ManyToMany,
ManyToOne,
OneWay,
ManyWays,
RichText,
} from "@strapi/icons";
export function getBackgroundColor(variant, theme) {
switch (variant) {
case "cross":
return theme.colors.neutral200;
case "dots":
return darkTheme.colors.neutral300;
case "lines":
return theme.colors.neutral150;
case "none":
return theme.colors.neutral100;
}
}
export function getIcon(attrType) {
switch (attrType.toLowerCase()) {
case "string":
case "text":
return <Text />;
case "email":
return <Email />;
case "enumeration":
return <Enumeration />;
case "password":
return <Password />;
case "boolean":
return <Boolean />;
case "relation":
return <Relation />;
case "datetime":
case "date":
case "time":
return <Date />;
case "integer":
case "decimal":
case "biginteger":
case "float":
return <Number />;
case "json":
return <Json />;
case "uid":
return <Uid />;
case "richtext":
return <RichText />;
case "media":
return <Media />;
case "onetomany": //
return <OneToMany />;
case "oneway":
return <OneWay />;
case "onetoone": //
return <OneToOne />;
case "manytomany": //
return <ManyToMany />;
case "manytoone": //
return <ManyToOne />;
case "manyways":
// Not sure
case "morphtomany":
return <ManyWays />;
}
}

View File

@ -0,0 +1,42 @@
{
"name": "content-type-explorer",
"version": "0.0.0",
"description": "This is the description of the plugin.",
"strapi": {
"name": "content-type-explorer",
"description": "Description of Content Type Explorer plugin",
"kind": "plugin",
"displayName": "Content Type Explorer"
},
"dependencies": {
"@strapi/design-system": "^1.6.3",
"@strapi/helper-plugin": "^4.6.0",
"@strapi/icons": "^1.6.3",
"prop-types": "^15.7.2"
},
"devDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^5.3.4",
"styled-components": "^5.3.6"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0",
"react-router-dom": "^5.3.4",
"styled-components": "^5.3.6"
},
"author": {
"name": "A Strapi developer"
},
"maintainers": [
{
"name": "A Strapi developer"
}
],
"engines": {
"node": ">=14.19.1 <=18.x.x",
"npm": ">=6.0.0"
},
"license": "MIT"
}

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = ({ strapi }) => {
// bootstrap phase
};

View File

@ -0,0 +1,6 @@
'use strict';
module.exports = {
default: {},
validator() {},
};

View File

@ -0,0 +1,3 @@
'use strict';
module.exports = {};

View File

@ -0,0 +1,20 @@
"use strict";
module.exports = ({ strapi }) => ({
getContentTypes(ctx) {
const data = strapi
.plugin("content-type-explorer")
.service("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;
},
});

View File

@ -0,0 +1,7 @@
"use strict";
const explorerController = require("./explorer-controller");
module.exports = {
explorerController,
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = ({ strapi }) => {
// destroy phase
};

View File

@ -0,0 +1,25 @@
'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');
module.exports = {
register,
bootstrap,
destroy,
config,
controllers,
routes,
services,
contentTypes,
policies,
middlewares,
};

View File

@ -0,0 +1,3 @@
'use strict';
module.exports = {};

View File

@ -0,0 +1,3 @@
'use strict';
module.exports = {};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = ({ strapi }) => {
// registeration phase
};

View File

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

View File

@ -0,0 +1,7 @@
"use strict";
module.exports = ({ strapi }) => ({
getContentTypes() {
return strapi.contentTypes;
},
});

View File

@ -0,0 +1,7 @@
"use strict";
const explorerService = require("./explorer-service");
module.exports = {
explorerService,
};

View File

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

View File

@ -0,0 +1,3 @@
'use strict';
module.exports = require('./server');