Skip to content

Commit 964dc60

Browse files
committed
Inital commit
1 parent 4cdcfc7 commit 964dc60

15 files changed

+7974
-0
lines changed

.gitignore

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/dist
2+
/.idea
3+
/vendor
4+
/node_modules
5+
package-lock.json
6+
composer.phar
7+
composer.lock
8+
phpunit.xml
9+
.phpunit.result.cache
10+
.DS_Store
11+
Thumbs.db

composer.json

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "cammac/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.3.3"
12+
},
13+
"autoload": {
14+
"psr-4": {
15+
"Cammac\\Workflow\\": "src/"
16+
}
17+
},
18+
"extra": {
19+
"laravel": {
20+
"providers": [
21+
"Cammac\\Workflow\\ToolServiceProvider"
22+
]
23+
}
24+
},
25+
"config": {
26+
"sort-packages": true
27+
},
28+
"minimum-stability": "dev",
29+
"prefer-stable": true
30+
}

config/workflow.php

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
use App\Request;
4+
5+
return [
6+
'workflows' => [
7+
8+
'request' => [
9+
'model' => Request::class,
10+
'column' => 'status',
11+
'states' => [
12+
'pending',
13+
'escalated',
14+
'approved',
15+
'rejected',
16+
],
17+
'transitions' => [
18+
'Approve' => [
19+
'from' => ['pending', 'escalated'],
20+
'to' => 'approved',
21+
'event' => \App\Events\RequestApproved::class,
22+
],
23+
'Escalate' => [
24+
'from' => ['pending'],
25+
'to' => 'escalated',
26+
'with_reasons' => 'escalation_note', // the column name
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',
35+
'label' => 'title',
36+
],
37+
],
38+
],
39+
40+
41+
'Back to My Employee' => [
42+
'from' => ['escalated'],
43+
'to' => 'pending',
44+
],
45+
],
46+
],
47+
],
48+
];

mix-manifest.json

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"/dist/js/tool.js": "/dist/js/tool.js",
3+
"/dist/css/tool.css": "/dist/css/tool.css"
4+
}

package.json

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
}

resources/js/components/Tool.vue

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<template>
2+
<div class="flex" v-if="Object.keys(transactions).length > 0">
3+
<div class="flex-1 m-1" v-for="(showModal, transaction) in transactions">
4+
<button class="bg-50 w-full btn m-1 p-2 rounded" @click.prevent="openModal(transaction)">{{transaction}}</button>
5+
<modal v-if="showModal" @modal-close="close(transaction)">
6+
<form
7+
class="bg-white rounded-lg shadow-lg overflow-hidden"
8+
style="width: 460px"
9+
>
10+
<slot>
11+
<div class="p-8">
12+
<heading :level="2" class="mb-6">{{ __('Choose one of the reasons') }}</heading>
13+
<p class="text-80 leading-normal">
14+
<select v-if="reasons(transaction) != 'textarea'" v-model="reason" class="form-control form-select w-full">
15+
<option v-for="(item, index) in reasons(transaction)" :value="index">{{ item}}</option>
16+
</select>
17+
<textarea v-else v-model="reason" class="form-control form-input-bordered h-auto w-full"></textarea>
18+
</p>
19+
</div>
20+
</slot>
21+
22+
<div class="bg-30 px-6 py-3 flex">
23+
<div class="ml-auto">
24+
<button
25+
type="button"
26+
data-testid="cancel-button"
27+
dusk="cancel-delete-button"
28+
@click.prevent="close(transaction)"
29+
class="btn text-80 font-normal h-9 px-3 mr-3 btn-link"
30+
>
31+
{{ __('Cancel') }}
32+
</button>
33+
<button
34+
id="confirm-delete-button"
35+
ref="confirmButton"
36+
data-testid="confirm-button"
37+
type="submit"
38+
@click="action(transaction)"
39+
class="btn btn-default btn-primary"
40+
>
41+
OK
42+
</button>
43+
</div>
44+
</div>
45+
</form>
46+
</modal>
47+
</div>
48+
</div>
49+
<div v-else>
50+
No action required
51+
</div>
52+
</template>
53+
54+
<script>
55+
56+
export default {
57+
props: ['resourceName', 'resourceId', 'field'],
58+
59+
data() {
60+
return {
61+
transactions: this.field.transactions,
62+
reason:''
63+
}
64+
},
65+
66+
methods: {
67+
close(transaction) {
68+
this.transactions[transaction] = false;
69+
70+
this.$emit('close')
71+
},
72+
73+
reasons(transaction) {
74+
try {
75+
return this.field.reasons[transaction] || [];
76+
} catch (e) {
77+
return [];
78+
}
79+
},
80+
async action(transaction) {
81+
var self = this,
82+
slug = `${this.field.workflow}/${this.resourceId}/${transaction.replace(/\s/g, '_')}/${this.reason}`;
83+
84+
await Nova.request().get(`/nova-vendor/workflow/${slug}`);
85+
86+
self.close(transaction);
87+
88+
self.$toasted.show('Resource successfully changed to ' + transaction, {type: 'success'});
89+
90+
self.$router.push({
91+
name: 'index',
92+
params: {
93+
resourceName: self.resourceName,
94+
resourceId: self.resourceId,
95+
},
96+
})
97+
},
98+
99+
reject: function () {
100+
console.log('reject')
101+
},
102+
openModal: function (transaction) {
103+
if (Object.keys(this.reasons(transaction)).length > 0) {
104+
this.transactions[transaction] = true;
105+
} else {
106+
this.action(transaction);
107+
}
108+
},
109+
},
110+
}
111+
</script>

resources/js/tool.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Nova.booting((Vue, router) => {
2+
Vue.component('workflow', require('./components/Tool'));
3+
})

resources/sass/tool.scss

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// Nova Tool CSS

routes/api.php

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?php
2+
3+
use Cammac\Workflow\Http\Controllers\WorkflowController;
4+
use Illuminate\Support\Facades\Route;
5+
6+
Route::get('/{workflow}/{id}/{transaction}/{reason?}', WorkflowController::class);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace Cammac\Workflow\Http\Controllers;
4+
5+
class WorkflowController
6+
{
7+
public function __invoke($workflow, $id, $transaction, $reason = null)
8+
{
9+
$workflow = $this->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

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace Cammac\Workflow;
4+
5+
use Laravel\Nova\Nova;
6+
use Laravel\Nova\Events\ServingNova;
7+
use Illuminate\Support\Facades\Route;
8+
use Illuminate\Support\ServiceProvider;
9+
10+
class ToolServiceProvider extends ServiceProvider
11+
{
12+
/**
13+
* Bootstrap any application services.
14+
*
15+
* @return void
16+
*/
17+
public function boot()
18+
{
19+
$this->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+
], 'nova-config');
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+
}

0 commit comments

Comments
 (0)