├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── dist ├── js │ └── field.js └── mix-manifest.json ├── img └── nova-duplicate-field-small.gif ├── package.json ├── resources └── js │ ├── components │ └── IndexField.vue │ └── field.js ├── routes └── api.php ├── src ├── DuplicateField.php ├── FieldServiceProvider.php └── Http │ └── Controllers │ └── DuplicateController.php ├── title.png └── webpack.mix.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [jackabox] 4 | patreon: jackabox 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | /node_modules 4 | package-lock.json 5 | composer.phar 6 | composer.lock 7 | phpunit.xml 8 | .phpunit.result.cache 9 | .DS_Store 10 | Thumbs.db 11 | 12 | yarn\.lock 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jack Whiting 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![Laravel Nova Duplicate Model](https://github.com/jackabox/nova-duplicate-field/raw/master/title.png) 2 | 3 | ### No Longer Maintained 4 | 5 | This plugin is no longer maintained. I've had no time to maintain this as I don't use Nova anymore. I'm looking for someone to take over this and handle it, let me know if you want to maintain it! See Issue #33. 6 | 7 | ### Information 8 | 9 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/jackabox/nova-duplicate-field?style=flat-square) 10 | ![Packagist](https://img.shields.io/packagist/dt/jackabox/nova-duplicate-field?style=flat-square) 11 | ![GitHub](https://img.shields.io/github/license/jackabox/nova-duplicate-field?style=flat-square) 12 | 13 | Allow users to duplicate a record through the Laravel Nova Admin Panel along with any relations that are required (currently works with HasMany). 14 | 15 | ### Installation 16 | 17 | ``` 18 | composer require jackabox/nova-duplicate-field 19 | ``` 20 | 21 | Reference the duplicate field at the top of your Nova resource and then include the necessary code within the fields. 22 | 23 | ```php 24 | use Jackabox\DuplicateField\DuplicateField 25 | ``` 26 | 27 | ```php 28 | DuplicateField::make('Duplicate') 29 | ->withMeta([ 30 | 'resource' => 'specialisms', // resource url 31 | 'model' => 'App\Models\Specialism', // model path 32 | 'id' => $this->id, // id of record 33 | 'relations' => ['one', 'two'], // an array of any relations to load (nullable). 34 | 'except' => ['status'], // an array of fields to not replicate (nullable). 35 | 'override' => ['status' => 'pending'] // an array of fields and values which will be set on the modal after duplicating (nullable). 36 | ]), 37 | ``` 38 | 39 | Duplicate field only works on the index view at the moment (plans to expand this are coming) and already passes through `onlyOnIndex()` as an option. 40 | 41 | ### Hooking Into Replication 42 | 43 | **Duplicate Field** uses a relatively standard replicate method which is available via the Eloquent model. To modify data as you are duplicating the field use an observer on the `replicating` method. 44 | 45 | ### Issues 46 | 47 | If there are any issues or requests feel free to open a GitHub issue or a pull request. 48 | 49 | ### Todo 50 | 51 | - [x] Duplicate relations alongside the main post. 52 | - [ ] Integrate reattaching of relations, rather than needing to duplicate (i.e. belongsToMany) 53 | - [ ] Catch errors to the end user. 54 | - [ ] Alert for the user (confirmation possibly). 55 | - [ ] Documentation on how to hide/show when needed. 56 | - [ ] Documentation on how to hook into replication. 57 | - [ ] Add a button to the resource view. 58 | - [ ] Clean up methods for `v1` 59 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jackabox/nova-duplicate-field", 3 | "description": "A Laravel Nova field to duplicate records.", 4 | "version": "0.3.0", 5 | "keywords": [ 6 | "laravel", 7 | "nova", 8 | "duplicate" 9 | ], 10 | "license": "MIT", 11 | "require": { 12 | "php": ">=7.1.0" 13 | }, 14 | "autoload": { 15 | "psr-4": { 16 | "Jackabox\\DuplicateField\\": "src/" 17 | } 18 | }, 19 | "extra": { 20 | "laravel": { 21 | "providers": [ 22 | "Jackabox\\DuplicateField\\FieldServiceProvider" 23 | ] 24 | } 25 | }, 26 | "config": { 27 | "sort-packages": true 28 | }, 29 | "minimum-stability": "dev", 30 | "prefer-stable": true 31 | } 32 | -------------------------------------------------------------------------------- /dist/js/field.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:r})},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=7)}([function(e,t,n){"use strict";var r=n(2),o=n(14),i=Object.prototype.toString;function s(e){return"[object Array]"===i.call(e)}function u(e){return null!==e&&"object"==typeof e}function a(e){return"[object Function]"===i.call(e)}function c(e,t){if(null!==e&&void 0!==e)if("object"!=typeof e&&(e=[e]),s(e))for(var n=0,r=e.length;n=200&&e<300}};a.headers={common:{Accept:"application/json, text/plain, */*"}},r.forEach(["delete","get","head"],function(e){a.headers[e]={}}),r.forEach(["post","put","patch"],function(e){a.headers[e]=r.merge(i)}),e.exports=a}).call(t,n(16))},function(e,t,n){"use strict";e.exports=function(e,t){return function(){for(var n=new Array(arguments.length),r=0;r1)for(var n=1;n=0)return;s[t]="set-cookie"===t?(s[t]?s[t]:[]).concat([n]):s[t]?s[t]+", "+n:n}}),s):s}},function(e,t,n){"use strict";var r=n(0);e.exports=r.isStandardBrowserEnv()?function(){var e,t=/(msie|trident)/i.test(navigator.userAgent),n=document.createElement("a");function o(e){var r=e;return t&&(n.setAttribute("href",r),r=n.href),n.setAttribute("href",r),{href:n.href,protocol:n.protocol?n.protocol.replace(/:$/,""):"",host:n.host,search:n.search?n.search.replace(/^\?/,""):"",hash:n.hash?n.hash.replace(/^#/,""):"",hostname:n.hostname,port:n.port,pathname:"/"===n.pathname.charAt(0)?n.pathname:"/"+n.pathname}}return e=o(window.location.href),function(t){var n=r.isString(t)?o(t):t;return n.protocol===e.protocol&&n.host===e.host}}():function(){return!0}},function(e,t,n){"use strict";var r=n(0);e.exports=r.isStandardBrowserEnv()?{write:function(e,t,n,o,i,s){var u=[];u.push(e+"="+encodeURIComponent(t)),r.isNumber(n)&&u.push("expires="+new Date(n).toGMTString()),r.isString(o)&&u.push("path="+o),r.isString(i)&&u.push("domain="+i),!0===s&&u.push("secure"),document.cookie=u.join("; ")},read:function(e){var t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove:function(e){this.write(e,"",Date.now()-864e5)}}:{write:function(){},read:function(){return null},remove:function(){}}},function(e,t,n){"use strict";var r=n(0);function o(){this.handlers=[]}o.prototype.use=function(e,t){return this.handlers.push({fulfilled:e,rejected:t}),this.handlers.length-1},o.prototype.eject=function(e){this.handlers[e]&&(this.handlers[e]=null)},o.prototype.forEach=function(e){r.forEach(this.handlers,function(t){null!==t&&e(t)})},e.exports=o},function(e,t,n){"use strict";var r=n(0),o=n(26),i=n(5),s=n(1),u=n(27),a=n(28);function c(e){e.cancelToken&&e.cancelToken.throwIfRequested()}e.exports=function(e){return c(e),e.baseURL&&!u(e.url)&&(e.url=a(e.baseURL,e.url)),e.headers=e.headers||{},e.data=o(e.data,e.headers,e.transformRequest),e.headers=r.merge(e.headers.common||{},e.headers[e.method]||{},e.headers||{}),r.forEach(["delete","get","head","post","put","patch","common"],function(t){delete e.headers[t]}),(e.adapter||s.adapter)(e).then(function(t){return c(e),t.data=o(t.data,t.headers,e.transformResponse),t},function(t){return i(t)||(c(e),t&&t.response&&(t.response.data=o(t.response.data,t.response.headers,e.transformResponse))),Promise.reject(t)})}},function(e,t,n){"use strict";var r=n(0);e.exports=function(e,t,n){return r.forEach(n,function(n){e=n(e,t)}),e}},function(e,t,n){"use strict";e.exports=function(e){return/^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(e)}},function(e,t,n){"use strict";e.exports=function(e,t){return t?e.replace(/\/+$/,"")+"/"+t.replace(/^\/+/,""):e}},function(e,t,n){"use strict";var r=n(6);function o(e){if("function"!=typeof e)throw new TypeError("executor must be a function.");var t;this.promise=new Promise(function(e){t=e});var n=this;e(function(e){n.reason||(n.reason=new r(e),t(n.reason))})}o.prototype.throwIfRequested=function(){if(this.reason)throw this.reason},o.source=function(){var e;return{token:new o(function(t){e=t}),cancel:e}},e.exports=o},function(e,t,n){"use strict";e.exports=function(e){return function(t){return e.apply(null,t)}}},function(e,t){e.exports={render:function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("a",{staticClass:"cursor-pointer text-70 hover:text-primary no-underline flex items-center",attrs:{href:""},on:{click:function(t){return t.preventDefault(),e.onClick(t)}}},[n("svg",{staticClass:"fill-current",attrs:{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",width:"24",height:"24"}},[n("path",{attrs:{d:"M17 7h2.25c.97 0 1.75.78 1.75 1.75v10.5c0 .97-.78 1.75-1.75 1.75H8.75C7.78 21 7 20.22 7 19.25V17H4.75C3.78 17 3 16.22 3 15.25V4.75C3 3.78 3.78 3 4.75 3h10.5c.97 0 1.75.78 1.75 1.75V7zm-2 0V5H5v10h2V8.75C7 7.78 7.78 7 8.75 7H15zM9 9v10h10V9H9z"}})]),e._v(" "),this.field.showText?n("span",[e._v("- Duplicate")]):e._e()])},staticRenderFns:[]}}]); -------------------------------------------------------------------------------- /dist/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/js/field.js": "/js/field.js" 3 | } -------------------------------------------------------------------------------- /img/nova-duplicate-field-small.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackabox/nova-duplicate-field/8b2899078aca14c414635c6adf3263603755ff31/img/nova-duplicate-field-small.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 6 | "watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 7 | "watch-poll": "npm run watch -- --watch-poll", 8 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", 9 | "prod": "npm run production", 10 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 11 | }, 12 | "devDependencies": { 13 | "cross-env": "^5.0.0", 14 | "laravel-mix": "^1.0", 15 | "laravel-nova": "^1.0" 16 | }, 17 | "dependencies": { 18 | "axios": "^0.18.0", 19 | "vue": "^2.5.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /resources/js/components/IndexField.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 56 | -------------------------------------------------------------------------------- /resources/js/field.js: -------------------------------------------------------------------------------- 1 | Nova.booting((Vue, router) => { 2 | Vue.component('index-duplicate-field', require('./components/IndexField')); 3 | }) 4 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | onlyOnIndex(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/FieldServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->booted(function () { 24 | $this->routes(); 25 | }); 26 | } 27 | 28 | public function routes() 29 | { 30 | if ($this->app->routesAreCached()) { 31 | return; 32 | } 33 | 34 | Route::middleware(['nova']) 35 | ->prefix('/nova-vendor/jackabox/nova-duplicate') 36 | ->group(__DIR__ . '/../routes/api.php'); 37 | } 38 | 39 | /** 40 | * Register any application services. 41 | * 42 | * @return void 43 | */ 44 | public function register() 45 | { 46 | // 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Http/Controllers/DuplicateController.php: -------------------------------------------------------------------------------- 1 | model::where('id', $request->id)->first(); 17 | 18 | if (!$model) { 19 | return [ 20 | 'status' => 404, 21 | 'message' => 'No model found.', 22 | 'destination' => config('nova.url') . config('nova.path') . '/resources/' . $request->resource . '/' 23 | ]; 24 | } 25 | 26 | $newModel = $model->replicate($request->except); 27 | 28 | if (is_array($request->override)) { 29 | foreach ($request->override as $field => $value) { 30 | $newModel->{$field} = $value; 31 | } 32 | } 33 | 34 | $newModel->push(); 35 | 36 | if (isset($request->relations) && !empty($request->relations)) { 37 | // load the relations 38 | $model->load($request->relations); 39 | 40 | foreach ($model->getRelations() as $relation => $items) { 41 | // works for hasMany 42 | foreach ($items as $item) { 43 | // clean up our models, remove the id and remove the appends 44 | unset($item->id); 45 | $item->setAppends([]); 46 | 47 | // create a relation on the new model with the data. 48 | $newModel->{$relation}()->create($item->toArray()); 49 | } 50 | } 51 | } 52 | 53 | // return response and redirect. 54 | return [ 55 | 'status' => 200, 56 | 'message' => 'Done', 57 | 'destination' => url(config('nova.path') . '/resources/' . $request->resource . '/' . $newModel->id) 58 | ]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackabox/nova-duplicate-field/8b2899078aca14c414635c6adf3263603755ff31/title.png -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | let mix = require("laravel-mix"); 2 | 3 | mix.setPublicPath("dist").js("resources/js/field.js", "js"); 4 | --------------------------------------------------------------------------------