├── dist ├── css │ └── field.css └── js │ └── field.js ├── resources ├── sass │ └── field.scss └── js │ ├── field.js │ └── components │ └── IndexField.vue ├── docs └── screenshot.png ├── mix-manifest.json ├── src ├── Http │ ├── Routes │ │ └── api.php │ ├── Requests │ │ └── ReorderResourceRequest.php │ └── Controllers │ │ └── ResourceSortingController.php ├── Sortable.php ├── Concerns │ └── SortsIndexEntries.php └── FieldServiceProvider.php ├── .gitignore ├── webpack.mix.js ├── composer.json ├── package.json ├── LICENSE.md └── README.md /dist/css/field.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/sass/field.scss: -------------------------------------------------------------------------------- 1 | // Nova Tool CSS 2 | -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxon/nova-field-sortable/HEAD/docs/screenshot.png -------------------------------------------------------------------------------- /mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/dist/js/field.js": "/dist/js/field.js", 3 | "/dist/css/field.css": "/dist/css/field.css" 4 | } -------------------------------------------------------------------------------- /resources/js/field.js: -------------------------------------------------------------------------------- 1 | Nova.booting((Vue, router) => { 2 | Vue.component('index-nova-field-sortable', require('./components/IndexField')); 3 | }) 4 | -------------------------------------------------------------------------------- /src/Http/Routes/api.php: -------------------------------------------------------------------------------- 1 | =7.1.0", 11 | "laravel/nova": "*", 12 | "spatie/eloquent-sortable": "^3.4" 13 | }, 14 | "autoload": { 15 | "psr-4": { 16 | "Naxon\\NovaFieldSortable\\": "src/" 17 | } 18 | }, 19 | "extra": { 20 | "laravel": { 21 | "providers": [ 22 | "Naxon\\NovaFieldSortable\\FieldServiceProvider" 23 | ] 24 | } 25 | }, 26 | "config": { 27 | "sort-packages": true 28 | }, 29 | "minimum-stability": "dev", 30 | "prefer-stable": true 31 | } 32 | -------------------------------------------------------------------------------- /src/Concerns/SortsIndexEntries.php: -------------------------------------------------------------------------------- 1 | when(empty($request->get('orderBy')), function (Builder $q) { 20 | $q->getQuery()->orders = []; 21 | 22 | return $q->orderBy(static::$defaultSortField); 23 | }); 24 | 25 | return $query; 26 | } 27 | } -------------------------------------------------------------------------------- /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 | "vue": "^2.5.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Http/Controllers/ResourceSortingController.php: -------------------------------------------------------------------------------- 1 | findResourceOrFail()->authorizeToUpdate($request); 13 | $direction = $request->get('direction', null); 14 | 15 | if (!in_array($direction, ['up', 'down'])) { 16 | return response('', 500); 17 | } 18 | 19 | $model = $request->findModelQuery()->firstOrFail(); 20 | 21 | if (!method_exists($model, 'moveOrderUp') || !method_exists($model, 'moveOrderDown')) { 22 | return response(__('Missing sorting methods on model :model', ['model' => get_class($model)]), 500); 23 | } 24 | 25 | if ($direction == 'up') { 26 | $model->moveOrderUp(); 27 | } else { 28 | $model->moveOrderDown(); 29 | } 30 | 31 | return response('', 200); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Daniel Naxon 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. -------------------------------------------------------------------------------- /src/FieldServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerRoutes(); 35 | } 36 | 37 | /** 38 | * Registers field routes 39 | * 40 | * @return void 41 | */ 42 | private function registerRoutes() 43 | { 44 | Route::domain(config('nova.domain', null)) 45 | ->middleware(config('nova.middleware', [])) 46 | ->prefix('/nova-vendor/naxon/nova-field-sortable') 47 | ->group(__DIR__ . '/Http/Routes/api.php'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /resources/js/components/IndexField.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Nova Sortable Field 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/naxon/nova-field-sortable.svg?style=flat-square)](https://packagist.org/packages/naxon/nova-field-sortable) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/naxon/nova-field-sortable.svg?style=flat-square)](https://packagist.org/packages/naxon/nova-field-sortable) 5 | 6 | ## Description 7 | 8 | This field adds reordering functionality to your resource's index using the awesome [eloquent-sortable](https://github.com/spatie/eloquent-sortable) package by the great people of [Spatie](https://spatie.be). 9 | 10 | ![screenshot](https://github.com/Naxon/nova-field-sortable/raw/master/docs/screenshot.png) 11 | 12 | ## Requrements 13 | 14 | * Laravel 5.6+ with Nova. 15 | * [spatie/eloquent-sortable](https://github.com/spatie/eloquent-sortable) (If not already installed, this package will install if for you and all you have to do is follow the [installation](https://github.com/spatie/eloquent-sortable#installation) instructions). 16 | 17 | ## Installation 18 | 19 | This package can be installed through Composer. 20 | ```bash 21 | composer require naxon/nova-field-sortable 22 | ``` 23 | 24 | ## Usage 25 | 26 | 1. Follow the [usage instructions](https://github.com/spatie/eloquent-sortable#usage) on the eloquent-sortable repository to make your model sortable. 27 | 2. Use the `Naxon\NovaFieldSortable\Concerns\SortsIndexEntries` trait in your Nova Resource. 28 | 3. Add a public static property called `$defaultSortField` to your resource, containing your sorting column (I recomment adding it in your main `app/Nova/Resource.php` file). 29 | 4. Add the `Naxon\NovaFieldSortable\Sortable` field to your Nova Resource `fields` method, using a label and your primary key column. 30 | 31 | ### Example 32 | 33 | ```php 34 | sortable(), 61 | 62 | Text::make('Title'), 63 | 64 | Sortable::make('Order', 'id') 65 | ->onlyOnIndex(), 66 | ]; 67 | } 68 | } 69 | 70 | ``` 71 | 72 | ## Security 73 | 74 | If you discover any security related issues, please email naxond@gmail.com instead of using the issue tracker. 75 | 76 | ## Credits 77 | 78 | * [Spatie](https://spatie.be) 79 | 80 | ## License 81 | 82 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 83 | -------------------------------------------------------------------------------- /dist/js/field.js: -------------------------------------------------------------------------------- 1 | !function(t){var e={};function r(n){if(e[n])return e[n].exports;var o=e[n]={i:n,l:!1,exports:{}};return t[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=t,r.c=e,r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get:n})},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=0)}([function(t,e,r){r(1),t.exports=r(9)},function(t,e,r){Nova.booting(function(t,e){t.component("index-nova-field-sortable",r(2))})},function(t,e,r){var n=r(3)(r(4),r(8),!1,null,null,null);t.exports=n.exports},function(t,e){t.exports=function(t,e,r,n,o,i){var a,c=t=t||{},s=typeof t.default;"object"!==s&&"function"!==s||(a=t,c=t.default);var u,f="function"==typeof c?c.options:c;if(e&&(f.render=e.render,f.staticRenderFns=e.staticRenderFns,f._compiled=!0),r&&(f.functional=!0),o&&(f._scopeId=o),i?(u=function(t){(t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),n&&n.call(this,t),t&&t._registeredComponents&&t._registeredComponents.add(i)},f._ssrRegister=u):n&&(u=n),u){var l=f.functional,h=l?f.render:f.beforeCreate;l?(f._injectStyles=u,f.render=function(t,e){return u.call(e),h(t,e)}):f.beforeCreate=h?[].concat(h,u):[u]}return{esModule:a,exports:c,options:f}}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=r(5),o=r.n(n);e.default={props:["resourceName","field"],methods:{reorderResource:function(){var t,e=(t=o.a.mark(function t(e){return o.a.wrap(function(t){for(;;)switch(t.prev=t.next){case 0:return t.prev=0,t.next=3,this.reorderRequest(e);case 3:t.sent,this.$toasted.show(this.__("The new order has been set!"),{type:"success"}),this.$router.go(this.$router.currentRoute),t.next=11;break;case 8:t.prev=8,t.t0=t.catch(0),this.$toasted.show(this.__("An error occured while trying to reorder the resource."),{type:"error"});case 11:case"end":return t.stop()}},t,this,[[0,8]])}),function(){var e=t.apply(this,arguments);return new Promise(function(t,r){return function n(o,i){try{var a=e[o](i),c=a.value}catch(t){return void r(t)}if(!a.done)return Promise.resolve(c).then(function(t){n("next",t)},function(t){n("throw",t)});t(c)}("next")})});return function(t){return e.apply(this,arguments)}}(),reorderRequest:function(t){return Nova.request().patch("/nova-vendor/naxon/nova-field-sortable/"+this.resourceName+"/"+this.field.value+"/reorder",{direction:t})},createFormData:function(t){return _.tap(new FormData,function(e){e.append("direction",t)})}}}},function(t,e,r){t.exports=r(6)},function(t,e,r){var n=function(){return this}()||Function("return this")(),o=n.regeneratorRuntime&&Object.getOwnPropertyNames(n).indexOf("regeneratorRuntime")>=0,i=o&&n.regeneratorRuntime;if(n.regeneratorRuntime=void 0,t.exports=r(7),o)n.regeneratorRuntime=i;else try{delete n.regeneratorRuntime}catch(t){n.regeneratorRuntime=void 0}},function(t,e){!function(e){"use strict";var r,n=Object.prototype,o=n.hasOwnProperty,i="function"==typeof Symbol?Symbol:{},a=i.iterator||"@@iterator",c=i.asyncIterator||"@@asyncIterator",s=i.toStringTag||"@@toStringTag",u="object"==typeof t,f=e.regeneratorRuntime;if(f)u&&(t.exports=f);else{(f=e.regeneratorRuntime=u?t.exports:{}).wrap=x;var l="suspendedStart",h="suspendedYield",p="executing",d="completed",v={},y={};y[a]=function(){return this};var g=Object.getPrototypeOf,m=g&&g(g(k([])));m&&m!==n&&o.call(m,a)&&(y=m);var w=E.prototype=b.prototype=Object.create(y);L.prototype=w.constructor=E,E.constructor=L,E[s]=L.displayName="GeneratorFunction",f.isGeneratorFunction=function(t){var e="function"==typeof t&&t.constructor;return!!e&&(e===L||"GeneratorFunction"===(e.displayName||e.name))},f.mark=function(t){return Object.setPrototypeOf?Object.setPrototypeOf(t,E):(t.__proto__=E,s in t||(t[s]="GeneratorFunction")),t.prototype=Object.create(w),t},f.awrap=function(t){return{__await:t}},R(O.prototype),O.prototype[c]=function(){return this},f.AsyncIterator=O,f.async=function(t,e,r,n){var o=new O(x(t,e,r,n));return f.isGeneratorFunction(e)?o:o.next().then(function(t){return t.done?t.value:o.next()})},R(w),w[s]="Generator",w[a]=function(){return this},w.toString=function(){return"[object Generator]"},f.keys=function(t){var e=[];for(var r in t)e.push(r);return e.reverse(),function r(){for(;e.length;){var n=e.pop();if(n in t)return r.value=n,r.done=!1,r}return r.done=!0,r}},f.values=k,P.prototype={constructor:P,reset:function(t){if(this.prev=0,this.next=0,this.sent=this._sent=r,this.done=!1,this.delegate=null,this.method="next",this.arg=r,this.tryEntries.forEach(C),!t)for(var e in this)"t"===e.charAt(0)&&o.call(this,e)&&!isNaN(+e.slice(1))&&(this[e]=r)},stop:function(){this.done=!0;var t=this.tryEntries[0].completion;if("throw"===t.type)throw t.arg;return this.rval},dispatchException:function(t){if(this.done)throw t;var e=this;function n(n,o){return c.type="throw",c.arg=t,e.next=n,o&&(e.method="next",e.arg=r),!!o}for(var i=this.tryEntries.length-1;i>=0;--i){var a=this.tryEntries[i],c=a.completion;if("root"===a.tryLoc)return n("end");if(a.tryLoc<=this.prev){var s=o.call(a,"catchLoc"),u=o.call(a,"finallyLoc");if(s&&u){if(this.prev=0;--r){var n=this.tryEntries[r];if(n.tryLoc<=this.prev&&o.call(n,"finallyLoc")&&this.prev=0;--e){var r=this.tryEntries[e];if(r.finallyLoc===t)return this.complete(r.completion,r.afterLoc),C(r),v}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.tryLoc===t){var n=r.completion;if("throw"===n.type){var o=n.arg;C(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(t,e,n){return this.delegate={iterator:k(t),resultName:e,nextLoc:n},"next"===this.method&&(this.arg=r),v}}}function x(t,e,r,n){var o=e&&e.prototype instanceof b?e:b,i=Object.create(o.prototype),a=new P(n||[]);return i._invoke=function(t,e,r){var n=l;return function(o,i){if(n===p)throw new Error("Generator is already running");if(n===d){if("throw"===o)throw i;return F()}for(r.method=o,r.arg=i;;){var a=r.delegate;if(a){var c=j(a,r);if(c){if(c===v)continue;return c}}if("next"===r.method)r.sent=r._sent=r.arg;else if("throw"===r.method){if(n===l)throw n=d,r.arg;r.dispatchException(r.arg)}else"return"===r.method&&r.abrupt("return",r.arg);n=p;var s=_(t,e,r);if("normal"===s.type){if(n=r.done?d:h,s.arg===v)continue;return{value:s.arg,done:r.done}}"throw"===s.type&&(n=d,r.method="throw",r.arg=s.arg)}}}(t,r,a),i}function _(t,e,r){try{return{type:"normal",arg:t.call(e,r)}}catch(t){return{type:"throw",arg:t}}}function b(){}function L(){}function E(){}function R(t){["next","throw","return"].forEach(function(e){t[e]=function(t){return this._invoke(e,t)}})}function O(t){var e;this._invoke=function(r,n){function i(){return new Promise(function(e,i){!function e(r,n,i,a){var c=_(t[r],t,n);if("throw"!==c.type){var s=c.arg,u=s.value;return u&&"object"==typeof u&&o.call(u,"__await")?Promise.resolve(u.__await).then(function(t){e("next",t,i,a)},function(t){e("throw",t,i,a)}):Promise.resolve(u).then(function(t){s.value=t,i(s)},a)}a(c.arg)}(r,n,e,i)})}return e=e?e.then(i,i):i()}}function j(t,e){var n=t.iterator[e.method];if(n===r){if(e.delegate=null,"throw"===e.method){if(t.iterator.return&&(e.method="return",e.arg=r,j(t,e),"throw"===e.method))return v;e.method="throw",e.arg=new TypeError("The iterator does not provide a 'throw' method")}return v}var o=_(n,t.iterator,e.arg);if("throw"===o.type)return e.method="throw",e.arg=o.arg,e.delegate=null,v;var i=o.arg;return i?i.done?(e[t.resultName]=i.value,e.next=t.nextLoc,"return"!==e.method&&(e.method="next",e.arg=r),e.delegate=null,v):i:(e.method="throw",e.arg=new TypeError("iterator result is not an object"),e.delegate=null,v)}function N(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.finallyLoc=t[2],e.afterLoc=t[3]),this.tryEntries.push(e)}function C(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function P(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(N,this),this.reset(!0)}function k(t){if(t){var e=t[a];if(e)return e.call(t);if("function"==typeof t.next)return t;if(!isNaN(t.length)){var n=-1,i=function e(){for(;++n