Commit 3923fd96 by jasl

first commit

parents
# This file is for unifying the coding style for different editors and IDEs
# editorconfig.org
root = true
[*]
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 2
end_of_line = lf
[*.md]
trim_trailing_whitespace = true
*.rb diff=ruby
*.gemspec diff=ruby
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global'
# Ignore bundler config.
.bundle/
# Ignore the default SQLite database.
test/dummy/db/*.sqlite3
test/dummy/db/*.sqlite3-journal
# Ignore all logfiles and tempfiles.
log/*.log
!test/dummy/log/.keep
!test/dummy/tmp/.keep
test/dummy/log/*.log
test/dummy/tmp/
# Ignore uploaded files in development
test/dummy/storage/*
!test/dummy/storage/.keep
pkg/
.byebug_history
node_modules/
test/dummy/public/packs
test/dummy/node_modules/
yarn-error.log
*.gem
inherit_gem:
rubocop-rails_config:
- config/rails.yml
AllCops:
TargetRubyVersion: 2.4
Exclude:
- 'test/dummy/db/schema.rb'
# frozen_string_literal: true
Style/FrozenStringLiteralComment:
Enabled: true
EnforcedStyle: when_needed
# Prefer &&/|| over and/or.
Style/AndOr:
Enabled: true
# Do not use braces for hash literals when they are the last argument of a
# method call.
Style/BracesAroundHashParameters:
Enabled: true
# Align `when` with `case`.
Layout/CaseIndentation:
Enabled: true
# Align comments with method definitions.
Layout/CommentIndentation:
Enabled: true
# No extra empty lines.
Layout/EmptyLines:
Enabled: true
# In a regular class definition, no empty lines around the body.
Layout/EmptyLinesAroundClassBody:
Enabled: true
# In a regular method definition, no empty lines around the body.
Layout/EmptyLinesAroundMethodBody:
Enabled: true
# In a regular module definition, no empty lines around the body.
Layout/EmptyLinesAroundModuleBody:
Enabled: true
# Use Ruby >= 1.9 syntax for hashes. Prefer {a: :b} over { :a => :b }.
Style/HashSyntax:
Enabled: true
# Method definitions after `private` or `protected` isolated calls need one
# extra level of indentation.
Layout/IndentationConsistency:
Enabled: true
EnforcedStyle: normal
# Two spaces, no tabs (for indentation).
Layout/IndentationWidth:
Enabled: true
Layout/SpaceAfterColon:
Enabled: true
Layout/SpaceAfterComma:
Enabled: true
Layout/SpaceAroundEqualsInParameterDefault:
Enabled: true
Layout/SpaceAroundKeyword:
Enabled: true
Layout/SpaceAroundOperators:
Enabled: true
Layout/SpaceBeforeFirstArg:
Enabled: true
# Defining a method with parameters needs parentheses.
Style/MethodDefParentheses:
Enabled: true
# Use `foo {}` not `foo{}`.
Layout/SpaceBeforeBlockBraces:
Enabled: true
# Use `foo { bar }` not `foo {bar}`.
Layout/SpaceInsideBlockBraces:
Enabled: true
# Use `{a: 1}` not `{ a:1 }`.
Layout/SpaceInsideHashLiteralBraces:
Enabled: false
Layout/SpaceInsideParens:
Enabled: true
# Check quotes usage according to lint rule below.
Style/StringLiterals:
Enabled: true
EnforcedStyle: double_quotes
# Detect hard tabs, no hard tabs.
Layout/Tab:
Enabled: true
# Blank lines should not have any spaces.
Layout/TrailingBlankLines:
Enabled: true
# No trailing whitespace.
Layout/TrailingWhitespace:
Enabled: true
# Use quotes for string literals when they are enough.
Style/UnneededPercentQ:
Enabled: true
# Align `end` with the matching keyword or starting expression except for
# assignments, where it should be aligned with the LHS.
Layout/EndAlignment:
Enabled: true
EnforcedStyleAlignWith: variable
# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
Lint/RequireParentheses:
Enabled: true
ruby-2.5.1
# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Declare your gem's dependencies in workflow_core.gemspec.
# Bundler will treat runtime dependencies like base dependencies, and
# development dependencies will be added by default to the :development group.
gemspec
# Declare any dependencies that are still in development here instead of in
# your gemspec. These might include edge Rails or gems from your path or
# Git. Remember to move these dependencies to your gemspec before releasing
# your gem to rubygems.org.
# To use a debugger
# gem 'byebug', group: [:development, :test]
gem "sqlite3"
# Use Puma as the app server
gem "puma"
# For better console experience
gem "pry-rails"
# Access an IRB console on exception pages or by using <%= console %> anywhere in the code.
gem "web-console"
gem "listen", ">= 3.0.5", "< 3.2"
# Call "byebug" anywhere in the code to stop execution and get a debugger console
gem "pry-byebug"
gem "better_errors"
gem "binding_of_caller"
# To support ES6
gem "sprockets", "~> 4.0.0.beta4"
# Support ES6
gem "babel-transpiler"
# Use SCSS for stylesheets
gem "sass-rails"
# Use Uglifier as compressor for JavaScript assets
gem "uglifier", ">= 1.3.0"
gem "jquery-rails"
gem "turbolinks"
gem "selectize-rails"
gem "bulma-rails"
gem "rubocop"
gem "rubocop-rails_config"
gem "form_core"
gem "duck_record"
gem "closure_tree"
gem "cocoon"
gem "script_core", github: "rails-engine/script_core", submodules: true
gem "graphviz"
GIT
remote: https://github.com/rails-engine/script_core.git
revision: f086b63c03061732c8a760b97d54a9d2e7cbec68
submodules: true
specs:
script_core (0.0.4)
msgpack (~> 1.0)
rake-compiler (~> 1.0)
PATH
remote: .
specs:
workflow_core (0.0.1)
rails (~> 5.2)
GEM
remote: https://rubygems.org/
specs:
actioncable (5.2.1)
actionpack (= 5.2.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailer (5.2.1)
actionpack (= 5.2.1)
actionview (= 5.2.1)
activejob (= 5.2.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.2.1)
actionview (= 5.2.1)
activesupport (= 5.2.1)
rack (~> 2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.2.1)
activesupport (= 5.2.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
activejob (5.2.1)
activesupport (= 5.2.1)
globalid (>= 0.3.6)
activemodel (5.2.1)
activesupport (= 5.2.1)
activerecord (5.2.1)
activemodel (= 5.2.1)
activesupport (= 5.2.1)
arel (>= 9.0)
activestorage (5.2.1)
actionpack (= 5.2.1)
activerecord (= 5.2.1)
marcel (~> 0.3.1)
activesupport (5.2.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
arel (9.0.0)
ast (2.4.0)
babel-source (5.8.35)
babel-transpiler (0.7.0)
babel-source (>= 4.0, < 6)
execjs (~> 2.0)
better_errors (2.5.0)
coderay (>= 1.0.0)
erubi (>= 1.0.0)
rack (>= 0.9.0)
bindex (0.5.0)
binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1)
builder (3.2.3)
bulma-rails (0.7.1)
sass (~> 3.2)
byebug (10.0.2)
closure_tree (7.0.0)
activerecord (>= 4.2.10)
with_advisory_lock (>= 4.0.0)
cocoon (1.2.11)
coderay (1.1.2)
concurrent-ruby (1.0.5)
crass (1.0.4)
debug_inspector (0.0.3)
duck_record (0.0.26)
activemodel (~> 5.0)
activesupport (~> 5.0)
erubi (1.7.1)
execjs (2.7.0)
ffi (1.9.25)
form_core (0.0.14)
duck_record (~> 0)
rails (~> 5.0)
globalid (0.4.1)
activesupport (>= 4.2.0)
graphviz (1.1.0)
process-pipeline
i18n (1.1.0)
concurrent-ruby (~> 1.0)
jaro_winkler (1.5.1)
jquery-rails (4.3.3)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
loofah (2.2.2)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.0)
mini_mime (>= 0.1.1)
marcel (0.3.3)
mimemagic (~> 0.3.2)
method_source (0.9.0)
mimemagic (0.3.2)
mini_mime (1.0.1)
mini_portile2 (2.3.0)
minitest (5.11.3)
msgpack (1.2.4)
nio4r (2.3.1)
nokogiri (1.8.4)
mini_portile2 (~> 2.3.0)
parallel (1.12.1)
parser (2.5.1.2)
ast (~> 2.4.0)
powerpack (0.1.2)
process-group (1.1.0)
process-pipeline (1.0.1)
process-group
pry (0.11.3)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
pry-byebug (3.6.0)
byebug (~> 10.0)
pry (~> 0.10)
pry-rails (0.3.6)
pry (>= 0.10.4)
puma (3.12.0)
rack (2.0.5)
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (5.2.1)
actioncable (= 5.2.1)
actionmailer (= 5.2.1)
actionpack (= 5.2.1)
actionview (= 5.2.1)
activejob (= 5.2.1)
activemodel (= 5.2.1)
activerecord (= 5.2.1)
activestorage (= 5.2.1)
activesupport (= 5.2.1)
bundler (>= 1.3.0)
railties (= 5.2.1)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.0.4)
loofah (~> 2.2, >= 2.2.2)
railties (5.2.1)
actionpack (= 5.2.1)
activesupport (= 5.2.1)
method_source
rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0)
rainbow (3.0.0)
rake (12.3.1)
rake-compiler (1.0.5)
rake
rb-fsevent (0.10.3)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
rubocop (0.59.2)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
parser (>= 2.5, != 2.5.1.1)
powerpack (~> 0.1)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
rubocop-rails_config (0.2.3)
railties (>= 3.0)
rubocop (~> 0.56)
ruby-progressbar (1.10.0)
ruby_dep (1.5.0)
sass (3.6.0)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sass-rails (5.0.7)
railties (>= 4.0.0, < 6)
sass (~> 3.1)
sprockets (>= 2.8, < 4.0)
sprockets-rails (>= 2.0, < 4.0)
tilt (>= 1.1, < 3)
selectize-rails (0.12.5)
sprockets (4.0.0.beta8)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.1)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sqlite3 (1.3.13)
thor (0.20.0)
thread_safe (0.3.6)
tilt (2.0.8)
turbolinks (5.2.0)
turbolinks-source (~> 5.2)
turbolinks-source (5.2.0)
tzinfo (1.2.5)
thread_safe (~> 0.1)
uglifier (4.1.19)
execjs (>= 0.3.0, < 3)
unicode-display_width (1.4.0)
web-console (3.7.0)
actionview (>= 5.0)
activemodel (>= 5.0)
bindex (>= 0.4.0)
railties (>= 5.0)
websocket-driver (0.7.0)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3)
with_advisory_lock (4.0.0)
activerecord (>= 4.2)
PLATFORMS
ruby
DEPENDENCIES
babel-transpiler
better_errors
binding_of_caller
bulma-rails
closure_tree
cocoon
duck_record
form_core
graphviz
jquery-rails
listen (>= 3.0.5, < 3.2)
pry-byebug
pry-rails
puma
rubocop
rubocop-rails_config
sass-rails
script_core!
selectize-rails
sprockets (~> 4.0.0.beta4)
sqlite3
turbolinks
uglifier (>= 1.3.0)
web-console
workflow_core!
BUNDLED WITH
1.16.4
Copyright 2018 Jun Jiang
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Workflow Core
====
> WorkflowCore is under development, the codebase is unoptimized and has many bad practices, I may do breaking changes even force pushing to master branch.
>
> In short, it's not ready yet, but I realize that a workflow engine is complicated to design and it needs a long term to done well, so I decide to open source at early stage.
>
> Any way, feedbacks and suggestions are highly welcome!
A Rails engine which providing essential infrastructure of workflow.
It's based on [Workflow Net](http://mlwiki.org/index.php/Workflow_Nets) technique.
The gem provides:
- Models to describe workflow nets
- Models to describe workflow instances
- Interfaces to define transitions
## Why “core”
Because it's not aim to "out-of-box", some gem like Devise giving developer an out-of-box experience, that's awesome, but on the other hand, it also introducing a very complex abstraction that may hard to understanding how it works, especially when you attempting to customize it.
I believe that the gem is tightly coupled with features that face to end users directly, so having a good customizability and easy to understanding are of the most concern, so I just wanna give you a domain framework that you can build your own that just fitting your need, and you shall have fully control and without any unnecessary abstraction.
BTW, the dummy app is a full-featured app with production level codebase that you can freely to reference it.
## Todo
- Make sure transit must be an atomic operation, and help developers to avoiding Rails' nested transaction pitfalls.
- Consider consequences of changing the Net, well handle Net changes. e.g: what about running instances?
- Stabilizing interfaces.
- Evaluate that can supporting async, scheduled and event-based transition properly.
- Efficiency (especially database queries).
- Easy to use.
- Transforming to graph representation for visualization and other usages (e.g proving [Soundness](http://mlwiki.org/index.php/Workflow_Soundness)).
- Polish codebase.
- Continually improving dummy app.
## Requirements
- MRI 2.3+
- Rails 5.0+
## Usage
See demo for now.
## Installation
Add this line to your Gemfile:
```ruby
gem 'workflow_core'
```
Or you may want to include the gem directly from GitHub:
```ruby
gem 'workflow_core', github: 'rails-engine/workflow_core'
```
And then execute:
```sh
$ bundle
```
Copy migrations
```sh
$ bin/rails workflow_core:install:migrations
```
Then do migrate
```sh
$ bin/rails db:migrate
```
## Demo
**Demo is also under development.**
The dummy app integrates with [Form Core](https://github.com/rails-engine/form_core) and [Script Core](https://github.com/rails-engine/script_core) shows an Approving Manage System.
![](_assets/dummy_overview.png)
### Features
#### Importing workflow definitions from a BPMN2 xml,
![](_assets/importing_bpmn.png)
Because there isn't have a easy-to-use web-based flowchart designer, I implement a stupid BPMN2 importer, it have many restrictions:
- Only supports `Sequence`, `Start event`, `End event`, `Parallel gateway` and `Exclusive gateway`
- Using gateway to fork flows must have corresponding join (or merge) gateway
- Only read `name` property, other such as `condition expression` must configure on the dummy app
You can check `_samples` folder, I've already provided some samples, or you can try a BPMN2 designer (e.g [Camunda modeler](https://github.com/camunda/camunda-modeler)).
#### Defining form
![](_assets/defining_form.png)
You can defining a dynamic form for a workflow.
In transition's options, you can configure field's accessibility
#### Exclusive choice configuration supports Ruby expression
![](_assets/editing_transition.png)
Exclusive choice is a special transition that needs to configure conditions that determine how to transit to a branch.
The condition is a Ruby expression, and running in a mRuby sandbox (powered by ScriptCore but it's also undone yet), and you can access form data through `@input[:payload]`, for example, there is a field named `approved`, we can check the field checked by `@input[:payload]["approved"]`
#### Run a workflow
See `Instance` tab, that should make sense.
### Usage
**You need install Graphviz first**
Clone the repository.
```sh
$ git clone https://github.com/rails-engine/workflow_core.git
```
Change directory
```sh
$ cd workflow_core
```
Run bundler
```sh
$ bundle install
```
Preparing database
```sh
$ bin/rails db:migrate
```
Start the Rails server
```sh
$ bin/rails s
```
Open your browser, and visit `http://localhost:3000`
## Contributing
Bug report or pull request are welcome.
### Make a pull request
1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request
Please write unit test with your code if necessary.
## License
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
# frozen_string_literal: true
begin
require "bundler/setup"
rescue LoadError
puts "You must `gem install bundler` and `bundle install` to run rake tasks"
end
require "rdoc/task"
RDoc::Task.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = "rdoc"
rdoc.title = "WorkflowCore"
rdoc.options << "--line-numbers"
rdoc.rdoc_files.include("README.md")
rdoc.rdoc_files.include("lib/**/*.rb")
end
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
load "rails/tasks/engine.rake"
load "rails/tasks/statistics.rake"
require "bundler/gem_tasks"
require "rake/testtask"
Rake::TestTask.new(:test) do |t|
t.libs << "test"
t.pattern = "test/**/*_test.rb"
t.verbose = false
end
task default: :test
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" id="Definitions_1ao30nf" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="2.0.1">
<bpmn:process id="Process_1" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0z87m50</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:task id="Task_1jpb9fm" name="Step 1">
<bpmn:incoming>SequenceFlow_0z87m50</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1ph0afh</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_0z87m50" sourceRef="StartEvent_1" targetRef="Task_1jpb9fm" />
<bpmn:task id="Task_0rnfiox" name="Step 2">
<bpmn:incoming>SequenceFlow_1ph0afh</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1h0vowt</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_1ph0afh" sourceRef="Task_1jpb9fm" targetRef="Task_0rnfiox" />
<bpmn:endEvent id="EndEvent_0d80vbz">
<bpmn:incoming>SequenceFlow_1h0vowt</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_1h0vowt" sourceRef="Task_0rnfiox" targetRef="EndEvent_0d80vbz" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="173" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_1jpb9fm_di" bpmnElement="Task_1jpb9fm">
<dc:Bounds x="259" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0z87m50_di" bpmnElement="SequenceFlow_0z87m50">
<di:waypoint x="209" y="120" />
<di:waypoint x="259" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Task_0rnfiox_di" bpmnElement="Task_0rnfiox">
<dc:Bounds x="409" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1ph0afh_di" bpmnElement="SequenceFlow_1ph0afh">
<di:waypoint x="359" y="120" />
<di:waypoint x="409" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="EndEvent_0d80vbz_di" bpmnElement="EndEvent_0d80vbz">
<dc:Bounds x="559" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1h0vowt_di" bpmnElement="SequenceFlow_1h0vowt">
<di:waypoint x="509" y="120" />
<di:waypoint x="559" y="120" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" id="Definitions_12bthnl" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="2.0.3">
<bpmn:process id="Process_1" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0ihulyl</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:task id="Task_0f5ie7w" name="Step 1">
<bpmn:incoming>SequenceFlow_0ihulyl</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1wljg2h</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_0ihulyl" sourceRef="StartEvent_1" targetRef="Task_0f5ie7w" />
<bpmn:sequenceFlow id="SequenceFlow_1wljg2h" sourceRef="Task_0f5ie7w" targetRef="ExclusiveGateway_01sqwfm" />
<bpmn:parallelGateway id="ExclusiveGateway_01sqwfm" name="Parallel Split">
<bpmn:incoming>SequenceFlow_1wljg2h</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_190bkby</bpmn:outgoing>
<bpmn:outgoing>SequenceFlow_03lhyba</bpmn:outgoing>
</bpmn:parallelGateway>
<bpmn:task id="Task_0ac7qx7" name="Step 2a">
<bpmn:incoming>SequenceFlow_190bkby</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_047f1j5</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_190bkby" sourceRef="ExclusiveGateway_01sqwfm" targetRef="Task_0ac7qx7" />
<bpmn:task id="Task_0ym8z2m" name="Step 2b">
<bpmn:incoming>SequenceFlow_03lhyba</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0rl07dr</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_03lhyba" sourceRef="ExclusiveGateway_01sqwfm" targetRef="Task_0ym8z2m" />
<bpmn:sequenceFlow id="SequenceFlow_047f1j5" sourceRef="Task_0ac7qx7" targetRef="ExclusiveGateway_1ni4fs4" />
<bpmn:parallelGateway id="ExclusiveGateway_1ni4fs4" name="Synchronization">
<bpmn:incoming>SequenceFlow_047f1j5</bpmn:incoming>
<bpmn:incoming>SequenceFlow_0rl07dr</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1a0tblh</bpmn:outgoing>
</bpmn:parallelGateway>
<bpmn:sequenceFlow id="SequenceFlow_0rl07dr" sourceRef="Task_0ym8z2m" targetRef="ExclusiveGateway_1ni4fs4" />
<bpmn:task id="Task_0h6xqca" name="Step 3">
<bpmn:incoming>SequenceFlow_1a0tblh</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_18vdzfl</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_1a0tblh" sourceRef="ExclusiveGateway_1ni4fs4" targetRef="Task_0h6xqca" />
<bpmn:endEvent id="EndEvent_0lta3qi">
<bpmn:incoming>SequenceFlow_18vdzfl</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_18vdzfl" sourceRef="Task_0h6xqca" targetRef="EndEvent_0lta3qi" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="173" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_0f5ie7w_di" bpmnElement="Task_0f5ie7w">
<dc:Bounds x="259" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0ihulyl_di" bpmnElement="SequenceFlow_0ihulyl">
<di:waypoint x="209" y="120" />
<di:waypoint x="259" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1wljg2h_di" bpmnElement="SequenceFlow_1wljg2h">
<di:waypoint x="359" y="120" />
<di:waypoint x="409" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="ParallelGateway_0ukzmc6_di" bpmnElement="ExclusiveGateway_01sqwfm">
<dc:Bounds x="409" y="95" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="484" y="110" width="62" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_0ac7qx7_di" bpmnElement="Task_0ac7qx7">
<dc:Bounds x="509" y="-19" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_190bkby_di" bpmnElement="SequenceFlow_190bkby">
<di:waypoint x="434" y="95" />
<di:waypoint x="434" y="21" />
<di:waypoint x="509" y="21" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Task_0ym8z2m_di" bpmnElement="Task_0ym8z2m">
<dc:Bounds x="509" y="190" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_03lhyba_di" bpmnElement="SequenceFlow_03lhyba">
<di:waypoint x="434" y="145" />
<di:waypoint x="434" y="230" />
<di:waypoint x="509" y="230" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_047f1j5_di" bpmnElement="SequenceFlow_047f1j5">
<di:waypoint x="609" y="21" />
<di:waypoint x="685" y="21" />
<di:waypoint x="685" y="95" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="ParallelGateway_14q60vy_di" bpmnElement="ExclusiveGateway_1ni4fs4">
<dc:Bounds x="660" y="95" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="566" y="110" width="79" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0rl07dr_di" bpmnElement="SequenceFlow_0rl07dr">
<di:waypoint x="609" y="230" />
<di:waypoint x="685" y="230" />
<di:waypoint x="685" y="145" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Task_0h6xqca_di" bpmnElement="Task_0h6xqca">
<dc:Bounds x="761" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1a0tblh_di" bpmnElement="SequenceFlow_1a0tblh">
<di:waypoint x="710" y="120" />
<di:waypoint x="761" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="EndEvent_0lta3qi_di" bpmnElement="EndEvent_0lta3qi">
<dc:Bounds x="912" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_18vdzfl_di" bpmnElement="SequenceFlow_18vdzfl">
<di:waypoint x="861" y="120" />
<di:waypoint x="912" y="120" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" id="Definitions_0w9rj1f" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="2.0.3">
<bpmn:process id="Process_1" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0qzo3lb</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:task id="Task_07nyace" name="Step 1">
<bpmn:incoming>SequenceFlow_0qzo3lb</bpmn:incoming>
<bpmn:incoming>SequenceFlow_1fr8c9x</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0v54qlb</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_0qzo3lb" sourceRef="StartEvent_1" targetRef="Task_07nyace" />
<bpmn:exclusiveGateway id="ExclusiveGateway_1jpd3ln">
<bpmn:incoming>SequenceFlow_0v54qlb</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1s8t845</bpmn:outgoing>
<bpmn:outgoing>SequenceFlow_1fr8c9x</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="SequenceFlow_0v54qlb" sourceRef="Task_07nyace" targetRef="ExclusiveGateway_1jpd3ln" />
<bpmn:task id="Task_0ym21fx" name="Step 2">
<bpmn:incoming>SequenceFlow_1s8t845</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0nzmb8s</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_1s8t845" sourceRef="ExclusiveGateway_1jpd3ln" targetRef="Task_0ym21fx" />
<bpmn:endEvent id="EndEvent_1jlbp8k">
<bpmn:incoming>SequenceFlow_0nzmb8s</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_0nzmb8s" sourceRef="Task_0ym21fx" targetRef="EndEvent_1jlbp8k" />
<bpmn:sequenceFlow id="SequenceFlow_1fr8c9x" sourceRef="ExclusiveGateway_1jpd3ln" targetRef="Task_07nyace" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="173" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_07nyace_di" bpmnElement="Task_07nyace">
<dc:Bounds x="259" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0qzo3lb_di" bpmnElement="SequenceFlow_0qzo3lb">
<di:waypoint x="209" y="120" />
<di:waypoint x="259" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="ExclusiveGateway_1jpd3ln_di" bpmnElement="ExclusiveGateway_1jpd3ln" isMarkerVisible="true">
<dc:Bounds x="409" y="95" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0v54qlb_di" bpmnElement="SequenceFlow_0v54qlb">
<di:waypoint x="359" y="120" />
<di:waypoint x="409" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Task_0ym21fx_di" bpmnElement="Task_0ym21fx">
<dc:Bounds x="509" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1s8t845_di" bpmnElement="SequenceFlow_1s8t845">
<di:waypoint x="459" y="120" />
<di:waypoint x="509" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="EndEvent_1jlbp8k_di" bpmnElement="EndEvent_1jlbp8k">
<dc:Bounds x="659" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0nzmb8s_di" bpmnElement="SequenceFlow_0nzmb8s">
<di:waypoint x="609" y="120" />
<di:waypoint x="659" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1fr8c9x_di" bpmnElement="SequenceFlow_1fr8c9x">
<di:waypoint x="434" y="120" />
<di:waypoint x="434" y="30" />
<di:waypoint x="309" y="30" />
<di:waypoint x="309" y="80" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
# frozen_string_literal: true
module WorkflowCore
class ApplicationJob < ActiveJob::Base
end
end
# frozen_string_literal: true
module WorkflowCore
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
end
# frozen_string_literal: true
module WorkflowCore
class Place < ApplicationRecord
self.table_name = "workflow_places"
belongs_to :workflow
belongs_to :input_transition, optional: true, foreign_key: "input_transition_id",
class_name: "WorkflowCore::Transition"
belongs_to :output_transition, optional: true, foreign_key: "output_transition_id",
class_name: "WorkflowCore::Transition"
has_many :tokens
end
end
# frozen_string_literal: true
module WorkflowCore
class Token < ApplicationRecord
self.table_name = "workflow_tokens"
belongs_to :instance,
class_name: "WorkflowCore::WorkflowInstance"
belongs_to :workflow
belongs_to :place
belongs_to :previous, optional: true,
class_name: "WorkflowCore::Token"
enum status: {
processing: 0,
completed: 1,
failed: 2,
unexpected: 3,
terminated: 4
}
end
end
# frozen_string_literal: true
module WorkflowCore
class Transition < ApplicationRecord
self.table_name = "workflow_transitions"
belongs_to :workflow
has_many :input_places, dependent: :nullify,
foreign_key: "output_transition_id", class_name: "WorkflowCore::Place"
has_many :output_places, dependent: :destroy,
foreign_key: "input_transition_id", class_name: "WorkflowCore::Place"
def fire(_token)
raise NotImplementedError
end
end
end
# frozen_string_literal: true
module WorkflowCore
class Workflow < ApplicationRecord
self.table_name = "workflows"
has_one :start_place, class_name: "WorkflowCore::Place", dependent: :destroy
has_many :transitions, class_name: "WorkflowCore::Transition", dependent: :destroy
has_many :places, class_name: "WorkflowCore::Place", dependent: :destroy
has_many :instances, class_name: "WorkflowCore::WorkflowInstance", dependent: :destroy
has_many :tokens, class_name: "WorkflowCore::Token", dependent: :destroy
end
end
# frozen_string_literal: true
module WorkflowCore
class WorkflowInstance < ApplicationRecord
self.table_name = "workflow_instances"
belongs_to :workflow
has_many :tokens, foreign_key: "instance_id", dependent: :destroy
enum status: {
processing: 0,
completed: 1,
failed: 2,
unexpected: 3,
terminated: 4
}
serialize :payload
end
end
#!/usr/bin/env ruby
# frozen_string_literal: true
# This command will automatically be run when you run "rails" with Rails gems
# installed from the root of your application.
ENGINE_ROOT = File.expand_path("..", __dir__)
ENGINE_PATH = File.expand_path("../lib/workflow_core/engine", __dir__)
APP_PATH = File.expand_path("../test/dummy/config/application", __dir__)
# Set up gems listed in the Gemfile.
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
require "rails/all"
require "rails/engine/commands"
# frozen_string_literal: true
class CreateWorkflows < ActiveRecord::Migration[5.2]
def change
create_table :workflows do |t|
t.string :type, null: false
t.timestamps
end
end
end
# frozen_string_literal: true
class CreateWorkflowPlaces < ActiveRecord::Migration[5.2]
def change
create_table :workflow_places do |t|
t.references :input_transition, foreign_key: {to_table: "workflow_transitions"}
t.references :output_transition, foreign_key: {to_table: "workflow_transitions"}
t.string :type, null: false
t.references :workflow, foreign_key: true
t.timestamps
end
end
end
# frozen_string_literal: true
class CreateWorkflowTransitions < ActiveRecord::Migration[5.2]
def change
create_table :workflow_transitions do |t|
t.string :type, null: false
t.references :workflow, foreign_key: true
t.timestamps
end
end
end
# frozen_string_literal: true
class CreateWorkflowInstances < ActiveRecord::Migration[5.2]
def change
create_table :workflow_instances do |t|
t.text :payload
t.integer :status, null: false, default: 0
t.references :workflow, foreign_key: true
t.timestamps
end
end
end
# frozen_string_literal: true
class CreateWorkflowTokens < ActiveRecord::Migration[5.2]
def change
create_table :workflow_tokens do |t|
t.integer :status, null: false, default: 0
t.references :place, foreign_key: {to_table: "workflow_places"}
t.references :previous, foreign_key: {to_table: "workflow_tokens"}
t.references :instance, foreign_key: {to_table: "workflow_instances"}
t.references :workflow, foreign_key: true
t.timestamps
end
end
end
# frozen_string_literal: true
# desc "Explaining what the task does"
# task :workflow_core do
# # Task goes here
# end
# frozen_string_literal: true
require "workflow_core/engine"
module WorkflowCore
# Your code goes here...
end
# frozen_string_literal: true
module WorkflowCore
class Engine < ::Rails::Engine
isolate_namespace WorkflowCore
end
end
# frozen_string_literal: true
module WorkflowCore
VERSION = "0.0.1"
end
ruby-2.5.1
\ No newline at end of file
# frozen_string_literal: true
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require_relative "config/application"
Rails.application.load_tasks
//= link_tree ../images
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css
//= require rails-ujs
//= require turbolinks
//= require jquery3
//= require selectize
//= require cocoon
// Cheat form https://github.com/jgthms/bulma/blob/master/docs/_javascript/main.js
document.addEventListener('DOMContentLoaded', () => {
// Dropdowns
const $dropdowns = getAll('.dropdown:not(.is-hoverable)');
if ($dropdowns.length > 0) {
$dropdowns.forEach($el => {
$el.addEventListener('click', event => {
event.stopPropagation();
$el.classList.toggle('is-active');
});
});
document.addEventListener('click', event => {
closeDropdowns();
});
}
function closeDropdowns() {
$dropdowns.forEach($el => {
$el.classList.remove('is-active');
});
}
// Functions
function getAll(selector) {
return Array.prototype.slice.call(document.querySelectorAll(selector), 0);
}
// Utils
function removeFromArray(array, value) {
if (array.includes(value)) {
const value_index = array.indexOf(value);
array.splice(value_index, 1);
}
return array;
}
Array.prototype.diff = function (a) {
return this.filter(function (i) {
return a.indexOf(i) < 0;
});
};
});
@import "selectize";
@import "bulma";
body {
display: flex;
min-height: 100vh;
flex-direction: column;
& nav.nav {
a.brand {
padding: 0.5rem 0.75em 0.5em 0;
}
}
& .main {
flex: 1;
}
& footer.footer {
padding: 3rem 1.5rem 3rem;
}
}
.flash {
overflow: hidden;
top: 0;
left: 0;
right: 0;
line-height: 2.5;
background: $info;
color: $text-invert;
text-align: center;
}
#alert {
background: $danger;
}
.nested_form_field {
.collection {
.nested_form {
border: 1px solid $green;
padding: 0.5em;
margin-left: 0.75em;
margin-bottom: 0.75rem;
}
}
}
.content {
.nested-content {
margin-left: 0.75em;
margin-bottom: 1em;
}
}
.message-body.content {
> :first-child {
margin-top: 0;
}
}
.field {
flex: 1;
}
# frozen_string_literal: true
class ApplicationController < ActionController::Base
helper_method :current_user
private
def current_user
@_current_user ||=
if session[:current_user_id].present?
User.where(id: session[:current_user_id]).first
else
nil
end
end
def require_signed_in
unless current_user
redirect_to users_url
end
end
end
# frozen_string_literal: true
class GroupsController < ApplicationController
before_action :set_group, only: [:show, :edit, :update, :destroy]
# GET /groups
def index
# https://ruby-china.org/topics/32802
@groups = Group.includes(:parent)
end
# GET /groups/new
def new
@group = Group.new
end
# GET /groups/1/edit
def edit
end
# POST /groups
def create
@group = Group.new(group_params)
if @group.save
redirect_to groups_url, notice: "Group was successfully created."
else
render :new
end
end
# PATCH/PUT /groups/1
def update
if @group.update(group_params)
redirect_to groups_url, notice: "Group was successfully updated."
else
render :edit
end
end
# DELETE /groups/1
def destroy
@group.destroy
redirect_to groups_url, notice: "Group was successfully destroyed."
end
private
# Use callbacks to share common setup or constraints between actions.
def set_group
@group = Group.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def group_params
params.require(:group).permit(:name, :parent_id)
end
end
# frozen_string_literal: true
class SessionsController < ApplicationController
def create
session[:current_user_id] = params[:user_id]
redirect_back fallback_location: root_url
end
def destroy
session[:current_user_id] = nil
redirect_to root_url
end
end
# frozen_string_literal: true
class UsersController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy]
# GET /users
def index
@users = User.all.includes(:group)
end
# GET /users/new
def new
@user = User.new
end
# GET /users/1/edit
def edit
end
# POST /users
def create
@user = User.new(user_params)
if @user.save
redirect_to users_url, notice: "User was successfully created."
else
render :new
end
end
# PATCH/PUT /users/1
def update
if @user.update(user_params)
redirect_to users_url, notice: "User was successfully updated."
else
render :edit
end
end
# DELETE /users/1
def destroy
@user.destroy
redirect_to users_url, notice: "User was successfully destroyed."
end
private
# Use callbacks to share common setup or constraints between actions.
def set_user
@user = User.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def user_params
params.require(:user).permit(:name, :group_id)
end
end
# frozen_string_literal: true
class Workflows::ApplicationController < ApplicationController
layout "workflows"
before_action :set_workflow
protected
# Use callbacks to share common setup or constraints between actions.
def set_workflow
@workflow = Workflow.find(params[:workflow_id])
end
end
# frozen_string_literal: true
module Workflows
class Fields::ApplicationController < ApplicationController
before_action :set_form
before_action :set_field
protected
# Use callbacks to share common setup or constraints between actions.
def set_form
@form = @workflow.form
end
def set_field
@field = @form.fields.find(params[:field_id])
end
end
end
# frozen_string_literal: true
module Workflows
class Fields::OptionsController < Fields::ApplicationController
before_action :set_options
def edit
end
def update
@options.assign_attributes(options_params)
if @options.valid? && @field.save(validate: false)
redirect_to workflow_fields_url(@workflow), notice: "Field was successfully updated."
else
render :edit
end
end
private
def set_options
@options = @field.options
end
def options_params
params.fetch(:options, {}).permit!
end
end
end
# frozen_string_literal: true
module Workflows
class Fields::ValidationsController < Fields::ApplicationController
before_action :set_validations
def edit
end
def update
@validations.assign_attributes(validations_params)
if @validations.valid? && @field.save(validate: false)
redirect_to workflow_fields_url(@workflow), notice: "Field was successfully updated."
else
render :edit
end
end
private
def set_validations
@validations = @field.validations
end
def validations_params
params.fetch(:validations, {}).permit!
end
end
end
# frozen_string_literal: true
class Workflows::FieldsController < Workflows::ApplicationController
before_action :set_form
before_action :set_field, only: %i[show edit update destroy]
# GET /workflows/1/fields
def index
@fields = @form.fields.all
end
# GET /workflows/fields/new
def new
@field = @form.fields.build
end
# GET /workflows/1/fields/1/edit
def edit
end
# POST /workflows/1/fields
def create
@field = @form.fields.build(field_params)
if @field.save
redirect_to workflow_fields_url(@workflow), notice: "Field was successfully created."
else
render :new
end
end
# PATCH/PUT /workflows/1/fields/1
def update
if @field.update(field_params)
redirect_to workflow_fields_url(@workflow), notice: "Field was successfully updated."
else
render :edit
end
end
# DELETE /workflows/1/fields/1
def destroy
@field.destroy
redirect_to workflow_fields_url(@workflow), notice: "Field was successfully destroyed."
end
private
# Use callbacks to share common setup or constraints between actions.
def set_form
@form = @workflow.form
end
def set_field
@field = @form.fields.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def field_params
params.fetch(:field, {}).permit(:name, :label, :hint, :type)
end
end
# frozen_string_literal: true
module Workflows
class Instances::ApplicationController < ApplicationController
layout "workflow_instances"
before_action :set_instance
protected
# Use callbacks to share common setup or constraints between actions.
def set_instance
@instance = @workflow.instances.find(params[:instance_id])
end
end
end
# frozen_string_literal: true
module Workflows
class Instances::TokensController < Instances::ApplicationController
before_action :set_token, only: %i[show fire]
before_action :set_form_model, only: %i[show fire]
# GET /workflows/1/tokens
def index
@tokens = @instance.tokens.includes(place: :output_transition)
end
# POST /workflows/1/tokens/1/fire
def show
@form_record = @virtual_model.load(@instance.payload)
end
# POST /workflows/1/tokens/1/fire
def fire
@form_record = @virtual_model.load(@instance.payload)
@form_record.assign_attributes(form_record_params)
if @form_record.valid?
@instance.update! payload: (@instance.payload || {}).merge(@form_record.serializable_hash)
@token.place.output_transition.fire(@token)
redirect_to workflow_instance_tokens_url(@workflow, @instance), notice: "Token was successfully fired."
else
render :show
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_token
@token = @instance.tokens.find(params[:id] || params[:token_id])
end
def set_form_model
@form = @workflow.form
overrides = @token.place.output_transition.options.field_overrides.map { |o| {o.name => {accessibility: o.accessibility}} }.reduce(&:merge)
@virtual_model = @form.to_virtual_model overrides: overrides
end
def form_record_params
params.fetch(:form_record, {}).permit!
end
end
end
# frozen_string_literal: true
class Workflows::InstancesController < Workflows::ApplicationController
before_action :set_instance, only: %i[show]
# GET /workflows/1/instances
def index
@instances = @workflow.instances.all
end
# GET /workflows/1/instances
def show
@form = @workflow.form
@virtual_model = @form.to_virtual_model
@form_record = @virtual_model.load(@instance.payload)
render layout: "workflow_instances"
end
# POST /workflows/1/instances
def create
@workflow.instances.create! type: "WorkflowInstance"
redirect_to workflow_instances_url(@workflow), notice: "instance was successfully created."
end
private
# Use callbacks to share common setup or constraints between actions.
def set_instance
@instance = @workflow.instances.find(params[:id])
end
end
# frozen_string_literal: true
class Workflows::LoadsController < Workflows::ApplicationController
# GET /workflows/1/load
def show
end
# PATCH/PUT /workflows/1/load
def update
bpmn_xml = permitted_params[:bpmn_xml].present? ? permitted_params[:bpmn_xml] : permitted_params[:bpmn_file]&.read
@workflow.load_from_bpmn!(bpmn_xml)
redirect_to workflow_url(@workflow), notice: "Workflow definition was successfully imported."
end
private
# Only allow a trusted parameter "white list" through.
def permitted_params
params.permit(:bpmn_xml, :bpmn_file)
end
end
# frozen_string_literal: true
module Workflows
class Transitions::ApplicationController < ApplicationController
before_action :set_transition
protected
# Use callbacks to share common setup or constraints between actions.
def set_transition
@transition = @workflow.transitions.find(params[:transition_id])
end
end
end
# frozen_string_literal: true
module Workflows
class Transitions::OptionsController < Transitions::ApplicationController
before_action :set_options
def edit
end
def update
@options.assign_attributes(options_params)
if @options.valid? && @transition.save(validate: false)
redirect_to workflow_transitions_url(@workflow), notice: "Transition was successfully updated."
else
render :edit
end
end
private
def set_options
@options = @transition.options
end
def options_params
params.fetch(:options, {}).permit!
end
end
end
# frozen_string_literal: true
class Workflows::TransitionsController < Workflows::ApplicationController
before_action :set_transition, only: %i[edit update destroy]
# GET /workflows/1/transitions
def index
@transitions = @workflow.transitions.all
end
# GET /workflows/transitions/new
def new
@transition = @workflow.transitions.build
end
# GET /workflows/1/transitions/1/edit
def edit
end
# POST /workflows/1/transitions
def create
@transition = @workflow.transitions.build(transition_params)
if @transition.save
redirect_to workflow_transitions_url(@workflow), notice: "transition was successfully created."
else
render :new
end
end
# PATCH/PUT /workflows/1/transitions/1
def update
if @transition.update(transition_params)
redirect_to workflow_transitions_url(@workflow), notice: "transition was successfully updated."
else
render :edit
end
end
# DELETE /workflows/1/transitions/1
def destroy
@transition.destroy
redirect_to workflow_transitions_url(@workflow), notice: "transition was successfully destroyed."
end
private
# Use callbacks to share common setup or constraints between actions.
def set_transition
@transition = @workflow.transitions.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def transition_params
params.fetch(:transition, {}).permit(:name, :type)
end
end
# frozen_string_literal: true
class WorkflowsController < ApplicationController
layout "application"
before_action :set_workflow, only: %i[show edit update destroy]
# GET /workflows
def index
@workflows = Workflow.all
end
# GET /workflows/new
def new
@workflow = Workflow.new
end
# GET /workflows/1
def show
render layout: "workflows"
end
# GET /workflows/1/edit
def edit
end
# POST /workflows
def create
@workflow = Workflow.new(workflow_params)
if @workflow.save
redirect_to workflow_url(@workflow), notice: "workflow was successfully created."
else
render :new
end
end
# PATCH/PUT /workflows/1
def update
if @workflow.update(workflow_params)
redirect_to workflow_url(@workflow), notice: "workflow was successfully updated."
else
render :edit
end
end
# DELETE /workflows/1
def destroy
@workflow.destroy
redirect_to workflows_url, notice: "workflow was successfully destroyed."
end
private
# Use callbacks to share common setup or constraints between actions.
def set_workflow
@workflow = Workflow.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def workflow_params
params.fetch(:workflow, {}).permit(:name, :description)
end
end
# frozen_string_literal: true
module ApplicationHelper
def options_for_enum_select(klass, attribute, selected = nil)
container = klass.public_send(attribute.to_s.pluralize).map do |k, v|
v ||= k
[klass.human_enum_value(attribute, k), v]
end
options_for_select(container, selected)
end
def present(model, options = {})
klass = options.delete(:presenter_class) || "#{model.class}Presenter".constantize
presenter = klass.new(model, self, options)
yield(presenter) if block_given?
presenter
end
end
# frozen_string_literal: true
module FieldsHelper
def options_for_field_types(selected: nil)
options_for_select(Field.descendants.map { |klass| [klass.model_name.human, klass.to_s] }, selected)
end
def field_label(form, field_name:)
field_name = field_name.to_s.split(".").first.to_sym
form.fields.select do |field|
field.name == field_name
end.first&.label
end
end
# frozen_string_literal: true
module TransitionsHelper
def options_for_transition_types(selected: nil)
options_for_select(Transition.descendants.map { |klass| [klass.model_name.human, klass.to_s] }, selected)
end
end
# frozen_string_literal: true
class ApplicationJob < ActiveJob::Base
end
# frozen_string_literal: true
module Bpmn
class DefinitionContainer
attr_reader :collection
def initialize(tokens)
@collection = tokens.map { |t| {t.id => t} }.reduce(&:merge!)
@start_id = tokens.first { |t| t.node_type == :start_event }.id
end
def start_event
@collection[@start_id]
end
def [](id)
@collection[id]
end
def slice(*ids)
@collection.slice(*ids).values
end
def self.parse(bpmn_xml)
tokens = Bpmn::Tokenizer.new.tokenize(bpmn_xml)
return nil unless tokens
new tokens
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokenizer
def tokenize(bpmn_xml)
return nil unless bpmn_xml.present?
doc = Nokogiri::XML(bpmn_xml) rescue nil
return nil unless doc && doc.errors.count.zero?
process = doc.at_xpath("//bpmn:process") rescue nil
return nil unless process
return unless process.present?
process.elements.map { |el| factory(el) }.compact
end
private
def factory(element)
case element.name
when "startEvent"
Bpmn::Tokens::StartEvent.new(element)
when "endEvent"
Bpmn::Tokens::EndEvent.new(element)
when "task"
Bpmn::Tokens::Task.new(element)
when "userTask"
Bpmn::Tokens::UserTask.new(element)
when "scriptTask"
Bpmn::Tokens::ScriptTask.new(element)
when "parallelGateway"
Bpmn::Tokens::ParallelGateway.new(element)
when "exclusiveGateway"
Bpmn::Tokens::ExclusiveGateway.new(element)
when "inclusiveGateway"
Bpmn::Tokens::InclusiveGateway.new(element)
when "sequenceFlow"
Bpmn::Tokens::SequenceFlow.new(element)
else
raise NotImplementedError.new("#{element.name} is unsupported yet.")
end
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::CommonToken < Bpmn::Tokens::Token
attr_reader :incoming_ids, :outgoing_ids
def initialize(element)
super
@incoming_ids = element.xpath("bpmn:incoming").map(&:content)
@outgoing_ids = element.xpath("bpmn:outgoing").map(&:content)
end
def to_hash
super.merge(
incoming_ids: incoming_ids,
outgoing_ids: outgoing_ids
)
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::EndEvent < Bpmn::Tokens::CommonToken
def event?
true
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::ExclusiveGateway < Bpmn::Tokens::CommonToken
attr_reader :default_flow_id
def initialize(element)
super
@default_flow_id = element["default"]
end
def gateway?
true
end
def to_hash
super.merge(
default_flow_id: default_flow_id
)
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::Extensions::ConditionExpression
attr_reader :language, :condition, :type
def initialize(element)
@language = element["language"]
@condition = element.content
@type = element["xsi:type"]
end
def self.factory(xelement)
ce = xelement.at_xpath("bpmn:conditionExpression")
return nil unless ce
new(ce)
end
def to_hash
{
language: language,
condition: condition,
type: type,
}
end
alias_method :to_h, :to_hash
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::InclusiveGateway < Bpmn::Tokens::CommonToken
attr_reader :default_flow_id
def initialize(element)
super
@default_flow_id = element["default"]
end
def gateway?
true
end
def to_hash
super.merge(
default_flow_id: default_flow_id
)
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::ParallelGateway < Bpmn::Tokens::CommonToken
def gateway?
true
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::ScriptTask < Bpmn::Tokens::CommonToken
attr_reader :script_format, :script
def initialize(element)
super
@script_format = element["scriptFormat"]
@script = element.at_xpath("bpmn:script").try(:content)
end
def task?
true
end
def to_hash
super.merge(
extensions: {
script_format: script_format,
script: script
}
)
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::SequenceFlow < Bpmn::Tokens::Token
attr_reader :source_id, :target_id
attr_reader :condition_expression
def initialize(element)
super
@source_id = element["sourceRef"]
@target_id = element["targetRef"]
@condition_expression = Bpmn::Tokens::Extensions::ConditionExpression.factory(element)
end
def flow?
true
end
def to_hash
super.merge(
source_id: source_id,
target_id: target_id,
extensions: {
condition_expression: condition_expression.to_hash
}
)
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::StartEvent < Bpmn::Tokens::CommonToken
def event?
true
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::Task < Bpmn::Tokens::CommonToken
def task?
true
end
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::Token
attr_reader :id, :name, :node_type
attr_reader :documentation
def initialize(element)
# @element = element
@id = element["id"]
@name = element["name"] || ""
@node_type = element.name.underscore.to_sym
@documentation = element.at_xpath("bpmn:documentation")&.content || ""
end
def gateway?
false
end
def event?
false
end
def task?
false
end
def flow?
false
end
def to_hash
{
node_id: id,
node_type: node_type,
name: name
}
end
alias_method :to_h, :to_hash
end
end
# frozen_string_literal: true
module Bpmn
class Tokens::UserTask < Bpmn::Tokens::CommonToken
def task?
true
end
end
end
# frozen_string_literal: true
class ApplicationMailer < ActionMailer::Base
default from: "from@example.com"
layout "mailer"
end
# frozen_string_literal: true
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
include ActsAsDefaultValue
include EnumAttributeLocalizable
end
# frozen_string_literal: true
# https://github.com/FooBarWidget/default_value_for
#
# Copyright (c) 2008-2012 Phusion
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
module ActsAsDefaultValue
extend ActiveSupport::Concern
class NormalValueContainer
def initialize(value)
@value = value
end
def evaluate(_instance)
if @value.duplicable?
@value.dup
else
@value
end
end
end
class BlockValueContainer
def initialize(block)
@block = block
end
def evaluate(instance)
if @block.arity == 0
@block.call
else
@block.call(instance)
end
end
end
included do
after_initialize :set_default_values
end
def initialize(attributes = nil)
@initialization_attributes = attributes.is_a?(Hash) ? attributes.stringify_keys : {}
super
end
def set_default_values
self.class._all_default_attribute_values.each do |attribute, container|
next unless new_record? || self.class._all_default_attribute_values_not_allowing_nil.include?(attribute)
connection_default_value_defined = new_record? && respond_to?("#{attribute}_changed?") && !send("#{attribute}_changed?")
column = self.class.columns.detect { |c| c.name == attribute }
attribute_blank =
if column && column.type == :boolean
send(attribute).nil?
else
send(attribute).blank?
end
next unless connection_default_value_defined || attribute_blank
# allow explicitly setting nil through allow nil option
next if @initialization_attributes.is_a?(Hash) &&
(
@initialization_attributes.has_key?(attribute) ||
(
@initialization_attributes.has_key?("#{attribute}_attributes") &&
nested_attributes_options.stringify_keys[attribute]
)
) &&
!self.class._all_default_attribute_values_not_allowing_nil.include?(attribute)
send("#{attribute}=", container.evaluate(self))
clear_attribute_changes [attribute] if has_attribute?(attribute)
end
end
def attributes_for_create(attribute_names)
attribute_names += self.class._all_default_attribute_values.keys.map(&:to_s).find_all do |name|
self.class.columns_hash.key?(name)
end
super
end
module ClassMethods
def _default_attribute_values # :nodoc:
@default_attribute_values ||= {}
end
def _default_attribute_values_not_allowing_nil # :nodoc:
@default_attribute_values_not_allowing_nil ||= Set.new
end
def _all_default_attribute_values # :nodoc:
if superclass.respond_to?(:_default_attribute_values)
superclass._all_default_attribute_values.merge(_default_attribute_values)
else
_default_attribute_values
end
end
def _all_default_attribute_values_not_allowing_nil # :nodoc:
if superclass.respond_to?(:_default_attribute_values_not_allowing_nil)
superclass._all_default_attribute_values_not_allowing_nil + _default_attribute_values_not_allowing_nil
else
_default_attribute_values_not_allowing_nil
end
end
# Declares a default value for the given attribute.
#
# Sets the default value to the given options parameter
#
# The <tt>options</tt> can be used to specify the following things:
# * <tt>allow_nil (default: true)</tt> - Sets explicitly passed nil values if option is set to true.
def default_value_for(attribute, value, **options)
allow_nil = options.fetch(:allow_nil, true)
container =
if value.is_a? Proc
BlockValueContainer.new(value)
else
NormalValueContainer.new(value)
end
_default_attribute_values[attribute.to_s] = container
_default_attribute_values_not_allowing_nil << attribute.to_s unless allow_nil
attribute
end
end
end
# frozen_string_literal: true
module EnumAttributeLocalizable
extend ActiveSupport::Concern
module ClassMethods
def human_enum_value(attribute, value, options = {})
parts = attribute.to_s.split(".")
attribute = parts.pop.pluralize
attributes_scope = "#{i18n_scope}.attributes"
if parts.any?
namespace = parts.join("/")
defaults = lookup_ancestors.map do |klass|
:"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}.#{value}"
end
defaults << :"#{attributes_scope}.#{namespace}.#{attribute}.#{value}"
else
defaults = lookup_ancestors.map do |klass|
:"#{attributes_scope}.#{klass.model_name.i18n_key}.#{attribute}.#{value}"
end
end
defaults << :"attributes.#{attribute}.#{value}"
defaults << options.delete(:default) if options[:default]
defaults << value.to_s.humanize
options[:default] = defaults
I18n.translate(defaults.shift, options)
end
end
end
# frozen_string_literal: true
module Concerns::Fields
module Validations::Acceptance
extend ActiveSupport::Concern
included do
attribute :acceptance, :boolean, default: false
end
def interpret_to(model, field_name, _accessibility, _options = {})
super
return unless acceptance
model.validates field_name, acceptance: true
end
end
end
# frozen_string_literal: true
module Concerns::Fields
module Validations::Confirmation
extend ActiveSupport::Concern
included do
attribute :confirmation, :boolean, default: false
end
def interpret_to(model, field_name, _accessibility, _options = {})
super
return unless confirmation
model.validates field_name, confirmation: true
end
end
end
# frozen_string_literal: true
module Concerns::Fields
module Validations::Exclusion
extend ActiveSupport::Concern
included do
embeds_one :exclusion, class_name: "Concerns::Fields::Validations::Exclusion::ExclusionOptions"
accepts_nested_attributes_for :exclusion
after_initialize do
build_exclusion unless exclusion
end
end
def interpret_to(model, field_name, accessibility, options = {})
super
exclusion&.interpret_to model, field_name, accessibility, options
end
class ExclusionOptions < FieldOptions
attribute :message, :string, default: ""
attribute :in, :string, default: [], array: true
def interpret_to(model, field_name, _accessibility, _options = {})
return if self.in.empty?
options = {in: self.in}
options[:message] = message if message.present?
model.validates field_name, exclusion: options, allow_blank: true
end
end
end
end
# frozen_string_literal: true
module Concerns::Fields
module Validations::Format
extend ActiveSupport::Concern
included do
embeds_one :format, class_name: "Concerns::Fields::Validations::Format::FormatOptions"
accepts_nested_attributes_for :format
after_initialize do
build_format unless format
end
end
def interpret_to(model, field_name, accessibility, options = {})
super
format&.interpret_to model, field_name, accessibility, options
end
class FormatOptions < FieldOptions
attribute :with, :string, default: ""
attribute :message, :string, default: ""
validate do
begin
Regexp.new(with) if with.present?
rescue RegexpError
errors.add :with, :invalid
end
end
def interpret_to(model, field_name, _accessibility, _options = {})
return if with.blank?
with = Regexp.new(self.with)
options = {with: with}
options[:message] = message if message.present?
model.validates field_name, format: options, allow_blank: true
end
end
end
end
# frozen_string_literal: true
module Concerns::Fields
module Validations::Inclusion
extend ActiveSupport::Concern
included do
embeds_one :inclusion, class_name: "Concerns::Fields::Validations::Inclusion::InclusionOptions"
accepts_nested_attributes_for :inclusion
after_initialize do
build_inclusion unless inclusion
end
end
def interpret_to(model, field_name, accessibility, options = {})
super
inclusion&.interpret_to model, field_name, accessibility, options
end
class InclusionOptions < FieldOptions
attribute :message, :string, default: ""
attribute :in, :string, default: [], array: true
def interpret_to(model, field_name, _accessibility, _options = {})
return if self.in.empty?
options = {in: self.in}
options[:message] = message if message.present?
model.validates field_name, inclusion: options, allow_blank: true
end
end
end
end
# frozen_string_literal: true
module Concerns::Fields
module Validations::Length
extend ActiveSupport::Concern
included do
embeds_one :length, class_name: "Concerns::Fields::Validations::Length::LengthOptions"
accepts_nested_attributes_for :length
after_initialize do
build_length unless length
end
end
def interpret_to(model, field_name, accessibility, options = {})
super
length&.interpret_to model, field_name, accessibility, options
end
class LengthOptions < FieldOptions
attribute :minimum, :integer, default: 0
attribute :maximum, :integer, default: 0
attribute :is, :integer, default: 0
validates :minimum, :maximum, :is,
numericality: {
greater_than_or_equal_to: 0
}
validates :maximum,
numericality: {
greater_than: :minimum
},
if: proc { |record| record.maximum <= record.minimum && record.maximum.positive? }
validates :is,
numericality: {
equal_to: 0
},
if: proc { |record| !record.maximum.zero? || !record.minimum.zero? }
def interpret_to(model, field_name, _accessibility, _options = {})
return if self.minimum.zero? && self.maximum.zero? && self.is.zero?
if is.positive?
model.validates field_name, length: {is: is}, allow_blank: true
return
end
options = {}
options[:minimum] = minimum if minimum.positive?
options[:maximum] = maximum if maximum.positive?
return if options.empty?
model.validates field_name, length: options, allow_blank: true
end
end
end
end
# frozen_string_literal: true
module Concerns::Fields
module Validations::Numericality
extend ActiveSupport::Concern
included do
embeds_one :numericality, class_name: "Concerns::Fields::Validations::Numericality::NumericalityOptions"
accepts_nested_attributes_for :numericality
after_initialize do
build_numericality unless numericality
end
end
def interpret_to(model, field_name, accessibility, options = {})
super
numericality&.interpret_to model, field_name, accessibility, options
end
class NumericalityOptions < FieldOptions
attribute :lower_bound_check, :string, default: "disabled"
attribute :upper_bound_check, :string, default: "disabled"
attribute :lower_bound_value, :float, default: 0.0
attribute :upper_bound_value, :float, default: 0.0
enum lower_bound_check: {
disabled: "disabled",
greater_than: "greater_than",
greater_than_or_equal_to: "greater_than_or_equal_to"
}, _prefix: :lower_bound_check
enum upper_bound_check: {
disabled: "disabled",
less_than: "less_than",
less_than_or_equal_to: "less_than_or_equal_to"
}, _prefix: :upper_bound_check
validates :upper_bound_value,
numericality: {
greater_than: :lower_bound_value
},
if: proc { upper_bound_check != "disabled" && lower_bound_check != "disabled" }
def interpret_to(model, field_name, _accessibility, _options = {})
options = {}
options[lower_bound_check] = lower_bound_value unless lower_bound_check_disabled?
options[upper_bound_check] = upper_bound_value unless upper_bound_check_disabled?
return if options.empty?
options.symbolize_keys!
model.validates field_name, numericality: options, allow_blank: true
end
end
end
end
# frozen_string_literal: true
module Concerns::Fields
module Validations::Presence
extend ActiveSupport::Concern
included do
attribute :presence, :boolean, default: false
end
def interpret_to(model, field_name, _accessibility, _options = {})
super
return unless presence
model.validates field_name, presence: true
end
end
end
# frozen_string_literal: true
# https://github.com/FooBarWidget/default_value_for
#
# Copyright (c) 2008-2012 Phusion
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
module FormCore
module ActsAsDefaultValue
extend ActiveSupport::Concern
class NormalValueContainer
def initialize(value)
@value = value
end
def evaluate(_instance)
if @value.duplicable?
@value.dup
else
@value
end
end
end
class BlockValueContainer
def initialize(block)
@block = block
end
def evaluate(instance)
if @block.arity == 0
@block.call
else
@block.call(instance)
end
end
end
included do
after_initialize :set_default_values
end
def initialize(attributes = nil)
@initialization_attributes = attributes.is_a?(Hash) ? attributes.stringify_keys : {}
super
end
def set_default_values
self.class._all_default_attribute_values.each do |attribute, container|
next unless new_record? || self.class._all_default_attribute_values_not_allowing_nil.include?(attribute)
attribute_blank =
if self.class.attribute_types[attribute]&.type == :boolean
send(attribute).nil?
else
send(attribute).blank?
end
next unless attribute_blank
# allow explicitly setting nil through allow nil option
next if @initialization_attributes.is_a?(Hash) &&
(
@initialization_attributes.has_key?(attribute) ||
(
@initialization_attributes.has_key?("#{attribute}_attributes") &&
nested_attributes_options.stringify_keys[attribute]
)
) &&
!self.class._all_default_attribute_values_not_allowing_nil.include?(attribute)
send("#{attribute}=", container.evaluate(self))
clear_attribute_changes [attribute] if has_attribute?(attribute)
end
end
module ClassMethods
def _default_attribute_values # :nodoc:
@default_attribute_values ||= {}
end
def _default_attribute_values_not_allowing_nil # :nodoc:
@default_attribute_values_not_allowing_nil ||= Set.new
end
def _all_default_attribute_values # :nodoc:
if superclass.respond_to?(:_default_attribute_values)
superclass._all_default_attribute_values.merge(_default_attribute_values)
else
_default_attribute_values
end
end
def _all_default_attribute_values_not_allowing_nil # :nodoc:
if superclass.respond_to?(:_default_attribute_values_not_allowing_nil)
superclass._all_default_attribute_values_not_allowing_nil + _default_attribute_values_not_allowing_nil
else
_default_attribute_values_not_allowing_nil
end
end
# Declares a default value for the given attribute.
#
# Sets the default value to the given options parameter
#
# The <tt>options</tt> can be used to specify the following things:
# * <tt>allow_nil (default: true)</tt> - Sets explicitly passed nil values if option is set to true.
def default_value_for(attribute, value, **options)
allow_nil = options.fetch(:allow_nil, true)
container =
if value.is_a? Proc
BlockValueContainer.new(value)
else
NormalValueContainer.new(value)
end
_default_attribute_values[attribute.to_s] = container
_default_attribute_values_not_allowing_nil << attribute.to_s unless allow_nil
attribute
end
end
end
end
# frozen_string_literal: true
class Field < ApplicationRecord
include FormCore::Concerns::Models::Field
self.table_name = "fields"
belongs_to :form, class_name: "Form", foreign_key: "form_id", touch: true
validates :label,
presence: true
validates :type,
inclusion: {
in: ->(_) { Field.descendants.map(&:to_s) }
},
allow_blank: false
default_value_for :name,
-> (_) { "field_#{SecureRandom.hex(3)}" },
allow_nil: false
def self.type_key
model_name.name.split("::").last.underscore
end
def type_key
self.class.type_key
end
def options_configurable?
options.is_a?(FieldOptions) && options.attributes.any?
end
def validations_configurable?
validations.is_a?(FieldOptions) && validations.attributes.any?
end
def attach_choices?
false
end
protected
def interpret_validations_to(model, accessibility, overrides = {})
return unless accessibility == :read_and_write
validations_overrides = overrides.fetch(:validations) { {} }
validations =
if validations_overrides.any?
self.validations.dup.update(validations_overrides)
else
self.validations
end
validations.interpret_to(model, name, accessibility)
end
def interpret_extra_to(model, accessibility, overrides = {})
options_overrides = overrides.fetch(:options) { {} }
options =
if options_overrides.any?
self.options.dup.update(options_overrides)
else
self.options
end
options.interpret_to(model, name, accessibility)
end
end
require_dependency "fields"
# frozen_string_literal: true
class FieldOptions < DuckRecord::Base
include FormCore::ActsAsDefaultValue
include EnumAttributeLocalizable
class_attribute :keeping_old_serialization
attr_accessor :raw_attributes
def interpret_to(_model, _field_name, _accessibility, _options = {})
end
def serializable_hash(options = {})
options = (options || {}).reverse_merge include: self.class._embeds_reflections.keys
super options
end
private
def _assign_attribute(k, v)
return unless respond_to?("#{k}=")
public_send("#{k}=", v)
end
class << self
def _embeds_reflections
_reflections.select { |_, v| v.is_a? DuckRecord::Reflection::EmbedsAssociationReflection }
end
def model_version
1
end
def root_key_for_serialization
"#{self}.#{model_version}"
end
def dump(obj)
return YAML.dump({}) unless obj
serializable_hash =
if obj.respond_to?(:serializable_hash)
obj.serializable_hash
elsif obj.respond_to?(:to_hash)
obj.to_hash
else
raise ArgumentError, "`obj` required can be cast to `Hash` -- #{obj.class}"
end.stringify_keys
data = {root_key_for_serialization => serializable_hash}
if keeping_old_serialization
data.reverse_merge! obj.raw_attributes
end
YAML.dump(data)
end
def load(yaml_or_hash)
case yaml_or_hash
when Hash
load_from_hash(yaml_or_hash)
when String
load_from_yaml(yaml_or_hash)
else
new
end
end
WHITELIST_CLASSES = [BigDecimal, Date, Time, Symbol]
def load_from_yaml(yaml)
return new if yaml.blank?
unless yaml.is_a?(String) && /^---/.match?(yaml)
return new
end
decoded = YAML.safe_load(yaml, WHITELIST_CLASSES)
unless decoded.is_a? Hash
return new
end
record = new decoded[root_key_for_serialization]
record.raw_attributes = decoded.freeze
record
end
def load_from_hash(hash)
return new if hash.blank?
record = new hash[root_key_for_serialization]
record.raw_attributes = hash.freeze
record
end
end
end
# frozen_string_literal: true
module Fields
%w[
text boolean decimal integer
].each do |type|
require_dependency "fields/#{type}_field"
end
end
# frozen_string_literal: true
module Fields
class BooleanField < Field
serialize :validations, Validations::BooleanField
serialize :options, Options::BooleanField
def stored_type
:boolean
end
end
end
# frozen_string_literal: true
module Fields
class DecimalField < Field
serialize :validations, Validations::DecimalField
serialize :options, Options::DecimalField
def stored_type
:decimal
end
end
end
# frozen_string_literal: true
module Fields
class IntegerField < Field
serialize :validations, Validations::IntegerField
serialize :options, Options::IntegerField
def stored_type
:integer
end
protected
def interpret_extra_to(model, accessibility, _overrides = {})
return if accessibility != :read_and_write
model.validates name, numericality: {only_integer: true}, allow_blank: true
end
end
end
# frozen_string_literal: true
module Fields::Options
class BooleanField < FieldOptions
end
end
# frozen_string_literal: true
module Fields::Options
class DecimalField < FieldOptions
attribute :step, :decimal, default: 0.01
validates :step,
numericality: {
greater_than_or_equal_to: 0.0
}
end
end
# frozen_string_literal: true
module Fields::Options
class IntegerField < FieldOptions
attribute :step, :integer, default: 0
validates :step,
numericality: {
only_integer: true,
greater_than_or_equal_to: 0
}
end
end
# frozen_string_literal: true
module Fields::Options
class TextField < FieldOptions
attribute :multiline, :boolean, default: false
end
end
# frozen_string_literal: true
module Fields
class TextField < Field
serialize :validations, Validations::TextField
serialize :options, Options::TextField
def stored_type
:string
end
end
end
# frozen_string_literal: true
module Fields::Validations
class BooleanField < FieldOptions
prepend Concerns::Fields::Validations::Acceptance
end
end
# frozen_string_literal: true
module Fields::Validations
class DecimalField < FieldOptions
prepend Concerns::Fields::Validations::Presence
prepend Concerns::Fields::Validations::Numericality
end
end
# frozen_string_literal: true
module Fields::Validations
class IntegerField < FieldOptions
prepend Concerns::Fields::Validations::Presence
prepend Concerns::Fields::Validations::Numericality
end
end
# frozen_string_literal: true
module Fields::Validations
class TextField < FieldOptions
prepend Concerns::Fields::Validations::Presence
prepend Concerns::Fields::Validations::Length
prepend Concerns::Fields::Validations::Format
end
end
# frozen_string_literal: true
class Form < ApplicationRecord
include FormCore::Concerns::Models::Form
self.table_name = "forms"
belongs_to :workflow
has_many :fields, foreign_key: "form_id", dependent: :destroy
end
# frozen_string_literal: true
class Group < ApplicationRecord
has_closure_tree dependent: :nullify
has_many :users, dependent: :nullify
validates :name,
presence: true
end
# frozen_string_literal: true
class Place < WorkflowCore::Place
def builtin?
false
end
def append_to_graph(g, prev: nil, arc_label: "", nodes: {})
key = "p_#{id}"
unless nodes[key]
nodes[key] = Graphviz::Node.new("p.#{id}#{": #{name}" if name.present?}", shape: "circle")
g << nodes[key]
end
current = nodes[key]
if prev && !prev.connected?(current)
prev.connect(current, label: arc_label)
end
if output_transition
next_node = nodes["t_#{output_transition.id}"]
if !next_node
output_transition.append_to_graph(g, prev: current, nodes: nodes)
else
current.connect(next_node, label: arc_label)
end
end
g
end
end
require_dependency "places"
# frozen_string_literal: true
module Places
%w[
start end
].each do |type|
require_dependency "places/#{type}_place"
end
end
# frozen_string_literal: true
class Places::EndPlace < Place
def builtin?
true
end
end
# frozen_string_literal: true
class Places::StartPlace < Place
def builtin?
true
end
end
# frozen_string_literal: true
class Token < WorkflowCore::Token
end
# frozen_string_literal: true
class Transition < WorkflowCore::Transition
serialize :options, Transitions::Options::Common
def auto_forward(next_token)
transition = next_token.place.output_transition
return unless transition
return unless transition.auto_forwardable?
transition.fire(next_token)
end
def auto_forwardable?
false
end
def options_configurable?
true
end
def self.type_key
model_name.name.split("::").last.underscore
end
def type_key
self.class.type_key
end
def append_to_graph(g, prev: nil, arc_label: "", nodes: {})
key = "t_#{id}"
unless nodes[key]
nodes[key] = Graphviz::Node.new("t.#{id}#{": #{name}" if name.present?}", shape: "box")
g << nodes[key]
end
current = nodes[key]
if prev && !prev.connected?(current)
prev.connect(current, label: arc_label)
end
output_places.each do |succ|
succ.append_to_graph(g, prev: current, nodes: nodes)
end
g
end
end
require_dependency "transitions"
# frozen_string_literal: true
module Transitions
%w[
sequence
start end
parallel_split synchronization
exclusive_choice simple_merge
].each do |type|
require_dependency "transitions/#{type}"
end
end
# frozen_string_literal: true
class Transitions::End < Transition
def fire(token)
p = output_places.first # assume only one output place
token.completed!
p.tokens.create! status: :completed,
previous: token, type: "Token",
workflow_id: workflow_id, instance_id: token.instance_id
token.instance.completed!
end
def auto_forwardable?
true
end
def options_configurable?
false
end
end
# frozen_string_literal: true
class Transitions::ExclusiveChoice < Transition
serialize :options, Transitions::Options::ExclusiveChoice
def fire(token)
instance = token.instance
next_place_id = options.conditions.select do |condition|
r = ScriptCore.run input: {payload: instance.payload},
sources: [["expression", "@output = #{condition.condition_expression}"]]
if r.errors.any?
raise r.errors.map(&:message).join("; ")
end
r.output
end.first&.place_id || options.default_next_place_id
next_place = workflow.places.find(next_place_id)
token.completed!
next_token = next_place.tokens.create! previous: token, type: "Token",
instance: token.instance, workflow: workflow
auto_forward(next_token)
end
def auto_forwardable?
false
end
end
# frozen_string_literal: true
module Transitions::Options
class Common < FieldOptions
embeds_many :field_overrides, class_name: "Transitions::Options::Common::FieldOverride"
accepts_nested_attributes_for :field_overrides
class FieldOverride < FieldOptions
attribute :name, :string
attribute :accessibility, :integer, default: 0
attr_readonly :name
enum accessibility: {read_and_write: 0, readonly: 1, hidden: 2},
_prefix: :access
validates :name,
presence: true
validates :accessibility,
inclusion: {in: self.accessibilities.keys.map(&:to_sym)}
def name
self[:name]&.to_sym
end
def accessibility
self[:accessibility]&.to_sym
end
end
end
end
# frozen_string_literal: true
module Transitions::Options
class ExclusiveChoice < Common
attribute :default_next_place_id, :integer
embeds_many :conditions, class_name: "Transitions::Options::ExclusiveChoice::Condition"
accepts_nested_attributes_for :conditions
validates :default_next_place_id,
presence: true
class Condition < FieldOptions
attribute :condition_expression, :string
attribute :place_id, :integer
validates :condition_expression, :place_id,
presence: true
end
end
end
# frozen_string_literal: true
class Transitions::ParallelSplit < Transition
def fire(token)
token.completed!
output_places.each do |p|
next_token = p.tokens.create! previous: token, type: "Token",
instance: token.instance, workflow: workflow
auto_forward(next_token)
end
def auto_forwardable?
true
end
def options_configurable?
false
end
end
end
# frozen_string_literal: true
class Transitions::Sequence < Transition
def fire(token)
p = output_places.first # assume only one output place
token.completed!
next_token = p.tokens.create! previous: token, type: "Token",
instance: token.instance, workflow: workflow
auto_forward(next_token)
end
end
# frozen_string_literal: true
class Transitions::SimpleMerge < Transition
def fire(token)
p = output_places.first # assume only one output place
# return unless p.tokens.size.zero?
token.completed!
# TODO: consider if status are not completed
completed_tokens =
input_places
.includes(:tokens).where(workflow_tokens: {instance_id: token.instance_id})
.map(&:tokens).flatten.select(&:completed?)
if completed_tokens.size == 1
next_token = p.tokens.create! previous: token, type: "Token",
instance: token.instance, workflow: workflow
auto_forward(next_token)
end
end
def auto_forwardable?
true
end
def options_configurable?
false
end
end
# frozen_string_literal: true
class Transitions::Start < Transition
# http://workflowpatterns.com/patterns/control/basic/wcp1.php
def fire(token)
p = output_places.first # assume only one output place
token.completed!
next_token = p.tokens.create! previous: token, type: "Token",
instance: token.instance, workflow: workflow
auto_forward(next_token)
end
end
# frozen_string_literal: true
class Transitions::Synchronization < Transition
def fire(token)
p = output_places.first # assume only one output place
# return unless p.tokens.size.zero?
token.completed!
# TODO: consider if status are not completed
completed_tokens =
input_places
.includes(:tokens).where(workflow_tokens: {instance_id: token.instance_id})
.map(&:tokens).flatten.select(&:completed?)
if completed_tokens.size == input_places.size
next_token = p.tokens.create! previous: token, type: "Token",
instance: token.instance, workflow: workflow
auto_forward(next_token)
end
end
def auto_forwardable?
true
end
def options_configurable?
false
end
end
# frozen_string_literal: true
class User < ApplicationRecord
belongs_to :group
validates :name,
presence: true
end
# frozen_string_literal: true
require_dependency "concerns/enum_attribute_localizable"
class VirtualModel < FormCore::VirtualModel
include FormCore::ActsAsDefaultValue
include EnumAttributeLocalizable
class << self
def nested_models
@nested_models ||= {}
end
def attr_readonly?(attr_name)
readonly_attributes.include? attr_name.to_s
end
def metadata
@metadata ||= {}
end
end
end
# frozen_string_literal: true
class Workflow < WorkflowCore::Workflow
has_one :form, dependent: :destroy
after_create :auto_create_form!
after_create :auto_create_start_place!
validates :name,
presence: true
# Graphviz::output(g, path: "test.pdf")
def to_graph
g = Graphviz::Graph.new # rankdir: "LR"
start_place.append_to_graph(g, nodes: {})
end
def load_from_bpmn!(bpmn_xml)
c = Bpmn::DefinitionContainer.parse(bpmn_xml)
return unless c
transitions.destroy_all
places.destroy_all
instances.destroy_all
tokens.destroy_all
place = create_start_place! type: "Places::StartPlace"
node_queue = c.start_event.outgoing_ids
convert_node_type = -> (node) do
case node.node_type
when :task
"Transitions::Sequence"
when :start_event
"Transitions::Start"
when :end_event
"Transitions::End"
when :parallel_gateway
node.outgoing_ids.size == 1 ? "Transitions::Synchronization" : "Transitions::ParallelSplit"
when :exclusive_gateway
node.outgoing_ids.size == 1 ? "Transitions::SimpleMerge" : "Transitions::ExclusiveChoice"
else
raise NotImplementedError.new("#{node.node_type} doesn't support yet.")
end
end
transaction do
loop do
break if node_queue.blank?
target_id = node_queue.pop
curr_node = c[target_id]
raise "#{target_id} not found!" unless curr_node
# puts "1 #{curr_node.name} #{curr_node.node_type} from #{curr_node.try(:incoming_ids)&.join(", ") || curr_node.try(:source_id)} to #{curr_node.try(:outgoing_ids)&.join(", ") || curr_node.try(:target_id)}"
if curr_node.flow?
target_id = curr_node.target_id
target = c[curr_node.target_id]
raise "#{target_id} not found!" unless target
target_transition = transitions.find_by uid: target.id
transition = transitions.find_by uid: c[curr_node.source_id].id
if transition && target_transition
transition.output_places.create! output_transition: target_transition,
type: "Place",
workflow: self
next
elsif transition
place = transition.output_places.create! type: "Place", workflow: self
end
curr_node = target
# puts "2 #{curr_node.name} #{curr_node.node_type} from #{curr_node.try(:incoming_ids)&.join(", ") || curr_node.try(:source_id)} to #{curr_node.try(:outgoing_ids)&.join(", ") || curr_node.try(:target_id)}"
end
transition = transitions.find_by uid: c[curr_node.id].id
unless transition
transition = place.create_output_transition! name: curr_node.name, uid: curr_node.id,
type: convert_node_type.call(curr_node),
workflow: self
end
dup_place = places.find_by input_transition_id: place.input_transition_id, output_transition_id: transition.id
if dup_place
place.destroy
place = dup_place
else
place.update! output_transition: transition
end
children = curr_node.outgoing_ids - transitions.where(uid: curr_node.outgoing_ids).map(&:uid)
if children.empty?
transition.output_places.create! type: "Place",
workflow: self
end
node_queue = children + node_queue
# puts "===="
# puts node_queue.join(", ")
# puts "===="
end
end
end
# TODO: remove in future
def self.seed
w = nil
transaction do
w = create name: "Demo"
p = w.start_place
t = p.create_output_transition! type: "Transitions::Sequence", workflow: w
p.update! output_transition: t
p = t.output_places.create! type: "Place", workflow: w
t = p.create_output_transition! type: "Transitions::Sequence", workflow: w
p.update! output_transition: t
p = t.output_places.create! type: "Place", workflow: w
t = p.create_output_transition! type: "Transitions::Sequence", workflow: w
p.update! output_transition: t
t.output_places.create! type: "Places::EndPlace", workflow: w
end
w
end
private
def auto_create_form!
create_form! type: "Form"
end
def auto_create_start_place!
create_start_place! type: "Places::StartPlace"
end
end
# frozen_string_literal: true
class WorkflowInstance < WorkflowCore::WorkflowInstance
after_create :auto_create_start_token!
private
def auto_create_start_token!
tokens.create! place: workflow.start_place,
workflow: workflow,
type: "Token"
end
end
# frozen_string_literal: true
class ApplicationPresenter < SimpleDelegator
def initialize(model, view, options = {})
super(model)
@model = model
@view = view
@options = options
end
end
# frozen_string_literal: true
module Concerns::Fields
module PresenterForNumberField
extend ActiveSupport::Concern
def min
return if @model.validations.numericality.lower_bound_check_disabled?
min = @model.validations.numericality.lower_bound_value
integer_only? ? min.to_i : min
end
def max
return if @model.validations.numericality.upper_bound_check_disabled?
max = @model.validations.numericality.upper_bound_value
integer_only? ? max.to_i : max
end
def step
step = @model.options.step
return if step.zero?
integer_only? ? step.to_i : step
end
def integer_only?
false
end
def to_builder_options
{min: min, max: max, step: step, required: required?}.reject { |_, v| v.blank? }
end
end
end
# frozen_string_literal: true
module Fields
class BooleanFieldPresenter < FieldPresenter
def required
@model.validations&.acceptance
end
def value_for_preview
super ? I18n.t("values.true") : I18n.t("values.false")
end
end
end
# frozen_string_literal: true
module Fields
class DecimalFieldPresenter < FieldPresenter
include Concerns::Fields::PresenterForNumberField
end
end
# frozen_string_literal: true
class Fields::FieldPresenter < ApplicationPresenter
def required
@model.validations&.presence
end
alias_method :required?, :required
def target
@options[:target]
end
def value
target&.read_attribute(@model.name)
end
def value_for_preview
target&.read_attribute(@model.name)
end
def access_readonly?
target.class.attr_readonly?(@model.name)
end
def access_hidden?
target.class.attribute_names.exclude?(@model.name.to_s) && target.class._reflections.keys.exclude?(@model.name.to_s)
end
def access_read_and_write
target.class.attribute_names.include?(@model.name.to_s) || target.class._reflections.keys.include?(@model.name.to_s)
end
def id
"form_field_#{@model.id}"
end
def nested_form_field?
false
end
def multiple_nested_form?
false
end
end
# frozen_string_literal: true
module Fields
class IntegerFieldPresenter < FieldPresenter
include Concerns::Fields::PresenterForNumberField
def integer_only?
true
end
end
end
# frozen_string_literal: true
module Fields
class TextFieldPresenter < FieldPresenter
def multiline
@model.options.multiline
end
alias_method :multiline?, :multiline
end
end
<h3 class="title">Decimal field</h3>
<div class="field">
<%= f.label :step, class: "label" %>
<div class="control">
<%= f.number_field :step, class: "input", min: 0, step: 0.01 %>
</div>
</div>
<hr>
<h3 class="title">Integer field</h3>
<div class="field">
<%= f.label :step, class: "label" %>
<div class="control">
<%= f.number_field :step, class: "input", min: 0, step: 1 %>
</div>
</div>
<hr>
<h3 class="title">Text field</h3>
<div class="field">
<div class="control">
<label class="checkbox">
<%= f.check_box :multiline %>
The field is a text area
</label>
</div>
</div>
<hr>
<div class="field">
<div class="control">
<label class="checkbox">
<%= f.check_box field.name, id: field.id, required: field.required, disabled: field.access_readonly? %>
<%= field.label %>
</label>
</div>
<% if field.hint.present? %>
<p class="help"><%= field.hint %></p>
<% end %>
</div>
<div class="field">
<%= f.label field.name, field.label, class: 'label' %>
<div class="control">
<%= f.number_field field.name, class: 'input', id: field.id, required: field.required, disabled: field.access_readonly?, **field.to_builder_options %>
</div>
<% if field.hint.present? %>
<p class="help"><%= field.hint %></p>
<% end %>
</div>
<div class="field">
<%= f.label field.name, field.label, class: 'label' %>
<div class="control">
<%= f.number_field field.name, class: 'input', id: field.id, required: field.required, disabled: field.access_readonly?, **field.to_builder_options %>
</div>
<% if field.hint.present? %>
<p class="help"><%= field.hint %></p>
<% end %>
</div>
<div class="field">
<%= f.label field.name, field.label, class: 'label' %>
<div class="control">
<% if field.options.multiline %>
<%= f.text_area field.name, class: 'textarea', id: field.id, required: field.required, disabled: field.access_readonly? %>
<% else %>
<%= f.text_field field.name, class: 'input', id: field.id, required: field.required, disabled: field.access_readonly? %>
<% end %>
</div>
<% if field.hint.present? %>
<p class="help"><%= field.hint %></p>
<% end %>
</div>
<% form.fields.map { |field| present(field, target: instance) }.each do |field| %>
<p><%= field.label %>: <%= field.value_for_preview %></p>
<% end %>
<% fields = form.fields.map { |field| present(field, target: instance) }.reject(&:access_hidden?) %>
<% return if fields.empty? %>
<div class="nested-content">
<% fields.each do |field| %>
<% if field.nested_form_field? %>
<% next unless field.value_for_preview %>
<p><%= field.label %>:</p>
<%= render "_form_core/preview/nested_form", form: field.nested_form, instance: field.value_for_preview %>
<% elsif field.multiple_nested_form? %>
<% next if field.value_for_preview.empty? %>
<p><%= field.label %>:</p>
<%= field.value_for_preview.map do |nested_instance| %>
<% render "_form_core/preview/nested_form", form: field.nested_form, instance: nested_instance %>
<% end.join("<hr>").html_safe %>
<% else %>
<p><%= field.label %>: <%= field.value_for_preview %></p>
<% end %>
<% end %>
</div>
<% options ||= {} %>
<%= form_with(model: instance, **options) do |f| %>
<% if instance.errors.any? %>
<article class="message is-danger">
<div class="message-header">
<p>
<%= pluralize(instance.errors.count, "error") %> prohibited this form from being submitted:
</p>
</div>
<div class="message-body content">
<ul>
<% instance.errors.messages.each do |name, messages| %>
<% messages.each do |message| %>
<li><%= "#{field_label(form, field_name: name)} #{message}" %></li>
<% end %>
<% end %>
</ul>
</div>
</article>
<% end %>
<% form.fields.each do |field| %>
<% field = present(field, target: instance) %>
<% next if field.access_hidden? %>
<%= render "_form_core/fields/#{field.type_key}", f: f, field: field %>
<% end %>
<div class="field is-grouped">
<div class="control">
<%= f.submit "Submit", class: "button is-primary" %>
</div>
<div class="control">
<%= link_to "Back", url_for(:back), class: "button is-link" %>
</div>
</div>
<% end %>
<h3 class="title">Acceptance</h3>
<div class="field">
<div class="control">
<label class="checkbox">
<%= f.check_box :acceptance %>
The field must be checked
</label>
</div>
</div>
<hr>
<h3 class="title">Confirmation</h3>
<div class="field">
<div class="control">
<label class="checkbox">
<%= f.check_box :confirmation %>
The field must be repeated in confirmation field
</label>
</div>
</div>
<hr>
<% validations.build_format unless validations.format %>
<% format = validations.format %>
<h3 class="title">Format</h3>
<%= f.fields_for :format, format do |ff| %>
<% if format.errors.any? %>
<article class="message is-danger">
<div class="message-header">
<p>
<%= pluralize(format.errors.count, "error") %> prohibited this form from being saved:
</p>
</div>
<div class="message-body">
<% format.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</div>
</article>
<% end %>
<div class="field">
<%= ff.label :with %>
<div class="control">
<%= ff.text_field :with, class: "input" %>
</div>
</div>
<div class="field">
<%= ff.label :message %>
<div class="control">
<%= ff.text_field :message, class: "input" %>
</div>
</div>
<% end %>
<hr>
<% validations.build_length unless validations.length %>
<% length = validations.length %>
<h3 class="title">Length</h3>
<%= f.fields_for :length, length do |ff| %>
<% if length.errors.any? %>
<article class="message is-danger">
<div class="message-header">
<p>
<%= pluralize(length.errors.count, "error") %> prohibited this form from being saved:
</p>
</div>
<div class="message-body">
<% length.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</div>
</article>
<% end %>
<div class="field">
<%= ff.label :minimum %>
<div class="control">
<%= ff.number_field :minimum, min: 0, step: 1, class: 'input' %>
</div>
</div>
<div class="field">
<%= ff.label :maximum %>
<div class="control">
<%= ff.number_field :maximum, min: 0, step: 1, class: 'input' %>
</div>
</div>
<div class="field">
<%= ff.label :is %>
<div class="control">
<%= ff.number_field :is, min: 0, step: 1, class: 'input' %>
</div>
</div>
<% end %>
<hr>
<% validations.build_numericality unless validations.numericality %>
<% numericality = validations.numericality %>
<h3 class="title">Numericality</h3>
<%= f.fields_for :numericality, numericality do |ff| %>
<% if numericality.errors.any? %>
<article class="message is-danger">
<div class="message-header">
<p>
<%= pluralize(numericality.errors.count, "error") %> prohibited this form from being saved:
</p>
</div>
<div class="message-body">
<% numericality.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</div>
</article>
<% end %>
<div class="field has-addons">
<div class="control">
<span class="select">
<%= ff.select :lower_bound_check, options_for_enum_select(numericality.class, :lower_bound_check, numericality.lower_bound_check), {}, class: "select" %>
</span>
</div>
<div class="control">
<%= ff.number_field :lower_bound_value, step: 0.01, class: "input" %>
</div>
</div>
<div class="field has-addons">
<div class="control">
<span class="select">
<%= ff.select :upper_bound_check, options_for_enum_select(numericality.class, :upper_bound_check, numericality.upper_bound_check), {}, class: "select" %>
</span>
</div>
<div class="control">
<%= ff.number_field :upper_bound_value, step: 0.01, class: "input" %>
</div>
</div>
<% end %>
<hr>
<h3 class="title">Presence</h3>
<div class="field">
<div class="control">
<label class="checkbox">
<%= f.check_box :presence %>
The field must not empty
</label>
</div>
</div>
<hr>
<div class="nested_form">
<% if f.object.errors.any? %>
<article class="message is-danger">
<div class="message-header">
<p>
<%= pluralize(f.object.errors.count, "error") %> prohibited this form from being submitted:
</p>
</div>
<div class="message-body content">
<ul>
<% f.object.errors.full_messages.each do |message| %>
<li>
<%= message %>
</li>
<% end %>
</ul>
</div>
</article>
<% end %>
<div class="field">
<%= f.label :condition_expression, class: "label" %>
<div class="control">
<%= f.text_field :condition_expression, class: "input" %>
</div>
</div>
<div class="field">
<%= f.label :place, class: "label" %>
<div class="control">
<div class="select">
<%= f.collection_select :place_id, transition.output_places, :id, :id, include_blank: true %>
</div>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<%= link_to_remove_association "Remove", f, class: "button is-small is-danger", wrapper_class: "nested_form" %>
</div>
</div>
</div>
<h3 class="title">Exclusive choice options</h3>
<div class="field">
<%= f.label :default_next_place, class: "label" %>
<div class="control">
<div class="select">
<%= f.collection_select :default_next_place_id, transition.output_places, :id, :id, include_blank: true %>
</div>
</div>
</div>
<div class="field nested_form_field">
<h2 class="label">Conditions</h2>
<div id="conditions" class="collection">
<%= f.fields_for :conditions do |ff| %>
<%= render "_workflow_core/transition_options/condition", f: ff, transition: transition %>
<% end %>
<div class="links">
<%= link_to_add_association "Add", f, "conditions",
class: "button is-small",
partial: "_workflow_core/transition_options/condition",
render_options: {
locals: {transition: transition}
} %>
</div>
</div>
</div>
<hr>
<div class="field">
<h2 class="label">Field overrides</h2>
<% @workflow.form.fields.each do |field| %>
<% field_override = options.field_overrides.find { |r| r.name == field.name } || options.field_overrides.build(name: field.name, accessibility: field.accessibility) %>
<%= f.fields_for :field_overrides, field_override do |ff| %>
<%= ff.hidden_field :name %>
<div class="field">
<%= label_tag field.name, field.label, class: "label" %>
<div class="control">
<% Field.accessibilities.each do |k, _| %>
<label class="radio">
<%= ff.radio_button :accessibility, k %>
<%= Field.human_enum_value :accessibility, k %>
</label>
<% end %>
</div>
</div>
<% end %>
<% end %>
</div>
<hr>
<%= form_with(model: group, local: true) do |f| %>
<% if group.errors.any? %>
<article class="message is-danger">
<div class="message-header">
<p>
<%= pluralize(group.errors.count, "error") %> prohibited this form from being saved:
</p>
</div>
<div class="message-body">
<% group.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</div>
</article>
<% end %>
<div class="field">
<%= f.label :name, class: "label" %>
<div class="control">
<%= f.text_field :name, id: :user_name, class: 'input' %>
</div>
</div>
<div class="field">
<%= f.label :parent, class: "label" %>
<div class="control">
<div class="select">
<% parent_candidates = Group.all.reject { |g| group.child_ids.include?(g.id) || g == group unless group.new_record? } %>
<%= f.collection_select :parent_id, parent_candidates, :id, :name, include_blank: true %>
</div>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<%= f.submit class: "button is-primary" %>
</div>
<div class="control">
<%= link_to "Back", url_for(:back), class: "button is-link" %>
</div>
</div>
<% end %>
<div class="section">
<div class="container">
<p class="title is-1">Editing group</p>
<%= render "form", group: @group %>
</div>
</div>
<div class="section">
<div class="container">
<p content="content">
<%= link_to "New group", new_group_path, class: "button is-primary" %>
</p>
<table class="table is-fullwidth">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Parent name</th>
<th></th>
</tr>
</thead>
<tbody>
<% @groups.each do |group| %>
<tr>
<td><%= group.id %></td>
<td><%= group.name %></td>
<td><%= group.parent&.name %></td>
<td>
<%= link_to "Edit", edit_group_path(group) %> |
<%= link_to "Destroy", group, method: :delete, data: {confirm: "Are you sure?"} %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<div class="section">
<div class="container">
<p class="title is-1">New group</p>
<%= render "form", group: @group %>
</div>
</div>
<% return if alert.blank? %>
<div id="alert" class="flash">
<p><%= alert %></p>
</div>
<footer class="footer">
<div class="container">
<div class="has-text-centered">
<p>
<strong>
<a href="https://github.com/rails-engine/workflow_core" target="_blank" rel="nofollow">Workflow Core</a>
</strong>
crafted by <a href="https://github.com/jasl" target="_blank" rel="nofollow">Jun Jiang</a>.
The source code and the dummy app are licensed under <a href="http://opensource.org/licenses/mit-license.php">MIT license</a>.
</p>
</div>
</div>
</footer>
<nav class="navbar has-shadow" role="navigation" aria-label="main navigation">
<div class="container">
<div class="navbar-brand">
<%= link_to "Workflow Core", root_path, class: "navbar-item" %>
</div>
<div class="navbar-end">
<%= link_to 'Users', users_path, class: "navbar-item #{"is-active" if controller_name == "users"}" %>
<%= link_to 'Groups', groups_path, class: "navbar-item #{"is-active" if controller_name == "groups"}" %>
<%= link_to 'Workflows', workflows_path, class: "navbar-item #{"is-active" if controller_name.start_with?("workflows")}" %>
<% if current_user %>
<%= link_to "Sign out #{current_user.name}", sessions_path, class: "navbar-item", method: :delete %>
<% end %>
</div>
</div>
</nav>
<% return if notice.blank? %>
<div id="notice" class="flash">
<p><%= notice %></p>
</div>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Workflow Core</title>
<%= csrf_meta_tags %>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0"/>
<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track": "reload" %>
<%= javascript_include_tag "application", "data-turbolinks-track": "reload" %>
</head>
<body>
<%= render "layouts/notice" %>
<%= render "layouts/alert" %>
<%= render "layouts/nav" %>
<main class="main">
<%= content_for?(:content) ? yield(:content) : yield %>
</main>
<%= render "layouts/footer" %>
</body>
</html>
<% content_for :sub_content do %>
<div class="container">
<div class="tabs">
<ul>
<%= content_tag(:li, class: "#{"is-active" if controller_name == "instances"}") do %>
<%= link_to "Payload", workflow_instance_path(@workflow, @instance) %>
<% end %>
<%= content_tag(:li, class: "#{"is-active" if controller_name.start_with?("tokens")}") do %>
<%= link_to "Tokens", workflow_instance_tokens_path(@workflow, @instance) %>
<% end %>
</ul>
</div>
</div>
<%= yield %>
<% end %>
<%= render template: "layouts/workflows" %>
<% content_for :content do %>
<section class="hero is-primary">
<div class="hero-body">
<div class="container">
<p class="title">
<%= @workflow.name %>
</p>
<p class="subtitle">
<%= @workflow.description %>
</p>
</div>
</div>
<div class="hero-foot">
<div class="container">
<nav class="tabs is-boxed">
<ul>
<%= content_tag(:li, class: "#{"is-active" if controller_name == "workflows"}") do %>
<%= link_to "Overview", workflow_path(@workflow) %>
<% end %>
<%= content_tag(:li, class: "#{"is-active" if controller_path.start_with?("workflows/fields")}") do %>
<%= link_to "Fields", workflow_fields_path(@workflow) %>
<% end %>
<%= content_tag(:li, class: "#{"is-active" if controller_path.start_with?("workflows/transitions")}") do %>
<%= link_to "Transitions", workflow_transitions_path(@workflow) %>
<% end %>
<%= content_tag(:li, class: "#{"is-active" if controller_path.start_with?("workflows/instances")}") do %>
<%= link_to "Instances", workflow_instances_path(@workflow) %>
<% end %>
<%= content_tag(:li, class: "#{"is-active" if controller_path.start_with?("workflows/loads")}") do %>
<%= link_to "Load form BPMN", workflow_load_path(@workflow) %>
<% end %>
</ul>
</nav>
</div>
</div>
</section>
<%= content_for?(:sub_content) ? yield(:sub_content) : yield %>
<% end %>
<%= render template: "layouts/application" %>
<%= form_with(model: user, local: true) do |f| %>
<% if user.errors.any? %>
<article class="message is-danger">
<div class="message-header">
<p>
<%= pluralize(user.errors.count, "error") %> prohibited this form from being saved:
</p>
</div>
<div class="message-body">
<% user.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</div>
</article>
<% end %>
<div class="field">
<%= f.label :name, class: "label" %>
<div class="control">
<%= f.text_field :name, id: :user_name, class: 'input' %>
</div>
</div>
<div class="field">
<%= f.label :group, class: "label" %>
<div class="control">
<div class="select">
<%= f.collection_select :group_id, Group.all, :id, :name, include_blank: true %>
</div>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<%= f.submit class: "button is-primary" %>
</div>
<div class="control">
<%= link_to "Back", url_for(:back), class: "button is-link" %>
</div>
</div>
<% end %>
<div class="section">
<div class="container">
<p class="title is-1">Editing user</p>
<%= render "form", user: @user %>
</div>
</div>
<div class="section">
<div class="container">
<p content="content">
<%= link_to "New user", new_user_path, class: "button is-primary" %>
</p>
<table class="table is-fullwidth">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Group</th>
<th></th>
</tr>
</thead>
<tbody>
<% @users.each do |user| %>
<tr>
<td><%= user.id %></td>
<td><%= user.name %></td>
<td><%= user.group.name %></td>
<td>
<% if current_user != user %>
<%= link_to "Sign in", sessions_path(user_id: user.id), method: :post %> |
<% end %>
<%= link_to "Edit", edit_user_path(user) %> |
<%= link_to "Destroy", user, method: :delete, data: {confirm: "Are you sure?"} %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<div class="section">
<div class="container">
<p class="title is-1">New user</p>
<%= render "form", user: @user %>
</div>
</div>
<%= form_with(model: workflow, local: true) do |f| %>
<% if workflow.errors.any? %>
<article class="message is-danger">
<div class="message-header">
<p>
<%= pluralize(workflow.errors.count, "error") %> prohibited this workflow from being saved:
</p>
</div>
<div class="message-body content">
<ul>
<% workflow.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
</article>
<% end %>
<div class="field">
<%= f.label :name, class: "label" %>
<div class="control">
<%= f.text_field :name, class: "input", placeholder: "Name" %>
</div>
</div>
<div class="field">
<%= f.label :description, class: "label" %>
<div class="control">
<%= f.text_area :description, class: "textarea", placeholder: "Description" %>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<%= f.submit class: "button is-primary" %>
</div>
<div class="control">
<%= link_to "Back", url_for(:back), class: "button is-link" %>
</div>
</div>
<% end %>
<div class="section">
<div class="container">
<p class="title is-1">Editing workflow</p>
<%= render "form", workflow: @workflow %>
</div>
</div>
<%= form_with(model: [@workflow, field.becomes(Field)], scope: :field, local: true) do |f| %>
<% if field.errors.any? %>
<article class="message is-danger">
<div class="message-header">
<p>
<%= pluralize(field.errors.count, "error") %> prohibited this form from being saved:
</p>
</div>
<div class="message-body content">
<ul>
<% field.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
</article>
<% end %>
<div class="field">
<%= f.label :name, class: "label" %>
<div class="control">
<%= f.text_field :name, class: "input", placeholder: "Name" %>
</div>
<p class="help">
The identifier of the field, the value must follow the pattern <code><%= Field::NAME_REGEX.source %></code> and Ruby's variable naming rule.
</p>
</div>
<div class="field">
<%= f.label :label, class: "label" %>
<div class="control">
<%= f.text_field :label, class: "input", placeholder: "Label" %>
</div>
</div>
<div class="field">
<%= f.label :hint, class: "label" %>
<div class="control">
<%= f.text_field :hint, class: "input", placeholder: "Hint" %>
</div>
</div>
<div class="field">
<%= f.label :type, class: "label" %>
<div class="control">
<span class="select">
<%= f.select :type, options_for_field_types(selected: field.class.to_s) %>
</span>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<%= f.submit class: "button is-primary" %>
</div>
<div class="control">
<%= link_to "Back", url_for(:back), class: "button is-link" %>
</div>
</div>
<% end %>
<div class="section">
<div class="container">
<p class="title is-1">Editing field</p>
<%= render "form", field: @field %>
</div>
</div>
<div class="section">
<div class="container">
<p content="content">
<%= link_to "New field", new_workflow_field_path(@workflow), class: "button is-primary" %>
</p>
<table class="table is-fullwidth">
<thead>
<tr>
<th>Name</th>
<th>Label</th>
<th>Type</th>
<th></th>
</tr>
</thead>
<tbody>
<% @fields.each do |field| %>
<tr>
<td><%= field.name %></td>
<td><%= field.label %></td>
<td><%= field.class.model_name.human %></td>
<td>
<% if field.validations_configurable? %>
<%= link_to "Validations", edit_workflow_field_validations_path(@workflow, field) %> |
<% end %>
<% if field.options_configurable? %>
<%= link_to "Options", edit_workflow_field_options_path(@workflow, field) %> |
<% end %>
<%= link_to "Edit", edit_workflow_field_path(@workflow, field) %> |
<%= link_to "Destroy", workflow_field_path(@workflow, field), method: :delete, data: {confirm: "Are you sure?"} %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<div class="section">
<div class="container">
<p class="title is-1">New field</p>
<%= render "form", field: @field %>
</div>
</div>
<%= form_with(model: options, scope: :options, url: workflow_field_options_path(@workflow, field), method: :patch, local: true) do |f| %>
<% if options.errors.any? %>
<article class="message is-danger">
<div class="message-header">
<p>
<%= pluralize(options.errors.count, "error") %> prohibited this form from being saved:
</p>
</div>
<div class="message-body content">
<ul>
<% options.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
</article>
<% end %>
<%= render "_form_core/field_options/#{field.type_key}", f: f, options: options %>
<div class="field is-grouped">
<div class="control">
<%= f.submit "Update", class: 'button is-primary' %>
</div>
<div class="control">
<%= link_to 'Back', url_for(:back), class: 'button is-link' %>
</div>
</div>
<% end %>
<div class="section">
<div class="container">
<p class="title is-1">Editing options</p>
<%= render "form", field: @field, options: @options %>
</div>
</div>
<%= form_with(model: validations, scope: :validations, url: workflow_field_validations_path(@workflow, field), method: :patch, local: true) do |f| %>
<% if validations.errors.any? %>
<article class="message is-danger">
<div class="message-header">
<p>
<%= pluralize(validations.errors.count, "error") %> prohibited this form from being saved:
</p>
</div>
<div class="message-body content">
<ul>
<% validations.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
</article>
<% end %>
<% validations.class.attribute_names.each do |attribute_name| %>
<%= render "_form_core/validations/#{attribute_name}", f: f, validations: validations %>
<% end %>
<% validations.class.reflections.keys.each do |attribute_name| %>
<%= render "_form_core/validations/#{attribute_name}", f: f, validations: validations %>
<% end %>
<div class="field is-grouped">
<div class="control">
<%= f.submit "Update", class: "button is-primary" %>
</div>
<div class="control">
<%= link_to "Back", url_for(:back), class: "button is-link" %>
</div>
</div>
<% end %>
<div class="section">
<div class="container">
<p class="title is-1">Editing validations</p>
<%= render "form", field: @field, validations: @validations %>
</div>
</div>
<div class="section">
<div class="container">
<p content="content">
<%= link_to "New workflow", new_workflow_path, class: "button is-primary" %>
</p>
<table class="table is-fullwidth">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Updated at</th>
<th></th>
</tr>
</thead>
<tbody>
<% @workflows.each do |workflow| %>
<tr>
<td><%= workflow.name %></td>
<td><%= workflow.description %></td>
<td><%= workflow.updated_at %></td>
<td>
<%= link_to "Show", workflow_path(workflow) %> |
<%= link_to "Edit", edit_workflow_path(workflow) %> |
<%= link_to "Destroy", workflow, method: :delete, data: {confirm: "Are you sure?"} %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<div class="section">
<div class="container">
<p content="content">
<%= link_to "New instance", workflow_instances_path(@workflow), method: :post, class: "button is-primary" %>
</p>
<table class="table is-fullwidth">
<thead>
<tr>
<th>Id</th>
<th>Status</th>
<th>Updated at</th>
<th></th>
</tr>
</thead>
<tbody>
<% @instances.each do |instance| %>
<tr>
<td><%= instance.id %></td>
<td><%= instance.status %></td>
<td><%= time_tag instance.updated_at %></td>
<td>
<%= link_to "Show", workflow_instance_path(@workflow, instance) %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<div class="section">
<div class="container">
<div class="content">
<%= render "_form_core/preview/form", form: @form, instance: @form_record %>
</div>
</div>
</div>
<div class="section">
<div class="container">
<table class="table is-fullwidth">
<thead>
<tr>
<th>Id</th>
<th>Status</th>
<th>Transition id</th>
<th>Transition name</th>
<th>Created at</th>
<th></th>
</tr>
</thead>
<tbody>
<tfoot>
<tr>
<th colspan="6">
Instance status: <%= @instance.status %>
</th>
</tr>
</tfoot>
<% @tokens.each do |token| %>
<tr>
<td><%= token.id %></td>
<td><%= token.status %></td>
<td><%= token.place&.output_transition&.id %></td>
<td><%= token.place&.output_transition&.name %></td>
<td><%= time_tag token.updated_at %></td>
<td>
<% if token.processing? && token.place&.output_transition %>
<%= link_to "Fire", workflow_instance_token_path(@workflow, @instance, token) %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<div class="section">
<div class="container">
<%= render "_form_core/render/form",
form: @form, instance: @form_record,
options: {scope: :form_record, url: workflow_instance_token_fire_path(@workflow, @instance, @token), local: true} %>
</div>
</div>
<div class="section">
<div class="container">
<%= form_with url: workflow_load_path(@workflow), method: :patch, local: true do |f| %>
<div class="field">
<div class="control">
<%= f.text_area :bpmn_xml, class: "textarea", rows: 20 %>
</div>
</div>
<div class="field">
<div class="control">
<%= f.file_field :bpmn_file %>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<%= f.submit "Load from BPMN", class: "button is-primary" %>
</div>
<div class="control">
<%= link_to "Back", url_for(:back), class: "button is-link" %>
</div>
</div>
<% end %>
</div>
</div>
<div class="section">
<div class="container">
<p class="title is-1">New workflow</p>
<%= render "form", workflow: @workflow %>
</div>
</div>
<div class="section">
<div class="container">
<p content="content">
<iframe id="iframe-visualize"
srcdoc="<%= Graphviz::output(@workflow.to_graph, format: "svg") %>"
width="100%"
onload="this.height = getElementById('iframe-visualize').contentDocument.body.scrollHeight;">
</iframe>
</p>
</div>
</div>
<%= form_with(model: [@workflow, transition.becomes(Transition)], scope: :transition, local: true) do |f| %>
<% if transition.errors.any? %>
<article class="message is-danger">
<div class="message-header">
<p>
<%= pluralize(transition.errors.count, "error") %> prohibited this form from being saved:
</p>
</div>
<div class="message-body content">
<ul>
<% transition.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
</article>
<% end %>
<div class="field">
<%= f.label :name, class: "label" %>
<div class="control">
<%= f.text_field :name, class: "input", placeholder: "Name" %>
</div>
</div>
<div class="field">
<%= f.label :type, class: "label" %>
<div class="control">
<span class="select">
<%= f.select :type, options_for_transition_types(selected: transition.class.to_s) %>
</span>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<%= f.submit class: "button is-primary" %>
</div>
<div class="control">
<%= link_to "Back", url_for(:back), class: "button is-link" %>
</div>
</div>
<% end %>
<div class="section">
<div class="container">
<p class="title is-1">Editing transition</p>
<%= render "form", transition: @transition %>
</div>
</div>
<div class="section">
<div class="container">
<table class="table is-fullwidth">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Type</th>
<th></th>
</tr>
</thead>
<tbody>
<% @transitions.each do |transition| %>
<tr>
<td><%= transition.id %></td>
<td><%= transition.name %></td>
<td><%= transition.class.model_name.human %></td>
<td>
<% if transition.options_configurable? %>
<%= link_to "Options", edit_workflow_transition_options_path(@workflow, transition) %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<div class="section">
<div class="container">
<p class="title is-1">New transition</p>
<%= render "form", transition: @transition %>
</div>
</div>
<%= form_with(model: options, scope: :options, url: workflow_transition_options_path(@workflow, transition), method: :patch, local: true) do |f| %>
<% if options.errors.any? %>
<article class="message is-danger">
<div class="message-header">
<p>
<%= pluralize(options.errors.count, "error") %> prohibited this form from being saved:
</p>
</div>
<div class="message-body content">
<ul>
<% options.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
</article>
<% end %>
<%= render "_workflow_core/transition_options/#{transition.type_key}", f: f, transition: transition, options: options %>
<%= render "_workflow_core/transition_options/field_overrides", f: f, transition: transition, options: options %>
<div class="field is-grouped">
<div class="control">
<%= f.submit "Update", class: 'button is-primary' %>
</div>
<div class="control">
<%= link_to 'Back', url_for(:back), class: 'button is-link' %>
</div>
</div>
<% end %>
<div class="section">
<div class="container">
<p class="title is-1">Editing options</p>
<%= render "form", transition: @transition, options: @options %>
</div>
</div>
#!/usr/bin/env ruby
# frozen_string_literal: true
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
load Gem.bin_path("bundler", "bundle")
#!/usr/bin/env ruby
# frozen_string_literal: true
APP_PATH = File.expand_path("../config/application", __dir__)
require_relative "../config/boot"
require "rails/commands"
#!/usr/bin/env ruby
# frozen_string_literal: true
require_relative "../config/boot"
require "rake"
Rake.application.run
#!/usr/bin/env ruby
# frozen_string_literal: true
require "fileutils"
include FileUtils
# path to your application root.
APP_ROOT = File.expand_path("..", __dir__)
def system!(*args)
system(*args) || abort("\n== Command #{args} failed ==")
end
chdir APP_ROOT do
# This script is a starting point to setup your application.
# Add necessary setup steps to this file.
puts "== Installing dependencies =="
system! "gem install bundler --conservative"
system("bundle check") || system!("bundle install")
# Install JavaScript dependencies if using Yarn
# system('bin/yarn')
# puts "\n== Copying sample files =="
# unless File.exist?('config/database.yml')
# cp 'config/database.yml.sample', 'config/database.yml'
# end
puts "\n== Preparing database =="
system! "bin/rails db:setup"
puts "\n== Removing old logs and tempfiles =="
system! "bin/rails log:clear tmp:clear"
puts "\n== Restarting application server =="
system! "bin/rails restart"
end
#!/usr/bin/env ruby
# frozen_string_literal: true
require "fileutils"
include FileUtils
# path to your application root.
APP_ROOT = File.expand_path("..", __dir__)
def system!(*args)
system(*args) || abort("\n== Command #{args} failed ==")
end
chdir APP_ROOT do
# This script is a way to update your development environment automatically.
# Add necessary update steps to this file.
puts "== Installing dependencies =="
system! "gem install bundler --conservative"
system("bundle check") || system!("bundle install")
# Install JavaScript dependencies if using Yarn
# system('bin/yarn')
puts "\n== Updating database =="
system! "bin/rails db:migrate"
puts "\n== Removing old logs and tempfiles =="
system! "bin/rails log:clear tmp:clear"
puts "\n== Restarting application server =="
system! "bin/rails restart"
end
#!/usr/bin/env ruby
# frozen_string_literal: true
APP_ROOT = File.expand_path("..", __dir__)
Dir.chdir(APP_ROOT) do
begin
exec "yarnpkg", *ARGV
rescue Errno::ENOENT
$stderr.puts "Yarn executable was not detected in the system."
$stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
exit 1
end
end
# frozen_string_literal: true
# This file is used by Rack-based servers to start the application.
require_relative "config/environment"
run Rails.application
# frozen_string_literal: true
require_relative "boot"
require "rails/all"
Bundler.require(*Rails.groups)
require "workflow_core"
Dir[Pathname.new(File.dirname(__FILE__)).realpath.parent.join("lib", "monkey_patches", "*.rb")].map do |file|
require file
end
module Dummy
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 5.2
# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
# -- all .rb files in that directory are automatically loaded after loading
# the framework and any gems in your application.
config.to_prepare do
Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c|
require_dependency(c)
end
end
end
end
# frozen_string_literal: true
# tmp file for with_advisory_lock, see https://github.com/ClosureTree/with_advisory_lock/issues/3
ENV["FLOCK_DIR"] ||= File.expand_path("../tmp/locks", __dir__)
# Set up gems listed in the Gemfile.
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__)
require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
$LOAD_PATH.unshift File.expand_path("../../../lib", __dir__)
development:
adapter: async
test:
adapter: async
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: dummy_production
# SQLite version 3.x
# gem install sqlite3
#
# Ensure the SQLite 3 gem is defined in your Gemfile
# gem 'sqlite3'
#
default: &default
adapter: sqlite3
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
development:
<<: *default
database: db/development.sqlite3
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: db/test.sqlite3
production:
<<: *default
database: db/production.sqlite3
# frozen_string_literal: true
# Load the Rails application.
require_relative "application"
# Initialize the Rails application.
Rails.application.initialize!
# frozen_string_literal: true
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# In the development environment your application's code is reloaded on
# every request. This slows down response time but is perfect for development
# since you don't have to restart the web server when you make code changes.
config.cache_classes = false
# Do not eager load code on boot.
config.eager_load = false
# Show full error reports.
config.consider_all_requests_local = true
# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
if Rails.root.join("tmp", "caching-dev.txt").exist?
config.action_controller.perform_caching = true
config.cache_store = :memory_store
config.public_file_server.headers = {
"Cache-Control" => "public, max-age=#{2.days.to_i}"
}
else
config.action_controller.perform_caching = false
config.cache_store = :null_store
end
# Store uploaded files on the local file system (see config/storage.yml for options)
config.active_storage.service = :local
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
config.action_mailer.perform_caching = false
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
# Raise an error on page load if there are pending migrations.
config.active_record.migration_error = :page_load
# Highlight code that triggered database queries in logs.
config.active_record.verbose_query_logs = true
# Debug mode disables concatenation and preprocessing of assets.
# This option may cause significant delays in view rendering with a large
# number of complex assets.
config.assets.debug = true
# Suppress logger output for asset requests.
config.assets.quiet = true
# Raises error for missing translations
# config.action_view.raise_on_missing_translations = true
# Use an evented file watcher to asynchronously detect changes in source code,
# routes, locales, etc. This feature depends on the listen gem.
# config.file_watcher = ActiveSupport::EventedFileUpdateChecker
end
# frozen_string_literal: true
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# Code is not reloaded between requests.
config.cache_classes = true
# Eager load code on boot. This eager loads most of Rails and
# your application in memory, allowing both threaded web servers
# and those relying on copy on write to perform better.
# Rake tasks automatically ignore this option for performance.
config.eager_load = true
# Full error reports are disabled and caching is turned on.
config.consider_all_requests_local = false
config.action_controller.perform_caching = true
# Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
# or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
# config.require_master_key = true
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?
# Compress JavaScripts and CSS.
config.assets.js_compressor = :uglifier
# config.assets.css_compressor = :sass
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
# `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.action_controller.asset_host = 'http://assets.example.com'
# Specifies the header that your server uses for sending files.
# config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
# Store uploaded files on the local file system (see config/storage.yml for options)
config.active_storage.service = :local
# Mount Action Cable outside main process or domain
# config.action_cable.mount_path = nil
# config.action_cable.url = 'wss://example.com/cable'
# config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true
# Use the lowest log level to ensure availability of diagnostic information
# when problems arise.
config.log_level = :debug
# Prepend all log lines with the following tags.
config.log_tags = [ :request_id ]
# Use a different cache store in production.
# config.cache_store = :mem_cache_store
# Use a real queuing backend for Active Job (and separate queues per environment)
# config.active_job.queue_adapter = :resque
# config.active_job.queue_name_prefix = "dummy_#{Rails.env}"
config.action_mailer.perform_caching = false
# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
# config.action_mailer.raise_delivery_errors = false
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true
# Send deprecation notices to registered listeners.
config.active_support.deprecation = :notify
# Use default logging formatter so that PID and timestamp are not suppressed.
config.log_formatter = ::Logger::Formatter.new
# Use a different logger for distributed setups.
# require 'syslog/logger'
# config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
if ENV["RAILS_LOG_TO_STDOUT"].present?
logger = ActiveSupport::Logger.new(STDOUT)
logger.formatter = config.log_formatter
config.logger = ActiveSupport::TaggedLogging.new(logger)
end
# Do not dump schema after migrations.
config.active_record.dump_schema_after_migration = false
end
# frozen_string_literal: true
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!
config.cache_classes = true
# Do not eager load code on boot. This avoids loading your whole application
# just for the purpose of running a single test. If you are using a tool that
# preloads Rails for running tests, you may have to set it to true.
config.eager_load = false
# Configure public file server for tests with Cache-Control for performance.
config.public_file_server.enabled = true
config.public_file_server.headers = {
"Cache-Control" => "public, max-age=#{1.hour.to_i}"
}
# Show full error reports and disable caching.
config.consider_all_requests_local = true
config.action_controller.perform_caching = false
# Raise exceptions instead of rendering exception templates.
config.action_dispatch.show_exceptions = false
# Disable request forgery protection in test environment.
config.action_controller.allow_forgery_protection = false
# Store uploaded files on the local file system in a temporary directory
config.active_storage.service = :test
config.action_mailer.perform_caching = false
# Tell Action Mailer not to deliver emails to the real world.
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
# Raises error for missing translations
# config.action_view.raise_on_missing_translations = true
end
# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# ActiveSupport::Reloader.to_prepare do
# ApplicationController.renderer.defaults.merge!(
# http_host: 'example.org',
# https: false
# )
# end
# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# Version of your assets, change this if you want to expire all your assets.
Rails.application.config.assets.version = "1.0"
# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path
# Add Yarn node_modules folder to the asset load path.
Rails.application.config.assets.paths << Rails.root.join("node_modules")
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in the app/assets
# folder are already added.
# Rails.application.config.assets.precompile += %w( admin.js admin.css )
# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
# Rails.backtrace_cleaner.remove_silencers!
# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# Define an application-wide content security policy
# For further information see the following documentation
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
# Rails.application.config.content_security_policy do |policy|
# policy.default_src :self, :https
# policy.font_src :self, :https, :data
# policy.img_src :self, :https, :data
# policy.object_src :none
# policy.script_src :self, :https
# policy.style_src :self, :https
# # Specify URI for violation reports
# # policy.report_uri "/csp-violation-report-endpoint"
# end
# If you are using UJS then enable automatic nonce generation
# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
# Report CSP violations to a specified URI
# For further information see the following documentation:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
# Rails.application.config.content_security_policy_report_only = true
# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# Specify a serializer for the signed and encrypted cookie jars.
# Valid options are :json, :marshal, and :hybrid.
Rails.application.config.action_dispatch.cookies_serializer = :json
# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# Configure sensitive parameters which will be filtered from the log file.
Rails.application.config.filter_parameters += [:password]
# frozen_string_literal: true
FormCore.virtual_model_class = ::VirtualModel
FormCore.virtual_model_coder_class = FormCore::HashCoder
# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# Add new inflection rules using the following format. Inflections
# are locale specific, and you may define rules for as many different
# locales as you wish. All of these examples are active by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.plural /^(ox)$/i, '\1en'
# inflect.singular /^(ox)en/i, '\1'
# inflect.irregular 'person', 'people'
# inflect.uncountable %w( fish sheep )
# end
# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.acronym 'RESTful'
# end
# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# Add new mime types for use in respond_to blocks:
# Mime::Type.register "text/richtext", :rtf
# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# This file contains settings for ActionController::ParamsWrapper which
# is enabled by default.
# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
ActiveSupport.on_load(:action_controller) do
wrap_parameters format: [:json]
end
# To enable root element in JSON for ActiveRecord objects.
# ActiveSupport.on_load(:active_record) do
# self.include_root_in_json = true
# end
# Files in the config/locales directory are used for internationalization
# and are automatically loaded by Rails. If you want to use locales other
# than English, add the necessary files in this directory.
#
# To use the locales, use `I18n.t`:
#
# I18n.t 'hello'
#
# In views, this is aliased to just `t`:
#
# <%= t('hello') %>
#
# To use a different locale, set it with `I18n.locale`:
#
# I18n.locale = :es
#
# This would use the information in config/locales/es.yml.
#
# The following keys must be escaped otherwise they will not be retrieved by
# the default I18n backend:
#
# true, false, on, off, yes, no
#
# Instead, surround them with single quotes.
#
# en:
# 'true': 'foo'
#
# To learn more, please read the Rails Internationalization guide
# available at http://guides.rubyonrails.org/i18n.html.
en:
values:
'true': 'Yes'
'false': 'No'
# frozen_string_literal: true
# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers: a minimum and maximum.
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
#
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
port ENV.fetch("PORT") { 3000 }
# Specifies the `environment` that Puma will run in.
#
environment ENV.fetch("RAILS_ENV") { "development" }
# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked webserver processes. If using threads and workers together
# the concurrency of the application would be max `threads` * `workers`.
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
#
# workers ENV.fetch("WEB_CONCURRENCY") { 2 }
# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
# process behavior so workers use less memory.
#
# preload_app!
# Allow puma to be restarted by `rails restart` command.
plugin :tmp_restart
# frozen_string_literal: true
Rails.application.routes.draw do
resources :users, except: %i[show]
resources :groups, except: %i[show]
resource :sessions, only: %i[create destroy]
resources :workflows do
scope module: :workflows do
resources :fields, except: %i[show] do
scope module: :fields do
resource :validations, only: %i[edit update]
resource :options, only: %i[edit update]
end
end
resources :transitions, except: %i[show] do
scope module: :transitions do
resource :options, only: %i[edit update]
end
end
resources :instances, only: %i[index show create] do
scope module: :instances do
resources :tokens, only: %i[index show] do
post "fire"
end
end
end
resource :load, only: %i[show update]
end
end
root to: "workflows#index"
end
# frozen_string_literal: true
%w[
.ruby-version
.rbenv-vars
tmp/restart.txt
tmp/caching-dev.txt
].each { |path| Spring.watch(path) }
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
local:
service: Disk
root: <%= Rails.root.join("storage") %>
# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
# service: S3
# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
# region: us-east-1
# bucket: your_own_bucket
# Remember not to checkin your GCS keyfile to a repository
# google:
# service: GCS
# project: your_project
# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
# bucket: your_own_bucket
# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
# microsoft:
# service: AzureStorage
# storage_account_name: your_account_name
# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
# container: your_container_name
# mirror:
# service: Mirror
# primary: local
# mirrors: [ amazon, google, microsoft ]
# frozen_string_literal: true
# This migration comes from form_core (originally 20170430190404)
class CreateForms < ActiveRecord::Migration[5.2]
def change
create_table :forms do |t|
t.string :type, null: false, index: true
t.timestamps
end
end
end
# frozen_string_literal: true
# This migration comes from form_core (originally 20170430191336)
class CreateFields < ActiveRecord::Migration[5.2]
def change
create_table :fields do |t|
t.string :name, null: false
t.integer :accessibility, null: false
t.text :validations
t.text :options
t.string :type, null: false, index: true
t.references :form, foreign_key: true
t.timestamps
end
end
end
# frozen_string_literal: true
class AddColumnsToFields < ActiveRecord::Migration[5.2]
def change
change_table :fields do |t|
t.string :label, default: ""
t.string :hint, default: ""
end
end
end
# frozen_string_literal: true
class AddPositionToFields < ActiveRecord::Migration[5.2]
def change
change_table :fields do |t|
t.integer :position
end
end
end
# frozen_string_literal: true
class AddColumnsToWorkflows < ActiveRecord::Migration[5.2]
def change
change_table :workflows do |t|
t.string :name, default: ""
t.text :description, default: ""
end
end
end
# frozen_string_literal: true
class AddWorkflowIdToForms < ActiveRecord::Migration[5.2]
def change
change_table :forms do |t|
t.references :workflow, foreign_key: true
end
end
end
# frozen_string_literal: true
class CreateGroups < ActiveRecord::Migration[5.2]
def change
create_table :groups do |t|
t.string :name
t.timestamps
end
end
end
# frozen_string_literal: true
class CreateUsers < ActiveRecord::Migration[5.2]
def change
create_table :users do |t|
t.string :name
t.references :group, foreign_key: true
t.timestamps
end
end
end
# frozen_string_literal: true
class AddColumnsToTransitions < ActiveRecord::Migration[5.2]
def change
change_table :workflow_transitions do |t|
t.string :name
t.string :uid
t.text :options
end
end
end
# frozen_string_literal: true
class AddColumnsToPlaces < ActiveRecord::Migration[5.2]
def change
change_table :workflow_places do |t|
t.string :name
t.string :uid
end
end
end
# frozen_string_literal: true
class AddParentIdToGroups < ActiveRecord::Migration[5.2]
def change
change_table :groups do |t|
t.references :parent, foreign_key: {to_table: "groups"}
end
end
end
# frozen_string_literal: true
class CreateGroupHierarchies < ActiveRecord::Migration[5.2]
def change
create_table :group_hierarchies, id: false do |t|
t.integer :ancestor_id, null: false
t.integer :descendant_id, null: false
t.integer :generations, null: false
end
add_index :group_hierarchies, [:ancestor_id, :descendant_id, :generations],
unique: true,
name: "group_anc_desc_idx"
add_index :group_hierarchies, [:descendant_id],
name: "group_desc_idx"
end
end
# frozen_string_literal: true
class AddColumnsToWorkflowInstances < ActiveRecord::Migration[5.2]
def change
change_table :workflow_instances do |t|
t.string :type
end
end
end
# frozen_string_literal: true
class AddColumnsToWorkflowTokens < ActiveRecord::Migration[5.2]
def change
change_table :workflow_tokens do |t|
t.string :type
end
end
end
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# Note that this schema.rb definition is the authoritative source for your
# database schema. If you need to create the application database on another
# system, you should be using db:schema:load, not running all the migrations
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
# you'll amass, the slower it'll run and the greater likelihood for issues).
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2018_09_22_095933) do
create_table "fields", force: :cascade do |t|
t.string "name", null: false
t.integer "accessibility", null: false
t.text "validations"
t.text "options"
t.string "type", null: false
t.integer "form_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "label", default: ""
t.string "hint", default: ""
t.integer "position"
t.index ["form_id"], name: "index_fields_on_form_id"
t.index ["type"], name: "index_fields_on_type"
end
create_table "forms", force: :cascade do |t|
t.string "type", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "workflow_id"
t.index ["type"], name: "index_forms_on_type"
t.index ["workflow_id"], name: "index_forms_on_workflow_id"
end
create_table "group_hierarchies", id: false, force: :cascade do |t|
t.integer "ancestor_id", null: false
t.integer "descendant_id", null: false
t.integer "generations", null: false
t.index ["ancestor_id", "descendant_id", "generations"], name: "group_anc_desc_idx", unique: true
t.index ["descendant_id"], name: "group_desc_idx"
end
create_table "groups", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "parent_id"
t.index ["parent_id"], name: "index_groups_on_parent_id"
end
create_table "users", force: :cascade do |t|
t.string "name"
t.integer "group_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["group_id"], name: "index_users_on_group_id"
end
create_table "workflow_instances", force: :cascade do |t|
t.text "payload"
t.integer "status", default: 0, null: false
t.integer "workflow_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "type"
t.index ["workflow_id"], name: "index_workflow_instances_on_workflow_id"
end
create_table "workflow_places", force: :cascade do |t|
t.integer "input_transition_id"
t.integer "output_transition_id"
t.string "type", null: false
t.integer "workflow_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "name"
t.string "uid"
t.index ["input_transition_id"], name: "index_workflow_places_on_input_transition_id"
t.index ["output_transition_id"], name: "index_workflow_places_on_output_transition_id"
t.index ["workflow_id"], name: "index_workflow_places_on_workflow_id"
end
create_table "workflow_tokens", force: :cascade do |t|
t.integer "status", default: 0, null: false
t.integer "place_id"
t.integer "previous_id"
t.integer "instance_id"
t.integer "workflow_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "type"
t.index ["instance_id"], name: "index_workflow_tokens_on_instance_id"
t.index ["place_id"], name: "index_workflow_tokens_on_place_id"
t.index ["previous_id"], name: "index_workflow_tokens_on_previous_id"
t.index ["workflow_id"], name: "index_workflow_tokens_on_workflow_id"
end
create_table "workflow_transitions", force: :cascade do |t|
t.string "type", null: false
t.integer "workflow_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "name"
t.string "uid"
t.text "options"
t.index ["workflow_id"], name: "index_workflow_transitions_on_workflow_id"
end
create_table "workflows", force: :cascade do |t|
t.string "type", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "name", default: ""
t.text "description", default: ""
end
end
# frozen_string_literal: true
require_relative "active_support/concern+prependable"
# frozen_string_literal: true
# Cheated from https://gitlab.com/gitlab-org/gitlab-ee/blob/master/config/initializers/0_as_concern.rb
# This module is based on: https://gist.github.com/bcardarella/5735987
module Prependable
def prepend_features(base)
if base.instance_variable_defined?(:@_dependencies)
base.instance_variable_get(:@_dependencies) << self
false
else
return false if base < self
super
base.singleton_class.send(:prepend, const_get("ClassMethods")) if const_defined?(:ClassMethods)
@_dependencies.each { |dep| base.send(:prepend, dep) }
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
end
end
end
module ActiveSupport
module Concern
prepend Prependable
alias_method :prepended, :included
end
end
# frozen_string_literal: true
class BigDecimal
# TODO: TEMP FIX BECAUSE SCRIPT_CORE NOT DONE YET.
def to_msgpack(*args)
to_f.to_msgpack(*args)
end
end
{
"name": "dummy",
"private": true,
"dependencies": {}
}
<!DOCTYPE html>
<html>
<head>
<title>The page you were looking for doesn't exist (404)</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
.rails-default-error-page {
background-color: #EFEFEF;
color: #2E2F30;
text-align: center;
font-family: arial, sans-serif;
margin: 0;
}
.rails-default-error-page div.dialog {
width: 95%;
max-width: 33em;
margin: 4em auto 0;
}
.rails-default-error-page div.dialog > div {
border: 1px solid #CCC;
border-right-color: #999;
border-left-color: #999;
border-bottom-color: #BBB;
border-top: #B00100 solid 4px;
border-top-left-radius: 9px;
border-top-right-radius: 9px;
background-color: white;
padding: 7px 12% 0;
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
.rails-default-error-page h1 {
font-size: 100%;
color: #730E15;
line-height: 1.5em;
}
.rails-default-error-page div.dialog > p {
margin: 0 0 1em;
padding: 1em;
background-color: #F7F7F7;
border: 1px solid #CCC;
border-right-color: #999;
border-left-color: #999;
border-bottom-color: #999;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-top-color: #DADADA;
color: #666;
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
</style>
</head>
<body class="rails-default-error-page">
<!-- This file lives in public/404.html -->
<div class="dialog">
<div>
<h1>The page you were looking for doesn't exist.</h1>
<p>You may have mistyped the address or the page may have moved.</p>
</div>
<p>If you are the application owner check the logs for more information.</p>
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>The change you wanted was rejected (422)</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
.rails-default-error-page {
background-color: #EFEFEF;
color: #2E2F30;
text-align: center;
font-family: arial, sans-serif;
margin: 0;
}
.rails-default-error-page div.dialog {
width: 95%;
max-width: 33em;
margin: 4em auto 0;
}
.rails-default-error-page div.dialog > div {
border: 1px solid #CCC;
border-right-color: #999;
border-left-color: #999;
border-bottom-color: #BBB;
border-top: #B00100 solid 4px;
border-top-left-radius: 9px;
border-top-right-radius: 9px;
background-color: white;
padding: 7px 12% 0;
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
.rails-default-error-page h1 {
font-size: 100%;
color: #730E15;
line-height: 1.5em;
}
.rails-default-error-page div.dialog > p {
margin: 0 0 1em;
padding: 1em;
background-color: #F7F7F7;
border: 1px solid #CCC;
border-right-color: #999;
border-left-color: #999;
border-bottom-color: #999;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-top-color: #DADADA;
color: #666;
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
</style>
</head>
<body class="rails-default-error-page">
<!-- This file lives in public/422.html -->
<div class="dialog">
<div>
<h1>The change you wanted was rejected.</h1>
<p>Maybe you tried to change something you didn't have access to.</p>
</div>
<p>If you are the application owner check the logs for more information.</p>
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>We're sorry, but something went wrong (500)</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
.rails-default-error-page {
background-color: #EFEFEF;
color: #2E2F30;
text-align: center;
font-family: arial, sans-serif;
margin: 0;
}
.rails-default-error-page div.dialog {
width: 95%;
max-width: 33em;
margin: 4em auto 0;
}
.rails-default-error-page div.dialog > div {
border: 1px solid #CCC;
border-right-color: #999;
border-left-color: #999;
border-bottom-color: #BBB;
border-top: #B00100 solid 4px;
border-top-left-radius: 9px;
border-top-right-radius: 9px;
background-color: white;
padding: 7px 12% 0;
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
.rails-default-error-page h1 {
font-size: 100%;
color: #730E15;
line-height: 1.5em;
}
.rails-default-error-page div.dialog > p {
margin: 0 0 1em;
padding: 1em;
background-color: #F7F7F7;
border: 1px solid #CCC;
border-right-color: #999;
border-left-color: #999;
border-bottom-color: #999;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-top-color: #DADADA;
color: #666;
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
</style>
</head>
<body class="rails-default-error-page">
<!-- This file lives in public/500.html -->
<div class="dialog">
<div>
<h1>We're sorry, but something went wrong.</h1>
</div>
<p>If you are the application owner check the logs for more information.</p>
</div>
</body>
</html>
# frozen_string_literal: true
# Configure Rails Environment
ENV["RAILS_ENV"] = "test"
require_relative "../test/dummy/config/environment"
ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)]
ActiveRecord::Migrator.migrations_paths << File.expand_path("../db/migrate", __dir__)
require "rails/test_help"
# Filter out Minitest backtrace while allowing backtrace from other libraries
# to be shown.
Minitest.backtrace_filter = Minitest::BacktraceFilter.new
# Load fixtures from the engine
if ActiveSupport::TestCase.respond_to?(:fixture_path=)
ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__)
ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path
ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files"
ActiveSupport::TestCase.fixtures :all
end
# frozen_string_literal: true
require "test_helper"
class WorkflowCore::Test < ActiveSupport::TestCase
test "truth" do
assert_kind_of Module, WorkflowCore
end
end
# frozen_string_literal: true
$:.push File.expand_path("lib", __dir__)
# Maintain your gem's version:
require "workflow_core/version"
# Describe your gem and declare its dependencies:
Gem::Specification.new do |s|
s.name = "workflow_core"
s.version = WorkflowCore::VERSION
s.authors = ["jasl"]
s.email = ["jasl9187@hotmail.com"]
s.homepage = "https://github.com/rails-engine/workflow_core"
s.summary = "A Rails engine which providing essential infrastructure of workflow. It's based on Workflow Nets."
s.description = "A Rails engine which providing essential infrastructure of workflow. It's based on Workflow Nets."
s.license = "MIT"
s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]
s.add_dependency "rails", "~> 5.2"
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment