Skip to content

Commit 94721a4

Browse files
authored
Add TaskRowComponent extensibility to Strata SDK tasks index (#304)
## Changes > What was added, updated, or removed in this PR. - Introduce Strata::Tasks::TaskRowComponent, following the CaseRowComponent pattern: configurable columns and headers, sortable cell data attributes, and row_classes for host styling. - Strata::TasksController#index now renders strata/tasks/index explicitly and exposes tasks_index_locals (task_row_component_class defaults to the base component) so applications can inject subclasses and pass row options without forking the template. - Add strata.tasks.index.actions_label, update the staff generator to stop emitting a redundant tasks/index wrapper, refresh implementing-tasks-views documentation, and remove the dummy app shim view now that the controller owns the render path. ## Context > Background context, more in-depth details of the implementation, and anything else you'd like to call out or ask reviewers. - The tasks index table is owned by the engine; host apps customize behavior by subclassing Strata::TasksController and overriding tasks_index_locals (e.g. super.merge(task_row_component_class:, task_row_component_options:)). That mirrors how cases use a configurable row component, without requiring a host app/views/tasks/index.html.erb. - For reviewers: Any app that still ships a local tasks/index wrapper should drop it and use tasks_index_locals + a TaskRowComponent subclass instead. ## Testing > Provide evidence that the code works as expected. Explain what was done for testing and the results of the test plan. Include screenshots, [GIF demos](https://www.cockos.com/licecap/), shell commands or output to help show the changes working as expected. ProTip: you can drag and drop or paste images into this textbox. - Component & generator: bundle exec rspec spec/components/strata/tasks/task_row_component_spec.rb spec/lib/generators/strata/generators/staff_generator_spec.rb — passing. - Controller: bundle exec rspec spec/controllers/strata/tasks_controller_spec.rb — passing; one example remains pending (pre-existing skip around routing). - Optional manual check: load the tasks index in the dummy app and confirm the table, filters, and "Pick next task" behave as before; add a screenshot or GIF if your team wants UI proof.
1 parent d81e08f commit 94721a4

14 files changed

Lines changed: 166 additions & 43 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<% self.class.columns.each do |column| %>
2+
<%= tag.td(**cell_html_options(column)) do %>
3+
<%= send(column) || "—" %>
4+
<% end %>
5+
<% end %>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# frozen_string_literal: true
2+
3+
module Strata
4+
module Tasks
5+
# TaskRowComponent renders one row of the tasks index table. Host apps can subclass
6+
# and override {.columns}, {.header_translation_for}, {.header_options_for}, and
7+
# column instance methods to add or reorder columns.
8+
#
9+
# @example Default usage (from +strata/tasks/index+)
10+
# <%= render TaskRowComponent.new(task: task) %>
11+
#
12+
class TaskRowComponent < ViewComponent::Base
13+
def initialize(task:, **)
14+
@task = task
15+
end
16+
17+
def self.columns
18+
%i[due_date type case_id created_date]
19+
end
20+
21+
def self.headers
22+
columns.map { |column| header_translation_for(column) }
23+
end
24+
25+
def self.header_translation_for(column)
26+
I18n.t("strata.tasks.index.columns.col_#{column}")
27+
end
28+
29+
def self.header_options_for(column)
30+
return { aria_sort: "descending" } if column == :created_date
31+
32+
{}
33+
end
34+
35+
def row_classes
36+
nil
37+
end
38+
39+
protected
40+
41+
def due_date
42+
helpers.local_en_us(@task.due_on)
43+
end
44+
45+
def type
46+
link_to I18n.t("tasks.types.#{@task.type.underscore}"),
47+
helpers.url_for(only_path: true, action: :show, id: @task.id.to_s)
48+
end
49+
50+
def case_id
51+
@task.case_id
52+
end
53+
54+
def created_date
55+
helpers.local_en_us(@task.created_at.to_date)
56+
end
57+
58+
def sort_value_for(column)
59+
case column.to_sym
60+
when :due_date
61+
helpers.time_since_epoch(@task.due_on)
62+
when :type
63+
@task.type
64+
when :case_id
65+
@task.case_id
66+
when :created_date
67+
helpers.time_since_epoch(@task.created_at)
68+
else
69+
nil
70+
end
71+
end
72+
73+
def cell_classes(_column)
74+
nil
75+
end
76+
77+
def cell_html_options(column)
78+
attrs = {}
79+
sv = sort_value_for(column)
80+
attrs[:data] = { sort_value: sv } unless sv.nil?
81+
cc = cell_classes(column)
82+
attrs[:class] = cc if cc.present?
83+
attrs
84+
end
85+
end
86+
end
87+
end

app/controllers/strata/tasks_controller.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ module Strata
44
# Controller for managing Strata::Task records. Handles listing, filtering, showing, and updating tasks.
55
# This controller helps a parent application manage tasks by not forcing the parent application to implement the same functionality.
66
class TasksController < ::StaffController
7-
helper DateHelper
8-
97
before_action :set_task, only: %i[ show update ]
108
before_action :set_case, only: %i[ show update ]
119
before_action :set_application_form, only: %i[ show update]
@@ -15,6 +13,7 @@ def index
1513
@task_types = Strata::Task.distinct(:type).unscope(:order).pluck(:type) # Postgres does not support using `order` with `distinct`, thus we have to unscope `order` here.
1614
@tasks = filter_tasks
1715
@unassigned_tasks = Strata::Task.incomplete.unassigned
16+
render "strata/tasks/index", locals: tasks_index_locals
1817
end
1918

2019
def show
@@ -44,6 +43,16 @@ def pick_up_next_task
4443

4544
protected
4645

46+
def tasks_index_locals
47+
{
48+
tasks: @tasks,
49+
task_types: @task_types,
50+
unassigned_tasks: @unassigned_tasks,
51+
task_row_component_class: Strata::Tasks::TaskRowComponent,
52+
task_row_component_options: {}
53+
}
54+
end
55+
4756
def set_task
4857
@task = Strata::Task.find(params[:id]) if params[:id].present?
4958
end

app/helpers/strata/application_helper.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ module Strata
77
# @see Strata::FormBuilder for more information about available form helpers
88
#
99
module ApplicationHelper
10+
include DateHelper
11+
1012
def strata_form_with(model: false, scope: nil, url: nil, format: nil, **options, &block)
1113
options[:builder] = Strata::FormBuilder
1214
args = { scope: scope, url: url, format: format, **options }

app/views/strata/tasks/index.html.erb

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
# @param tasks [Array<Strata::Task>] Array of tasks to display
44
# @param unassigned_tasks [Array<Strata::Task>] Array of unassigned tasks for button state
55
# @param task_types [Array<String>] Array of task types for filtering
6+
# @param task_row_component_class [Class] ViewComponent subclass of {Strata::Tasks::TaskRowComponent}
7+
# @param task_row_component_options [Hash] Extra keyword arguments passed to each row component
68
#
79
# @example Task types array
810
# ["Strata::Task", "Strata::OtherTask", "MyCustomTask"]
@@ -13,6 +15,8 @@
1315
# - strata.tasks.index.tabs.completed: Label for the completed tasks tab
1416
#
1517
%>
18+
<% task_row_component_class = local_assigns.fetch(:task_row_component_class, Strata::Tasks::TaskRowComponent) %>
19+
<% task_row_component_options = local_assigns.fetch(:task_row_component_options, {}) %>
1620
<!-- Main content -->
1721
<div class="grid-container">
1822
<h1 class="usa-heading-xl"><%= t("strata.tasks.index.title") %></h1>
@@ -31,7 +35,7 @@
3135
<%= render partial: 'strata/tasks/due_date_filter' %>
3236
</div>
3337
<div class="grid-col-12 tablet:grid-col-3">
34-
<label class="usa-label" for="next-task-button" style="margin-bottom: 0.5rem">Actions:</label>
38+
<label class="usa-label" for="next-task-button" style="margin-bottom: 0.5rem"><%= t("strata.tasks.index.actions_label") %></label>
3539
<%= button_to t("strata.tasks.actions.pick_next"), url_for(only_path: true, action: :pick_up_next_task),
3640
id: "next-task-button",
3741
disabled: !unassigned_tasks&.any?,
@@ -44,19 +48,16 @@
4448
<!-- Tasks -->
4549
<%= render Strata::US::TableComponent.new(striped: true, sticky_header: true, sortable: true, classes: "width-full") do |table| %>
4650
<% table.with_caption do %><span class="usa-sr-only"><%= t("strata.tasks.index.title") %></span><% end %>
47-
<% table.with_header(sortable: true) { t("strata.tasks.index.columns.col_due_date") } %>
48-
<% table.with_header(sortable: true) { t("strata.tasks.index.columns.col_type") } %>
49-
<% table.with_header(sortable: true) { t("strata.tasks.index.columns.col_case_id") } %>
50-
<% table.with_header(sortable: true, aria_sort: "descending") { t("strata.tasks.index.columns.col_created_date") } %>
51+
<% task_row_component_class.columns.each do |column| %>
52+
<% table.with_header(sortable: true, **task_row_component_class.header_options_for(column)) do %>
53+
<%= task_row_component_class.header_translation_for(column) %>
54+
<% end %>
55+
<% end %>
5156

5257
<% tasks.each do |task| %>
53-
<% table.with_row do |row| %>
54-
<% row.with_cell(sort_value: time_since_epoch(task.due_on)) { local_en_us(task.due_on) } %>
55-
<% row.with_cell(sort_value: task.type) do %>
56-
<%= link_to t("tasks.types.#{task.type.underscore}"), url_for(only_path: true, action: :show, id: task.id.to_s) %>
57-
<% end %>
58-
<% row.with_cell(sort_value: task.case_id) { task.case_id } %>
59-
<% row.with_cell(sort_value: time_since_epoch(task.created_at)) { local_en_us(task.created_at.to_date) } %>
58+
<% row_component = task_row_component_class.new(task: task, **task_row_component_options) %>
59+
<% table.with_row(classes: row_component.row_classes) do %>
60+
<%= render row_component %>
6061
<% end %>
6162
<% end %>
6263
<% end %>

config/locales/strata/en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ en:
6969
tasks: "Tasks"
7070
index:
7171
title: "Tasks"
72+
actions_label: "Actions:"
7273
tabs:
7374
assigned: "Assigned"
7475
completed: "Completed"

docs/implementing-tasks-views.md

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,40 @@
11
# Implementing Task Views
22

3-
This guide explains how to implement and customize the task views in the Strata engine, with a focus on the `index.html.erb` template.
3+
This guide explains how to implement and customize the task views in the Strata engine, with a focus on the tasks index.
44

55
## Quick Start Implementation
66

7-
To implement the tasks index view in your application, create an `index.html.erb` file in your views directory with the following content:
7+
`Strata::TasksController#index` renders the engine template `strata/tasks/index` and passes locals from `tasks_index_locals`. Host applications normally **do not** add `app/views/tasks/index.html.erb`; override `tasks_index_locals` (or `index`) in your `TasksController` subclass to customize behavior.
88

9-
```erb
10-
<%= render template: 'strata/tasks/index', locals: {
11-
tasks: @tasks,
12-
task_types: @task_types,
13-
unassigned_tasks: @unassigned_tasks
14-
} %>
15-
```
9+
### Default locals
10+
11+
The engine supplies:
1612

17-
### Required Local Variables
13+
- `tasks`, `task_types`, `unassigned_tasks` (from instance variables)
14+
- `task_row_component_class` (defaults to `Strata::Tasks::TaskRowComponent`)
15+
- `task_row_component_options` (defaults to `{}`)
1816

19-
The view requires the following local variables to be passed:
17+
### Custom task columns
2018

21-
- `tasks`: An array of `Strata::Task` objects to display in the list
22-
- `task_types`: An array of available task types for filtering
23-
- `unassigned_tasks`: An array of `Strata::Task` objects used to determine the state of the "Pick Next Task" button
19+
Subclass `Strata::Tasks::TaskRowComponent` and pass your class in `tasks_index_locals`, following the same pattern as `Strata::Cases::CaseRowComponent` for the cases index.
2420

25-
### Controller Setup
21+
### Controller setup
2622

27-
In your controller, ensure you have the following instance variables set:
23+
Override `tasks_index_locals` when you need extra row options or a custom row component:
2824

2925
```ruby
30-
def index
31-
@tasks = Strata::Task.where(assignee_id: current_user.id) # or your task retrieval logic
32-
@task_types = ["MyCustomTask", "OtherTask", Strata::Task.name]
33-
@unassigned_tasks = Strata::Task.where(assignee_id: nil) # or your unassigned tasks logic
26+
protected
27+
28+
def tasks_index_locals
29+
super.merge(
30+
task_row_component_class: MyApp::TaskRowComponent,
31+
task_row_component_options: { my_data: @my_data }
32+
)
3433
end
3534
```
3635

36+
The default `Strata::TasksController#index` sets `@task_types`, `@tasks`, and `@unassigned_tasks` before rendering.
37+
3738
## Internationalization (i18n)
3839

3940
The view uses several translation keys that you can override in your application:

lib/generators/strata/staff/USAGE

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ This will create:
1414

1515
Views:
1616
app/views/staff/index.html.erb - Staff dashboard index page
17-
app/views/tasks/index.html.erb - Tasks listing page that renders strata/tasks/index template
1817
app/views/tasks/show.html.erb - Task detail page that renders strata/tasks/show template
1918

2019
Tests:
@@ -31,6 +30,6 @@ This will create:
3130
Notes:
3231
- The generated staff controller includes TODO comments for implementing authentication and authorization
3332
- The tasks controller includes TODO comments for setting up case and application form relationships
34-
- The generated views render existing strata/tasks templates with appropriate locals
33+
- Strata::TasksController renders the tasks index from the engine; override tasks_index_locals to customize row components
3534
- You'll need to customize the case_classes method in the staff controller to match your application's case models
3635
- The generated spec file includes TODO placeholders for implementing comprehensive tests

lib/generators/strata/staff/staff_generator.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ def create_tasks_controller
2020

2121
def create_views
2222
template "staff_index.html.erb", "app/views/staff/index.html.erb"
23-
template "tasks_index.html.erb", "app/views/tasks/index.html.erb"
2423
template "task_show.html.erb", "app/views/tasks/show.html.erb"
2524
end
2625

lib/generators/strata/staff/templates/tasks_index.html.erb

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)