├── .editorconfig
├── .gitattributes
├── .gitignore
├── LICENCE
├── README.md
├── admin
└── src
│ ├── assets
│ └── prismjs.css
│ ├── components
│ ├── DataViewer
│ │ ├── index.js
│ │ └── styles.js
│ ├── DropFileZone
│ │ ├── DragAndDropIcon.js
│ │ ├── index.js
│ │ └── styles.js
│ ├── FormatIcon
│ │ └── index.js
│ ├── ImportForm
│ │ └── index.js
│ ├── Layout
│ │ └── index.js
│ ├── MappingTable
│ │ ├── TableBody.js
│ │ ├── TableHeader.js
│ │ ├── index.js
│ │ └── styles.js
│ ├── MediaPreview
│ │ └── index.js
│ ├── OptionsExport
│ │ └── index.js
│ ├── RawInputForm
│ │ ├── index.js
│ │ └── styles.js
│ ├── UploadFileForm
│ │ └── index.js
│ └── common
│ │ ├── Block
│ │ ├── index.js
│ │ └── styles.js
│ │ ├── Loader
│ │ ├── index.js
│ │ └── styles.js
│ │ ├── Row.js
│ │ └── index.js
│ ├── constants
│ ├── formats.js
│ └── options.js
│ ├── containers
│ ├── App
│ │ └── index.js
│ ├── DataMapper
│ │ └── index.js
│ ├── ExportPage
│ │ └── index.js
│ ├── ImportPage
│ │ └── index.js
│ └── Initializer
│ │ └── index.js
│ ├── hooks
│ └── useContentTypes.js
│ ├── index.js
│ ├── lifecycles.js
│ ├── pluginId.js
│ ├── translations
│ ├── ar.json
│ ├── cs.json
│ ├── de.json
│ ├── en.json
│ ├── es.json
│ ├── fr.json
│ ├── id.json
│ ├── index.js
│ ├── it.json
│ ├── ko.json
│ ├── ms.json
│ ├── nl.json
│ ├── pl.json
│ ├── pt-BR.json
│ ├── pt.json
│ ├── ru.json
│ ├── sk.json
│ ├── th.json
│ ├── tr.json
│ ├── uk.json
│ ├── vi.json
│ ├── zh-Hans.json
│ └── zh.json
│ └── utils
│ ├── exportUtils.js
│ ├── formatFileContent.js
│ ├── getTrad.js
│ ├── highlight.js
│ ├── mediaFormat.js
│ └── readFileContent.js
├── config
└── routes.json
├── constants
├── contentTypes.js
├── permissions.js
└── relations.js
├── controllers
└── import-export-content.js
├── package.json
└── services
├── analyzer.js
├── contentParser
├── csvParser.js
├── index.js
└── textFormats.js
├── exporter
├── exportUtils.js
└── index.js
├── import-export-content.js
├── importer
├── importMediaFiles.js
├── importUtils.js
└── index.js
└── utils
├── contentChecker.js
├── fieldUtils.js
└── formatsValidator.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = false
6 | indent_style = space
7 | indent_size = 2
8 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # From https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes
2 |
3 | # Handle line endings automatically for files detected as text
4 | # and leave all files detected as binary untouched.
5 | * text=auto
6 |
7 | #
8 | # The above will handle all files NOT found below
9 | #
10 |
11 | #
12 | ## These files are text and should be normalized (Convert crlf => lf)
13 | #
14 |
15 | # source code
16 | *.php text
17 | *.css text
18 | *.sass text
19 | *.scss text
20 | *.less text
21 | *.styl text
22 | *.js text eol=lf
23 | *.coffee text
24 | *.json text
25 | *.htm text
26 | *.html text
27 | *.xml text
28 | *.svg text
29 | *.txt text
30 | *.ini text
31 | *.inc text
32 | *.pl text
33 | *.rb text
34 | *.py text
35 | *.scm text
36 | *.sql text
37 | *.sh text
38 | *.bat text
39 |
40 | # templates
41 | *.ejs text
42 | *.hbt text
43 | *.jade text
44 | *.haml text
45 | *.hbs text
46 | *.dot text
47 | *.tmpl text
48 | *.phtml text
49 |
50 | # git config
51 | .gitattributes text
52 | .gitignore text
53 | .gitconfig text
54 |
55 | # code analysis config
56 | .jshintrc text
57 | .jscsrc text
58 | .jshintignore text
59 | .csslintrc text
60 |
61 | # misc config
62 | *.yaml text
63 | *.yml text
64 | .editorconfig text
65 |
66 | # build config
67 | *.npmignore text
68 | *.bowerrc text
69 |
70 | # Heroku
71 | Procfile text
72 | .slugignore text
73 |
74 | # Documentation
75 | *.md text
76 | LICENSE text
77 | AUTHORS text
78 |
79 |
80 | #
81 | ## These files are binary and should be left untouched
82 | #
83 |
84 | # (binary is a macro for -text -diff)
85 | *.png binary
86 | *.jpg binary
87 | *.jpeg binary
88 | *.gif binary
89 | *.ico binary
90 | *.mov binary
91 | *.mp4 binary
92 | *.mp3 binary
93 | *.flv binary
94 | *.fla binary
95 | *.swf binary
96 | *.gz binary
97 | *.zip binary
98 | *.7z binary
99 | *.ttf binary
100 | *.eot binary
101 | *.woff binary
102 | *.pyc binary
103 | *.pdf binary
104 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Don't check auto-generated stuff into git
2 | coverage
3 | node_modules
4 | stats.json
5 | package-lock.json
6 |
7 | # Cruft
8 | .DS_Store
9 | npm-debug.log
10 | .idea
11 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Edison Peñuela
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Strapi plugin import-export-content
2 |
3 | Plugin to import and export content according to user permissions in json or csv format.
4 |
5 | ## Installation
6 |
7 | ```bash
8 | yarn add strapi-plugin-import-export-content
9 | ```
10 |
11 | or
12 |
13 | ```bash
14 | npm i strapi-plugin-import-export-content
15 | ```
16 |
17 | ## Rebuild your administration panel
18 |
19 | New releases can introduce changes to the administration panel that require a rebuild. Rebuild the admin panel with one of the following commands:
20 |
21 | ```bash
22 | yarn build --clean
23 | ```
24 |
25 | or
26 |
27 | ```bash
28 | npm run build -- --clean
29 | ```
30 |
31 | ## Features
32 |
33 | ### Import
34 |
35 | - Read data from CSV and JSON file or from typing raw text
36 | - Import contents to collection or single Type
37 | - Manual mapping from source fields to destination fields
38 | - Recognize format of inputs and content types
39 | - Import content as draft or public
40 | - Upload media from URL
41 | - Import Media by id or object with id key
42 | - Import Relations by id or object with id key
43 | - Import Components and Dynamic Zone Content as json objects
44 |
45 | ### Export
46 |
47 | - Export CSV and JSON contents allowed for the user
48 | - Download files or copy exported data to clipboard
49 | - Options to remove ids and timestamps
50 | - Options to export media as ids, urls, full content or full content without formats
51 | - Options to export relatons as ids or full content
52 |
53 | ## Author
54 |
55 | Edison Peñuela – [@EdisonPeM](https://github.com/EdisonPeM/) – edisonpe961206@hotmail.com
56 |
57 | ## Acknowledgments
58 |
59 | This plugin has been inspired by the tutorial [How to create an import content plugin](https://strapi.io/blog/how-to-create-an-import-content-plugin-part-1-4)
60 |
--------------------------------------------------------------------------------
/admin/src/assets/prismjs.css:
--------------------------------------------------------------------------------
1 | /* PrismJS 1.23.0
2 | https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript */
3 | /**
4 | * prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML
5 | * Based on https://github.com/chriskempson/tomorrow-theme
6 | * @author Rose Pritchard
7 | */
8 |
9 | code[class*="language-"],
10 | pre[class*="language-"] {
11 | color: #ccc;
12 | background: none;
13 | font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
14 | font-size: 1em;
15 | text-align: left;
16 | white-space: pre;
17 | word-spacing: normal;
18 | word-break: normal;
19 | word-wrap: normal;
20 | line-height: 1.5;
21 |
22 | -moz-tab-size: 4;
23 | -o-tab-size: 4;
24 | tab-size: 4;
25 |
26 | -webkit-hyphens: none;
27 | -moz-hyphens: none;
28 | -ms-hyphens: none;
29 | hyphens: none;
30 | }
31 |
32 | /* Code blocks */
33 | pre[class*="language-"] {
34 | padding: 1em;
35 | margin: 0.5em 0;
36 | overflow: auto;
37 | }
38 |
39 | :not(pre) > code[class*="language-"],
40 | pre[class*="language-"] {
41 | background: #2d2d2d;
42 | }
43 |
44 | /* Inline code */
45 | :not(pre) > code[class*="language-"] {
46 | padding: 0.1em;
47 | border-radius: 0.3em;
48 | white-space: normal;
49 | }
50 |
51 | .token.comment,
52 | .token.block-comment,
53 | .token.prolog,
54 | .token.doctype,
55 | .token.cdata {
56 | color: #999;
57 | }
58 |
59 | .token.punctuation {
60 | color: #ccc;
61 | }
62 |
63 | .token.tag,
64 | .token.attr-name,
65 | .token.namespace,
66 | .token.deleted {
67 | color: #e2777a;
68 | }
69 |
70 | .token.function-name {
71 | color: #6196cc;
72 | }
73 |
74 | .token.boolean,
75 | .token.number,
76 | .token.function {
77 | color: #f08d49;
78 | }
79 |
80 | .token.property,
81 | .token.class-name,
82 | .token.constant,
83 | .token.symbol {
84 | color: #f8c555;
85 | }
86 |
87 | .token.selector,
88 | .token.important,
89 | .token.atrule,
90 | .token.keyword,
91 | .token.builtin {
92 | color: #cc99cd;
93 | }
94 |
95 | .token.string,
96 | .token.char,
97 | .token.attr-value,
98 | .token.regex,
99 | .token.variable {
100 | color: #7ec699;
101 | }
102 |
103 | .token.operator,
104 | .token.entity,
105 | .token.url {
106 | color: #67cdcc;
107 | }
108 |
109 | .token.important,
110 | .token.bold {
111 | font-weight: bold;
112 | }
113 | .token.italic {
114 | font-style: italic;
115 | }
116 |
117 | .token.entity {
118 | cursor: help;
119 | }
120 |
121 | .token.inserted {
122 | color: green;
123 | }
124 |
--------------------------------------------------------------------------------
/admin/src/components/DataViewer/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { Code } from "./styles";
4 |
5 | import formatFileContent from "../../utils/formatFileContent";
6 | import highlight from "../../utils/highlight";
7 |
8 | function DataViewer({ data, type }) {
9 | const content = formatFileContent({
10 | content: data,
11 | mimeType: type,
12 | });
13 |
14 | const __html = highlight(content, type);
15 | return (
16 |
17 |
18 |
19 | );
20 | }
21 |
22 | DataViewer.defaultProps = {
23 | data: "",
24 | type: "",
25 | };
26 |
27 | DataViewer.propTypes = {
28 | data: PropTypes.string,
29 | type: PropTypes.string,
30 | };
31 |
32 | export default DataViewer;
33 |
--------------------------------------------------------------------------------
/admin/src/components/DataViewer/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const Code = styled.pre`
4 | min-height: 200px;
5 | height: 200px;
6 | width: 100%;
7 | border-radius: 4px;
8 |
9 | background: #1e1e1e;
10 | color: #fafafa;
11 |
12 | margin: 0;
13 | padding: 1.2rem;
14 | overflow: auto;
15 | white-space: pre-wrap;
16 | line-height: 2rem;
17 | cursor: auto;
18 |
19 | resize: vertical;
20 |
21 | &::first-line {
22 | color: #f8c555;
23 | }
24 | `;
25 |
--------------------------------------------------------------------------------
/admin/src/components/DropFileZone/DragAndDropIcon.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const DragAndDropIcon = () => (
4 |
110 | );
111 |
112 | export default DragAndDropIcon;
113 |
--------------------------------------------------------------------------------
/admin/src/components/DropFileZone/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import DragAndDropIcon from "./DragAndDropIcon";
5 | import { Label, P } from "./styles";
6 |
7 | function DropFileZone({
8 | acceptMimeTypes,
9 | acceptFilesTypes,
10 | onUploadFile,
11 | onUploadError,
12 | }) {
13 | const validateFile = (file) => {
14 | if (acceptMimeTypes.includes(file.type)) {
15 | onUploadFile(file);
16 | } else {
17 | onUploadError();
18 | }
19 | };
20 |
21 | const handleFileChange = ({ target: { files } }) => validateFile(files[0]);
22 |
23 | const [isDragging, setIsDragging] = useState(false);
24 | const handleDragEnter = () => setIsDragging(true);
25 | const handleDragLeave = () => setIsDragging(false);
26 | const stopDragEvent = (ev) => ev.preventDefault() && ev.stopPropagation();
27 | const handleDrop = (ev) => {
28 | ev.preventDefault();
29 | setIsDragging(false);
30 |
31 | const { files } = ev.dataTransfer;
32 | validateFile(files[0]);
33 | };
34 |
35 | return (
36 |
59 | );
60 | }
61 |
62 | DropFileZone.defaultProps = {
63 | acceptMimeTypes: [],
64 | acceptFilesTypes: [],
65 | onUploadFile: () => {},
66 | onUploadError: () => {},
67 | };
68 |
69 | DropFileZone.propTypes = {
70 | acceptMimeTypes: PropTypes.array,
71 | acceptFilesTypes: PropTypes.array,
72 | onUploadFile: PropTypes.func,
73 | onUploadError: PropTypes.func,
74 | };
75 |
76 | export default DropFileZone;
77 |
--------------------------------------------------------------------------------
/admin/src/components/DropFileZone/styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from "styled-components";
2 |
3 | export const Label = styled.label`
4 | position: relative;
5 | height: 200px;
6 | width: 100%;
7 |
8 | border: 2px dashed #e3e9f3;
9 | border-radius: 5px;
10 | cursor: pointer;
11 |
12 | display: flex;
13 | flex-direction: column;
14 | justify-content: center;
15 | align-items: center;
16 | text-align: center;
17 |
18 | .icon {
19 | width: 82px;
20 | path {
21 | fill: #ccd0da;
22 | }
23 | }
24 |
25 | .isDragging {
26 | position: absolute;
27 | top: 0;
28 | bottom: 0;
29 | left: 0;
30 | right: 0;
31 | }
32 |
33 | .underline {
34 | color: #1c5de7;
35 | text-decoration: underline;
36 | cursor: pointer;
37 | }
38 |
39 | ${({ isDragging }) => {
40 | if (isDragging) {
41 | return css`
42 | background-color: rgba(28, 93, 231, 0.05) !important;
43 | border: 2px dashed rgba(28, 93, 231, 0.5) !important;
44 | `;
45 | }
46 | }}
47 | `;
48 |
49 | export const P = styled.p`
50 | margin-top: 10px;
51 | text-align: center;
52 | font-size: 13px;
53 | color: #9ea7b8;
54 | u {
55 | color: #1c5de7;
56 | }
57 | `;
58 |
--------------------------------------------------------------------------------
/admin/src/components/FormatIcon/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | // FORMATS
4 | import {
5 | Bool as BoolIcon,
6 | Json as JsonIcon,
7 | Text as TextIcon,
8 | NumberIcon,
9 | Pending as TimeIcon,
10 | Enumeration as ListIcon,
11 | Media as MediaIcon,
12 | Email as EmailIcon,
13 | Calendar as DateIcon,
14 | RichText as RichTextIcon,
15 | } from "@buffetjs/icons";
16 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
17 | import { faLink, faRandom } from "@fortawesome/free-solid-svg-icons";
18 |
19 | const ICONS = {
20 | string: TextIcon,
21 |
22 | // Sub Types of String
23 | email: EmailIcon,
24 | text: RichTextIcon,
25 | date: DateIcon,
26 | time: TimeIcon,
27 | url: ({ fill }) => ,
28 | media: MediaIcon,
29 |
30 | // Others
31 | boolean: BoolIcon,
32 | number: NumberIcon,
33 | object: JsonIcon,
34 |
35 | // temp Array
36 | array: ListIcon,
37 |
38 | // mixed formats
39 | mixed: ({ fill }) => ,
40 | };
41 |
42 | function FormatIcon({ format }) {
43 | const Icon = ICONS[format] || TextIcon;
44 | return ;
45 | }
46 |
47 | export default FormatIcon;
48 |
--------------------------------------------------------------------------------
/admin/src/components/ImportForm/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | function ImportForm() {
4 | const contentTypes = [];
5 |
6 | const [contentTypeSelected, setContentType] = useState("");
7 | const handleSelectContentType = ({ target: { value } }) => {
8 | setContentType(value);
9 | };
10 |
11 | return (
12 |
13 |
14 |
26 | );
27 | }
28 |
29 | export default ImportForm;
30 |
--------------------------------------------------------------------------------
/admin/src/components/Layout/index.js:
--------------------------------------------------------------------------------
1 | import React, { memo } from "react";
2 | import PropTypes from "prop-types";
3 | import { HeaderNav, PluginHeader } from "strapi-helper-plugin";
4 |
5 | function Layout({ navLinks, children }) {
6 | return (
7 |
8 |
12 |
13 |
{children}
14 |
15 | );
16 | }
17 |
18 | Layout.defaultProps = {
19 | navLinks: [],
20 | children: null,
21 | };
22 |
23 | Layout.propTypes = {
24 | navLinks: PropTypes.arrayOf(PropTypes.object),
25 | children: PropTypes.any,
26 | };
27 |
28 | export default memo(Layout);
29 |
--------------------------------------------------------------------------------
/admin/src/components/MappingTable/TableBody.js:
--------------------------------------------------------------------------------
1 | import React, { memo } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5 | import { faTrashAlt } from "@fortawesome/free-solid-svg-icons/faTrashAlt";
6 |
7 | import MediaPreview from "../MediaPreview";
8 | import { isUrlMedia } from "../../utils/mediaFormat";
9 |
10 | function TableBody({ rows, headers, onDeleteItem, onlyFistRow }) {
11 | return (
12 |
13 | {rows.map((row, i) => (
14 |
15 | {headers.map(({ name }, j) => {
16 | const cell = row[name];
17 |
18 | if (cell === undefined || cell === null) return - | ;
19 | if (Array.isArray(cell)) {
20 | return (
21 |
22 | {cell.map((cellItem, k) => {
23 | if (typeof cellItem === "object")
24 | return (
25 | {`${JSON.stringify(
26 | cellItem
27 | )}`}
28 | );
29 |
30 | if (typeof cellItem === "string" && isUrlMedia(cellItem))
31 | return (
32 |
33 |
34 |
35 | );
36 |
37 | return {`${cellItem}`} ;
38 | })}
39 | |
40 | );
41 | }
42 |
43 | if (typeof cell === "object")
44 | return (
45 |
46 | {JSON.stringify(cell)}
47 | |
48 | );
49 |
50 | if (typeof cell === "string" && isUrlMedia(cell))
51 | return (
52 |
53 |
58 | |
59 | );
60 |
61 | return {`${cell}`} | ;
62 | })}
63 |
64 |
67 | |
68 |
69 | ))}
70 |
71 | );
72 | }
73 |
74 | TableBody.defaultProps = {
75 | rows: [],
76 | headers: [],
77 | onDeleteItem: () => {},
78 | onlyFistRow: false,
79 | };
80 |
81 | TableBody.propTypes = {
82 | rows: PropTypes.array,
83 | headers: PropTypes.array,
84 | onDeleteItem: PropTypes.func,
85 | onlyFistRow: PropTypes.bool,
86 | };
87 |
88 | export default memo(TableBody);
89 |
--------------------------------------------------------------------------------
/admin/src/components/MappingTable/TableHeader.js:
--------------------------------------------------------------------------------
1 | import React, { memo } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { Label, Select } from "@buffetjs/core";
5 | import FormatIcon from "../FormatIcon";
6 |
7 | function TableHeader({ headers, headersSelectOptions, onChangeSelect }) {
8 | return (
9 |
10 |
11 | {headers.map(({ name, format }) => (
12 |
13 | {name}
14 |
15 |
16 |
17 | |
18 | ))}
19 | |
20 |
21 |
22 | {headers.map(({ name, value }) => (
23 |
24 |
33 | |
34 | ))}
35 | Del |
36 |
37 |
38 | );
39 | }
40 |
41 | TableHeader.defaultProps = {
42 | headers: [],
43 | headersSelectOptions: [],
44 | onChangeSelect: () => {},
45 | };
46 |
47 | TableHeader.propTypes = {
48 | headers: PropTypes.array,
49 | headersSelectOptions: PropTypes.array,
50 | onChangeSelect: PropTypes.func,
51 | };
52 |
53 | export default memo(TableHeader);
54 |
--------------------------------------------------------------------------------
/admin/src/components/MappingTable/index.js:
--------------------------------------------------------------------------------
1 | import React, { memo } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { TableWrapper } from "./styles";
5 | import TableHeader from "./TableHeader";
6 | import TableBody from "./TableBody";
7 |
8 | function MappingTable({
9 | mappingHeaders,
10 | mappingRows,
11 | headersMappingOptions,
12 | onChangeMapping,
13 | onDeleteRow,
14 | onlyFistRow,
15 | }) {
16 | return (
17 |
18 |
31 |
32 | );
33 | }
34 |
35 | MappingTable.defaultProps = {
36 | mappingHeaders: [],
37 | mappingRows: [],
38 | headersMappingOptions: [],
39 | onChangeMapping: () => {},
40 | onDeleteRow: () => {},
41 | onlyFistRow: false,
42 | };
43 |
44 | MappingTable.propTypes = {
45 | mappingHeaders: PropTypes.array,
46 | mappingRows: PropTypes.array,
47 | headersMappingOptions: PropTypes.array,
48 | onChangeMapping: PropTypes.func,
49 | onDeleteRow: PropTypes.func,
50 | onlyFistRow: PropTypes.bool,
51 | };
52 |
53 | export default memo(MappingTable);
54 |
--------------------------------------------------------------------------------
/admin/src/components/MappingTable/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const TableWrapper = styled.div`
4 | width: 100%;
5 | overflow: auto;
6 | background: white;
7 | margin-top: 1rem;
8 | border-radius: 4px;
9 | border: 1px solid #e3e9f3;
10 | max-height: 300px;
11 |
12 | table {
13 | width: 100%;
14 | text-align: center;
15 |
16 | th {
17 | min-width: 15ch;
18 | background-color: #f3f3f4;
19 | font-weight: bold;
20 | padding: 10px;
21 | white-space: nowrap;
22 |
23 | select.unselected {
24 | color: #ccc;
25 | option {
26 | color: #333740;
27 | }
28 | }
29 | }
30 |
31 | th:last-child {
32 | min-width: 50px;
33 | width: 50px;
34 | }
35 |
36 | td:last-child {
37 | border-left: 1px solid #ccc;
38 | max-width: 50px;
39 | padding: 0;
40 |
41 | button {
42 | border: none;
43 | outline: none;
44 | padding: 15px;
45 | background: transparent;
46 | font-size: 1.2rem;
47 | opacity: 0.5;
48 | cursor: pointer;
49 |
50 | &:hover {
51 | opacity: 1;
52 | }
53 | }
54 | }
55 |
56 | tbody {
57 | &.fist-row-selected {
58 | tr:not(:first-child) {
59 | color: #999;
60 | }
61 | }
62 |
63 | tr {
64 | &:nth-child(even) {
65 | background-color: #fafafa;
66 | }
67 |
68 | &:hover {
69 | background-color: #efefef;
70 | }
71 | }
72 |
73 | td {
74 | padding: 15px;
75 | max-width: 15ch;
76 | white-space: nowrap;
77 | overflow: hidden;
78 | text-overflow: ellipsis;
79 | }
80 | }
81 | }
82 | `;
83 |
84 | export { TableWrapper };
85 |
--------------------------------------------------------------------------------
/admin/src/components/MediaPreview/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { getMediaFormat } from "../../utils/mediaFormat";
3 |
4 | function MediaPreview({ url, ...oterProps }) {
5 | const { type, format } = getMediaFormat(url);
6 |
7 | switch (type) {
8 | case "image":
9 | return
;
10 |
11 | case "video":
12 | return (
13 |
16 | );
17 |
18 | case "audio":
19 | return (
20 |
23 | );
24 |
25 | default:
26 | return null;
27 | }
28 | }
29 |
30 | export default MediaPreview;
31 |
--------------------------------------------------------------------------------
/admin/src/components/OptionsExport/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Select, Checkbox } from "@buffetjs/core";
3 |
4 | import BASE_OPTIONS from "../../constants/options";
5 |
6 | function OptionsExport({ values, onChange }) {
7 | return (
8 |
9 |
10 | {BASE_OPTIONS.map(({ name, label, type, optionalValues }) => {
11 | const handleChange = ({ target: { value } }) => onChange(name, value);
12 |
13 | if (type === "select") {
14 | return (
15 |
16 | {label}:
17 |
24 |
25 | );
26 | } else if (type === "boolean") {
27 | return (
28 |
29 |
36 |
37 | );
38 | }
39 |
40 | return null;
41 | })}
42 |
43 | );
44 | }
45 |
46 | export default OptionsExport;
47 |
--------------------------------------------------------------------------------
/admin/src/components/RawInputForm/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, memo } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { EditorWrapper } from "./styles";
5 | import Editor from "react-simple-code-editor";
6 |
7 | import { Label, Select, Button } from "@buffetjs/core";
8 | import { Row } from "../../components/common";
9 |
10 | import FORMATS from "../../constants/formats";
11 | import highlight from "../../utils/highlight";
12 |
13 | const fortmatsOptions = FORMATS.map(({ name, mimeType }) => ({
14 | label: name,
15 | value: mimeType,
16 | }));
17 |
18 | function RawInputForm({ onSubmit }) {
19 | const [rawText, setRawText] = useState("");
20 | const [rawFormat, setRawFormat] = useState(FORMATS[0].mimeType || "");
21 |
22 | const handleSubmit = (ev) => {
23 | ev.preventDefault();
24 | onSubmit({
25 | data: rawText,
26 | type: rawFormat,
27 | });
28 | };
29 |
30 | return (
31 |
56 | );
57 | }
58 |
59 | RawInputForm.defaultProps = {
60 | onSubmit: () => {},
61 | };
62 |
63 | RawInputForm.propTypes = {
64 | onSubmit: PropTypes.func,
65 | };
66 |
67 | export default memo(RawInputForm);
68 |
--------------------------------------------------------------------------------
/admin/src/components/RawInputForm/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const EditorWrapper = styled.div`
4 | width: 100%;
5 | min-height: 200px;
6 | height: 200px;
7 |
8 | border: 1px solid #e3e9f3;
9 | border-radius: 4px;
10 |
11 | overflow: auto;
12 | resize: vertical;
13 |
14 | .editor {
15 | // color: #333740;
16 | // background-color: #ffffff;
17 |
18 | background: #1e1e1e;
19 | color: #fafafa;
20 |
21 | min-height: 100%;
22 |
23 | textarea:focus {
24 | outline: none;
25 | }
26 |
27 | pre {
28 | color: inherit;
29 |
30 | &::first-line {
31 | color: #f8c555;
32 | }
33 | }
34 | }
35 | `;
36 |
37 | export { EditorWrapper };
38 |
--------------------------------------------------------------------------------
/admin/src/components/UploadFileForm/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, memo } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import DropFileZone from "../DropFileZone";
5 | import DataViewer from "../DataViewer";
6 |
7 | import { Button } from "@buffetjs/core";
8 | import { Row } from "../common";
9 |
10 | import FORMATS from "../../constants/formats";
11 | import readFileContent from "../../utils/readFileContent";
12 |
13 | function UploadFileForm({ onSubmit }) {
14 | const [file, setFile] = useState(null);
15 | const [data, setData] = useState("");
16 |
17 | const handleFileUpload = async (file) => {
18 | try {
19 | const content = await readFileContent(file);
20 | setData(content);
21 | setFile(file);
22 | } catch (err) {
23 | strapi.notification.toggle({
24 | type: "warning",
25 | message: "import.file.content.error",
26 | });
27 | }
28 | };
29 |
30 | const removeFile = () => {
31 | setData(null);
32 | setFile(null);
33 | };
34 |
35 | // Form Controls
36 | const handleSubmit = (ev) => {
37 | ev.preventDefault();
38 | onSubmit({ data, type: file.type });
39 | };
40 |
41 | return (
42 |
76 | );
77 | }
78 |
79 | UploadFileForm.defaultProps = {
80 | onSubmit: () => {},
81 | };
82 |
83 | UploadFileForm.propTypes = {
84 | onSubmit: PropTypes.func.isRequired,
85 | };
86 |
87 | export default memo(UploadFileForm);
88 |
--------------------------------------------------------------------------------
/admin/src/components/common/Block/index.js:
--------------------------------------------------------------------------------
1 | import React, { memo } from "react";
2 | import PropTypes from "prop-types";
3 | import { Wrapper, Sub } from "./styles";
4 |
5 | function Block({ children, description, style, title }) {
6 | return (
7 |
8 |
9 |
10 | {!!title && {title}
} {!!description && {description}
}
11 |
12 | {children}
13 |
14 |
15 | );
16 | }
17 |
18 | Block.defaultProps = {
19 | children: null,
20 | description: null,
21 | style: {},
22 | title: null,
23 | };
24 |
25 | Block.propTypes = {
26 | children: PropTypes.any,
27 | description: PropTypes.string,
28 | style: PropTypes.object,
29 | title: PropTypes.string,
30 | };
31 | export default memo(Block);
32 |
--------------------------------------------------------------------------------
/admin/src/components/common/Block/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const Wrapper = styled.div`
4 | margin-bottom: 35px;
5 | background: #ffffff;
6 | padding: 22px 28px 18px;
7 | border-radius: 2px;
8 | box-shadow: 0 2px 4px #e3e9f3;
9 | -webkit-font-smoothing: antialiased;
10 |
11 | position: relative;
12 | `;
13 |
14 | const Sub = styled.div`
15 | padding-top: 0px;
16 | line-height: 18px;
17 | > p:first-child {
18 | margin-bottom: 1px;
19 | font-weight: 700;
20 | color: #333740;
21 | font-size: 1.8rem;
22 | }
23 | > p {
24 | color: #787e8f;
25 | font-size: 13px;
26 | }
27 | `;
28 |
29 | export { Wrapper, Sub };
30 |
--------------------------------------------------------------------------------
/admin/src/components/common/Loader/index.js:
--------------------------------------------------------------------------------
1 | import React, { memo } from "react";
2 | import { Wrapper } from "./styles";
3 | import { LoadingIndicator } from "@buffetjs/styles";
4 |
5 | function Loader() {
6 | return (
7 |
8 |
14 |
15 | );
16 | }
17 |
18 | export default memo(Loader);
19 |
--------------------------------------------------------------------------------
/admin/src/components/common/Loader/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const Wrapper = styled.div`
4 | position: absolute;
5 | top: 0;
6 | left: 0;
7 | right: 0;
8 | bottom: 0;
9 | z-index: 100;
10 |
11 | background-color: #00000010;
12 |
13 | display: flex;
14 | justify-content: center;
15 | align-items: center;
16 | `;
17 |
18 | export { Wrapper };
19 |
--------------------------------------------------------------------------------
/admin/src/components/common/Row.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const Row = styled.div.attrs(({ className, ...otherProps }) => ({
4 | ...otherProps,
5 | className: `row ${className || ""}`,
6 | }))`
7 | padding-top: 18px;
8 | `;
9 |
10 | export default Row;
11 |
--------------------------------------------------------------------------------
/admin/src/components/common/index.js:
--------------------------------------------------------------------------------
1 | export { default as Row } from "./Row";
2 | export { default as Block } from "./Block";
3 | export { default as Loader } from "./Loader";
4 |
--------------------------------------------------------------------------------
/admin/src/constants/formats.js:
--------------------------------------------------------------------------------
1 | const FORMATS = [
2 | { name: "csv", mimeType: "text/csv", ext: ".csv" },
3 | { name: "csv-excel", mimeType: "application/vnd.ms-excel", ext: ".csv" },
4 | { name: "json", mimeType: "application/json", ext: ".json" },
5 | ];
6 |
7 | export default FORMATS;
8 |
--------------------------------------------------------------------------------
/admin/src/constants/options.js:
--------------------------------------------------------------------------------
1 | const BASE_OPTIONS = [
2 | {
3 | name: "medias",
4 | type: "select",
5 | label: "Select how medias will be exported.",
6 | optionalValues: ["none", "ids", "url", "without-formats", "full"],
7 | defaultValue: "full",
8 | },
9 | {
10 | name: "relations",
11 | type: "select",
12 | label: "Select how relation will be exported.",
13 | optionalValues: ["none", "ids", "full"],
14 | defaultValue: "full",
15 | },
16 | {
17 | name: "ids",
18 | label: "Remove Ids",
19 | type: "boolean",
20 | defaultValue: false,
21 | },
22 | {
23 | name: "timestamp",
24 | label: "Remove TimeStamps",
25 | type: "boolean",
26 | defaultValue: false,
27 | }
28 | ];
29 |
30 | export default BASE_OPTIONS;
31 |
--------------------------------------------------------------------------------
/admin/src/containers/App/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * This component is the skeleton around the actual pages, and should only
4 | * contain code that should be seen on all pages. (e.g. navigation bar)
5 | *
6 | */
7 |
8 | import React from "react";
9 | import { Redirect, Route, Switch } from "react-router-dom";
10 | import Layout from "../../components/Layout";
11 | // Utils
12 | import pluginId from "../../pluginId";
13 | // Pages
14 | import ImportPage from "../ImportPage";
15 | import ExportPage from "../ExportPage";
16 |
17 | import useContentTypes from "../../hooks/useContentTypes";
18 |
19 | import "../../assets/prismjs.css";
20 |
21 | const pathTo = (uri = "") => `/plugins/${pluginId}/${uri}`;
22 | const navLinks = [
23 | {
24 | name: "Import Data",
25 | to: pathTo("import"),
26 | },
27 | {
28 | name: "Export Data",
29 | to: pathTo("export"),
30 | },
31 | ];
32 |
33 | function App() {
34 | const userContentTypes = useContentTypes();
35 |
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | {/* Default Route Retur to Import Page */}
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | export default App;
55 |
--------------------------------------------------------------------------------
/admin/src/containers/DataMapper/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useMemo, useCallback } from "react";
2 | import PropTypes from "prop-types";
3 | import { Prompt } from "react-router-dom";
4 |
5 | import { Button, Checkbox } from "@buffetjs/core";
6 | import { Loader, Row } from "../../components/common";
7 | import MappingTable from "../../components/MappingTable";
8 |
9 | import { request } from "strapi-helper-plugin";
10 | import pluginId from "../../pluginId";
11 |
12 | const filterIgnoreFields = (fieldName) =>
13 | ![
14 | "id",
15 | "created_at",
16 | "created_by",
17 | "updated_at",
18 | "updated_by",
19 | "published_at",
20 | ].includes(fieldName);
21 |
22 | function DataMapper({ analysis, target, onImport }) {
23 | const { fieldsInfo, parsedData } = analysis;
24 | const { kind, attributes, options } = target;
25 |
26 | const isSingleType = kind === "singleType";
27 | const [uploadAsDraft, setUploadAsDraft] = useState(options.draftAndPublish);
28 |
29 | const filteredAttributes = useMemo(
30 | () => Object.keys(attributes).filter(filterIgnoreFields),
31 | [attributes]
32 | );
33 |
34 | const [mappedFields, setMappedFields] = useState(() => {
35 | const fields = {};
36 | Object.keys(fieldsInfo).forEach((field) => {
37 | const { format } = fieldsInfo[field];
38 | const targetField = filteredAttributes.includes(field) ? field : "none";
39 | const targetFormat = attributes[targetField]
40 | ? attributes[targetField].type
41 | : null;
42 |
43 | fields[field] = { format, targetField, targetFormat };
44 | });
45 | return fields;
46 | });
47 |
48 | // Mapping Table Headers
49 | const headers = useMemo(
50 | () =>
51 | Object.keys(mappedFields).map((field) => ({
52 | name: field,
53 | format: mappedFields[field].format,
54 | value: mappedFields[field].targetField,
55 | })),
56 | [mappedFields]
57 | );
58 |
59 | // Options to Map
60 | const destinationOptions = useMemo(
61 | () =>
62 | [{ label: "None", value: "none" }].concat(
63 | filteredAttributes.map((field) => ({ label: field, value: field }))
64 | ),
65 | [filteredAttributes]
66 | );
67 |
68 | // Handler Mapping
69 | const selectDestinationField = useCallback(
70 | (source) => ({ target: { value } }) => {
71 | setMappedFields((fields) => ({
72 | ...fields,
73 | [source]: {
74 | ...fields[source],
75 | targetField: value,
76 | targetFormat: value !== "none" ? attributes[value].type : undefined,
77 | },
78 | }));
79 | },
80 | [attributes]
81 | );
82 |
83 | // Mapping Table Rows
84 | const [importItems, setImportItems] = useState(parsedData);
85 | const deleteItem = useCallback((deleteItem) => () =>
86 | setImportItems((items) => items.filter((item) => item !== deleteItem))
87 | );
88 |
89 | // Upload Data
90 | const [isLoading, setIsLoadig] = useState(false);
91 | const uploadData = async () => {
92 | // Prevent Upload Empty Data;
93 | if (importItems.length === 0) {
94 | strapi.notification.toggle({
95 | type: "warning",
96 | message: "import.items.empty",
97 | });
98 |
99 | // Finish with the import
100 | return onImport();
101 | }
102 |
103 | try {
104 | setIsLoadig(true);
105 | const { message } = await request(`/${pluginId}/import`, {
106 | method: "POST",
107 | body: {
108 | target,
109 | fields: mappedFields,
110 | items: importItems,
111 | asDraft: uploadAsDraft,
112 | },
113 | });
114 |
115 | strapi.notification.toggle({ type: "info", message });
116 | } catch (error) {
117 | console.log(error);
118 | strapi.notification.toggle({
119 | type: "warning",
120 | message: `import.items.error`,
121 | });
122 | }
123 |
124 | setIsLoadig(false);
125 | onImport();
126 | };
127 |
128 | return (
129 | <>
130 | {isLoading && }
131 |
132 |
133 |
134 | Map the Import Data to Destination Field
135 |
144 |
145 |
146 | Count of Items to Import:
147 | {kind === "singleType" ? 1 : importItems.length}
148 |
149 | {options.draftAndPublish && (
150 |
151 | setUploadAsDraft(!uploadAsDraft)}
156 | />
157 |
158 | )}
159 |
160 |
161 |
168 |
169 | >
170 | );
171 | }
172 |
173 | DataMapper.defaultProps = {
174 | analysis: {},
175 | target: {},
176 | onImport: () => {},
177 | };
178 |
179 | DataMapper.propTypes = {
180 | analysis: PropTypes.any,
181 | target: PropTypes.any,
182 | onImport: PropTypes.func,
183 | };
184 |
185 | export default DataMapper;
186 |
--------------------------------------------------------------------------------
/admin/src/containers/ExportPage/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * ExportPage
4 | *
5 | */
6 |
7 | import React, { memo, useState, useMemo } from "react";
8 | import PropTypes from "prop-types";
9 |
10 | import { Loader, Block, Row } from "../../components/common";
11 | import { Select, Label, Button } from "@buffetjs/core";
12 | import DataViewer from "../../components/DataViewer";
13 |
14 | import FORMATS from "../../constants/formats";
15 |
16 | import pluginId from "../../pluginId";
17 | import { request } from "strapi-helper-plugin";
18 | import { downloadFile, copyClipboard } from "../../utils/exportUtils";
19 |
20 | import { Collapse } from "reactstrap";
21 | import { FilterIcon } from "strapi-helper-plugin";
22 | import BASE_OPTIONS from "../../constants/options";
23 | import OptionsExport from "../../components/OptionsExport";
24 |
25 | const exportFormatsOptions = FORMATS.map(({ name, mimeType }) => ({
26 | label: name,
27 | value: mimeType,
28 | }));
29 |
30 | function ImportPage({ contentTypes }) {
31 | const [target, setTarget] = useState(null);
32 | const [sourceExports, setSourceExports] = useState("");
33 | const [exportFormat, setExportFormat] = useState("application/json");
34 | const [contentToExport, setContentToExport] = useState("");
35 |
36 | const sourceOptions = useMemo(
37 | () =>
38 | [{ label: "Select Export Source", value: "" }].concat(
39 | contentTypes.map(({ uid, info, apiID }) => ({
40 | label: info.label || apiID,
41 | value: uid,
42 | }))
43 | ),
44 | [contentTypes]
45 | );
46 |
47 | // Source Options Handler
48 | const handleSelectSourceExports = ({ target: { value } }) => {
49 | setSourceExports(value);
50 | setTarget(contentTypes.find(({ uid }) => uid === value));
51 | setContentToExport("");
52 | };
53 |
54 | // Source Options Handler
55 | const handleSelectExportFormat = ({ target: { value } }) => {
56 | setExportFormat(value);
57 | setContentToExport("");
58 | };
59 |
60 | // Options to exporting
61 | const [isOptionsOpen, setIsOptionsOpen] = useState(false);
62 | const [options, setOptions] = useState(
63 | BASE_OPTIONS.reduce((acc, { name, defaultValue }) => {
64 | acc[name] = defaultValue;
65 | return acc;
66 | }, {})
67 | );
68 |
69 | const handleChangeOptions = (option, value) => {
70 | setOptions({
71 | ...options,
72 | [option]: value,
73 | });
74 | };
75 |
76 | // Request to Get Available Content
77 | const [isLoading, setIsLoadig] = useState(false);
78 | const getContent = async () => {
79 | if (sourceExports === "")
80 | return strapi.notification.toggle({
81 | type: "warning",
82 | message: "export.source.empty",
83 | });
84 |
85 | try {
86 | setIsLoadig(true);
87 | const { data } = await request(`/${pluginId}/export`, {
88 | method: "POST",
89 | body: { target, type: exportFormat, options },
90 | });
91 |
92 | setContentToExport(data);
93 | } catch (error) {
94 | strapi.notification.toggle({
95 | type: "warning",
96 | message: `export.items.error`,
97 | });
98 | }
99 |
100 | setIsLoadig(false);
101 | };
102 |
103 | // Export Options
104 | const handleDownload = () => {
105 | downloadFile(target.info.name, contentToExport, exportFormat);
106 | };
107 | const handleCopy = () => copyClipboard(contentToExport);
108 |
109 | return (
110 |
115 | {isLoading && }
116 |
117 |
118 |
119 |
125 |
126 |
127 |
128 |
134 |
135 |
136 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
163 |
164 |
165 |
172 |
173 |
174 |
181 |
182 |
183 |
184 | );
185 | }
186 |
187 | ImportPage.defaultProps = {
188 | contentTypes: [],
189 | };
190 |
191 | ImportPage.propTypes = {
192 | contentTypes: PropTypes.array,
193 | };
194 |
195 | export default memo(ImportPage);
196 |
--------------------------------------------------------------------------------
/admin/src/containers/ImportPage/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * ImportPage
4 | *
5 | */
6 |
7 | import React, { memo, useState, useMemo } from "react";
8 | import PropTypes from "prop-types";
9 | import UploadFileForm from "../../components/UploadFileForm";
10 | import RawInputForm from "../../components/RawInputForm";
11 | import DataMapper from "../DataMapper";
12 |
13 | import { Loader, Block, Row } from "../../components/common";
14 | import { Select, Label } from "@buffetjs/core";
15 |
16 | import { request } from "strapi-helper-plugin";
17 | import pluginId from "../../pluginId";
18 |
19 | const importSourcesOptions = [
20 | { label: "Upload file", value: "upload" },
21 | { label: "Raw text", value: "raw" },
22 | ];
23 |
24 | function ImportPage({ contentTypes }) {
25 | // Import Source and Import Destination States
26 | const [importSource, setImportSource] = useState("upload");
27 | const [importDest, setImportDest] = useState("");
28 | const importDestOptions = useMemo(
29 | () =>
30 | [{ label: "Select Import Destination", value: "" }].concat(
31 | contentTypes.map(({ uid, info, apiID }) => ({
32 | label: info.label || apiID,
33 | value: uid,
34 | }))
35 | ),
36 | [contentTypes]
37 | );
38 |
39 | // Analysis
40 | const [analysis, setAnalysis] = useState(null);
41 | const [target, setTarget] = useState(null);
42 | const [isLoading, setIsLoadig] = useState(false);
43 | const analizeImports = async ({ data, type }) => {
44 | // Prevent Empty Destination
45 | if (importDest === "")
46 | return strapi.notification.toggle({
47 | type: "warning",
48 | message: "import.destination.empty",
49 | });
50 |
51 | // Send Request
52 | try {
53 | setIsLoadig(true);
54 | const response = await request(`/${pluginId}/pre-analyze`, {
55 | method: "POST",
56 | body: { data, type },
57 | });
58 |
59 | // Set Content Type Data to map
60 | setTarget(contentTypes.find(({ uid }) => uid === importDest));
61 | setAnalysis(response.data);
62 |
63 | // Notifications
64 | strapi.notification.toggle({
65 | type: "success",
66 | message: "import.analyze.success",
67 | });
68 | } catch (error) {
69 | console.error(error);
70 | strapi.notification.toggle({
71 | type: "warning",
72 | message: "import.analyze.error",
73 | });
74 | }
75 |
76 | setIsLoadig(false);
77 | };
78 |
79 | // Reset analysis and target
80 | const endImport = () => {
81 | setAnalysis(null);
82 | setTarget(null);
83 | };
84 |
85 | return (
86 |
91 | {analysis === null ? (
92 | <>
93 | {isLoading && }
94 |
95 |
96 |
97 |
104 |
105 |
106 |
113 |
114 | {importSource === "upload" ? (
115 |
116 | ) : (
117 |
118 | )}
119 | >
120 | ) : (
121 |
122 | )}
123 |
124 | );
125 | }
126 |
127 | ImportPage.defaultProps = {
128 | contentTypes: [],
129 | };
130 |
131 | ImportPage.propTypes = {
132 | contentTypes: PropTypes.array,
133 | };
134 |
135 | export default memo(ImportPage);
136 |
--------------------------------------------------------------------------------
/admin/src/containers/Initializer/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Initializer
4 | *
5 | */
6 |
7 | import { useEffect, useRef } from 'react';
8 | import PropTypes from 'prop-types';
9 | import pluginId from '../../pluginId';
10 |
11 | const Initializer = ({ updatePlugin }) => {
12 | const ref = useRef();
13 | ref.current = updatePlugin;
14 |
15 | useEffect(() => {
16 | ref.current(pluginId, 'isReady', true);
17 | }, []);
18 |
19 | return null;
20 | };
21 |
22 | Initializer.propTypes = {
23 | updatePlugin: PropTypes.func.isRequired,
24 | };
25 |
26 | export default Initializer;
27 |
--------------------------------------------------------------------------------
/admin/src/hooks/useContentTypes.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useContext } from "react";
2 | import { request, UserContext, hasPermissions } from "strapi-helper-plugin";
3 |
4 | const permissions = ({ uid }) =>
5 | ["create", "read", "update"].map((permission) => ({
6 | action: `plugins::content-manager.explorer.${permission}`,
7 | subject: uid,
8 | }));
9 |
10 | function useContentTypes() {
11 | const [contentTypes, setContentTypes] = useState([]);
12 | const userContextData = useContext(UserContext);
13 | const userPermissions = userContextData.userPermissions || userContextData;
14 |
15 | useEffect(() => {
16 | const fetchContentTypes = async () => {
17 | // Get All content Types
18 | const { data } = await request("/content-manager/content-types");
19 | const contentTypes = data.filter(({ uid }) =>
20 | uid.startsWith("application::")
21 | );
22 |
23 | // Get Permissions of each content Type
24 | const contentTypesWithPermissions = await Promise.all(
25 | contentTypes.map(async (contentType) => {
26 | const hasPermission = await hasPermissions(
27 | userPermissions,
28 | permissions(contentType)
29 | );
30 |
31 | return { ...contentType, hasPermission };
32 | })
33 | );
34 |
35 | // Filter each content Type by permissions
36 | const filteredContentTypes = contentTypesWithPermissions.filter(
37 | ({ hasPermission }) => hasPermission
38 | );
39 |
40 | // SetState
41 | setContentTypes(filteredContentTypes);
42 | };
43 |
44 | fetchContentTypes();
45 | }, [userPermissions]);
46 |
47 | return contentTypes;
48 | }
49 |
50 | export default useContentTypes;
51 |
--------------------------------------------------------------------------------
/admin/src/index.js:
--------------------------------------------------------------------------------
1 | import pluginPkg from "../../package.json";
2 | import pluginId from "./pluginId";
3 | import App from "./containers/App";
4 | import Initializer from "./containers/Initializer";
5 | import lifecycles from "./lifecycles";
6 | import trads from "./translations";
7 |
8 | export default (strapi) => {
9 | const pluginDescription =
10 | pluginPkg.strapi.description || pluginPkg.description;
11 | const icon = pluginPkg.strapi.icon;
12 | const name = pluginPkg.strapi.name;
13 |
14 | const plugin = {
15 | blockerComponent: null,
16 | blockerComponentProps: {},
17 | description: pluginDescription,
18 | icon,
19 | id: pluginId,
20 | initializer: Initializer,
21 | injectedComponents: [],
22 | isReady: false,
23 | isRequired: pluginPkg.strapi.required || false,
24 | layout: null,
25 | lifecycles,
26 | mainComponent: App,
27 | name,
28 | preventComponentRendering: false,
29 | trads,
30 | menu: {
31 | pluginsSectionLinks: [
32 | {
33 | destination: `/plugins/${pluginId}`,
34 | icon,
35 | label: {
36 | id: `${pluginId}.plugin.name`,
37 | defaultMessage: name,
38 | },
39 | name,
40 | permissions: [
41 | // Uncomment to set the permissions of the plugin here
42 | // {
43 | // action: '', // the action name should be plugins::plugin-name.actionType
44 | // subject: null,
45 | // },
46 | ],
47 | },
48 | ],
49 | },
50 | };
51 |
52 | return strapi.registerPlugin(plugin);
53 | };
54 |
--------------------------------------------------------------------------------
/admin/src/lifecycles.js:
--------------------------------------------------------------------------------
1 | function lifecycles() {}
2 |
3 | export default lifecycles;
4 |
--------------------------------------------------------------------------------
/admin/src/pluginId.js:
--------------------------------------------------------------------------------
1 | const pluginPkg = require('../../package.json');
2 | const pluginId = pluginPkg.name.replace(
3 | /^strapi-plugin-/i,
4 | ''
5 | );
6 |
7 | module.exports = pluginId;
8 |
--------------------------------------------------------------------------------
/admin/src/translations/ar.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/cs.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/de.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/admin/src/translations/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "import.mapper.unsaved": "",
3 | "import.destination.empty": "",
4 | "import.analyze.success": "",
5 | "import.analyze.error": "",
6 | "import.items.empty": "",
7 | "import.items.succesfully": "",
8 | "import.items.error": "",
9 | "import.file.content.error": "",
10 | "import.file.type.error": "",
11 | "export.source.empty": "",
12 | "export.items.error": "",
13 | "plugin.name": "Import / Export Content"
14 | }
15 |
--------------------------------------------------------------------------------
/admin/src/translations/es.json:
--------------------------------------------------------------------------------
1 | {
2 | "import.mapper.unsaved": "",
3 | "import.destination.empty": "",
4 | "import.analyze.success": "",
5 | "import.analyze.error": "",
6 | "import.items.empty": "",
7 | "import.items.succesfully": "",
8 | "import.items.error": "",
9 | "import.file.content.error": "",
10 | "import.file.type.error": "",
11 | "plugin.name": "Importar / Exportar Contenido"
12 | }
13 |
--------------------------------------------------------------------------------
/admin/src/translations/fr.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/id.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/index.js:
--------------------------------------------------------------------------------
1 | import ar from './ar.json';
2 | import cs from './cs.json';
3 | import de from './de.json';
4 | import en from './en.json';
5 | import es from './es.json';
6 | import fr from './fr.json';
7 | import id from './id.json';
8 | import it from './it.json';
9 | import ko from './ko.json';
10 | import ms from './ms.json';
11 | import nl from './nl.json';
12 | import pl from './pl.json';
13 | import ptBR from './pt-BR.json';
14 | import pt from './pt.json';
15 | import ru from './ru.json';
16 | import th from './th.json';
17 | import tr from './tr.json';
18 | import uk from './uk.json';
19 | import vi from './vi.json';
20 | import zhHans from './zh-Hans.json';
21 | import zh from './zh.json';
22 | import sk from './sk.json';
23 |
24 | const trads = {
25 | ar,
26 | cs,
27 | de,
28 | en,
29 | es,
30 | fr,
31 | id,
32 | it,
33 | ko,
34 | ms,
35 | nl,
36 | pl,
37 | 'pt-BR': ptBR,
38 | pt,
39 | ru,
40 | th,
41 | tr,
42 | uk,
43 | vi,
44 | 'zh-Hans': zhHans,
45 | zh,
46 | sk,
47 | };
48 |
49 | export default trads;
50 |
--------------------------------------------------------------------------------
/admin/src/translations/it.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/ko.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/ms.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/nl.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/pl.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/admin/src/translations/pt-BR.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/pt.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/ru.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/admin/src/translations/sk.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/th.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/tr.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/admin/src/translations/uk.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/admin/src/translations/vi.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/zh-Hans.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/admin/src/translations/zh.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/admin/src/utils/exportUtils.js:
--------------------------------------------------------------------------------
1 | import FORMATS from "../constants/formats";
2 |
3 | function downloadFile(name, content, type) {
4 | const format = FORMATS.find(({ mimeType }) => mimeType === type);
5 |
6 | const file = new Blob([content], { type });
7 | const fileURL = window.URL.createObjectURL(file);
8 |
9 | const dateTime = new Date().toLocaleDateString();
10 | const el = document.createElement("a");
11 | el.href = fileURL;
12 | el.download = `${name}_${dateTime}${format.ext}` || "file.txt";
13 | el.click();
14 | }
15 |
16 | async function copyClipboard(content) {
17 | if (navigator.clipboard) {
18 | try {
19 | await navigator.clipboard.writeText(content);
20 | } catch (err) {
21 | console.error("Failed to copy!", err);
22 | return strapi.notification.toggle({
23 | type: "warning",
24 | message: "Copy to Clipboard Error",
25 | });
26 | }
27 | } else {
28 | const el = document.createElement("textarea");
29 | el.textContent = content;
30 | el.select();
31 | document.execCommand("copy");
32 | }
33 |
34 | strapi.notification.toggle({
35 | type: "success",
36 | message: "Content Copy to Clipboard",
37 | });
38 | }
39 |
40 | export { downloadFile, copyClipboard };
41 |
--------------------------------------------------------------------------------
/admin/src/utils/formatFileContent.js:
--------------------------------------------------------------------------------
1 | function formatFileContent({ content, mimeType }) {
2 | switch (mimeType) {
3 | case "application/json":
4 | try {
5 | const jsonData = JSON.parse(content);
6 | return JSON.stringify(jsonData, null, 4);
7 | } catch (error) {
8 | return "";
9 | }
10 |
11 | case "text/csv":
12 | default:
13 | return content;
14 | }
15 | }
16 |
17 | export default formatFileContent;
18 |
--------------------------------------------------------------------------------
/admin/src/utils/getTrad.js:
--------------------------------------------------------------------------------
1 | import pluginId from '../pluginId';
2 |
3 | const getTrad = id => `${pluginId}.${id}`;
4 |
5 | export default getTrad;
6 |
--------------------------------------------------------------------------------
/admin/src/utils/highlight.js:
--------------------------------------------------------------------------------
1 | import {
2 | highlight as prismHighlight,
3 | languages as prismLanguages,
4 | } from "prismjs";
5 | import "prismjs/components/prism-json";
6 |
7 | import FORMATS from "../constants/formats";
8 |
9 | prismLanguages.csv = {
10 | value: /[^\r\n",]|"(?:[^"]|"")"(?!")/,
11 | punctuation: /[,;]/,
12 | };
13 |
14 | prismLanguages["csv-excel"] = {
15 | value: /[^\r\n",]|"(?:[^"]|"")"(?!")/,
16 | punctuation: /[,;]/,
17 | };
18 |
19 | const languages = FORMATS.reduce((langs, { name, mimeType }) => {
20 | langs[mimeType] = prismLanguages[name];
21 | return langs;
22 | }, {});
23 |
24 | const highlight = (code, mimetype) => prismHighlight(code, languages[mimetype]);
25 |
26 | export default highlight;
27 |
--------------------------------------------------------------------------------
/admin/src/utils/mediaFormat.js:
--------------------------------------------------------------------------------
1 | export function isUrlMedia(url) {
2 | try {
3 | const parsed = new URL(url);
4 | const format = parsed.pathname.split(".").pop().toLowerCase();
5 | switch (format) {
6 | case "png":
7 | case "gif":
8 | case "jpg":
9 | case "jpeg":
10 | case "svg":
11 | case "mp3":
12 | case "wav":
13 | case "ogg":
14 | case "mp4":
15 | case "ogg":
16 | case "webm":
17 | case "avi":
18 | return true;
19 | default:
20 | return false;
21 | }
22 | } catch (error) {
23 | return false;
24 | }
25 | }
26 |
27 | export function getMediaFormat(url) {
28 | const parsed = new URL(url);
29 | const format = parsed.pathname.split(".").pop().toLowerCase();
30 | switch (format) {
31 | case "png":
32 | case "gif":
33 | case "jpg":
34 | case "jpeg":
35 | case "svg":
36 | return { type: "image", format };
37 | case "mp3":
38 | case "wav":
39 | case "ogg":
40 | return { type: "audio", format };
41 | case "mp4":
42 | case "ogg":
43 | case "webm":
44 | case "avi":
45 | return { type: "video", format };
46 | default:
47 | break;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/admin/src/utils/readFileContent.js:
--------------------------------------------------------------------------------
1 | async function readFileContent(file) {
2 | const reader = new FileReader();
3 | return new Promise((resolve, reject) => {
4 | reader.onload = ({ target: { result } }) => resolve(result);
5 | reader.onerror = reject;
6 | reader.readAsText(file);
7 | });
8 | }
9 |
10 | export default readFileContent;
11 |
--------------------------------------------------------------------------------
/config/routes.json:
--------------------------------------------------------------------------------
1 | {
2 | "routes": [
3 | {
4 | "method": "GET",
5 | "path": "/",
6 | "handler": "import-export-content.index",
7 | "config": {
8 | "policies": []
9 | }
10 | },
11 | {
12 | "method": "POST",
13 | "path": "/pre-analyze",
14 | "handler": "import-export-content.preAnalyzeContent",
15 | "config": {
16 | "policies": []
17 | }
18 | },
19 | {
20 | "method": "POST",
21 | "path": "/import",
22 | "handler": "import-export-content.importItems",
23 | "config": {
24 | "policies": []
25 | }
26 | },
27 | {
28 | "method": "POST",
29 | "path": "/export",
30 | "handler": "import-export-content.exportItems",
31 | "config": {
32 | "policies": []
33 | }
34 | }
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/constants/contentTypes.js:
--------------------------------------------------------------------------------
1 | const { contentTypes } = require("strapi-utils");
2 | const { constants } = contentTypes;
3 | const {
4 | ID_ATTRIBUTE,
5 | PUBLISHED_AT_ATTRIBUTE,
6 | CREATED_BY_ATTRIBUTE,
7 | UPDATED_BY_ATTRIBUTE,
8 | SINGLE_TYPE,
9 | COLLECTION_TYPE,
10 | } = constants;
11 | module.exports = {
12 | ID_ATTRIBUTE,
13 | PUBLISHED_AT_ATTRIBUTE,
14 | CREATED_BY_ATTRIBUTE,
15 | UPDATED_BY_ATTRIBUTE,
16 | SINGLE_TYPE,
17 | COLLECTION_TYPE,
18 | };
19 |
--------------------------------------------------------------------------------
/constants/permissions.js:
--------------------------------------------------------------------------------
1 | const PERMISSIONS = {
2 | read: "plugins::content-manager.explorer.read",
3 | create: "plugins::content-manager.explorer.create",
4 | update: "plugins::content-manager.explorer.update",
5 | };
6 |
7 | module.exports = PERMISSIONS;
8 |
--------------------------------------------------------------------------------
/constants/relations.js:
--------------------------------------------------------------------------------
1 | const { relations } = require("strapi-utils");
2 | const { constants } = relations;
3 | const { MANY_RELATIONS } = constants;
4 | module.exports = { MANY_RELATIONS };
5 |
--------------------------------------------------------------------------------
/controllers/import-export-content.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const pluginPkg = require("../package.json");
4 | const PLUGIN_ID = pluginPkg.name.replace(/^strapi-plugin-/i, "");
5 |
6 | function getService(service = PLUGIN_ID) {
7 | const SERVICES = strapi.plugins[PLUGIN_ID].services;
8 | return SERVICES[service];
9 | }
10 |
11 | const PERMISSIONS = require("../constants/permissions");
12 |
13 | /**
14 | * import-export-content.js controller
15 | *
16 | * @description: A set of functions called "actions" of the `import-export-content` plugin.
17 | */
18 |
19 | module.exports = {
20 | /**
21 | * Default action.
22 | *
23 | * @return {Object}
24 | */
25 |
26 | index: async (ctx) => {
27 | ctx.send({ message: "ok" }); // Send 200 `ok`
28 | },
29 |
30 | preAnalyzeContent: async (ctx) => {
31 | const { data, type } = ctx.request.body;
32 | if (!data || !type) {
33 | return ctx.throw(400, "Required parameters missing");
34 | }
35 |
36 | try {
37 | const service = getService();
38 | const data = await service.preAnalyzeContent(ctx);
39 | ctx.send({ data, message: "ok" });
40 | } catch (error) {
41 | console.error(error);
42 | ctx.throw(406, `could not parse: ${error}`);
43 | }
44 | },
45 |
46 | importItems: async (ctx) => {
47 | const { target, fields, items } = ctx.request.body;
48 |
49 | if (!target || !fields || !items) {
50 | return ctx.throw(400, "Required parameters missing");
51 | }
52 |
53 | const { userAbility } = ctx.state;
54 | if (
55 | userAbility.cannot(PERMISSIONS.create, target.uid) &&
56 | userAbility.cannot(PERMISSIONS.update, target.uid)
57 | ) {
58 | return ctx.forbidden();
59 | }
60 |
61 | try {
62 | const service = getService();
63 | const results = await service.importItems(ctx);
64 | const succesfully = results.every((res) => res);
65 | ctx.send({
66 | succesfully,
67 | message: succesfully
68 | ? "All Data Imported"
69 | : results.some((res) => res)
70 | ? "Some Items Imported"
71 | : "No Items Imported",
72 | });
73 | } catch (error) {
74 | console.error(error);
75 | ctx.throw(406, `could not parse: ${error}`);
76 | }
77 | },
78 |
79 | exportItems: async (ctx) => {
80 | const { target, type, options } = ctx.request.body;
81 |
82 | if (!target || !type || !options) {
83 | return ctx.throw(400, "Required parameters missing");
84 | }
85 |
86 | const { userAbility } = ctx.state;
87 | if (userAbility.cannot(PERMISSIONS.read, target.uid)) {
88 | return ctx.forbidden();
89 | }
90 |
91 | try {
92 | const service = getService();
93 | const data = await service.exportItems(ctx);
94 | ctx.send({ data, message: "ok" });
95 | } catch (error) {
96 | console.error(error);
97 | ctx.throw(406, `could not parse: ${error}`);
98 | }
99 | },
100 | };
101 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "strapi-plugin-import-export-content",
3 | "version": "0.4.2",
4 | "description": "This is a plugin for import and export content of collection types.",
5 | "license": "MIT",
6 | "strapi": {
7 | "name": "Import / Export Content",
8 | "icon": "sync-alt",
9 | "description": "Import and Export your Content"
10 | },
11 | "scripts": {
12 | "test": "echo \"No test specified\""
13 | },
14 | "dependencies": {
15 | "csv-parse": "^4.15.3",
16 | "csv-string": "^4.0.1",
17 | "prismjs": "^1.23.0",
18 | "react-simple-code-editor": "^0.11.0"
19 | },
20 | "author": "Edison Peñuela ",
21 | "maintainers": [
22 | "Edison Peñuela "
23 | ],
24 | "engines": {
25 | "node": ">=10.16.0 <=14.x.x",
26 | "npm": ">=6.0.0"
27 | },
28 | "repository": {
29 | "type": "git",
30 | "url": "git+https://github.com/EdisonPeM/strapi-plugin-import-export-content.git"
31 | },
32 | "keywords": [
33 | "strapi",
34 | "plugin",
35 | "strapi",
36 | "import",
37 | "content",
38 | "export",
39 | "content"
40 | ],
41 | "bugs": {
42 | "url": "https://github.com/EdisonPeM/strapi-plugin-import-export-content/issues"
43 | },
44 | "homepage": "https://github.com/EdisonPeM/strapi-plugin-import-export-content#readme"
45 | }
46 |
--------------------------------------------------------------------------------
/services/analyzer.js:
--------------------------------------------------------------------------------
1 | const {
2 | getFieldsFromItems,
3 | getFormatFromField,
4 | } = require("./utils/fieldUtils");
5 |
6 | function analyze(items) {
7 | const fieldsFormats = {};
8 | const fieldNames = getFieldsFromItems(items);
9 | fieldNames.forEach((fieldName) => (fieldsFormats[fieldName] = []));
10 |
11 | items.forEach((item) => {
12 | fieldNames.forEach((fieldName) => {
13 | const fieldData = item[fieldName];
14 |
15 | // Get format from valid data
16 | if (fieldData !== null && fieldData !== undefined) {
17 | const fieldFormat = getFormatFromField(fieldData);
18 | fieldsFormats[fieldName].push(fieldFormat);
19 | }
20 | });
21 | });
22 |
23 | const fieldsInfo = {};
24 | Object.keys(fieldsFormats).map((fieldName) => {
25 | const fieldFormats = fieldsFormats[fieldName].map((value) =>
26 | value === "text" ? "string" : value
27 | );
28 | const uniqueFormats = new Set(fieldFormats);
29 | const format = uniqueFormats.size > 1 ? "mixed" : [...uniqueFormats][0];
30 |
31 | fieldsInfo[fieldName] = {
32 | count: fieldFormats.length,
33 | format,
34 | };
35 | });
36 |
37 | return fieldsInfo;
38 | }
39 |
40 | module.exports = { analyze };
41 |
--------------------------------------------------------------------------------
/services/contentParser/csvParser.js:
--------------------------------------------------------------------------------
1 | const {
2 | textIsNumber,
3 | textToNumber,
4 | textIsBoolean,
5 | textToBoolean,
6 | textIsObject,
7 | textToObject,
8 | } = require("./textFormats");
9 | const CsvParser = require("csv-parse/lib/sync");
10 | const CSV = require("csv-string");
11 |
12 | function csvToJson(text) {
13 | return CsvParser(text, {
14 | delimiter: CSV.detect(text),
15 | columns: true,
16 | trim: true,
17 |
18 | // Try to convert the format of the values
19 | cast: (value) => {
20 | if (value === "") return null;
21 | else if (textIsNumber(value)) return textToNumber(value);
22 | else if (textIsBoolean(value)) return textToBoolean(value);
23 | else if (textIsObject(value)) return textToObject(value);
24 | else return value;
25 | },
26 | });
27 | }
28 |
29 | function jsonToCsv(data, headers) {
30 | const escapeQuote = (text) => text.replace(/\"/g, '""');
31 | return headers
32 | .map((header) => {
33 | const element = data[header];
34 | if (element === null || element == undefined) return "";
35 |
36 | if (typeof element === "object") {
37 | const textObject = JSON.stringify(element);
38 | return `"${escapeQuote(textObject)}"`;
39 | }
40 |
41 | if (
42 | typeof element === "string" &&
43 | (element.includes("\n") ||
44 | element.includes(",") ||
45 | element.includes('"'))
46 | ) {
47 | return `"${escapeQuote(element)}"`;
48 | }
49 |
50 | return element;
51 | })
52 | .join();
53 | }
54 |
55 | module.exports = {
56 | csvToJson,
57 | jsonToCsv,
58 | };
59 |
--------------------------------------------------------------------------------
/services/contentParser/index.js:
--------------------------------------------------------------------------------
1 | const { csvToJson, jsonToCsv } = require("./csvParser");
2 | const { getFieldsFromItems } = require("../utils/fieldUtils");
3 |
4 | const toArray = (arr) => (Array.isArray(arr) ? arr : [arr]);
5 |
6 | function getItemsFromContent({ data, type }) {
7 | switch (type) {
8 | case "text/csv":
9 | case "application/vnd.ms-excel": {
10 | return csvToJson(data);
11 | }
12 |
13 | case "application/json":
14 | default: {
15 | return toArray(JSON.parse(data));
16 | }
17 | }
18 | }
19 |
20 | function getContentFromItems({ items, type }) {
21 | switch (type) {
22 | case "text/csv":
23 | case "application/vnd.ms-excel": {
24 | const mappedItems = toArray(items);
25 | const headers = getFieldsFromItems(mappedItems);
26 | const data = mappedItems
27 | .map((item) => jsonToCsv(item, headers))
28 | .join("\n");
29 |
30 | return `${headers.join()}\n${data}`;
31 | }
32 |
33 | case "application/json":
34 | default:
35 | return JSON.stringify(items);
36 | }
37 | }
38 |
39 | module.exports = {
40 | getItemsFromContent,
41 | getContentFromItems,
42 | };
43 |
--------------------------------------------------------------------------------
/services/contentParser/textFormats.js:
--------------------------------------------------------------------------------
1 | // ------------------------------------- //
2 | // Number Validation //
3 | // ------------------------------------- //
4 | function textIsNumber(value) {
5 | if (typeof value === "string" && value.trim() !== "") {
6 | return !isNaN(value);
7 | }
8 |
9 | return false;
10 | }
11 |
12 | function textToNumber(value) {
13 | return parseFloat(value);
14 | }
15 |
16 | // -------------------------------------- //
17 | // Boolean Validation //
18 | // -------------------------------------- //
19 | const booleanStringPossibleValues = {
20 | trueValues: ["true", "t"],
21 | falseValues: ["false", "f"],
22 | };
23 |
24 | function textIsBoolean(value) {
25 | return (
26 | booleanStringPossibleValues.trueValues.includes(value.toLowerCase()) ||
27 | booleanStringPossibleValues.falseValues.includes(value.toLowerCase())
28 | );
29 | }
30 |
31 | function textToBoolean(value) {
32 | return booleanStringPossibleValues.trueValues.includes(value);
33 | }
34 |
35 | // ------------------------------------- //
36 | // Object Validation //
37 | // ------------------------------------- //
38 | function textIsObject(value) {
39 | try {
40 | JSON.parse(value);
41 | return true;
42 | } catch {
43 | return false;
44 | }
45 | }
46 |
47 | function textToObject(value) {
48 | return JSON.parse(value);
49 | }
50 |
51 | // --------------------------- //
52 | // Exports //
53 | // --------------------------- //
54 | module.exports = {
55 | textIsNumber,
56 | textToNumber,
57 | textIsBoolean,
58 | textToBoolean,
59 | textIsObject,
60 | textToObject,
61 | };
62 |
--------------------------------------------------------------------------------
/services/exporter/exportUtils.js:
--------------------------------------------------------------------------------
1 | const ignoreFields = {
2 | ids: ["id"],
3 | timestamp: ["created_at", "updated_at", "published_at"],
4 | user: ["created_by", "updated_by"],
5 | };
6 |
7 | function mapMedias(media, options) {
8 | if (options.medias == "ids") {
9 | if (Array.isArray(media)) {
10 | return media.map(({ id }) => id);
11 | } else {
12 | return media.id;
13 | }
14 | }
15 |
16 | if (options.medias == "url") {
17 | if (Array.isArray(media)) {
18 | return media.map(({ url }) => url);
19 | } else {
20 | return media.url;
21 | }
22 | }
23 |
24 | if (options.medias == "without-formats") {
25 | if (Array.isArray(media)) {
26 | media.forEach((med) => delete med.formats);
27 | } else {
28 | delete media.formats;
29 | }
30 | }
31 |
32 | // options.medias == "full"
33 | if (Array.isArray(media)) {
34 | return media.map((med) => cleanFields(med, options, {}));
35 | } else {
36 | return cleanFields(media, options, {});
37 | }
38 | }
39 |
40 | function mapRelations(relation, options, attributes) {
41 | if (options.relations == "ids") {
42 | if (Array.isArray(relation)) {
43 | return relation.map(({ id }) => id);
44 | } else {
45 | return relation.id;
46 | }
47 | }
48 |
49 | // options.relations == "full"
50 | if (Array.isArray(relation)) {
51 | return relation.map((rel) => cleanFields(rel, options, attributes));
52 | } else {
53 | return cleanFields(relation, options, attributes);
54 | }
55 | }
56 |
57 | function cleanFields(item, options, attributes) {
58 | if (item === null || item === undefined) return;
59 |
60 | const mappedItem = { ...item };
61 | if (options.ids) {
62 | ignoreFields.ids.forEach((key) => {
63 | delete mappedItem[key];
64 | });
65 | }
66 |
67 | if (options.timestamp) {
68 | ignoreFields.timestamp.forEach((key) => {
69 | delete mappedItem[key];
70 | });
71 | }
72 |
73 | // Always true
74 | ignoreFields.user.forEach((key) => {
75 | delete mappedItem[key];
76 | });
77 |
78 | // -----------------------------
79 | Object.keys(attributes).forEach((itemKey) => {
80 | if (mappedItem[itemKey]) {
81 | const { type, model, collection } = attributes[itemKey];
82 | if (type === "media" || model == "file" || collection == "file") {
83 | if (options.medias == "none") {
84 | return delete mappedItem[itemKey];
85 | } else {
86 | mappedItem[itemKey] = mapMedias(mappedItem[itemKey], options);
87 | }
88 | }
89 |
90 | if (type === "relation") {
91 | const { attributes } = model
92 | ? strapi.models[model]
93 | : strapi.models[collection];
94 |
95 | if (options.relations == "none") {
96 | return delete mappedItem[itemKey];
97 | } else {
98 | mappedItem[itemKey] = mapRelations(
99 | mappedItem[itemKey],
100 | options,
101 | attributes
102 | );
103 | }
104 | }
105 |
106 | if (type == "component") {
107 | const { repeatable, component } = attributes[itemKey];
108 | const { attributes: componentAttributes } =
109 | strapi.components[component];
110 |
111 | if (repeatable) {
112 | mappedItem[itemKey] = mappedItem[itemKey].map((componentItem) =>
113 | cleanFields(componentItem, options, componentAttributes)
114 | );
115 | } else {
116 | mappedItem[itemKey] = cleanFields(
117 | mappedItem[itemKey],
118 | options,
119 | componentAttributes
120 | );
121 | }
122 | }
123 |
124 | if (type == "dynamiczone") {
125 | mappedItem[itemKey] = mappedItem[itemKey].map((dynamicItem) => {
126 | const { attributes: componentAttributes } =
127 | strapi.components[dynamicItem["__component"]];
128 | return cleanFields(dynamicItem, options, componentAttributes);
129 | });
130 | }
131 | }
132 | });
133 | // -----------------------------
134 |
135 | return mappedItem;
136 | }
137 |
138 | module.exports = { cleanFields };
139 |
--------------------------------------------------------------------------------
/services/exporter/index.js:
--------------------------------------------------------------------------------
1 | const PERMISSIONS = require("../../constants/permissions");
2 | const { cleanFields } = require("./exportUtils");
3 |
4 | async function getData(target, options, userAbility) {
5 | const { uid, attributes } = target;
6 | const permissionsManager =
7 | strapi.admin.services.permission.createPermissionsManager({
8 | ability: userAbility,
9 | model: uid,
10 | });
11 |
12 | // Filter content by permissions
13 | const query = permissionsManager.queryFrom({ _limit: -1 }, PERMISSIONS.read);
14 |
15 | const items = await strapi.entityService.find(
16 | { params: query },
17 | { model: uid }
18 | );
19 |
20 | return Array.isArray(items)
21 | ? items.map((item) => cleanFields(item, options, attributes))
22 | : [cleanFields(items, options, attributes)];
23 | }
24 |
25 | module.exports = {
26 | getData,
27 | };
28 |
--------------------------------------------------------------------------------
/services/import-export-content.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /**
4 | * import-export-content.js service
5 | *
6 | * @description: A set of functions similar to controller's actions to avoid code duplication.
7 | */
8 |
9 | const { getItemsFromContent, getContentFromItems } = require("./contentParser");
10 | const { analyze } = require("./analyzer");
11 |
12 | const { mapFieldsToTargetFields } = require("./utils/fieldUtils");
13 | const { importContent } = require("./importer");
14 | const {
15 | CREATED_BY_ATTRIBUTE,
16 | UPDATED_BY_ATTRIBUTE,
17 | PUBLISHED_AT_ATTRIBUTE,
18 | } = require("../constants/contentTypes");
19 |
20 | const { getData } = require("./exporter");
21 |
22 | module.exports = {
23 | preAnalyzeContent: (ctx) => {
24 | const { data, type } = ctx.request.body;
25 | const items = getItemsFromContent({ data, type });
26 | const fieldsInfo = analyze(items);
27 | return { fieldsInfo, parsedData: items };
28 | },
29 |
30 | importItems: async (ctx) => {
31 | const { user } = ctx.state;
32 | const { target, fields, items, asDraft } = ctx.request.body;
33 |
34 | const {
35 | attributes,
36 | options: { draftAndPublish },
37 | } = target;
38 |
39 | const mappedItems = await mapFieldsToTargetFields({
40 | items,
41 | fields,
42 | attributes,
43 | user,
44 | });
45 | return importContent(target, mappedItems, {
46 | [CREATED_BY_ATTRIBUTE]: user,
47 | [UPDATED_BY_ATTRIBUTE]: user,
48 | [PUBLISHED_AT_ATTRIBUTE]: draftAndPublish && asDraft ? null : Date.now(),
49 | });
50 | },
51 |
52 | exportItems: async (ctx) => {
53 | const { target, type, options } = ctx.request.body;
54 | const { userAbility } = ctx.state;
55 | const exportItems = await getData(target, options, userAbility);
56 |
57 | if (target.kind === "singleType") {
58 | return getContentFromItems({ items: exportItems[0], type });
59 | } else {
60 | return getContentFromItems({ items: exportItems, type });
61 | }
62 | },
63 | };
64 |
--------------------------------------------------------------------------------
/services/importer/importMediaFiles.js:
--------------------------------------------------------------------------------
1 | const { urlIsMedia } = require("../utils/formatsValidator");
2 | const request = require("request");
3 |
4 | const fetchFiles = (url) =>
5 | new Promise((resolve, reject) => {
6 | request({ url, method: "GET", encoding: null }, async (err, res, body) => {
7 | if (err) reject(err);
8 |
9 | const type = res.headers["content-type"].split(";").shift();
10 | const size = parseInt(res.headers["content-length"]) | 0;
11 |
12 | const parsed = new URL(url);
13 | const fullName = parsed.pathname.split("/").pop().toLowerCase();
14 |
15 | resolve({
16 | buffer: body,
17 | filename: fullName,
18 | name: fullName.replace(/\.[a-zA-Z]*$/, ""),
19 | type,
20 | size,
21 | });
22 | });
23 | });
24 |
25 | async function importMediaFromUrl(url, user) {
26 | if (!urlIsMedia(url)) return null;
27 |
28 | try {
29 | const mediaInfo = await fetchFiles(url);
30 | const fileInfo = {
31 | name: mediaInfo.name,
32 | alternativeText: "",
33 | caption: "",
34 | };
35 |
36 | const { optimize } = strapi.plugins.upload.services["image-manipulation"];
37 | const { buffer, info } = await optimize(mediaInfo.buffer);
38 |
39 | const uploadService = strapi.plugins.upload.services.upload;
40 | const formattedFile = uploadService.formatFileInfo(mediaInfo, fileInfo);
41 |
42 | const fileData = {
43 | ...formattedFile,
44 | ...info,
45 | buffer,
46 | };
47 |
48 | const uploadedFile = await uploadService.uploadFileAndPersist(fileData, {
49 | user,
50 | });
51 |
52 | if (uploadedFile && uploadedFile.id) return uploadedFile.id;
53 | return null;
54 | } catch (err) {
55 | console.error(err);
56 | return null;
57 | }
58 | }
59 |
60 | module.exports = {
61 | importMediaFromUrl,
62 | };
63 |
--------------------------------------------------------------------------------
/services/importer/importUtils.js:
--------------------------------------------------------------------------------
1 | const importToCollectionType = async (uid, item) => {
2 | try {
3 | await strapi.entityService.create({ data: item }, { model: uid });
4 | // await strapi.query(uid).create(item);
5 | return true;
6 | } catch (error) {
7 | return false;
8 | }
9 | };
10 |
11 | const importToSingleType = async (uid, item) => {
12 | try {
13 | const existing = await strapi.query(uid).find({});
14 | if (existing.length > 0) {
15 | const { id } = existing[0];
16 | delete item.created_by;
17 | await strapi.query(uid).update({ id }, item);
18 | } else {
19 | strapi.query(uid).create(item);
20 | }
21 | return [true];
22 | } catch (error) {
23 | return [false];
24 | }
25 | };
26 |
27 | module.exports = {
28 | importToCollectionType,
29 | importToSingleType,
30 | };
31 |
--------------------------------------------------------------------------------
/services/importer/index.js:
--------------------------------------------------------------------------------
1 | const {
2 | COLLECTION_TYPE,
3 | SINGLE_TYPE,
4 | } = require("../../constants/contentTypes");
5 | const { importToCollectionType, importToSingleType } = require("./importUtils");
6 |
7 | function importContent(target, items, options) {
8 | const { uid, kind } = target;
9 | switch (kind) {
10 | case COLLECTION_TYPE:
11 | return Promise.all(
12 | items.map((item) =>
13 | importToCollectionType(uid, {
14 | ...item,
15 | ...options,
16 | })
17 | )
18 | );
19 |
20 | case SINGLE_TYPE:
21 | return importToSingleType(uid, {
22 | ...items[0],
23 | ...options,
24 | });
25 |
26 | default:
27 | throw new Error("Tipe is not supported");
28 | }
29 | }
30 |
31 | module.exports = {
32 | importContent,
33 | };
34 |
--------------------------------------------------------------------------------
/services/utils/contentChecker.js:
--------------------------------------------------------------------------------
1 | const { MANY_RELATIONS } = require("../../constants/relations");
2 | const { urlIsMedia } = require("./formatsValidator");
3 | const { importMediaFromUrl } = require("../importer/importMediaFiles");
4 |
5 | function getId(value) {
6 | if (typeof value === "number") return value;
7 | if (typeof value === "object" && value.id) return value.id;
8 | return null;
9 | }
10 |
11 | async function getValidRelations(value, attribute) {
12 | const { relationType, targetModel } = attribute;
13 | if (MANY_RELATIONS.includes(relationType)) {
14 | const relations = Array.isArray(value) ? value : [value];
15 | const ids = relations.map(getId);
16 | const entities = await strapi.query(targetModel).find({ id_in: ids });
17 | return entities.map(({ id }) => id);
18 | } else {
19 | const relation = Array.isArray(value) ? value[0] : value;
20 | const id = getId(relation);
21 | const entity = await strapi.query(targetModel).findOne({ id });
22 | return entity ? entity.id : null;
23 | }
24 | }
25 |
26 | async function getValidMedia(value, attribute, user) {
27 | const { multiple } = attribute;
28 | if (multiple) {
29 | const medias = Array.isArray(value) ? value : [value];
30 | const urls = medias.filter((v) => urlIsMedia(v));
31 | const uploadedFiles = await Promise.all(
32 | urls.map((url) => importMediaFromUrl(url, user))
33 | );
34 |
35 | const ids = medias.map(getId).filter((v) => v !== null);
36 | const entities = await strapi.query("file", "upload").find({ id_in: ids });
37 |
38 | return [...uploadedFiles, ...entities.map(({ id }) => id)];
39 | } else {
40 | const media = Array.isArray(value) ? value[0] : value;
41 |
42 | // Upload url to plugin upload
43 | if (urlIsMedia(media)) {
44 | return importMediaFromUrl(media, user);
45 | }
46 |
47 | const id = getId(media);
48 | const entity = await strapi.query("file", "upload").findOne({ id });
49 | return entity ? entity.id : null;
50 | }
51 | }
52 |
53 | async function getValidSingleComponent(value, attributes, user) {
54 | const mappedComponent = {};
55 | for (const attr in attributes) {
56 | const element = value[attr];
57 | if (element) {
58 | let mappedElement = element;
59 | const { type, model, collection, plugin } = attributes[attr];
60 | if (plugin && plugin === "upload") {
61 | const multiple = collection && !model;
62 | mappedElement = await getValidMedia(element, { multiple }, user);
63 | } else if (model || collection) {
64 | const targetModel = collection || model;
65 | const relationType = collection && !model ? "manyWay" : "oneWay";
66 | mappedElement = await getValidRelations(element, {
67 | relationType,
68 | targetModel,
69 | });
70 | } else if (type === "component") {
71 | mappedElement = await getValidComponent(
72 | element,
73 | attributes[attr],
74 | user
75 | );
76 | }
77 |
78 | mappedComponent[attr] = mappedElement;
79 | }
80 | }
81 |
82 | return mappedComponent;
83 | }
84 | async function getValidComponent(value, attribute, user) {
85 | const { repeatable, component } = attribute;
86 | const { attributes } = strapi.components[component];
87 |
88 | if (repeatable) {
89 | const componentValues = Array.isArray(value) ? value : [value];
90 | return Promise.all(
91 | componentValues.map((val) =>
92 | getValidSingleComponent(val, attributes, user)
93 | )
94 | );
95 | } else {
96 | const componentValue = Array.isArray(value) ? value[0] : value;
97 | return getValidSingleComponent(componentValue, attributes, user);
98 | }
99 | }
100 |
101 | async function getValidDynamic(value, attribute, user) {
102 | const { components } = attribute;
103 | const dynamicValues = Array.isArray(value) ? value : [];
104 |
105 | return Promise.all(
106 | dynamicValues.map(async (dynamicComponent) => {
107 | const { __component } = dynamicComponent;
108 | if (
109 | !__component ||
110 | !components.includes(__component) ||
111 | !strapi.components[__component]
112 | ) {
113 | return null;
114 | }
115 |
116 | const { attributes } = strapi.components[__component];
117 | const content = await getValidSingleComponent(
118 | dynamicComponent,
119 | attributes,
120 | user
121 | );
122 | return { __component, ...content };
123 | })
124 | );
125 | }
126 |
127 | module.exports = {
128 | getValidRelations,
129 | getValidMedia,
130 | getValidComponent,
131 | getValidDynamic,
132 | };
133 |
--------------------------------------------------------------------------------
/services/utils/fieldUtils.js:
--------------------------------------------------------------------------------
1 | const {
2 | stringIsEmail,
3 | stringIsDate,
4 | stringIsHour,
5 | stringIsUrl,
6 | urlIsMedia,
7 | } = require("./formatsValidator");
8 | const {
9 | getValidRelations,
10 | getValidMedia,
11 | getValidComponent,
12 | getValidDynamic,
13 | } = require("./contentChecker");
14 |
15 | function getFormatFromField(field) {
16 | switch (typeof field) {
17 | case "number":
18 | return "number";
19 | case "boolean":
20 | return "boolean";
21 | case "object":
22 | if (Array.isArray(field)) return "array";
23 | return "object";
24 | case "string":
25 | if (stringIsEmail(field)) return "email";
26 | if (stringIsDate(field)) return "date";
27 | if (stringIsHour(field)) return "time";
28 | if (stringIsUrl(field)) {
29 | if (urlIsMedia(field)) return "media";
30 | return "url";
31 | }
32 | if (field.length > 255) return "text";
33 | return "string";
34 | }
35 | }
36 |
37 | function getFieldsFromItems(items) {
38 | const fieldNames = new Set();
39 | items.forEach((item) => {
40 | try {
41 | Object.keys(item).forEach((fieldName) => fieldNames.add(fieldName));
42 | } catch (err) {
43 | console.error(err);
44 | }
45 | });
46 |
47 | return Array.from(fieldNames);
48 | }
49 |
50 | function mapFieldsToTargetFields({ items, fields, attributes, user }) {
51 | const fieldNames = getFieldsFromItems(items);
52 | return Promise.all(
53 | items.map(async (item) => {
54 | const mappedItem = {};
55 |
56 | for (const fieldname of fieldNames) {
57 | const { targetField } = fields[fieldname];
58 | if (targetField && targetField !== "none") {
59 | const attribute = attributes[targetField];
60 | let targetItem = item[fieldname];
61 |
62 | // Validate ids and import medias
63 | if (attribute.type === "relation") {
64 | targetItem = await getValidRelations(targetItem, attribute);
65 | } else if (attribute.type === "media") {
66 | targetItem = await getValidMedia(targetItem, attribute, user);
67 | } else if (attribute.type === "component") {
68 | targetItem = await getValidComponent(targetItem, attribute, user);
69 | } else if (attribute.type === "dynamiczone") {
70 | targetItem = await getValidDynamic(targetItem, attribute, user);
71 | }
72 |
73 | mappedItem[targetField] = targetItem;
74 | }
75 | }
76 |
77 | return mappedItem;
78 | })
79 | );
80 | }
81 |
82 | module.exports = {
83 | getFormatFromField,
84 | getFieldsFromItems,
85 | mapFieldsToTargetFields,
86 | };
87 |
--------------------------------------------------------------------------------
/services/utils/formatsValidator.js:
--------------------------------------------------------------------------------
1 | const DATE_ISO_REGEXP =
2 | /^(\d{4})(-(\d{2}))??(-(\d{2}))??(T(\d{2}):(\d{2})(:(\d{2}))??(\.(\d+))??(([\+\-]{1}\d{2}:\d{2})|Z)??)??$/;
3 | function stringIsDate(data) {
4 | DATE_ISO_REGEXP.lastIndex = 0;
5 | return DATE_ISO_REGEXP.test(data);
6 | }
7 |
8 | // Validate String Formats
9 | const EMAIL_REGEXP =
10 | /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
11 | function stringIsEmail(data) {
12 | EMAIL_REGEXP.lastIndex = 0;
13 | return EMAIL_REGEXP.test(data);
14 | }
15 |
16 | const HOUR_REGEXP = /^((?:(?:0|1)\d|2[0-3])):([0-5]\d):([0-5]\d)(\.\d{0,3})?$/;
17 | function stringIsHour(data) {
18 | HOUR_REGEXP.lastIndex = 0;
19 | return HOUR_REGEXP.test(data);
20 | }
21 |
22 | const URL_REGEXP =
23 | /^[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?$/;
24 | function stringIsUrl(data) {
25 | URL_REGEXP.lastIndex = 0;
26 | return URL_REGEXP.test(data);
27 | }
28 |
29 | function urlIsMedia(url) {
30 | try {
31 | const parsed = new URL(url);
32 | const extension = parsed.pathname.split(".").pop().toLowerCase();
33 | switch (extension) {
34 | case "png":
35 | case "gif":
36 | case "jpg":
37 | case "jpeg":
38 | case "svg":
39 | case "bmp":
40 | case "tif":
41 | case "tiff":
42 | return true;
43 | case "mp3":
44 | case "wav":
45 | case "ogg":
46 | return true;
47 | case "mp4":
48 | case "avi":
49 | return true;
50 | default:
51 | return false;
52 | }
53 | } catch (error) {
54 | return false;
55 | }
56 | }
57 |
58 | module.exports = {
59 | stringIsEmail,
60 | stringIsDate,
61 | stringIsHour,
62 | stringIsUrl,
63 | urlIsMedia,
64 | };
65 |
--------------------------------------------------------------------------------