First commit

This commit is contained in:
Baboo7 2022-05-27 17:44:21 +02:00
commit 56f867827c
52 changed files with 1388 additions and 0 deletions

132
.gitignore vendored Normal file
View File

@ -0,0 +1,132 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.DS_Store

57
README.md Normal file
View File

@ -0,0 +1,57 @@
# Strapi Plugin Import Export
This plugin helps you import and export data from and to your database in just few clicks.
<img src="./doc/scr-ui.png" alt="UI" width="500"/>
## Requirements
Strapi v4 is required.
## Installation
```
yarn add strapi-plugin-import-export-entries
```
or
```
npm i strapi-plugin-import-export-entries
```
## Rebuild the admin panel
New releases can introduce changes to the administration panel that require a rebuild. Rebuild the admin panel with one of the following commands:
```
yarn build --clean
```
or
```
npm run build --clean
```
## Features
### Import
- Import data directly from the Content Manager
- Read data from CSV and JSON file or from typing raw text
- Import contents to collection type (NOT single type yet)
### Export
- Export data directly from the Content Manager
- Export CSV and JSON contents
- Download files or copy exported data to clipboard
## Author
Baboo - [@Baboo7](https://github.com/Baboo7)
## Acknowledgments
This plugin (and especially this README) took strong inspiration from the [strapi-plugin-import-export-content](https://github.com/EdisonPeM/strapi-plugin-import-export-content#readme) from [EdisonPeM](https://github.com/EdisonPeM).

View File

@ -0,0 +1,13 @@
import { request } from "@strapi/helper-plugin";
const getByContentType = async ({ slug, search, applySearch }) => {
const data = await request(`/import-export/export/contentTypes`, {
method: "POST",
body: { slug, search, applySearch },
});
return data;
};
export default {
getByContentType,
};

View File

@ -0,0 +1 @@
export { default } from "./ExportProxy";

View File

@ -0,0 +1,13 @@
import { request } from "@strapi/helper-plugin";
const importData = async ({ slug, data, format }) => {
const resData = await request(`/import-export/import`, {
method: "POST",
body: { slug, data, format },
});
return resData;
};
export default {
importData,
};

View File

@ -0,0 +1 @@
export { default } from "./ImportProxy";

View File

@ -0,0 +1,31 @@
import EditorLib from "@monaco-editor/react";
import React from "react";
import "./style.css";
export const Editor = ({
content = "",
language = "csv",
readOnly = false,
onChange,
style,
}) => {
return (
<EditorLib
className="plugin-ie-editor"
style={style}
height="30vh"
theme="vs-dark"
language={language}
value={content}
onChange={onChange}
options={{
readOnly,
minimap: {
enabled: false,
},
scrollBeyondLastLine: false,
}}
/>
);
};

View File

@ -0,0 +1 @@
export * from "./Editor";

View File

@ -0,0 +1,4 @@
.plugin-ie-editor {
overflow: hidden;
border-radius: 8px;
}

View File

@ -0,0 +1,216 @@
import { Button } from "@strapi/design-system/Button";
import { Checkbox } from "@strapi/design-system/Checkbox";
import { EmptyStateLayout } from "@strapi/design-system/EmptyStateLayout";
import {
ModalLayout,
ModalBody,
ModalHeader,
ModalFooter,
} from "@strapi/design-system/ModalLayout";
import { Flex } from "@strapi/design-system/Flex";
import { Grid, GridItem } from "@strapi/design-system/Grid";
import { Loader } from "@strapi/design-system/Loader";
import { Portal } from "@strapi/design-system/Portal";
import { Select, Option } from "@strapi/design-system/Select";
import { Typography } from "@strapi/design-system/Typography";
import IconFile from "@strapi/icons/File";
import { pick } from "lodash";
import React, { useEffect, useState } from "react";
import { useIntl } from "react-intl";
import { useLocation } from "react-router-dom";
import qs from "qs";
import "./style.css";
import ExportProxy from "../../api/exportProxy";
import { useDownloadFile } from "../../hooks/useDownloadFile";
import { useSlug } from "../../hooks/useSlug";
import { dataConverterConfigs, dataFormats } from "../../utils/dataConverter";
import getTrad from "../../utils/getTrad";
import { Editor } from "../Editor/Editor";
import { useAlerts } from "../../hooks/useAlerts";
export const ExportModal = ({ onClose }) => {
const { formatMessage } = useIntl();
const { search } = useLocation();
const { downloadFile, withTimestamp } = useDownloadFile();
const { slug } = useSlug();
const { notify } = useAlerts();
const [optionApplyFilters, setOptionApplyFilters] = useState(false);
const [exportFormat, setExportFormat] = useState(dataFormats.CSV);
const [data, setData] = useState(null);
const [dataConverted, setDataConverted] = useState("");
const [fetchingData, setFetchingData] = useState(false);
useEffect(() => {
convertData();
}, [data, exportFormat]);
const getData = async () => {
const searchQry = qs.stringify(pick(qs.parse(search), ["filters", "sort"]));
setFetchingData(true);
const data = await getEntries(slug, searchQry);
setData(data);
setFetchingData(false);
};
const convertData = async () => {
if (!data) {
return;
}
const converter = dataConverterConfigs[exportFormat];
if (!converter) {
throw new Error(
`File extension ${exportFormat} not supported to export data.`
);
}
const { convertData } = converter;
setDataConverted(convertData(data));
};
const writeDataToFile = async () => {
const converter = dataConverterConfigs[exportFormat];
if (!converter) {
throw new Error(
`File extension ${exportFormat} not supported to export data.`
);
}
const { fileExt, fileContentType } = converter;
const fileName = `export_${slug}.${fileExt}`
.replaceAll(":", "-")
.replaceAll("--", "-");
downloadFile(
dataConverted,
withTimestamp(fileName),
`${fileContentType};charset=utf-8;`
);
};
const copyToClipboard = () => {
navigator.clipboard.writeText(dataConverted);
notify(
"Copied to clipboard",
"Your data has been copied to your clipboard successfully.",
"success"
);
};
const clearData = () => {
setData(null);
};
const getEntries = async (slug, search) => {
const data = await ExportProxy.getByContentType({
slug,
search,
applySearch: optionApplyFilters,
});
return data.data;
};
return (
<Portal>
<ModalLayout onClose={onClose} labelledBy="title">
<ModalHeader>
<Typography
fontWeight="bold"
textColor="neutral800"
as="h2"
id="title"
>
Export
</Typography>
</ModalHeader>
<ModalBody className="plugin-ie-export_modal_body">
{fetchingData && (
<>
<Flex justifyContent="center">
<Loader>Fetching data...</Loader>
</Flex>
</>
)}
{!data && !fetchingData && (
<>
<Flex direction="column" alignItems="start" gap="16px">
<Typography fontWeight="bold" textColor="neutral800" as="h2">
Options
</Typography>
<Checkbox
value={optionApplyFilters}
onValueChange={setOptionApplyFilters}
>
Apply filters and sort to exported data.
</Checkbox>
</Flex>
</>
)}
{data && !fetchingData && (
<>
<Grid gap={8}>
<GridItem col={12}>
<Select
id="export-format"
label="Export Format"
required
placeholder="Export Format"
value={exportFormat}
onChange={setExportFormat}
>
<Option value={dataFormats.CSV}>
{dataFormats.CSV.toUpperCase()}
</Option>
<Option value={dataFormats.JSON}>
{dataFormats.JSON.toUpperCase()}
</Option>
</Select>
</GridItem>
</Grid>
{!!data && (
<Editor content={dataConverted} language={exportFormat} />
)}
</>
)}
</ModalBody>
<ModalFooter
startActions={
<>
{!!data && (
<Button variant="tertiary" onClick={clearData}>
{formatMessage({
id: getTrad("plugin.cta.back-to-options"),
})}
</Button>
)}
</>
}
endActions={
<>
{!data && (
<Button onClick={getData}>
{formatMessage({ id: getTrad("plugin.cta.get-data") })}
</Button>
)}
{!!data && (
<>
<Button variant="secondary" onClick={copyToClipboard}>
{formatMessage({
id: getTrad("plugin.cta.copy-to-clipboard"),
})}
</Button>
<Button onClick={writeDataToFile}>
{formatMessage({ id: getTrad("plugin.cta.download-file") })}
</Button>
</>
)}
</>
}
/>
</ModalLayout>
</Portal>
);
};

View File

@ -0,0 +1 @@
export * from "./ExportModal";

View File

@ -0,0 +1,4 @@
.plugin-ie-export_modal_body > *:not(:first-child) {
margin-top: 16px;
}

View File

@ -0,0 +1,188 @@
import { Button } from "@strapi/design-system/Button";
import { Flex } from "@strapi/design-system/Flex";
import {
ModalLayout,
ModalBody,
ModalHeader,
ModalFooter,
} from "@strapi/design-system/ModalLayout";
import { Portal } from "@strapi/design-system/Portal";
import { Typography } from "@strapi/design-system/Typography";
import IconFile from "@strapi/icons/File";
import React, { useState } from "react";
import { useIntl } from "react-intl";
import "./style.css";
import ImportProxy from "../../api/importProxy";
import { useSlug } from "../../hooks/useSlug";
import { dataFormats } from "../../utils/dataConverter";
import getTrad from "../../utils/getTrad";
import { Editor } from "../Editor/Editor";
import { useAlerts } from "../../hooks/useAlerts";
export const ImportModal = ({ onClose }) => {
const { formatMessage } = useIntl();
const { slug } = useSlug();
const { notify } = useAlerts();
const [data, setData] = useState("");
const [dataFormat, setDataFormat] = useState(dataFormats.CSV);
const [labelClassNames, setLabelClassNames] = useState(
"plugin-ie-import_modal_input-label"
);
const onDataChanged = (data) => {
setData(data);
};
const onReadFile = (e) => {
const file = e.target.files[0];
readFile(file);
};
const readFile = (file) => {
if (file.type === "text/csv") {
setDataFormat(dataFormats.CSV);
} else if (file.type === "application/json") {
setDataFormat(dataFormats.JSON);
} else {
throw new Error(`File type ${file.type} not supported.`);
}
const reader = new FileReader();
reader.onload = async (e) => {
const text = e.target.result;
setData(text);
};
reader.readAsText(file);
};
const removeFile = () => {
setData("");
};
const uploadData = async () => {
try {
await ImportProxy.importData({ slug, data, format: dataFormat });
notify(
"Import successful",
"Your data has been imported successfully. Refresh your page to see the latest updates.",
"success"
);
} catch (err) {
notify(
"Import failed",
"An error occured while importing your data",
"danger"
);
}
};
const handleDragOver = (e) => {
e.preventDefault();
e.stopPropagation();
};
const handleDragEnter = (e) => {
e.preventDefault();
e.stopPropagation();
setLabelClassNames(
[
labelClassNames,
"plugin-ie-import_modal_input-label--dragged-over",
].join(" ")
);
};
const handleDragLeave = () => {
setLabelClassNames(
labelClassNames.replaceAll(
"plugin-ie-import_modal_input-label--dragged-over",
""
)
);
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
handleDragLeave();
const file = e.dataTransfer.files[0];
readFile(file);
};
return (
<Portal>
<ModalLayout onClose={onClose} labelledBy="title">
<ModalHeader>
<Typography
fontWeight="bold"
textColor="neutral800"
as="h2"
id="title"
>
Import
</Typography>
</ModalHeader>
<ModalBody className="plugin-ie-import_modal_body">
{!data && (
<Flex>
<label
className={labelClassNames}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
>
<span style={{ fontSize: 80 }}>
<IconFile />
</span>
<Typography
style={{ fontSize: "1rem", fontWeight: 500 }}
textColor="neutral600"
as="p"
>
Drag &amp; drop your file into this area or browse for a file
to upload
</Typography>
<input
type="file"
accept=".csv,.json"
hidden=""
onChange={onReadFile}
/>
</label>
</Flex>
)}
{data && (
<Editor
content={data}
language={dataFormat}
onChange={onDataChanged}
/>
)}
</ModalBody>
<ModalFooter
startActions={
<>
{data && (
<Button onClick={removeFile} variant="tertiary">
{formatMessage({ id: getTrad("plugin.cta.remove-file") })}
</Button>
)}
</>
}
endActions={
<>
{data && (
<Button onClick={uploadData}>
{formatMessage({ id: getTrad("plugin.cta.import") })}
</Button>
)}
</>
}
/>
</ModalLayout>
</Portal>
);
};

View File

@ -0,0 +1 @@
export * from "./ImportModal";

View File

@ -0,0 +1,47 @@
.plugin-ie-import_modal_body > *:not(:first-child) {
margin-top: 16px;
}
.plugin-ie-import_modal_input-label {
--hover-color: hsl(210, 100%, 50%);
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
padding: 48px;
border-width: 3px;
border-color: #ddd;
border-style: dashed;
border-radius: 12px;
cursor: pointer;
}
.plugin-ie-import_modal_input-label:hover {
border-color: var(--hover-color);
}
.plugin-ie-import_modal_input-label--dragged-over {
border-color: var(--hover-color);
}
.plugin-ie-import_modal_input-label--dragged-over::after {
content: "";
display: block;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 5;
}
.plugin-ie-import_modal_input-label > *:not(:first-child) {
margin-top: 16px;
}
.plugin-ie-import_modal_input-label input {
display: none;
}

View File

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

View File

@ -0,0 +1,33 @@
import { Alert } from "@strapi/design-system/Alert";
import { Button } from "@strapi/design-system/Button";
import { Portal } from "@strapi/design-system/Portal";
import Download from "@strapi/icons/Download";
import React from "react";
import { useIntl } from "react-intl";
import "./style.css";
import getTrad from "../../../utils/getTrad";
import { ExportModal } from "../../ExportModal";
import { useAlerts } from "../../../hooks/useAlerts";
export const Alerts = () => {
const { alerts, removeAlert } = useAlerts();
return (
<Portal>
<div className="plugin-ie-alerts">
{alerts?.map(({ id, title, message, variant }) => (
<Alert
key={id}
closeLabel="Close"
title={title}
variant={variant}
onClose={() => removeAlert(id)}
>
{message}
</Alert>
))}
</div>
</Portal>
);
};

View File

@ -0,0 +1 @@
export * from "./Alerts";

View File

@ -0,0 +1,12 @@
.plugin-ie-alerts {
position: fixed;
top: 0;
left: 50%;
transform: translate(-50%, 0);
z-index: 10000;
padding: 16px;
}
.plugin-ie-alerts > *:not(:first-child) {
margin-top: 16px;
}

View File

@ -0,0 +1,31 @@
import { Button } from "@strapi/design-system/Button";
import Download from "@strapi/icons/Download";
import React, { useState } from "react";
import { useIntl } from "react-intl";
import getTrad from "../../utils/getTrad";
import { ExportModal } from "../ExportModal";
export const Export = () => {
const { formatMessage } = useIntl();
const [exportVisible, setExportVisible] = useState(false);
const openExportModal = () => {
setExportVisible(true);
};
const closeExportModal = () => {
setExportVisible(false);
};
return (
<>
<Button startIcon={<Download />} onClick={openExportModal}>
{formatMessage({ id: getTrad("plugin.cta.export") })}
</Button>
{exportVisible && <ExportModal onClose={closeExportModal} />}
</>
);
};

View File

@ -0,0 +1,31 @@
import { Button } from "@strapi/design-system/Button";
import Upload from "@strapi/icons/Upload";
import React, { useState } from "react";
import { useIntl } from "react-intl";
import getTrad from "../../utils/getTrad";
import { ImportModal } from "../ImportModal";
export const Import = () => {
const { formatMessage } = useIntl();
const [importVisible, setImportVisible] = useState(false);
const openImportModal = () => {
setImportVisible(true);
};
const closeImportModal = () => {
setImportVisible(false);
};
return (
<>
<Button startIcon={<Upload />} onClick={openImportModal}>
{formatMessage({ id: getTrad("plugin.cta.import") })}
</Button>
{importVisible && <ImportModal onClose={closeImportModal} />}
</>
);
};

View File

@ -0,0 +1,40 @@
import { useState, useRef } from "react";
import { singletonHook } from "react-singleton-hook";
const init = { loading: true };
const useAlertsImpl = () => {
const [alerts, setAlerts] = useState([]);
const [idCount, setIdCount] = useState(0);
const alertsRef = useRef(alerts);
alertsRef.current = alerts;
const notify = (title, message, variant = "default") => {
const alert = {
id: idCount,
timeout: setTimeout(() => removeAlert(idCount), 8000),
variant,
title,
message,
};
setAlerts(alerts.concat(alert));
setIdCount(idCount + 1);
};
const removeAlert = (id) => {
const alerts = alertsRef.current;
const alert = alerts.find((a) => a.id === id);
clearTimeout(alert.timeout);
const alertsFiltered = alerts.filter((a) => a.id !== id);
setAlerts(alertsFiltered);
};
return {
alerts,
notify,
removeAlert,
};
};
export const useAlerts = singletonHook(init, useAlertsImpl);

View File

@ -0,0 +1,24 @@
export const useDownloadFile = () => {
const downloadFile = (content, filename, contentType) => {
var blob = new Blob([content], { type: contentType });
var url = URL.createObjectURL(blob);
var link = document.createElement("a");
link.href = url;
link.setAttribute("download", filename);
link.click();
};
const withTimestamp = (fileName) => {
const ts = new Date().toISOString().replace(/\D/g, "").substring(2);
const name = fileName.split(".").slice(0, -1).join(".").concat(`_${ts}`);
const extension = fileName.split(".").slice(-1);
return [name, extension].join(".");
};
return {
downloadFile,
withTimestamp,
};
};

View File

@ -0,0 +1,17 @@
import { last } from "lodash";
import { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
export const useSlug = () => {
const { pathname } = useLocation();
const [slug, setSlug] = useState("");
useEffect(() => {
setSlug(last(pathname.split("/")));
}, [pathname]);
return {
slug,
};
};

57
admin/src/index.js Normal file
View File

@ -0,0 +1,57 @@
import { prefixPluginTranslations } from "@strapi/helper-plugin";
import pluginPkg from "../../package.json";
import pluginId from "./pluginId";
import Initializer from "./components/Initializer";
import { Export } from "./components/Injected/export";
import { Import } from "./components/Injected/import";
import { Alerts } from "./components/Injected/Alerts";
const name = pluginPkg.strapi.name;
export default {
register(app) {
app.registerPlugin({
id: pluginId,
initializer: Initializer,
isReady: false,
name,
});
},
bootstrap(app) {
app.injectContentManagerComponent("listView", "actions", {
name: `${pluginId}-alerts`,
Component: Alerts,
});
app.injectContentManagerComponent("listView", "actions", {
name: `${pluginId}-import`,
Component: Import,
});
app.injectContentManagerComponent("listView", "actions", {
name: `${pluginId}-export`,
Component: Export,
});
},
async registerTrads({ locales }) {
const importedTrads = await Promise.all(
locales.map((locale) => {
return import(`./translations/${locale}.json`)
.then(({ default: data }) => {
return {
data: prefixPluginTranslations(data, pluginId),
locale,
};
})
.catch(() => {
return {
data: {},
locale,
};
});
})
);
return Promise.resolve(importedTrads);
},
};

View File

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

View File

@ -0,0 +1,20 @@
/*
*
* HomePage
*
*/
import React, { memo } from 'react';
// import PropTypes from 'prop-types';
import pluginId from '../../pluginId';
const HomePage = () => {
return (
<div>
<h1>{pluginId}&apos;s HomePage</h1>
<p>Happy coding</p>
</div>
);
};
export default memo(HomePage);

5
admin/src/pluginId.js Normal file
View File

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

View File

@ -0,0 +1,12 @@
{
"plugin.name": "Import Export",
"plugin.cta.back-to-options": "Back To Options",
"plugin.cta.cancel": "Cancel",
"plugin.cta.copy-to-clipboard": "Copy To Clipboard",
"plugin.cta.download-file": "Download File",
"plugin.cta.get-data": "Fetch Data",
"plugin.cta.export": "Export",
"plugin.cta.import": "Import",
"plugin.cta.remove-file": "Remove File"
}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,40 @@
/**
* axios with a custom config.
*/
import axios from 'axios';
import { auth } from '@strapi/helper-plugin';
const instance = axios.create({
baseURL: process.env.STRAPI_ADMIN_BACKEND_URL,
});
instance.interceptors.request.use(
async config => {
config.headers = {
Authorization: `Bearer ${auth.getToken()}`,
Accept: 'application/json',
'Content-Type': 'application/json',
};
return config;
},
error => {
Promise.reject(error);
}
);
instance.interceptors.response.use(
response => response,
error => {
// whatever you want to do with the error
if (error.response?.status === 401) {
auth.clearAppStorage();
window.location.reload();
}
throw error;
}
);
export default instance;

View File

@ -0,0 +1,41 @@
export const dataFormats = {
CSV: "csv",
JSON: "json",
};
export const convertRowToArray = (row, keys) => {
return keys.map((key) => row[key]);
};
export const convertArrayToCsv = (row) => {
return row
.map(String)
.map((v) => v.replaceAll('"', '""'))
.map((v) => `"${v}"`)
.join(",");
};
export const convertToCsv = (rows) => {
const columnTitles = Object.keys(rows[0]);
const content = [convertArrayToCsv(columnTitles)]
.concat(
rows
.map((row) => convertRowToArray(row, columnTitles))
.map(convertArrayToCsv)
)
.join("\r\n");
return content;
};
export const dataConverterConfigs = {
[dataFormats.CSV]: {
convertData: convertToCsv,
fileExt: "csv",
fileContentType: "text/csv",
},
[dataFormats.JSON]: {
convertData: (data) => JSON.stringify(data, null, "\t"),
fileExt: "json",
fileContentType: "application/json",
},
};

View File

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

BIN
doc/scr-ui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

52
package.json Normal file
View File

@ -0,0 +1,52 @@
{
"name": "strapi-plugin-import-export-entries",
"version": "1.0.0",
"description": "This plugin helps you import and export data from and to your database in just few clicks.",
"strapi": {
"name": "Import / Export",
"description": "Import and export data from and to your database",
"kind": "plugin",
"displayName": "Import Export"
},
"dependencies": {
"@monaco-editor/react": "4.4.5",
"csvtojson": "2.0.10",
"react-singleton-hook": "3.3.0"
},
"peerDependencies": {
"@strapi/strapi": "^4.0.0"
},
"author": {
"name": "Baboo",
"url" : "https://github.com/Baboo7"
},
"maintainers": [
{
"name": "Baboo",
"url" : "https://github.com/Baboo7"
}
],
"engines": {
"node": ">=12.x.x <=16.x.x",
"npm": ">=6.0.0"
},
"keywords": [
"strapi",
"plugin",
"strapi",
"import",
"data",
"export",
"data",
"content"
],
"repository": {
"type": "git",
"url": "git+https://github.com/Baboo7/strapi-plugin-import-export-entries.git"
},
"bugs": {
"url": "https://github.com/Baboo7/strapi-plugin-import-export-entries/issues"
},
"homepage": "https://github.com/Baboo7/strapi-plugin-import-export-entries#readme",
"license": "MIT"
}

5
server/bootstrap.js vendored Normal file
View File

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

6
server/config/index.js Normal file
View File

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

View File

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

View File

@ -0,0 +1,39 @@
"use strict";
const qs = require("qs");
const exportData = async (ctx) => {
let { slug, search, applySearch } = ctx.request.body;
let query = {};
if (applySearch) {
query = buildFilterQuery(search);
}
const entries = await strapi.db.query(slug).findMany(query);
ctx.body = {
data: entries,
};
};
const buildFilterQuery = (search) => {
let { filters, sort } = qs.parse(search);
let where = filters;
const [attr, value] = sort?.split(":").map((v) => v.toLowerCase());
let orderBy = {};
if (attr && value) {
orderBy[attr] = value;
}
return {
where,
orderBy,
};
};
module.exports = ({ strapi }) => ({
exportData,
});

View File

@ -0,0 +1,62 @@
"use strict";
const csvtojson = require("csvtojson");
const isEmpty = require("lodash/isEmpty");
const importData = async (ctx) => {
const { slug, data: dataRaw, format } = ctx.request.body;
let data;
if (format === "csv") {
data = await csvtojson().fromString(dataRaw);
} else if (format === "json") {
data = JSON.parse(dataRaw);
}
const processed = await Promise.all(data.map(updateOrCreateFlow(slug)));
const failures = processed
.filter((p) => !p.success)
.map((f) => ({ error: f.error, data: f.args[0] }));
ctx.body = {
failures,
};
};
const updateOrCreateFlow = (slug) => async (d) => {
const res = await catchError((d) => updateOrCreate(slug, d), d);
return res;
};
const updateOrCreate = async (slug, data) => {
const where = {};
if (data.id) {
where.id = data.id;
}
let entry;
if (isEmpty(where)) {
entry = await strapi.db.query(slug).create({
data,
});
} else {
entry = await strapi.db.query(slug).update({
where,
data,
});
}
};
const catchError = async (fn, ...args) => {
try {
await fn(...args);
return { success: true };
} catch (err) {
return { success: false, error: err.message, args };
}
};
module.exports = ({ strapi }) => ({
importData,
});

View File

@ -0,0 +1,9 @@
"use strict";
const exportController = require("./export");
const importController = require("./import");
module.exports = {
export: exportController,
import: importController,
};

3
server/destroy.js Normal file
View File

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

25
server/index.js Normal file
View File

@ -0,0 +1,25 @@
'use strict';
const register = require('./register');
const bootstrap = require('./bootstrap');
const destroy = require('./destroy');
const config = require('./config');
const contentTypes = require('./content-types');
const controllers = require('./controllers');
const routes = require('./routes');
const middlewares = require('./middlewares');
const policies = require('./policies');
const services = require('./services');
module.exports = {
register,
bootstrap,
destroy,
config,
controllers,
routes,
services,
contentTypes,
policies,
middlewares,
};

View File

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

3
server/policies/index.js Normal file
View File

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

5
server/register.js Normal file
View File

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

13
server/routes/export.js Normal file
View File

@ -0,0 +1,13 @@
module.exports = {
type: "admin",
routes: [
{
method: "POST",
path: "/export/contentTypes",
handler: "export.exportData",
config: {
policies: [],
},
},
],
};

13
server/routes/import.js Normal file
View File

@ -0,0 +1,13 @@
module.exports = {
type: "admin",
routes: [
{
method: "POST",
path: "/import",
handler: "import.importData",
config: {
policies: [],
},
},
],
};

7
server/routes/index.js Normal file
View File

@ -0,0 +1,7 @@
const exportRoutes = require("./export");
const importRoutes = require("./import");
module.exports = {
export: exportRoutes,
import: importRoutes,
};

3
server/services/index.js Normal file
View File

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

3
strapi-admin.js Normal file
View File

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

3
strapi-server.js Normal file
View File

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