├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── .npmignore ├── README.md ├── admin └── src │ ├── components │ ├── DuplicateButton │ │ └── index.jsx │ └── Initializer │ │ └── index.js │ ├── index.js │ ├── pluginId.js │ ├── prefixPluginTranslations.js │ └── translations │ ├── de.json │ ├── en.json │ ├── fr.json │ ├── pl.json │ └── uk.json ├── doc ├── logo-128.png ├── logo-160.jpg ├── logo.png └── screen.jpg ├── package-lock.json ├── package.json └── rollup.config.mjs /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | - push 8 | 9 | jobs: 10 | publish-npm: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 18 17 | registry-url: https://registry.npmjs.org/ 18 | - run: npm publish 19 | env: 20 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # misc 5 | .DS_Store 6 | 7 | # debug 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # build 13 | /admin/dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /admin/source 2 | doc 3 | ./.* 4 | rollup.config.mjs 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Logo - Strapi Duplicate Button 3 |
4 | 5 | # Strapi Plugin Duplicate Button 6 | > Adds a Duplicate Button to the edit view 7 | 8 | ![](doc/screen.jpg) 9 | 10 | ## How to Install 11 | 12 | Copy the following code and run from your terminal 13 | 14 | ``` 15 | #yarn 16 | yarn add strapi-plugin-duplicate-button 17 | ``` 18 | ``` 19 | #pnpm 20 | pnpm add strapi-plugin-duplicate-button 21 | ``` 22 | ``` 23 | #npm 24 | npm install strapi-plugin-duplicate-button 25 | ``` 26 | 27 | The plugin should now be active and show the duplicate button. 28 | 29 | ## How to use 30 | After activation of the Plugin, click the Duplicate Button in the edit view. 31 | 32 | ## Troubleshooting 33 | If the duplicate button does not show up, try adding the following attribute in the `config/plugins.js` file: 34 | 35 | ``` 36 | 'duplicate-button': true 37 | ``` 38 | Or if you do not have the plugins.js file yet, add the file with the following contents: 39 | ``` 40 | module.exports = () => ({ 41 | 'duplicate-button': true, 42 | }); 43 | ``` 44 | 45 | ## Issues 46 | 47 | Please report any issues on [GitHub](https://github.com/lautr/strapi-plugin-duplicate-button/issues/new). 48 | -------------------------------------------------------------------------------- /admin/src/components/DuplicateButton/index.jsx: -------------------------------------------------------------------------------- 1 | import { LinkButton } from "@strapi/design-system"; 2 | import { Duplicate } from "@strapi/icons"; 3 | import React from "react"; 4 | import { useIntl } from "react-intl"; 5 | import { useLocation, useNavigate, useParams } from "react-router-dom"; 6 | 7 | const DuplicateButton = () => { 8 | const { id: documentId, slug: modelUID, collectionType } = useParams(); 9 | const isSingleType = collectionType === "single-types"; 10 | 11 | const { formatMessage } = useIntl(); 12 | 13 | const content = { 14 | id: "duplicate-button.components.duplicate.button", 15 | defaultMessage: "Duplicate", 16 | }; 17 | 18 | const navigate = useNavigate(); 19 | const { pathname, search } = useLocation(); 20 | const goToCloneRoute = (evt) => { 21 | evt.preventDefault(); 22 | navigate(pathname.replace(modelUID, `${modelUID}/clone`) + search); 23 | }; 24 | 25 | if (isSingleType || !documentId || !modelUID) return null; 26 | if (documentId === "create" || documentId === "clone") return null; 27 | return ( 28 | <> 29 | {window && window.location ? ( 30 | } 40 | onClick={goToCloneRoute} 41 | > 42 | {formatMessage(content)} 43 | 44 | ) : null} 45 | 46 | ); 47 | }; 48 | 49 | export default DuplicateButton; 50 | -------------------------------------------------------------------------------- /admin/src/components/Initializer/index.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import { useEffect, useRef } from "react"; 3 | import pluginId from "../../pluginId"; 4 | 5 | const Initializer = ({ setPlugin }) => { 6 | const ref = useRef(); 7 | ref.current = setPlugin; 8 | 9 | useEffect(() => { 10 | ref.current(pluginId); 11 | }, []); 12 | 13 | return null; 14 | }; 15 | 16 | Initializer.propTypes = { 17 | setPlugin: PropTypes.func.isRequired, 18 | }; 19 | 20 | export default Initializer; 21 | -------------------------------------------------------------------------------- /admin/src/index.js: -------------------------------------------------------------------------------- 1 | import pluginPkg from "../../package.json"; 2 | import DuplicateButton from "./components/DuplicateButton"; 3 | import Initializer from "./components/Initializer"; 4 | import pluginId from "./pluginId"; 5 | import prefixPluginTranslations from "./prefixPluginTranslations"; 6 | 7 | const name = pluginPkg.strapi.name; 8 | 9 | export default { 10 | register(app) { 11 | app.registerPlugin({ 12 | id: pluginId, 13 | initializer: Initializer, 14 | isReady: false, 15 | name, 16 | }); 17 | }, 18 | 19 | bootstrap(app) { 20 | app 21 | .getPlugin("content-manager") 22 | .injectComponent("editView", "right-links", { 23 | name: pluginId, 24 | Component: DuplicateButton, 25 | }); 26 | }, 27 | async registerTrads({ locales }) { 28 | const importedTrads = await Promise.all( 29 | locales.map((locale) => { 30 | return import(`./translations/${locale}.json`) 31 | .then(({ default: data }) => { 32 | return { 33 | data: prefixPluginTranslations(data, pluginId), 34 | locale, 35 | }; 36 | }) 37 | .catch(() => { 38 | return { 39 | data: {}, 40 | locale, 41 | }; 42 | }); 43 | }) 44 | ); 45 | 46 | return Promise.resolve(importedTrads); 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /admin/src/pluginId.js: -------------------------------------------------------------------------------- 1 | import pluginPkg from "../../package.json"; 2 | 3 | const pluginId = pluginPkg.name.replace( 4 | /^(@[^-,.][\w,-]+\/|strapi-)plugin-/i, 5 | "" 6 | ); 7 | 8 | export default pluginId; 9 | -------------------------------------------------------------------------------- /admin/src/prefixPluginTranslations.js: -------------------------------------------------------------------------------- 1 | const prefixPluginTranslations = (trad, pluginId) => { 2 | if (!pluginId) { 3 | throw new TypeError("pluginId can't be empty"); 4 | } 5 | 6 | return Object.keys(trad).reduce((acc, current) => { 7 | acc[`${pluginId}.${current}`] = trad[current]; 8 | 9 | return acc; 10 | }, {}); 11 | }; 12 | 13 | export default prefixPluginTranslations; 14 | -------------------------------------------------------------------------------- /admin/src/translations/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "components.duplicate.button": "Duplizieren" 3 | } 4 | -------------------------------------------------------------------------------- /admin/src/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "components.duplicate.button": "Duplicate" 3 | } 4 | -------------------------------------------------------------------------------- /admin/src/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "components.duplicate.button": "Dupliquer" 3 | } 4 | -------------------------------------------------------------------------------- /admin/src/translations/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "components.duplicate.button": "Duplikuj" 3 | } 4 | -------------------------------------------------------------------------------- /admin/src/translations/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "components.duplicate.button": "Дублювати" 3 | } 4 | -------------------------------------------------------------------------------- /doc/logo-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lautr/strapi-plugin-duplicate-button/ca38f303a78ecc95a69342e80f7746557d19917b/doc/logo-128.png -------------------------------------------------------------------------------- /doc/logo-160.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lautr/strapi-plugin-duplicate-button/ca38f303a78ecc95a69342e80f7746557d19917b/doc/logo-160.jpg -------------------------------------------------------------------------------- /doc/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lautr/strapi-plugin-duplicate-button/ca38f303a78ecc95a69342e80f7746557d19917b/doc/logo.png -------------------------------------------------------------------------------- /doc/screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lautr/strapi-plugin-duplicate-button/ca38f303a78ecc95a69342e80f7746557d19917b/doc/screen.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "strapi-plugin-duplicate-button", 3 | "version": "2.0.0", 4 | "description": "Adds a duplicate button to the edit view", 5 | "strapi": { 6 | "name": "duplicate-button", 7 | "description": "Adds a duplicate button to the edit view", 8 | "kind": "plugin", 9 | "displayName": "Duplicate Button" 10 | }, 11 | "peerDependencies": { 12 | "@strapi/design-system": "^2.0.0-rc.10", 13 | "@strapi/icons": "^2.0.0-rc.10", 14 | "@strapi/strapi": "^5.0.0-rc.17", 15 | "@strapi/utils": "^5.0.0-rc.17", 16 | "react-intl": "6.6.2", 17 | "react-router-dom": "6.22.3" 18 | }, 19 | "author": { 20 | "name": "Johannes Lauter" 21 | }, 22 | "maintainers": [ 23 | { 24 | "name": "Johannes Lauter" 25 | } 26 | ], 27 | "exports": { 28 | "./strapi-admin": { 29 | "source": "./admin/src/index.js", 30 | "import": "./admin/dist/index.mjs", 31 | "require": "./admin/dist/index.js", 32 | "default": "./admin/dist/index.js" 33 | }, 34 | "./package.json": "./package.json" 35 | }, 36 | "scripts": { 37 | "build": "rollup -c", 38 | "prepublish": "npm run build" 39 | }, 40 | "engines": { 41 | "node": ">=18.0.0 <=20.x.x" 42 | }, 43 | "license": "MIT", 44 | "devDependencies": { 45 | "@babel/preset-react": "^7.24.7", 46 | "@rollup/plugin-babel": "^6.0.4", 47 | "@rollup/plugin-dynamic-import-vars": "^2.1.2", 48 | "@rollup/plugin-json": "^6.1.0", 49 | "@rollup/plugin-node-resolve": "^15.2.3" 50 | } 51 | } -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import babel from "@rollup/plugin-babel"; 2 | import dynamicImportVariables from "@rollup/plugin-dynamic-import-vars"; 3 | import json from "@rollup/plugin-json"; 4 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 5 | import path from "path"; 6 | 7 | export default { 8 | input: "admin/src/index.js", 9 | output: [ 10 | { 11 | file: "admin/dist/index.mjs", 12 | format: "es", 13 | sourcemap: true, 14 | inlineDynamicImports: true, 15 | }, 16 | { 17 | file: "admin/dist/index.js", 18 | format: "cjs", 19 | sourcemap: true, 20 | inlineDynamicImports: true, 21 | }, 22 | ], 23 | plugins: [ 24 | nodeResolve({ 25 | extensions: [".js", ".jsx"], 26 | }), 27 | json(), 28 | babel({ 29 | presets: ["@babel/preset-react"], 30 | babelHelpers: "bundled", 31 | }), 32 | dynamicImportVariables({}), 33 | ], 34 | external: (id) => { 35 | return id.startsWith(path.resolve("node_modules")); 36 | }, 37 | }; 38 | --------------------------------------------------------------------------------