Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 43 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,24 @@ If you are new to the terminology, see:
- Transition actions presented as buttons per step option
- Permission model aligned to native Discourse category permissions
- Workflow discovery list (`/workflow`) with quick filters
- Kanban view toggle for compatible single-workflow lists
- Workflow view selector in discovery (`List`, `Kanban`, `Chart`) when applicable
- Kanban view for compatible single-workflow lists
- Drag/drop transitions in Kanban with legal/illegal drop-zone highlighting
- Keyboard Kanban transitions on focused cards (`ArrowLeft` / `ArrowRight`) when legal
- Workflow-level Kanban tags toggle (`show_kanban_tags`, default `true`)
- Workflow burn down chart view (`/workflow/charts` and chart mode in `/workflow`) for single-workflow context
- Chart period selector (`1` to `12` weeks), complete-week windows (Sunday to Saturday), per-step colored series
- Overdue behavior with hierarchy:
- global default (`workflow_overdue_days_default`)
- workflow override
- step override
- `0` disables overdue behavior at that scope
- Workflow overdue indicator column in the workflow topic list
- Stale state transition handling with explicit user-facing error and automatic refresh
- Transition audit trail via small action posts
- Workflow visualization modal from topic and list links
- Data Explorer audit query support
- Data Explorer workflow stats query support for chart-oriented time series
- Optional AI-assisted step handling with prompt + option guardrails

## Quickstart
Expand All @@ -41,8 +46,9 @@ If you are new to the terminology, see:
2. Go to `Admin -> Plugins -> Discourse Workflow`, create a Workflow, then save it.
3. Add Workflow Steps (Categories in journey order), then add Step Options (actions/transitions).
4. Create a Topic in the first step Category and transition it through actions from the topic banner.
5. Use `/workflow` to view queue state, apply quick filters, toggle `List`/`Kanban`, and visualize progress.
5. Use `/workflow` to view queue state, apply quick filters, and switch between `List` / `Kanban` / `Chart` views when available.
6. In Kanban, click a card to open the topic, drag cards to legal target steps, or use keyboard arrows on focused cards.
7. Use `/workflow/charts` (or `Chart` view in `/workflow`) to see step counts over time for the currently scoped single workflow.

## Setup

Expand All @@ -53,6 +59,7 @@ If you are new to the terminology, see:
- `workflow_openai_api_key`: API key for AI actions.
- `workflow_ai_model`: model used for AI actions.
- `workflow_ai_prompt_system`: system prompt support for AI transitions.
- `workflow_charts_allowed_groups`: non-admin groups allowed to view workflow charts.

### Workflow definition setup

Expand Down Expand Up @@ -93,6 +100,35 @@ Compatibility requires:

For each directed edge, Kanban drag/keyboard transitions are option-agnostic and deterministic.

### Chart view behavior and access model

Chart view is shown when the current workflow discovery context resolves to a single workflow.

- Route support: `/workflow/charts` and chart mode in `/workflow` via `workflow_view=chart`
- View selector behavior: `Chart` is only shown when the user can view charts and the current discovery context is a single workflow
- Period selection: `1` to `12` weeks
- Time windows: complete weeks (Sunday through Saturday)
- Series: one line per step, color derived from step category color (or parent category color fallback)
- Response scope: chart payload includes selected workflow metadata (`selected_workflow_id`, `selected_workflow_name`) plus series data for that selected workflow context

Access model for charts is intentionally separate from topic-level category access:

- Admins can always view charts
- Users in `workflow_charts_allowed_groups` can view charts
- Chart access is aggregate and workflow-level; it is intentionally not constrained to per-topic visibility rules
- This allows operational/reporting audiences to monitor workflow throughput without granting direct access to every underlying topic

If you want stricter chart data visibility, keep `workflow_charts_allowed_groups` empty and rely on admin-only access.

### Background jobs

The plugin schedules and runs the following jobs:

- `Jobs::DiscourseWorkflow::DailyStats`: records daily workflow step counts
- `Jobs::DiscourseWorkflow::AiTransitions`: runs AI-enabled transitions
- `Jobs::DiscourseWorkflow::DataExplorerQueriesCompleteness`: ensures default workflow Data Explorer queries exist
- `Jobs::DiscourseWorkflow::TopicArrivalNotifier`: sends first-post arrival notifications on workflow transitions

