diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 32a6122..c7b7f72 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -75,3 +75,15 @@ form { font-weight: 500; text-transform: uppercase; } + +details.dropdown { + td & { + margin: 0; + } + + &.small { + --pico-form-element-spacing-vertical: 0.25em; + --pico-form-element-spacing-horizontal: 0.5em; + } + +} diff --git a/app/controllers/tasks_controller.rb b/app/controllers/tasks_controller.rb index b3c1693..05e61d6 100644 --- a/app/controllers/tasks_controller.rb +++ b/app/controllers/tasks_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class TasksController < ApplicationController - before_action :fetch_task, only: %w[show edit update delete] + before_action :fetch_task, only: %w[show edit update delete change_status] def index self.current_project = fetch_project @@ -11,7 +11,7 @@ class TasksController < ApplicationController Task.all end - @tasks = @tasks.includes(:project, :status) + @tasks = @tasks.includes(:status, project: :task_statuses) end def show; end @@ -49,6 +49,19 @@ class TasksController < ApplicationController redirect_to tasks_path(project: @task.project) end + def change_status + @form = Tasks::ChangeStatus.new(params.expect(task: :status_id)) + + if @form.perform(@task) + respond_to do |format| + format.html { redirect_to task_path(@task) } + format.turbo_stream + end + else + head :unprocessable_entity + end + end + private def fetch_project diff --git a/app/helpers/tasks_helper.rb b/app/helpers/tasks_helper.rb index 75ace05..d825650 100644 --- a/app/helpers/tasks_helper.rb +++ b/app/helpers/tasks_helper.rb @@ -15,4 +15,12 @@ module TasksHelper content_tag(:span, status.name, class: ['badge', 'task-status', status.category.dasherize]) end + + def task_status_selector(task, selector_class: '', id: nil, with_form: false) + # TODO: extract into a component probably + + raise 'You should pass id if you want the form' if with_form && id.blank? + + render partial: 'tasks/status_selector', locals: { task:, selector_class:, id:, with_form: } + end end diff --git a/app/javascript/controllers/tasks/status_selector_controller.js b/app/javascript/controllers/tasks/status_selector_controller.js new file mode 100644 index 0000000..0d86e7a --- /dev/null +++ b/app/javascript/controllers/tasks/status_selector_controller.js @@ -0,0 +1,17 @@ +import { Controller } from "@hotwired/stimulus" + +class StatusSelectorController extends Controller { + static targets = ['form', 'statusField'] + + changeStatus(event) { + let statusId = event.currentTarget.dataset.statusId + this.statusFieldTarget.value = statusId + this.formTarget.requestSubmit() + } + + finalize() { + this.element.open = false + } +} + +export default StatusSelectorController diff --git a/app/services/application_service.rb b/app/services/application_service.rb index 8112289..e8e2e2c 100644 --- a/app/services/application_service.rb +++ b/app/services/application_service.rb @@ -4,8 +4,6 @@ class ApplicationService include ActiveModel::Model include ActiveModel::Attributes - attr_reader :errors - def perform # Override this end diff --git a/app/services/tasks/change_status.rb b/app/services/tasks/change_status.rb new file mode 100644 index 0000000..a15d176 --- /dev/null +++ b/app/services/tasks/change_status.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Tasks + class ChangeStatus < ApplicationService + attribute :status_id, :integer + + validates :status_id, presence: true + attr_reader :task + + delegate :model_name, to: Task + + def perform(task) + @task = task + @id = task.id + + return false unless valid? + + @task.status_id = status_id + save @task + end + end +end diff --git a/app/views/tasks/_status_selector.html.slim b/app/views/tasks/_status_selector.html.slim new file mode 100644 index 0000000..9fdf54c --- /dev/null +++ b/app/views/tasks/_status_selector.html.slim @@ -0,0 +1,2 @@ +details.dropdown class=selector_class id=id data-controller="tasks--status-selector" + = render partial: 'status_selector_inner', locals: {selector_id: id, task:, with_form:} diff --git a/app/views/tasks/_status_selector_inner.html.slim b/app/views/tasks/_status_selector_inner.html.slim new file mode 100644 index 0000000..8ddd2c5 --- /dev/null +++ b/app/views/tasks/_status_selector_inner.html.slim @@ -0,0 +1,9 @@ +summary= task_status_badge task.status +ul + - task.project.task_statuses.default_order.each do |status| + li + a href="#" data-status-id="#{status.id}" data-action="tasks--status-selector#changeStatus:prevent" = task_status_badge status +- if with_form + = form_with model: Tasks::ChangeStatus.new, url: change_status_task_path(task), method: :patch, data: {'tasks--status-selector-target': 'form', action: 'turbo:submit-end->tasks--status-selector#finalize'} do |f| + = hidden_field_tag :selector_id, selector_id + = f.hidden_field :status_id, data: {'tasks--status-selector-target': 'statusField'} diff --git a/app/views/tasks/change_status.turbo_stream.slim b/app/views/tasks/change_status.turbo_stream.slim new file mode 100644 index 0000000..ac80b73 --- /dev/null +++ b/app/views/tasks/change_status.turbo_stream.slim @@ -0,0 +1,2 @@ +- selector_id = params.fetch(:selector_id) += turbo_stream.update selector_id, render(partial: 'status_selector_inner', locals: {selector_id:, task: @task, with_form: true}) diff --git a/app/views/tasks/index.html.slim b/app/views/tasks/index.html.slim index 3f37f61..25f0a4d 100644 --- a/app/views/tasks/index.html.slim +++ b/app/views/tasks/index.html.slim @@ -11,7 +11,8 @@ h1= tasks_index_title - cache task do tr td= link_to task.full_number, task_path(task) - td= task_status_badge task.status + td + = task_status_selector task, selector_class: 'small', id: "task_status_selector_#{task.id}", with_form: true td= task.title td = link_to 'Edit', edit_task_path(task) diff --git a/config/routes.rb b/config/routes.rb index ac03d36..86a6e85 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -17,5 +17,7 @@ Rails.application.routes.draw do # root "posts#index" resources :projects - resources :tasks + resources :tasks do + patch :change_status, on: :member + end end