diff --git a/.rubocop.yml b/.rubocop.yml index e6fba09..1c53b41 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -28,6 +28,10 @@ Bundler/OrderedGems: Style/Documentation: Enabled: false +Style/FrozenStringLiteralComment: + Exclude: + - db/migrate/**/*.rb + Rails/ActionOrder: Enabled: false diff --git a/app/controllers/tasks_controller.rb b/app/controllers/tasks_controller.rb index 359a4e4..d606fb6 100644 --- a/app/controllers/tasks_controller.rb +++ b/app/controllers/tasks_controller.rb @@ -17,11 +17,12 @@ class TasksController < ApplicationController def show; end def new - @form = Tasks::Create.new(project_id: fetch_project&.id) + @project = fetch_project || Project.order(:name).first + @form = Tasks::Create.new(project_id: @project.id) end def create - @form = Tasks::Create.new(params.expect(task: %i[project_id title description])) + @form = Tasks::Create.new(params.expect(task: %i[project_id title description status_id])) if @form.perform redirect_to tasks_path(project: @form.project) else @@ -30,11 +31,11 @@ class TasksController < ApplicationController end def edit - @form = Tasks::Update.new(id: @task.id, title: @task.title, description: @task.description) + @form = Tasks::Update.new(@task.attributes.slice(*Tasks::Update.attribute_names)) end def update - @form = Tasks::Update.new(params.expect(task: %i[title description])) + @form = Tasks::Update.new(params.expect(task: %i[title description status_id])) if @form.perform(@task) redirect_to task_path(@task) diff --git a/app/javascript/controllers/form_projects_selector_controller.js b/app/javascript/controllers/form_projects_selector_controller.js new file mode 100644 index 0000000..d2ca090 --- /dev/null +++ b/app/javascript/controllers/form_projects_selector_controller.js @@ -0,0 +1,22 @@ +import { Controller } from '@hotwired/stimulus' + +// TODO: unite with ProjectsSelectorController? +class FormProjectsSelectorController extends Controller { + static values = { + frame: String + } + + connect() { + console.log("Connected", this.element) + } + + 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}) + } +} + +export default FormProjectsSelectorController diff --git a/app/models/task.rb b/app/models/task.rb index 3f42f7b..4602a95 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -2,9 +2,11 @@ class Task < ApplicationRecord belongs_to :project + belongs_to :status, class_name: 'TaskStatus' validates :number, :title, presence: true validates :number, numericality: { greater_than: 0 } + validate :associations_should_have_same_project has_rich_text :description @@ -25,4 +27,12 @@ class Task < ApplicationRecord project = Project.find_by!(code: project_code.downcase) find_by!(project:, number:) end + + private + + def associations_should_have_same_project + return if status&.project == project + + errors.add(:status, "Doesn't belong in the same project") + end end diff --git a/app/models/task_status.rb b/app/models/task_status.rb index 45573fa..a13768d 100644 --- a/app/models/task_status.rb +++ b/app/models/task_status.rb @@ -7,4 +7,6 @@ class TaskStatus < ApplicationRecord validates :name, presence: true, uniqueness: { scope: :project } validates :category, presence: true + + scope :default_order, -> { order(:category, :name) } end diff --git a/app/services/tasks/create.rb b/app/services/tasks/create.rb index 1ec07ed..e464d27 100644 --- a/app/services/tasks/create.rb +++ b/app/services/tasks/create.rb @@ -5,18 +5,21 @@ module Tasks attribute :project_id, :integer attribute :title, :string attribute :description, :string + attribute :status_id, :integer validates :project_id, :title, presence: true delegate :model_name, to: Task - attr_reader :project, :task + attr_reader :task + + def project + @project ||= Project.find(project_id) + end def perform - @project = Project.find(project_id) - - @task = @project.tasks.build(title:, description:, number: @project.next_task_number) - @task.save.tap { @errors = @task.errors } + @task = project.tasks.build(title:, description:, status_id:, number: @project.next_task_number) + save @task end end end diff --git a/app/services/tasks/update.rb b/app/services/tasks/update.rb index f61f195..7c1b478 100644 --- a/app/services/tasks/update.rb +++ b/app/services/tasks/update.rb @@ -5,6 +5,7 @@ module Tasks attribute :id, :integer attribute :title, :string attribute :description, :string + attribute :status_id, :integer validates :title, presence: true @@ -15,7 +16,7 @@ module Tasks def perform(task) @task = task @id = task.id - @task.assign_attributes(title:, description:) + @task.assign_attributes(title:, description:, status_id:) save @task end end diff --git a/app/views/tasks/edit.html.slim b/app/views/tasks/edit.html.slim index 7b4f831..71a98ec 100644 --- a/app/views/tasks/edit.html.slim +++ b/app/views/tasks/edit.html.slim @@ -3,6 +3,9 @@ h1 =< @task.full_number = form_with model: @form do |form| + .field + = form.label :status_id + = form.select :status_id, current_project.task_statuses.default_order.map { |ts| [ts.name, ts.id] } .field = form.label :title = form.text_field :title diff --git a/app/views/tasks/new.html.slim b/app/views/tasks/new.html.slim index 49c9465..aa3b1f1 100644 --- a/app/views/tasks/new.html.slim +++ b/app/views/tasks/new.html.slim @@ -1,9 +1,13 @@ h1 New task -= form_with model: @form do |form| += form_with model: @form, data: {controller: 'form-projects-selector', 'form-projects-selector-frame-value' => 'status_select'} do |form| .field = form.label :project_id - = form.select :project_id, Project.order(:name).map { |p| [p.name, p.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 + .field + = form.label :status_id + = form.select :status_id, TaskStatus.where(project: @form.project).default_order.map { |ts| [ts.name, ts.id] } .field = form.label :title = form.text_field :title diff --git a/db/migrate/20251025132011_add_status_to_tasks.rb b/db/migrate/20251025132011_add_status_to_tasks.rb new file mode 100644 index 0000000..f970058 --- /dev/null +++ b/db/migrate/20251025132011_add_status_to_tasks.rb @@ -0,0 +1,5 @@ +class AddStatusToTasks < ActiveRecord::Migration[8.0] + def change + add_reference :tasks, :status, foreign_key: { to_table: :task_statuses } + end +end diff --git a/db/schema.rb b/db/schema.rb index 7bbe5e3..a893960 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_10_25_131322) do +ActiveRecord::Schema[8.0].define(version: 2025_10_25_132011) do create_table "action_text_rich_texts", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t| t.string "name", null: false t.text "body", size: :long @@ -86,7 +86,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_25_131322) do t.string "title", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "status_id" t.index ["project_id"], name: "index_tasks_on_project_id" + t.index ["status_id"], name: "index_tasks_on_status_id" end create_table "users", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t| @@ -102,4 +104,5 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_25_131322) do add_foreign_key "sessions", "users" add_foreign_key "task_statuses", "projects" add_foreign_key "tasks", "projects" + add_foreign_key "tasks", "task_statuses", column: "status_id" end diff --git a/lib/tasks/data_migrations.rake b/lib/tasks/data_migrations.rake new file mode 100644 index 0000000..f1507c7 --- /dev/null +++ b/lib/tasks/data_migrations.rake @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +namespace :data_migrations do + desc 'Set initial status for tasks' + task set_initial_status_for_tasks: :environment do + Task.includes(project: :task_statuses).in_batches do |tasks| + projects = Project.where(id: tasks.pluck(:project_id)) + statuses = TaskStatus.backlog.where(project: projects).group_by(&:project) + + tasks.each do |task| + task.update!(status: statuses[task.project].first) + end + end + end +end