export as image
parent
8336ba449e
commit
18bc292879
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -15,6 +15,7 @@ import OptionsBar from "../../components/OptionsBar";
|
|||
import "reactflow/dist/style.css";
|
||||
import "./styles.css";
|
||||
import { useDigramStore } from "../../store";
|
||||
import { ExportModal } from "../../components/ExportModal";
|
||||
|
||||
const useEffectSkipInitial = (func, deps) => {
|
||||
const didMount = useRef(false);
|
||||
|
@ -40,6 +41,8 @@ const HomePage = () => {
|
|||
toggleOption,
|
||||
options,
|
||||
setData,
|
||||
showModal,
|
||||
setShowModal,
|
||||
} = useDigramStore();
|
||||
|
||||
const nodeTypes = useMemo(() => ({ special: CustomNode }), []);
|
||||
|
@ -52,14 +55,14 @@ const HomePage = () => {
|
|||
[]
|
||||
);
|
||||
|
||||
const refresh = async () => {
|
||||
const regenrate = async () => {
|
||||
const { data } = await get(`/strapi-content-type-explorer/get-types`);
|
||||
setData(data);
|
||||
drawDiagram();
|
||||
};
|
||||
|
||||
useEffectSkipInitial(() => {
|
||||
refresh();
|
||||
regenrate();
|
||||
}, [options.showAdminTypes, options.showPluginTypes]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -75,23 +78,30 @@ const HomePage = () => {
|
|||
options.showDefaultFields,
|
||||
]);
|
||||
|
||||
const ref = useRef(null);
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
|
||||
<HeaderLayout
|
||||
title="Content-Type Explorer"
|
||||
primaryAction={
|
||||
<Button variant="secondary" startIcon={<Download />}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
startIcon={<Download />}
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
Export Diagram
|
||||
</Button>
|
||||
}
|
||||
secondaryAction={
|
||||
<Button variant="primary" startIcon={<Refresh />} onClick={refresh}>
|
||||
Draw Again
|
||||
<Button variant="primary" startIcon={<Refresh />} onClick={regenrate}>
|
||||
Regenrate
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<OptionsBar />
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
height: "100%",
|
||||
borderTop: `1px solid ${theme.colors.neutral150}`,
|
||||
|
@ -141,6 +151,7 @@ const HomePage = () => {
|
|||
color={getBackgroundColor(options.backgroundPattern, theme)}
|
||||
/>
|
||||
</ReactFlow>
|
||||
{showModal && <ExportModal imageRef={ref} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -14,6 +14,7 @@ export const useDigramStore = create(
|
|||
nodes: [],
|
||||
edges: [],
|
||||
data: [],
|
||||
showModal: false,
|
||||
options: {
|
||||
snapToGrid: false,
|
||||
showTypes: true,
|
||||
|
@ -32,6 +33,11 @@ export const useDigramStore = create(
|
|||
data: contentTypesData,
|
||||
});
|
||||
},
|
||||
setShowModal: (bool) => {
|
||||
set({
|
||||
showModal: bool,
|
||||
});
|
||||
},
|
||||
toggleOption: (optionName, optionValue = null) => {
|
||||
let newOptions = {
|
||||
...get().options,
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tisoap/react-flow-smart-edge": "^3.0.0",
|
||||
"html-to-image": "^1.11.11",
|
||||
"reactflow": "^11.10.2",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
|
@ -8927,6 +8928,11 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/html-to-image": {
|
||||
"version": "1.11.11",
|
||||
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.11.tgz",
|
||||
"integrity": "sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA=="
|
||||
},
|
||||
"node_modules/html-webpack-plugin": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz",
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@tisoap/react-flow-smart-edge": "^3.0.0",
|
||||
"html-to-image": "^1.11.11",
|
||||
"reactflow": "^11.10.2",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue