├── .gitignore ├── LICENSE ├── composer.json ├── config └── nova-button.php ├── dist ├── js │ └── field.js └── mix-manifest.json ├── package-lock.json ├── package.json ├── readme.md ├── resources ├── field.js └── js │ ├── components │ ├── DetailField.vue │ ├── FormField.vue │ ├── IndexField.vue │ └── NovaButton.vue │ ├── field.js │ └── queue.js ├── routes └── api.php ├── src ├── Button.php ├── Events │ └── ButtonClick.php ├── Http │ └── Controllers │ │ └── ButtonController.php └── Providers │ └── FieldServiceProvider.php └── webpack.mix.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /vendor/ 3 | /node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Brian Dillingham 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 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dillingham/nova-button", 3 | "description": "Nova button package", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Brian Dillingham", 8 | "email": "bdillingham88@gmail.com" 9 | } 10 | ], 11 | "autoload": { 12 | "psr-4": { 13 | "NovaButton\\": "src/" 14 | } 15 | }, 16 | "extra": { 17 | "laravel": { 18 | "providers": [ 19 | "NovaButton\\Providers\\FieldServiceProvider" 20 | ] 21 | } 22 | }, 23 | "minimum-stability": "dev", 24 | "require": {} 25 | } 26 | -------------------------------------------------------------------------------- /config/nova-button.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'style' => 'link-primary', 16 | 'loadingStyle' => 'link-grey', 17 | 'loadingText' => 'Loading', 18 | 'successStyle' => 'link-success', 19 | 'successText' => 'Success', 20 | 'errorStyle' => 'link-danger', 21 | 'errorText' => 'Failed', 22 | ], 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Style options 27 | |-------------------------------------------------------------------------- 28 | | 29 | | This key value pair allows you to override the default nova button styles 30 | | and add entirely new style key value pairs altogether that you need. 31 | | The css classes below are Tailwind-CSS that are come with Nova. 32 | | 33 | */ 34 | 35 | 'styles' => [ 36 | // Fill 37 | 'success' => 'cursor-pointer btn btn-default bg-success text-white', 38 | 'primary' => 'cursor-pointer btn btn-default btn-primary', 39 | 'warning' => 'cursor-pointer btn btn-default bg-warning text-white', 40 | 'danger' => 'cursor-pointer btn btn-default bg-danger text-white', 41 | 'info' => 'cursor-pointer btn btn-default bg-info text-white', 42 | 'grey' => 'cursor-pointer btn btn-default bg-60 text-white', 43 | // Outline 44 | 'success-outline' => 'cursor-pointer btn btn-default border border-success text-success', 45 | 'primary-outline' => 'cursor-pointer btn btn-default border border-primary text-primary', 46 | 'warning-outline' => 'cursor-pointer btn btn-default border border-warning text-warning ', 47 | 'danger-outline' => 'cursor-pointer btn btn-default border border-danger text-danger ', 48 | 'info-outline' => 'cursor-pointer btn btn-default border border-info text-info ', 49 | 'grey-outline' => 'cursor-pointer btn btn-default border border-60 text-80 ', 50 | // Link 51 | 'success-link' => 'cursor-pointer dim inline-block text-success font-bold btn btn-link', 52 | 'primary-link' => 'cursor-pointer dim inline-block text-primary font-bold btn btn-link', 53 | 'warning-link' => 'cursor-pointer dim inline-block text-warning font-bold btn btn-link', 54 | 'danger-link' => 'cursor-pointer dim inline-block text-danger font-bold btn btn-link', 55 | 'info-link' => 'cursor-pointer dim inline-block text-info font-bold btn btn-link', 56 | 'grey-link' => 'cursor-pointer dim inline-block text-80 font-bold btn btn-link', 57 | // Custom 58 | 'custom' => 'bg-orange', 59 | 60 | ], 61 | ]; 62 | -------------------------------------------------------------------------------- /dist/js/field.js: -------------------------------------------------------------------------------- 1 | !function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get:r})},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=3)}([function(t,e){t.exports=function(t,e,n,r,o,i){var s,a=t=t||{},l=typeof t.default;"object"!==l&&"function"!==l||(s=t,a=t.default);var c,u="function"==typeof a?a.options:a;if(e&&(u.render=e.render,u.staticRenderFns=e.staticRenderFns,u._compiled=!0),n&&(u.functional=!0),o&&(u._scopeId=o),i?(c=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__),r&&r.call(this,t),t&&t._registeredComponents&&t._registeredComponents.add(i)},u._ssrRegister=c):r&&(c=r),c){var f=u.functional,d=f?u.render:u.beforeCreate;f?(u._injectStyles=c,u.render=function(t,e){return c.call(e),d(t,e)}):u.beforeCreate=d?[].concat(d,c):[c]}return{esModule:s,exports:a,options:u}}},function(t,e,n){"use strict";var r=n(2);e.a={data:function(){return{openModal:!1}},methods:{reload:function(){var t=this;this.field.reload&&r.a.allowsReload()&&window.setTimeout(function(){t.$router.go()},200)},modalReload:function(){var t=this;window.setTimeout(function(){t.openModal=!1,t.reload()},400)}}}},function(t,e,n){"use strict";n.d(e,"a",function(){return o});var r=function(){function t(t,e){for(var n=0;nn.parts.length&&(r.parts.length=n.parts.length)}else{var s=[];for(o=0;o=0,i=o&&r.regeneratorRuntime;if(r.regeneratorRuntime=void 0,t.exports=n(14),o)r.regeneratorRuntime=i;else try{delete r.regeneratorRuntime}catch(t){r.regeneratorRuntime=void 0}},function(t,e){!function(e){"use strict";var n,r=Object.prototype,o=r.hasOwnProperty,i="function"==typeof Symbol?Symbol:{},s=i.iterator||"@@iterator",a=i.asyncIterator||"@@asyncIterator",l=i.toStringTag||"@@toStringTag",c="object"==typeof t,u=e.regeneratorRuntime;if(u)c&&(t.exports=u);else{(u=e.regeneratorRuntime=c?t.exports:{}).wrap=x;var f="suspendedStart",d="suspendedYield",h="executing",p="completed",v={},m={};m[s]=function(){return this};var g=Object.getPrototypeOf,y=g&&g(g(S([])));y&&y!==r&&o.call(y,s)&&(m=y);var b=L.prototype=w.prototype=Object.create(m);C.prototype=b.constructor=L,L.constructor=C,L[l]=C.displayName="GeneratorFunction",u.isGeneratorFunction=function(t){var e="function"==typeof t&&t.constructor;return!!e&&(e===C||"GeneratorFunction"===(e.displayName||e.name))},u.mark=function(t){return Object.setPrototypeOf?Object.setPrototypeOf(t,L):(t.__proto__=L,l in t||(t[l]="GeneratorFunction")),t.prototype=Object.create(b),t},u.awrap=function(t){return{__await:t}},k(E.prototype),E.prototype[a]=function(){return this},u.AsyncIterator=E,u.async=function(t,e,n,r){var o=new E(x(t,e,n,r));return u.isGeneratorFunction(e)?o:o.next().then(function(t){return t.done?t.value:o.next()})},k(b),b[l]="Generator",b[s]=function(){return this},b.toString=function(){return"[object Generator]"},u.keys=function(t){var e=[];for(var n in t)e.push(n);return e.reverse(),function n(){for(;e.length;){var r=e.pop();if(r in t)return n.value=r,n.done=!1,n}return n.done=!0,n}},u.values=S,R.prototype={constructor:R,reset:function(t){if(this.prev=0,this.next=0,this.sent=this._sent=n,this.done=!1,this.delegate=null,this.method="next",this.arg=n,this.tryEntries.forEach(N),!t)for(var e in this)"t"===e.charAt(0)&&o.call(this,e)&&!isNaN(+e.slice(1))&&(this[e]=n)},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 r(r,o){return a.type="throw",a.arg=t,e.next=r,o&&(e.method="next",e.arg=n),!!o}for(var i=this.tryEntries.length-1;i>=0;--i){var s=this.tryEntries[i],a=s.completion;if("root"===s.tryLoc)return r("end");if(s.tryLoc<=this.prev){var l=o.call(s,"catchLoc"),c=o.call(s,"finallyLoc");if(l&&c){if(this.prev=0;--n){var r=this.tryEntries[n];if(r.tryLoc<=this.prev&&o.call(r,"finallyLoc")&&this.prev=0;--e){var n=this.tryEntries[e];if(n.finallyLoc===t)return this.complete(n.completion,n.afterLoc),N(n),v}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var n=this.tryEntries[e];if(n.tryLoc===t){var r=n.completion;if("throw"===r.type){var o=r.arg;N(n)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(t,e,r){return this.delegate={iterator:S(t),resultName:e,nextLoc:r},"next"===this.method&&(this.arg=n),v}}}function x(t,e,n,r){var o=e&&e.prototype instanceof w?e:w,i=Object.create(o.prototype),s=new R(r||[]);return i._invoke=function(t,e,n){var r=f;return function(o,i){if(r===h)throw new Error("Generator is already running");if(r===p){if("throw"===o)throw i;return j()}for(n.method=o,n.arg=i;;){var s=n.delegate;if(s){var a=T(s,n);if(a){if(a===v)continue;return a}}if("next"===n.method)n.sent=n._sent=n.arg;else if("throw"===n.method){if(r===f)throw r=p,n.arg;n.dispatchException(n.arg)}else"return"===n.method&&n.abrupt("return",n.arg);r=h;var l=_(t,e,n);if("normal"===l.type){if(r=n.done?p:d,l.arg===v)continue;return{value:l.arg,done:n.done}}"throw"===l.type&&(r=p,n.method="throw",n.arg=l.arg)}}}(t,n,s),i}function _(t,e,n){try{return{type:"normal",arg:t.call(e,n)}}catch(t){return{type:"throw",arg:t}}}function w(){}function C(){}function L(){}function k(t){["next","throw","return"].forEach(function(e){t[e]=function(t){return this._invoke(e,t)}})}function E(t){var e;this._invoke=function(n,r){function i(){return new Promise(function(e,i){!function e(n,r,i,s){var a=_(t[n],t,r);if("throw"!==a.type){var l=a.arg,c=l.value;return c&&"object"==typeof c&&o.call(c,"__await")?Promise.resolve(c.__await).then(function(t){e("next",t,i,s)},function(t){e("throw",t,i,s)}):Promise.resolve(c).then(function(t){l.value=t,i(l)},s)}s(a.arg)}(n,r,e,i)})}return e=e?e.then(i,i):i()}}function T(t,e){var r=t.iterator[e.method];if(r===n){if(e.delegate=null,"throw"===e.method){if(t.iterator.return&&(e.method="return",e.arg=n,T(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=_(r,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=n),e.delegate=null,v):i:(e.method="throw",e.arg=new TypeError("iterator result is not an object"),e.delegate=null,v)}function M(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 N(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function R(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(M,this),this.reset(!0)}function S(t){if(t){var e=t[s];if(e)return e.call(t);if("function"==typeof t.next)return t;if(!isNaN(t.length)){var r=-1,i=function e(){for(;++rsortable(), 39 | Text::make('Name', 'name'), 40 | Button::make('Notify'), 41 | ]; 42 | } 43 | ``` 44 | 45 | Quick links: [Button Styles](https://github.com/dillingham/nova-button#button-styles) | [Event text / style](https://github.com/dillingham/nova-button#button-state) | [Navigation](https://github.com/dillingham/nova-button#button-navigation) | [CSS classes](https://github.com/dillingham/nova-button#button-classes) | [Lens example](https://github.com/dillingham/nova-button#example) 46 | 47 | --- 48 | 49 | ### Backend events 50 | 51 | By default, clicking the button will trigger a backend event via ajax. 52 | 53 | Default event: `NovaButton\Events\ButtonClick` 54 | 55 | The event will receive the resource model it was triggered from & the key 56 | 57 | - `$event->resource` = `model` 58 | - `$event->key` = `"notify"` 59 | 60 | Adding a custom key 61 | 62 | ```php 63 | Button::make('Notify', 'notify-some-user') 64 | ``` 65 | Adding a custom event 66 | ```php 67 | Button::make('Notify')->event('App\Events\NotifyRequested') 68 | ``` 69 | 70 | You register listeners in your EventServiceProvider 71 | 72 | ### Nova Routes 73 | 74 | You can also choose to navigate any of the Nova routes 75 | 76 | ```php 77 | Button::make('Text')->route('vuejs-route-name', ['id' => 1]) 78 | Button::make('Text')->index('App\Nova\User') 79 | Button::make('Text')->detail('App\Nova\User', $this->user_id) 80 | Button::make('Text')->create('App\Nova\User') 81 | Button::make('Text')->edit('App\Nova\User', $this->user_id) 82 | Button::make('Text')->lens('App\Nova\User', 'users-without-confirmation') 83 | ``` 84 | You can also enable a resource's filters 85 | ```php 86 | Button::make('Text')->index('App\Nova\Order')->withFilters([ 87 | 'App\Nova\Filters\UserOrders' => $this->user_id, 88 | 'App\Nova\Filters\OrderStatus' => 'active', 89 | ]) 90 | ``` 91 | 92 | ### Links 93 | ```php 94 | Button::make('Text')->link('https://nova.laravel.com') 95 | Button::make('Text')->link('https://nova.laravel.com', '_self') 96 | ``` 97 | 98 | ### Visiblity 99 | 100 | You will likely want to show or hide buttons depending on model values 101 | ```php 102 | Button::make('Activate')->visible($this->is_active == false), 103 | Button::make('Deactivate')->visible($this->is_active == true), 104 | ``` 105 | 106 | Also [field authorization](https://nova.laravel.com/docs/1.0/resources/authorization.html#fields) via canSee() & [showing / hiding fields](https://nova.laravel.com/docs/1.0/resources/fields.html#showing-hiding-fields) hideFromIndex(), etc 107 | 108 | ### Reload 109 | After events are triggered, reload the page. 110 | 111 | ```php 112 | Button::make('Notify')->reload() 113 | ``` 114 | If you click many buttons, reloading will wait for all buttons to finish. 115 | 116 | If an error occurs, it will not reload the page. 117 | 118 | 119 | ### Confirm 120 | You can require a confirmation for descructive actions 121 | 122 | ```php 123 | Button::make('Cancel Account')->confirm('Are you sure?'), 124 | Button::make('Cancel Account')->confirm('title', 'content'), 125 | ``` 126 | 127 | ### Button state 128 | When using events, you want visual feedback for the end user. 129 | 130 | This is especially useful for long running listeners. 131 | 132 | ```php 133 | Button::make('Remind User')->loadingText('Sending..')->successText('Sent!') 134 | ``` 135 | 136 | | Event | Text | Style | 137 | | -- | -- | -- | 138 | | loading | `loadingText('Loading..')` | `loadingStyle('grey-outline')` | 139 | | success | `successText('Done!')` | `successStyle('success')` | 140 | | error | `errorText('Failed')` | `errorStyle('danger')` | 141 | 142 | Defaults defined in the `nova-button` config. Add methods when you want to change for specific resources 143 | 144 | 145 | ### Button styles 146 | 147 | This package makes use of [tailwind-css](https://tailwindcss.com) classes / default: `link` 148 | 149 | ```php 150 | Button::make('Confirm')->style('primary') 151 | ``` 152 | 153 | | Fill | Outline | Link | 154 | |---|---|---| 155 | | primary | primary-outline | primary-link | 156 | | success | success-outline | success-link | 157 | | danger | danger-outline | danger-link | 158 | | warning | warning-outline | warning-link | 159 | | info | info-outline | info-link | 160 | | grey | grey-outline | grey-link | 161 | 162 | Each key adds classes from the `nova-button` config 163 | ```php 164 | 'primary' => 'btn btn-default btn-primary' 165 | ``` 166 | 167 | ### Style config 168 | Publish the nova-button config to add / edit [available styles & defaults](https://github.com/dillingham/nova-button/blob/master/config/nova-button.php) 169 | ``` 170 | php artisan vendor:publish --tag=nova-button -- force 171 | ``` 172 | 173 | ### Button classes 174 | 175 | You can also add classes manually 176 | 177 | ```php 178 | Button::make('Refund')->classes('some-class') 179 | ``` 180 | Also able to style the following css classes 181 | 182 | ```css 183 | .nova-button 184 | .nova-button-{resource-name} 185 | .nova-button-success 186 | .nova-button-error 187 | .nova-button-loading 188 | ``` 189 | 190 | --- 191 | 192 | # Example 193 | 194 | Use [lenses](https://nova.laravel.com/docs/1.0/lenses/defining-lenses.html) with buttons for a very focused user experience 195 | 196 | ![lens-button](https://user-images.githubusercontent.com/29180903/50742642-31f30c00-11dc-11e9-96c2-e0534e963aed.png) 197 | 198 | ```php 199 | select(['users.id', 'users.name']) 209 | ->whereNull('email_verified_at'); 210 | } 211 | 212 | public function fields(Request $request) 213 | { 214 | return [ 215 | ID::make('ID', 'id'), 216 | Text::make('Name', 'name'), 217 | Button::make('Mark As Confirmed'), 218 | ]; 219 | } 220 | } 221 | ``` 222 | Register a listener for `\NovaButton\Events\ButtonClick` in your [EventServiceProvider](https://laravel.com/docs/5.7/events) 223 | ```php 224 | key == 'mark-as-confirmed') { 233 | $event->resource->email_verified_at = now(); 234 | $event->resource->save(); 235 | } 236 | } 237 | } 238 | ``` 239 | No `key` check required when you register an event for this listener 240 | 241 | ```php 242 | Button::make('Confirm')->event('App\Events\ConfirmClick') 243 | ``` 244 | 245 | # Telescope inspection 246 | 247 | ![event-triggered](https://user-images.githubusercontent.com/29180903/50742633-1a1b8800-11dc-11e9-8a2d-5ec70d3fcae4.png) 248 | 249 | --- 250 | 251 | # Author 252 | 253 | Hi 👋, Im Brian Dillingham, creator of this Nova package [and others](https://novapackages.com/collaborators/dillingham) 254 | 255 | Hope you find it useful. Feel free to reach out with feedback. 256 | 257 | Follow me on twitter: [@sir_brian_d](https://twitter.com/sir_brian_d) 258 | -------------------------------------------------------------------------------- /resources/field.js: -------------------------------------------------------------------------------- 1 | import {queue} from './js/queue'; 2 | 3 | export default { 4 | data() { 5 | return { 6 | openModal: false 7 | } 8 | }, 9 | methods: { 10 | reload() { 11 | if(this.field.reload && queue.allowsReload()) { 12 | window.setTimeout(() => { 13 | this.$router.go() 14 | }, 200) 15 | } 16 | }, 17 | modalReload() { 18 | window.setTimeout(() => { 19 | this.openModal = false; 20 | this.reload() 21 | }, 400) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /resources/js/components/DetailField.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 51 | -------------------------------------------------------------------------------- /resources/js/components/FormField.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 51 | -------------------------------------------------------------------------------- /resources/js/components/IndexField.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 51 | -------------------------------------------------------------------------------- /resources/js/components/NovaButton.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 26 | 27 | 134 | -------------------------------------------------------------------------------- /resources/js/field.js: -------------------------------------------------------------------------------- 1 | Nova.booting((Vue, router) => { 2 | Vue.component('nova-button', require('./components/NovaButton')); 3 | Vue.component('index-nova-button', require('./components/IndexField')); 4 | Vue.component('detail-nova-button', require('./components/DetailField')); 5 | Vue.component('form-nova-button', require('./components/FormField')); 6 | }) 7 | -------------------------------------------------------------------------------- /resources/js/queue.js: -------------------------------------------------------------------------------- 1 | class Queue 2 | { 3 | constructor() { 4 | this.items = []; 5 | this.hasSuccess = false; 6 | this.hasError = false; 7 | } 8 | 9 | add(id) 10 | { 11 | this.items.push(id); 12 | } 13 | 14 | remove(id) 15 | { 16 | this.items = this.items.filter((item) => { 17 | return id != item 18 | }) 19 | } 20 | 21 | count() 22 | { 23 | return this.items.length; 24 | } 25 | 26 | allowsReload() 27 | { 28 | return this.count() == 0 && this.hasSuccess && this.hasError == false; 29 | } 30 | } 31 | 32 | export let queue = new Queue(); -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | name = $name; 61 | $this->text = $name; 62 | $this->key = $key ?? Str::kebab($name); 63 | $this->attribute = $this->key; 64 | $this->config = config('nova-button'); 65 | $this->addDefaultSettings(); 66 | } 67 | 68 | public function resolve($resource, $attribute = null) 69 | { 70 | parent::resolve($resource, $attribute); 71 | 72 | $this->classes[] = 'nova-button-'.strtolower(class_basename($resource)); 73 | $this->classes[] = Arr::get($this->config, "styles.{$this->style}"); 74 | $this->loadingClasses = Arr::get($this->config, "styles.{$this->loadingStyle}"); 75 | $this->successClasses = Arr::get($this->config, "styles.{$this->successStyle}"); 76 | $this->errorClasses = Arr::get($this->config, "styles.{$this->errorStyle}"); 77 | 78 | $this->withMeta([ 79 | 'key' => $this->key, 80 | 'type' => $this->type, 81 | 'link' => $this->link, 82 | 'text' => $this->text, 83 | 'event' => $this->event, 84 | 'label' => $this->label, 85 | 'route' => $this->route, 86 | 'reload' => $this->reload, 87 | 'confirm' => $this->confirm, 88 | 'visible' => $this->visible, 89 | 'classes' => $this->classes, 90 | 'indexName' => $this->indexName, 91 | 'title' => $this->title, 92 | 'indexAlign' => $this->indexAlign, 93 | 'errorText' => $this->errorText, 94 | 'errorClasses' => $this->errorClasses, 95 | 'successText' => $this->successText, 96 | 'successClasses' => $this->successClasses, 97 | 'loadingText' => $this->loadingText, 98 | 'loadingClasses' => $this->loadingClasses, 99 | ]); 100 | } 101 | 102 | public function style($style) 103 | { 104 | $this->style = $style; 105 | 106 | return $this; 107 | } 108 | 109 | public function loadingStyle($loadingStyle) 110 | { 111 | $this->loadingStyle = $loadingStyle; 112 | 113 | return $this; 114 | } 115 | 116 | public function successStyle($successStyle) 117 | { 118 | $this->successStyle = $successStyle; 119 | 120 | return $this; 121 | } 122 | 123 | public function errorStyle($errorStyle) 124 | { 125 | $this->errorStyle = $errorStyle; 126 | 127 | return $this; 128 | } 129 | 130 | public function classes($classes) 131 | { 132 | $this->classes[] = $classes; 133 | 134 | return $this; 135 | } 136 | 137 | public function confirm($message1 = null, $message2 = null) 138 | { 139 | $this->confirm = [ 140 | 'title' => __('Confirmation'), 141 | 'body' => null, 142 | ]; 143 | 144 | if ($message1 && $message2 == null) { 145 | $this->confirm['body'] = $message1; 146 | } 147 | 148 | if ($message1 && $message2) { 149 | $this->confirm['title'] = $message1; 150 | $this->confirm['body'] = $message2; 151 | } 152 | 153 | return $this; 154 | } 155 | 156 | public function reload($reload = true) 157 | { 158 | $this->reload = $reload; 159 | 160 | return $this; 161 | } 162 | 163 | public function event($event) 164 | { 165 | $this->event = $event; 166 | 167 | return $this; 168 | } 169 | 170 | public function visible($condition) 171 | { 172 | $this->visible = $condition; 173 | 174 | return $this; 175 | } 176 | 177 | public function update($update) 178 | { 179 | $this->update = $update; 180 | 181 | return $this; 182 | } 183 | 184 | public function loadingText($loadingText) 185 | { 186 | $this->loadingText = $loadingText; 187 | 188 | return $this; 189 | } 190 | 191 | public function successText($successText) 192 | { 193 | $this->successText = $successText; 194 | 195 | return $this; 196 | } 197 | 198 | public function errorText($errorText) 199 | { 200 | $this->errorText = $errorText; 201 | 202 | return $this; 203 | } 204 | 205 | public function label($label) 206 | { 207 | $this->label = $label; 208 | 209 | return $this; 210 | } 211 | 212 | public function title($title) 213 | { 214 | $this->title = $title; 215 | 216 | return $this; 217 | } 218 | 219 | public function index($namespace) 220 | { 221 | $this->route('index', [ 222 | 'resourceName' => $this->normalizeResourceName($namespace), 223 | ]); 224 | 225 | return $this; 226 | } 227 | 228 | public function detail($namespace, $id) 229 | { 230 | $this->route('detail', [ 231 | 'resourceName' => $this->normalizeResourceName($namespace), 232 | 'resourceId' => $id, 233 | ]); 234 | 235 | return $this; 236 | } 237 | 238 | public function create($namespace) 239 | { 240 | $this->route('create', [ 241 | 'resourceName' => $this->normalizeResourceName($namespace), 242 | ]); 243 | 244 | return $this; 245 | } 246 | 247 | public function edit($namespace, $id) 248 | { 249 | $this->route('edit', [ 250 | 'resourceName' => $this->normalizeResourceName($namespace), 251 | 'resourceId' => $id, 252 | ]); 253 | 254 | return $this; 255 | } 256 | 257 | public function lens($namespace, $key) 258 | { 259 | $this->route('lens', [ 260 | 'resourceName' => $this->normalizeResourceName($namespace), 261 | 'lens' => $key, 262 | ]); 263 | 264 | return $this; 265 | } 266 | 267 | public function link($href, $target = '_blank') 268 | { 269 | $this->type = 'link'; 270 | $this->link = compact('href', 'target'); 271 | 272 | return $this; 273 | } 274 | 275 | public function route($name, $params) 276 | { 277 | $this->type = 'route'; 278 | 279 | $this->route = [ 280 | 'name' => $name, 281 | 'params' => $params, 282 | 'query' => [], 283 | ]; 284 | 285 | return $this; 286 | } 287 | 288 | /** 289 | * Add params to route. 290 | * 291 | * @param array $params 292 | * 293 | * @return $this 294 | */ 295 | public function withParams(array $params) 296 | { 297 | $this->route['query'] = array_merge($this->route['query'], $params); 298 | 299 | return $this; 300 | } 301 | 302 | /** 303 | * Add filters to index view. 304 | * 305 | * @param array $filters 306 | * 307 | * @return $this 308 | */ 309 | public function withFilters(array $filters) 310 | { 311 | $key = $this->route['params']['resourceName'].'_filter'; 312 | 313 | $this->route['query'][$key] = base64_encode(json_encode(collect($filters)->map(function ($value, $key) { 314 | return [ 315 | 'class' => $key, 316 | 'value' => $value, 317 | ]; 318 | })->values())); 319 | 320 | return $this; 321 | } 322 | 323 | /** 324 | * @param string $namespace 325 | * 326 | * @return string 327 | */ 328 | protected function normalizeResourceName($namespace) 329 | { 330 | return class_exists($namespace) && is_subclass_of($namespace, Resource::class) 331 | ? $namespace::uriKey() : $namespace; 332 | } 333 | 334 | public function addDefaultSettings() 335 | { 336 | $this->addLinkFallbacks(); 337 | $this->style = Arr::get($this->config, 'defaults.style', 'link-primary'); 338 | $this->loadingText = Arr::get($this->config, 'defaults.loadingText', 'Loading'); 339 | $this->loadingStyle = Arr::get($this->config, 'defaults.loadingStyle', str_replace('primary', 'grey', $this->style)); 340 | $this->errorText = Arr::get($this->config, 'defaults.errorText', 'Error!'); 341 | $this->errorStyle = Arr::get($this->config, 'defaults.errorStyle', str_replace('primary', 'danger', $this->style)); 342 | $this->successText = Arr::get($this->config, 'defaults.successText', 'Success!'); 343 | $this->successStyle = Arr::get($this->config, 'defaults.successStyle', str_replace('primary', 'success', $this->style)); 344 | } 345 | 346 | public function addLinkFallbacks() 347 | { 348 | if (!Arr::has($this->config, 'styles.link-primary')) { 349 | $this->config['styles']['link-primary'] = 'cursor-pointer dim inline-block text-primary font-bold no-underline'; 350 | } 351 | 352 | if (!Arr::has($this->config, 'styles.link-success')) { 353 | $this->config['styles']['link-success'] = 'cursor-pointer dim inline-block text-success font-bold no-underline'; 354 | } 355 | 356 | if (!Arr::has($this->config, 'styles.link-grey')) { 357 | $this->config['styles']['link-grey'] = 'cursor-pointer dim inline-block text-grey font-bold no-underline'; 358 | } 359 | 360 | if (!Arr::has($this->config, 'styles.link-danger')) { 361 | $this->config['styles']['link-danger'] = 'cursor-pointer dim inline-block text-danger font-bold no-underline'; 362 | } 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /src/Events/ButtonClick.php: -------------------------------------------------------------------------------- 1 | resource = $resource; 17 | $this->key = $key; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Http/Controllers/ButtonController.php: -------------------------------------------------------------------------------- 1 | event; 13 | 14 | $resource = $request->findModelQuery()->firstOrFail(); 15 | 16 | event(new $event($resource, $request->buttonKey)); 17 | 18 | return response('ok', 200); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Providers/FieldServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 20 | __DIR__.'/../../config/nova-button.php' => config_path('nova-button.php'), 21 | ], 'nova-button'); 22 | 23 | Nova::serving(function (ServingNova $event) { 24 | Nova::script('nova-button', __DIR__.'/../../dist/js/field.js'); 25 | }); 26 | } 27 | 28 | /** 29 | * Register any application services. 30 | * 31 | * @return void 32 | */ 33 | public function register() 34 | { 35 | $this->mergeConfigFrom(__DIR__.'/../../config/nova-button.php', 'nova-button'); 36 | 37 | $this->registerRoutes(); 38 | } 39 | 40 | /** 41 | * Registers field routes. 42 | * 43 | * @return void 44 | */ 45 | private function registerRoutes() 46 | { 47 | Route::middleware(['nova']) 48 | ->prefix('nova-vendor/nova-button') 49 | ->group(__DIR__.'/../../routes/api.php'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | let mix = require('laravel-mix') 2 | 3 | mix.setPublicPath('dist').js('resources/js/field.js', 'js') --------------------------------------------------------------------------------