├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── config └── workflow.php ├── details.png ├── diagram.png ├── dist ├── css │ └── tool.css └── js │ └── tool.js ├── mix-manifest.json ├── package.json ├── resources ├── js │ ├── components │ │ └── Tool.vue │ └── tool.js └── sass │ └── tool.scss ├── routes └── api.php ├── src ├── Http │ └── Controllers │ │ └── WorkflowController.php ├── ToolServiceProvider.php └── Workflow.php ├── webpack.mix.js ├── yarn-error.log └── yarn.lock /.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 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Abdullah AlGethami 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 | # Workflow Resource Tool for Laravel Nova 2 | 3 | This package helps you to create workflow on your Nova application. It's built on top of this package [winzou/state-machine](https://github.com/winzou/state-machine) 4 | 5 | ![screenshot](./diagram.png) 6 | 7 | ![screenshot](./details.png) 8 | 9 | 10 | ## Installation 11 | 12 | You can install the package in to a Laravel app that uses [Nova](https://nova.laravel.com) via composer: 13 | 14 | ```bash 15 | composer require cammac/nova-workflow 16 | ``` 17 | 18 | Next, publish the config file 19 | 20 | ```bash 21 | php artisan vendor:publish --tag workflow 22 | ``` 23 | 24 | open `config/workflow.php` and define your workflow 25 | 26 | ## Configuration 27 | 28 | 29 | you can define inside `workflows` element workflow name and it's config as following: 30 | 31 | | field | mandatory | Description | 32 | | -------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------- | 33 | | model | Yes | Model class you want to do your workflow on | 34 | | column | Yes | Column name you want to monitor | 35 | | states | Yes | All possible states | 36 | | transitions | Yes | All possible transitions | 37 | | from | Yes | array: From states | 38 | | to | Yes | To state | 39 | | event | No | Event class that will be fired after the transition is completed | 40 | | style_classes | No | apply your css classes | 41 | | with_reasons | No | string: column inside your model will be filled with the transition | 42 | | with_reasons | No | array: will generate a dropdown list from with_reasons.model with `id` as option's value and `label` as option's text | 43 | 44 | 45 | ## Usage 46 | 47 | To display the workflow that are associated with a given Nova resource, you need to add the workflow Resource Tool to your resource. 48 | 49 | For example, in your `app/Nova/Order.php` file: 50 | 51 | ```php 52 | 53 | use Cammac\Workflow\Workflow; 54 | 55 | ... 56 | 57 | public function fields(Request $request) 58 | { 59 | return [ 60 | ID::make()->sortable(), 61 | 62 | // Your other fields 63 | 64 | Workflow::make('request')->onlyOnDetail() // request is the workflow name defined in workflow configuration file 65 | 66 | ]; 67 | } 68 | ``` 69 | 70 | This will automatically search possible transitions for the current status 71 | 72 | ## License 73 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 74 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cammac/nova-workflow", 3 | "description": "A Laravel Nova resource tool.", 4 | "keywords": [ 5 | "laravel", 6 | "nova" 7 | ], 8 | "license": "MIT", 9 | "require": { 10 | "php": ">=7.1.0", 11 | "winzou/state-machine": "0.4.1" 12 | }, 13 | "autoload": { 14 | "psr-4": { 15 | "Cammac\\Workflow\\": "src/" 16 | } 17 | }, 18 | "repositories": [ 19 | { 20 | "type": "vcs", 21 | "url": "https://github.com/sebdesign/state-machine" 22 | 23 | } 24 | ], 25 | "extra": { 26 | "laravel": { 27 | "providers": [ 28 | "Cammac\\Workflow\\ToolServiceProvider" 29 | ] 30 | } 31 | }, 32 | "config": { 33 | "sort-packages": true 34 | }, 35 | "minimum-stability": "dev", 36 | "prefer-stable": true 37 | } 38 | -------------------------------------------------------------------------------- /config/workflow.php: -------------------------------------------------------------------------------- 1 | [ 6 | 7 | 'request' => [ // workflow name 8 | 'model' => \App\Request::class, //workflow applied to this model 9 | 'column' => 'status', // on this column 10 | 'states' => [ // the possible statuses 11 | 'pending', 12 | 'escalated', 13 | 'approved', 14 | 'rejected', 15 | ], 16 | 17 | 'transitions' => [ 18 | 'Approve' => [ 19 | 'from' => ['pending', 'escalated'], 20 | 'to' => 'approved', 21 | 'event' => \App\Events\RequestApproved::class, // fire event 22 | 'style_classes' => 'bg-success text-20' 23 | ], 24 | 'Escalate' => [ 25 | 'from' => ['pending'], 26 | 'to' => 'escalated', 27 | ], 28 | 'Reject' => [ 29 | 'from' => ['pending', 'escalated'], 30 | 'to' => 'rejected', 31 | 'with_reasons' => [ // to create a dropdown 32 | 'model' => \App\RejectionReason::class, 33 | 'columns' => [ 34 | 'id' => 'id', // value of the option 35 | 'label' => 'title', // option label 36 | ], 37 | ], 38 | 'style_classes' => 'bg-danger text-20' 39 | ], 40 | 41 | 'Back to My Employee' => [ 42 | 'from' => ['escalated'], 43 | 'to' => 'pending', 44 | 'with_reasons' => 'escalation_note', // display a free textarea to write the comment on the this column name 45 | ], 46 | ], 47 | ], 48 | ], 49 | ]; -------------------------------------------------------------------------------- /details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algethamy/nova-workflow/5c72ae93538879a3e7549bfbb112ed1be922b5b3/details.png -------------------------------------------------------------------------------- /diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algethamy/nova-workflow/5c72ae93538879a3e7549bfbb112ed1be922b5b3/diagram.png -------------------------------------------------------------------------------- /dist/css/tool.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algethamy/nova-workflow/5c72ae93538879a3e7549bfbb112ed1be922b5b3/dist/css/tool.css -------------------------------------------------------------------------------- /dist/js/tool.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("workflow",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,s=t=t||{},c=typeof t.default;"object"!==c&&"function"!==c||(a=t,s=t.default);var u,l="function"==typeof s?s.options:s;if(e&&(l.render=e.render,l.staticRenderFns=e.staticRenderFns,l._compiled=!0),r&&(l.functional=!0),o&&(l._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)},l._ssrRegister=u):n&&(u=n),u){var f=l.functional,h=f?l.render:l.beforeCreate;f?(l._injectStyles=u,l.render=function(t,e){return u.call(e),h(t,e)}):l.beforeCreate=h?[].concat(h,u):[u]}return{esModule:a,exports:s,options:l}}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=r(5),o=r.n(n);e.default={props:["resourceName","resourceId","field"],data:function(){return{transactions:this.field.transactions,reason:""}},methods:{close:function(t){this.transactions[t]=!1,this.$emit("close")},classes:function(t){return this.field.styles[t]},reasons:function(t){try{return this.field.reasons[t]||[]}catch(t){return[]}},action:function(){var t,e=(t=o.a.mark(function t(e){var r,n;return o.a.wrap(function(t){for(;;)switch(t.prev=t.next){case 0:return r=this,n=this.field.workflow+"/"+this.resourceId+"/"+e.replace(/\s/g,"_")+"/"+this.reason,t.next=3,Nova.request().get("/nova-vendor/workflow/"+n);case 3:r.close(e),r.$toasted.show("Resource successfully changed to "+e,{type:"success"}),r.$router.push({name:"index",params:{resourceName:r.resourceName,resourceId:r.resourceId}});case 6:case"end":return t.stop()}},t,this)}),function(){var e=t.apply(this,arguments);return new Promise(function(t,r){return function n(o,i){try{var a=e[o](i),s=a.value}catch(t){return void r(t)}if(!a.done)return Promise.resolve(s).then(function(t){n("next",t)},function(t){n("throw",t)});t(s)}("next")})});return function(t){return e.apply(this,arguments)}}(),reject:function(){console.log("reject")},openModal:function(t){Object.keys(this.reasons(t)).length>0?this.transactions[t]=!0:this.action(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",s=i.asyncIterator||"@@asyncIterator",c=i.toStringTag||"@@toStringTag",u="object"==typeof t,l=e.regeneratorRuntime;if(l)u&&(t.exports=l);else{(l=e.regeneratorRuntime=u?t.exports:{}).wrap=_;var f="suspendedStart",h="suspendedYield",p="executing",d="completed",v={},m={};m[a]=function(){return this};var y=Object.getPrototypeOf,g=y&&y(y(P([])));g&&g!==n&&o.call(g,a)&&(m=g);var w=E.prototype=b.prototype=Object.create(m);L.prototype=w.constructor=E,E.constructor=L,E[c]=L.displayName="GeneratorFunction",l.isGeneratorFunction=function(t){var e="function"==typeof t&&t.constructor;return!!e&&(e===L||"GeneratorFunction"===(e.displayName||e.name))},l.mark=function(t){return Object.setPrototypeOf?Object.setPrototypeOf(t,E):(t.__proto__=E,c in t||(t[c]="GeneratorFunction")),t.prototype=Object.create(w),t},l.awrap=function(t){return{__await:t}},C(O.prototype),O.prototype[s]=function(){return this},l.AsyncIterator=O,l.async=function(t,e,r,n){var o=new O(_(t,e,r,n));return l.isGeneratorFunction(e)?o:o.next().then(function(t){return t.done?t.value:o.next()})},C(w),w[c]="Generator",w[a]=function(){return this},w.toString=function(){return"[object Generator]"},l.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}},l.values=P,R.prototype={constructor:R,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(N),!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 s.type="throw",s.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],s=a.completion;if("root"===a.tryLoc)return n("end");if(a.tryLoc<=this.prev){var c=o.call(a,"catchLoc"),u=o.call(a,"finallyLoc");if(c&&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),N(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;N(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(t,e,n){return this.delegate={iterator:P(t),resultName:e,nextLoc:n},"next"===this.method&&(this.arg=r),v}}}function _(t,e,r,n){var o=e&&e.prototype instanceof b?e:b,i=Object.create(o.prototype),a=new R(n||[]);return i._invoke=function(t,e,r){var n=f;return function(o,i){if(n===p)throw new Error("Generator is already running");if(n===d){if("throw"===o)throw i;return S()}for(r.method=o,r.arg=i;;){var a=r.delegate;if(a){var s=j(a,r);if(s){if(s===v)continue;return s}}if("next"===r.method)r.sent=r._sent=r.arg;else if("throw"===r.method){if(n===f)throw n=d,r.arg;r.dispatchException(r.arg)}else"return"===r.method&&r.abrupt("return",r.arg);n=p;var c=x(t,e,r);if("normal"===c.type){if(n=r.done?d:h,c.arg===v)continue;return{value:c.arg,done:r.done}}"throw"===c.type&&(n=d,r.method="throw",r.arg=c.arg)}}}(t,r,a),i}function x(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 C(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 s=x(t[r],t,n);if("throw"!==s.type){var c=s.arg,u=c.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){c.value=t,i(c)},a)}a(s.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=x(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 k(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(k,this),this.reset(!0)}function P(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(;++n0?r("div",{staticClass:"flex"},t._l(t.transactions,function(e,n){return r("div",{staticClass:"flex-1 m-1"},[r("button",{staticClass:"bg-50 w-full btn m-1 p-2 rounded",class:t.classes(n),on:{click:function(e){e.preventDefault(),t.openModal(n)}}},[t._v(t._s(n))]),t._v(" "),e?r("modal",{on:{"modal-close":function(e){t.close(n)}}},[r("form",{staticClass:"bg-white rounded-lg shadow-lg overflow-hidden",staticStyle:{width:"460px"}},[t._t("default",[r("div",{staticClass:"p-8"},[r("heading",{staticClass:"mb-6",attrs:{level:2}},[t._v(t._s(t.__("Choose one of the reasons")))]),t._v(" "),r("p",{staticClass:"text-80 leading-normal"},["textarea"!=t.reasons(n)?r("select",{directives:[{name:"model",rawName:"v-model",value:t.reason,expression:"reason"}],staticClass:"form-control form-select w-full",on:{change:function(e){var r=Array.prototype.filter.call(e.target.options,function(t){return t.selected}).map(function(t){return"_value"in t?t._value:t.value});t.reason=e.target.multiple?r:r[0]}}},t._l(t.reasons(n),function(e,n){return r("option",{domProps:{value:n}},[t._v(t._s(e))])}),0):r("textarea",{directives:[{name:"model",rawName:"v-model",value:t.reason,expression:"reason"}],staticClass:"form-control form-input-bordered h-auto w-full",domProps:{value:t.reason},on:{input:function(e){e.target.composing||(t.reason=e.target.value)}}})])],1)]),t._v(" "),r("div",{staticClass:"bg-30 px-6 py-3 flex"},[r("div",{staticClass:"ml-auto"},[r("button",{staticClass:"btn text-80 font-normal h-9 px-3 mr-3 btn-link",attrs:{type:"button","data-testid":"cancel-button",dusk:"cancel-delete-button"},on:{click:function(e){e.preventDefault(),t.close(n)}}},[t._v("\n "+t._s(t.__("Cancel"))+"\n ")]),t._v(" "),r("button",{ref:"confirmButton",refInFor:!0,staticClass:"btn btn-default btn-primary",attrs:{id:"confirm-delete-button","data-testid":"confirm-button",type:"submit"},on:{click:function(e){t.action(n)}}},[t._v("\n OK\n ")])])])],2)]):t._e()],1)}),0):r("div",[t._v("\n No action required\n")])},staticRenderFns:[]}},function(t,e){}]); -------------------------------------------------------------------------------- /mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/dist/js/tool.js": "/dist/js/tool.js", 3 | "/dist/css/tool.css": "/dist/css/tool.css" 4 | } -------------------------------------------------------------------------------- /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 | }, 16 | "dependencies": { 17 | "vue": "^2.5.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /resources/js/components/Tool.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 116 | -------------------------------------------------------------------------------- /resources/js/tool.js: -------------------------------------------------------------------------------- 1 | Nova.booting((Vue, router) => { 2 | Vue.component('workflow', require('./components/Tool')); 3 | }) 4 | -------------------------------------------------------------------------------- /resources/sass/tool.scss: -------------------------------------------------------------------------------- 1 | // Nova Tool CSS 2 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | getWorkflowSetting($workflow); 10 | 11 | /** @var \Illuminate\Database\Eloquent\Model $model */ 12 | $model = app($workflow['model'])->findOrFail($id); 13 | 14 | $stateMachine = new \SM\StateMachine\StateMachine($model, $workflow); 15 | 16 | $transaction = $this->cleanTransaction($transaction); 17 | 18 | $stateMachine->apply($transaction); 19 | 20 | try { 21 | \DB::transaction(function () use ($model, $workflow, $transaction, $reason) { 22 | if (!empty($reason)) { 23 | if (!is_array($reason_field = data_get($workflow, "transitions.$transaction.with_reasons"))) { 24 | $model->update([ 25 | $reason_field => $reason, 26 | ]); 27 | } else { 28 | 29 | /** @var \Illuminate\Database\Eloquent\Model $reason_model */ 30 | $reason_model = app(data_get($workflow, "transitions.$transaction.with_reasons.model")); 31 | 32 | $model->update([ 33 | $reason_model->getForeignKey() => $reason, 34 | ]); 35 | } 36 | } else { 37 | $model->save(); 38 | } 39 | 40 | $event = data_get($workflow, "transitions.$transaction.event", false); 41 | 42 | if ($event) { 43 | event(new $event($model)); 44 | } 45 | }); 46 | } catch (\Exception $exception) { 47 | return response()->json($exception->getMessage(), 400); 48 | } 49 | } 50 | 51 | /** 52 | * @param $workflow 53 | * @return array|\Illuminate\Config\Repository|mixed 54 | */ 55 | protected function getWorkflowSetting($workflow) 56 | { 57 | $workflow = collect(config("workflow.workflows.$workflow")); 58 | $workflow = $workflow->merge(['property_path' => $workflow['column']]); 59 | 60 | return $workflow->toArray(); 61 | } 62 | 63 | private function cleanTransaction($transaction) 64 | { 65 | return str_replace('_', ' ', $transaction); 66 | } 67 | } -------------------------------------------------------------------------------- /src/ToolServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->booted(function () { 20 | $this->routes(); 21 | }); 22 | 23 | Nova::serving(function (ServingNova $event) { 24 | Nova::script('workflow', __DIR__.'/../dist/js/tool.js'); 25 | Nova::style('workflow', __DIR__.'/../dist/css/tool.css'); 26 | }); 27 | 28 | 29 | $this->publishes([ 30 | __DIR__.'/../config/workflow.php' => config_path('workflow.php'), 31 | ], 'workflow'); 32 | } 33 | 34 | /** 35 | * Register the tool's routes. 36 | * 37 | * @return void 38 | */ 39 | protected function routes() 40 | { 41 | if ($this->app->routesAreCached()) { 42 | return; 43 | } 44 | 45 | Route::middleware(['nova']) 46 | ->prefix('nova-vendor/workflow') 47 | ->group(__DIR__.'/../routes/api.php'); 48 | } 49 | 50 | /** 51 | * Register any application services. 52 | * 53 | * @return void 54 | */ 55 | public function register() 56 | { 57 | // 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Workflow.php: -------------------------------------------------------------------------------- 1 | merge(['property_path' => $workflow['column']]); 26 | 27 | /** @var \Illuminate\Database\Eloquent\Model $model */ 28 | // $model = app($workflow['model'])->findOrFail(array_last(request()->segments())); 29 | $model = app($workflow['model'])->findOrFail(Arr::last(request()->segments())); 30 | 31 | $stateMachine = new \SM\StateMachine\StateMachine($model, $workflow->toArray()); 32 | 33 | $array = $stateMachine->getPossibleTransitions(); 34 | 35 | $this->fetch_reasons($workflow, $array); 36 | 37 | $this->withMeta([ 38 | 'workflow' => $workflow_name, 39 | 'transactions' => $this->get_transitions($array), 40 | 'styles' => $this->get_styles($workflow), 41 | ]); 42 | 43 | 44 | } catch (ModelNotFoundException $e) { 45 | } 46 | } 47 | 48 | /** 49 | * Get the displayable name of the resource tool. 50 | * 51 | * @return string 52 | */ 53 | public function name() 54 | { 55 | return 'Workflow'; 56 | } 57 | 58 | /** 59 | * Get the component name for the resource tool. 60 | * 61 | * @return string 62 | */ 63 | public function component() 64 | { 65 | return 'workflow'; 66 | } 67 | 68 | /** 69 | * Prepare the panel for JSON serialization. 70 | * 71 | * @return array 72 | */ 73 | public function jsonSerialize() 74 | { 75 | return array_merge([ 76 | 'component' => 'panel', 77 | 'name' => $this->name, 78 | 'showToolbar' => $this->showToolbar, 79 | ], $this->element->meta()); 80 | } 81 | 82 | /** 83 | * @param array $workflow 84 | * @param array $array 85 | */ 86 | protected function fetchReasons($workflow, array $array) 87 | { 88 | collect($workflow['transitions'])->filter(function ($trans, $trans_label) use ($array) { 89 | return in_array($trans_label, $array) && array_key_exists('with_reasons', $trans); 90 | })->each(function ($trans, $trans_label) { 91 | if (!is_array($trans['with_reasons'])) { 92 | return $this->setReasons([ 93 | $trans_label => 'textarea', 94 | ]); 95 | } 96 | 97 | /** @var \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Query\Builder $model */ 98 | $model = app($trans['with_reasons']['model']); 99 | 100 | $columns = $trans['with_reasons']['columns']; 101 | 102 | $this->setReasons([ 103 | $trans_label => $model 104 | ->get(array_values($columns)) 105 | ->groupBy($columns['id']) 106 | ->map(function (Collection $rows) use ($columns) { 107 | return data_get($rows, "0.$columns[label]"); 108 | }) 109 | ->toArray(), 110 | ]); 111 | }); 112 | } 113 | 114 | public function setReasons($reasons) 115 | { 116 | $this->withMeta([ 117 | "reasons" => collect(data_get($this, 'element.meta.reasons', []))->merge($reasons), 118 | ]); 119 | } 120 | 121 | private function get_transitions(array $array) 122 | { 123 | $transactions = []; 124 | foreach ($array as $trans) { 125 | $transactions[$trans] = false; 126 | } 127 | 128 | return $transactions; 129 | } 130 | 131 | private function get_styles(Collection $workflow) 132 | { 133 | return collect($workflow->get('transitions'))->reject(function ($trans) { 134 | return !isset($trans['style_classes']); 135 | })->map(function ($trans) { 136 | return $trans['style_classes']; 137 | })->toArray(); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | let mix = require('laravel-mix') 2 | 3 | mix.js('resources/js/tool.js', 'dist/js') 4 | .sass('resources/sass/tool.scss', 'dist/css') 5 | -------------------------------------------------------------------------------- /yarn-error.log: -------------------------------------------------------------------------------- 1 | Arguments: 2 | /Users/a.algethami/.nvm/versions/node/v6.14.3/bin/node /Users/a.algethami/.yarn/bin/yarn.js 3 | 4 | PATH: 5 | /Users/a.algethami/.nvm/versions/node/v6.14.3/bin:/Users/a.algethami/.yarn/bin:/Users/a.algethami/.config/yarn/global/node_modules/.bin:/Users/a.algethami/bin:/usr/local/bin:/Users/a.algethami/bin:/Users/abdullah/Code/spark-installer:/Users/a.algethami/.composer/vendor/bin/:/usr/local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/a.algethami/Code/nova/node_modules/.bin:/Users/a.algethami/.composer/vendor/bin:~/bin:/Users/abdullah/Code/spark-installer:/Users/a.algethami/.composer/vendor/bin/:/usr/local/bin 6 | 7 | Yarn version: 8 | 1.7.0 9 | 10 | Node version: 11 | 6.14.3 12 | 13 | Platform: 14 | darwin x64 15 | 16 | Trace: 17 | SyntaxError: /Users/a.algethami/Code/nova/nova-components/Workflow/package.json: Unexpected token } in JSON at position 1066 18 | at Object.parse (native) 19 | at /Users/a.algethami/.yarn/lib/cli.js:1130:59 20 | at next (native) 21 | at step (/Users/a.algethami/.yarn/lib/cli.js:98:30) 22 | at /Users/a.algethami/.yarn/lib/cli.js:109:13 23 | 24 | npm manifest: 25 | { 26 | "private": true, 27 | "scripts": { 28 | "dev": "npm run development", 29 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 30 | "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", 31 | "watch-poll": "npm run watch -- --watch-poll", 32 | "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", 33 | "prod": "npm run production", 34 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 35 | }, 36 | "devDependencies": { 37 | "cross-env": "^5.0.0", 38 | "laravel-mix": "^1.0" 39 | }, 40 | "dependencies": { 41 | "vue": "^2.5.0", 42 | "laravel-nova": "^1.0.3", 43 | } 44 | } 45 | 46 | yarn manifest: 47 | No manifest 48 | 49 | Lockfile: 50 | No lockfile 51 | --------------------------------------------------------------------------------