├── .editorconfig
├── .gitattributes
├── .gitignore
├── README.md
├── admin
└── src
│ ├── components
│ └── MappingTable
│ │ ├── MappingOptions.js
│ │ ├── TargetFieldSelect.js
│ │ └── index.js
│ ├── containers
│ ├── App
│ │ ├── actions.js
│ │ ├── constants.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── selectors.js
│ ├── CreateImportPage
│ │ ├── ExternalUrlForm
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ ├── InputFormatSettings
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ ├── RawInputForm
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ ├── UploadFileForm
│ │ │ └── index.js
│ │ ├── actions.js
│ │ ├── constants.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ ├── saga.js
│ │ ├── selectors.js
│ │ └── styles.scss
│ ├── HomePage
│ │ ├── actions.js
│ │ ├── constants.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ ├── saga.js
│ │ ├── selectors.js
│ │ └── styles.scss
│ └── NotFoundPage
│ │ └── index.js
│ ├── pluginId.js
│ └── translations
│ ├── ar.json
│ ├── de.json
│ ├── en.json
│ ├── es.json
│ ├── fr.json
│ ├── it.json
│ ├── ko.json
│ ├── nl.json
│ ├── pl.json
│ ├── pt-BR.json
│ ├── pt.json
│ ├── ru.json
│ ├── tr.json
│ ├── zh-Hans.json
│ └── zh.json
├── azure-pipelines.yml
├── config
├── functions
│ └── bootstrap.js
├── queries
│ ├── bookshelf.js
│ └── mongoose.js
├── routes.json
└── settings.json
├── controllers
└── ImportConfig.js
├── models
├── ImportConfig.js
├── ImportConfig.settings.json
├── Importeditem.js
└── Importeditem.settings.json
├── package.json
├── services
├── ImportConfig.js
├── ImportItems.js
├── UndoItems.js
└── utils
│ ├── FileDataResolver.js
│ ├── FileDataResolver.spec.js
│ ├── analyzer.js
│ ├── analyzer.spec.js
│ ├── fieldUtils.js
│ ├── fieldUtils.spec.js
│ ├── fileFromBuffer.js
│ ├── fileUtils.js
│ ├── fileUtils.spec.js
│ ├── getMediaUrlsFromFieldData.js
│ ├── getMediaUrlsFromFieldData.spec.js
│ ├── getUploadProvider.js
│ ├── importFields.js
│ ├── importFields.spec.js
│ ├── importMediaFiles.js
│ ├── stringIsEmail.js
│ ├── stringIsEmail.spec.js
│ ├── urlIsMedia.js
│ ├── urlIsMedia.spec.js
│ ├── validateUrl.js
│ └── validateUrl.spec.js
└── video_thumbnail.png
/.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 | admin/build
7 |
8 | # Cruft
9 | .DS_Store
10 | npm-debug.log
11 | .idea
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Strapi Import Content plugin
2 |
3 | Import RSS items to your content type with Strapi.
4 |
5 | [](https://dev.azure.com/joebeuckman0156/Strapi%20Plugins/_build/latest?definitionId=1&branchName=master)
6 |
7 | ### Installation
8 |
9 | ```
10 | cd my-strapi-project/plugins
11 | git clone https://github.com/jbeuckm/strapi-plugin-import-content.git import-content
12 | cd import-content && npm install
13 | cd ../..
14 | npm run setup --plugins
15 | ```
16 |
17 | _\* the last step takes a notoriously long time..._
18 |
19 | ### Configuration
20 |
21 | When plugin has been installed, you need to allow access to the endpoints.
22 |
23 | 1. Navigate to Users & Permissions.
24 | 2. Pick the role you would like to give permission.
25 | 3. Scroll down and expand the section **Import Content**.
26 | 4. Check "Select All" for the endpoints under "Importconfig".
27 | 5. Scroll up and press "Save"
28 |
29 | ### Usage
30 |
31 | Click for video demo:
32 | [](https://youtu.be/NOFioYMKPJk)
33 |
--------------------------------------------------------------------------------
/admin/src/components/MappingTable/MappingOptions.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import TargetFieldSelect from './TargetFieldSelect';
3 |
4 | const MappingOptions = ({ stat, onChange, targetModel }) => {
5 | return (
6 |
7 | {stat.format === 'xml' && (
8 |
9 |
10 | onChange({ stripTags: e.target.checked })}
13 | />
14 |
15 | )}
16 | {stat.hasMediaUrls && (
17 |
18 |
19 |
22 | onChange({ importMediaToField: targetField })
23 | }
24 | />
25 |
26 | )}
27 |
28 | );
29 | };
30 |
31 | export default MappingOptions;
32 |
--------------------------------------------------------------------------------
/admin/src/components/MappingTable/TargetFieldSelect.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const TargetFieldSelect = ({ targetModel, onChange }) => (
4 |
16 | );
17 |
18 | export default TargetFieldSelect;
19 |
--------------------------------------------------------------------------------
/admin/src/components/MappingTable/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import MappingOptions from './MappingOptions';
4 | import TargetFieldSelect from './TargetFieldSelect';
5 | import _ from 'lodash';
6 |
7 | class MappingTable extends Component {
8 | state = { mapping: {} };
9 |
10 | changeMappingOptions = stat => options => {
11 | console.log(stat, options);
12 |
13 | let newState = _.cloneDeep(this.state);
14 | for (let key in options) {
15 | _.set(newState, `mapping[${stat.fieldName}][${key}]`, options[key]);
16 | }
17 | this.setState(newState, () => this.props.onChange(this.state.mapping));
18 | };
19 |
20 | setMapping = (source, targetField) => {
21 | const state = _.set(
22 | this.state,
23 | `mapping[${source}]['targetField']`,
24 | targetField
25 | );
26 | this.setState(state, () => this.props.onChange(this.state.mapping));
27 | };
28 |
29 | render() {
30 | const { analysis, targetModel } = this.props;
31 | return (
32 |
33 |
34 |
Found {analysis.itemCount} items...
35 |
36 |
37 | Field Name |
38 | Count |
39 | Format |
40 | Min Length |
41 | Max Length |
42 | Avg Length |
43 | Options |
44 | Destination |
45 |
46 | {analysis.fieldStats.map(stat => (
47 |
48 | {stat.fieldName} |
49 | {stat.count} |
50 | {stat.format} |
51 | {stat.minLength} |
52 | {stat.maxLength} |
53 | {stat.meanLength} |
54 |
55 |
60 | |
61 |
62 | {targetModel && (
63 |
66 | this.setMapping(stat.fieldName, targetField)
67 | }
68 | />
69 | )}
70 | |
71 |
72 | ))}
73 |
74 |
75 |
76 | );
77 | }
78 | }
79 |
80 | MappingTable.propTypes = {
81 | analysis: PropTypes.object.isRequired,
82 | targetModel: PropTypes.object,
83 | onChange: PropTypes.func
84 | };
85 |
86 | export default MappingTable;
87 |
--------------------------------------------------------------------------------
/admin/src/containers/App/actions.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * App actions
4 | *
5 | */
6 |
--------------------------------------------------------------------------------
/admin/src/containers/App/constants.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * App constants
4 | *
5 | */
6 |
--------------------------------------------------------------------------------
/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 PropTypes from 'prop-types';
10 | import { connect } from 'react-redux';
11 | import { createStructuredSelector } from 'reselect';
12 | import { Switch, Route } from 'react-router-dom';
13 | import { compose } from 'redux';
14 |
15 | import pluginId from 'pluginId';
16 |
17 | import HomePage from 'containers/HomePage';
18 | import NotFoundPage from 'containers/NotFoundPage';
19 | import CreateImportPage from 'containers/CreateImportPage';
20 |
21 | import reducer from './reducer';
22 |
23 | class App extends React.Component {
24 | render() {
25 | return (
26 |
27 |
28 |
29 |
34 |
35 |
36 |
37 | );
38 | }
39 | }
40 |
41 | App.contextTypes = {
42 | plugins: PropTypes.object,
43 | updatePlugin: PropTypes.func
44 | };
45 |
46 | App.propTypes = {
47 | history: PropTypes.object.isRequired
48 | };
49 |
50 | const mapStateToProps = createStructuredSelector({});
51 |
52 | const withConnect = connect(mapStateToProps);
53 | const withReducer = strapi.injectReducer({ key: 'global', reducer, pluginId });
54 |
55 | export default compose(
56 | withReducer,
57 | withConnect
58 | )(App);
59 |
--------------------------------------------------------------------------------
/admin/src/containers/App/reducer.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * App reducer
4 | *
5 | */
6 |
7 | import { fromJS } from 'immutable';
8 |
9 | const initialState = fromJS({});
10 |
11 | function appReducer(state = initialState, action) {
12 | switch (action.type) {
13 | default:
14 | return state;
15 | }
16 | }
17 |
18 | export default appReducer;
19 |
--------------------------------------------------------------------------------
/admin/src/containers/App/selectors.js:
--------------------------------------------------------------------------------
1 | // import { createSelector } from 'reselect';
2 | // import pluginId from 'pluginId';
3 |
4 | /**
5 | * Direct selector to the list state domain
6 | */
7 |
8 | // const selectGlobalDomain = () => state => state.get(`${pluginId}_global`);
9 |
10 | export {};
11 |
--------------------------------------------------------------------------------
/admin/src/containers/CreateImportPage/ExternalUrlForm/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { injectIntl } from 'react-intl';
4 | import Label from 'components/Label';
5 |
6 | import styles from './styles.scss';
7 |
8 | export class ExternalUrlForm extends Component {
9 | state = {
10 | url: null
11 | };
12 |
13 | preAnalyzeImportFile = async event => {
14 | const url = event.target.value;
15 |
16 | this.props.onRequestAnalysis({ source: 'url', options: { url } });
17 | };
18 |
19 | render() {
20 | const { loadingAnalysis } = this.props;
21 |
22 | return (
23 |
24 |
25 |
30 |
31 | );
32 | }
33 | }
34 |
35 | ExternalUrlForm.propTypes = {
36 | onRequestAnalysis: PropTypes.func.isRequired,
37 | loadingAnalysis: PropTypes.bool.isRequired
38 | };
39 |
40 | export default injectIntl(ExternalUrlForm);
41 |
--------------------------------------------------------------------------------
/admin/src/containers/CreateImportPage/ExternalUrlForm/styles.scss:
--------------------------------------------------------------------------------
1 | .urlInput {
2 | width: 400px;
3 | margin-left: 10px;
4 | background-color: #bbb;
5 | }
6 |
--------------------------------------------------------------------------------
/admin/src/containers/CreateImportPage/InputFormatSettings/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { injectIntl } from 'react-intl';
4 | import _ from 'lodash';
5 |
6 | import Label from 'components/Label';
7 | import styles from './styles.scss';
8 |
9 | export class InputFormatSettings extends Component {
10 | onChangeOption = option => event => {
11 | const settings = _.clone(this.props.settings);
12 | settings[option] = event.target.value;
13 |
14 | this.props.onChange(settings);
15 | };
16 |
17 | render() {
18 | const { type, settings } = this.props;
19 |
20 | return (
21 |
22 | {type === 'csv' && (
23 |
24 |
25 |
30 |
31 |
38 |
39 | )}
40 |
41 | );
42 | }
43 | }
44 |
45 | InputFormatSettings.propTypes = {
46 | onChange: PropTypes.func.isRequired,
47 | type: PropTypes.string.isRequired,
48 | settings: PropTypes.object.isRequired
49 | };
50 |
51 | export default injectIntl(InputFormatSettings);
52 |
--------------------------------------------------------------------------------
/admin/src/containers/CreateImportPage/InputFormatSettings/styles.scss:
--------------------------------------------------------------------------------
1 | .settingsInputField {
2 | background-color: #fff;
3 | margin-left: 10px;
4 | margin-left: 15px;
5 | width: 50px;
6 | padding-left: 7px;
7 | padding-right: 7px;
8 | }
9 |
--------------------------------------------------------------------------------
/admin/src/containers/CreateImportPage/RawInputForm/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { injectIntl } from 'react-intl';
4 | import Label from 'components/Label';
5 | import InputTextArea from 'components/InputTextArea';
6 | import InputSelect from 'components/InputSelect';
7 | import Button from 'components/Button';
8 | import InputSpacer from 'components/InputSpacer';
9 |
10 | import styles from './styles.scss';
11 |
12 | export class RawInputForm extends Component {
13 | state = {
14 | rawText: '',
15 | dataFormat: 'text/csv'
16 | };
17 |
18 | dataFormats = [{ label: 'csv', value: 'text/csv' }];
19 |
20 | changeDataFormat = event => {
21 | this.setState({ dataFormat: event.target.value });
22 | };
23 |
24 | textChanged = async event => {
25 | const rawText = event.target.value;
26 | this.setState({ rawText });
27 | };
28 |
29 | clickAnalyze = () => {
30 | const { dataFormat, rawText } = this.state;
31 |
32 | this.props.onRequestAnalysis({
33 | source: 'raw',
34 | type: dataFormat,
35 | options: { rawText }
36 | });
37 | };
38 |
39 | render() {
40 | const { loadingAnalysis } = this.props;
41 | const { dataFormat, rawText } = this.state;
42 |
43 | return (
44 |
45 |
46 |
47 |
48 |
49 | |
50 |
51 |
57 | |
58 |
59 |
65 | |
66 |
67 |
68 |
69 |
78 |
79 | );
80 | }
81 | }
82 |
83 | RawInputForm.propTypes = {
84 | onRequestAnalysis: PropTypes.func.isRequired,
85 | loadingAnalysis: PropTypes.bool.isRequired
86 | };
87 |
88 | export default injectIntl(RawInputForm);
89 |
--------------------------------------------------------------------------------
/admin/src/containers/CreateImportPage/RawInputForm/styles.scss:
--------------------------------------------------------------------------------
1 | .rawTextInput {
2 | width: 100%;
3 | margin-left: 10px;
4 | background-color: #bbb;
5 | }
6 |
--------------------------------------------------------------------------------
/admin/src/containers/CreateImportPage/UploadFileForm/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { injectIntl } from 'react-intl';
4 |
5 | import Button from 'components/Button';
6 | import Label from 'components/Label';
7 | import InputSpacer from 'components/InputSpacer';
8 |
9 | function readFileContent(file) {
10 | const reader = new FileReader();
11 | return new Promise((resolve, reject) => {
12 | reader.onload = event => resolve(event.target.result);
13 | reader.onerror = reject;
14 | reader.readAsText(file);
15 | });
16 | }
17 |
18 | export class UploadFileForm extends Component {
19 | state = {
20 | source: 'url',
21 | file: null,
22 | type: null,
23 | options: {
24 | filename: null
25 | }
26 | };
27 |
28 | onChangeImportFile = event => {
29 | const file = event.target.files[0];
30 |
31 | this.setState({
32 | file,
33 | type: file.type,
34 | options: {
35 | ...this.state.options,
36 | filename: file.name
37 | }
38 | });
39 | };
40 |
41 | clickAnalyzeUploadFile = async () => {
42 | const { file, options } = this.state;
43 |
44 | const data = await readFileContent(file);
45 |
46 | this.props.onRequestAnalysis({
47 | source: 'upload',
48 | type: file.type,
49 | options,
50 | data
51 | });
52 | };
53 |
54 | onChangeOption = option => event => {
55 | this.setState({
56 | options: { ...this.state.options, [option]: event.target.value }
57 | });
58 | };
59 |
60 | render() {
61 | const { loadingAnalysis } = this.props;
62 |
63 | return (
64 |
83 | );
84 | }
85 | }
86 |
87 | UploadFileForm.propTypes = {
88 | onRequestAnalysis: PropTypes.func.isRequired,
89 | loadingAnalysis: PropTypes.bool.isRequired
90 | };
91 |
92 | export default injectIntl(UploadFileForm);
93 |
--------------------------------------------------------------------------------
/admin/src/containers/CreateImportPage/actions.js:
--------------------------------------------------------------------------------
1 | import {
2 | LOAD_MODELS,
3 | LOAD_MODELS_SUCCESS,
4 | LOAD_MODELS_ERROR,
5 | PRE_ANALYZE,
6 | PRE_ANALYZE_SUCCESS,
7 | PRE_ANALYZE_ERROR,
8 | SAVE_IMPORT_CONFIG,
9 | SAVE_IMPORT_CONFIG_ERROR,
10 | SAVE_IMPORT_CONFIG_SUCCESS
11 | } from "./constants";
12 |
13 | export const loadModels = () => ({
14 | type: LOAD_MODELS
15 | });
16 | export const loadModelsSuccess = models => ({
17 | type: LOAD_MODELS_SUCCESS,
18 | payload: { models }
19 | });
20 | export const loadModelsError = error => ({
21 | type: LOAD_MODELS_ERROR,
22 | error: true,
23 | payload: error
24 | });
25 |
26 | export const preAnalyze = importConfig => ({
27 | type: PRE_ANALYZE,
28 | payload: { importConfig }
29 | });
30 | export const preAnalyzeSuccess = analysis => ({
31 | type: PRE_ANALYZE_SUCCESS,
32 | payload: { analysis }
33 | });
34 | export const preAnalyzeError = error => ({
35 | type: PRE_ANALYZE_ERROR,
36 | error: true,
37 | payload: error
38 | });
39 |
40 | export const saveImportConfig = importConfig => ({
41 | type: SAVE_IMPORT_CONFIG,
42 | payload: { importConfig }
43 | });
44 | export const saveImportConfigSuccess = saved => ({
45 | type: SAVE_IMPORT_CONFIG_SUCCESS,
46 | payload: { saved }
47 | });
48 | export const saveImportConfigError = error => ({
49 | type: SAVE_IMPORT_CONFIG_ERROR,
50 | error: true,
51 | payload: error
52 | });
53 |
--------------------------------------------------------------------------------
/admin/src/containers/CreateImportPage/constants.js:
--------------------------------------------------------------------------------
1 | import pluginId from "pluginId";
2 |
3 | const buildString = suffix => `${pluginId}/CreateImportPage/${suffix}`;
4 |
5 | export const LOAD_MODELS = buildString("LOAD_MODELS");
6 | export const LOAD_MODELS_SUCCESS = buildString("LOAD_MODELS_SUCCESS");
7 | export const LOAD_MODELS_ERROR = buildString("LOAD_MODELS_ERROR");
8 |
9 | export const PRE_ANALYZE = buildString("PRE_ANALYZE");
10 | export const PRE_ANALYZE_SUCCESS = buildString("PRE_ANALYZE_SUCCESS");
11 | export const PRE_ANALYZE_ERROR = buildString("PRE_ANALYZE_ERROR");
12 |
13 | export const SAVE_IMPORT_CONFIG = buildString("SAVE_IMPORT_CONFIG");
14 | export const SAVE_IMPORT_CONFIG_SUCCESS = buildString(
15 | "SAVE_IMPORT_CONFIG_SUCCESS"
16 | );
17 | export const SAVE_IMPORT_CONFIG_ERROR = buildString("SAVE_IMPORT_CONFIG_ERROR");
18 |
--------------------------------------------------------------------------------
/admin/src/containers/CreateImportPage/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { createStructuredSelector } from 'reselect';
5 | import { injectIntl } from 'react-intl';
6 | import { compose } from 'redux';
7 | import pluginId from 'pluginId';
8 |
9 | import Button from 'components/Button';
10 | import PluginHeader from 'components/PluginHeader';
11 | import InputSelect from 'components/InputSelect';
12 | import InputSpacer from 'components/InputSpacer';
13 | import Label from 'components/Label';
14 |
15 | import ExternalUrlForm from './ExternalUrlForm';
16 | import UploadFileForm from './UploadFileForm';
17 | import RawInputForm from './RawInputForm';
18 | import InputFormatSettings from './InputFormatSettings';
19 | import MappingTable from '../../components/MappingTable';
20 |
21 | import styles from './styles.scss';
22 | import { loadModels, preAnalyze, saveImportConfig } from './actions';
23 | import {
24 | makeSelectLoading,
25 | makeSelectModels,
26 | makeSelectAnalyzing,
27 | makeSelectAnalysis,
28 | makeSelectCreated,
29 | makeSelectSaving
30 | } from './selectors';
31 | import reducer from './reducer';
32 | import saga from './saga';
33 |
34 | export class CreateImportPage extends Component {
35 | importSources = [
36 | { label: 'External URL ', value: 'url' },
37 | { label: 'Upload file', value: 'upload' },
38 | { label: 'Raw text', value: 'raw' }
39 | ];
40 |
41 | state = {
42 | importSource: 'url',
43 | analysisConfig: null,
44 | selectedContentType: null,
45 | fieldMapping: {},
46 | inputFormatSettings: { delimiter: ',', skipRows: 0 }
47 | };
48 |
49 | componentDidMount() {
50 | this.props.loadModels();
51 | }
52 |
53 | componentWillReceiveProps(nextProps) {
54 | if (nextProps.models && !this.state.selectedContentType) {
55 | this.setState({ selectedContentType: nextProps.models[0].name });
56 | }
57 | if (!this.props.created && nextProps.created) {
58 | this.props.history.push(`/plugins/${pluginId}`);
59 | }
60 | }
61 |
62 | getAnalysisConfigWithSettings = analysisConfig => {
63 | const { inputFormatSettings } = this.state;
64 |
65 | return {
66 | ...analysisConfig,
67 | options: {
68 | ...analysisConfig.options,
69 | ...inputFormatSettings
70 | }
71 | };
72 | };
73 |
74 | onRequestAnalysis = async analysisConfig => {
75 | this.analysisConfig = analysisConfig;
76 |
77 | const analysisConfigWithSettings = this.getAnalysisConfigWithSettings(
78 | analysisConfig
79 | );
80 |
81 | this.props.preAnalyze(analysisConfigWithSettings);
82 | };
83 |
84 | selectContentType = event => {
85 | const selectedContentType = event.target.value;
86 |
87 | this.setState({ selectedContentType });
88 | };
89 |
90 | getTargetModel = () => {
91 | const { models } = this.props;
92 | if (!models) return null;
93 |
94 | return models.find(model => model.name === this.state.selectedContentType);
95 | };
96 |
97 | setFieldMapping = fieldMapping => {
98 | this.setState({ fieldMapping });
99 | };
100 |
101 | onSaveImport = () => {
102 | const { selectedContentType, fieldMapping } = this.state;
103 | const { analysisConfig } = this;
104 |
105 | const analysisConfigWithSettings = this.getAnalysisConfigWithSettings(
106 | analysisConfig
107 | );
108 |
109 | const importConfig = {
110 | ...analysisConfigWithSettings,
111 | contentType: selectedContentType,
112 | fieldMapping
113 | };
114 |
115 | this.props.saveImportConfig(importConfig);
116 | };
117 |
118 | selectImportSource = event => {
119 | this.setState({ importSource: event.target.value });
120 | };
121 |
122 | updateInputFormatSettings = newSettings => {
123 | this.setState({ inputFormatSettings: newSettings });
124 | };
125 |
126 | render() {
127 | const { models, loading, loadingAnalysis, saving, analysis } = this.props;
128 |
129 | const { importSource, inputFormatSettings, fieldMapping } = this.state;
130 |
131 | const saveDisabled = loading || saving || fieldMapping === {};
132 |
133 | const modelOptions =
134 | models &&
135 | models.map(({ name }) => ({
136 | label: name,
137 | value: name
138 | }));
139 |
140 | return (
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
155 | |
156 |
157 | {loading && Loading content types... }
158 | {modelOptions && (
159 |
160 |
161 |
165 |
166 | )}
167 | |
168 |
169 |
170 |
171 |
172 |
173 | {importSource === 'upload' && (
174 |
178 | )}
179 |
180 | {importSource === 'url' && (
181 |
185 | )}
186 |
187 | {importSource === 'raw' && (
188 |
192 | )}
193 |
194 |
195 |
200 |
201 | {loadingAnalysis &&
Analyzing...
}
202 |
203 |
204 |
205 |
206 |
207 |
208 | {analysis && (
209 |
214 | )}
215 |
216 |
217 |
218 |
224 |
225 |
226 |
227 | );
228 | }
229 | }
230 |
231 | CreateImportPage.contextTypes = {
232 | router: PropTypes.object
233 | };
234 |
235 | CreateImportPage.propTypes = {
236 | models: PropTypes.object.isRequired,
237 | loadModels: PropTypes.func.isRequired,
238 | preAnalyze: PropTypes.func.isRequired,
239 | loading: PropTypes.bool.isRequired,
240 | saving: PropTypes.bool.isRequired,
241 | created: PropTypes.object.isRequired
242 | };
243 |
244 | const mapDispatchToProps = {
245 | loadModels,
246 | preAnalyze,
247 | saveImportConfig
248 | };
249 |
250 | const mapStateToProps = createStructuredSelector({
251 | loading: makeSelectLoading(),
252 | models: makeSelectModels(),
253 | loadingAnalysis: makeSelectAnalyzing(),
254 | analysis: makeSelectAnalysis(),
255 | created: makeSelectCreated(),
256 | saving: makeSelectSaving()
257 | });
258 |
259 | const withConnect = connect(
260 | mapStateToProps,
261 | mapDispatchToProps
262 | );
263 |
264 | const withReducer = strapi.injectReducer({
265 | key: 'createImportPage',
266 | reducer,
267 | pluginId
268 | });
269 |
270 | const withSaga = strapi.injectSaga({ key: 'createImportPage', saga, pluginId });
271 |
272 | export default compose(
273 | withReducer,
274 | withSaga,
275 | withConnect
276 | )(injectIntl(CreateImportPage));
277 |
--------------------------------------------------------------------------------
/admin/src/containers/CreateImportPage/reducer.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from "immutable";
2 |
3 | import {
4 | LOAD_MODELS,
5 | LOAD_MODELS_SUCCESS,
6 | LOAD_MODELS_ERROR,
7 | PRE_ANALYZE,
8 | PRE_ANALYZE_SUCCESS,
9 | PRE_ANALYZE_ERROR,
10 | SAVE_IMPORT_CONFIG,
11 | SAVE_IMPORT_CONFIG_SUCCESS,
12 | SAVE_IMPORT_CONFIG_ERROR
13 | } from "./constants";
14 |
15 | const initialState = fromJS({
16 | loading: false,
17 | models: null,
18 | error: null,
19 |
20 | analyzing: false,
21 | analysis: null,
22 | preAnalyzeError: null,
23 |
24 | saving: false,
25 | created: null,
26 | saveError: null
27 | });
28 |
29 | function createImportPageReducer(state = initialState, action) {
30 | const { type, payload } = action;
31 |
32 | switch (type) {
33 | case LOAD_MODELS:
34 | return state.set("loading", true);
35 | case LOAD_MODELS_SUCCESS: {
36 | const filtered = payload.models.filter(
37 | model => !["importconfig", "importeditem"].includes(model.name)
38 | );
39 | return state.set("loading", false).set("models", filtered);
40 | }
41 | case LOAD_MODELS_ERROR:
42 | return state.set("loading", false).set("error", payload);
43 |
44 | case PRE_ANALYZE:
45 | return state.set("analyzing", true).set("analysis", null);
46 | case PRE_ANALYZE_SUCCESS: {
47 | return state.set("analyzing", false).set("analysis", payload.analysis);
48 | }
49 | case PRE_ANALYZE_ERROR:
50 | return state.set("analyzing", false).set("preAnalyzeError", payload);
51 |
52 | case SAVE_IMPORT_CONFIG:
53 | return state.set("saving", true).set("created", null);
54 | case SAVE_IMPORT_CONFIG_SUCCESS: {
55 | return state.set("saving", false).set("created", payload.saved);
56 | }
57 | case SAVE_IMPORT_CONFIG_ERROR:
58 | return state.set("loading", false).set("saveError", payload);
59 |
60 | default:
61 | return state;
62 | }
63 | }
64 |
65 | export default createImportPageReducer;
66 |
--------------------------------------------------------------------------------
/admin/src/containers/CreateImportPage/saga.js:
--------------------------------------------------------------------------------
1 | import { fork, takeLatest, call, put } from "redux-saga/effects";
2 | import request from "utils/request";
3 |
4 | import {
5 | loadModelsSuccess,
6 | loadModelsError,
7 | preAnalyzeSuccess,
8 | preAnalyzeError,
9 | saveImportConfigError,
10 | saveImportConfigSuccess
11 | } from "./actions";
12 | import { LOAD_MODELS, PRE_ANALYZE, SAVE_IMPORT_CONFIG } from "./constants";
13 |
14 | export function* loadModels() {
15 | try {
16 | const { allModels } = yield call(request, "/content-type-builder/models", {
17 | method: "GET"
18 | });
19 |
20 | yield put(loadModelsSuccess(allModels));
21 | } catch (err) {
22 | strapi.notification.error("notification.error");
23 | yield put(loadModelsError(err));
24 | }
25 | }
26 |
27 | export function* preAnalyze(event) {
28 | try {
29 | const { importConfig } = event.payload;
30 |
31 | const analysis = yield call(
32 | request,
33 | "/import-content/preAnalyzeImportFile",
34 | {
35 | method: "POST",
36 | body: importConfig
37 | }
38 | );
39 |
40 | yield put(preAnalyzeSuccess(analysis));
41 | } catch (error) {
42 | strapi.notification.error("notification.error");
43 | yield put(preAnalyzeError(error));
44 | }
45 | }
46 |
47 | export function* saveImportConfig(event) {
48 | try {
49 | const { importConfig } = event.payload;
50 |
51 | const saved = yield call(request, "/import-content", {
52 | method: "POST",
53 | body: importConfig
54 | });
55 |
56 | yield put(saveImportConfigSuccess(saved));
57 | } catch (error) {
58 | strapi.notification.error("notification.error");
59 | yield put(saveImportConfigError(error));
60 | }
61 | }
62 |
63 | export function* defaultSaga() {
64 | yield fork(takeLatest, LOAD_MODELS, loadModels);
65 | yield fork(takeLatest, PRE_ANALYZE, preAnalyze);
66 | yield fork(takeLatest, SAVE_IMPORT_CONFIG, saveImportConfig);
67 | }
68 |
69 | export default defaultSaga;
70 |
--------------------------------------------------------------------------------
/admin/src/containers/CreateImportPage/selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from "reselect";
2 | import pluginId from "pluginId";
3 |
4 | /**
5 | * Direct selector to the examplePage state domain
6 | */
7 | const selectCreateImportPageDomain = () => state =>
8 | state.get(`${pluginId}_createImportPage`);
9 |
10 | /**
11 | * Default selector used by HomePage
12 | */
13 |
14 | const makeSelectLoading = () =>
15 | createSelector(
16 | selectCreateImportPageDomain(),
17 | substate => substate.get("loading")
18 | );
19 | const makeSelectModels = () =>
20 | createSelector(
21 | selectCreateImportPageDomain(),
22 | substate => substate.get("models")
23 | );
24 |
25 | const makeSelectAnalyzing = () =>
26 | createSelector(
27 | selectCreateImportPageDomain(),
28 | substate => substate.get("analyzing")
29 | );
30 | const makeSelectAnalysis = () =>
31 | createSelector(
32 | selectCreateImportPageDomain(),
33 | substate => substate.get("analysis")
34 | );
35 |
36 | const makeSelectCreated = () =>
37 | createSelector(
38 | selectCreateImportPageDomain(),
39 | substate => substate.get("created")
40 | );
41 | const makeSelectSaving = () =>
42 | createSelector(
43 | selectCreateImportPageDomain(),
44 | substate => substate.get("saving")
45 | );
46 |
47 | export {
48 | makeSelectLoading,
49 | makeSelectModels,
50 | makeSelectAnalyzing,
51 | makeSelectAnalysis,
52 | makeSelectCreated,
53 | makeSelectSaving
54 | };
55 |
--------------------------------------------------------------------------------
/admin/src/containers/CreateImportPage/styles.scss:
--------------------------------------------------------------------------------
1 | .createImportPage {
2 | padding: 1.7rem 3rem;
3 | background: rgba(14, 22, 34, 0.02);
4 | min-height: calc(100vh - 6rem);
5 | }
6 |
--------------------------------------------------------------------------------
/admin/src/containers/HomePage/actions.js:
--------------------------------------------------------------------------------
1 | import {
2 | LOAD_IMPORT_CONFIGS,
3 | LOAD_IMPORT_CONFIGS_ERROR,
4 | LOAD_IMPORT_CONFIGS_SUCCESS,
5 | UNDO_IMPORT,
6 | UNDO_IMPORT_SUCCESS,
7 | UNDO_IMPORT_ERROR,
8 | DELETE_IMPORT,
9 | DELETE_IMPORT_SUCCESS,
10 | DELETE_IMPORT_ERROR
11 | } from "./constants";
12 |
13 | export const loadImportConfigs = () => ({
14 | type: LOAD_IMPORT_CONFIGS
15 | });
16 | export const loadImportConfigsSuccess = importConfigs => ({
17 | type: LOAD_IMPORT_CONFIGS_SUCCESS,
18 | payload: { importConfigs }
19 | });
20 | export const loadImportConfigsError = error => ({
21 | type: LOAD_IMPORT_CONFIGS_ERROR,
22 | payload: error,
23 | error: true
24 | });
25 |
26 | export const undoImport = id => ({
27 | type: UNDO_IMPORT,
28 | payload: { id }
29 | });
30 | export const undoImportSuccess = () => ({
31 | type: UNDO_IMPORT_SUCCESS
32 | });
33 | export const undoImportError = error => ({
34 | type: UNDO_IMPORT_ERROR,
35 | payload: error,
36 | error: true
37 | });
38 |
39 | export const deleteImport = id => ({
40 | type: DELETE_IMPORT,
41 | payload: { id }
42 | });
43 | export const deleteImportSuccess = () => ({
44 | type: DELETE_IMPORT_SUCCESS
45 | });
46 | export const deleteImportError = error => ({
47 | type: DELETE_IMPORT_ERROR,
48 | payload: error,
49 | error: true
50 | });
51 |
--------------------------------------------------------------------------------
/admin/src/containers/HomePage/constants.js:
--------------------------------------------------------------------------------
1 | import pluginId from "pluginId";
2 |
3 | const buildString = suffix => `${pluginId}/HomePage/${suffix}`;
4 |
5 | export const LOAD_IMPORT_CONFIGS = buildString("LOAD_IMPORT_CONFIGS");
6 | export const LOAD_IMPORT_CONFIGS_ERROR = buildString(
7 | "LOAD_IMPORT_CONFIGS_ERROR"
8 | );
9 | export const LOAD_IMPORT_CONFIGS_SUCCESS = buildString(
10 | "LOAD_IMPORT_CONFIGS_SUCCESS"
11 | );
12 |
13 | export const UNDO_IMPORT = buildString("UNDO_IMPORT");
14 | export const UNDO_IMPORT_SUCCESS = buildString("UNDO_IMPORT_SUCCESS");
15 | export const UNDO_IMPORT_ERROR = buildString("UNDO_IMPORT_ERROR");
16 |
17 | export const DELETE_IMPORT = buildString("DELETE_IMPORT");
18 | export const DELETE_IMPORT_SUCCESS = buildString("DELETE_IMPORT_SUCCESS");
19 | export const DELETE_IMPORT_ERROR = buildString("DELETE_IMPORT_ERROR");
20 |
--------------------------------------------------------------------------------
/admin/src/containers/HomePage/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Button from 'components/Button';
3 | import IcoContainer from 'components/IcoContainer';
4 | import PropTypes from 'prop-types';
5 | import { connect } from 'react-redux';
6 | import { createStructuredSelector } from 'reselect';
7 | import { injectIntl } from 'react-intl';
8 | import { compose } from 'redux';
9 | import pluginId from 'pluginId';
10 | import moment from 'moment';
11 |
12 | import {
13 | selectImportConfigs,
14 | selectImportConfigsError,
15 | selectImportConfigsLoading
16 | } from './selectors';
17 |
18 | import styles from './styles.scss';
19 |
20 | import { loadImportConfigs, undoImport, deleteImport } from './actions';
21 | import reducer from './reducer';
22 | import saga from './saga';
23 |
24 | export class HomePage extends Component {
25 | componentDidMount() {
26 | this.props.loadImportConfigs();
27 | }
28 |
29 | componentWillReceiveProps(nextProps) {
30 | if (this.props.loading && !nextProps.loading) {
31 | if (
32 | nextProps.importConfigs &&
33 | nextProps.importConfigs.some(config => config.ongoing)
34 | ) {
35 | setTimeout(() => this.props.loadImportConfigs(), 2000);
36 | }
37 | }
38 | }
39 |
40 | navigateToCreateImport = () => {
41 | this.props.history.push(`/plugins/${pluginId}/create`);
42 | };
43 |
44 | deleteImport = id => () => {
45 | this.props.deleteImport(id);
46 | };
47 |
48 | undoImport = id => () => {
49 | this.props.undoImport(id);
50 | };
51 |
52 | getSourceText = item => {
53 | switch (item.source) {
54 | case 'upload':
55 | return item.options.filename;
56 | case 'url':
57 | return item.options.url;
58 | }
59 | };
60 |
61 | render() {
62 | const { importConfigs } = this.props;
63 |
64 | return (
65 |
66 |
71 |
72 |
73 |
74 |
75 | Source |
76 | Content Type |
77 | Updated |
78 | Items |
79 |
80 |
81 |
82 | {importConfigs &&
83 | importConfigs.map(item => {
84 | const updatedAt = moment(item.updated_at);
85 |
86 | return (
87 |
88 | {this.getSourceText(item)} |
89 | {item.contentType} |
90 | {updatedAt.fromNow()} |
91 | {item.importedCount} |
92 |
93 |
109 | |
110 |
111 | );
112 | })}
113 |
114 |
115 |
116 | );
117 | }
118 | }
119 |
120 | HomePage.contextTypes = {
121 | router: PropTypes.object
122 | };
123 |
124 | HomePage.propTypes = {
125 | history: PropTypes.object.isRequired,
126 | loadImports: PropTypes.func.isRequired,
127 | importConfigs: PropTypes.array,
128 | undoImport: PropTypes.func.isRequired,
129 | deleteImport: PropTypes.func.isRequired
130 | };
131 |
132 | const mapDispatchToProps = {
133 | loadImportConfigs,
134 | undoImport,
135 | deleteImport
136 | };
137 |
138 | const mapStateToProps = createStructuredSelector({
139 | importConfigs: selectImportConfigs(),
140 | loading: selectImportConfigsLoading(),
141 | error: selectImportConfigsError()
142 | });
143 |
144 | const withConnect = connect(
145 | mapStateToProps,
146 | mapDispatchToProps
147 | );
148 |
149 | const withReducer = strapi.injectReducer({
150 | key: 'homePage',
151 | reducer,
152 | pluginId
153 | });
154 | const withSaga = strapi.injectSaga({ key: 'homePage', saga, pluginId });
155 |
156 | export default compose(
157 | withReducer,
158 | withSaga,
159 | withConnect
160 | )(injectIntl(HomePage));
161 |
--------------------------------------------------------------------------------
/admin/src/containers/HomePage/reducer.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable';
2 |
3 | import {
4 | LOAD_IMPORT_CONFIGS,
5 | LOAD_IMPORT_CONFIGS_SUCCESS,
6 | LOAD_IMPORT_CONFIGS_ERROR
7 | } from './constants';
8 |
9 | const initialState = fromJS({
10 | importConfigs: null,
11 | loading: false,
12 | error: null
13 | });
14 |
15 | function homePageReducer(state = initialState, action) {
16 | const { type, payload } = action;
17 |
18 | switch (type) {
19 | case LOAD_IMPORT_CONFIGS:
20 | return state.set('loading', true);
21 |
22 | case LOAD_IMPORT_CONFIGS_SUCCESS:
23 | return state
24 | .set('loading', false)
25 | .set('importConfigs', payload.importConfigs);
26 |
27 | case LOAD_IMPORT_CONFIGS_ERROR:
28 | return state.set('loading', false).set('error', payload);
29 |
30 | default:
31 | return state;
32 | }
33 | }
34 |
35 | export default homePageReducer;
36 |
--------------------------------------------------------------------------------
/admin/src/containers/HomePage/saga.js:
--------------------------------------------------------------------------------
1 | import { fork, takeLatest, call, put } from "redux-saga/effects";
2 | import request from "utils/request";
3 |
4 | import {
5 | loadImportConfigs,
6 | loadImportConfigsSuccess,
7 | loadImportConfigsError,
8 | undoImportError,
9 | undoImportSuccess,
10 | deleteImportError,
11 | deleteImportSuccess
12 | } from "./actions";
13 | import { LOAD_IMPORT_CONFIGS, DELETE_IMPORT, UNDO_IMPORT } from "./constants";
14 |
15 | export function* loadImportConfigsSaga() {
16 | try {
17 | const importConfigs = yield call(request, "/import-content", {
18 | method: "GET"
19 | });
20 |
21 | yield put(loadImportConfigsSuccess(importConfigs));
22 | } catch (error) {
23 | strapi.notification.error("notification.error");
24 | yield put(loadImportConfigsError(error));
25 | }
26 | }
27 |
28 | export function* deleteImportSaga(event) {
29 | const { id } = event.payload;
30 |
31 | try {
32 | const importConfigs = yield call(request, `/import-content/${id}`, {
33 | method: "DELETE"
34 | });
35 |
36 | yield put(deleteImportSuccess(importConfigs));
37 | yield put(loadImportConfigs());
38 | } catch (error) {
39 | strapi.notification.error("notification.error");
40 | yield put(deleteImportError(error));
41 | }
42 | }
43 |
44 | export function* undoImportSaga(event) {
45 | const { id } = event.payload;
46 |
47 | try {
48 | const importConfigs = yield call(request, `/import-content/${id}/undo`, {
49 | method: "POST"
50 | });
51 |
52 | yield put(undoImportSuccess(importConfigs));
53 | yield put(loadImportConfigs());
54 | } catch (error) {
55 | strapi.notification.error("notification.error");
56 | yield put(undoImportError(error));
57 | }
58 | }
59 |
60 | export function* defaultSaga() {
61 | yield fork(takeLatest, LOAD_IMPORT_CONFIGS, loadImportConfigsSaga);
62 | yield fork(takeLatest, UNDO_IMPORT, undoImportSaga);
63 | yield fork(takeLatest, DELETE_IMPORT, deleteImportSaga);
64 | }
65 |
66 | export default defaultSaga;
67 |
--------------------------------------------------------------------------------
/admin/src/containers/HomePage/selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import pluginId from 'pluginId';
3 | /**
4 | * Direct selector to the homePage state domain
5 | */
6 | const selectHomePageDomain = () => state => state.get(`${pluginId}_homePage`);
7 |
8 | /**
9 | * Default selector used by HomePage
10 | */
11 |
12 | export const selectImportConfigs = () =>
13 | createSelector(selectHomePageDomain(), substate =>
14 | substate.get('importConfigs')
15 | );
16 |
17 | export const selectImportConfigsError = () =>
18 | createSelector(selectHomePageDomain(), substate => substate.get('error'));
19 |
20 | export const selectImportConfigsLoading = () =>
21 | createSelector(selectHomePageDomain(), substate => substate.get('loading'));
22 |
--------------------------------------------------------------------------------
/admin/src/containers/HomePage/styles.scss:
--------------------------------------------------------------------------------
1 | .homePage {
2 | padding: 1.7rem 3rem;
3 | background: rgba(14, 22, 34, 0.02);
4 | min-height: calc(100vh - 6rem);
5 | }
6 |
7 | .inProgress {
8 | background-color: rgba(180, 255, 180, 0.5);
9 | }
10 |
11 | th,
12 | td {
13 | padding-right: 15px;
14 | }
15 |
--------------------------------------------------------------------------------
/admin/src/containers/NotFoundPage/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NotFoundPage
3 | *
4 | * This is the page we show when the user visits a url that doesn't have a route
5 | *
6 | * NOTE: while this component should technically be a stateless functional
7 | * component (SFC), hot reloading does not currently support SFCs. If hot
8 | * reloading is not a neccessity for you then you can refactor it and remove
9 | * the linting exception.
10 | */
11 |
12 | import React from 'react';
13 |
14 | import NotFound from 'components/NotFound';
15 |
16 | export default class NotFoundPage extends React.Component {
17 | render() {
18 | return ;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/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/de.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/admin/src/translations/en.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/es.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/fr.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/it.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/admin/src/translations/ko.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:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbeuckm/strapi-plugin-import-content/6c6131826bff162b107294005b035ab0c43a72a2/admin/src/translations/ru.json
--------------------------------------------------------------------------------
/admin/src/translations/tr.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/admin/src/translations/zh-Hans.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbeuckm/strapi-plugin-import-content/6c6131826bff162b107294005b035ab0c43a72a2/admin/src/translations/zh-Hans.json
--------------------------------------------------------------------------------
/admin/src/translations/zh.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | # Node.js with React
2 | # Build a Node.js project that uses React.
3 | # Add steps that analyze code, save build artifacts, deploy, and more:
4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
5 |
6 | trigger:
7 | - master
8 |
9 | pool:
10 | vmImage: 'Ubuntu-16.04'
11 |
12 | steps:
13 | - task: NodeTool@0
14 | inputs:
15 | versionSpec: '10.x'
16 | displayName: 'Install Node.js'
17 |
18 | - script: |
19 | npm install
20 | npm run test
21 | displayName: 'npm install and test'
22 |
--------------------------------------------------------------------------------
/config/functions/bootstrap.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const resetOngoingImports = async () => {
4 | const entries = await strapi
5 | .query('importconfig', 'import-content')
6 | .find({ ongoing: true });
7 |
8 | const resetImportsPromises = entries.map(importConfig =>
9 | strapi
10 | .query('importconfig', 'import-content')
11 | .update({ id: importConfig.id }, { ongoing: false })
12 | );
13 |
14 | return await Promise.all(resetImportsPromises);
15 | };
16 |
17 | const findAuthenticatedRole = async () => {
18 | const result = await strapi
19 | .query('role', 'users-permissions')
20 | .findOne({ type: 'authenticated' });
21 |
22 | return result;
23 | };
24 |
25 | const setDefaultPermissions = async () => {
26 | const role = await findAuthenticatedRole();
27 |
28 | const permissions = await strapi
29 | .query('permission', 'users-permissions')
30 | .find({ type: 'import-content', role: role.id });
31 |
32 | await Promise.all(
33 | permissions.map(p =>
34 | strapi
35 | .query('permission', 'users-permissions')
36 | .update({ id: p.id }, { enabled: true })
37 | )
38 | );
39 | };
40 |
41 | const isFirstRun = async () => {
42 | const pluginStore = strapi.store({
43 | environment: strapi.config.environment,
44 | type: 'plugin',
45 | name: 'import-content'
46 | });
47 |
48 | const initHasRun = await pluginStore.get({ key: 'initHasRun' });
49 |
50 | await pluginStore.set({ key: 'initHasRun', value: true });
51 |
52 | return !initHasRun;
53 | };
54 |
55 | module.exports = async callback => {
56 | const shouldSetDefaultPermissions = await isFirstRun();
57 |
58 | if (shouldSetDefaultPermissions) {
59 | await setDefaultPermissions();
60 | }
61 |
62 | await resetOngoingImports();
63 |
64 | callback();
65 | };
66 |
--------------------------------------------------------------------------------
/config/queries/bookshelf.js:
--------------------------------------------------------------------------------
1 | const _ = require("lodash");
2 | const { convertRestQueryParams, buildQuery } = require("strapi-utils");
3 |
4 | module.exports = {
5 | find: async function(params, populate) {
6 | const model = this;
7 |
8 | const filters = convertRestQueryParams(params);
9 |
10 | return this.query(buildQuery({ model, filters }))
11 | .fetchAll({
12 | withRelated: populate || this.associations.map(x => x.alias)
13 | })
14 | .then(data => data.toJSON());
15 | },
16 |
17 | count: async function(params = {}) {
18 | const model = this;
19 |
20 | const { where } = convertRestQueryParams(params);
21 |
22 | return this.query(buildQuery({ model, filters: { where } })).count();
23 | },
24 |
25 | findOne: async function(params, populate) {
26 | const primaryKey = params[this.primaryKey] || params.id;
27 |
28 | if (primaryKey) {
29 | params = {
30 | [this.primaryKey]: primaryKey
31 | };
32 | }
33 |
34 | const record = await this.forge(params).fetch({
35 | withRelated: populate || this.associations.map(x => x.alias)
36 | });
37 |
38 | return record ? record.toJSON() : record;
39 | },
40 |
41 | create: async function(params) {
42 | return this.forge()
43 | .save(
44 | Object.keys(params).reduce((acc, current) => {
45 | if (
46 | _.get(this._attributes, [current, "type"]) ||
47 | _.get(this._attributes, [current, "model"])
48 | ) {
49 | acc[current] = params[current];
50 | }
51 |
52 | return acc;
53 | }, {})
54 | )
55 | .catch(err => {
56 | if (err.detail) {
57 | const field = _.last(_.words(err.detail.split("=")[0]));
58 | err = { message: `This ${field} is already taken`, field };
59 | }
60 |
61 | throw err;
62 | });
63 | },
64 |
65 | update: async function(search, params = {}) {
66 | if (_.isEmpty(params)) {
67 | params = search;
68 | }
69 |
70 | const primaryKey = search[this.primaryKey] || search.id;
71 |
72 | if (primaryKey) {
73 | search = {
74 | [this.primaryKey]: primaryKey
75 | };
76 | } else {
77 | const entry = await module.exports.findOne.call(this, search);
78 |
79 | search = {
80 | [this.primaryKey]: entry[this.primaryKey] || entry.id
81 | };
82 | }
83 |
84 | return this.forge(search)
85 | .save(params, {
86 | patch: true
87 | })
88 | .catch(err => {
89 | console.log("SQLite does not parse this error correctly", { err });
90 | const field = _.last(_.words(err.detail.split("=")[0]));
91 | const error = { message: `This ${field} is already taken`, field };
92 |
93 | throw error;
94 | });
95 | },
96 |
97 | delete: async function(params) {
98 | return await this.forge({
99 | [this.primaryKey]: params[this.primaryKey] || params.id
100 | }).destroy();
101 | },
102 |
103 | search: async function(params) {
104 | return this.query(function(qb) {
105 | qb.whereRaw(`LOWER(hash) LIKE ?`, [`%${params.id}%`]).orWhereRaw(
106 | `LOWER(name) LIKE ?`,
107 | [`%${params.id}%`]
108 | );
109 | }).fetchAll();
110 | },
111 |
112 | addPermission: async function(params) {
113 | return this.forge(params).save();
114 | },
115 |
116 | removePermission: async function(params) {
117 | return this.forge({
118 | [this.primaryKey]: params[this.primaryKey] || params.id
119 | }).destroy();
120 | }
121 | };
122 |
--------------------------------------------------------------------------------
/config/queries/mongoose.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash');
2 | const { convertRestQueryParams, buildQuery } = require('strapi-utils');
3 |
4 | module.exports = {
5 | find: async function(params, populate) {
6 | const model = this;
7 | const filters = convertRestQueryParams(params);
8 |
9 | return buildQuery({
10 | model,
11 | filters,
12 | populate: populate || model.associations.map(x => x.alias),
13 | }).lean();
14 | },
15 |
16 | count: async function(params) {
17 | const model = this;
18 |
19 | const filters = convertRestQueryParams(params);
20 |
21 | return buildQuery({
22 | model,
23 | filters: { where: filters.where },
24 | }).count();
25 | },
26 |
27 | findOne: async function(params, populate) {
28 | const primaryKey = params[this.primaryKey] || params.id;
29 |
30 | if (primaryKey) {
31 | params = {
32 | [this.primaryKey]: primaryKey,
33 | };
34 | }
35 |
36 | return this.findOne(params)
37 | .populate(populate || this.associations.map(x => x.alias).join(' '))
38 | .lean();
39 | },
40 |
41 | create: async function(params) {
42 | // Exclude relationships.
43 | const values = Object.keys(params).reduce((acc, current) => {
44 | if (
45 | _.get(this._attributes, [current, 'type']) ||
46 | _.get(this._attributes, [current, 'model'])
47 | ) {
48 | acc[current] = params[current];
49 | }
50 |
51 | return acc;
52 | }, {});
53 |
54 | return this.create(values).catch(err => {
55 | if (err.message.indexOf('index:') !== -1) {
56 | const message = err.message.split('index:');
57 | const field = _.words(_.last(message).split('_')[0]);
58 | const error = { message: `This ${field} is already taken`, field };
59 |
60 | throw error;
61 | }
62 |
63 | throw err;
64 | });
65 | },
66 |
67 | update: async function(search, params = {}) {
68 | if (_.isEmpty(params)) {
69 | params = search;
70 | }
71 |
72 | const primaryKey = search[this.primaryKey] || search.id;
73 |
74 | if (primaryKey) {
75 | search = {
76 | [this.primaryKey]: primaryKey,
77 | };
78 | }
79 |
80 | return this.updateOne(search, params, {
81 | strict: false,
82 | }).catch(error => {
83 | const field = _.last(_.words(error.message.split('_')[0]));
84 | const err = { message: `This ${field} is already taken`, field };
85 |
86 | throw err;
87 | });
88 | },
89 |
90 | delete: async function(params) {
91 | // Delete entry.
92 | return this.remove({
93 | [this.primaryKey]: params[this.primaryKey] || params.id,
94 | });
95 | },
96 |
97 | search: async function(params) {
98 | const re = new RegExp(params.id, 'i');
99 |
100 | return this.find({
101 | $or: [{ hash: re }, { name: re }],
102 | });
103 | },
104 |
105 | addPermission: async function(params) {
106 | return this.create(params);
107 | },
108 |
109 | removePermission: async function(params) {
110 | return this.remove(params);
111 | },
112 | };
113 |
--------------------------------------------------------------------------------
/config/routes.json:
--------------------------------------------------------------------------------
1 | {
2 | "routes": [
3 | {
4 | "method": "POST",
5 | "path": "/preAnalyzeImportFile",
6 | "handler": "ImportConfig.preAnalyzeImportFile",
7 | "config": {
8 | "policies": []
9 | }
10 | },
11 | {
12 | "method": "POST",
13 | "path": "/",
14 | "handler": "ImportConfig.create",
15 | "config": {
16 | "policies": []
17 | }
18 | },
19 | {
20 | "method": "GET",
21 | "path": "/",
22 | "handler": "ImportConfig.index",
23 | "config": {
24 | "policies": []
25 | }
26 | },
27 | {
28 | "method": "POST",
29 | "path": "/:importId/undo",
30 | "handler": "ImportConfig.undo",
31 | "config": {
32 | "policies": []
33 | }
34 | },
35 | {
36 | "method": "DELETE",
37 | "path": "/:importId",
38 | "handler": "ImportConfig.delete",
39 | "config": {
40 | "policies": []
41 | }
42 | }
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/config/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "IMPORT_THROTTLE": 100,
3 | "UNDO_THROTTLE": 100
4 | }
5 |
--------------------------------------------------------------------------------
/controllers/ImportConfig.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const {
3 | resolveFileDataFromRequest
4 | } = require('../services/utils/FileDataResolver');
5 |
6 | module.exports = {
7 | index: async ctx => {
8 | const entries = await strapi.query('importconfig', 'import-content').find();
9 |
10 | const withCounts = entries.map(entry => ({
11 | ...entry,
12 | importedCount: entry.importeditems.length,
13 | importeditems: undefined
14 | }));
15 |
16 | ctx.send(withCounts);
17 | },
18 |
19 | create: async ctx => {
20 | const services = strapi.plugins['import-content'].services;
21 | const importConfig = ctx.request.body;
22 | console.log('create', importConfig);
23 | importConfig.ongoing = true;
24 |
25 | const record = await strapi
26 | .query('importconfig', 'import-content')
27 | .create(importConfig);
28 |
29 | ctx.send(record);
30 |
31 | const { contentType, body } = await resolveFileDataFromRequest(ctx);
32 |
33 | services['importitems'].importItems(record, { contentType, body });
34 | },
35 |
36 | undo: async ctx => {
37 | const importId = ctx.params.importId;
38 |
39 | const importConfig = await strapi
40 | .query('importconfig', 'import-content')
41 | .findOne({ id: importId });
42 |
43 | console.log('undo', importId);
44 |
45 | ctx.send(importConfig);
46 |
47 | strapi.plugins['import-content'].services['undoitems'].undoItems(
48 | importConfig
49 | );
50 | },
51 |
52 | delete: async ctx => {
53 | const importId = ctx.params.importId;
54 |
55 | await strapi.query('importconfig', 'import-content').delete({
56 | id: importId
57 | });
58 |
59 | ctx.send({ message: 'ok' });
60 | },
61 |
62 | preAnalyzeImportFile: async ctx => {
63 | const services = strapi.plugins['import-content'].services;
64 |
65 | const { contentType, body, options } = await resolveFileDataFromRequest(
66 | ctx
67 | );
68 |
69 | try {
70 | const data = await services['importconfig'].preAnalyzeImportFile({
71 | contentType,
72 | body,
73 | options
74 | });
75 |
76 | ctx.send(data);
77 | } catch (error) {
78 | ctx.response.notAcceptable('could not parse', error);
79 | }
80 | }
81 | };
82 |
--------------------------------------------------------------------------------
/models/ImportConfig.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | // Before saving a value.
5 | // Fired before an `insert` or `update` query.
6 | // beforeSave: async (model) => {},
7 | // After saving a value.
8 | // Fired after an `insert` or `update` query.
9 | // afterSave: async (model, result) => {},
10 | // Before fetching all values.
11 | // Fired before a `fetchAll` operation.
12 | // beforeFetchAll: async (model) => {},
13 | // After fetching all values.
14 | // Fired after a `fetchAll` operation.
15 | // afterFetchAll: async (model, results) => {},
16 | // Fired before a `fetch` operation.
17 | // beforeFetch: async (model) => {},
18 | // After fetching a value.
19 | // Fired after a `fetch` operation.
20 | // afterFetch: async (model, result) => {},
21 | // Before creating a value.
22 | // Fired before `insert` query.
23 | // beforeCreate: async (model) => {},
24 | // After creating a value.
25 | // Fired after `insert` query.
26 | // afterCreate: async (model, result) => {},
27 | // Before updating a value.
28 | // Fired before an `update` query.
29 | // beforeUpdate: async (model) => {},
30 | // After updating a value.
31 | // Fired after an `update` query.
32 | // afterUpdate: async (model, result) => {},
33 | // Before destroying a value.
34 | // Fired before a `delete` query.
35 | // beforeDestroy: async (model) => {},
36 | // After destroying a value.
37 | // Fired after a `delete` query.
38 | // afterDestroy: async (model, result) => {}
39 | };
40 |
--------------------------------------------------------------------------------
/models/ImportConfig.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "connection": "default",
3 | "info": {
4 | "name": "importconfig",
5 | "description":
6 | "Record an executed import and relate its products for possible later deletion"
7 | },
8 | "options": {
9 | "timestamps": true
10 | },
11 | "attributes": {
12 | "date": {
13 | "type": "date"
14 | },
15 | "source": {
16 | "type": "string"
17 | },
18 | "options": {
19 | "type": "json"
20 | },
21 | "contentType": {
22 | "type": "string"
23 | },
24 | "fieldMapping": {
25 | "type": "json"
26 | },
27 | "ongoing": {
28 | "type": "boolean"
29 | },
30 | "importeditems": {
31 | "collection": "importeditem",
32 | "via": "importconfig",
33 | "plugin": "import-content"
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/models/Importeditem.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * Lifecycle callbacks for the `Importeditem` model.
5 | */
6 |
7 | module.exports = {
8 | // Before saving a value.
9 | // Fired before an `insert` or `update` query.
10 | // beforeSave: async (model, attrs, options) => {},
11 |
12 | // After saving a value.
13 | // Fired after an `insert` or `update` query.
14 | // afterSave: async (model, response, options) => {},
15 |
16 | // Before fetching a value.
17 | // Fired before a `fetch` operation.
18 | // beforeFetch: async (model, columns, options) => {},
19 |
20 | // After fetching a value.
21 | // Fired after a `fetch` operation.
22 | // afterFetch: async (model, response, options) => {},
23 |
24 | // Before fetching all values.
25 | // Fired before a `fetchAll` operation.
26 | // beforeFetchAll: async (model, columns, options) => {},
27 |
28 | // After fetching all values.
29 | // Fired after a `fetchAll` operation.
30 | // afterFetchAll: async (model, response, options) => {},
31 |
32 | // Before creating a value.
33 | // Fired before an `insert` query.
34 | // beforeCreate: async (model, attrs, options) => {},
35 |
36 | // After creating a value.
37 | // Fired after an `insert` query.
38 | // afterCreate: async (model, attrs, options) => {},
39 |
40 | // Before updating a value.
41 | // Fired before an `update` query.
42 | // beforeUpdate: async (model, attrs, options) => {},
43 |
44 | // After updating a value.
45 | // Fired after an `update` query.
46 | // afterUpdate: async (model, attrs, options) => {},
47 |
48 | // Before destroying a value.
49 | // Fired before a `delete` query.
50 | // beforeDestroy: async (model, attrs, options) => {},
51 |
52 | // After destroying a value.
53 | // Fired after a `delete` query.
54 | // afterDestroy: async (model, attrs, options) => {}
55 | };
56 |
--------------------------------------------------------------------------------
/models/Importeditem.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "connection": "default",
3 | "info": {
4 | "name": "importeditem",
5 | "description":
6 | "A record of an item that has been imported with a given ImportConfig"
7 | },
8 | "options": {
9 | "increments": true,
10 | "timestamps": true,
11 | "comment": ""
12 | },
13 | "attributes": {
14 | "ContentType": {
15 | "type": "string"
16 | },
17 | "ContentId": {
18 | "type": "integer"
19 | },
20 | "importconfig": {
21 | "model": "importconfig",
22 | "via": "importeditems",
23 | "plugin": "import-content"
24 | },
25 | "importedFiles": {
26 | "type": "json"
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "strapi-plugin-import-content",
3 | "version": "0.3.4",
4 | "description": "Import your content into Strapi.",
5 | "repository": {
6 | "type": "git",
7 | "url": "git@github.com:jbeuckm/strapi-plugin-import-content.git"
8 | },
9 | "strapi": {
10 | "name": "Import Content",
11 | "icon": "plug",
12 | "description": "Import RSS feed or uploaded CSV data into Strapi."
13 | },
14 | "scripts": {
15 | "analyze:clean": "node ./node_modules/strapi-helper-plugin/node_modules/.bin/rimraf stats.json",
16 | "preanalyze": "npm run analyze:clean",
17 | "analyze": "node ./node_modules/strapi-helper-plugin/lib/internals/scripts/analyze.js",
18 | "prebuild": "npm run build:clean",
19 | "build:dev": "node ./node_modules/strapi-helper-plugin/node_modules/.bin/cross-env NODE_ENV=development node ./node_modules/strapi-helper-plugin/node_modules/.bin/webpack --config node_modules/strapi-helper-plugin/lib/internals/webpack/webpack.prod.babel.js --color -p --progress",
20 | "build": "node ./node_modules/strapi-helper-plugin/node_modules/.bin/cross-env NODE_ENV=production node node_modules/strapi-helper-plugin/node_modules/.bin/webpack --config node_modules/strapi-helper-plugin/lib/internals/webpack/webpack.prod.babel.js --color -p --progress",
21 | "build:clean": "node ./node_modules/strapi-helper-plugin/node_modules/.bin/rimraf admin/build",
22 | "start": "node ./node_modules/strapi-helper-plugin/node_modules/.bin/cross-env NODE_ENV=development node ./node_modules/strapi-helper-plugin/lib/server",
23 | "generate": "node ./node_modules/strapi-helper-plugin/node_modules/plop --plopfile ./node_modules/strapi-helper-plugin/lib/internals/generators/index.js",
24 | "lint": "node ./node_modules/strapi-helper-plugin/node_modules/.bin/eslint --ignore-path .gitignore --ignore-pattern '/admin/build/' --config ./node_modules/strapi-helper-plugin/lib/internals/eslint/.eslintrc.json admin",
25 | "prettier": "node ./node_modules/strapi-helper-plugin/node_modules/.bin/prettier --single-quote --trailing-comma es5 --write \"{admin,__{tests,mocks}__}/**/*.js\"",
26 | "test": "jest",
27 | "test:watch": "jest --watch",
28 | "prepublishOnly": "npm run build"
29 | },
30 | "dependencies": {
31 | "content-type-parser": "^1.0.2",
32 | "csv-parse": "^4.4.1",
33 | "get-urls": "^9.1.0",
34 | "jest": "^24.7.1",
35 | "joi": "^14.3.1",
36 | "lodash": "^4.17.11",
37 | "moment": "^2.24.0",
38 | "nock": "^10.0.6",
39 | "request": "^2.88.0",
40 | "rss-parser": "^3.7.0",
41 | "simple-statistics": "^7.0.2",
42 | "striptags": "^3.1.1"
43 | },
44 | "devDependencies": {
45 | "strapi-helper-plugin": "3.0.0-alpha.26"
46 | },
47 | "author": {
48 | "name": "Joseph Beuckman",
49 | "email": "joe@beigerecords.com",
50 | "url": "https://github.com/jbeuckm"
51 | },
52 | "maintainers": [
53 | {
54 | "name": "Joseph Beuckman",
55 | "email": "joe@beigerecords.com",
56 | "url": "https://github.com/jbeuckm"
57 | }
58 | ],
59 | "engines": {
60 | "node": ">= 10.0.0",
61 | "npm": ">= 6.0.0"
62 | },
63 | "license": "MIT"
64 | }
65 |
--------------------------------------------------------------------------------
/services/ImportConfig.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const fileUtils = require('./utils/fileUtils');
3 | const analyzer = require('./utils/analyzer');
4 | module.exports = {
5 | preAnalyzeImportFile: async ({ contentType, body, options }) => {
6 | const { sourceType, items } = await fileUtils.getItemsForFileData({
7 | contentType,
8 | body,
9 | options
10 | });
11 |
12 | const analysis = analyzer.analyze(sourceType, items);
13 |
14 | return { sourceType, ...analysis };
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/services/ImportItems.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const _ = require('lodash');
3 | const fileUtils = require('./utils/fileUtils');
4 | const importFields = require('./utils/importFields');
5 | const importMediaFiles = require('./utils/importMediaFiles');
6 |
7 | const queues = {};
8 |
9 | const importNextItem = async importConfig => {
10 | const sourceItem = queues[importConfig.id].shift();
11 | if (!sourceItem) {
12 | console.log('import complete');
13 |
14 | await strapi
15 | .query('importconfig', 'import-content')
16 | .update({ id: importConfig.id }, { ongoing: false });
17 |
18 | return;
19 | }
20 |
21 | const importedItem = await importFields(
22 | sourceItem,
23 | importConfig.fieldMapping
24 | );
25 |
26 | const savedContent = await strapi.models[importConfig.contentType]
27 | .forge(importedItem)
28 | .save();
29 |
30 | const uploadedFiles = await importMediaFiles(
31 | savedContent,
32 | sourceItem,
33 | importConfig
34 | );
35 | const fileIds = _.map(_.flatten(uploadedFiles), 'id');
36 |
37 | await strapi.query('importeditem', 'import-content').create({
38 | importconfig: importConfig.id,
39 | ContentId: savedContent.id,
40 | ContentType: importConfig.contentType,
41 | importedFiles: { fileIds }
42 | });
43 |
44 | const { IMPORT_THROTTLE } = strapi.plugins['import-content'].config;
45 | setTimeout(() => importNextItem(importConfig), IMPORT_THROTTLE);
46 | };
47 |
48 | module.exports = {
49 | importItems: (importConfig, { contentType, body }) =>
50 | new Promise(async (resolve, reject) => {
51 | const importConfigRecord = importConfig.attributes;
52 | console.log('importitems', importConfigRecord);
53 |
54 | try {
55 | const { items } = await fileUtils.getItemsForFileData({
56 | contentType,
57 | body,
58 | options: importConfigRecord.options
59 | });
60 |
61 | queues[importConfigRecord.id] = items;
62 | } catch (error) {
63 | reject(error);
64 | }
65 |
66 | resolve({
67 | status: 'import started',
68 | importConfigId: importConfigRecord.id
69 | });
70 |
71 | importNextItem(importConfigRecord);
72 | })
73 | };
74 |
--------------------------------------------------------------------------------
/services/UndoItems.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const _ = require('lodash');
3 |
4 | const queues = {};
5 |
6 | const removeImportedFiles = async (fileIds, uploadConfig) => {
7 | const removePromises = fileIds.map(id =>
8 | strapi.plugins['upload'].services.upload.remove({ id }, uploadConfig)
9 | );
10 |
11 | return await Promise.all(removePromises);
12 | };
13 |
14 | const undoNextItem = async (importConfig, uploadConfig) => {
15 | const item = queues[importConfig.id].shift();
16 |
17 | if (!item) {
18 | console.log('undo complete');
19 |
20 | await strapi
21 | .query('importconfig', 'import-content')
22 | .update({ id: importConfig.id }, { ongoing: false });
23 |
24 | return;
25 | }
26 |
27 | await strapi.models[importConfig.contentType]
28 | .forge({ id: item.ContentId })
29 | .destroy();
30 |
31 | const importedFileIds = _.compact(item.importedFiles.fileIds);
32 |
33 | await removeImportedFiles(importedFileIds, uploadConfig);
34 |
35 | await strapi.query('importeditem', 'import-content').delete({
36 | id: item.id
37 | });
38 |
39 | const { UNDO_THROTTLE } = strapi.plugins['import-content'].config;
40 | setTimeout(() => undoNextItem(importConfig, uploadConfig), UNDO_THROTTLE);
41 | };
42 |
43 | module.exports = {
44 | undoItems: importConfig =>
45 | new Promise(async (resolve, reject) => {
46 | try {
47 | queues[importConfig.id] = importConfig.importeditems;
48 | } catch (error) {
49 | reject(error);
50 | }
51 |
52 | await strapi
53 | .query('importconfig', 'import-content')
54 | .update({ id: importConfig.id }, { ongoing: true });
55 |
56 | resolve({
57 | status: 'undo started',
58 | importConfigId: importConfig.id
59 | });
60 |
61 | const uploadConfig = await strapi
62 | .store({
63 | environment: strapi.config.environment,
64 | type: 'plugin',
65 | name: 'upload'
66 | })
67 | .get({ key: 'provider' });
68 |
69 | undoNextItem(importConfig, uploadConfig);
70 | })
71 | };
72 |
--------------------------------------------------------------------------------
/services/utils/FileDataResolver.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const request = require('request');
3 | const validateUrl = require('./validateUrl');
4 |
5 | const getDataFromUrl = url => {
6 | return new Promise((resolve, reject) => {
7 | if (!validateUrl(url)) return reject('invalid URL');
8 |
9 | request(url, null, async (err, res, body) => {
10 | if (err) {
11 | reject(err);
12 | }
13 |
14 | resolve({ contentType: res.headers['content-type'], body });
15 | });
16 | });
17 | };
18 |
19 | const resolveFileDataFromRequest = async ctx => {
20 | const { source, type, options, data } = ctx.request.body;
21 |
22 | switch (source) {
23 | case 'upload':
24 | return { contentType: type, body: data, options };
25 |
26 | case 'url':
27 | const { contentType, body } = await getDataFromUrl(options.url);
28 | return { contentType, body, options };
29 |
30 | case 'raw':
31 | return {
32 | contentType: type,
33 | body: options.rawText,
34 | options
35 | };
36 | }
37 | };
38 |
39 | module.exports = {
40 | resolveFileDataFromRequest,
41 | getDataFromUrl
42 | };
43 |
--------------------------------------------------------------------------------
/services/utils/FileDataResolver.spec.js:
--------------------------------------------------------------------------------
1 | const nock = require('nock');
2 | const {
3 | resolveFileDataFromRequest,
4 | getDataFromUrl
5 | } = require('./FileDataResolver');
6 |
7 | const TEST_BASE_URL = 'http://test';
8 | const TEST_PATH = '/';
9 |
10 | describe('resolveFileDataFromRequest', () => {
11 | it('requests data for "url" source', async () => {
12 | nock(TEST_BASE_URL)
13 | .get(TEST_PATH)
14 | .reply(200, 'data', { 'Content-Type': 'text/csv' });
15 |
16 | const ctx = {
17 | request: {
18 | body: { source: 'url', options: { url: TEST_BASE_URL + TEST_PATH } }
19 | }
20 | };
21 |
22 | const response = await resolveFileDataFromRequest(ctx);
23 |
24 | console.log(response);
25 | expect(response).toHaveProperty('contentType');
26 | expect(response).toHaveProperty('body');
27 | expect(response).toHaveProperty('options');
28 | });
29 |
30 | it('parses data for "raw" source', async () => {
31 | const ctx = {
32 | request: {
33 | body: {
34 | source: 'raw',
35 | type: 'text/csv',
36 | options: {},
37 | data: 'key1,key2\nvalue1,value2'
38 | }
39 | }
40 | };
41 |
42 | const response = await resolveFileDataFromRequest(ctx);
43 |
44 | console.log(response);
45 | expect(response).toHaveProperty('contentType');
46 | expect(response).toHaveProperty('body');
47 | expect(response).toHaveProperty('options');
48 | });
49 |
50 | it('parses data for "upload" source', async () => {
51 | const ctx = {
52 | request: { body: { source: 'upload', data: 'abc123' } }
53 | };
54 |
55 | const response = await resolveFileDataFromRequest(ctx);
56 | console.log(response);
57 | expect(response).toHaveProperty('contentType');
58 | expect(response).toHaveProperty('body');
59 | expect(response).toHaveProperty('options');
60 | });
61 | });
62 |
63 | describe('getDataFromUrl', () => {
64 | it('fetches type and data from a url', async () => {
65 | nock(TEST_BASE_URL)
66 | .get(TEST_PATH)
67 | .reply(200, 'data', { 'Content-Type': 'text/csv' });
68 |
69 | const response = await getDataFromUrl(TEST_BASE_URL + TEST_PATH);
70 |
71 | expect(response).toHaveProperty('contentType');
72 | expect(response).toHaveProperty('body');
73 | });
74 | });
75 |
--------------------------------------------------------------------------------
/services/utils/analyzer.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const _ = require('lodash');
3 | var ss = require('simple-statistics');
4 | const { compileStatsForFieldData } = require('./fieldUtils');
5 |
6 | const getFieldNameSet = items => {
7 | const fieldNames = new Set();
8 | items.forEach(item => {
9 | try {
10 | Object.keys(item).forEach(fieldName => fieldNames.add(fieldName));
11 | } catch (e) {
12 | console.log(e);
13 | }
14 | });
15 |
16 | return fieldNames;
17 | };
18 |
19 | const analyze = (sourceType, items) => {
20 | const fieldNames = getFieldNameSet(items);
21 |
22 | const fieldAnalyses = {};
23 | fieldNames.forEach(fieldName => (fieldAnalyses[fieldName] = []));
24 |
25 | items.forEach(item => {
26 | fieldNames.forEach(fieldName => {
27 | const fieldData = item[fieldName];
28 | const fieldStats = compileStatsForFieldData(fieldData);
29 | fieldAnalyses[fieldName].push(fieldStats);
30 | });
31 | });
32 |
33 | const fieldStats = Object.keys(fieldAnalyses).map(fieldName => {
34 | const fieldAnalysis = fieldAnalyses[fieldName];
35 |
36 | const fieldStat = { fieldName, count: fieldAnalysis.length };
37 |
38 | try {
39 | fieldStat.format = _
40 | .chain(fieldAnalysis)
41 | .countBy('format')
42 | .map((value, key) => ({ count: value, type: key }))
43 | .sortBy('count')
44 | .reverse()
45 | .head()
46 | .get('type')
47 | .value();
48 | } catch (e) {
49 | console.log(e);
50 | }
51 |
52 | fieldStat.hasMediaUrls = fieldAnalysis.some(fa => Boolean(fa.hasMediaUrls));
53 |
54 | const lengths = _.map(fieldAnalysis, 'length');
55 |
56 | fieldStat.minLength = ss.min(lengths);
57 | fieldStat.maxLength = ss.max(lengths);
58 | fieldStat.meanLength = ss.mean(lengths).toFixed(2);
59 |
60 | return fieldStat;
61 | });
62 |
63 | return { itemCount: items.length, fieldStats };
64 | };
65 |
66 | module.exports = { getFieldNameSet, analyze };
67 |
--------------------------------------------------------------------------------
/services/utils/analyzer.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { getFieldNameSet, analyze } = require('./analyzer');
4 |
5 | describe('getFieldNameSet', () => {
6 | it('finds set of all field names', () => {
7 | const fieldNames = getFieldNameSet([{ a: 1 }, { a: 1, b: 1 }, { c: 1 }]);
8 |
9 | expect(fieldNames).toEqual(new Set(['a', 'b', 'c']));
10 | });
11 | });
12 |
13 | describe('analyze', () => {
14 | it('generates an analysis from an item array', () => {
15 | const analysis = analyze({}, []);
16 |
17 | expect(analysis.itemCount).toEqual(0);
18 | expect(analysis.fieldStats).toEqual([]);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/services/utils/fieldUtils.js:
--------------------------------------------------------------------------------
1 | const getUrls = require('get-urls');
2 | const urlIsMedia = require('./urlIsMedia');
3 | const striptags = require('striptags');
4 | const stringIsEmail = require('./stringIsEmail');
5 |
6 | const detectStringFieldFormat = data => {
7 | if (new Date(data).toString() !== 'Invalid Date') return 'date';
8 |
9 | if (stringIsEmail(data)) return 'email';
10 |
11 | if (data.length !== striptags(data).length) {
12 | return 'xml';
13 | }
14 |
15 | return 'string';
16 | };
17 |
18 | const detectFieldFormat = data => {
19 | switch (typeof data) {
20 | case 'number':
21 | return 'number';
22 |
23 | case 'boolean':
24 | return 'boolean';
25 |
26 | case 'object':
27 | return 'object';
28 |
29 | case 'string':
30 | return detectStringFieldFormat(data);
31 | }
32 | };
33 |
34 | const compileStatsForFieldData = fieldData => {
35 | const stats = {};
36 |
37 | switch (typeof fieldData) {
38 | case 'string':
39 | try {
40 | const urls = Array.from(getUrls(fieldData));
41 |
42 | const l = urls.length;
43 | for (let i = 0; i < l; ++i) {
44 | if (urlIsMedia(urls[i])) {
45 | stats.hasMediaUrls = true;
46 | break;
47 | }
48 | }
49 | } catch (e) {
50 | console.log(e);
51 | }
52 | stats.length = fieldData.length;
53 | break;
54 |
55 | case 'object':
56 | if (urlIsMedia(fieldData.url)) {
57 | stats.hasMediaUrls = true;
58 | }
59 | stats.length = JSON.stringify(fieldData).length;
60 | break;
61 |
62 | default:
63 | console.log(typeof fieldData, fieldData);
64 | }
65 |
66 | stats.format = detectFieldFormat(fieldData);
67 |
68 | return stats;
69 | };
70 |
71 | module.exports = {
72 | detectStringFieldFormat,
73 | detectFieldFormat,
74 | compileStatsForFieldData
75 | };
76 |
--------------------------------------------------------------------------------
/services/utils/fieldUtils.spec.js:
--------------------------------------------------------------------------------
1 | const fieldUtils = require('./fieldUtils');
2 |
3 | describe('detectStringFieldFormat', () => {
4 | it('detects a date string', () => {
5 | const dateString = new Date().toString();
6 | const result = fieldUtils.detectStringFieldFormat(dateString);
7 | expect(result).toEqual('date');
8 | });
9 |
10 | it('detects en email', () => {
11 | const email = 'joe@beigerecords.com';
12 | const result = fieldUtils.detectStringFieldFormat(email);
13 | expect(result).toEqual('email');
14 | });
15 | });
16 |
17 | describe('detectFieldFormat', () => {
18 | it('detects a number', () => {
19 | const result = fieldUtils.detectFieldFormat(1);
20 | expect(result).toEqual('number');
21 | });
22 |
23 | it('detects a boolean', () => {
24 | const result = fieldUtils.detectFieldFormat(false);
25 | expect(result).toEqual('boolean');
26 | });
27 |
28 | it('detects a string', () => {
29 | const result = fieldUtils.detectFieldFormat('hello');
30 | expect(result).toEqual('string');
31 | });
32 | });
33 |
34 | describe('compileStatsForFieldData', () => {
35 | it('returns an object', () => {
36 | const stringData = 'hello http://website';
37 | const stats = fieldUtils.compileStatsForFieldData(stringData);
38 | expect(stats.length).toEqual(stringData.length);
39 | expect(stats.format).toEqual('string');
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/services/utils/fileFromBuffer.js:
--------------------------------------------------------------------------------
1 | const crypto = require('crypto');
2 | const uuid = require('uuid/v4');
3 |
4 | function niceHash(buffer) {
5 | return crypto
6 | .createHash('sha256')
7 | .update(buffer)
8 | .digest('base64')
9 | .replace(/=/g, '')
10 | .replace(/\//g, '-')
11 | .replace(/\+/, '_');
12 | }
13 |
14 | const fileFromBuffer = (mimeType, extension, buffer) => {
15 | const fid = uuid();
16 |
17 | return {
18 | buffer,
19 | sha256: niceHash(buffer),
20 | hash: fid.replace(/-/g, ''),
21 |
22 | name: `${fid}.${extension}`,
23 | ext: `.${extension}`,
24 | mime: mimeType,
25 | size: (buffer.length / 1000).toFixed(2)
26 | };
27 | };
28 |
29 | module.exports = fileFromBuffer;
30 |
--------------------------------------------------------------------------------
/services/utils/fileUtils.js:
--------------------------------------------------------------------------------
1 | const contentTypeParser = require('content-type-parser');
2 | const RssParser = require('rss-parser');
3 | const parse = require('csv-parse/lib/sync');
4 |
5 | const getItemsForFileData = ({ contentType, body, options }) =>
6 | new Promise(async (resolve, reject) => {
7 | const parsedContentType = contentTypeParser(contentType);
8 |
9 | if (parsedContentType.isXML()) {
10 | const parser = new RssParser();
11 | const feed = await parser.parseString(body);
12 |
13 | return resolve({ sourceType: 'rss', items: feed.items });
14 | }
15 |
16 | if (contentType === 'text/csv') {
17 | const items = parse(body, {
18 | ...options,
19 | columns: true
20 | });
21 | return resolve({ sourceType: 'csv', items });
22 | }
23 |
24 | reject({
25 | contentType: parsedContentType.toString()
26 | });
27 | });
28 |
29 | module.exports = { getItemsForFileData };
30 |
--------------------------------------------------------------------------------
/services/utils/fileUtils.spec.js:
--------------------------------------------------------------------------------
1 | const { getItemsForFileData } = require("./fileUtils");
2 |
3 | jest.mock("content-type-parser", () => () => ({
4 | isXML: () => true
5 | }));
6 |
7 | jest.mock(
8 | "rss-parser",
9 | () =>
10 | class RssParser {
11 | parseString() {
12 | return { items: [] };
13 | }
14 | }
15 | );
16 | describe("getItemsForFileData", () => {
17 | it("returns items from an rss file", async () => {
18 | const response = await getItemsForFileData("application/xml", "");
19 |
20 | expect(response.items).toEqual([]);
21 | expect(response.sourceType).toEqual("rss");
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/services/utils/getMediaUrlsFromFieldData.js:
--------------------------------------------------------------------------------
1 | const getUrls = require('get-urls');
2 | const urlIsMedia = require('./urlIsMedia');
3 |
4 | const getMediaUrlsFromFieldData = fieldData => {
5 | switch (typeof fieldData) {
6 | case 'string':
7 | return Array.from(getUrls(fieldData)).filter(urlIsMedia);
8 |
9 | case 'object':
10 | return urlIsMedia(fieldData.url) ? [fieldData.url] : [];
11 | }
12 | };
13 |
14 | module.exports = getMediaUrlsFromFieldData;
15 |
--------------------------------------------------------------------------------
/services/utils/getMediaUrlsFromFieldData.spec.js:
--------------------------------------------------------------------------------
1 | const getMediaUrlsFromFieldData = require('./getMediaUrlsFromFieldData');
2 |
3 | const TEST_URL = 'http://site.com/image.png';
4 |
5 | describe('getMediaUrlsFromFieldData', () => {
6 | it('finds urls in string', () => {
7 | expect(getMediaUrlsFromFieldData(TEST_URL)).toEqual([TEST_URL]);
8 | expect(getMediaUrlsFromFieldData('💩')).toEqual([]);
9 | });
10 |
11 | it('finds urls in object', () => {
12 | const TEST_OBJECT = { url: TEST_URL };
13 | expect(getMediaUrlsFromFieldData(TEST_OBJECT)).toEqual([TEST_URL]);
14 | expect(getMediaUrlsFromFieldData({})).toEqual([]);
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/services/utils/getUploadProvider.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash');
2 | const path = require('path');
3 |
4 | module.exports = async () => {
5 | const uploadProviderConfig = await strapi
6 | .store({
7 | environment: strapi.config.environment,
8 | type: 'plugin',
9 | name: 'upload'
10 | })
11 | .get({ key: 'provider' });
12 |
13 | // Get upload provider settings to configure the provider to use.
14 | const provider = _.find(strapi.plugins.upload.config.providers, {
15 | provider: uploadProviderConfig.provider
16 | });
17 |
18 | if (!provider) {
19 | throw new Error(
20 | `The provider package isn't installed. Please run \`npm install strapi-provider-upload-${
21 | uploadProviderConfig.provider
22 | }\``
23 | );
24 | }
25 |
26 | const getPath = file =>
27 | uploadProviderConfig.provider === 'local'
28 | ? path.join(strapi.config.appPath, strapi.config.public.path, file.url)
29 | : file.url;
30 |
31 | return {
32 | config: uploadProviderConfig,
33 | provider,
34 | actions: await provider.init(uploadProviderConfig),
35 | getPath
36 | };
37 | };
38 |
--------------------------------------------------------------------------------
/services/utils/importFields.js:
--------------------------------------------------------------------------------
1 | const striptags = require('striptags');
2 |
3 | const importFields = async (sourceItem, fieldMapping) => {
4 | const importedItem = {};
5 |
6 | Object.keys(fieldMapping).forEach(async sourceField => {
7 | const { targetField, stripTags } = fieldMapping[sourceField];
8 | if (!targetField || targetField === 'none') {
9 | return;
10 | }
11 |
12 | const originalValue = sourceItem[sourceField];
13 |
14 | importedItem[targetField] = stripTags
15 | ? striptags(originalValue)
16 | : originalValue;
17 | });
18 | return importedItem;
19 | };
20 |
21 | module.exports = importFields;
22 |
--------------------------------------------------------------------------------
/services/utils/importFields.spec.js:
--------------------------------------------------------------------------------
1 | const importFields = require('./importFields');
2 |
3 | describe('importField', () => {
4 | it('copies field data into result', async () => {
5 | const result = await importFields(
6 | { a: 'hello' },
7 | { a: { targetField: 'b' } }
8 | );
9 | expect(result['b']).toEqual('hello');
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/services/utils/importMediaFiles.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash');
2 | const request = require('request');
3 | const getUploadProvider = require('./getUploadProvider');
4 | const fileFromBuffer = require('./fileFromBuffer');
5 | const getMediaUrlsFromFieldData = require('./getMediaUrlsFromFieldData');
6 |
7 | const fetchAndStoreFiles = url =>
8 | new Promise((resolve, reject) => {
9 | request({ url, method: 'GET', encoding: null }, async (err, res, body) => {
10 | if (err) {
11 | reject(err);
12 | }
13 |
14 | const mimeType = res.headers['content-type'].split(';').shift();
15 |
16 | const parsed = new URL(url);
17 | const extension = parsed.pathname
18 | .split('.')
19 | .pop()
20 | .toLowerCase();
21 |
22 | const { provider, actions } = await getUploadProvider();
23 |
24 | const fileDescriptor = fileFromBuffer(mimeType, extension, body);
25 |
26 | await actions.upload(fileDescriptor);
27 |
28 | delete fileDescriptor.buffer;
29 |
30 | fileDescriptor.provider = provider.provider;
31 |
32 | resolve(fileDescriptor);
33 | });
34 | });
35 |
36 | const relateFileToContent = async ({
37 | contentType,
38 | contentId,
39 | targetField,
40 | fileDescriptor
41 | }) => {
42 | fileDescriptor.related = [
43 | {
44 | refId: contentId,
45 | ref: contentType,
46 | source: 'content-manager',
47 | field: targetField
48 | }
49 | ];
50 |
51 | return await strapi.plugins['upload'].services.upload.add(fileDescriptor);
52 | };
53 |
54 | const importMediaFiles = async (savedContent, sourceItem, importConfig) => {
55 | const { fieldMapping, contentType } = importConfig;
56 |
57 | const uploadedFileDescriptors = _.mapValues(
58 | fieldMapping,
59 | async (mapping, sourceField) => {
60 | if (mapping.importMediaToField) {
61 | const urls = getMediaUrlsFromFieldData(sourceItem[sourceField]);
62 |
63 | const uploadPromises = _.uniq(urls).map(fetchAndStoreFiles);
64 |
65 | const fileDescriptors = await Promise.all(uploadPromises);
66 |
67 | const relateContentPromises = fileDescriptors.map(fileDescriptor =>
68 | relateFileToContent({
69 | contentType,
70 | contentId: savedContent.id,
71 | targetField: mapping.importMediaToField,
72 | fileDescriptor
73 | })
74 | );
75 |
76 | return await Promise.all(relateContentPromises);
77 | }
78 | }
79 | );
80 |
81 | return await Promise.all(_.values(uploadedFileDescriptors));
82 | };
83 |
84 | module.exports = importMediaFiles;
85 |
--------------------------------------------------------------------------------
/services/utils/stringIsEmail.js:
--------------------------------------------------------------------------------
1 | const EMAIL_REGEXP = /^(([^<>()\[\]\\.,;:\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,}))$/;
2 |
3 | const stringIsEmail = data => {
4 | EMAIL_REGEXP.lastIndex = 0;
5 | return EMAIL_REGEXP.test(data);
6 | };
7 |
8 | module.exports = stringIsEmail;
9 |
--------------------------------------------------------------------------------
/services/utils/stringIsEmail.spec.js:
--------------------------------------------------------------------------------
1 | const stringIsEmail = require('./stringIsEmail');
2 |
3 | const GOOD_EMAILS = ['jim@jimspage.com', 'j45987098712@gmail.org'];
4 | const NON_EMAILS = ['💩', 'http://businesstime.com'];
5 |
6 | describe('stringIsEmail', () => {
7 | it('recognizes good emails', () => {
8 | GOOD_EMAILS.forEach(str => expect(stringIsEmail(str)).toBeTruthy());
9 | });
10 | it('recognizes non emails', () => {
11 | NON_EMAILS.forEach(str => expect(stringIsEmail(str)).toBeFalsy());
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/services/utils/urlIsMedia.js:
--------------------------------------------------------------------------------
1 | const urlIsMedia = url => {
2 | try {
3 | const parsed = new URL(url);
4 |
5 | const extension = parsed.pathname
6 | .split('.')
7 | .pop()
8 | .toLowerCase();
9 |
10 | switch (extension) {
11 | case 'png':
12 | case 'gif':
13 | case 'jpg':
14 | case 'jpeg':
15 | case 'svg':
16 | case 'bmp':
17 | case 'tif':
18 | case 'tiff':
19 | return true;
20 |
21 | case 'mp3':
22 | case 'wav':
23 | case 'ogg':
24 | return true;
25 |
26 | case 'mp4':
27 | case 'avi':
28 | return true;
29 |
30 | default:
31 | return false;
32 | }
33 | } catch (error) {
34 | // Was likely a bad URL
35 | return false;
36 | }
37 | };
38 |
39 | module.exports = urlIsMedia;
40 |
--------------------------------------------------------------------------------
/services/utils/urlIsMedia.spec.js:
--------------------------------------------------------------------------------
1 | const urlIsMedia = require('./urlIsMedia');
2 |
3 | describe('urlIsMedia', () => {
4 | it('returns true for image urls', () => {
5 | const URLS = [
6 | 'http://site.com/image.gif',
7 | 'http://other.site.com/image.gif?hello=1',
8 | 'http://127.0.0.1/folder/image.PNG',
9 | 'http://www.website.com/two/folders/gif.jpeg.JPEG',
10 | 'https://site.com/thing.jpg'
11 | ];
12 |
13 | URLS.forEach(url => {
14 | expect(urlIsMedia(url)).toBeTruthy();
15 | });
16 | });
17 |
18 | it('returns false for non-image urls', () => {
19 | const URLS = [
20 | 'http://site.com/gif.text',
21 | 'https://homepage/location.gis',
22 | 'https://localhost:333/page.html'
23 | ];
24 |
25 | URLS.forEach(url => {
26 | expect(urlIsMedia(url)).toBeFalsy();
27 | });
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/services/utils/validateUrl.js:
--------------------------------------------------------------------------------
1 | var urlRegEx = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-]*)?\??(?:[\-\+=&;%@\.\w]*)#?(?:[\.\!\/\\\w]*))?)/g;
2 |
3 | const URL_REGEXP = new RegExp(urlRegEx);
4 |
5 | const validateUrl = url => {
6 | URL_REGEXP.lastIndex = 0;
7 | return URL_REGEXP.test(url);
8 | };
9 |
10 | module.exports = validateUrl;
11 |
--------------------------------------------------------------------------------
/services/utils/validateUrl.spec.js:
--------------------------------------------------------------------------------
1 | const validateUrl = require('./validateUrl');
2 |
3 | const GOOD_URLS = [
4 | 'http://cernalerts.web.cern.ch/cernalerts/?feed=cern%20hot%20news',
5 | 'https://www.nasa.gov/rss/dyn/lg_image_of_the_day.rss'
6 | ];
7 |
8 | const BAD_URLS = ['hello my name is', '1234'];
9 |
10 | describe('validateUrl', () => {
11 | it('returns true for valid urls', () => {
12 | GOOD_URLS.forEach(url => expect(validateUrl(url)).toBeTruthy());
13 | });
14 | it('returns false for not urls', () => {
15 | BAD_URLS.forEach(url => expect(validateUrl(url)).toBeFalsy());
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/video_thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbeuckm/strapi-plugin-import-content/6c6131826bff162b107294005b035ab0c43a72a2/video_thumbnail.png
--------------------------------------------------------------------------------