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