Tasks creation (mostly)
This commit is contained in:
@@ -11,7 +11,7 @@ class TasksController < ApplicationController
|
|||||||
Task.all
|
Task.all
|
||||||
end
|
end
|
||||||
|
|
||||||
@tasks = @tasks.includes(:status, project: :task_statuses)
|
@tasks = @tasks.includes(:status, :workflow, project: :task_statuses)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
@@ -22,7 +22,7 @@ class TasksController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
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
|
if @form.perform
|
||||||
redirect_to tasks_path(project: @form.project)
|
redirect_to tasks_path(project: @form.project)
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -2,20 +2,14 @@ import { Controller } from '@hotwired/stimulus'
|
|||||||
|
|
||||||
// TODO: unite with ProjectsSelectorController?
|
// TODO: unite with ProjectsSelectorController?
|
||||||
class FormProjectsSelectorController extends Controller {
|
class FormProjectsSelectorController extends Controller {
|
||||||
static values = {
|
static targets = ['frame']
|
||||||
frame: String
|
|
||||||
}
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
console.log("Connected", this.element)
|
|
||||||
}
|
|
||||||
|
|
||||||
changeProject(event) {
|
changeProject(event) {
|
||||||
const loc = new URL(location)
|
const loc = new URL(location)
|
||||||
const selected = event.target.selectedOptions[0]
|
const selected = event.target.selectedOptions[0]
|
||||||
const code = selected.dataset.code
|
const code = selected.dataset.code
|
||||||
loc.searchParams.set('project', code)
|
loc.searchParams.set('project', code)
|
||||||
Turbo.visit(loc.toString(), {frame: this.frameValue})
|
Turbo.visit(loc.toString(), {frame: this.frameTarget})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ module Projects
|
|||||||
class PostInitJob < ApplicationJob
|
class PostInitJob < ApplicationJob
|
||||||
queue_as :default
|
queue_as :default
|
||||||
|
|
||||||
include Projects::CreateDefaultTaskStatuses
|
include Projects::CreateDefaults
|
||||||
|
|
||||||
def perform(project_id)
|
def perform(project_id)
|
||||||
project = Project.preparing.find(project_id)
|
project = Project.preparing.find(project_id)
|
||||||
|
|
||||||
project.transaction do
|
project.transaction do
|
||||||
|
workflow = create_default_workflow(project)
|
||||||
create_tasks_number_sequence(project)
|
create_tasks_number_sequence(project)
|
||||||
create_default_task_statuses(project)
|
create_default_task_statuses(project, workflow)
|
||||||
project.update!(status: :ready)
|
project.update!(status: :ready)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class Project < ApplicationRecord
|
|||||||
|
|
||||||
has_many :tasks, dependent: :restrict_with_exception
|
has_many :tasks, dependent: :restrict_with_exception
|
||||||
has_many :task_statuses, dependent: :destroy
|
has_many :task_statuses, dependent: :destroy
|
||||||
|
has_many :workflows, dependent: :destroy
|
||||||
|
|
||||||
has_rich_text :description
|
has_rich_text :description
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -34,8 +34,8 @@ class Task < ApplicationRecord
|
|||||||
private
|
private
|
||||||
|
|
||||||
def associations_should_have_same_project
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,6 +8,15 @@ class TaskStatus < ApplicationRecord
|
|||||||
|
|
||||||
validates :name, presence: true, uniqueness: { scope: :project }
|
validates :name, presence: true, uniqueness: { scope: :project }
|
||||||
validates :category, presence: true
|
validates :category, presence: true
|
||||||
|
validate :associations_should_have_same_project
|
||||||
|
|
||||||
scope :default_order, -> { order(:category, :name) }
|
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
|
end
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class Workflow < ApplicationRecord
|
|||||||
belongs_to :project
|
belongs_to :project
|
||||||
|
|
||||||
has_many :tasks, dependent: :restrict_with_exception
|
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 :icon, { task: 'task', warning: 'warning' }, default: 'task', scopes: false
|
||||||
enum :color, { blue: 'blue', gray: 'gray', yellow: 'yellow', red: 'red' }, default: 'gray', scopes: false
|
enum :color, { blue: 'blue', gray: 'gray', yellow: 'yellow', red: 'red' }, default: 'gray', scopes: false
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ module Tasks
|
|||||||
attribute :title, :string
|
attribute :title, :string
|
||||||
attribute :description, :string
|
attribute :description, :string
|
||||||
attribute :status_id, :integer
|
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
|
delegate :model_name, to: Task
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ module Tasks
|
|||||||
end
|
end
|
||||||
|
|
||||||
def perform
|
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
|
save @task
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
- cache task do
|
- cache task do
|
||||||
tr id="task_#{task.id}"
|
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= link_to task.full_number, task_path(task)
|
||||||
td
|
td
|
||||||
= task_status_selector task, with_form: true
|
= task_status_selector task, with_form: true
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
h1 New task
|
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
|
.field
|
||||||
= form.label :project_id
|
= 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'}
|
= 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
|
.field
|
||||||
= form.label :status_id
|
= form.label :status_id
|
||||||
= form.select :status_id, TaskStatus.where(project: @form.project).default_order.map { |ts| [ts.name, ts.id] }
|
= form.select :status_id, TaskStatus.where(project: @form.project).default_order.map { |ts| [ts.name, ts.id] }
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
@@ -12,4 +12,27 @@ namespace :data_migrations do
|
|||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><g fill='none' fill-rule='evenodd'><path d='M24 0v24H0V0zM12.594 23.258l-.012.002-.071.035-.02.004-.014-.004-.071-.036c-.01-.003-.019 0-.024.006l-.004.01-.017.428.005.02.01.013.104.074.015.004.012-.004.104-.074.012-.016.004-.017-.017-.427c-.002-.01-.009-.017-.016-.018m.264-.113-.014.002-.184.093-.01.01-.003.011.018.43.005.012.008.008.201.092c.012.004.023 0 .029-.008l.004-.014-.034-.614c-.003-.012-.01-.02-.02-.022m-.715.002a.023.023 0 0 0-.027.006l-.006.014-.034.614c0 .012.007.02.017.024l.015-.002.201-.093.01-.008.003-.011.018-.43-.003-.012-.01-.01z'/><path fill='#09244BFF' d='M15 2a2 2 0 0 1 1.732 1H18a2 2 0 0 1 2 2v12a5 5 0 0 1-5 5H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h1.268A2 2 0 0 1 9 2zM7 5H6v15h9a3 3 0 0 0 3-3V5h-1a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2m9.238 4.379a1 1 0 0 1 0 1.414l-4.95 4.95a1 1 0 0 1-1.414 0l-2.12-2.122a1 1 0 0 1 1.413-1.414l1.415 1.414 4.242-4.242a1 1 0 0 1 1.414 0M15 4H9v1h6z'/></g></svg>
|
||||||
|
After Width: | Height: | Size: 996 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><g fill='none'><path d='M24 0v24H0V0zM12.593 23.258l-.011.002-.071.035-.02.004-.014-.004-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01-.017.428.005.02.01.013.104.074.015.004.012-.004.104-.074.012-.016.004-.017-.017-.427c-.002-.01-.009-.017-.017-.018m.265-.113-.013.002-.185.093-.01.01-.003.011.018.43.005.012.008.007.201.093c.012.004.023 0 .029-.008l.004-.014-.034-.614c-.003-.012-.01-.02-.02-.022m-.715.002a.023.023 0 0 0-.027.006l-.006.014-.034.614c0 .012.007.02.017.024l.015-.002.201-.093.01-.008.004-.011.017-.43-.003-.012-.01-.01z'/><path fill='#09244BFF' d='M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2m0 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16m0 11a1 1 0 1 1 0 2 1 1 0 0 1 0-2m0-9a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0V7a1 1 0 0 1 1-1'/></g></svg>
|
||||||
|
After Width: | Height: | Size: 850 B |
Reference in New Issue
Block a user