### AI actions

You can leverage AI to handle a step. You need `workflow_openai_api_key`, AI enabled on the step, and a prompt including both `{{options}}` and `{{topic}}`. You can also tune behavior with `workflow_ai_model` and `workflow_ai_prompt_system`.
Expand Down Expand Up @@ -131,7 +167,11 @@ Actions on a Topic are captured in a Small Action Post to help users understand

## Dashboard

A Topic Discovery filter `Workflow` gives a list of workflow instances (special workflow topics).
A Topic Discovery filter `Workflow` gives a list of workflow instances (special workflow topics), with three presentation modes when available:

- `List`: sortable workflow topic list with workflow columns and quick filters
- `Kanban`: actionable card board for compatible single-workflow views
- `Chart`: step count trends over time for a single workflow

You should keep Workflow Categories and ideally tags distinct, so you can also use those to filter for all workflow instances that are at a particular stage, or have a specific tag.

Expand Down
101 changes: 101 additions & 0 deletions app/controllers/discourse_workflow/workflow_charts_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# frozen_string_literal: true

module DiscourseWorkflow
class WorkflowChartsController < ApplicationController
requires_plugin ::DiscourseWorkflow::PLUGIN_NAME

before_action :ensure_logged_in
before_action :ensure_workflow_enabled
before_action :ensure_can_view_charts

def index
weeks = normalized_weeks
workflows =
::DiscourseWorkflow::Workflow
.where(enabled: true)
.ordered
.includes(workflow_steps: { category: :parent_category })
selected_workflow = selected_workflow_for(workflows)
date_range = chart_date_range(weeks)

render_json_dump(
{
weeks: weeks,
labels: date_range.map(&:iso8601),
range_start: date_range.first.iso8601,
range_end: date_range.last.iso8601,
selected_workflow_id: selected_workflow&.id,
selected_workflow_name: selected_workflow&.name,
series: build_series(selected_workflow, date_range),
},
)
end

private

def ensure_workflow_enabled
raise Discourse::NotFound if !SiteSetting.workflow_enabled
end

def ensure_can_view_charts
return if ::DiscourseWorkflow::ChartsPermissions.can_view?(current_user)

raise Discourse::InvalidAccess.new(
nil,
nil,
custom_message: "discourse_workflow.errors.charts_access_denied",
)
end

def normalized_weeks
return 2 if params[:weeks].blank?

requested = params[:weeks].to_i
requested = 1 if requested <= 0
[requested, 12].min
end

def selected_workflow_for(workflows)
selected_id = params[:workflow_id].to_i
workflows.find { |workflow| workflow.id == selected_id } || workflows.first
end

def chart_date_range(weeks)
end_date = Date.current.end_of_week(:saturday)
start_date = end_date - ((weeks * 7) - 1).days
(start_date..end_date).to_a
end

def build_series(workflow, date_range)
return [] if workflow.blank?

steps = workflow.workflow_steps.sort_by { |step| step.position.to_i }
return [] if steps.blank?

step_ids = steps.map(&:id)
stats =
::DiscourseWorkflow::WorkflowStat
.where(workflow_id: workflow.id, workflow_step_id: step_ids)
.where(
cob_date: date_range.first.beginning_of_day..date_range.last.end_of_day,
)
.group("DATE(cob_date)", :workflow_step_id)
.sum(:count)
counts_by_day_step =
stats.each_with_object({}) do |((date_value, step_id), count), memo|
memo[[date_value.to_date, step_id]] = count
end

steps.map do |step|
category = step.category
{
step_id: step.id,
step_name: step.name,
step_position: step.position.to_i,
color: category&.color || category&.parent_category&.color,
data: date_range.map { |date| counts_by_day_step.fetch([date, step.id], 0) },
}
end
end
end
end
13 changes: 13 additions & 0 deletions app/jobs/regular/discourse_workflow/topic_arrival_notifier.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module Jobs
module DiscourseWorkflow
class TopicArrivalNotifier < ::Jobs::Base
def execute(args)
return unless topic = Topic.find_by(id: args[:topic_id])

