diff --git a/admin/src/containers/ExportPage/index.js b/admin/src/containers/ExportPage/index.js index 316f309..0a02dbf 100644 --- a/admin/src/containers/ExportPage/index.js +++ b/admin/src/containers/ExportPage/index.js @@ -3,108 +3,20 @@ * ExportPage * */ - -import React, { memo, useState, useMemo } from "react"; +import React, { memo, useState } from "react"; import PropTypes from "prop-types"; import { Loader, Block, Row } from "../../components/common"; -import { Select, Label, Button } from "@buffetjs/core"; -import DataViewer from "../../components/DataViewer"; - -import FORMATS from "../../constants/formats"; - -import pluginId from "../../pluginId"; -import { request } from "strapi-helper-plugin"; -import { downloadFile, copyClipboard } from "../../utils/exportUtils"; - -import { Collapse } from "reactstrap"; -import { FilterIcon } from "strapi-helper-plugin"; -import BASE_OPTIONS from "../../constants/options"; -import OptionsExport from "../../components/OptionsExport"; - -const exportFormatsOptions = FORMATS.map(({ name, mimeType }) => ({ - label: name, - value: mimeType, -})); - -function ImportPage({ contentTypes }) { - const [target, setTarget] = useState(null); - const [sourceExports, setSourceExports] = useState(""); - const [exportFormat, setExportFormat] = useState("application/json"); - const [contentToExport, setContentToExport] = useState(""); - - const sourceOptions = useMemo( - () => - [{ label: "Select Export Source", value: "" }].concat( - contentTypes.map(({ uid, info, apiID }) => ({ - label: info.label || apiID, - value: uid, - })) - ), - [contentTypes] - ); - - // Source Options Handler - const handleSelectSourceExports = ({ target: { value } }) => { - setSourceExports(value); - setTarget(contentTypes.find(({ uid }) => uid === value)); - setContentToExport(""); - }; - - // Source Options Handler - const handleSelectExportFormat = ({ target: { value } }) => { - setExportFormat(value); - setContentToExport(""); - }; +import { Select, Label } from "@buffetjs/core"; - // Options to exporting - const [isOptionsOpen, setIsOptionsOpen] = useState(false); - const [options, setOptions] = useState( - BASE_OPTIONS.reduce((acc, { name, defaultValue }) => { - acc[name] = defaultValue; - return acc; - }, {}) - ); +import SingleExport from "../SingleExport"; +import GroupExport from "../GroupExport"; - const handleChangeOptions = (option, value) => { - setOptions({ - ...options, - [option]: value, - }); - }; +function ExportPage({ contentTypes }) { + const [isLoading, setIsLoading] = useState(false); + const [exportType, setExportType] = useState("single"); - // Request to Get Available Content - const [isLoading, setIsLoadig] = useState(false); - const getContent = async () => { - if (sourceExports === "") - return strapi.notification.toggle({ - type: "warning", - message: "export.source.empty", - }); - - try { - setIsLoadig(true); - const { data } = await request(`/${pluginId}/export`, { - method: "POST", - body: { target, type: exportFormat, options }, - }); - - setContentToExport(data); - } catch (error) { - strapi.notification.toggle({ - type: "warning", - message: `export.items.error`, - }); - } - - setIsLoadig(false); - }; - - // Export Options - const handleDownload = () => { - downloadFile(target.info.name, contentToExport, exportFormat); - }; - const handleCopy = () => copyClipboard(contentToExport); + const handleSelectExportType = ({ target: { value } }) => setExportType(value); return ( {isLoading && } + - Export Source + Export Type - - - Export Format - - - - setIsOptionsOpen((v) => !v)} - className="w-100" - icon={} - label="Options" - color="cancel" - /> - - - - - - - - - - - - - - - - - - - - - + + {exportType === 'single' && ( + + )} + {exportType === 'group' && ( + + )} - ); + ) } -ImportPage.defaultProps = { +ExportPage.defaultProps = { contentTypes: [], }; -ImportPage.propTypes = { +ExportPage.propTypes = { contentTypes: PropTypes.array, }; -export default memo(ImportPage); +export default memo(ExportPage); diff --git a/admin/src/containers/GroupExport/index.js b/admin/src/containers/GroupExport/index.js new file mode 100644 index 0000000..fc9a4f3 --- /dev/null +++ b/admin/src/containers/GroupExport/index.js @@ -0,0 +1,171 @@ +/* + * + * GroupExport + * + */ + +import React, { memo, useState, useMemo } from "react"; +import PropTypes from "prop-types"; + +import { Row } from "../../components/common"; +import { Select, Label, Button, Checkbox } from "@buffetjs/core"; + +import pluginId from "../../pluginId"; +import { request, auth } from "strapi-helper-plugin"; + +import { Collapse } from "reactstrap"; +import { FilterIcon } from "strapi-helper-plugin"; +import OptionsExport from "../../components/OptionsExport"; +import useExportFormats from "../../hooks/useExportFormat"; +import useExportOptions from "../../hooks/useExportOptions"; + +function GroupExport({ contentTypes, setIsLoading }) { + const [targets, setTargets] = useState( + contentTypes.reduce((acc, type) => ({ + ...acc, [type.uid]: { + enabled: true, + label: type.info.label || apiID, + } + }), {}) + ); + + const { + exportFormat, + setExportFormat, + exportFormatsOptions, + } = useExportFormats(); + + const { + options, + isOptionsOpen, + setIsOptionsOpen, + updateOption, + } = useExportOptions(); + + const handleSelectExportFormat = ({ target: { value } }) => { + setExportFormat(value); + }; + + const getContent = async () => { + if (Object.keys(targets).every(uid => !targets[uid].enabled)) + return strapi.notification.toggle({ + type: "warning", + message: "export.source.empty", + }); + + try { + setIsLoading(true); + + const token = auth.getToken(); + + const response = await fetch(`/${pluginId}/export-multi`, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + targets: contentTypes.filter(({ uid }) => !!targets[uid].enabled), + type: exportFormat, + options + }), + }); + + if (response.status === 200) { + const blob = await response.blob(); + + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `export.zip`; + document.body.appendChild(a); + a.click(); + a.remove(); + } + } catch (error) { + strapi.notification.toggle({ + type: "warning", + message: `export.items.error`, + }); + } + + setIsLoading(false); + }; + + return ( + <> + + + Export Source + {Object.keys(targets).map(current => { + return ( + + { + setTargets(prevState => ({ + ...prevState, + [current]: { + ...targets[current], + enabled: value, + }, + })); + }} + /> + + ); + })} + + + + + Export Format + + + + setIsOptionsOpen((v) => !v)} + className="w-100" + icon={} + label="Options" + color="cancel" + /> + + + + + + + + + + + + + + + > + ); +} + +GroupExport.defaultProps = { + contentTypes: [], +}; + +GroupExport.propTypes = { + contentTypes: PropTypes.array, + setIsLoading: PropTypes.func, +}; + +export default memo(GroupExport); diff --git a/admin/src/containers/SingleExport/index.js b/admin/src/containers/SingleExport/index.js new file mode 100644 index 0000000..4b54b41 --- /dev/null +++ b/admin/src/containers/SingleExport/index.js @@ -0,0 +1,180 @@ +/* + * + * ExportPage + * + */ + +import React, { memo, useState, useMemo } from "react"; +import PropTypes from "prop-types"; + +import { Loader, Block, Row } from "../../components/common"; +import { Select, Label, Button } from "@buffetjs/core"; +import DataViewer from "../../components/DataViewer"; + +import pluginId from "../../pluginId"; +import { request } from "strapi-helper-plugin"; +import { downloadFile, copyClipboard } from "../../utils/exportUtils"; + +import { Collapse } from "reactstrap"; +import { FilterIcon } from "strapi-helper-plugin"; +import OptionsExport from "../../components/OptionsExport"; +import useExportFormats from "../../hooks/useExportFormat"; +import useExportOptions from "../../hooks/useExportOptions"; + +function ImportPage({ contentTypes, setIsLoading }) { + const [target, setTarget] = useState(null); + + const { + exportFormat, + setExportFormat, + exportFormatsOptions, + } = useExportFormats(); + + const { + options, + isOptionsOpen, + setIsOptionsOpen, + updateOption, + } = useExportOptions(); + + const [sourceExports, setSourceExports] = useState(""); + const [contentToExport, setContentToExport] = useState(""); + + const sourceOptions = useMemo( + () => + [{ label: "Select Export Source", value: "" }].concat( + contentTypes.map(({ uid, info, apiID }) => ({ + label: info.label || apiID, + value: uid, + })) + ), + [contentTypes] + ); + + // Source Options Handler + const handleSelectSourceExports = ({ target: { value } }) => { + setSourceExports(value); + setTarget(contentTypes.find(({ uid }) => uid === value)); + setContentToExport(""); + }; + + // Source Options Handler + const handleSelectExportFormat = ({ target: { value } }) => { + setExportFormat(value); + setContentToExport(""); + }; + + const getContent = async () => { + if (sourceExports === "") + return strapi.notification.toggle({ + type: "warning", + message: "export.source.empty", + }); + + try { + setIsLoading(true); + const { data } = await request(`/${pluginId}/export`, { + method: "POST", + body: { target, type: exportFormat, options }, + }); + + setContentToExport(data); + } catch (error) { + strapi.notification.toggle({ + type: "warning", + message: `export.items.error`, + }); + } + + setIsLoading(false); + }; + + // Export Options + const handleDownload = () => { + downloadFile(target.info.name, contentToExport, exportFormat); + }; + const handleCopy = () => copyClipboard(contentToExport); + + return ( + <> + + + Export Source + + + + Export Format + + + + setIsOptionsOpen((v) => !v)} + className="w-100" + icon={} + label="Options" + color="cancel" + /> + + + + + + + + + + + + + + + + + + + + + + + + > + ); +} + +ImportPage.defaultProps = { + contentTypes: [], +}; + +ImportPage.propTypes = { + contentTypes: PropTypes.array, + setIsLoading: PropTypes.func, +}; + +export default memo(ImportPage); diff --git a/admin/src/hooks/useExportFormat.js b/admin/src/hooks/useExportFormat.js new file mode 100644 index 0000000..3ac6e2b --- /dev/null +++ b/admin/src/hooks/useExportFormat.js @@ -0,0 +1,19 @@ +import { useState } from 'react'; +import FORMATS from '../constants/formats'; + +const exportFormatsOptions = FORMATS.map(({ name, mimeType }) => ({ + label: name, + value: mimeType, +})); + +function useExportFormats(defaultFormat = "application/json") { + const [exportFormat, setExportFormat] = useState(defaultFormat); + + return { + exportFormat, + setExportFormat, + exportFormatsOptions, + } +} + +export default useExportFormats; \ No newline at end of file diff --git a/admin/src/hooks/useExportOptions.js b/admin/src/hooks/useExportOptions.js new file mode 100644 index 0000000..25c7d39 --- /dev/null +++ b/admin/src/hooks/useExportOptions.js @@ -0,0 +1,29 @@ + +import { useState } from 'react'; +import BASE_OPTIONS from '../constants/options'; + +function useExportOptions() { + const [isOptionsOpen, setIsOptionsOpen] = useState(false); + const [options, setOptions] = useState( + BASE_OPTIONS.reduce((acc, { name, defaultValue }) => { + acc[name] = defaultValue; + return acc; + }, {}) + ); + + const updateOption = (option, value) => { + setOptions({ + ...options, + [option]: value, + }); + }; + + return { + options, + isOptionsOpen, + setIsOptionsOpen, + updateOption, + } +} + +export default useExportOptions; \ No newline at end of file diff --git a/config/routes.json b/config/routes.json index 61b0fd1..ac20b68 100644 --- a/config/routes.json +++ b/config/routes.json @@ -31,6 +31,14 @@ "config": { "policies": [] } + }, + { + "method": "POST", + "path": "/export-multi", + "handler": "import-export-content.exportItemsMulti", + "config": { + "policies": [] + } } ] -} +} \ No newline at end of file diff --git a/controllers/import-export-content.js b/controllers/import-export-content.js index e0f5bad..fe480c7 100644 --- a/controllers/import-export-content.js +++ b/controllers/import-export-content.js @@ -1,15 +1,17 @@ "use strict"; const pluginPkg = require("../package.json"); +const PERMISSIONS = require("../constants/permissions"); +const JSZip = require("jszip"); +const mimeExtension = require("../services/utils/mimeExtension"); const PLUGIN_ID = pluginPkg.name.replace(/^strapi-plugin-/i, ""); + function getService(service = PLUGIN_ID) { const SERVICES = strapi.plugins[PLUGIN_ID].services; return SERVICES[service]; } -const PERMISSIONS = require("../constants/permissions"); - /** * import-export-content.js controller * @@ -67,8 +69,8 @@ module.exports = { message: succesfully ? "All Data Imported" : results.some((res) => res) - ? "Some Items Imported" - : "No Items Imported", + ? "Some Items Imported" + : "No Items Imported", }); } catch (error) { console.error(error); @@ -90,11 +92,54 @@ module.exports = { try { const service = getService(); - const data = await service.exportItems(ctx); + const data = await service.exportItems({ + target, + type, + options, + }, ctx); ctx.send({ data, message: "ok" }); } catch (error) { console.error(error); ctx.throw(406, `could not parse: ${error}`); } }, + + exportItemsMulti: async (ctx) => { + const { targets, type, options } = JSON.parse(ctx.request.body); + + if (!targets || !type || !options) { + return ctx.throw(400, "Required parameters missing"); + } + + const { userAbility } = ctx.state; + + try { + const zip = new JSZip(); + + const createOperation = async (target) => { + const data = await getService().exportItems({ + target, + options, + type, + }, ctx); + + zip.file(`${target.info.name}.${mimeExtension(type)}`, data); + } + + for (const target of targets) { + if (userAbility.cannot(PERMISSIONS.read, target.uid)) { + return ctx.forbidden(); + } + } + + await Promise.all(targets.map(createOperation)); + + const buffer = await zip.generateAsync({ type: 'nodebuffer' }); + + ctx.send(buffer); + } catch (error) { + console.error(error); + ctx.throw(406, `could not parse: ${error}`); + } + }, }; diff --git a/package.json b/package.json index 291f791..a272023 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "strapi-plugin-import-export-content", - "version": "0.4.2", + "version": "0.4.3", "description": "This is a plugin for import and export content of collection types.", "license": "MIT", "strapi": { @@ -14,6 +14,7 @@ "dependencies": { "csv-parse": "^4.15.3", "csv-string": "^4.0.1", + "jszip": "^3.7.1", "prismjs": "^1.23.0", "react-simple-code-editor": "^0.11.0" }, @@ -42,4 +43,4 @@ "url": "https://github.com/EdisonPeM/strapi-plugin-import-export-content/issues" }, "homepage": "https://github.com/EdisonPeM/strapi-plugin-import-export-content#readme" -} +} \ No newline at end of file diff --git a/services/import-export-content.js b/services/import-export-content.js index 0b17a4b..2d5e474 100644 --- a/services/import-export-content.js +++ b/services/import-export-content.js @@ -49,8 +49,8 @@ module.exports = { }); }, - exportItems: async (ctx) => { - const { target, type, options } = ctx.request.body; + exportItems: async (data, ctx) => { + const { target, type, options } = data; const { userAbility } = ctx.state; const exportItems = await getData(target, options, userAbility); diff --git a/services/utils/mimeExtension.js b/services/utils/mimeExtension.js new file mode 100644 index 0000000..470fa89 --- /dev/null +++ b/services/utils/mimeExtension.js @@ -0,0 +1,13 @@ +function mimeExtension(type) { + switch (type) { + case "text/csv": + case "application/vnd.ms-excel": { + return 'csv'; + } + case "application/json": { + return 'json' + } + } +} + +module.exports = mimeExtension; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..c55e63a --- /dev/null +++ b/yarn.lock @@ -0,0 +1,105 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +csv-parse@^4.15.3: + version "4.16.3" + resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-4.16.3.tgz#7ca624d517212ebc520a36873c3478fa66efbaf7" + integrity sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg== + +csv-string@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/csv-string/-/csv-string-4.1.0.tgz#3fd5101e67cfb4e0f428f40263837044737f1457" + integrity sha512-67UM45fosQvvh+HUvC8iNgg2B4UHWJ5Qud7TWy1EcbIQ9levAFKWudMk2k8U3LgXZP/Drr6eBwBwbSWq2N8HZA== + +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= + +inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +jszip@^3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.7.1.tgz#bd63401221c15625a1228c556ca8a68da6fda3d9" + integrity sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + set-immediate-shim "~1.0.1" + +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + +pako@~1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + +prismjs@^1.23.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057" + integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +react-simple-code-editor@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/react-simple-code-editor/-/react-simple-code-editor-0.11.0.tgz#bb57c7c29b570f2ab229872599eac184f5bc673c" + integrity sha512-xGfX7wAzspl113ocfKQAR8lWPhavGWHL3xSzNLeseDRHysT+jzRBi/ExdUqevSMos+7ZtdfeuBOXtgk9HTwsrw== + +readable-stream@~2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +set-immediate-shim@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E= + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=