diff --git a/app/controllers/tasks_controller.rb b/app/controllers/tasks_controller.rb
index 05e61d6..9776ea1 100644
--- a/app/controllers/tasks_controller.rb
+++ b/app/controllers/tasks_controller.rb
@@ -11,7 +11,7 @@ class TasksController < ApplicationController
Task.all
end
- @tasks = @tasks.includes(:status, project: :task_statuses)
+ @tasks = @tasks.includes(:status, :workflow, project: :task_statuses)
end
def show; end
@@ -22,7 +22,7 @@ class TasksController < ApplicationController
end
def create
- @form = Tasks::Create.new(params.expect(task: %i[project_id title description status_id]))
+ @form = Tasks::Create.new(params.expect(task: %i[project_id title description status_id workflow_id]))
if @form.perform
redirect_to tasks_path(project: @form.project)
else
diff --git a/app/javascript/controllers/form_projects_selector_controller.js b/app/javascript/controllers/form_projects_selector_controller.js
index d2ca090..f89d0c1 100644
--- a/app/javascript/controllers/form_projects_selector_controller.js
+++ b/app/javascript/controllers/form_projects_selector_controller.js
@@ -2,20 +2,14 @@ import { Controller } from '@hotwired/stimulus'
// TODO: unite with ProjectsSelectorController?
class FormProjectsSelectorController extends Controller {
- static values = {
- frame: String
- }
-
- connect() {
- console.log("Connected", this.element)
- }
+ static targets = ['frame']
changeProject(event) {
const loc = new URL(location)
const selected = event.target.selectedOptions[0]
const code = selected.dataset.code
loc.searchParams.set('project', code)
- Turbo.visit(loc.toString(), {frame: this.frameValue})
+ Turbo.visit(loc.toString(), {frame: this.frameTarget})
}
}
diff --git a/app/jobs/projects/post_init_job.rb b/app/jobs/projects/post_init_job.rb
index a01fe32..3d6843a 100644
--- a/app/jobs/projects/post_init_job.rb
+++ b/app/jobs/projects/post_init_job.rb
@@ -4,14 +4,15 @@ module Projects
class PostInitJob < ApplicationJob
queue_as :default
- include Projects::CreateDefaultTaskStatuses
+ include Projects::CreateDefaults
def perform(project_id)
project = Project.preparing.find(project_id)
project.transaction do
+ workflow = create_default_workflow(project)
create_tasks_number_sequence(project)
- create_default_task_statuses(project)
+ create_default_task_statuses(project, workflow)
project.update!(status: :ready)
end
diff --git a/app/models/project.rb b/app/models/project.rb
index f29799c..50799f3 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -8,6 +8,7 @@ class Project < ApplicationRecord
has_many :tasks, dependent: :restrict_with_exception
has_many :task_statuses, dependent: :destroy
+ has_many :workflows, dependent: :destroy
has_rich_text :description
diff --git a/app/models/task.rb b/app/models/task.rb
index 1b736d3..a5d7730 100644
--- a/app/models/task.rb
+++ b/app/models/task.rb
@@ -34,8 +34,8 @@ class Task < ApplicationRecord
private
def associations_should_have_same_project
- return if status&.project == project
+ return if [project, status.project, workflow.project].uniq == [project]
- errors.add(:status, "Doesn't belong in the same project")
+ errors.add(:base, "Project isn't the same across associations")
end
end
diff --git a/app/models/task_status.rb b/app/models/task_status.rb
index 8ba0a17..e2f43ea 100644
--- a/app/models/task_status.rb
+++ b/app/models/task_status.rb
@@ -8,6 +8,15 @@ class TaskStatus < ApplicationRecord
validates :name, presence: true, uniqueness: { scope: :project }
validates :category, presence: true
+ validate :associations_should_have_same_project
scope :default_order, -> { order(:category, :name) }
+
+ private
+
+ def associations_should_have_same_project
+ return if workflow.project == project
+
+ errors.add(:workflow, "Doesn't belong in the same project")
+ end
end
diff --git a/app/models/workflow.rb b/app/models/workflow.rb
index e69b8b2..a39f941 100644
--- a/app/models/workflow.rb
+++ b/app/models/workflow.rb
@@ -4,7 +4,7 @@ class Workflow < ApplicationRecord
belongs_to :project
has_many :tasks, dependent: :restrict_with_exception
- has_mnay :statuses, dependent: :restrict_with_error
+ has_many :statuses, dependent: :restrict_with_error
enum :icon, { task: 'task', warning: 'warning' }, default: 'task', scopes: false
enum :color, { blue: 'blue', gray: 'gray', yellow: 'yellow', red: 'red' }, default: 'gray', scopes: false
diff --git a/app/services/tasks/create.rb b/app/services/tasks/create.rb
index 42f6bf5..f7cb1a1 100644
--- a/app/services/tasks/create.rb
+++ b/app/services/tasks/create.rb
@@ -6,8 +6,9 @@ module Tasks
attribute :title, :string
attribute :description, :string
attribute :status_id, :integer
+ attribute :workflow_id, :integer
- validates :project_id, :title, :status_id, presence: true
+ validates :project_id, :title, :status_id, :workflow_id, presence: true
delegate :model_name, to: Task
@@ -18,7 +19,7 @@ module Tasks
end
def perform
- @task = project.tasks.build(title:, description:, status_id:, number: @project.next_task_number)
+ @task = project.tasks.build(title:, description:, status_id:, workflow_id:, number: @project.next_task_number)
save @task
end
end
diff --git a/app/views/tasks/_table_row.html.slim b/app/views/tasks/_table_row.html.slim
index 45fa2c3..e359c9b 100644
--- a/app/views/tasks/_table_row.html.slim
+++ b/app/views/tasks/_table_row.html.slim
@@ -1,5 +1,8 @@
- cache task do
tr id="task_#{task.id}"
+ td
+ / TODO: extract into a presenter (and especially add color)
+ = image_tag('mingcute/task_line.svg', title: task.workflow.name)
td= link_to task.full_number, task_path(task)
td
= task_status_selector task, with_form: true
diff --git a/app/views/tasks/new.html.slim b/app/views/tasks/new.html.slim
index aa3b1f1..df2dbcf 100644
--- a/app/views/tasks/new.html.slim
+++ b/app/views/tasks/new.html.slim
@@ -1,10 +1,14 @@
h1 New task
-= form_with model: @form, data: {controller: 'form-projects-selector', 'form-projects-selector-frame-value' => 'status_select'} do |form|
+= form_with model: @form, data: {controller: 'form-projects-selector'} do |form|
.field
= form.label :project_id
= form.select :project_id, Project.order(:name).map { |p| [p.name, p.id, {'data-code': p.code}] }, {}, data: {action: 'form-projects-selector#changeProject'}
- = turbo_frame_tag :status_select do
+ = turbo_frame_tag :project_field_selects, data: {'form-projects-selector-target': 'frame'} do
+ .field
+ = form.label :workflow
+ = form.select :workflow_id, Workflow.where(project: @form.project).map { |w| [w.name, w.id] }
+
.field
= form.label :status_id
= form.select :status_id, TaskStatus.where(project: @form.project).default_order.map { |ts| [ts.name, ts.id] }
diff --git a/lib/projects/create_default_task_statuses.rb b/lib/projects/create_default_task_statuses.rb
deleted file mode 100644
index a92a1fe..0000000
--- a/lib/projects/create_default_task_statuses.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- module CreateDefaultTaskStatuses
- module_function
-
- def create_default_task_statuses(project)
- # TODO: make it configurable/templatable?
-
- project.transaction do
- project.task_statuses.create!(category: :backlog, name: 'Backlog')
- project.task_statuses.create!(category: :analysis, name: 'To do')
- project.task_statuses.create!(category: :development, name: 'In development')
- project.task_statuses.create!(category: :fulfillment, name: 'Done')
- end
- end
- end
-end
diff --git a/lib/projects/create_defaults.rb b/lib/projects/create_defaults.rb
new file mode 100644
index 0000000..d44d609
--- /dev/null
+++ b/lib/projects/create_defaults.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Projects
+ module CreateDefaults
+ module_function
+
+ def create_default_workflow(project)
+ project.transaction do
+ project.workflows.create!(name: 'Default')
+ end
+ end
+
+ def create_default_task_statuses(project, workflow)
+ # TODO: make it configurable/templatable?
+
+ project.transaction do
+ project.task_statuses.create!(workflow:, category: :backlog, name: 'Backlog')
+ project.task_statuses.create!(workflow:, category: :analysis, name: 'To do')
+ project.task_statuses.create!(workflow:, category: :development, name: 'In development')
+ project.task_statuses.create!(workflow:, category: :fulfillment, name: 'Done')
+ end
+ end
+ end
+end
diff --git a/lib/tasks/data_migrations.rake b/lib/tasks/data_migrations.rake
index f1507c7..5b42249 100644
--- a/lib/tasks/data_migrations.rake
+++ b/lib/tasks/data_migrations.rake
@@ -12,4 +12,27 @@ namespace :data_migrations do
end
end
end
+
+ desc 'Create initial workflows for projects'
+ task create_initial_workflows: :environment do
+ Project.find_each do |project|
+ Projects::CreateDefaults.create_default_workflow(project)
+ end
+ end
+
+ desc 'Set workflows for statuses'
+ task set_default_task_status_workflows: :create_initial_workflows do
+ TaskStatus.includes(project: :workflows).find_each do |ts|
+ ts.workflow = ts.project.workflows.first
+ ts.save!
+ end
+ end
+
+ desc 'Set workflows for tasks'
+ task set_default_task_workflows: %i[create_initial_workflows set_default_task_status_workflows] do
+ Task.includes(project: :workflows).find_each do |task|
+ task.workflow = task.project.workflows.first
+ task.save!
+ end
+ end
end
diff --git a/vendor/assets/images/mingcute/task_line.svg b/vendor/assets/images/mingcute/task_line.svg
new file mode 100644
index 0000000..e303c87
--- /dev/null
+++ b/vendor/assets/images/mingcute/task_line.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/vendor/assets/images/mingcute/warning_line.svg b/vendor/assets/images/mingcute/warning_line.svg
new file mode 100644
index 0000000..5ecd642
--- /dev/null
+++ b/vendor/assets/images/mingcute/warning_line.svg
@@ -0,0 +1 @@
+
\ No newline at end of file