inner_plan 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +28 -0
  3. data/Rakefile +8 -0
  4. data/app/assets/config/inner_plan_manifest.js +6 -0
  5. data/app/assets/stylesheets/inner_plan/application.css +24 -0
  6. data/app/components/application_view.rb +4 -0
  7. data/app/components/inner_plan/breadcrumbs_component.rb +26 -0
  8. data/app/components/inner_plan/groups/edit_view.rb +72 -0
  9. data/app/components/inner_plan/groups/new_view.rb +66 -0
  10. data/app/components/inner_plan/groups/row/handle_component.rb +30 -0
  11. data/app/components/inner_plan/groups/row_component.rb +31 -0
  12. data/app/components/inner_plan/groups/rows_component.rb +17 -0
  13. data/app/components/inner_plan/groups/show_menu_component.rb +30 -0
  14. data/app/components/inner_plan/groups/show_view.rb +77 -0
  15. data/app/components/inner_plan/lists/completed_tasks_component.rb +30 -0
  16. data/app/components/inner_plan/lists/edit_view.rb +62 -0
  17. data/app/components/inner_plan/lists/index_view.rb +57 -0
  18. data/app/components/inner_plan/lists/new_view.rb +49 -0
  19. data/app/components/inner_plan/lists/ongoing_tasks_component.rb +17 -0
  20. data/app/components/inner_plan/lists/row/handle_component.rb +37 -0
  21. data/app/components/inner_plan/lists/row_component.rb +55 -0
  22. data/app/components/inner_plan/lists/show_menu_component.rb +37 -0
  23. data/app/components/inner_plan/lists/show_view.rb +76 -0
  24. data/app/components/inner_plan/progress_bar_separator_component.rb +20 -0
  25. data/app/components/inner_plan/tasks/completed_toggler_component.rb +27 -0
  26. data/app/components/inner_plan/tasks/edit_view.rb +82 -0
  27. data/app/components/inner_plan/tasks/form/item_component.rb +21 -0
  28. data/app/components/inner_plan/tasks/form/row_component.rb +19 -0
  29. data/app/components/inner_plan/tasks/inline_form_view.rb +62 -0
  30. data/app/components/inner_plan/tasks/row/addons_component.rb +22 -0
  31. data/app/components/inner_plan/tasks/row/base_addon_component.rb +19 -0
  32. data/app/components/inner_plan/tasks/row/description_addon_component.rb +13 -0
  33. data/app/components/inner_plan/tasks/row/due_on_addon_component.rb +14 -0
  34. data/app/components/inner_plan/tasks/row/group_addon_component.rb +11 -0
  35. data/app/components/inner_plan/tasks/row/handle_component.rb +40 -0
  36. data/app/components/inner_plan/tasks/row_component.rb +35 -0
  37. data/app/components/inner_plan/tasks/rows_component.rb +22 -0
  38. data/app/components/inner_plan/tasks/show/item_component.rb +20 -0
  39. data/app/components/inner_plan/tasks/show/row_component.rb +19 -0
  40. data/app/components/inner_plan/tasks/show_menu_component.rb +30 -0
  41. data/app/components/inner_plan/tasks/show_view.rb +76 -0
  42. data/app/components/inner_plan/user_with_avatar_component.rb +17 -0
  43. data/app/concepts/inner_plan/list/operation/index.rb +11 -0
  44. data/app/concepts/inner_plan/task/operation/complete.rb +12 -0
  45. data/app/concepts/inner_plan/task/operation/create.rb +46 -0
  46. data/app/concepts/inner_plan/task/operation/reopen.rb +12 -0
  47. data/app/concepts/inner_plan/task/operation/update.rb +25 -0
  48. data/app/concepts/inner_plan/task/operation/update_position.rb +25 -0
  49. data/app/controllers/inner_plan/application_controller.rb +12 -0
  50. data/app/controllers/inner_plan/groups_controller.rb +59 -0
  51. data/app/controllers/inner_plan/lists_controller.rb +59 -0
  52. data/app/controllers/inner_plan/tasks_controller.rb +73 -0
  53. data/app/helpers/inner_plan/application_helper.rb +4 -0
  54. data/app/javascript/inner_plan/application.js +15 -0
  55. data/app/javascript/inner_plan/controllers/groups_controller.js +47 -0
  56. data/app/javascript/inner_plan/controllers/lists_controller.js +30 -0
  57. data/app/javascript/inner_plan/controllers/task_inline_form_controller.js +40 -0
  58. data/app/javascript/inner_plan/controllers/tasks_controller.js +47 -0
  59. data/app/jobs/inner_plan/application_job.rb +4 -0
  60. data/app/mailers/inner_plan/application_mailer.rb +6 -0
  61. data/app/models/inner_plan/application_record.rb +5 -0
  62. data/app/models/inner_plan/list.rb +29 -0
  63. data/app/models/inner_plan/task.rb +36 -0
  64. data/app/services/inner_plan/tasks/description_renderer.rb +18 -0
  65. data/app/services/inner_plan/tasks/title_renderer.rb +18 -0
  66. data/app/views/inner_plan/shared/_blankslate.svg.erb +1 -0
  67. data/app/views/inner_plan/tasks/complete.turbo_stream.erb +16 -0
  68. data/app/views/inner_plan/tasks/create.turbo_stream.erb +11 -0
  69. data/app/views/inner_plan/tasks/create_failure.turbo_stream.erb +5 -0
  70. data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
  71. data/app/views/layouts/inner_plan/application.html.erb +38 -0
  72. data/config/importmap.rb +8 -0
  73. data/config/routes.rb +17 -0
  74. data/db/migrate/20241205203943_create_inner_plan_tables.rb +37 -0
  75. data/db/seeds.rb +14 -0
  76. data/lib/inner_plan/configuration.rb +116 -0
  77. data/lib/inner_plan/engine.rb +22 -0
  78. data/lib/inner_plan/smart_array.rb +66 -0
  79. data/lib/inner_plan/version.rb +3 -0
  80. data/lib/inner_plan.rb +28 -0
  81. data/lib/tasks/inner_plan_tasks.rake +4 -0
  82. metadata +265 -0
