Skip to content

Instantly share code, notes, and snippets.

@julianrubisch
Created April 21, 2021 12:22
Show Gist options
  • Save julianrubisch/923c15008fa06c7bc3d620193ef9fca7 to your computer and use it in GitHub Desktop.
Save julianrubisch/923c15008fa06c7bc3d620193ef9fca7 to your computer and use it in GitHub Desktop.
# app/helpers/application_helper.rb
module ApplicationHelper
def onboarding_step_path(workflow)
board = workflow.user.boards&.first
case workflow.next_step
when :create_board
new_board_path
when :create_embed
board_path(board)
when :create_comment
board_embed_path(board.id, board.embeds&.first)
else
"#"
end
end
end
<!-- app/views/boards/index.html.erb -->
<div class="md:flex md:items-center md:justify-between md:space-x-2">
<!-- ... -->
<%= link_to new_board_path, class: "..." do %>
<%= render WorkflowStepIndicatorComponent.new(active: current_user.may_progress_to?("board")) %>
<i class="fas fa-plus flex-shrink-0 mr-1.5 h-5 w-5"></i>
<span>Add Board</span>
<% end %>
</div>
# app/reflexes/onboarding_reflex.rb
class OnboardingReflex < ApplicationReflex
def skip
workflow = current_user.onboarding_workflow
next_step = workflow.aasm.events.first.name # :create_board etc.
workflow.send "#{next_step}!" # use bang method to persist
end
end
# app/models/onboarding_workflow.rb
class OnboardingWorkflow < ApplicationRecord
include AASM
belongs_to :user
aasm do
state :registered, initial: true
state :board_created
state :embed_created
state :comment_created
state :finished
event :create_board do
transitions from: :registered, to: :board_created
end
event :create_embed do
transitions from: :board_created, to: :embed_created
end
event :create_comment do
transitions from: :embed_created, to: :comment_created
end
event :finish do
transitions from: :comment_created, to: :finished
end
end
end
# app/models/user.rb
class User < ApplicationRecord
# ...
has_one :onboarding_workflow
# ...
end
# app/models/onboarding_workflow.rb
class OnboardingWorkflow < ApplicationRecord
include AASM
belongs_to :user
aasm do
state :registered,
initial: true,
display: "Welcome! You can create a board by clicking on <strong>Add Board</strong>".html_safe
state :board_created,
display: "Success! Now click on your board and create an <strong>embed</strong>".html_safe
state :embed_created,
display: "Great! You can start a conversation about this embed by creating a <strong>comment</strong>".html_safe
state :comment_created,
display: "All done! Congratulations!"
state :finished
# ...
end
end
# app/models/onboarding_workflow.rb
class OnboardingWorkflow < ApplicationRecord
# ...
after_save_commit do
StreamWorkflowJob.perform_now(user: user)
end
end
# app/models/onboarding_workflow
class OnboardingWorkflow < ApplicationRecord
# ...
def next_step
aasm.events.first.name # all triggerable events from the current state, i.e. :create_board etc.
end
end
# db/migrate/....create_onboarding_workflows.rb
class CreateOnboardingWorkflows < ActiveRecord::Migration[6.1]
def change
create_table :onboarding_workflows do |t|
t.references :user, null: false, foreign_key: true
t.string :aasm_state
t.timestamps
end
end
end
# app/jobs/stream_workflow_job.rb
class StreamWorkflowJob < ApplicationJob
include CableReady::Broadcaster
queue_as :default
def perform(user:)
cable_ready[WorkflowChannel].outer_html(
selector: "#workflow-banner",
html: ApplicationController.render(partial: "workflows/workflow_banner", locals: {user: user})
).broadcast_to(user)
end
end
# app/models/user.rb
class User < ApplicationRecord
# ...
%i(create_board create_embed create_comment finish may_finish? may_create_board? may_create_embed? may_create_comment?).each do |event|
delegate event, to: :onboarding_workflow
end
# ...
def may_progress_to?(to)
self.send("may_create_#{to}?")
end
end
<% if user && !user.onboarding_workflow.finished? %>
<div class="fixed inset-x-0 bottom-0" id="workflow-banner">
<div class="bg-lime-700">
<div class="max-w-7xl mx-auto py-3 px-3 sm:px-6 lg:px-8">
<div class="flex items-center justify-between flex-wrap">
<div class="w-0 flex-1 flex items-center">
<p class="ml-3 font-normal text-white truncate">
<span>
<%= user.onboarding_workflow.aasm.human_state %>
</span>
</p>
</div>
<div class="order-3 mt-2 flex-shrink-0 w-full sm:order-2 sm:mt-0 sm:w-auto">
<button data-reflex="click->Onboarding#skip" class="flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-lime-600 bg-white hover:bg-lime-50">
<% if user.may_finish? %>
Finish
<% else %>
Skip
<% end %>
</button>
</div>
</div>
</div>
</div>
</div>
<% end %>
<% unless user.may_finish? %>
<%= link_to "Take me there", onboarding_step_path(user.onboarding_workflow), class: "flex items-center justify-center px-4 py-2 border border-white rounded-md text-sm font-medium text-white" %>
<% end %>
<button data-reflex="click->Onboarding#skip" class="flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-lime-600 bg-white hover:bg-lime-50">
<% if user.may_finish? %>
Finish
<% else %>
Skip
<% end %>
</button>
<!-- app/components/workflow_step_indicator_component.html.erb -->
<span class="absolute top-0 right-0 flex h-3 w-3 -mr-1 -mt-1">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-lime-800 opacity-75"></span>
<span class="relative inline-flex rounded-full h-3 w-3 bg-lime-400"></span>
</span>
# app/components/workflow_step_indicator_component.rb
class WorkflowStepIndicatorComponent < ViewComponent::Base
def initialize(active:)
@active = active
end
def render?
@active
end
end
# app/models/concerns/workflowable.rb
module Workflowable
extend ActiveSupport::Concern
included do
after_save_commit :progress_workflow
end
def progress_workflow
return unless user.reload.may_progress_to?(model_name.singular) && users&.one?
user.onboarding_workflow.send("create_#{model_name.singular}!") # bang persists
end
def user
super
rescue NoMethodError
users.first
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment