Spike workflows

This commit is contained in:
Jose Farias
2024-10-21 15:33:30 -06:00
parent b1f5498914
commit b0565fbf20
35 changed files with 397 additions and 42 deletions
@@ -0,0 +1,21 @@
class Bubbles::StagePickersController < ApplicationController
include BubbleScoped, BucketScoped
before_action :set_workflows, :set_selected_workflow
def new
end
private
def set_workflows
@workflows = Current.account.workflows
end
def set_selected_workflow
@selected_workflow = if params[:workflow_id]
@workflows.find params[:workflow_id]
else
@bubble.workflow || @workflows.first
end
end
end
@@ -0,0 +1,8 @@
class Bubbles::StagingsController < ApplicationController
include BubbleScoped, BucketScoped
def create
@bubble.toggle_stage Current.account.stages.find(params[:stage_id])
redirect_to new_bucket_bubble_stage_picker_path(@bucket, @bubble)
end
end
View File
@@ -0,0 +1,12 @@
module WorkflowScoped
extend ActiveSupport::Concern
included do
before_action :set_workflow
end
private
def set_workflow
@workflow = Current.account.workflows.find params[:workflow_id]
end
end
@@ -0,0 +1,36 @@
class Workflows::StagesController < ApplicationController
include WorkflowScoped
before_action :set_stage, only: %i[ edit update destroy ]
def new
@stage = @workflow.stages.new
end
def create
@stage = @workflow.stages.create! stage_params
redirect_to account_workflow_path(@workflow)
end
def edit
end
def update
@stage.update! stage_params
redirect_to account_workflow_path(@workflow)
end
def destroy
@stage.destroy
redirect_to account_workflow_path(@workflow)
end
private
def set_stage
@stage = @workflow.stages.find params[:id]
end
def stage_params
params.require(:workflow_stage).permit(:name)
end
end
+43
View File
@@ -0,0 +1,43 @@
class WorkflowsController < ApplicationController
before_action :set_workflow, only: %i[ show edit update destroy ]
def index
@workflows = Current.account.workflows
end
def new
@workflow = Current.account.workflows.new
end
def create
@workflow = Current.account.workflows.create! workflow_params
# FIXME: this should definitely change.
%w[ Triage WIP On-hold ].each { |name| @workflow.stages.create! name: name }
redirect_to account_workflows_path
end
def show
end
def edit
end
def update
@workflow.update! workflow_params
redirect_to account_workflow_path(@workflow)
end
def destroy
@workflow.destroy
redirect_to account_workflows_path
end
private
def set_workflow
@workflow = Current.account.workflows.find params[:id]
end
def workflow_params
params.require(:workflow).permit(:name)
end
end
+10
View File
@@ -0,0 +1,10 @@
module WorkflowsHelper
def link_to_stage_picker(bubble, workflow)
link_to workflow.name, new_bucket_bubble_stage_picker_path(bubble.bucket, bubble, workflow_id: workflow.id)
end
def button_to_set_stage(bubble, stage)
button_to stage.name, bucket_bubble_stagings_path(bubble.bucket, bubble, stage_id: stage.id),
method: :post, class: [ "btn btn--small", { "fill-selected": stage == bubble.stage } ]
end
end
+3
View File
@@ -6,5 +6,8 @@ class Account < ApplicationRecord
has_many :buckets, dependent: :destroy
has_many :bubbles, through: :buckets
has_many :workflows, dependent: :destroy
has_many :stages, through: :workflows, class_name: "Workflow::Stage"
has_many :tags, dependent: :destroy
end
+1 -1
View File
@@ -1,5 +1,5 @@
class Bubble < ApplicationRecord
include Assignable, Boostable, Colored, Commentable, Eventable, Poppable, Searchable, Taggable, Threaded
include Assignable, Boostable, Colored, Commentable, Eventable, Poppable, Searchable, Staged, Taggable, Threaded
belongs_to :bucket
belongs_to :creator, class_name: "User", default: -> { Current.user }
+21
View File
@@ -0,0 +1,21 @@
module Bubble::Staged
extend ActiveSupport::Concern
included do
belongs_to :stage, class_name: "Workflow::Stage", optional: true
end
def workflow
stage&.workflow
end
def toggle_stage(stage)
if self.stage == stage
update! stage: nil
track_event :unstaged, stage_id: stage.id
else
update! stage: stage
track_event :staged, stage_id: stage.id
end
end
end
+9 -4
View File
@@ -25,9 +25,10 @@ class Bubble::Thread::Rollup
def sorted_entries
entries.sort_by do |entry|
case entry.action
when "created" then [ 1, entry.created_at ]
when "assigned" then [ 2, entry.created_at ]
when "boosted" then [ 3, entry.creator, entry.created_at ]
when "created" then [ 1, entry.created_at ]
when "assigned" then [ 2, entry.created_at ]
when "staged", "unstaged" then [ 3, entry.created_at ]
when "boosted" then [ 4, entry.creator, entry.created_at ]
end
end
end
@@ -41,9 +42,13 @@ class Bubble::Thread::Rollup
when "created"
"added by #{entry.creator.name} #{time_ago_in_words(entry.created_at)} ago"
when "assigned"
summary = "assigned to #{entry.assignee_names.to_sentence}"
summary = "assigned to #{entry.assignees.map(&:name).to_sentence}"
summary += " #{time_ago_in_words(entry.created_at)} ago" unless first_position?
summary
when "staged"
"#{entry.creator.name} moved this to '#{entry.stage.name}'"
when "unstaged"
"#{entry.creator.name} removed this from '#{entry.stage.name}'"
when "boosted"
"#{entry.creator.name} +#{chunk_size}"
end
+2 -2
View File
@@ -1,7 +1,7 @@
class Event < ApplicationRecord
THREADABLE_ACTIONS = %w[ assigned boosted created ]
THREADABLE_ACTIONS = %w[ assigned boosted created staged unstaged ]
include Assignments
include Assignments, Stages
belongs_to :creator, class_name: "User"
belongs_to :bubble, touch: true
+2 -7
View File
@@ -5,12 +5,7 @@ module Event::Assignments
store_accessor :particulars, :assignee_ids
end
def assignee_names
assignees.map &:name
def assignees
@assignees ||= account.users.where id: assignee_ids
end
private
def assignees
@assignees ||= account.users.where id: assignee_ids
end
end
+11
View File
@@ -0,0 +1,11 @@
module Event::Stages
extend ActiveSupport::Concern
included do
store_accessor :particulars, :stage_id
end
def stage
@stage ||= account.stages.find_by_id stage_id
end
end
+4
View File
@@ -0,0 +1,4 @@
class Workflow < ApplicationRecord
belongs_to :account
has_many :stages, dependent: :delete_all
end
+3
View File
@@ -0,0 +1,3 @@
class Workflow::Stage < ApplicationRecord
belongs_to :workflow
end
+5
View File
@@ -21,3 +21,8 @@
<div class="panel borderless center pad fill-none flex flex-column gap">
<%= render partial: "accounts/users/user", collection: @users %>
</div>
<div class="panel borderless center pad fill-none flex flex-column gap">
<h2>Workflows</h2>
<%= turbo_frame_tag :workflows, src: account_workflows_path %>
</div>
+11
View File
@@ -0,0 +1,11 @@
<% if bubble.popped? %>
<%= button_to bucket_bubble_pop_path(bubble.bucket, bubble), method: :delete, class: "btn" do %>
<%= image_tag "pop.svg", aria: { hidden: true }, size: 24 %>
<span>Un-pop</span>
<% end %>
<% else %>
<%= button_to bucket_bubble_pop_path(bubble.bucket, bubble), class: "btn" do %>
<%= image_tag "pop.svg", aria: { hidden: true }, size: 24 %>
<span>Pop</span>
<% end %>
<% end %>
@@ -18,8 +18,8 @@
<span class="txt-small">Update</span>
<% end %>
<%= button_tag type: :submit, form: dom_id(view, :delete_form), class: "btn", style: "font-size: 0.4em;" do %>
<%= image_tag "alert.svg", aria: { hidden: true }, size: 24 %>
<%= button_tag type: :submit, form: dom_id(view, :delete_form), class: "btn", style: "font-size: 0.4em;", data: { turbo_confirm: "Are you sure you want to delete this filter?" } do %>
<%= image_tag "minus.svg", aria: { hidden: true }, size: 24 %>
<span class="txt-small">Delete</span>
<% end %>
<% end %>
+5 -12
View File
@@ -7,18 +7,11 @@
<span class="for-screen-reader">All Bubbles</span>
<% end %>
<% if @bubble.popped? %>
<%= button_to bucket_bubble_pop_path(@bubble.bucket, @bubble), method: :delete, class: "btn" do %>
<%= image_tag "pop.svg", aria: { hidden: true }, size: 24 %>
<span>Un-pop</span>
<% end %>
<% else %>
<%= button_to bucket_bubble_pop_path(@bubble.bucket, @bubble), class: "btn" do %>
<%= image_tag "pop.svg", aria: { hidden: true }, size: 24 %>
<span>Pop</span>
<% end %>
<% end %>
</nav>
<section class="flex flex-column align-end gap">
<%= render "bubbles/pop_toggle", bubble: @bubble %>
<%= turbo_frame_tag dom_id(@bubble, :stage_picker), src: new_bucket_bubble_stage_picker_path(@bubble.bucket, @bubble) %>
</section>
</nav>
<% end %>
<div class="bubble__perma flex justify-center center margin-block-end-double">
@@ -0,0 +1,25 @@
<%= turbo_frame_tag dom_id(@bubble, :stage_picker) do %>
<% if @selected_workflow %>
<article class="border txt-large" style="position: absolute; top: 5rem; right: 1rem;" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn btn--plain filter__button" data-action="click->dialog#toggle" data-dialog-modal-value="true">
<%= @selected_workflow.name %>
</button>
<dialog class="filter__popup panel fill-white shadow" data-dialog-target="dialog">
<menu class="filter__menu unpad margin-none">
<% @workflows.each do |workflow| %>
<li><%= link_to_stage_picker @bubble, workflow %></li>
<% end %>
</menu>
</dialog>
<div class="border-top flex flex-column pad gap">
<menu class="filter__menu unpad margin-none">
<% @selected_workflow.stages.each do |stage| %>
<li><%= button_to_set_stage @bubble, stage %></li>
<% end %>
</menu>
</div>
</article>
<% end %>
<% end %>
+26
View File
@@ -0,0 +1,26 @@
<%= turbo_frame_tag workflow do %>
<article class="border">
<div class="flex pad gap justify-space-between">
<%= turbo_frame_tag dom_id(workflow, :header) do %>
<h3 class="txt-nowrap"><%= workflow.name %></h3>
<div class="flex gap">
<%= link_to edit_account_workflow_path(workflow), class: "btn btn--small" do %>
<%= image_tag "pencil.svg", aria: { hidden: true }, size: 24 %>
<% end %>
<%= button_to account_workflow_path(workflow), method: :delete, class: "btn btn--small", data: { turbo_frame: :workflows, turbo_confirm: "Are you sure you want to delete this workflow?" } do %>
<%= image_tag "minus.svg", aria: { hidden: true }, size: 24 %>
<% end %>
</div>
<% end %>
</div>
<div class="border-top flex flex-column pad gap">
<%= turbo_frame_tag dom_id(workflow, :stages) do %>
<%= render partial: "workflows/stages/stage", collection: workflow.stages %>
<%= link_to "New stage", new_account_workflow_stage_path(workflow), class: "btn btn--small" %>
<% end %>
</div>
</article>
<% end %>
+17
View File
@@ -0,0 +1,17 @@
<%= turbo_frame_tag dom_id(@workflow, :header) do %>
<%= form_with model: @workflow, url: account_workflow_path(@workflow) do |form| %>
<div class="flex pad gap justify-space-between">
<%= form.text_field :name, required: true, autofocus: true %>
<div class="flex gap">
<%= form.button type: :submit, class: "btn btn--small" do %>
<%= image_tag "check.svg", aria: { hidden: true }, size: 24 %>
<% end %>
<%= link_to account_workflow_path(@workflow), class: "btn btn--small", data: { turbo_frame: dom_id(@workflow), turbo_confirm: "Are you sure you want to delete this workflow?" } do %>
<%= image_tag "remove.svg", aria: { hidden: true }, size: 24 %>
<% end %>
</div>
</div>
<% end %>
<% end %>
+7
View File
@@ -0,0 +1,7 @@
<%= turbo_frame_tag :workflows do %>
<section class="flex flex-wrap gap">
<%= render partial: "workflows/workflow", collection: @workflows %>
</section>
<%= link_to "New workflow", new_account_workflow_path, class: "btn" %>
<% end %>
+6
View File
@@ -0,0 +1,6 @@
<%= turbo_frame_tag :workflows do %>
<%= form_with model: @workflow, url: account_workflows_path do |form| %>
<%= form.text_field :name, required: true, autofocus: true %>
<%= form.submit %>
<% end %>
<% end %>
+1
View File
@@ -0,0 +1 @@
<%= render "workflows/workflow", workflow: @workflow %>
@@ -0,0 +1,17 @@
<div class="flex pad gap justify-space-between">
<%= turbo_frame_tag stage do %>
<h4 class="txt-nowrap"><%= stage.name %></h4>
<div class="flex gap">
<%= link_to edit_account_workflow_stage_path(stage.workflow, stage), class: "btn btn--small" do %>
<%= image_tag "pencil.svg", aria: { hidden: true }, size: 24 %>
<% end %>
<% if stage_counter >= 1 %>
<%= button_to account_workflow_stage_path(stage.workflow, stage), method: :delete, class: "btn btn--small", data: { turbo_frame: dom_id(stage.workflow), turbo_confirm: "Are you sure you want to delete this stage?" } do %>
<%= image_tag "minus.svg", aria: { hidden: true }, size: 24 %>
<% end %>
<% end %>
</div>
<% end %>
</div>
+17
View File
@@ -0,0 +1,17 @@
<%= turbo_frame_tag @stage do %>
<%= form_with model: @stage, url: account_workflow_stage_path(@stage.workflow, @stage) do |form| %>
<div class="flex pad gap justify-space-between">
<%= form.text_field :name, required: true, autofocus: true %>
<div class="flex gap">
<%= form.button type: :submit, class: "btn btn--small" do %>
<%= image_tag "check.svg", aria: { hidden: true }, size: 24 %>
<% end %>
<%= link_to account_workflow_path(@workflow), class: "btn btn--small", data: { turbo_frame: dom_id(@workflow), turbo_confirm: "Are you sure you want to delete this workflow?" } do %>
<%= image_tag "remove.svg", aria: { hidden: true }, size: 24 %>
<% end %>
</div>
</div>
<% end %>
<% end %>
+6
View File
@@ -0,0 +1,6 @@
<%= turbo_frame_tag dom_id(@stage.workflow, :stages) do %>
<%= form_with model: @stage, url: account_workflow_stages_path(@stage.workflow) do |form| %>
<%= form.text_field :name, required: true, autofocus: true %>
<%= form.submit %>
<% end %>
<% end %>
+18 -12
View File
@@ -8,28 +8,30 @@ Rails.application.routes.draw do
end
end
resolve "Bubble" do |bubble, options|
route_for :bucket_bubble, bubble.bucket, bubble, options
end
resources :buckets do
scope module: :buckets do
resources :views
end
resources :bubbles do
scope module: :bubbles do
resource :image
resource :pop
end
resources :assignments
resources :boosts
resources :comments
resources :tags, shallow: true
scope module: :bubbles do
resource :image
resource :pop
resource :stage_picker
resources :stagings
end
end
resources :tags, only: :index
end
resolve "Bubble" do |bubble, options|
route_for :bucket_bubble, bubble.bucket, bubble, options
scope module: :buckets do
resources :views
end
end
resource :first_run
@@ -41,6 +43,10 @@ Rails.application.routes.draw do
end
end
resources :workflows do
resources :stages, module: :workflows
end
get "join/:join_code", to: "users#new", as: :join
post "join/:join_code", to: "users#create"
get "up", to: "rails/health#show", as: :rails_health_check
@@ -0,0 +1,10 @@
class CreateWorkflows < ActiveRecord::Migration[8.0]
def change
create_table :workflows do |t|
t.references :account, null: false, foreign_key: true
t.string :name, null: false
t.timestamps
end
end
end
@@ -0,0 +1,10 @@
class CreateWorkflowStages < ActiveRecord::Migration[8.0]
def change
create_table :workflow_stages do |t|
t.references :workflow, null: false, foreign_key: true
t.string :name, null: false
t.timestamps
end
end
end
@@ -0,0 +1,5 @@
class AddWorkflowStageToBubbles < ActiveRecord::Migration[8.0]
def change
add_reference :bubbles, :stage, null: true, foreign_key: { to_table: :workflow_stages }
end
end
Generated
+22 -1
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2024_10_17_202003) do
ActiveRecord::Schema[8.0].define(version: 2024_10_18_223027) do
create_table "accesses", force: :cascade do |t|
t.integer "bucket_id", null: false
t.integer "user_id", null: false
@@ -77,7 +77,9 @@ ActiveRecord::Schema[8.0].define(version: 2024_10_17_202003) do
t.date "due_on"
t.integer "bucket_id", null: false
t.integer "boost_count", default: 0, null: false
t.integer "stage_id"
t.index ["bucket_id"], name: "index_bubbles_on_bucket_id"
t.index ["stage_id"], name: "index_bubbles_on_stage_id"
end
create_table "bucket_views", force: :cascade do |t|
@@ -166,14 +168,33 @@ ActiveRecord::Schema[8.0].define(version: 2024_10_17_202003) do
t.index ["email_address"], name: "index_users_on_email_address", unique: true
end
create_table "workflow_stages", force: :cascade do |t|
t.integer "workflow_id", null: false
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["workflow_id"], name: "index_workflow_stages_on_workflow_id"
end
create_table "workflows", force: :cascade do |t|
t.integer "account_id", null: false
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_workflows_on_account_id"
end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "bubbles", "workflow_stages", column: "stage_id"
add_foreign_key "pops", "bubbles"
add_foreign_key "pops", "users"
add_foreign_key "sessions", "users"
add_foreign_key "taggings", "bubbles"
add_foreign_key "taggings", "tags"
add_foreign_key "users", "accounts"
add_foreign_key "workflow_stages", "workflows"
add_foreign_key "workflows", "accounts"
# Virtual tables defined in this database.
# Note that virtual tables may not work with other database engines. Be careful if changing database.
+1 -1
View File
@@ -17,7 +17,7 @@ class BubbleTest < ActiveSupport::TestCase
assert_equal users(:kevin, :jz, :david), bubbles(:logo).assignees
assert_equal users(:david, :kevin), bubbles(:logo).assigners.uniq
assert_equal [ users(:david).id ], bubbles(:logo).events.last.assignee_ids
assert_equal [ "David" ], bubbles(:logo).events.last.assignee_names
assert_equal [ "David" ], bubbles(:logo).events.last.assignees.map(&:name)
end
test "searchable by title" do