├── .styleci.yml ├── resources ├── img │ ├── 42workflows.png │ ├── workflow_concept.png │ └── nature_background.jpeg ├── views │ ├── fields │ │ ├── text_field.blade.php │ │ ├── dropdown_field.blade.php │ │ ├── data_bus_resource_field.blade.php │ │ ├── trix_input_field.blade.php │ │ └── text_input_field.blade.php │ ├── parts │ │ └── button_trigger.blade.php │ ├── layouts │ │ ├── task_node_html.blade.php │ │ ├── workflow_app.blade.php │ │ ├── conditions_overlay.blade.php │ │ ├── logs_overlay.blade.php │ │ └── settings_overlay.blade.php │ ├── create.blade.php │ ├── edit.blade.php │ ├── index.blade.php │ └── diagram.blade.php ├── fonts │ ├── iconfont │ │ ├── MaterialIcons-Regular.eot │ │ ├── MaterialIcons-Regular.ttf │ │ ├── MaterialIcons-Regular.woff │ │ ├── MaterialIcons-Regular.woff2 │ │ └── MaterialIcons-Regular.ijmap │ └── nunito-sans-v5-latin │ │ ├── nunito-sans-v5-latin-regular.eot │ │ ├── nunito-sans-v5-latin-regular.ttf │ │ ├── nunito-sans-v5-latin-regular.woff │ │ └── nunito-sans-v5-latin-regular.woff2 ├── sass │ ├── workflow.scss │ └── _drawflow.scss ├── js │ └── workflow.js ├── lang │ └── en │ │ └── workflows.php └── css │ └── drawflow.min.css ├── src ├── Fields │ ├── TextField.php │ ├── FieldInterface.php │ ├── DropdownField.php │ ├── TextInputField.php │ ├── TrixInputField.php │ └── Fieldable.php ├── Triggers │ ├── ReRunTrigger.php │ ├── ObserverTrigger.php │ ├── WorkflowObservable.php │ ├── ButtonTrigger.php │ └── Trigger.php ├── DataBuses │ ├── Resource.php │ ├── ConfigResource.php │ ├── ValueResource.php │ ├── DataBusResource.php │ ├── DataBussable.php │ ├── DataBus.php │ └── ModelResource.php ├── WorkflowsFacade.php ├── Tasks │ ├── SaveModel.php │ ├── HttpStatus.php │ ├── DomPDF.php │ ├── RunCommand.php │ ├── Execute.php │ ├── PregReplace.php │ ├── TaskInterface.php │ ├── ChangeModel.php │ ├── SendSlackMessage.php │ ├── LoadModel.php │ ├── SendMail.php │ ├── HtmlInput.php │ ├── TextInput.php │ └── Task.php ├── Workflow.php ├── Notifications │ └── SlackNotification.php ├── Loggers │ ├── TaskLog.php │ └── WorkflowLog.php ├── Jobs │ └── ProcessWorkflow.php ├── WorkflowsServiceProvider.php ├── Workflows.php └── Http │ └── Controllers │ └── WorkflowController.php ├── mix-manifest.json ├── CHANGELOG.md ├── public ├── fonts │ └── vendor │ │ └── @fortawesome │ │ └── fontawesome-free │ │ ├── webfa-solid-900.eot │ │ ├── webfa-solid-900.ttf │ │ ├── webfa-brands-400.eot │ │ ├── webfa-brands-400.ttf │ │ ├── webfa-brands-400.woff │ │ ├── webfa-brands-400.woff2 │ │ ├── webfa-regular-400.eot │ │ ├── webfa-regular-400.ttf │ │ ├── webfa-regular-400.woff │ │ ├── webfa-solid-900.woff │ │ ├── webfa-solid-900.woff2 │ │ └── webfa-regular-400.woff2 └── js │ └── workflow.js.LICENSE.txt ├── database └── migrations │ ├── 2020_04_30_130710_create_workflows_table.php │ ├── 2020_04_30_133430_create_task_logs_table.php │ ├── 2020_06_23_154141_create_triggers_table.php │ ├── 2020_04_30_130638_create_tasks_table.php │ ├── 2020_07_09_173228_create_workflow_logs_table.php │ └── 2022_02_21_173228_update_cascade_delete.php ├── webpack.mix.js ├── LICENSE.md ├── composer.json ├── package.json ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── config └── config.php └── README.md /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | 3 | disabled: 4 | - single_class_element_per_statement 5 | -------------------------------------------------------------------------------- /resources/img/42workflows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/42coders/workflows/HEAD/resources/img/42workflows.png -------------------------------------------------------------------------------- /resources/img/workflow_concept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/42coders/workflows/HEAD/resources/img/workflow_concept.png -------------------------------------------------------------------------------- /resources/img/nature_background.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/42coders/workflows/HEAD/resources/img/nature_background.jpeg -------------------------------------------------------------------------------- /resources/views/fields/text_field.blade.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/Fields/TextField.php: -------------------------------------------------------------------------------- 1 | value" df-name> 2 | @foreach($options as $optionValue => $optionName) 3 | 4 | @endforeach 5 | 6 | -------------------------------------------------------------------------------- /src/Triggers/ReRunTrigger.php: -------------------------------------------------------------------------------- 1 | triggerable->start($log->elementable); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /resources/views/fields/data_bus_resource_field.blade.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /resources/js/workflow.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | $.ajaxSetup({ 4 | headers: { 5 | 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') 6 | } 7 | }); 8 | 9 | import queryBuilder from 'jQuery-QueryBuilder/dist/js/query-builder.standalone'; 10 | window.queryBuilder = queryBuilder; 11 | 12 | import Drawflow from './drawflow'; 13 | window.Drawflow = Drawflow; 14 | -------------------------------------------------------------------------------- /resources/views/parts/button_trigger.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @csrf 3 | 4 | 5 | 6 |
7 | -------------------------------------------------------------------------------- /src/DataBuses/Resource.php: -------------------------------------------------------------------------------- 1 | 'model', 9 | ]; 10 | 11 | public static $output = [ 12 | 'Output' => 'output', 13 | ]; 14 | 15 | public static $icon = ''; 16 | 17 | public function execute(): void 18 | { 19 | $model = $this->getData('model'); 20 | 21 | $model->save(); 22 | 23 | $this->setData('output', $model); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Tasks/HttpStatus.php: -------------------------------------------------------------------------------- 1 | 'url', 11 | ]; 12 | 13 | public static $output = [ 14 | 'HTTP Status' => 'http_status', 15 | ]; 16 | 17 | public static $icon = ''; 18 | 19 | public function execute(): void 20 | { 21 | $this->setData('http_status', Http::get($this->getData('url'))->status()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Tasks/DomPDF.php: -------------------------------------------------------------------------------- 1 | 'html', 11 | ]; 12 | 13 | public static $output = [ 14 | 'PDFFile' => 'pdf_file', 15 | ]; 16 | 17 | public static $icon = ''; 18 | 19 | public function execute(): void 20 | { 21 | $pdf = Pdf::loadHTML($this->getData('html')); 22 | $this->setDataArray('pdf_file', $pdf->output()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Tasks/RunCommand.php: -------------------------------------------------------------------------------- 1 | 'command', 11 | ]; 12 | 13 | public static $output = [ 14 | 'Command Output' => 'command_output', 15 | ]; 16 | 17 | public static $icon = ''; 18 | 19 | public function execute(): void 20 | { 21 | 22 | $this->setData('command_output', Artisan::call($this->getData('command'))); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /public/js/workflow.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * Sizzle CSS Selector Engine v2.3.5 3 | * https://sizzlejs.com/ 4 | * 5 | * Copyright JS Foundation and other contributors 6 | * Released under the MIT license 7 | * https://js.foundation/ 8 | * 9 | * Date: 2020-03-14 10 | */ 11 | 12 | /*! 13 | * jQuery JavaScript Library v3.5.1 14 | * https://jquery.com/ 15 | * 16 | * Includes Sizzle.js 17 | * https://sizzlejs.com/ 18 | * 19 | * Copyright JS Foundation and other contributors 20 | * Released under the MIT license 21 | * https://jquery.org/license 22 | * 23 | * Date: 2020-05-04T22:49Z 24 | */ 25 | -------------------------------------------------------------------------------- /src/Tasks/Execute.php: -------------------------------------------------------------------------------- 1 | 'command', 9 | ]; 10 | 11 | public static $output = [ 12 | 'Command Output' => 'command_output', 13 | ]; 14 | 15 | public static $icon = ''; 16 | 17 | public function execute(): void 18 | { 19 | chdir(base_path()); 20 | dd(shell_exec($this->getData('command').' 2>&1')); 21 | $this->setData('command_output', shell_exec($this->getData('command'))); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /resources/views/layouts/task_node_html.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | {!! $icon !!} {{ $elementName }} 4 |
5 |
6 | {{ $element->data_fields['description']['value'] ?? '' }} 7 |
8 | 12 |
13 | -------------------------------------------------------------------------------- /src/Fields/DropdownField.php: -------------------------------------------------------------------------------- 1 | options = $options; 12 | } 13 | 14 | public static function make(array $options) 15 | { 16 | return new self($options); 17 | } 18 | 19 | public function render($element, $value, $field) 20 | { 21 | return view('workflows::fields.dropdown_field', [ 22 | 'field' => $field, 23 | 'value' => $value, 24 | 'options' => $this->options, 25 | ])->render(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Tasks/PregReplace.php: -------------------------------------------------------------------------------- 1 | 'pattern', 9 | 'Replacement' => 'replacement', 10 | 'Subject' => 'subject', 11 | ]; 12 | 13 | public static $output = [ 14 | 'Preg Replace Output' => 'preg_replace_output', 15 | ]; 16 | 17 | public static $icon = ''; 18 | 19 | public function execute(): void 20 | { 21 | $this->setData('preg_replace_output', preg_replace( 22 | $this->getData('pattern'), 23 | $this->getData('replacement'), 24 | $this->getData('subject'))); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Tasks/TaskInterface.php: -------------------------------------------------------------------------------- 1 | 'model', 9 | 'Field' => 'field', 10 | 'Value' => 'value', 11 | ]; 12 | 13 | public static $output = [ 14 | 'Output' => 'output', 15 | ]; 16 | 17 | public static $icon = ''; 18 | 19 | public function execute(): void 20 | { 21 | $model = $this->getData('model') ?? $this->dataBus->model; 22 | $field = $this->getData('field'); 23 | $value = $this->getData('value'); 24 | 25 | $model->$field = $value; 26 | 27 | $this->setData('output', $model); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Triggers/ObserverTrigger.php: -------------------------------------------------------------------------------- 1 | '; 10 | 11 | public static $fields = [ 12 | 'Class' => 'class', 13 | 'Event' => 'event', 14 | ]; 15 | 16 | public function inputFields(): array 17 | { 18 | $fields = [ 19 | 'class' => DropdownField::make(config('workflows.triggers.Observers.classes')), 20 | 'event' => DropdownField::make(array_combine(config('workflows.triggers.Observers.events'), config('workflows.triggers.Observers.events'))), 21 | ]; 22 | 23 | return $fields; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Tasks/SendSlackMessage.php: -------------------------------------------------------------------------------- 1 | 'channel', 12 | 'Message' => 'message', 13 | ]; 14 | 15 | public static $output = [ 16 | 'Output' => 'output', 17 | ]; 18 | 19 | public static $icon = ''; 20 | 21 | public function execute(): void 22 | { 23 | $channel = $this->getData('channel'); 24 | $message = $this->getData('message'); 25 | 26 | Notification::route('slack', env('WORKFLOW_SLACK_CHANNEL')) 27 | ->notify(new SlackNotification($channel, $message)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /database/migrations/2020_04_30_130710_create_workflows_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('name'); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists(config('workflows.db_prefix').'workflows'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/DataBuses/ConfigResource.php: -------------------------------------------------------------------------------- 1 | inputField($field)) { 22 | return $element->inputField($field)->render($field, $value); 23 | } 24 | 25 | return view('workflows::fields.text_field', [ 26 | 'value' => $value, 27 | 'field' => $field, 28 | ])->render(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Tasks/LoadModel.php: -------------------------------------------------------------------------------- 1 | 'model_class', 11 | 'Model Id' => 'model_id', 12 | ]; 13 | 14 | public static $output = [ 15 | 'Output' => 'output', 16 | ]; 17 | 18 | public static $icon = ''; 19 | 20 | public function inputFields(): array 21 | { 22 | $fields = [ 23 | 'model_class' => DropdownField::make(config('workflows.task_settings.LoadModel.classes')), 24 | ]; 25 | 26 | return $fields; 27 | } 28 | 29 | public function execute(): void 30 | { 31 | $modelClass = $this->getData('model_class'); 32 | $modelId = $this->getData('model_id'); 33 | 34 | $model = $modelClass::find($modelId); 35 | 36 | $this->setData('output', $model); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Workflow.php: -------------------------------------------------------------------------------- 1 | table = config('workflows.db_prefix').$this->table; 20 | parent::__construct($attributes); 21 | } 22 | 23 | public function tasks() 24 | { 25 | return $this->hasMany('the42coders\Workflows\Tasks\Task'); 26 | } 27 | 28 | public function triggers() 29 | { 30 | return $this->hasMany('the42coders\Workflows\Triggers\Trigger'); 31 | } 32 | 33 | public function logs() 34 | { 35 | return $this->hasMany('the42coders\Workflows\Loggers\WorkflowLog'); 36 | } 37 | 38 | public function getTriggerByClass($class) 39 | { 40 | return $this->triggers()->where('type', $class)->first(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | let mix = require('laravel-mix'); 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Mix Asset Management 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Mix provides a clean, fluent API for defining some Webpack build steps 9 | | for your Laravel application. By default, we are compiling the Sass 10 | | file for your application, as well as bundling up your JS files. 11 | | 12 | */ 13 | 14 | mix.webpackConfig({ 15 | resolve: { 16 | alias: { 17 | jquery: 'jquery/src/jquery' 18 | } 19 | } 20 | }); 21 | 22 | mix.autoload({ 23 | jquery: ['$', 'window.jQuery'] 24 | }); 25 | 26 | mix.config.fileLoaderDirs.fonts = 'public/fonts'; 27 | 28 | mix.js([ 29 | 'node_modules/jquery/dist/jquery.js', 30 | 'resources/js/workflow.js', 31 | 'node_modules/bootstrap/dist/js/bootstrap.bundle.js', 32 | ], 'public/js/workflow.js') 33 | .sass('resources/sass/workflow.scss', 'public/css/workflow.css'); 34 | -------------------------------------------------------------------------------- /src/Notifications/SlackNotification.php: -------------------------------------------------------------------------------- 1 | message = $message; 24 | $this->to = $to; 25 | } 26 | 27 | /** 28 | * Get the notification's delivery channels. 29 | * 30 | * @param mixed $notifiable 31 | * @return array 32 | */ 33 | public function via($notifiable) 34 | { 35 | return ['slack']; 36 | } 37 | 38 | public function toSlack($notifiable): SlackMessage 39 | { 40 | return (new SlackMessage) 41 | ->to($this->to) 42 | ->content($this->message); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Max Hutschenreiter 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. -------------------------------------------------------------------------------- /database/migrations/2020_04_30_133430_create_task_logs_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->bigInteger('workflow_log_id'); 19 | $table->bigInteger('task_id'); 20 | $table->string('name'); 21 | $table->string('status'); 22 | $table->text('message')->nullable(); 23 | $table->dateTime('start'); 24 | $table->dateTime('end')->nullable(); 25 | $table->timestamps(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists(config('workflows.db_prefix').'task_logs'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /database/migrations/2020_06_23_154141_create_triggers_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('type'); 19 | $table->string('name'); 20 | $table->boolean('queueable')->default(true); 21 | $table->json('data_fields')->nullable(); 22 | $table->json('conditions')->nullable(); 23 | $table->bigInteger('workflow_id'); 24 | $table->integer('pos_x'); 25 | $table->integer('pos_y'); 26 | $table->timestamps(); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists(config('workflows.db_prefix').'triggers'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /resources/views/create.blade.php: -------------------------------------------------------------------------------- 1 | @extends(config('workflows.layout')) 2 | 3 | @section(config('workflows.section')) 4 |
5 |
6 |
7 |

{{ __('workflows::workflows.Create a new Workflow')}}

8 |
9 |
10 | 11 |
12 |
13 |
14 | @csrf 15 |
16 |
17 | 18 |
19 |
20 |
21 | {{ __('workflows::workflows.Cancel')}} 22 | 23 |
24 |
25 |
26 |
27 |
28 | @endsection 29 | -------------------------------------------------------------------------------- /resources/views/edit.blade.php: -------------------------------------------------------------------------------- 1 | @extends(config('workflows.layout')) 2 | 3 | @section(config('workflows.section')) 4 |
5 |
6 |
7 |

{{ __('workflows::workflows.Edit')}} {{ $workflow->name }} Workflow

8 |
9 |
10 | 11 |
12 |
13 |
14 | @csrf 15 |
16 |
17 | 18 |
19 |
20 |
21 | {{ __('workflows::workflows.Cancel')}} 22 | 23 |
24 |
25 |
26 |
27 |
28 | @endsection 29 | -------------------------------------------------------------------------------- /src/DataBuses/ValueResource.php: -------------------------------------------------------------------------------- 1 | get($field) == $value; 24 | case 'not_equal': 25 | return $dataBus->get($field) != $value; 26 | default: 27 | return true; 28 | } 29 | } 30 | 31 | public static function loadResourceIntelligence(Model $element, $value, $field) 32 | { 33 | if ($element->inputField($field)) { 34 | return $element->inputField($field)->render($element, $value, $field); 35 | } 36 | 37 | return view('workflows::fields.text_field', [ 38 | 'value' => $value, 39 | 'field' => $field, 40 | ])->render(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Fields/TextInputField.php: -------------------------------------------------------------------------------- 1 | $dataBusValue) { 27 | $placholders['data_bus'][$dataBusKey] = '$dataBus->get(\\\''.$dataBusValue.'\\\')'; 28 | } 29 | 30 | $placholders['model'] = ModelResource::getValues($element, $value, $field); 31 | foreach ($placholders['model'] as $modelKey => $modelValue) { 32 | $placholders['model'][$modelKey] = '$model->'.$modelValue; 33 | } 34 | 35 | return view('workflows::fields.text_input_field', [ 36 | 'field' => $field, 37 | 'value' => $value, 38 | 'placeholders' => $placholders, 39 | ])->render(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Fields/TrixInputField.php: -------------------------------------------------------------------------------- 1 | $dataBusValue) { 27 | $placholders['data_bus'][$dataBusKey] = '$dataBus->get(\\\''.$dataBusValue.'\\\')'; 28 | } 29 | 30 | $placholders['model'] = ModelResource::getValues($element, $value, $field); 31 | foreach ($placholders['model'] as $modelKey => $modelValue) { 32 | $placholders['model'][$modelKey] = '$model->'.$modelValue; 33 | } 34 | 35 | return view('workflows::fields.trix_input_field', [ 36 | 'field' => $field, 37 | 'value' => $value, 38 | 'placeholders' => $placholders, 39 | ])->render(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /database/migrations/2020_04_30_130638_create_tasks_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->bigInteger('workflow_id')->index(); 19 | $table->bigInteger('parentable_id')->nullable()->index(); 20 | $table->string('parentable_type')->nullable()->index(); 21 | $table->string('type'); 22 | $table->string('name'); 23 | $table->json('data_fields')->nullable(); 24 | $table->json('conditions')->nullable(); 25 | $table->integer('node_id')->nullable(); 26 | $table->integer('pos_x')->default(0); 27 | $table->integer('pos_y')->default(0); 28 | $table->timestamps(); 29 | }); 30 | } 31 | 32 | /** 33 | * Reverse the migrations. 34 | * 35 | * @return void 36 | */ 37 | public function down() 38 | { 39 | Schema::dropIfExists(config('workflows.db_prefix').'tasks'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/DataBuses/DataBusResource.php: -------------------------------------------------------------------------------- 1 | data[$value]; 12 | } 13 | 14 | public static function checkCondition(Model $element, DataBus $dataBus, string $field, string $operator, string $value) 15 | { 16 | switch ($operator) { 17 | case 'equal': 18 | return $dataBus->data[$dataBus->data[$field]] == $value; 19 | case 'not_equal': 20 | return $dataBus->data[$dataBus->data[$field]] != $value; 21 | default: 22 | return true; 23 | } 24 | } 25 | 26 | public static function getValues(Model $element, $value, $field) 27 | { 28 | return $element->getParentDataBusKeys(); 29 | } 30 | 31 | public static function loadResourceIntelligence(Model $element, $value, $field) 32 | { 33 | $fields = self::getValues($element, $value, $field); 34 | 35 | return view('workflows::fields.data_bus_resource_field', [ 36 | 'fields' => $fields, 37 | 'value' => $value, 38 | 'field' => $field, 39 | ])->render(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/DataBuses/DataBussable.php: -------------------------------------------------------------------------------- 1 | belongsTo('the42coders\Workflows\Workflow'); 10 | } 11 | 12 | public function getParentDataBusKeys($passedFields = []) 13 | { 14 | $newFields = $passedFields; 15 | 16 | if (! empty($this->parentable)) { 17 | //foreach($this->parentable::$fields as $key => $value){ 18 | // $newFields[$key] = $this->parentable->name.' - '.$value; 19 | //} 20 | foreach ($this->parentable::$output as $key => $value) { 21 | $newFields[$this->parentable->name.' - '.$key.' - '.$this->parentable->getFieldValue($value)] = $this->parentable->getFieldValue($value); 22 | } 23 | 24 | $newFields = $this->parentable->getParentDataBusKeys($newFields); 25 | } 26 | 27 | return $newFields; 28 | } 29 | 30 | public function getData(string $value, string $default = '') 31 | { 32 | return $this->dataBus->get($value, $default); 33 | } 34 | 35 | public function setDataArray(string $key, $value) 36 | { 37 | return $this->dataBus->setOutputArray($key, $value); 38 | } 39 | 40 | public function setData(string $key, $value) 41 | { 42 | return $this->dataBus->setOutput($key, $value); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /database/migrations/2020_07_09_173228_create_workflow_logs_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->bigInteger('workflow_id'); 19 | $table->bigInteger('elementable_id')->nullable()->index(); 20 | $table->string('elementable_type')->nullable()->index(); 21 | $table->bigInteger('triggerable_id')->nullable()->index(); 22 | $table->string('triggerable_type')->nullable()->index(); 23 | $table->string('name'); 24 | $table->string('status'); 25 | $table->text('message')->nullable(); 26 | $table->text('databus')->nullable(); 27 | $table->dateTime('start'); 28 | $table->dateTime('end')->nullable(); 29 | $table->timestamps(); 30 | }); 31 | } 32 | 33 | /** 34 | * Reverse the migrations. 35 | * 36 | * @return void 37 | */ 38 | public function down() 39 | { 40 | Schema::dropIfExists(config('workflows.db_prefix').'workflow_logs'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /resources/lang/en/workflows.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'ObserverTrigger' => 'Observer Trigger', 6 | 'ButtonTrigger' => 'Button Trigger', 7 | 'SendMail' => 'Send Mail', 8 | 'Execute' => 'Execute', 9 | 'PregReplace' => 'Preg Replace', 10 | 'HtmlInput' => 'Html Input', 11 | 'DomPDF' => 'DomPDF', 12 | 'HttpStatus' => 'Http Status', 13 | 'LoadModel' => 'Load Model', 14 | 'ChangeModel' => 'Change Model', 15 | 'SaveModel' => 'Save Model', 16 | 'SendSlackMessage' => 'Send Slack Message', 17 | 'TextInput' => 'Text Input', 18 | 'OpenAiTask' => 'OpenAi Task', 19 | 'RunCommand' => 'RunCommand', 20 | ], 21 | 'Resources' => [ 22 | 'ValueResource' => 'From Direct Input', 23 | 'ModelResource' => 'From Model', 24 | 'DataResource' => 'From Workflow', 25 | 'ConfigResource' => 'From Config', 26 | ], 27 | 'Logs' => [ 28 | 'workflowLogs' => 'Workflow Logs', 29 | ], 30 | 'Close' => 'Close', 31 | 'Save' => 'Save', 32 | 'Delete' => 'Delete', 33 | 'delete' => 'delete', 34 | 'Settings' => 'Settings', 35 | 'Cancel' => 'Cancel', 36 | 'Edit' => 'Edit', 37 | 'edit' => 'edit', 38 | 'show' => 'show', 39 | 'create' => 'create', 40 | 'Create a new Workflow' => 'Create a new Workflow', 41 | 'Name' => 'Name', 42 | 'Tasks' => 'Tasks', 43 | 'Created at' => 'Created at', 44 | 'ReRun' => 'ReRun', 45 | ]; 46 | -------------------------------------------------------------------------------- /resources/views/fields/trix_input_field.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @foreach($placeholders as $placholderCategories => $placholder) 3 | 15 | @endforeach 16 | 17 |
18 | 20 | 21 |
22 |
23 | 29 | -------------------------------------------------------------------------------- /src/Tasks/SendMail.php: -------------------------------------------------------------------------------- 1 | 'subject', 9 | 'Recipients' => 'recipients', 10 | 'Sender' => 'sender', 11 | 'Content' => 'content', 12 | 'Files' => 'files', 13 | 'File_Name' => 'file_name', 14 | 'CC' => 'cc', 15 | 'BCC' => 'bcc', 16 | ]; 17 | 18 | public static $icon = ''; 19 | 20 | public function execute(): void 21 | { 22 | $dataBus = $this->dataBus; 23 | 24 | \Mail::html($dataBus->get('content'), function ($message) use ($dataBus) { 25 | $message->subject($dataBus->get('subject')) 26 | ->to($dataBus->get('recipients')) 27 | ->from($dataBus->get('sender')); 28 | $counter = 1; 29 | if (is_array($dataBus->get('files'))) { 30 | foreach ($dataBus->get('files') as $file) { 31 | $message->attachData($file, $dataBus->get('file_name'), [ 32 | 'mime' => 'application/pdf', 33 | ]); 34 | $counter++; 35 | } 36 | } 37 | if (! empty($dataBus->get('cc'))) { 38 | $message->cc($dataBus->get('cc')); 39 | } 40 | if (! empty($dataBus->get('bcc'))) { 41 | $message->bcc($dataBus->get('bcc')); 42 | } 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Tasks/HtmlInput.php: -------------------------------------------------------------------------------- 1 | 'html', 12 | ]; 13 | 14 | public static $output = [ 15 | 'HtmlOutput' => 'html_output', 16 | ]; 17 | 18 | public static $icon = ''; 19 | 20 | public function inputFields(): array 21 | { 22 | return [ 23 | 'html' => TrixInputField::make(), 24 | ]; 25 | } 26 | 27 | public function execute(): void 28 | { 29 | $html = str_replace('>', '>', $this->getData('html')); 30 | 31 | $php = Blade::compileString($html); 32 | $html = $this->render($php, [ 33 | 'model' => $this->model, 34 | 'dataBus' => $this->dataBus, 35 | ]); 36 | 37 | $this->setData('html_output', $html); 38 | } 39 | 40 | public function render($__php, $__data) 41 | { 42 | $obLevel = ob_get_level(); 43 | ob_start(); 44 | extract($__data, EXTR_SKIP); 45 | try { 46 | eval('?'.'>'.$__php); 47 | } catch (Exception $e) { 48 | while (ob_get_level() > $obLevel) { 49 | ob_end_clean(); 50 | } 51 | throw $e; 52 | } catch (Throwable $e) { 53 | while (ob_get_level() > $obLevel) { 54 | ob_end_clean(); 55 | } 56 | throw new FatalThrowableError($e); 57 | } 58 | 59 | return ob_get_clean(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Tasks/TextInput.php: -------------------------------------------------------------------------------- 1 | 'text', 12 | ]; 13 | 14 | public static $output = [ 15 | 'TextOutput' => 'text_output', 16 | ]; 17 | 18 | public static $icon = ''; 19 | 20 | public function inputFields(): array 21 | { 22 | return [ 23 | 'text' => TextInputField::make(), 24 | ]; 25 | } 26 | 27 | public function execute(): void 28 | { 29 | 30 | $text = str_replace('>', '>', $this->getData('text')); 31 | 32 | $php = Blade::compileString($text); 33 | $text = $this->render($php, [ 34 | 'model' => $this->model, 35 | 'dataBus' => $this->dataBus, 36 | ]); 37 | 38 | $this->setData('text_output', $text); 39 | } 40 | 41 | public function render($__php, $__data) 42 | { 43 | $obLevel = ob_get_level(); 44 | ob_start(); 45 | extract($__data, EXTR_SKIP); 46 | try { 47 | eval('?'.'>'.$__php); 48 | } catch (Exception $e) { 49 | while (ob_get_level() > $obLevel) { 50 | ob_end_clean(); 51 | } 52 | throw $e; 53 | } catch (Throwable $e) { 54 | while (ob_get_level() > $obLevel) { 55 | ob_end_clean(); 56 | } 57 | throw new FatalThrowableError($e); 58 | } 59 | 60 | return ob_get_clean(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "the42coders/workflows", 3 | "description": "This Package allows you to automate your Laravel Application from your Backend.", 4 | "keywords": [ 5 | "42coders", 6 | "workflows" 7 | ], 8 | "homepage": "https://github.com/42coders/workflows", 9 | "license": "MIT", 10 | "type": "library", 11 | "authors": [ 12 | { 13 | "name": "Max Hutschenreiter", 14 | "email": "max@42coders.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": ">7.2", 20 | "barryvdh/laravel-dompdf": "^0.9.0|^1.0|^2.0", 21 | "doctrine/dbal": "^3.4", 22 | "guzzlehttp/guzzle": "^7", 23 | "illuminate/support": "*" 24 | }, 25 | "require-dev": { 26 | "orchestra/testbench": "^7", 27 | "phpunit/phpunit": "^9" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "the42coders\\Workflows\\": "src" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "the42coders\\Workflows\\Tests\\": "tests" 37 | } 38 | }, 39 | "scripts": { 40 | "test": "vendor/bin/phpunit", 41 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 42 | 43 | }, 44 | "config": { 45 | "sort-packages": true 46 | }, 47 | "extra": { 48 | "laravel": { 49 | "providers": [ 50 | "the42coders\\Workflows\\WorkflowsServiceProvider" 51 | ], 52 | "aliases": { 53 | "Workflows": "the42coders\\Workflows\\WorkflowsFacade" 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/DataBuses/DataBus.php: -------------------------------------------------------------------------------- 1 | data = $data; 14 | } 15 | 16 | public function collectData(Model $model, $fields): void 17 | { 18 | foreach ($fields as $name => $field) { 19 | //TODO: Quick fix to remove description but handle/filter this better in the future :( 20 | 21 | if ($name === 'description') { 22 | continue; 23 | } 24 | 25 | $field_value = $field['value'] ?? ''; 26 | 27 | if ($name === 'file' && ! $field_value) { 28 | continue; 29 | } 30 | 31 | $className = $field['type'] ?? ValueResource::class; 32 | $resource = new $className(); 33 | 34 | $this->data[$name] = $resource->getData($name, $field_value, $model, $this); 35 | } 36 | } 37 | 38 | public function toString() 39 | { 40 | $output = ''; 41 | 42 | foreach ($this->data as $line) { 43 | $output .= $line.'\n'; 44 | } 45 | 46 | return $output; 47 | } 48 | 49 | public function get(string $key, string $default = null) 50 | { 51 | return $this->data[$key] ?? $default; 52 | } 53 | 54 | public function setOutput(string $key, $value) 55 | { 56 | $this->data[$this->get($key, $key)] = $value; 57 | } 58 | 59 | public function setOutputArray(string $key, string $value) 60 | { 61 | $this->data[$this->get($key, $key)][] = $value; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Loggers/TaskLog.php: -------------------------------------------------------------------------------- 1 | table = config('workflows.db_prefix').$this->table; 29 | parent::__construct($attributes); 30 | } 31 | 32 | public static function createHelper(int $workflow_log_id, int $task_id, string $task_name, string $status = null, string $message = '', $start = null, $end = null): TaskLog 33 | { 34 | return TaskLog::create([ 35 | 'status' => $status ?? self::$STATUS_START, 36 | 'workflow_log_id' => $workflow_log_id, 37 | 'task_id' => $task_id, 38 | 'name' => $task_name, 39 | 'message' => $message, 40 | 'start' => $start ?? Carbon::now(), 41 | 'end' => $end, 42 | ]); 43 | } 44 | 45 | public function setError(string $errorMessage) 46 | { 47 | $this->message = $errorMessage; 48 | $this->status = self::$STATUS_ERROR; 49 | $this->end = Carbon::now(); 50 | $this->save(); 51 | } 52 | 53 | public function finish() 54 | { 55 | $this->status = self::$STATUS_FINISHED; 56 | $this->end = Carbon::now(); 57 | $this->save(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /resources/css/drawflow.min.css: -------------------------------------------------------------------------------- 1 | .drawflow,.drawflow .parent-node{position:relative}.parent-drawflow{display:flex;overflow:hidden;touch-action:none;outline:0}.drawflow{width:100%;height:100%;user-select:none}.drawflow .drawflow-node{display:flex;align-items:center;position:absolute;background:#0ff;width:160px;min-height:40px;border-radius:4px;border:2px solid #000;color:#000;z-index:2;padding:15px}.drawflow .drawflow-node.selected{background:red}.drawflow .drawflow-node:hover{cursor:move}.drawflow .drawflow-node .inputs,.drawflow .drawflow-node .outputs{width:0}.drawflow .drawflow-node .drawflow_content_node{width:100%;display:block}.drawflow .drawflow-node .input,.drawflow .drawflow-node .output{position:relative;width:20px;height:20px;background:#fff;border-radius:50%;border:2px solid #000;cursor:crosshair;z-index:1;margin-bottom:5px}.drawflow .drawflow-node .input{left:-27px;top:2px;background:#ff0}.drawflow .drawflow-node .output{right:-3px;top:2px}.drawflow svg{z-index:0;position:absolute;overflow:visible!important}.drawflow .connection{position:absolute;transform:translate(9999px,9999px)}.drawflow .connection .main-path{fill:none;stroke-width:5px;stroke:#4682b4;transform:translate(-9999px,-9999px)}.drawflow .connection .main-path:hover{stroke:#1266ab;cursor:pointer}.drawflow .connection .main-path.selected{stroke:#43b993}.drawflow .main-path{fill:none;stroke-width:5px;stroke:#4682b4}.drawflow .selectbox{z-index:3;position:absolute;transform:translate(9999px,9999px)}.drawflow .selectbox rect{fill:#00f;opacity:.5;stroke:#ff0;stroke-width:5;stroke-opacity:.5;transform:translate(-9999px,-9999px)}.drawflow-delete{position:absolute;display:block;width:30px;height:30px;background:#000;color:#fff;z-index:4;border:2px solid #fff;line-height:30px;font-weight:700;text-align:center;border-radius:50%;font-family:monospace;cursor:pointer}.drawflow>.drawflow-delete{margin-left:-15px;margin-top:15px}.parent-node .drawflow-delete{right:-15px;top:-15px} -------------------------------------------------------------------------------- /src/DataBuses/ModelResource.php: -------------------------------------------------------------------------------- 1 | {$value}; 13 | } 14 | 15 | public static function getValues(Model $element, $value, $field_name) 16 | { 17 | $classes = []; 18 | foreach ($element->workflow->triggers as $trigger) { 19 | if (isset($trigger->data_fields['class']['value'])) { 20 | $classes[] = $trigger->data_fields['class']['value']; 21 | } 22 | } 23 | 24 | $variables = []; 25 | foreach ($classes as $class) { 26 | $model = new $class; 27 | foreach (Schema::getColumnListing($model->getTable()) as $item) { 28 | $variables[$class.'->'.$item] = $item; 29 | } 30 | } 31 | 32 | return $variables; 33 | } 34 | 35 | public static function checkCondition(Model $element, DataBus $dataBus, string $field, string $operator, string $value) 36 | { 37 | switch ($operator) { 38 | case 'equal': 39 | return $element->{$field} == $value; 40 | case 'not_equal': 41 | return $element->{$field} != $value; 42 | default: 43 | return true; 44 | } 45 | } 46 | 47 | public static function loadResourceIntelligence(Model $element, $value, $field_name) 48 | { 49 | $variables = self::getValues($element, $value, $field_name); 50 | 51 | return view('workflows::fields.data_bus_resource_field', [ 52 | 'fields' => $variables, 53 | 'value' => $value, 54 | 'field' => $field_name, 55 | ])->render(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /resources/views/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends(config('workflows.layout')) 2 | 3 | @section(config('workflows.section')) 4 |
5 |
6 |
7 |

Workflows

8 |
9 |
10 |
11 | 14 |
15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | @foreach($workflows as $workflow) 25 | 26 | 27 | 28 | 29 | 34 | 35 | @endforeach 36 |
{{ __('workflows::workflows.Name')}}{{ __('workflows::workflows.Tasks')}}{{ __('workflows::workflows.Created at')}}
{{ $workflow->name }}{{ $workflow->tasks->count() }}{{ $workflow->created_at->format('d.m.Y') }} 30 | - 31 | - 32 | 33 |
37 | {{ $workflows->links() }} 38 |
39 |
40 |
41 | @endsection 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workflows", 3 | "version": "1.0.0", 4 | "description": "[![Latest Version on Packagist](https://img.shields.io/packagist/v/42coders/workflows.svg?style=flat-square)](https://packagist.org/packages/42coders/workflows) [![Build Status](https://img.shields.io/travis/42coders/workflows/master.svg?style=flat-square)](https://travis-ci.org/42coders/workflows) [![Quality Score](https://img.shields.io/scrutinizer/g/42coders/workflows.svg?style=flat-square)](https://scrutinizer-ci.com/g/42coders/workflows) [![Total Downloads](https://img.shields.io/packagist/dt/42coders/workflows.svg?style=flat-square)](https://packagist.org/packages/42coders/workflows)", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "dev": "NODE_ENV=development webpack --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 11 | "watch": "NODE_ENV=development webpack --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 12 | "hot": "NODE_ENV=development webpack-dev-server --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", 13 | "production": "NODE_ENV=production webpack --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+ssh://git@bitbucket.org/maxhutschenreiter/workflow_package.git" 18 | }, 19 | "author": "", 20 | "license": "ISC", 21 | "homepage": "https://bitbucket.org/maxhutschenreiter/workflow_package#readme", 22 | "devDependencies": { 23 | "laravel-mix": "^5.0.5", 24 | "resolve-url-loader": "^3.1.0", 25 | "sass": "^1.26.10", 26 | "sass-loader": "^8.0.2", 27 | "vue-template-compiler": "^2.6.12" 28 | }, 29 | "dependencies": { 30 | "@fortawesome/fontawesome-free": "^5.14.0", 31 | "bootstrap": "^4.5.2", 32 | "jQuery-QueryBuilder": "^2.5.2", 33 | "jquery": "^3.5.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Jobs/ProcessWorkflow.php: -------------------------------------------------------------------------------- 1 | model = $model; 33 | $this->dataBus = $dataBus; 34 | $this->trigger = $trigger; 35 | $this->log = $log; 36 | } 37 | 38 | /** 39 | * Execute the job. 40 | * 41 | * @return void 42 | */ 43 | public function handle() 44 | { 45 | DB::beginTransaction(); 46 | try { 47 | foreach ($this->trigger->children as $task) { 48 | $task->init($this->model, $this->dataBus, $this->log); 49 | $task->execute(); 50 | $task->pastExecute(); 51 | } 52 | } catch (\Throwable $e) { 53 | DB::rollBack(); 54 | $this->log->setError($e->getMessage(), $this->dataBus); 55 | $this->log->createTaskLogsFromMemory(); 56 | if(config('workflows.debug')) { 57 | throw $e; 58 | } 59 | //dd($e); 60 | } 61 | 62 | $this->log->finish(); 63 | DB::commit(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /resources/views/layouts/workflow_app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{ config('app.name', 'Laravel') }} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 47 | 48 |
49 | @yield('content') 50 |
51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /resources/views/fields/text_input_field.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @foreach($placeholders as $placholderCategories => $placholder) 3 | 15 | @endforeach 16 | 17 |
18 | 19 |
20 |
21 | 45 | -------------------------------------------------------------------------------- /resources/views/layouts/conditions_overlay.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |

{!! $element::$icon !!} {{ $element::getTranslation() }} {{__('workflows::workflows.Settings') }}

7 |
8 |
9 |
10 |
11 |
12 | 36 |
37 |
38 |
39 | 45 |
46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /src/WorkflowsServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadTranslationsFrom(__DIR__.'/../resources/lang', 'workflows'); 19 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'workflows'); 20 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 21 | 22 | if ($this->app->runningInConsole()) { 23 | $this->publishes([ 24 | __DIR__.'/../config/config.php' => config_path('workflows.php'), 25 | ], 'config'); 26 | 27 | // Publishing the views. 28 | $this->publishes([ 29 | __DIR__.'/../resources/views' => resource_path('views/vendor/workflows'), 30 | ], 'views'); 31 | 32 | // Publishing assets. 33 | $this->publishes([ 34 | __DIR__.'/../public/css' => public_path('vendor/workflows/css'), 35 | __DIR__.'/../public/js' => public_path('vendor/workflows/js'), 36 | __DIR__.'/../resources/img' => public_path('vendor/workflows/img'), 37 | //TODO: super hacky to copy it over to public/fonts but have not found a better solution by now. 38 | __DIR__.'/../public/fonts' => public_path('public/fonts'), 39 | ], 'assets'); 40 | 41 | // Publishing the translation files. 42 | $this->publishes([ 43 | __DIR__.'/../resources/lang' => resource_path('lang/vendor/workflows'), 44 | ], 'lang'); 45 | 46 | // Registering package commands. 47 | // $this->commands([]); 48 | } 49 | } 50 | 51 | /** 52 | * Register the application services. 53 | */ 54 | public function register() 55 | { 56 | // Automatically apply the package configuration 57 | $this->mergeConfigFrom(__DIR__.'/../config/config.php', 'workflows'); 58 | 59 | // Register the main class to use with the facade 60 | $this->app->singleton('workflows', function () { 61 | return new Workflows; 62 | }); 63 | 64 | App::register(\Barryvdh\DomPDF\ServiceProvider::class); 65 | //App::register(\Guzz) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /database/migrations/2022_02_21_173228_update_cascade_delete.php: -------------------------------------------------------------------------------- 1 | bigInteger('task_id')->unsigned()->change(); 18 | $table->foreign('task_id')->references('id')->on(config('workflows.db_prefix').'tasks')->onDelete('cascade'); 19 | }); 20 | 21 | Schema::table(config('workflows.db_prefix').'tasks', function (Blueprint $table) { 22 | $table->dropIndex(['workflow_id']); 23 | $table->bigInteger('workflow_id')->unsigned()->change(); 24 | $table->foreign('workflow_id')->references('id')->on(config('workflows.db_prefix').'workflows')->onDelete('cascade'); 25 | }); 26 | 27 | Schema::table(config('workflows.db_prefix').'triggers', function (Blueprint $table) { 28 | $table->bigInteger('workflow_id')->unsigned()->change(); 29 | $table->foreign('workflow_id')->references('id')->on(config('workflows.db_prefix').'workflows')->onDelete('cascade'); 30 | }); 31 | 32 | Schema::table(config('workflows.db_prefix').'workflow_logs', function (Blueprint $table) { 33 | $table->bigInteger('workflow_id')->unsigned()->change(); 34 | $table->foreign('workflow_id')->references('id')->on(config('workflows.db_prefix').'workflows')->onDelete('cascade'); 35 | }); 36 | } 37 | 38 | /** 39 | * Reverse the migrations. 40 | * 41 | * @return void 42 | */ 43 | public function down() 44 | { 45 | Schema::table(config('workflows.db_prefix').'tasks', function (Blueprint $table) { 46 | $table->dropForeign(['workflow_id']); 47 | }); 48 | 49 | Schema::table(config('workflows.db_prefix').'triggers', function (Blueprint $table) { 50 | $table->dropForeign(['workflow_id']); 51 | }); 52 | 53 | Schema::table(config('workflows.db_prefix').'workflow_logs', function (Blueprint $table) { 54 | $table->dropForeign(['workflow_id']); 55 | }); 56 | 57 | Schema::table(config('workflows.db_prefix').'task_logs', function (Blueprint $table) { 58 | $table->dropForeign(['task_id']); 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Triggers/WorkflowObservable.php: -------------------------------------------------------------------------------- 1 | where('data_fields->class->value', 'like', '%'.$className.'%') 58 | ->where('data_fields->event->value', $event) 59 | ->get(); 60 | } 61 | 62 | public static function startWorkflows(Model $model, string $event) 63 | { 64 | if (! in_array($event, config('workflows.triggers.Observers.events'))) { 65 | return false; 66 | } 67 | 68 | foreach (self::getRegisteredTriggers(get_class($model), $event) as $trigger) { 69 | $trigger->start($model); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Fields/Fieldable.php: -------------------------------------------------------------------------------- 1 | data_fields[$field])) { 25 | return ''; 26 | } 27 | 28 | return $this->data_fields[$field]['value'] ?? ''; 29 | } 30 | 31 | /** 32 | * Returns the Field Type. If Field is not existing it returns an empty String. 33 | * 34 | * @param string $field 35 | * @return string 36 | */ 37 | public function getFieldType(string $field): string 38 | { 39 | return $this->data_fields[$field]['type'] ?? ''; 40 | } 41 | 42 | /** 43 | * Check if the Field is from the passed resourceType. 44 | * 45 | * @param string $field 46 | * @param string $resourceType 47 | * @return bool 48 | */ 49 | public function fieldIsResourceType(string $field, string $resourceType): bool 50 | { 51 | return $this->getFieldType($field) === $resourceType; 52 | } 53 | 54 | /** 55 | * Pass selected back if the resourceType is selected for this field. If not an empty String. 56 | * 57 | * @param string $field 58 | * @param string $resourceType 59 | * @return string 60 | */ 61 | public function fieldIsSelected(string $field, string $resourceType): string 62 | { 63 | return $this->fieldIsResourceType($field, $resourceType) ? 'selected' : ''; 64 | } 65 | 66 | /** 67 | * Loads Resource Intelligence from the corresponding DataResourceClass. 68 | * If non is set its taking the first defined one from Config. 69 | * 70 | * @param string $field 71 | * @return string 72 | */ 73 | public function loadResourceIntelligence(string $field): string 74 | { 75 | if (! isset($this->data_fields[$field])) { 76 | $resources = config('workflows.data_resources'); 77 | $class = reset($resources); 78 | } else { 79 | $className = $this->getFieldType($field); 80 | $class = new $className(); 81 | } 82 | 83 | return $class::loadResourceIntelligence($this, $this->getFieldValue($field), $field); 84 | } 85 | 86 | public function inputFields(): array 87 | { 88 | return []; 89 | } 90 | 91 | public function inputField($key) 92 | { 93 | return $this->inputFields()[$key] ?? null; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /resources/views/layouts/logs_overlay.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 8 |
9 |
10 |
11 |

{{ __('workflows::workflows.Logs.workflowLogs') }}

12 |
13 |
14 | 15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | @foreach($workflowLogs as $workflowLog) 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | @endforeach 42 |
IDNameStatusMessageTasks ProcessedTime in SecondsStartEndReRun
{{ $workflowLog->id }}{{ $workflowLog->name }}{{ $workflowLog->status }}{{ $workflowLog->message }}{{ $workflowLog->taskLogs()->count() }} / {{ $workflowLog->workflow->tasks()->count() }}{{ $workflowLog->start->diffInSeconds($workflowLog->end) }}{{ $workflowLog->start }}{{ $workflowLog->end }}
43 |
44 |
45 |
46 | 49 |
50 |
51 |
52 |
53 | 65 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /src/Workflows.php: -------------------------------------------------------------------------------- 1 | empty(config('workflows.prefix')) ? 'workflows' : config('workflows.prefix'), 'namespace' => __NAMESPACE__.'\Http\Controllers'], function () { 12 | Route::get('/', 'WorkflowController@index')->name('workflow.index'); 13 | Route::get('create', 'WorkflowController@create')->name('workflow.create'); 14 | Route::post('store', 'WorkflowController@store')->name('workflow.store'); 15 | Route::get('{workflow}', 'WorkflowController@show')->name('workflow.show'); 16 | Route::get('{workflow}/edit', 'WorkflowController@edit')->name('workflow.edit'); 17 | Route::get('{workflow}/delete', 'WorkflowController@delete')->name('workflow.delete'); 18 | Route::post('{workflow}/update', 'WorkflowController@update')->name('workflow.update'); 19 | 20 | /** diagram routes */ 21 | Route::post('diagram/{workflow}/addTask', 'WorkflowController@addTask')->name('workflow.addTask'); 22 | Route::post('diagram/{workflow}/addTrigger', 'WorkflowController@addTrigger')->name('workflow.addTrigger'); 23 | Route::post('diagram/{workflow}/addConnection', 'WorkflowController@addConnection')->name('workflow.addConnection'); 24 | Route::post('diagram/{workflow}/removeConnection', 'WorkflowController@removeConnection')->name('workflow.removeConnection'); 25 | Route::post('diagram/{workflow}/removeTask', 'WorkflowController@removeTask')->name('workflow.removeTask'); 26 | Route::post('diagram/{workflow}/updateNodePosition', 'WorkflowController@updateNodePosition')->name('workflow.updateNodePosition'); 27 | 28 | /** settings routes */ 29 | Route::post('settings/{workflow}/changeConditions', 'WorkflowController@changeConditions')->name('workflow.changeConditions'); 30 | Route::post('settings/{workflow}/changeValues', 'WorkflowController@changeValues')->name('workflow.changeValues'); 31 | Route::post('settings/{workflow}/getElementSettings', 'WorkflowController@getElementSettings')->name('workflow.getElementSettings'); 32 | Route::post('settings/{workflow}/getElementConditions', 'WorkflowController@getElementConditions')->name('workflow.getElementConditions'); 33 | Route::post('settings/{workflow}/loadResourceIntelligence', 'WorkflowController@loadResourceIntelligence')->name('workflow.loadResourceIntelligence'); 34 | 35 | /** log routes */ 36 | Route::post('logs/reRun/{workflow_log_id}', 'WorkflowController@reRun')->name('workflow.reRun'); 37 | Route::post('logs/reRun/', 'WorkflowController@reRun')->name('workflow.reRunJSHelper'); 38 | Route::post('logs/{workflow}/getLogs', 'WorkflowController@getLogs')->name('workflow.getLogs'); 39 | 40 | /** triggers */ 41 | Route::post('button_trigger/execute/{id}', 'WorkflowController@triggerButton')->name('workflows.triggers.button'); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /resources/views/layouts/settings_overlay.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |

{!! $element::$icon !!} {{ $element::getTranslation() }} {{__('workflows::workflows.Settings') }}

7 |
8 |
9 |
10 |
11 | @foreach($element::$commonFields as $fieldName => $field) 12 |

{{ $fieldName }}

13 |
14 | 16 |
17 | @endforeach 18 | @foreach($element::$fields as $fieldName => $field) 19 |

{{ $fieldName }}

20 |
21 | 28 |
29 |
30 | {!! $element->loadResourceIntelligence($field) !!} 31 |
32 | @endforeach 33 | @foreach($element::$output as $fieldName => $field) 34 |

{{ $fieldName }}

35 |
36 | 38 |
39 | @endforeach 40 |
41 |
42 |
43 | 49 |
50 |
51 |
52 |
53 | 54 | 55 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at max@42coders. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/Triggers/ButtonTrigger.php: -------------------------------------------------------------------------------- 1 | '; 12 | 13 | public static $fields = [ 14 | 'Name' => 'name', 15 | 'Category' => 'category', 16 | 'Class' => 'class', 17 | 'Caption' => 'caption', 18 | 'CSSClasses' => 'css_classes', 19 | 'CSSStyle' => 'css_style', 20 | ]; 21 | 22 | public function inputFields(): array 23 | { 24 | $fields = [ 25 | 'class' => DropdownField::make(config('workflows.triggers.Button.classes')), 26 | 'category' => DropdownField::make(config('workflows.triggers.Button.categories')), 27 | ]; 28 | 29 | return $fields; 30 | } 31 | 32 | /** 33 | * Renders the button_trigger blade template based on the ButtonTrigger values and the Model passed to the Trigger. 34 | * 35 | * @param Model $model 36 | * @return string 37 | */ 38 | public function renderButton(Model $model): string 39 | { 40 | return view('workflows::parts.button_trigger', [ 41 | 'caption' => $this->getFieldValue('caption'), 42 | 'css_classes' => $this->getFieldValue('css_classes'), 43 | 'css_style' => $this->getFieldValue('css_style'), 44 | 'model' => $model, 45 | 'triggerId' => $this->id, 46 | ]); 47 | } 48 | 49 | /** 50 | * Renders a TriggerButton based on the Workflow Id. It will only render the first Trigger if two 51 | * triggers are existing in the Workflow. 52 | * 53 | * @param int $workflow_id 54 | * @param Model $model 55 | * @return string 56 | */ 57 | public static function renderButtonByWorkflowId(int $workflow_id, Model $model): string 58 | { 59 | $workflow = Workflow::find($workflow_id); 60 | 61 | if (empty($workflow)) { 62 | return ''; 63 | } 64 | 65 | $buttonTrigger = $workflow->getTriggerByClass(self::class); 66 | 67 | if (empty($buttonTrigger)) { 68 | return ''; 69 | } 70 | 71 | return $buttonTrigger->renderButton($model); 72 | } 73 | 74 | /** 75 | * Renders a Trigger Button by its defined Name. 76 | * 77 | * @param string $name 78 | * @param Model $model 79 | * @return string 80 | */ 81 | public static function renderButtonByName(string $name, Model $model): string 82 | { 83 | $buttonTrigger = self::where('data_fields->name->value', $name)->first(); 84 | 85 | if (empty($buttonTrigger)) { 86 | return ''; 87 | } 88 | 89 | return $buttonTrigger->renderButton($model); 90 | } 91 | 92 | /** 93 | * Renders all Trigger Buttons with the same category. 94 | * 95 | * @param string $categoryName 96 | * @param Model $model 97 | * @return string 98 | */ 99 | public static function renderButtonsByCategory(string $categoryName, Model $model): string 100 | { 101 | $buttonTriggers = self::where('data_fields->category->value', $categoryName)->get(); 102 | 103 | $html = ''; 104 | 105 | foreach ($buttonTriggers as $buttonTrigger) { 106 | $html .= $buttonTrigger->renderButton($model); 107 | } 108 | 109 | return $html; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Loggers/WorkflowLog.php: -------------------------------------------------------------------------------- 1 | 'datetime', 24 | 'end' => 'datetime', 25 | ]; 26 | 27 | protected $fillable = [ 28 | 'workflow_id', 29 | 'name', 30 | 'status', 31 | 'message', 32 | 'start', 33 | 'elementable_id', 34 | 'elementable_type', 35 | 'triggerable_id', 36 | 'triggerable_type', 37 | ]; 38 | 39 | public function __construct(array $attributes = []) 40 | { 41 | $this->table = config('workflows.db_prefix').$this->table; 42 | parent::__construct($attributes); 43 | } 44 | 45 | public function workflow() 46 | { 47 | return $this->belongsTo('the42coders\Workflows\Workflow'); 48 | } 49 | 50 | public function taskLogs() 51 | { 52 | return $this->hasMany('the42coders\Workflows\Loggers\TaskLog'); 53 | } 54 | 55 | public function elementable() 56 | { 57 | return $this->morphTo(); 58 | } 59 | 60 | public function triggerable() 61 | { 62 | return $this->morphTo(); 63 | } 64 | 65 | public static function createHelper(Model $workflow, Model $element, $trigger): WorkflowLog 66 | { 67 | return WorkflowLog::create([ 68 | 'workflow_id' => $workflow->id, 69 | 'name' => $workflow->name, 70 | 'elementable_id' => $element->id, 71 | 'elementable_type' => get_class($element), 72 | 'triggerable_id' => $trigger->id, 73 | 'triggerable_type' => get_class($trigger), 74 | 'status' => self::$STATUS_START, 75 | 'message' => '', 76 | 'start' => Carbon::now(), 77 | ]); 78 | } 79 | 80 | public function setError(string $errorMessage, DataBus $dataBus) 81 | { 82 | $this->message = $errorMessage; 83 | //$this->databus = $dataBus->toString(); 84 | $this->status = self::$STATUS_ERROR; 85 | $this->end = Carbon::now(); 86 | $this->save(); 87 | } 88 | 89 | public function finish() 90 | { 91 | $this->status = self::$STATUS_FINISHED; 92 | $this->end = Carbon::now(); 93 | $this->save(); 94 | } 95 | 96 | public function addTaskLog(int $workflow_log_id, int $task_id, string $task_name, string $status, string $message, $start, $end = null) 97 | { 98 | $this->taskLogsArray[$task_id] = [ 99 | 'workflow_log_id' => $workflow_log_id, 100 | 'task_id' => $task_id, 101 | 'task_name' => $task_name, 102 | 'status' => $status, 103 | 'message' => $message, 104 | 'start' => $start, 105 | 'end' => $end, 106 | ]; 107 | } 108 | 109 | public function updateTaskLog(int $task_id, string $message, string $status, \DateTime $end) 110 | { 111 | $this->taskLogsArray[$task_id]['message'] = $message; 112 | $this->taskLogsArray[$task_id]['status'] = $status; 113 | $this->taskLogsArray[$task_id]['end'] = $end; 114 | } 115 | 116 | public function createTaskLogsFromMemory() 117 | { 118 | foreach ($this->taskLogsArray as $taskLog) { 119 | TaskLog::updateOrCreate( 120 | [ 121 | 'workflow_log_id' => $taskLog['workflow_log_id'], 122 | 'task_id' => $taskLog['task_id'], 123 | ], 124 | [ 125 | 'name' => $taskLog['task_name'], 126 | 'status' => $taskLog['status'], 127 | 'message' => $taskLog['message'], 128 | 'start' => $taskLog['start'], 129 | 'end' => $taskLog['end'], 130 | ] 131 | ); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Triggers/Trigger.php: -------------------------------------------------------------------------------- 1 | '; 22 | 23 | protected $fillable = [ 24 | 'workflow_id', 25 | 'parent_id', 26 | 'type', 27 | 'name', 28 | 'data', 29 | 'node_id', 30 | 'pos_x', 31 | 'pos_y', 32 | ]; 33 | 34 | public static $output = []; 35 | public static $fields = []; 36 | public static $fields_definitions = []; 37 | 38 | protected $casts = [ 39 | 'data_fields' => 'array', 40 | ]; 41 | 42 | public static $commonFields = [ 43 | 'Description' => 'description', 44 | ]; 45 | 46 | public function __construct(array $attributes = []) 47 | { 48 | $this->table = config('workflows.db_prefix').$this->table; 49 | parent::__construct($attributes); 50 | } 51 | 52 | public function children() 53 | { 54 | return $this->morphMany('the42coders\Workflows\Tasks\Task', 'parentable'); 55 | } 56 | 57 | /** 58 | * Return Collection of models by type. 59 | * 60 | * @param array $attributes 61 | * @param null $connection 62 | * @return \App\Models\Action 63 | */ 64 | public function newFromBuilder($attributes = [], $connection = null) 65 | { 66 | $entryClassName = '\\'.Arr::get((array) $attributes, 'type'); 67 | 68 | if (class_exists($entryClassName) 69 | && is_subclass_of($entryClassName, self::class) 70 | ) { 71 | $model = new $entryClassName(); 72 | } else { 73 | $model = $this->newInstance(); 74 | } 75 | 76 | $model->exists = true; 77 | $model->setRawAttributes((array) $attributes, true); 78 | $model->setConnection($connection ?: $this->connection); 79 | 80 | return $model; 81 | } 82 | 83 | public function start(Model $model, array $data = []) 84 | { 85 | $log = WorkflowLog::createHelper($this->workflow, $model, $this); 86 | $dataBus = new DataBus($data); 87 | 88 | try { 89 | $this->checkConditions($model, $dataBus); 90 | } catch (\Exception $e) { 91 | $log->setError($e->getMessage(), $dataBus); 92 | if(config('workflows.debug')) { 93 | throw $e; 94 | } 95 | exit; 96 | } 97 | 98 | ProcessWorkflow::dispatch($model, $dataBus, $this, $log); 99 | } 100 | 101 | public function checkConditions(Model $model, DataBus $data): bool 102 | { 103 | //TODO: This needs to get smoother :( 104 | if (empty($this->conditions)) { 105 | return true; 106 | } 107 | 108 | $conditions = json_decode($this->conditions); 109 | 110 | foreach ($conditions->rules as $rule) { 111 | $ruleDetails = explode('-', $rule->id); 112 | $DataBus = $ruleDetails[0]; 113 | $field = $ruleDetails[1]; 114 | 115 | $result = config('workflows.data_resources')[$DataBus]::checkCondition($model, $data, $field, $rule->operator, $rule->value); 116 | 117 | if (! $result) { 118 | throw new \Exception('The Condition for Task '.$this->name.' with the field '.$rule->field.' '.$rule->operator.' '.$rule->value.' failed.'); 119 | } 120 | } 121 | 122 | return true; 123 | } 124 | 125 | public function getSettings() 126 | { 127 | return view('workflows::layouts.settings_overlay', [ 128 | 'element' => $this, 129 | ]); 130 | } 131 | 132 | public static function getTranslation(): string 133 | { 134 | return __(static::getTranslationKey()); 135 | } 136 | 137 | public static function getTranslationKey(): string 138 | { 139 | $className = (new \ReflectionClass(new static))->getShortName(); 140 | 141 | return "workflows::workflows.Elements.{$className}"; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | 'workflows::layouts.workflow_app', 14 | 'section' => 'content', 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Tasks 19 | |-------------------------------------------------------------------------- 20 | | 21 | | Here you can register all the Tasks which should be used in the Workflow Package. You can also deactivate Tasks 22 | | just by deleting them here. 23 | | 24 | */ 25 | 'tasks' => [ 26 | 'SendMail' => the42coders\Workflows\Tasks\SendMail::class, 27 | 'Execute' => the42coders\Workflows\Tasks\Execute::class, 28 | 'PregReplace' => the42coders\Workflows\Tasks\PregReplace::class, 29 | 'HtmlInput' => the42coders\Workflows\Tasks\HtmlInput::class, 30 | 'DomPDF' => the42coders\Workflows\Tasks\DomPDF::class, 31 | 'HttpStatus' => the42coders\Workflows\Tasks\HttpStatus::class, 32 | 'LoadModel' => the42coders\Workflows\Tasks\LoadModel::class, 33 | 'ChangeModel' => the42coders\Workflows\Tasks\ChangeModel::class, 34 | 'SaveModel' => the42coders\Workflows\Tasks\SaveModel::class, 35 | 'SendSlackMessage' => the42coders\Workflows\Tasks\SendSlackMessage::class, 36 | 'TextInput' => the42coders\Workflows\Tasks\TextInput::class, 37 | ], 38 | 39 | 'task_settings' => [ 40 | 'LoadModel' => [ 41 | 'classes' => [ 42 | \App\Models\User::class => 'User', 43 | ], 44 | ], 45 | ], 46 | 47 | /* 48 | |-------------------------------------------------------------------------- 49 | | Data Resources 50 | |-------------------------------------------------------------------------- 51 | | 52 | | Here you can register all the Data Resources which should be used in the Workflow Package. You can also 53 | | deactivate Data Resources just by deleting them here. 54 | | 55 | */ 56 | 'data_resources' => [ 57 | 'ValueResource' => the42coders\Workflows\DataBuses\ValueResource::class, 58 | 'ModelResource' => the42coders\Workflows\DataBuses\ModelResource::class, 59 | 'DataResource' => the42coders\Workflows\DataBuses\DataBusResource::class, 60 | 'ConfigResource' => the42coders\Workflows\DataBuses\ConfigResource::class, 61 | ], 62 | 63 | /* 64 | |-------------------------------------------------------------------------- 65 | | Triggers 66 | |-------------------------------------------------------------------------- 67 | | 68 | | Here you can register all the Triggers which should be used in the Workflow Package. You can also 69 | | deactivate Triggers just by deleting them here. 70 | | 71 | | Observers 72 | | 73 | | Events: 74 | | You can register all the events the Trigger should listen to here. 75 | | 76 | | Classes: 77 | | You can register the Classes which can be used for the ObserverTrigger. 78 | | 79 | */ 80 | 'triggers' => [ 81 | 82 | 'types' => [ 83 | 'ObserverTrigger' => the42coders\Workflows\Triggers\ObserverTrigger::class, 84 | 'ButtonTrigger' => the42coders\Workflows\Triggers\ButtonTrigger::class, 85 | ], 86 | 87 | 'Observers' => [ 88 | 'events' => [ 89 | 'retrieved', 90 | 'creating', 91 | 'created', 92 | 'updating', 93 | 'updated', 94 | 'saving', 95 | 'saved', 96 | 'deleting', 97 | 'deleted', 98 | 'restoring', 99 | 'restored', 100 | 'forceDeleted', 101 | ], 102 | 'classes' => [ 103 | \App\Models\User::class => 'User', 104 | \the42coders\Workflows\Loggers\WorkflowLog::class => 'WorkflowLog', 105 | ], 106 | ], 107 | 'Button' => [ 108 | 'classes' => [ 109 | \App\Models\User::class => 'User', 110 | ], 111 | 'categories' => [ 112 | 'all' => 'All', 113 | ], 114 | ], 115 | 116 | ], 117 | 'queue' => 'redis', 118 | 119 | /* 120 | |-------------------------------------------------------------------------- 121 | | Routes 122 | |-------------------------------------------------------------------------- 123 | | 124 | | Configure if the package should load it's default routes. Default its not using the default routes. We recommend 125 | | using them as described in the Documentation because you should put a Auth middleware on them. 126 | */ 127 | 'prefix' => 'workflows', 128 | 129 | /* 130 | |-------------------------------------------------------------------------- 131 | | Database prefixing 132 | |-------------------------------------------------------------------------- 133 | | 134 | | We know how annoying it can be if a package brings a table name into your system which you are even worse another 135 | | package all ready uses. With the db_prefix you can set a prefix to the tables to avoid this conflict. 136 | | This changes needs to be done before the Migrations are running. 137 | */ 138 | 'db_prefix' => '', 139 | 140 | ]; 141 | -------------------------------------------------------------------------------- /src/Tasks/Task.php: -------------------------------------------------------------------------------- 1 | '; 23 | 24 | public $dataBus = null; 25 | public $model = null; 26 | public $workflowLog = null; 27 | 28 | protected $fillable = [ 29 | 'workflow_id', 30 | 'parent_id', 31 | 'type', 32 | 'name', 33 | 'data', 34 | 'node_id', 35 | 'pos_x', 36 | 'pos_y', 37 | ]; 38 | 39 | public static $commonFields = [ 40 | 'Description' => 'description', 41 | ]; 42 | 43 | protected $casts = [ 44 | 'data_fields' => 'array', 45 | ]; 46 | 47 | public static $fields = []; 48 | public static $output = []; 49 | 50 | public function __construct(array $attributes = []) 51 | { 52 | $this->table = config('workflows.db_prefix').$this->table; 53 | parent::__construct($attributes); 54 | } 55 | 56 | public function workflow() 57 | { 58 | return $this->belongsTo('the42coders\Workflows\Workflow'); 59 | } 60 | 61 | public function getFields() 62 | { 63 | return $this->fields; 64 | } 65 | 66 | public function parentable() 67 | { 68 | return $this->morphTo(); 69 | } 70 | 71 | public function children() 72 | { 73 | return $this->morphMany('the42coders\Workflows\Tasks\Task', 'parentable'); 74 | } 75 | 76 | /** 77 | * Return Collection of models by type. 78 | * 79 | * @param array $attributes 80 | * @param null $connection 81 | * @return \App\Models\Action 82 | */ 83 | public function newFromBuilder($attributes = [], $connection = null) 84 | { 85 | $entryClassName = '\\'.Arr::get((array) $attributes, 'type'); 86 | 87 | if (class_exists($entryClassName) 88 | && is_subclass_of($entryClassName, self::class) 89 | ) { 90 | $model = new $entryClassName(); 91 | } else { 92 | $model = $this->newInstance(); 93 | } 94 | 95 | $model->exists = true; 96 | $model->setRawAttributes((array) $attributes, true); 97 | $model->setConnection($connection ?: $this->connection); 98 | 99 | return $model; 100 | } 101 | 102 | /** 103 | * Check if all Conditions for this Action pass. 104 | * 105 | * @param Model $model 106 | * @return bool 107 | */ 108 | public function checkConditions(Model $model, DataBus $data): bool 109 | { 110 | //TODO: This needs to get smoother :( 111 | 112 | if (empty($this->conditions)) { 113 | return true; 114 | } 115 | 116 | $conditions = json_decode($this->conditions); 117 | 118 | foreach ($conditions->rules as $rule) { 119 | $ruleDetails = explode('-', $rule->id); 120 | $DataBus = $ruleDetails[0]; 121 | $field = $ruleDetails[1]; 122 | 123 | $result = config('workflows.data_resources')[$DataBus]::checkCondition($model, $data, $field, $rule->operator, $rule->value); 124 | 125 | if (! $result) { 126 | throw new \Exception('The Condition for Task '.$this->name.' with the field '.$rule->field.' '.$rule->operator.' '.$rule->value.' failed.'); 127 | } 128 | } 129 | 130 | return true; 131 | } 132 | 133 | public function init(Model $model, DataBus $data, WorkflowLog $log) 134 | { 135 | $this->model = $model; 136 | $this->dataBus = $data; 137 | $this->workflowLog = $log; 138 | $this->workflowLog->addTaskLog($this->workflowLog->id, $this->id, $this->name, TaskLog::$STATUS_START, json_encode($this->data_fields), \Illuminate\Support\Carbon::now()); 139 | 140 | $this->log = TaskLog::createHelper($log->id, $this->id, $this->name); 141 | 142 | $this->dataBus->collectData($model, $this->data_fields); 143 | 144 | try { 145 | $this->checkConditions($model, $this->dataBus); 146 | } catch (ConditionFailedError $e) { 147 | throw $e; 148 | } 149 | } 150 | 151 | /** 152 | * Execute the Action return Value tells you about the success. 153 | * 154 | * @return bool 155 | */ 156 | public function execute(): void 157 | { 158 | } 159 | 160 | public function pastExecute() 161 | { 162 | if (empty($this->children)) { 163 | return 'nothing to do'; //TODO: TASK IS FINISHED 164 | } 165 | $this->log->finish(); 166 | $this->workflowLog->updateTaskLog($this->id, '', TaskLog::$STATUS_FINISHED, \Illuminate\Support\Carbon::now()); 167 | foreach ($this->children as $child) { 168 | $child->init($this->model, $this->dataBus, $this->workflowLog); 169 | try { 170 | $child->execute(); 171 | } catch (\Throwable $e) { 172 | $child->workflowLog->updateTaskLog($child->id, $e->getMessage(), TaskLog::$STATUS_ERROR, \Illuminate\Support\Carbon::now()); 173 | throw $e; 174 | } 175 | $child->pastExecute(); 176 | } 177 | } 178 | 179 | public function getSettings() 180 | { 181 | return view('workflows::layouts.settings_overlay', [ 182 | 'element' => $this, 183 | ]); 184 | } 185 | 186 | public static function getTranslation(): string 187 | { 188 | return __(static::getTranslationKey()); 189 | } 190 | 191 | public static function getTranslationKey(): string 192 | { 193 | $className = (new \ReflectionClass(new static))->getShortName(); 194 | 195 | return "workflows::workflows.Elements.{$className}"; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Workflows add Drag & Drop automation's to your Laravel application. 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/the42coders/workflows.svg?style=flat-square)](https://packagist.org/packages/the42coders/workflows) 4 | [![Build Status](https://img.shields.io/travis/42coders/workflows/master.svg?style=flat-square)](https://travis-ci.org/42coders/workflows) 5 | [![Quality Score](https://github.styleci.io/repos/295739465/shield)](https://github.styleci.io/repos/295739465/shield) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/the42coders/workflows.svg?style=flat-square)](https://packagist.org/packages/the42coders/workflows) 7 | 8 | ![Logo](https://github.com/42coders/workflows/blob/master/resources/img/42workflows.png?raw=true) 9 | 10 | The Workflow Package adds Drag & Drop Workflows to your Laravel Application. A Workflow consists of Triggers and Tasks. 11 | The Trigger is responsible for starting a Workflow. The Tasks are single nodes of code execution. 12 | The package comes with some handy tasks bundled, but you can easily write your own as well. 13 | 14 | ![Screenshot](resources/img/workflow_concept.png) 15 | 16 | If you are interested in news and updates 17 | - Follow me on [Twitter](https://twitter.com/gwagwagwa) && || register to our [Newsletter](https://workflows.42coders.com) 18 | 19 | [Video Tutorial](http://www.youtube.com/watch?v=J-fplZGlTZI "Short Introduction Video") 20 | 21 | [![Alt text](https://img.youtube.com/vi/J-fplZGlTZI/mqdefault.jpg)](https://www.youtube.com/watch?v=J-fplZGlTZI) 22 | 23 | ## Installation 24 | 25 | You can install the package via composer: 26 | 27 | ```bash 28 | composer require the42coders/workflows 29 | ``` 30 | 31 | You need to register the routes to your web.php routes File as well. 32 | Since the Workflow Package is very powerful make sure to secure the routes with whatever authentication 33 | you use in the rest of your app. 34 | 35 | ```php 36 | Route::group(['middleware' => ['auth']], function () { 37 | \the42coders\Workflows\Workflows::routes(); 38 | }); 39 | ``` 40 | 41 | You need to publish the assets of the Package 42 | 43 | ```bash 44 | php artisan vendor:publish --provider="the42coders\Workflows\WorkflowsServiceProvider" --tag=assets 45 | ``` 46 | 47 | Other publishable Contents are 48 | 49 | config 50 | 51 | ```bash 52 | php artisan vendor:publish --provider="the42coders\Workflows\WorkflowsServiceProvider" --tag=config 53 | ``` 54 | 55 | language 56 | 57 | ```bash 58 | php artisan vendor:publish --provider="the42coders\Workflows\WorkflowsServiceProvider" --tag=lang 59 | ``` 60 | 61 | views 62 | 63 | ```bash 64 | php artisan vendor:publish --provider="the42coders\Workflows\WorkflowsServiceProvider" --tag=views 65 | ``` 66 | 67 | ## Usage 68 | 69 | The Workflow Package is working out of the Box in your Laravel application. Just go to the route /workflows 70 | to get started. 71 | 72 | 73 | ### Workflows 74 | 75 | A Workflow is gets started by a Trigger and then executes the Tasks in the Order you set them. 76 | To pass information between the Tasks we have the DataBus. 77 | 78 | ### Triggers 79 | 80 | A Trigger is the Starting Point and defines how a Workflow gets called. More Triggers coming soon. 81 | 82 | #### ObserverTrigger 83 | 84 | The Observer Trigger can listen to Eloquent Model Events and will then pass the Model which triggered the Event to the 85 | Workflow. 86 | 87 | To make it Work add the WorkflowObservable to your Eloquent Model. 88 | 89 | ``` php 90 | use WorkflowObservable; 91 | ``` 92 | 93 | #### ButtonTrigger 94 | 95 | The Button Trigger is able to render a button in your "frontend" and Execute a Workflow based by a click on it. 96 | ButtonTrigger also accept an Model which they pass to the Workflow. 97 | 98 | You can influence the buttons by adding your own classes or styles directly to each ButtonTrigger. 99 | Also you can publish the blade and change it according to your needs. 100 | 101 | You have multiple ways of rendering ButtonTrigger. 102 | 103 | ##### ByName 104 | ``` php 105 | {!! the42coders\Workflows\Triggers\ButtonTrigger::renderButtonByName('name', $model) !!} 106 | ``` 107 | 108 | ##### ByWorkflowId 109 | ``` php 110 | {!! the42coders\Workflows\Triggers\ButtonTrigger::renderButtonByWorkflowId(workflow_id, $model) !!} 111 | ``` 112 | 113 | ##### ByCategory 114 | This will return all Triggers from the Category. 115 | 116 | ``` php 117 | {!! the42coders\Workflows\Triggers\ButtonTrigger::renderButtonsByCategory('categoryName', $model) !!} 118 | ``` 119 | 120 | 121 | ### Tasks 122 | 123 | A Task is a single code execution Node in the Workflow. 124 | 125 | Task | Description 126 | ---- | ----------- 127 | ChangeModel | Changes an Eloquent Model (Its not saving the changes to the DB) 128 | DomPDF | The DomPDF Task offers you a way to generate a PDF from HTML and put it to the DataBus (Works great with the HtmlInput Task). 129 | Execute | The Execute Task offers you to execute Shell Commands and is able to push the output of them to the DataBus. 130 | HtmlInput | The HtmlInput Task offers you a Trix Input Field which is able to render Blade. You can put in placeholders for dynamic content in two Ways. From the Model passed through the Workflow or from the DataBus. 131 | HttpStatus | The HttpStatus offers you a way to receive the Http Status of a given URL. 132 | PregReplace | The PregReplace Task offers you a way to to a preg replace on a Value from the Model or a DataBus Variable. 133 | LoadModel | Loads an Eloquent Model from the DB. You can provide the Class and the id. 134 | SaveFile | The SaveFile Task allows you to save Data to a File. Works easily with your registered Storage defines. 135 | SaveModel | Saves an Eloquent Model. 136 | SendMail | The SendMail Task allows you to send a Mail. You can pass the Content and Attachments to it. (Works great with HtmlInput and DomPDF) 137 | SendSlackMessage | This Task let you send a Slack Message. Please read the Section about Slack Notifications to make your app ready too use this. 138 | 139 | #### SendSlackMessage 140 | 141 | To send Slack messages you need to follow this 3 points. 142 | 1. You need to install Slack notifications [Laravel Slack Documentation](https://laravel.com/docs/8.x/notifications#slack-notifications) 143 | 2. You need to set up an incoming Slack Webhook [Slack Documentation](https://api.slack.com/messaging/webhooks) 144 | 3. Set the WebhookUrl to your env file with WORKFLOW_SLACK_CHANNEL=YourSlackWebhookUrl 145 | 146 | ### DataBus 147 | 148 | The DataBus is a way to pass information between the single Tasks. This keeps the Tasks independent of each other. 149 | 150 | Resource | Description 151 | ---- | ----------- 152 | ValueResource | The Value Resource is the simplest Resource. You can just write your Data in an input field. 153 | ConfigResource | The Config Resource lets you access values from your Config Files. 154 | ModelResource | The ModelResource lets you access the Data from the passed Eloquent Model. 155 | DataBusResource | The DataBusResource lets you access the Data from the DataBus. This means all values which got set by a previous Task are access able here. 156 | 157 | 158 | 159 | 160 | ### Testing 161 | 162 | ``` bash 163 | composer test 164 | ``` 165 | 166 | ### Changelog 167 | 168 | Please see [CHANGELOG](CHANGELOG.md) for more information about what has changed recently. 169 | 170 | ## Contributing 171 | 172 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 173 | 174 | ### Security 175 | 176 | If you discover any security related issues, please email max@42coders.com instead of using the issue tracker. 177 | 178 | ## Credits 179 | 180 | - [Max Hutschenreiter](https://github.com/42coders) 181 | - [All Contributors](../../contributors) 182 | - jerosoler for [Drawflow](https://github.com/jerosoler/Drawflow) 183 | 184 | ## License 185 | 186 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 187 | 188 | ## Laravel Package Boilerplate 189 | 190 | This package was generated using the [Laravel Package Boilerplate](https://laravelpackageboilerplate.com). 191 | -------------------------------------------------------------------------------- /resources/sass/_drawflow.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --border-color: #cacaca; 3 | --background-color: #ffffff; 4 | 5 | --background-box-title: #f7f7f7; 6 | } 7 | 8 | html, body { 9 | margin: 0px; 10 | padding: 0px; 11 | width: 100vw; 12 | height: 100vh; 13 | overflow: hidden; 14 | font-family: 'Roboto', sans-serif; 15 | } 16 | 17 | header { 18 | height: 66px; 19 | border-bottom: 1px solid var(--border-color); 20 | padding-left: 20px; 21 | } 22 | header h2 { 23 | margin: 0px; 24 | line-height: 66px; 25 | } 26 | header a { 27 | color: black; 28 | } 29 | .github-link{ 30 | position: absolute; 31 | top: 10px; 32 | right: 20px; 33 | color: black; 34 | } 35 | 36 | .wrapper { 37 | width: 100%; 38 | height: calc(100vh - 67px); 39 | display: flex; 40 | } 41 | 42 | .col { 43 | overflow: auto; 44 | width: 300px; 45 | height: 100%; 46 | border-right: 1px solid var(--border-color); 47 | } 48 | 49 | .drag-drawflow { 50 | line-height: 50px; 51 | border-bottom: 1px solid var(--border-color); 52 | padding-left: 20px; 53 | cursor: move; 54 | user-select: none; 55 | } 56 | .menu { 57 | position: absolute; 58 | height: 40px; 59 | display: block; 60 | background: white; 61 | width: 100%; 62 | } 63 | .menu ul { 64 | padding: 0px; 65 | margin: 0px; 66 | line-height: 40px; 67 | } 68 | 69 | .menu ul li { 70 | display: inline-block; 71 | margin-left: 10px; 72 | border-right: 1px solid var(--border-color); 73 | padding-right: 10px; 74 | line-height: 40px; 75 | cursor: pointer; 76 | } 77 | 78 | .menu ul li.selected { 79 | font-weight: bold; 80 | } 81 | 82 | .btn-export { 83 | float: right; 84 | position: absolute; 85 | top: 10px; 86 | right: 10px; 87 | color: white; 88 | font-weight: bold; 89 | border: 1px solid #0e5ba3; 90 | background: #4ea9ff; 91 | padding: 5px 10px; 92 | border-radius: 4px; 93 | cursor: pointer; 94 | z-index: 5; 95 | } 96 | 97 | .btn-logs{ 98 | float: right; 99 | position: absolute; 100 | bottom: 10px; 101 | right: 158px; 102 | display: flex; 103 | font-size: 24px; 104 | color: white; 105 | padding: 5px 10px; 106 | background: #555555; 107 | border-radius: 4px; 108 | border-right: 1px solid var(--border-color); 109 | z-index: 5; 110 | cursor: pointer; 111 | } 112 | 113 | 114 | .btn-clear { 115 | float: right; 116 | position: absolute; 117 | top: 10px; 118 | right: 85px; 119 | color: white; 120 | font-weight: bold; 121 | border: 1px solid #96015b; 122 | background: #e3195a; 123 | padding: 5px 10px; 124 | border-radius: 4px; 125 | cursor: pointer; 126 | z-index: 5; 127 | } 128 | .swal-wide{ 129 | width:80% !important; 130 | } 131 | 132 | .btn-lock { 133 | float: right; 134 | position: absolute; 135 | bottom: 10px; 136 | right: 110px; 137 | display: flex; 138 | font-size: 24px; 139 | color: white; 140 | padding: 5px 10px; 141 | background: #555555; 142 | border-radius: 4px; 143 | border-right: 1px solid var(--border-color); 144 | z-index: 5; 145 | cursor: pointer; 146 | } 147 | 148 | .bar-zoom { 149 | float: right; 150 | position: absolute; 151 | bottom: 10px; 152 | right: 10px; 153 | display: flex; 154 | font-size: 24px; 155 | color: white; 156 | padding: 5px 10px; 157 | background: #555555; 158 | border-radius: 4px; 159 | border-right: 1px solid var(--border-color); 160 | z-index: 5; 161 | } 162 | .bar-zoom svg { 163 | cursor: pointer; 164 | padding-left: 10px; 165 | } 166 | .bar-zoom svg:nth-child(1) { 167 | padding-left: 0px; 168 | } 169 | 170 | #drawflow { 171 | position: relative; 172 | width: calc(100vw - 301px); 173 | height: calc(100%); 174 | //background: var(--background-color); 175 | //background-size: 25px 25px; 176 | 177 | background-repeat: no-repeat; 178 | background-size: cover; 179 | // linear-gradient(to right, #f1f1f1 1px, transparent 1px), 180 | // linear-gradient(to bottom, #f1f1f1 1px, transparent 1px); 181 | } 182 | 183 | @media only screen and (max-width: 768px) { 184 | .col { 185 | width: 50px; 186 | } 187 | .col .drag-drawflow span { 188 | display:none; 189 | } 190 | #drawflow { 191 | width: calc(100vw - 51px); 192 | } 193 | } 194 | 195 | 196 | 197 | /* Editing Drawflow */ 198 | 199 | .drawflow .drawflow-node { 200 | background: var(--background-color); 201 | border: 1px solid var(--border-color); 202 | -webkit-box-shadow: 0 2px 15px 2px var(--border-color); 203 | box-shadow: 0 2px 15px 2px var(--border-color); 204 | padding: 0px; 205 | width: 200px; 206 | } 207 | 208 | .drawflow .drawflow-node.selected { 209 | background: white; 210 | border: 1px solid #4ea9ff; 211 | -webkit-box-shadow: 0 2px 20px 2px #4ea9ff; 212 | box-shadow: 0 2px 20px 2px #4ea9ff; 213 | } 214 | 215 | .drawflow .drawflow-node.selected .title-box { 216 | color: #22598c; 217 | /*border-bottom: 1px solid #4ea9ff;*/ 218 | } 219 | 220 | .drawflow .connection .main-path { 221 | stroke: #4ea9ff; 222 | stroke-width: 3px; 223 | } 224 | 225 | .drawflow .drawflow-node .input, .drawflow .drawflow-node .output { 226 | height: 15px; 227 | width: 15px; 228 | border: 2px solid var(--border-color); 229 | } 230 | 231 | .drawflow .drawflow-node .input:hover, .drawflow .drawflow-node .output:hover { 232 | background: #4ea9ff; 233 | } 234 | 235 | .drawflow .drawflow-node .output { 236 | right: 10px; 237 | } 238 | 239 | .drawflow .drawflow-node .input { 240 | left: -10px; 241 | background: white; 242 | } 243 | 244 | .drawflow > .drawflow-delete { 245 | border: 2px solid #43b993; 246 | background: white; 247 | color: #43b993; 248 | -webkit-box-shadow: 0 2px 20px 2px #43b993; 249 | box-shadow: 0 2px 20px 2px #43b993; 250 | } 251 | 252 | .drawflow-delete { 253 | border: 2px solid #4ea9ff; 254 | background: white; 255 | color: #4ea9ff; 256 | -webkit-box-shadow: 0 2px 20px 2px #4ea9ff; 257 | box-shadow: 0 2px 20px 2px #4ea9ff; 258 | } 259 | 260 | .drawflow-node .title-box { 261 | height: 50px; 262 | line-height: 50px; 263 | background: var(--background-box-title); 264 | border-bottom: 1px solid #e9e9e9; 265 | border-radius: 4px 4px 0px 0px; 266 | padding-left: 10px; 267 | } 268 | .drawflow .title-box svg { 269 | position: initial; 270 | } 271 | .drawflow-node .box { 272 | padding: 10px 20px 20px 20px; 273 | font-size: 14px; 274 | color: #555555; 275 | 276 | } 277 | .drawflow-node .box p { 278 | margin-top: 5px; 279 | margin-bottom: 5px; 280 | } 281 | 282 | .drawflow-node.welcome { 283 | width: 250px; 284 | } 285 | 286 | .drawflow-node.slack .title-box { 287 | border-radius: 4px; 288 | } 289 | 290 | .drawflow-node input, .drawflow-node select, .drawflow-node textarea { 291 | border-radius: 4px; 292 | border: 1px solid var(--border-color); 293 | height: 30px; 294 | line-height: 30px; 295 | font-size: 16px; 296 | width: 158px; 297 | color: #555555; 298 | } 299 | 300 | .drawflow-node textarea { 301 | height: 100px; 302 | } 303 | 304 | 305 | .drawflow-node.personalized { 306 | background: red; 307 | height: 200px; 308 | text-align: center; 309 | color: white; 310 | } 311 | .drawflow-node.personalized .input { 312 | background: yellow; 313 | } 314 | .drawflow-node.personalized .output { 315 | background: green; 316 | } 317 | 318 | .drawflow-node.personalized.selected { 319 | background: blue; 320 | } 321 | 322 | .settings-container{ 323 | display: none; 324 | position: absolute; 325 | top: 0; 326 | left: 0; 327 | z-index: 42; 328 | background-color: #ffffff; 329 | width: 100%; 330 | height: 100%; 331 | padding-top: 100px; 332 | } 333 | 334 | .settings-button{ 335 | font-size: 20px; 336 | &:hover{ 337 | color: lightgray; 338 | cursor: pointer; 339 | } 340 | } 341 | 342 | .modal-backdrop { 343 | z-index: -1; 344 | } 345 | 346 | /* Modal */ 347 | 348 | .modal { 349 | //display: none; 350 | position: fixed; 351 | z-index: 42; 352 | left: 50%; 353 | //top: 0; 354 | width: 100vw; 355 | height: 100vh; 356 | overflow: auto; 357 | background-color: rgb(0,0,0); 358 | background-color: rgba(0,0,0,0.7); 359 | 360 | } 361 | /* 362 | .modal-content { 363 | position: relative; 364 | background-color: #fefefe; 365 | margin: 15% auto; 366 | //padding: 20px; 367 | //border: 1px solid #888; 368 | // width: 400px; 369 | } 370 | 371 | /* The Close Button *//* 372 | .modal .close { 373 | color: #aaa; 374 | float: right; 375 | font-size: 28px; 376 | font-weight: bold; 377 | cursor:pointer; 378 | } 379 | 380 | @media only screen and (max-width: 768px) { 381 | .modal-content { 382 | width: 80%; 383 | } 384 | } 385 | */ 386 | 387 | -------------------------------------------------------------------------------- /src/Http/Controllers/WorkflowController.php: -------------------------------------------------------------------------------- 1 | $workflows]); 21 | } 22 | 23 | public function show($id) 24 | { 25 | $workflow = Workflow::find($id); 26 | 27 | return view('workflows::diagram', ['workflow' => $workflow]); 28 | } 29 | 30 | public function create() 31 | { 32 | return view('workflows::create'); 33 | } 34 | 35 | public function store(Request $request) 36 | { 37 | $workflow = Workflow::create($request->all()); 38 | 39 | return redirect(route('workflow.show', ['workflow' => $workflow])); 40 | } 41 | 42 | public function edit($id) 43 | { 44 | $workflow = Workflow::find($id); 45 | 46 | return view('workflows::edit', [ 47 | 'workflow' => $workflow, 48 | ]); 49 | } 50 | 51 | public function update(Request $request, $id) 52 | { 53 | $workflow = Workflow::find($id); 54 | 55 | $workflow->update($request->all()); 56 | 57 | return redirect(route('workflow.index')); 58 | } 59 | 60 | /** 61 | * Deletes the Workflow and over cascading also the Tasks, TaskLogs, WorkflowLogs and Triggers. 62 | * 63 | * @param $id 64 | * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector 65 | */ 66 | public function delete($id) 67 | { 68 | $workflow = Workflow::find($id); 69 | 70 | $workflow->delete(); 71 | 72 | return redirect(route('workflow.index')); 73 | } 74 | 75 | public function addTask($id, Request $request) 76 | { 77 | $workflow = Workflow::find($id); 78 | if ($request->data['type'] == 'trigger') { 79 | return [ 80 | 'task' => '', 81 | ]; 82 | } 83 | $task = Task::where('workflow_id', $workflow->id)->where('node_id', $request->id)->first(); 84 | 85 | if (! empty($task)) { 86 | $task->pos_x = $request->pos_x; 87 | $task->pos_y = $request->pos_y; 88 | $task->save(); 89 | 90 | return ['task' => $task]; 91 | } 92 | 93 | if (array_key_exists($request->name, config('workflows.tasks'))) { 94 | $task = config('workflows.tasks')[$request->name]::create([ 95 | 'type' => config('workflows.tasks')[$request->name], 96 | 'workflow_id' => $workflow->id, 97 | 'name' => $request->name, 98 | 'data_fields' => null, 99 | 'node_id' => $request->id, 100 | 'pos_x' => $request->pos_x, 101 | 'pos_y' => $request->pos_y, 102 | ]); 103 | } 104 | 105 | return [ 106 | 'task' => $task, 107 | 'node_id' => $request->id, 108 | ]; 109 | } 110 | 111 | public function addTrigger($id, Request $request) 112 | { 113 | $workflow = Workflow::find($id); 114 | 115 | if (array_key_exists($request->name, config('workflows.triggers.types'))) { 116 | $trigger = config('workflows.triggers.types')[$request->name]::create([ 117 | 'type' => config('workflows.triggers.types')[$request->name], 118 | 'workflow_id' => $workflow->id, 119 | 'name' => $request->name, 120 | 'data_fields' => null, 121 | 'pos_x' => $request->pos_x, 122 | 'pos_y' => $request->pos_y, 123 | ]); 124 | } 125 | 126 | return [ 127 | 'trigger' => $trigger, 128 | 'node_id' => $request->id, 129 | ]; 130 | } 131 | 132 | public function changeConditions($id, Request $request) 133 | { 134 | $workflow = Workflow::find($id); 135 | 136 | if ($request->type == 'task') { 137 | $element = $workflow->tasks->find($request->id); 138 | } 139 | 140 | if ($request->type == 'trigger') { 141 | $element = $workflow->triggers->find($request->id); 142 | } 143 | 144 | $element->conditions = $request->data; 145 | $element->save(); 146 | 147 | return $element; 148 | } 149 | 150 | public function changeValues($id, Request $request) 151 | { 152 | $workflow = Workflow::find($id); 153 | 154 | if ($request->type == 'task') { 155 | $element = $workflow->tasks->find($request->id); 156 | } 157 | 158 | if ($request->type == 'trigger') { 159 | $element = $workflow->triggers->find($request->id); 160 | } 161 | 162 | $data = []; 163 | 164 | foreach ($request->data as $key => $value) { 165 | $path = explode('->', $key); 166 | $data[$path[0]][$path[1]] = $value; 167 | } 168 | $element->data_fields = $data; 169 | $element->save(); 170 | 171 | return $element; 172 | } 173 | 174 | public function updateNodePosition($id, Request $request) 175 | { 176 | $element = $this->getElementByNode($id, $request->node); 177 | 178 | $element->pos_x = $request->node['pos_x']; 179 | $element->pos_y = $request->node['pos_y']; 180 | $element->save(); 181 | 182 | return ['status' => 'success']; 183 | } 184 | 185 | public function getElementByNode($workflow_id, $node) 186 | { 187 | if ($node['data']['type'] == 'task') { 188 | $element = Task::where('workflow_id', $workflow_id)->where('id', $node['data']['task_id'])->first(); 189 | } 190 | 191 | if ($node['data']['type'] == 'trigger') { 192 | $element = Trigger::where('workflow_id', $workflow_id)->where('id', $node['data']['trigger_id'])->first(); 193 | } 194 | 195 | return $element; 196 | } 197 | 198 | public function addConnection($id, Request $request) 199 | { 200 | $workflow = Workflow::find($id); 201 | 202 | if ($request->parent_element['data']['type'] == 'trigger') { 203 | $parentElement = Trigger::where('workflow_id', $workflow->id)->where('id', $request->parent_element['data']['trigger_id'])->first(); 204 | } 205 | if ($request->parent_element['data']['type'] == 'task') { 206 | $parentElement = Task::where('workflow_id', $workflow->id)->where('id', $request->parent_element['data']['task_id'])->first(); 207 | } 208 | if ($request->child_element['data']['type'] == 'trigger') { 209 | $childElement = Trigger::where('workflow_id', $workflow->id)->where('id', $request->child_element['data']['trigger_id'])->first(); 210 | } 211 | if ($request->child_element['data']['type'] == 'task') { 212 | $childElement = Task::where('workflow_id', $workflow->id)->where('id', $request->child_element['data']['task_id'])->first(); 213 | } 214 | 215 | $childElement->parentable_id = $parentElement->id; 216 | $childElement->parentable_type = get_class($parentElement); 217 | 218 | $childElement->save(); 219 | 220 | return ['status' => 'success']; 221 | } 222 | 223 | public function removeConnection($id, Request $request) 224 | { 225 | $workflow = Workflow::find($id); 226 | 227 | $childTask = Task::where('workflow_id', $workflow->id)->where('node_id', $request->input_id)->first(); 228 | 229 | $childTask->parentable_id = 0; 230 | $childTask->parentable_type = null; 231 | $childTask->save(); 232 | 233 | return ['status' => 'success']; 234 | } 235 | 236 | public function removeTask($id, Request $request) 237 | { 238 | $workflow = Workflow::find($id); 239 | 240 | $element = $this->getElementByNode($id, $request->node); 241 | 242 | $element->delete(); 243 | 244 | return [ 245 | 'status' => 'success', 246 | ]; 247 | } 248 | 249 | public function getElementSettings($id, Request $request) 250 | { 251 | $workflow = Workflow::find($id); 252 | 253 | if ($request->type == 'task') { 254 | $element = Task::where('workflow_id', $workflow->id)->where('id', $request->element_id)->first(); 255 | } 256 | if ($request->type == 'trigger') { 257 | $element = Trigger::where('workflow_id', $workflow->id)->where('id', $request->element_id)->first(); 258 | } 259 | 260 | return view('workflows::layouts.settings_overlay', [ 261 | 'element' => $element, 262 | ]); 263 | } 264 | 265 | public function getElementConditions($id, Request $request) 266 | { 267 | $workflow = Workflow::find($id); 268 | 269 | if ($request->type == 'task') { 270 | $element = Task::where('workflow_id', $workflow->id)->where('id', $request->element_id)->first(); 271 | } 272 | if ($request->type == 'trigger') { 273 | $element = Trigger::where('workflow_id', $workflow->id)->where('id', $request->element_id)->first(); 274 | } 275 | 276 | $filter = []; 277 | 278 | foreach (config('workflows.data_resources') as $resourceName => $resourceClass) { 279 | $filter[$resourceName] = $resourceClass::getValues($element, null, null); 280 | } 281 | 282 | return view('workflows::layouts.conditions_overlay', [ 283 | 'element' => $element, 284 | 'conditions' => $element->conditions, 285 | 'allFilters' => $filter, 286 | ]); 287 | } 288 | 289 | public function loadResourceIntelligence($id, Request $request) 290 | { 291 | $workflow = Workflow::find($id); 292 | 293 | if ($request->type == 'task') { 294 | $element = Task::where('workflow_id', $workflow->id)->where('id', $request->element_id)->first(); 295 | } 296 | if ($request->type == 'trigger') { 297 | $element = Trigger::where('workflow_id', $workflow->id)->where('id', $request->element_id)->first(); 298 | } 299 | 300 | if (in_array($request->resource, config('workflows.data_resources'))) { 301 | $className = $request->resource ?? 'the42coders\\Workflows\\DataBuses\\ValueResource'; 302 | $resource = new $className(); 303 | $html = $resource->loadResourceIntelligence($element, $request->value, $request->field_name); 304 | } 305 | 306 | return response()->json([ 307 | 'html' => $html, 308 | 'id' => $request->field_name, 309 | ]); 310 | } 311 | 312 | public function getLogs($id) 313 | { 314 | $workflow = Workflow::find($id); 315 | 316 | $workflowLogs = $workflow->logs()->orderBy('start', 'desc')->get(); 317 | //TODO: get Pagination working 318 | 319 | return view('workflows::layouts.logs_overlay', [ 320 | 'workflowLogs' => $workflowLogs, 321 | ]); 322 | } 323 | 324 | public function reRun($workflowLogId) 325 | { 326 | $log = WorkflowLog::find($workflowLogId); 327 | 328 | ReRunTrigger::startWorkflow($log); 329 | 330 | return [ 331 | 'status' => 'started', 332 | ]; 333 | } 334 | 335 | public function triggerButton(Request $request, $triggerId) 336 | { 337 | $trigger = Trigger::findOrFail($triggerId); 338 | $className = $request->model_class; 339 | $resource = new $className(); 340 | 341 | $model = $resource->find($request->model_id); 342 | 343 | $trigger->start($model, []); 344 | 345 | return redirect()->back()->with('sucess', 'Button Triggered a Workflow'); 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /resources/views/diagram.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{ config('app.name', 'Laravel') }} 11 | 12 | 13 | 14 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 38 | 43 | 44 | 45 | 46 |
47 |
48 | 49 |
50 |
51 |
52 |
53 | @foreach(config('workflows.triggers.types') as $taskName => $taskClass) 54 |
55 | {!! $taskClass::$icon !!} {{$taskClass::getTranslation() }} 56 |
57 | @endforeach 58 | @foreach(config('workflows.tasks') as $taskName => $taskClass) 59 |
60 | {!! $taskClass::$icon !!} {{ __('workflows::workflows.Elements.'.$taskName) }} 61 |
62 | @endforeach 63 |
64 |
65 |
66 |
67 | 68 |
69 |
70 | 71 | 73 |
74 |
75 | 76 | 77 | 78 |
79 |
80 |
81 |
82 |
83 | 84 |
85 |
86 | 87 |
88 | 526 | 527 | 528 | 529 | 530 | -------------------------------------------------------------------------------- /resources/fonts/iconfont/MaterialIcons-Regular.ijmap: -------------------------------------------------------------------------------- 1 | {"icons":{"e84d":{"name":"3d Rotation"},"eb3b":{"name":"Ac Unit"},"e190":{"name":"Access Alarm"},"e191":{"name":"Access Alarms"},"e192":{"name":"Access Time"},"e84e":{"name":"Accessibility"},"e914":{"name":"Accessible"},"e84f":{"name":"Account Balance"},"e850":{"name":"Account Balance Wallet"},"e851":{"name":"Account Box"},"e853":{"name":"Account Circle"},"e60e":{"name":"Adb"},"e145":{"name":"Add"},"e439":{"name":"Add A Photo"},"e193":{"name":"Add Alarm"},"e003":{"name":"Add Alert"},"e146":{"name":"Add Box"},"e147":{"name":"Add Circle"},"e148":{"name":"Add Circle Outline"},"e567":{"name":"Add Location"},"e854":{"name":"Add Shopping Cart"},"e39d":{"name":"Add To Photos"},"e05c":{"name":"Add To Queue"},"e39e":{"name":"Adjust"},"e630":{"name":"Airline Seat Flat"},"e631":{"name":"Airline Seat Flat Angled"},"e632":{"name":"Airline Seat Individual Suite"},"e633":{"name":"Airline Seat Legroom Extra"},"e634":{"name":"Airline Seat Legroom Normal"},"e635":{"name":"Airline Seat Legroom Reduced"},"e636":{"name":"Airline Seat Recline Extra"},"e637":{"name":"Airline Seat Recline Normal"},"e195":{"name":"Airplanemode Active"},"e194":{"name":"Airplanemode Inactive"},"e055":{"name":"Airplay"},"eb3c":{"name":"Airport Shuttle"},"e855":{"name":"Alarm"},"e856":{"name":"Alarm Add"},"e857":{"name":"Alarm Off"},"e858":{"name":"Alarm On"},"e019":{"name":"Album"},"eb3d":{"name":"All Inclusive"},"e90b":{"name":"All Out"},"e859":{"name":"Android"},"e85a":{"name":"Announcement"},"e5c3":{"name":"Apps"},"e149":{"name":"Archive"},"e5c4":{"name":"Arrow Back"},"e5db":{"name":"Arrow Downward"},"e5c5":{"name":"Arrow Drop Down"},"e5c6":{"name":"Arrow Drop Down Circle"},"e5c7":{"name":"Arrow Drop Up"},"e5c8":{"name":"Arrow Forward"},"e5d8":{"name":"Arrow Upward"},"e060":{"name":"Art Track"},"e85b":{"name":"Aspect Ratio"},"e85c":{"name":"Assessment"},"e85d":{"name":"Assignment"},"e85e":{"name":"Assignment Ind"},"e85f":{"name":"Assignment Late"},"e860":{"name":"Assignment Return"},"e861":{"name":"Assignment Returned"},"e862":{"name":"Assignment Turned In"},"e39f":{"name":"Assistant"},"e3a0":{"name":"Assistant Photo"},"e226":{"name":"Attach File"},"e227":{"name":"Attach Money"},"e2bc":{"name":"Attachment"},"e3a1":{"name":"Audiotrack"},"e863":{"name":"Autorenew"},"e01b":{"name":"Av Timer"},"e14a":{"name":"Backspace"},"e864":{"name":"Backup"},"e19c":{"name":"Battery Alert"},"e1a3":{"name":"Battery Charging Full"},"e1a4":{"name":"Battery Full"},"e1a5":{"name":"Battery Std"},"e1a6":{"name":"Battery Unknown"},"eb3e":{"name":"Beach Access"},"e52d":{"name":"Beenhere"},"e14b":{"name":"Block"},"e1a7":{"name":"Bluetooth"},"e60f":{"name":"Bluetooth Audio"},"e1a8":{"name":"Bluetooth Connected"},"e1a9":{"name":"Bluetooth Disabled"},"e1aa":{"name":"Bluetooth Searching"},"e3a2":{"name":"Blur Circular"},"e3a3":{"name":"Blur Linear"},"e3a4":{"name":"Blur Off"},"e3a5":{"name":"Blur On"},"e865":{"name":"Book"},"e866":{"name":"Bookmark"},"e867":{"name":"Bookmark Border"},"e228":{"name":"Border All"},"e229":{"name":"Border Bottom"},"e22a":{"name":"Border Clear"},"e22b":{"name":"Border Color"},"e22c":{"name":"Border Horizontal"},"e22d":{"name":"Border Inner"},"e22e":{"name":"Border Left"},"e22f":{"name":"Border Outer"},"e230":{"name":"Border Right"},"e231":{"name":"Border Style"},"e232":{"name":"Border Top"},"e233":{"name":"Border Vertical"},"e06b":{"name":"Branding Watermark"},"e3a6":{"name":"Brightness 1"},"e3a7":{"name":"Brightness 2"},"e3a8":{"name":"Brightness 3"},"e3a9":{"name":"Brightness 4"},"e3aa":{"name":"Brightness 5"},"e3ab":{"name":"Brightness 6"},"e3ac":{"name":"Brightness 7"},"e1ab":{"name":"Brightness Auto"},"e1ac":{"name":"Brightness High"},"e1ad":{"name":"Brightness Low"},"e1ae":{"name":"Brightness Medium"},"e3ad":{"name":"Broken Image"},"e3ae":{"name":"Brush"},"e6dd":{"name":"Bubble Chart"},"e868":{"name":"Bug Report"},"e869":{"name":"Build"},"e43c":{"name":"Burst Mode"},"e0af":{"name":"Business"},"eb3f":{"name":"Business Center"},"e86a":{"name":"Cached"},"e7e9":{"name":"Cake"},"e0b0":{"name":"Call"},"e0b1":{"name":"Call End"},"e0b2":{"name":"Call Made"},"e0b3":{"name":"Call Merge"},"e0b4":{"name":"Call Missed"},"e0e4":{"name":"Call Missed Outgoing"},"e0b5":{"name":"Call Received"},"e0b6":{"name":"Call Split"},"e06c":{"name":"Call To Action"},"e3af":{"name":"Camera"},"e3b0":{"name":"Camera Alt"},"e8fc":{"name":"Camera Enhance"},"e3b1":{"name":"Camera Front"},"e3b2":{"name":"Camera Rear"},"e3b3":{"name":"Camera Roll"},"e5c9":{"name":"Cancel"},"e8f6":{"name":"Card Giftcard"},"e8f7":{"name":"Card Membership"},"e8f8":{"name":"Card Travel"},"eb40":{"name":"Casino"},"e307":{"name":"Cast"},"e308":{"name":"Cast Connected"},"e3b4":{"name":"Center Focus Strong"},"e3b5":{"name":"Center Focus Weak"},"e86b":{"name":"Change History"},"e0b7":{"name":"Chat"},"e0ca":{"name":"Chat Bubble"},"e0cb":{"name":"Chat Bubble Outline"},"e5ca":{"name":"Check"},"e834":{"name":"Check Box"},"e835":{"name":"Check Box Outline Blank"},"e86c":{"name":"Check Circle"},"e5cb":{"name":"Chevron Left"},"e5cc":{"name":"Chevron Right"},"eb41":{"name":"Child Care"},"eb42":{"name":"Child Friendly"},"e86d":{"name":"Chrome Reader Mode"},"e86e":{"name":"Class"},"e14c":{"name":"Clear"},"e0b8":{"name":"Clear All"},"e5cd":{"name":"Close"},"e01c":{"name":"Closed Caption"},"e2bd":{"name":"Cloud"},"e2be":{"name":"Cloud Circle"},"e2bf":{"name":"Cloud Done"},"e2c0":{"name":"Cloud Download"},"e2c1":{"name":"Cloud Off"},"e2c2":{"name":"Cloud Queue"},"e2c3":{"name":"Cloud Upload"},"e86f":{"name":"Code"},"e3b6":{"name":"Collections"},"e431":{"name":"Collections Bookmark"},"e3b7":{"name":"Color Lens"},"e3b8":{"name":"Colorize"},"e0b9":{"name":"Comment"},"e3b9":{"name":"Compare"},"e915":{"name":"Compare Arrows"},"e30a":{"name":"Computer"},"e638":{"name":"Confirmation Number"},"e0d0":{"name":"Contact Mail"},"e0cf":{"name":"Contact Phone"},"e0ba":{"name":"Contacts"},"e14d":{"name":"Content Copy"},"e14e":{"name":"Content Cut"},"e14f":{"name":"Content Paste"},"e3ba":{"name":"Control Point"},"e3bb":{"name":"Control Point Duplicate"},"e90c":{"name":"Copyright"},"e150":{"name":"Create"},"e2cc":{"name":"Create New Folder"},"e870":{"name":"Credit Card"},"e3be":{"name":"Crop"},"e3bc":{"name":"Crop 16 9"},"e3bd":{"name":"Crop 3 2"},"e3bf":{"name":"Crop 5 4"},"e3c0":{"name":"Crop 7 5"},"e3c1":{"name":"Crop Din"},"e3c2":{"name":"Crop Free"},"e3c3":{"name":"Crop Landscape"},"e3c4":{"name":"Crop Original"},"e3c5":{"name":"Crop Portrait"},"e437":{"name":"Crop Rotate"},"e3c6":{"name":"Crop Square"},"e871":{"name":"Dashboard"},"e1af":{"name":"Data Usage"},"e916":{"name":"Date Range"},"e3c7":{"name":"Dehaze"},"e872":{"name":"Delete"},"e92b":{"name":"Delete Forever"},"e16c":{"name":"Delete Sweep"},"e873":{"name":"Description"},"e30b":{"name":"Desktop Mac"},"e30c":{"name":"Desktop Windows"},"e3c8":{"name":"Details"},"e30d":{"name":"Developer Board"},"e1b0":{"name":"Developer Mode"},"e335":{"name":"Device Hub"},"e1b1":{"name":"Devices"},"e337":{"name":"Devices Other"},"e0bb":{"name":"Dialer Sip"},"e0bc":{"name":"Dialpad"},"e52e":{"name":"Directions"},"e52f":{"name":"Directions Bike"},"e532":{"name":"Directions Boat"},"e530":{"name":"Directions Bus"},"e531":{"name":"Directions Car"},"e534":{"name":"Directions Railway"},"e566":{"name":"Directions Run"},"e533":{"name":"Directions Subway"},"e535":{"name":"Directions Transit"},"e536":{"name":"Directions Walk"},"e610":{"name":"Disc Full"},"e875":{"name":"Dns"},"e612":{"name":"Do Not Disturb"},"e611":{"name":"Do Not Disturb Alt"},"e643":{"name":"Do Not Disturb Off"},"e644":{"name":"Do Not Disturb On"},"e30e":{"name":"Dock"},"e7ee":{"name":"Domain"},"e876":{"name":"Done"},"e877":{"name":"Done All"},"e917":{"name":"Donut Large"},"e918":{"name":"Donut Small"},"e151":{"name":"Drafts"},"e25d":{"name":"Drag Handle"},"e613":{"name":"Drive Eta"},"e1b2":{"name":"Dvr"},"e3c9":{"name":"Edit"},"e568":{"name":"Edit Location"},"e8fb":{"name":"Eject"},"e0be":{"name":"Email"},"e63f":{"name":"Enhanced Encryption"},"e01d":{"name":"Equalizer"},"e000":{"name":"Error"},"e001":{"name":"Error Outline"},"e926":{"name":"Euro Symbol"},"e56d":{"name":"Ev Station"},"e878":{"name":"Event"},"e614":{"name":"Event Available"},"e615":{"name":"Event Busy"},"e616":{"name":"Event Note"},"e903":{"name":"Event Seat"},"e879":{"name":"Exit To App"},"e5ce":{"name":"Expand Less"},"e5cf":{"name":"Expand More"},"e01e":{"name":"Explicit"},"e87a":{"name":"Explore"},"e3ca":{"name":"Exposure"},"e3cb":{"name":"Exposure Neg 1"},"e3cc":{"name":"Exposure Neg 2"},"e3cd":{"name":"Exposure Plus 1"},"e3ce":{"name":"Exposure Plus 2"},"e3cf":{"name":"Exposure Zero"},"e87b":{"name":"Extension"},"e87c":{"name":"Face"},"e01f":{"name":"Fast Forward"},"e020":{"name":"Fast Rewind"},"e87d":{"name":"Favorite"},"e87e":{"name":"Favorite Border"},"e06d":{"name":"Featured Play List"},"e06e":{"name":"Featured Video"},"e87f":{"name":"Feedback"},"e05d":{"name":"Fiber Dvr"},"e061":{"name":"Fiber Manual Record"},"e05e":{"name":"Fiber New"},"e06a":{"name":"Fiber Pin"},"e062":{"name":"Fiber Smart Record"},"e2c4":{"name":"File Download"},"e2c6":{"name":"File Upload"},"e3d3":{"name":"Filter"},"e3d0":{"name":"Filter 1"},"e3d1":{"name":"Filter 2"},"e3d2":{"name":"Filter 3"},"e3d4":{"name":"Filter 4"},"e3d5":{"name":"Filter 5"},"e3d6":{"name":"Filter 6"},"e3d7":{"name":"Filter 7"},"e3d8":{"name":"Filter 8"},"e3d9":{"name":"Filter 9"},"e3da":{"name":"Filter 9 Plus"},"e3db":{"name":"Filter B And W"},"e3dc":{"name":"Filter Center Focus"},"e3dd":{"name":"Filter Drama"},"e3de":{"name":"Filter Frames"},"e3df":{"name":"Filter Hdr"},"e152":{"name":"Filter List"},"e3e0":{"name":"Filter None"},"e3e2":{"name":"Filter Tilt Shift"},"e3e3":{"name":"Filter Vintage"},"e880":{"name":"Find In Page"},"e881":{"name":"Find Replace"},"e90d":{"name":"Fingerprint"},"e5dc":{"name":"First Page"},"eb43":{"name":"Fitness Center"},"e153":{"name":"Flag"},"e3e4":{"name":"Flare"},"e3e5":{"name":"Flash Auto"},"e3e6":{"name":"Flash Off"},"e3e7":{"name":"Flash On"},"e539":{"name":"Flight"},"e904":{"name":"Flight Land"},"e905":{"name":"Flight Takeoff"},"e3e8":{"name":"Flip"},"e882":{"name":"Flip To Back"},"e883":{"name":"Flip To Front"},"e2c7":{"name":"Folder"},"e2c8":{"name":"Folder Open"},"e2c9":{"name":"Folder Shared"},"e617":{"name":"Folder Special"},"e167":{"name":"Font Download"},"e234":{"name":"Format Align Center"},"e235":{"name":"Format Align Justify"},"e236":{"name":"Format Align Left"},"e237":{"name":"Format Align Right"},"e238":{"name":"Format Bold"},"e239":{"name":"Format Clear"},"e23a":{"name":"Format Color Fill"},"e23b":{"name":"Format Color Reset"},"e23c":{"name":"Format Color Text"},"e23d":{"name":"Format Indent Decrease"},"e23e":{"name":"Format Indent Increase"},"e23f":{"name":"Format Italic"},"e240":{"name":"Format Line Spacing"},"e241":{"name":"Format List Bulleted"},"e242":{"name":"Format List Numbered"},"e243":{"name":"Format Paint"},"e244":{"name":"Format Quote"},"e25e":{"name":"Format Shapes"},"e245":{"name":"Format Size"},"e246":{"name":"Format Strikethrough"},"e247":{"name":"Format Textdirection L To R"},"e248":{"name":"Format Textdirection R To L"},"e249":{"name":"Format Underlined"},"e0bf":{"name":"Forum"},"e154":{"name":"Forward"},"e056":{"name":"Forward 10"},"e057":{"name":"Forward 30"},"e058":{"name":"Forward 5"},"eb44":{"name":"Free Breakfast"},"e5d0":{"name":"Fullscreen"},"e5d1":{"name":"Fullscreen Exit"},"e24a":{"name":"Functions"},"e927":{"name":"G Translate"},"e30f":{"name":"Gamepad"},"e021":{"name":"Games"},"e90e":{"name":"Gavel"},"e155":{"name":"Gesture"},"e884":{"name":"Get App"},"e908":{"name":"Gif"},"eb45":{"name":"Golf Course"},"e1b3":{"name":"Gps Fixed"},"e1b4":{"name":"Gps Not Fixed"},"e1b5":{"name":"Gps Off"},"e885":{"name":"Grade"},"e3e9":{"name":"Gradient"},"e3ea":{"name":"Grain"},"e1b8":{"name":"Graphic Eq"},"e3eb":{"name":"Grid Off"},"e3ec":{"name":"Grid On"},"e7ef":{"name":"Group"},"e7f0":{"name":"Group Add"},"e886":{"name":"Group Work"},"e052":{"name":"Hd"},"e3ed":{"name":"Hdr Off"},"e3ee":{"name":"Hdr On"},"e3f1":{"name":"Hdr Strong"},"e3f2":{"name":"Hdr Weak"},"e310":{"name":"Headset"},"e311":{"name":"Headset Mic"},"e3f3":{"name":"Healing"},"e023":{"name":"Hearing"},"e887":{"name":"Help"},"e8fd":{"name":"Help Outline"},"e024":{"name":"High Quality"},"e25f":{"name":"Highlight"},"e888":{"name":"Highlight Off"},"e889":{"name":"History"},"e88a":{"name":"Home"},"eb46":{"name":"Hot Tub"},"e53a":{"name":"Hotel"},"e88b":{"name":"Hourglass Empty"},"e88c":{"name":"Hourglass Full"},"e902":{"name":"Http"},"e88d":{"name":"Https"},"e3f4":{"name":"Image"},"e3f5":{"name":"Image Aspect Ratio"},"e0e0":{"name":"Import Contacts"},"e0c3":{"name":"Import Export"},"e912":{"name":"Important Devices"},"e156":{"name":"Inbox"},"e909":{"name":"Indeterminate Check Box"},"e88e":{"name":"Info"},"e88f":{"name":"Info Outline"},"e890":{"name":"Input"},"e24b":{"name":"Insert Chart"},"e24c":{"name":"Insert Comment"},"e24d":{"name":"Insert Drive File"},"e24e":{"name":"Insert Emoticon"},"e24f":{"name":"Insert Invitation"},"e250":{"name":"Insert Link"},"e251":{"name":"Insert Photo"},"e891":{"name":"Invert Colors"},"e0c4":{"name":"Invert Colors Off"},"e3f6":{"name":"Iso"},"e312":{"name":"Keyboard"},"e313":{"name":"Keyboard Arrow Down"},"e314":{"name":"Keyboard Arrow Left"},"e315":{"name":"Keyboard Arrow Right"},"e316":{"name":"Keyboard Arrow Up"},"e317":{"name":"Keyboard Backspace"},"e318":{"name":"Keyboard Capslock"},"e31a":{"name":"Keyboard Hide"},"e31b":{"name":"Keyboard Return"},"e31c":{"name":"Keyboard Tab"},"e31d":{"name":"Keyboard Voice"},"eb47":{"name":"Kitchen"},"e892":{"name":"Label"},"e893":{"name":"Label Outline"},"e3f7":{"name":"Landscape"},"e894":{"name":"Language"},"e31e":{"name":"Laptop"},"e31f":{"name":"Laptop Chromebook"},"e320":{"name":"Laptop Mac"},"e321":{"name":"Laptop Windows"},"e5dd":{"name":"Last Page"},"e895":{"name":"Launch"},"e53b":{"name":"Layers"},"e53c":{"name":"Layers Clear"},"e3f8":{"name":"Leak Add"},"e3f9":{"name":"Leak Remove"},"e3fa":{"name":"Lens"},"e02e":{"name":"Library Add"},"e02f":{"name":"Library Books"},"e030":{"name":"Library Music"},"e90f":{"name":"Lightbulb Outline"},"e919":{"name":"Line Style"},"e91a":{"name":"Line Weight"},"e260":{"name":"Linear Scale"},"e157":{"name":"Link"},"e438":{"name":"Linked Camera"},"e896":{"name":"List"},"e0c6":{"name":"Live Help"},"e639":{"name":"Live Tv"},"e53f":{"name":"Local Activity"},"e53d":{"name":"Local Airport"},"e53e":{"name":"Local Atm"},"e540":{"name":"Local Bar"},"e541":{"name":"Local Cafe"},"e542":{"name":"Local Car Wash"},"e543":{"name":"Local Convenience Store"},"e556":{"name":"Local Dining"},"e544":{"name":"Local Drink"},"e545":{"name":"Local Florist"},"e546":{"name":"Local Gas Station"},"e547":{"name":"Local Grocery Store"},"e548":{"name":"Local Hospital"},"e549":{"name":"Local Hotel"},"e54a":{"name":"Local Laundry Service"},"e54b":{"name":"Local Library"},"e54c":{"name":"Local Mall"},"e54d":{"name":"Local Movies"},"e54e":{"name":"Local Offer"},"e54f":{"name":"Local Parking"},"e550":{"name":"Local Pharmacy"},"e551":{"name":"Local Phone"},"e552":{"name":"Local Pizza"},"e553":{"name":"Local Play"},"e554":{"name":"Local Post Office"},"e555":{"name":"Local Printshop"},"e557":{"name":"Local See"},"e558":{"name":"Local Shipping"},"e559":{"name":"Local Taxi"},"e7f1":{"name":"Location City"},"e1b6":{"name":"Location Disabled"},"e0c7":{"name":"Location Off"},"e0c8":{"name":"Location On"},"e1b7":{"name":"Location Searching"},"e897":{"name":"Lock"},"e898":{"name":"Lock Open"},"e899":{"name":"Lock Outline"},"e3fc":{"name":"Looks"},"e3fb":{"name":"Looks 3"},"e3fd":{"name":"Looks 4"},"e3fe":{"name":"Looks 5"},"e3ff":{"name":"Looks 6"},"e400":{"name":"Looks One"},"e401":{"name":"Looks Two"},"e028":{"name":"Loop"},"e402":{"name":"Loupe"},"e16d":{"name":"Low Priority"},"e89a":{"name":"Loyalty"},"e158":{"name":"Mail"},"e0e1":{"name":"Mail Outline"},"e55b":{"name":"Map"},"e159":{"name":"Markunread"},"e89b":{"name":"Markunread Mailbox"},"e322":{"name":"Memory"},"e5d2":{"name":"Menu"},"e252":{"name":"Merge Type"},"e0c9":{"name":"Message"},"e029":{"name":"Mic"},"e02a":{"name":"Mic None"},"e02b":{"name":"Mic Off"},"e618":{"name":"Mms"},"e253":{"name":"Mode Comment"},"e254":{"name":"Mode Edit"},"e263":{"name":"Monetization On"},"e25c":{"name":"Money Off"},"e403":{"name":"Monochrome Photos"},"e7f2":{"name":"Mood"},"e7f3":{"name":"Mood Bad"},"e619":{"name":"More"},"e5d3":{"name":"More Horiz"},"e5d4":{"name":"More Vert"},"e91b":{"name":"Motorcycle"},"e323":{"name":"Mouse"},"e168":{"name":"Move To Inbox"},"e02c":{"name":"Movie"},"e404":{"name":"Movie Creation"},"e43a":{"name":"Movie Filter"},"e6df":{"name":"Multiline Chart"},"e405":{"name":"Music Note"},"e063":{"name":"Music Video"},"e55c":{"name":"My Location"},"e406":{"name":"Nature"},"e407":{"name":"Nature People"},"e408":{"name":"Navigate Before"},"e409":{"name":"Navigate Next"},"e55d":{"name":"Navigation"},"e569":{"name":"Near Me"},"e1b9":{"name":"Network Cell"},"e640":{"name":"Network Check"},"e61a":{"name":"Network Locked"},"e1ba":{"name":"Network Wifi"},"e031":{"name":"New Releases"},"e16a":{"name":"Next Week"},"e1bb":{"name":"Nfc"},"e641":{"name":"No Encryption"},"e0cc":{"name":"No Sim"},"e033":{"name":"Not Interested"},"e06f":{"name":"Note"},"e89c":{"name":"Note Add"},"e7f4":{"name":"Notifications"},"e7f7":{"name":"Notifications Active"},"e7f5":{"name":"Notifications None"},"e7f6":{"name":"Notifications Off"},"e7f8":{"name":"Notifications Paused"},"e90a":{"name":"Offline Pin"},"e63a":{"name":"Ondemand Video"},"e91c":{"name":"Opacity"},"e89d":{"name":"Open In Browser"},"e89e":{"name":"Open In New"},"e89f":{"name":"Open With"},"e7f9":{"name":"Pages"},"e8a0":{"name":"Pageview"},"e40a":{"name":"Palette"},"e925":{"name":"Pan Tool"},"e40b":{"name":"Panorama"},"e40c":{"name":"Panorama Fish Eye"},"e40d":{"name":"Panorama Horizontal"},"e40e":{"name":"Panorama Vertical"},"e40f":{"name":"Panorama Wide Angle"},"e7fa":{"name":"Party Mode"},"e034":{"name":"Pause"},"e035":{"name":"Pause Circle Filled"},"e036":{"name":"Pause Circle Outline"},"e8a1":{"name":"Payment"},"e7fb":{"name":"People"},"e7fc":{"name":"People Outline"},"e8a2":{"name":"Perm Camera Mic"},"e8a3":{"name":"Perm Contact Calendar"},"e8a4":{"name":"Perm Data Setting"},"e8a5":{"name":"Perm Device Information"},"e8a6":{"name":"Perm Identity"},"e8a7":{"name":"Perm Media"},"e8a8":{"name":"Perm Phone Msg"},"e8a9":{"name":"Perm Scan Wifi"},"e7fd":{"name":"Person"},"e7fe":{"name":"Person Add"},"e7ff":{"name":"Person Outline"},"e55a":{"name":"Person Pin"},"e56a":{"name":"Person Pin Circle"},"e63b":{"name":"Personal Video"},"e91d":{"name":"Pets"},"e0cd":{"name":"Phone"},"e324":{"name":"Phone Android"},"e61b":{"name":"Phone Bluetooth Speaker"},"e61c":{"name":"Phone Forwarded"},"e61d":{"name":"Phone In Talk"},"e325":{"name":"Phone Iphone"},"e61e":{"name":"Phone Locked"},"e61f":{"name":"Phone Missed"},"e620":{"name":"Phone Paused"},"e326":{"name":"Phonelink"},"e0db":{"name":"Phonelink Erase"},"e0dc":{"name":"Phonelink Lock"},"e327":{"name":"Phonelink Off"},"e0dd":{"name":"Phonelink Ring"},"e0de":{"name":"Phonelink Setup"},"e410":{"name":"Photo"},"e411":{"name":"Photo Album"},"e412":{"name":"Photo Camera"},"e43b":{"name":"Photo Filter"},"e413":{"name":"Photo Library"},"e432":{"name":"Photo Size Select Actual"},"e433":{"name":"Photo Size Select Large"},"e434":{"name":"Photo Size Select Small"},"e415":{"name":"Picture As Pdf"},"e8aa":{"name":"Picture In Picture"},"e911":{"name":"Picture In Picture Alt"},"e6c4":{"name":"Pie Chart"},"e6c5":{"name":"Pie Chart Outlined"},"e55e":{"name":"Pin Drop"},"e55f":{"name":"Place"},"e037":{"name":"Play Arrow"},"e038":{"name":"Play Circle Filled"},"e039":{"name":"Play Circle Outline"},"e906":{"name":"Play For Work"},"e03b":{"name":"Playlist Add"},"e065":{"name":"Playlist Add Check"},"e05f":{"name":"Playlist Play"},"e800":{"name":"Plus One"},"e801":{"name":"Poll"},"e8ab":{"name":"Polymer"},"eb48":{"name":"Pool"},"e0ce":{"name":"Portable Wifi Off"},"e416":{"name":"Portrait"},"e63c":{"name":"Power"},"e336":{"name":"Power Input"},"e8ac":{"name":"Power Settings New"},"e91e":{"name":"Pregnant Woman"},"e0df":{"name":"Present To All"},"e8ad":{"name":"Print"},"e645":{"name":"Priority High"},"e80b":{"name":"Public"},"e255":{"name":"Publish"},"e8ae":{"name":"Query Builder"},"e8af":{"name":"Question Answer"},"e03c":{"name":"Queue"},"e03d":{"name":"Queue Music"},"e066":{"name":"Queue Play Next"},"e03e":{"name":"Radio"},"e837":{"name":"Radio Button Checked"},"e836":{"name":"Radio Button Unchecked"},"e560":{"name":"Rate Review"},"e8b0":{"name":"Receipt"},"e03f":{"name":"Recent Actors"},"e91f":{"name":"Record Voice Over"},"e8b1":{"name":"Redeem"},"e15a":{"name":"Redo"},"e5d5":{"name":"Refresh"},"e15b":{"name":"Remove"},"e15c":{"name":"Remove Circle"},"e15d":{"name":"Remove Circle Outline"},"e067":{"name":"Remove From Queue"},"e417":{"name":"Remove Red Eye"},"e928":{"name":"Remove Shopping Cart"},"e8fe":{"name":"Reorder"},"e040":{"name":"Repeat"},"e041":{"name":"Repeat One"},"e042":{"name":"Replay"},"e059":{"name":"Replay 10"},"e05a":{"name":"Replay 30"},"e05b":{"name":"Replay 5"},"e15e":{"name":"Reply"},"e15f":{"name":"Reply All"},"e160":{"name":"Report"},"e8b2":{"name":"Report Problem"},"e56c":{"name":"Restaurant"},"e561":{"name":"Restaurant Menu"},"e8b3":{"name":"Restore"},"e929":{"name":"Restore Page"},"e0d1":{"name":"Ring Volume"},"e8b4":{"name":"Room"},"eb49":{"name":"Room Service"},"e418":{"name":"Rotate 90 Degrees Ccw"},"e419":{"name":"Rotate Left"},"e41a":{"name":"Rotate Right"},"e920":{"name":"Rounded Corner"},"e328":{"name":"Router"},"e921":{"name":"Rowing"},"e0e5":{"name":"Rss Feed"},"e642":{"name":"Rv Hookup"},"e562":{"name":"Satellite"},"e161":{"name":"Save"},"e329":{"name":"Scanner"},"e8b5":{"name":"Schedule"},"e80c":{"name":"School"},"e1be":{"name":"Screen Lock Landscape"},"e1bf":{"name":"Screen Lock Portrait"},"e1c0":{"name":"Screen Lock Rotation"},"e1c1":{"name":"Screen Rotation"},"e0e2":{"name":"Screen Share"},"e623":{"name":"Sd Card"},"e1c2":{"name":"Sd Storage"},"e8b6":{"name":"Search"},"e32a":{"name":"Security"},"e162":{"name":"Select All"},"e163":{"name":"Send"},"e811":{"name":"Sentiment Dissatisfied"},"e812":{"name":"Sentiment Neutral"},"e813":{"name":"Sentiment Satisfied"},"e814":{"name":"Sentiment Very Dissatisfied"},"e815":{"name":"Sentiment Very Satisfied"},"e8b8":{"name":"Settings"},"e8b9":{"name":"Settings Applications"},"e8ba":{"name":"Settings Backup Restore"},"e8bb":{"name":"Settings Bluetooth"},"e8bd":{"name":"Settings Brightness"},"e8bc":{"name":"Settings Cell"},"e8be":{"name":"Settings Ethernet"},"e8bf":{"name":"Settings Input Antenna"},"e8c0":{"name":"Settings Input Component"},"e8c1":{"name":"Settings Input Composite"},"e8c2":{"name":"Settings Input Hdmi"},"e8c3":{"name":"Settings Input Svideo"},"e8c4":{"name":"Settings Overscan"},"e8c5":{"name":"Settings Phone"},"e8c6":{"name":"Settings Power"},"e8c7":{"name":"Settings Remote"},"e1c3":{"name":"Settings System Daydream"},"e8c8":{"name":"Settings Voice"},"e80d":{"name":"Share"},"e8c9":{"name":"Shop"},"e8ca":{"name":"Shop Two"},"e8cb":{"name":"Shopping Basket"},"e8cc":{"name":"Shopping Cart"},"e261":{"name":"Short Text"},"e6e1":{"name":"Show Chart"},"e043":{"name":"Shuffle"},"e1c8":{"name":"Signal Cellular 4 Bar"},"e1cd":{"name":"Signal Cellular Connected No Internet 4 Bar"},"e1ce":{"name":"Signal Cellular No Sim"},"e1cf":{"name":"Signal Cellular Null"},"e1d0":{"name":"Signal Cellular Off"},"e1d8":{"name":"Signal Wifi 4 Bar"},"e1d9":{"name":"Signal Wifi 4 Bar Lock"},"e1da":{"name":"Signal Wifi Off"},"e32b":{"name":"Sim Card"},"e624":{"name":"Sim Card Alert"},"e044":{"name":"Skip Next"},"e045":{"name":"Skip Previous"},"e41b":{"name":"Slideshow"},"e068":{"name":"Slow Motion Video"},"e32c":{"name":"Smartphone"},"eb4a":{"name":"Smoke Free"},"eb4b":{"name":"Smoking Rooms"},"e625":{"name":"Sms"},"e626":{"name":"Sms Failed"},"e046":{"name":"Snooze"},"e164":{"name":"Sort"},"e053":{"name":"Sort By Alpha"},"eb4c":{"name":"Spa"},"e256":{"name":"Space Bar"},"e32d":{"name":"Speaker"},"e32e":{"name":"Speaker Group"},"e8cd":{"name":"Speaker Notes"},"e92a":{"name":"Speaker Notes Off"},"e0d2":{"name":"Speaker Phone"},"e8ce":{"name":"Spellcheck"},"e838":{"name":"Star"},"e83a":{"name":"Star Border"},"e839":{"name":"Star Half"},"e8d0":{"name":"Stars"},"e0d3":{"name":"Stay Current Landscape"},"e0d4":{"name":"Stay Current Portrait"},"e0d5":{"name":"Stay Primary Landscape"},"e0d6":{"name":"Stay Primary Portrait"},"e047":{"name":"Stop"},"e0e3":{"name":"Stop Screen Share"},"e1db":{"name":"Storage"},"e8d1":{"name":"Store"},"e563":{"name":"Store Mall Directory"},"e41c":{"name":"Straighten"},"e56e":{"name":"Streetview"},"e257":{"name":"Strikethrough S"},"e41d":{"name":"Style"},"e5d9":{"name":"Subdirectory Arrow Left"},"e5da":{"name":"Subdirectory Arrow Right"},"e8d2":{"name":"Subject"},"e064":{"name":"Subscriptions"},"e048":{"name":"Subtitles"},"e56f":{"name":"Subway"},"e8d3":{"name":"Supervisor Account"},"e049":{"name":"Surround Sound"},"e0d7":{"name":"Swap Calls"},"e8d4":{"name":"Swap Horiz"},"e8d5":{"name":"Swap Vert"},"e8d6":{"name":"Swap Vertical Circle"},"e41e":{"name":"Switch Camera"},"e41f":{"name":"Switch Video"},"e627":{"name":"Sync"},"e628":{"name":"Sync Disabled"},"e629":{"name":"Sync Problem"},"e62a":{"name":"System Update"},"e8d7":{"name":"System Update Alt"},"e8d8":{"name":"Tab"},"e8d9":{"name":"Tab Unselected"},"e32f":{"name":"Tablet"},"e330":{"name":"Tablet Android"},"e331":{"name":"Tablet Mac"},"e420":{"name":"Tag Faces"},"e62b":{"name":"Tap And Play"},"e564":{"name":"Terrain"},"e262":{"name":"Text Fields"},"e165":{"name":"Text Format"},"e0d8":{"name":"Textsms"},"e421":{"name":"Texture"},"e8da":{"name":"Theaters"},"e8db":{"name":"Thumb Down"},"e8dc":{"name":"Thumb Up"},"e8dd":{"name":"Thumbs Up Down"},"e62c":{"name":"Time To Leave"},"e422":{"name":"Timelapse"},"e922":{"name":"Timeline"},"e425":{"name":"Timer"},"e423":{"name":"Timer 10"},"e424":{"name":"Timer 3"},"e426":{"name":"Timer Off"},"e264":{"name":"Title"},"e8de":{"name":"Toc"},"e8df":{"name":"Today"},"e8e0":{"name":"Toll"},"e427":{"name":"Tonality"},"e913":{"name":"Touch App"},"e332":{"name":"Toys"},"e8e1":{"name":"Track Changes"},"e565":{"name":"Traffic"},"e570":{"name":"Train"},"e571":{"name":"Tram"},"e572":{"name":"Transfer Within A Station"},"e428":{"name":"Transform"},"e8e2":{"name":"Translate"},"e8e3":{"name":"Trending Down"},"e8e4":{"name":"Trending Flat"},"e8e5":{"name":"Trending Up"},"e429":{"name":"Tune"},"e8e6":{"name":"Turned In"},"e8e7":{"name":"Turned In Not"},"e333":{"name":"Tv"},"e169":{"name":"Unarchive"},"e166":{"name":"Undo"},"e5d6":{"name":"Unfold Less"},"e5d7":{"name":"Unfold More"},"e923":{"name":"Update"},"e1e0":{"name":"Usb"},"e8e8":{"name":"Verified User"},"e258":{"name":"Vertical Align Bottom"},"e259":{"name":"Vertical Align Center"},"e25a":{"name":"Vertical Align Top"},"e62d":{"name":"Vibration"},"e070":{"name":"Video Call"},"e071":{"name":"Video Label"},"e04a":{"name":"Video Library"},"e04b":{"name":"Videocam"},"e04c":{"name":"Videocam Off"},"e338":{"name":"Videogame Asset"},"e8e9":{"name":"View Agenda"},"e8ea":{"name":"View Array"},"e8eb":{"name":"View Carousel"},"e8ec":{"name":"View Column"},"e42a":{"name":"View Comfy"},"e42b":{"name":"View Compact"},"e8ed":{"name":"View Day"},"e8ee":{"name":"View Headline"},"e8ef":{"name":"View List"},"e8f0":{"name":"View Module"},"e8f1":{"name":"View Quilt"},"e8f2":{"name":"View Stream"},"e8f3":{"name":"View Week"},"e435":{"name":"Vignette"},"e8f4":{"name":"Visibility"},"e8f5":{"name":"Visibility Off"},"e62e":{"name":"Voice Chat"},"e0d9":{"name":"Voicemail"},"e04d":{"name":"Volume Down"},"e04e":{"name":"Volume Mute"},"e04f":{"name":"Volume Off"},"e050":{"name":"Volume Up"},"e0da":{"name":"Vpn Key"},"e62f":{"name":"Vpn Lock"},"e1bc":{"name":"Wallpaper"},"e002":{"name":"Warning"},"e334":{"name":"Watch"},"e924":{"name":"Watch Later"},"e42c":{"name":"Wb Auto"},"e42d":{"name":"Wb Cloudy"},"e42e":{"name":"Wb Incandescent"},"e436":{"name":"Wb Iridescent"},"e430":{"name":"Wb Sunny"},"e63d":{"name":"Wc"},"e051":{"name":"Web"},"e069":{"name":"Web Asset"},"e16b":{"name":"Weekend"},"e80e":{"name":"Whatshot"},"e1bd":{"name":"Widgets"},"e63e":{"name":"Wifi"},"e1e1":{"name":"Wifi Lock"},"e1e2":{"name":"Wifi Tethering"},"e8f9":{"name":"Work"},"e25b":{"name":"Wrap Text"},"e8fa":{"name":"Youtube Searched For"},"e8ff":{"name":"Zoom In"},"e900":{"name":"Zoom Out"},"e56b":{"name":"Zoom Out Map"}}} --------------------------------------------------------------------------------