PostAlerter.new.after_save_post(topic.first_post, true)
end
end
end
end
11 changes: 0 additions & 11 deletions app/jobs/regular/workflow_topic_arrival_notifier.rb

This file was deleted.

15 changes: 15 additions & 0 deletions app/jobs/scheduled/discourse_workflow/ai_transitions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module Jobs
module DiscourseWorkflow
class AiTransitions < ::Jobs::Scheduled
sidekiq_options retry: false

every 1.hour

def execute(args = {})
::DiscourseWorkflow::AiActions.new.transition_all
end
end
end
end
15 changes: 15 additions & 0 deletions app/jobs/scheduled/discourse_workflow/daily_stats.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module Jobs
module DiscourseWorkflow
class DailyStats < ::Jobs::Scheduled
sidekiq_options retry: false

every 24.hours

def execute(args = {})
::DiscourseWorkflow::Stats.new.calculate_daily_stats
end
end
end
end
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
# frozen_string_literal: true

class ::Jobs::WorkflowDataExplorerQueriesCompleteness < ::Jobs::Scheduled
sidekiq_options retry: false
module Jobs
module DiscourseWorkflow
class DataExplorerQueriesCompleteness < ::Jobs::Scheduled
sidekiq_options retry: false

every 24.hours
every 24.hours

def execute(args)
if !ActiveRecord::Base.connection.table_exists?(:data_explorer_queries)
Rails.logger.warn "Skipping WorkflowDataExplorerQueriesCompleteness: doesn't look like Data Explorer plugin is properly installed"
return
end
def execute(args = {})
if !ActiveRecord::Base.connection.table_exists?(:data_explorer_queries)
Rails.logger.warn "Skipping DataExplorerQueriesCompleteness: doesn't look like Data Explorer plugin is properly installed"
return
end

if !::DiscourseDataExplorer::Query.exists?(name: "Workflow Stats (default)")
query_sql = <<~SQL
if !::DiscourseDataExplorer::Query.exists?(name: "Workflow Stats (default)")
query_sql = <<~SQL
-- [params]
-- int :workflow_id = 1
-- int :num_of_days_history = 14
Expand All @@ -28,7 +30,7 @@ def execute(args)
AND wstt.workflow_id = :workflow_id
SQL

DB.exec <<~SQL, now: Time.zone.now, query_sql: query_sql
DB.exec <<~SQL, now: Time.zone.now, query_sql: query_sql
INSERT INTO data_explorer_queries(name, description, sql, created_at, updated_at)
VALUES
('Workflow Stats (default)',
Expand All @@ -37,12 +39,12 @@ def execute(args)
:now,
:now)
SQL
end
end

if !::DiscourseDataExplorer::Query.exists?(
name: "Workflow Audit Log (default)"
)
query_sql = <<~SQL
if !::DiscourseDataExplorer::Query.exists?(
name: "Workflow Audit Log (default)"
)
query_sql = <<~SQL
-- [params]
-- int :workflow_id = 1
-- int :num_of_days_history = 14
Expand All @@ -58,7 +60,7 @@ def execute(args)
AND workflow_id = :workflow_id
SQL

DB.exec <<~SQL, now: Time.zone.now, query_sql: query_sql
DB.exec <<~SQL, now: Time.zone.now, query_sql: query_sql
INSERT INTO data_explorer_queries(name, description, sql, created_at, updated_at)
VALUES
('Workflow Audit Log (default)',
Expand All @@ -67,6 +69,8 @@ def execute(args)
:now,
:now)
SQL
end
end
end
end
end
11 changes: 0 additions & 11 deletions app/jobs/scheduled/workflow_ai_transitions.rb

This file was deleted.

11 changes: 0 additions & 11 deletions app/jobs/scheduled/workflow_daily_stats.rb

This file was deleted.

4 changes: 2 additions & 2 deletions app/models/discourse_workflow/workflow_stat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
module ::DiscourseWorkflow
class WorkflowStat < ActiveRecord::Base
self.table_name = 'workflow_stats'
has_one :workflow
has_one :workflow_step
belongs_to :workflow
belongs_to :workflow_step
validates :cob_date, presence: true
validates :workflow_id, presence: true
validates :workflow_step_id, presence: true
Expand Down
Loading