@@ -0,0 +1,30 @@
1
+ module InnerPlan::Tasks
2
+ class ShowMenuComponent < Phlex::HTML
3
+ include Phlex::Rails::Helpers::LinkTo
4
+
5
+ def initialize(task)
6
+ @task = task
7
+ end
8
+
9
+ def template
10
+ div(class: "dropdown") {
11
+ button(class: "btn btn-link text-reset p-0 opacity-50", data_bs_toggle: "dropdown") {
12
+ render(Phlex::Icons::Tabler::DotsCircleHorizontal.new(width: 35, height: 35))
13
+ }
14
+
15
+ ul(class: "dropdown-menu dropdown-menu-end") {
16
+ li {
17
+ link_to(helpers.edit_task_path(@task), class: 'dropdown-item d-flex align-items-center gap-2', data: { turbo_frame: :_top }) {
18
+ render(Phlex::Icons::Tabler::Pencil.new(width: 18, height: 18, class: 'text-secondary'))
19
+ span { "Edit task" }
20
+ }
21
+ }
22
+ }
23
+ }
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :task
29
+ end
30
+ end
@@ -0,0 +1,76 @@
1
+ module InnerPlan::Tasks
2
+ class ShowView < ApplicationView
3
+ include Phlex::Rails::Helpers::ClassNames
4
+ include Phlex::Rails::Helpers::LinkTo
5
+ include Phlex::Rails::Helpers::Routes
6
+
7
+ attr_reader :task
8
+
9
+ def initialize(task:)
10
+ @task = task
11
+ end
12
+
13
+ def template
14
+ header(class: "mb-3") do
15
+ render(InnerPlan::BreadcrumbsComponent.new) do |c|
16
+ c.with_breadcrumb do
17
+ link_to "Home", helpers.root_path, class: "text-reset", data: { turbo_frame: :_top }
18
+ end
19
+
20
+ if @task.list.list.present?
21
+ c.with_breadcrumb do
22
+ link_to @task.list.list.title, @task.list.list, class: "text-reset"
23
+ end
24
+ c.with_breadcrumb do
25
+ link_to @task.list.title, helpers.group_path(@task.list), class: "text-reset"
26
+ end
27
+ else
28
+ c.with_breadcrumb do
29
+ link_to @task.list.title, @task.list, class: "text-reset"
30
+ end
31
+ end
32
+
33
+ c.with_breadcrumb(active: true) { "Task ##{@task.id}" }
34
+ end
35
+ end
36
+
37
+ div(class: "d-flex mb-2") do
38
+ div(class: "text-end me-2") do
39
+ div(style: "display:block;margin-top:-0.25rem;margin-left:-0.8rem") do
40
+ render(
41
+ InnerPlan::Tasks::CompletedTogglerComponent.new(
42
+ @task,
43
+ context: :tasks_show,
44
+ size: 39
45
+ )
46
+ )
47
+ end
48
+ end
49
+ div(class: "w-100") do
50
+ h1(class: "h3") do
51
+ link_to @task.title,
52
+ helpers.edit_task_path(@task, focus: :title),
53
+ class: "text-reset text-decoration-none"
54
+ end
55
+ end
56
+ div(class: "text-end") do
57
+ render(InnerPlan::Tasks::ShowMenuComponent.new(@task))
58
+ end
59
+ end
60
+
61
+ InnerPlan.configuration.task_show_view_rows.each do |row|
62
+ render InnerPlan::Tasks::Form::RowComponent.new do |component|
63
+ row.content.each do |item|
64
+ component.with_column(span: item.options[:span]) do
65
+ render(item.content.call(context: self))
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ def description
73
+ InnerPlan::Tasks::DescriptionRenderer.call(task: @task)[:description]
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,17 @@
1
+ module InnerPlan
2
+ class UserWithAvatarComponent < Phlex::HTML
3
+ include Phlex::Rails::Helpers::ImageTag
4
+
5
+ def initialize(user, avatar_size: 20)
6
+ @user = user
7
+ @avatar_size = avatar_size
8
+ end
9
+
10
+ def template
11
+ image_tag(@user.inner_plan_avatar_url, size: @avatar_size,
12
+ class: 'rounded-circle me-1',
13
+ style: 'margin-top:-0.1rem')
14
+ plain @user.inner_plan_to_s
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ module InnerPlan::List::Operation
2
+ class Index < Trailblazer::Operation
3
+ step :find_models
4
+
5
+ private
6
+
7
+ def find_models(ctx, **)
8
+ ctx[:models] = InnerPlan::List.ordered_by_position.root
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ module InnerPlan::Task::Operation
2
+ class Complete < Trailblazer::Operation
3
+ step Model::Find(InnerPlan::Task, find_by: :id)
4
+ step :mark_as_complete
5
+
6
+ private
7
+
8
+ def mark_as_complete(ctx, model:, **)
9
+ model.complete!
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,46 @@
1
+ module InnerPlan::Task::Operation
2
+ class Create < Trailblazer::Operation
3
+ step :find_list
4
+ step :initialize_model
5
+ step :assign_list
6
+ step :assign_user
7
+ step :assign_position
8
+ left :validate_model
9
+ step :save_model
10
+
11
+ private
12
+
13
+ def find_list(ctx, list_id:, **)
14
+ ctx[:list] = InnerPlan::List.find(list_id)
15
+ end
16
+
17
+ def initialize_model(ctx, params:, **)
18
+ model = InnerPlan::Task.new
19
+ model.title = params[:title]
20
+ model.description = params[:description]
21
+ model.due_on = params[:due_on]
22
+
23
+ ctx[:model] = model
24
+ end
25
+
26
+ def assign_list(ctx, list:, model:, **)
27
+ model.list = list
28
+ end
29
+
30
+ def assign_position(ctx, model:, **)
31
+ model.position = :last
32
+ end
33
+
34
+ def assign_user(ctx, current_user:, model:, **)
35
+ model.user = current_user
36
+ end
37
+
38
+ def validate_model(ctx, model:, **)
39
+ model.validate
40
+ end
41
+
42
+ def save_model(_ctx, model:, **)
43
+ model.save
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,12 @@
1
+ module InnerPlan::Task::Operation
2
+ class Reopen < Trailblazer::Operation
3
+ step Model::Find(InnerPlan::Task, find_by: :id)
4
+ step :reopen
5
+
6
+ private
7
+
8
+ def reopen(ctx, model:, **)
9
+ model.reopen!
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,25 @@
1
+ module InnerPlan::Task::Operation
2
+ class Update < Trailblazer::Operation
3
+ step Model::Find(InnerPlan::Task, find_by: :id)
4
+ step :assign_attributes
5
+ left :validate_model
6
+ step :update_model
7
+
8
+ private
9
+
10
+ def assign_attributes(ctx, model:, params:, **)
11
+ model.title = params[:title]
12
+ model.description = params[:description]
13
+ model.due_on = params[:due_on]
14
+ true
15
+ end
16
+
17
+ def validate_model(ctx, model:, **)
18
+ model.validate
19
+ end
20
+
21
+ def update_model(ctx, model:, **)
22
+ model.save
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ module InnerPlan::Task::Operation
2
+ class UpdatePosition < Trailblazer::Operation
3
+ step Model::Find(InnerPlan::Task, find_by: :id)
4
+ step :assign_position
5
+ step :assign_list
6
+ step :save
7
+
8
+ private
9
+
10
+ def assign_position(ctx, params:, model:, **)
11
+ model.position = { before: params.dig(:task, :position, :before) }
12
+ end
13
+
14
+ def assign_list(ctx, params:, model:, **)
15
+ list_id = params.dig(:task, :list_id)
16
+ return true unless list_id
17
+
18
+ model.list = InnerPlan::List.find(list_id)
19
+ end
20
+
21
+ def save(ctx, model:, **)
22
+ model.save
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,12 @@
1
+ module InnerPlan
2
+ class ApplicationController < ActionController::Base
3
+ helper_method :current_inner_plan_user
4
+
5
+ private
6
+
7
+ def current_inner_plan_user
8
+ @current_inner_plan_user ||=
9
+ public_send(InnerPlan.configuration.current_user_method)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,59 @@
1
+ module InnerPlan
2
+ class GroupsController < ApplicationController
3
+ def show
4
+ @group = InnerPlan::List.sub.find(params[:id])
5
+ render InnerPlan::Groups::ShowView.new(group: @group)
6
+ end
7
+
8
+ def edit
9
+ @group = InnerPlan::List.sub.find(params[:id])
10
+ render InnerPlan::Groups::EditView.new(group: @group)
11
+ end
12
+
13
+ def update
14
+ @group = InnerPlan::List.sub.find(params[:id])
15
+
16
+ if @group.update(group_params)
17
+ redirect_to group_path(@group)
18
+ else
19
+ render :edit, status: :unprocessable_entity
20
+ end
21
+ end
22
+
23
+ def new
24
+ @list = InnerPlan::List.root.find(params[:list_id])
25
+ @group = @list.lists.new(params[:id])
26
+ render InnerPlan::Groups::NewView.new(group: @group)
27
+ end
28
+
29
+ def create
30
+ @group = InnerPlan::List.new(group_params)
31
+ @group.list = InnerPlan::List.root.find(params[:list_id])
32
+ @group.user = current_inner_plan_user
33
+ @group.position = :last
34
+
35
+ if @group.save
36
+ redirect_to @group.list
37
+ else
38
+ render :new, status: :unprocessable_entity
39
+ end
40
+ end
41
+
42
+ def update_position
43
+ @group = InnerPlan::List.sub.find(params[:id])
44
+ @group.position = { before: update_positions_params[:position][:before] }
45
+ @group.list = InnerPlan::List.root.find(update_positions_params[:list_id]) if update_positions_params[:list_id]
46
+ @group.save!
47
+ end
48
+
49
+ private
50
+
51
+ def group_params
52
+ params.require(:list).permit(:title, :description)
53
+ end
54
+
55
+ def update_positions_params
56
+ params.require(:list).permit(:list_id, :position, position: :before)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,59 @@
1
+ module InnerPlan
2
+ class ListsController < ApplicationController
3
+ def index
4
+ @lists = InnerPlan::List::Operation::Index.call[:models]
5
+ render InnerPlan::Lists::IndexView.new(lists: @lists)
6
+ end
7
+
8
+ def show
9
+ @list = InnerPlan::List.root.find(params[:id])
10
+ render InnerPlan::Lists::ShowView.new(list: @list)
11
+ end
12
+
13
+ def new
14
+ @list = InnerPlan::List.new
15
+ render InnerPlan::Lists::NewView.new(list: @list)
16
+ end
17
+
18
+ def create
19
+ @list = InnerPlan::List.new(list_params)
20
+ @list.user = current_inner_plan_user
21
+
22
+ if @list.save
23
+ redirect_to lists_path
24
+ else
25
+ render InnerPlan::Lists::NewView.new(list: @list), status: :unprocessable_entity
26
+ end
27
+ end
28
+
29
+ def edit
30
+ @list = InnerPlan::List.root.find(params[:id])
31
+ render InnerPlan::Lists::EditView.new(list: @list)
32
+ end
33
+
34
+ def update
35
+ @list = InnerPlan::List.root.find(params[:id])
36
+ if @list.update(list_params)
37
+ redirect_to list_path(@list)
38
+ else
39
+ render InnerPlan::Lists::EditView.new(list: @list)
40
+ end
41
+ end
42
+
43
+ def update_position
44
+ @list = InnerPlan::List.root.find(params[:id])
45
+ @list.position = { before: update_positions_params[:position][:before] }
46
+ @list.save!
47
+ end
48
+
49
+ private
50
+
51
+ def list_params
52
+ params.require(:list).permit(:title, :description)
53
+ end
54
+
55
+ def update_positions_params
56
+ params.require(:list).permit(:position, position: :before)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,73 @@
1
+ module InnerPlan
2
+ class TasksController < ApplicationController
3
+ def index
4
+ @lists = InnerPlan::List::Operation::Index.call
5
+ end
6
+
7
+ def new
8
+ @task = InnerPlan::Task.new
9
+ @list = InnerPlan::List.find(params[:list_id])
10
+ end
11
+
12
+ def show
13
+ @task = InnerPlan::Task.find(params[:id])
14
+ render InnerPlan::Tasks::ShowView.new(task: @task)
15
+ end
16
+
17
+ def create
18
+ result = InnerPlan::Task::Operation::Create.call(
19
+ list_id: params[:list_id],
20
+ current_user: current_inner_plan_user,
21
+ params: params.fetch(:task, {})
22
+ )
23
+ @task, @list = result[:model], result[:list]
24
+
25
+ if result.success?
26
+ @new_task = InnerPlan::Task.new
27
+ else
28
+ render :create_failure, status: :unprocessable_entity
29
+ end
30
+ end
31
+
32
+ def edit
33
+ @task = InnerPlan::Task.find(params[:id])
34
+ render InnerPlan::Tasks::EditView.new(task: @task, focus: params[:focus])
35
+ end
36
+
37
+ def update
38
+ result = InnerPlan::Task::Operation::Update.call(
39
+ params: params.fetch(:task, {}).merge(id: params[:id])
40
+ )
41
+ @task = result[:model]
42
+
43
+ if result.success?
44
+ redirect_to task_path(@task)
45
+ else
46
+ render :edit, status: :unprocessable_entity
47
+ end
48
+ end
49
+
50
+ def update_position
51
+ result = InnerPlan::Task::Operation::UpdatePosition.call(params: params)
52
+ @task = result[:model]
53
+ end
54
+
55
+ def complete
56
+ result = InnerPlan::Task::Operation::Complete.call(params: params)
57
+ @task = result[:model]
58
+
59
+ respond_to do |format|
60
+ format.turbo_stream
61
+ end
62
+ end
63
+
64
+ def reopen
65
+ result = InnerPlan::Task::Operation::Reopen.call(params: params)
66
+ @task = result[:model]
67
+
68
+ respond_to do |format|
69
+ format.turbo_stream { render :complete }
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,4 @@
1
+ module InnerPlan
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,15 @@
1
+ import { Application } from "@hotwired/stimulus"
2
+ import * as bootstrap from 'bootstrap'
3
+
4
+ import ListsController from "controllers/lists_controller"
5
+ import GroupsController from "controllers/groups_controller"
6
+ import TasksController from "controllers/tasks_controller"
7
+ import TaskInlineFormController from "controllers/task_inline_form_controller"
8
+
9
+ // Stimulus
10
+ window.Stimulus = Application.start()
11
+
12
+ Stimulus.register("lists", ListsController)
13
+ Stimulus.register("groups", GroupsController)
14
+ Stimulus.register("tasks", TasksController)
15
+ Stimulus.register("task-inline-form", TaskInlineFormController)
@@ -0,0 +1,47 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ import Sortable from "sortablejs"
3
+ import { patch } from "@rails/request.js"
4
+
5
+ export default class extends Controller {
6
+ static values = {
7
+ listId: Number
8
+ };
9
+
10
+ connect() {
11
+ this.sortable = Sortable.create(this.element, {
12
+ handle: '.group-handle',
13
+ group: 'group',
14
+ onUpdate: this.onUpdate.bind(this),
15
+ onAdd: this.onAdd.bind(this),
16
+ })
17
+ }
18
+
19
+ destroy() {
20
+ this.sortable.destroy();
21
+ }
22
+
23
+ onAdd({ item }) {
24
+ const nextGroupEl = item.nextElementSibling;
25
+ const nextGroupId = nextGroupEl ? nextGroupEl.dataset.id : null
26
+
27
+ patch(item.dataset.updateUrl, {
28
+ body: {
29
+ list: {
30
+ list_id: this.listIdValue,
31
+ position: { before: nextGroupId }
32
+ }
33
+ }
34
+ })
35
+ }
36
+
37
+ onUpdate({ item }) {
38
+ const nextGroupEl = item.nextElementSibling;
39
+ const nextGroupId = nextGroupEl ? nextGroupEl.dataset.id : null
40
+
41
+ patch(item.dataset.updateUrl, {
42
+ body: {
43
+ list: { position: { before: nextGroupId } }
44
+ }
45
+ })
46
+ }
47
+ }
@@ -0,0 +1,30 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ import Sortable from "sortablejs"
3
+ import { patch } from "@rails/request.js"
4
+
5
+ export default class extends Controller {
6
+ static targets = ['list'];
7
+
8
+ connect() {
9
+ this.sortable = Sortable.create(this.element, {
10
+ handle: '.list-handle',
11
+ group: 'list',
12
+ onUpdate: this.onUpdate.bind(this)
13
+ })
14
+ }
15
+
16
+ destroy() {
17
+ this.sortable.destroy();
18
+ }
19
+
20
+ onUpdate({ item }) {
21
+ const nextListEl = item.nextElementSibling;
22
+ const nextListId = nextListEl ? nextListEl.dataset.id : null
23
+
24
+ patch(item.dataset.updateUrl, {
25
+ body: {
26
+ list: { position: { before: nextListId } }
27
+ }
28
+ })
29
+ }
30
+ }
@@ -0,0 +1,40 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ['form', 'toggler', 'defaultInput']
5
+
6
+ static values = {
7
+ formVisible: {
8
+ type: Boolean,
9
+ default: false
10
+ }
11
+ }
12
+
13
+ formVisibleValueChanged(value) {
14
+ if (value) {
15
+ this.showForm()
16
+ } else {
17
+ this.hideForm()
18
+ }
19
+ }
20
+
21
+ escPressed() {
22
+ this.formVisibleValue = false
23
+ }
24
+
25
+ togglerClicked(e) {
26
+ e.preventDefault()
27
+ this.formVisibleValue = true
28
+ }
29
+
30
+ showForm() {
31
+ this.formTarget.classList.remove('d-none')
32
+ this.togglerTarget.classList.add('d-none')
33
+ this.defaultInputTarget.focus()
34
+ }
35
+
36
+ hideForm() {
37
+ this.formTarget.classList.add('d-none')
38
+ this.togglerTarget.classList.remove('d-none')
39
+ }
40
+ }
@@ -0,0 +1,47 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ import Sortable from "sortablejs"
3
+ import { patch } from "@rails/request.js"
4
+
5
+ export default class extends Controller {
6
+ static values = {
7
+ listId: Number
8
+ };
9
+
10
+ connect() {
11
+ this.sortable = Sortable.create(this.element, {
12
+ handle: '.task-handle',
13
+ group: 'task',
14
+ onUpdate: this.onUpdate.bind(this),
15
+ onAdd: this.onAdd.bind(this),
16
+ })
17
+ }
18
+
19
+ destroy() {
20
+ this.sortable.destroy();
21
+ }
22
+
23
+ onAdd({ item }) {
24
+ const nextTaskEl = item.nextElementSibling;
25
+ const nextTaskId = nextTaskEl ? nextTaskEl.dataset.id : null
26
+
27
+ patch(item.dataset.updateUrl, {
28
+ body: {
29
+ task: {
30
+ list_id: this.listIdValue,
31
+ position: { before: nextTaskId }
32
+ }
33
+ }
34
+ })
35
+ }
36
+
37
+ onUpdate({ item }) {
38
+ const nextTaskEl = item.nextElementSibling;
39
+ const nextTaskId = nextTaskEl ? nextTaskEl.dataset.id : null
40
+
41
+ patch(item.dataset.updateUrl, {
42
+ body: {
43
+ task: { position: { before: nextTaskId } }
44
+ }
45
+ })
46
+ }
47
+ }
@@ -0,0 +1,4 @@
1
+ module InnerPlan
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module InnerPlan
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module InnerPlan
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end