Skip to content

Commit 5773a5b

Browse files
committed
tasks can be filtered by namespace
1 parent b7cce0a commit 5773a5b

File tree

8 files changed

+260
-26
lines changed

8 files changed

+260
-26
lines changed

app/controllers/maintenance_tasks/application_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class ApplicationController < MaintenanceTasks.parent_controller.constantize
1111
policy.style_src_elem(
1212
BULMA_CDN,
1313
# <style> tag in app/views/layouts/maintenance_tasks/application.html.erb
14-
"'sha256-WHHDQLdkleXnAN5zs0GDXC5ls41CHUaVsJtVpaNx+EM='",
14+
"'sha256-bPoBNSY4OPj4PVy3JR+57IqkZNesZ1sKbHQAhkSThxQ='",
1515
)
1616
capybara_lockstep_scripts = [
1717
"'sha256-1AoN3ZtJC5OvqkMgrYvhZjp4kI8QjJjO7TAyKYiDw+U='",

app/controllers/maintenance_tasks/tasks_controller.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,17 @@ class TasksController < ApplicationController
1212
# Renders the maintenance_tasks/tasks page, displaying
1313
# available tasks to users, grouped by category.
1414
def index
15-
@available_tasks = TaskDataIndex.available_tasks.group_by(&:category)
15+
all_tasks = TaskDataIndex.available_tasks
16+
@namespaces = helpers.extract_namespaces(all_tasks)
17+
@selected_namespace = params[:namespace]
18+
19+
filtered_tasks = if @selected_namespace.present?
20+
all_tasks.select { |task| task.name.start_with?("#{@selected_namespace}::") }
21+
else
22+
all_tasks
23+
end
24+
25+
@available_tasks = filtered_tasks.group_by(&:category)
1626
end
1727

1828
# Renders the page responsible for providing Task actions to users.

app/helpers/maintenance_tasks/tasks_helper.rb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,5 +199,55 @@ def attribute_required?(task, parameter_name)
199199
validator.kind == :presence
200200
end
201201
end
202+
203+
# Extracts unique namespaces from a collection of tasks.
204+
#
205+
# @param tasks [Array] collection of task objects with a name method.
206+
# @return [Array<String>] sorted unique namespaces.
207+
def extract_namespaces(tasks)
208+
tasks
209+
.filter_map { |task| task.name.deconstantize.presence }
210+
.uniq
211+
.sort
212+
end
213+
214+
# Builds a nested tree structure from a flat list of namespaces.
215+
#
216+
# @param namespaces [Array<String>] flat list of namespaces.
217+
# @return [Hash] nested tree structure.
218+
def build_namespace_tree(namespaces)
219+
namespaces.each_with_object({}) do |namespace, tree|
220+
parts = namespace.split("::")
221+
parts.each_with_index.inject(tree) do |current, (part, index)|
222+
full_path = parts[0..index].join("::")
223+
current[part] ||= { name: part, full_path: full_path, children: {} }
224+
current[part][:children]
225+
end
226+
end
227+
end
228+
229+
# Renders a namespace tree node with nested children using details/summary.
230+
#
231+
# @param node [Hash] the node to render { name:, full_path:, children: }.
232+
# @param selected_namespace [String, nil] the currently selected namespace.
233+
# @return [ActiveSupport::SafeBuffer] HTML for the node.
234+
def render_namespace_node(node, selected_namespace)
235+
is_active = selected_namespace == node[:full_path]
236+
is_expanded = is_active || selected_namespace&.start_with?("#{node[:full_path]}::")
237+
link = link_to(node[:name], tasks_path(namespace: node[:full_path]), class: class_names("is-active" => is_active))
238+
239+
content_tag(:li) do
240+
if node[:children].present?
241+
content_tag(:details, open: is_expanded || nil) do
242+
content_tag(:summary) { link } +
243+
content_tag(:ul) do
244+
safe_join(node[:children].values.sort_by { |n| n[:name] }.map { |child| render_namespace_node(child, selected_namespace) })
245+
end
246+
end
247+
else
248+
link
249+
end
250+
end
251+
end
202252
end
203253
end

app/views/layouts/maintenance_tasks/application.html.erb

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,69 @@
6060
color: #ff6685;
6161
font-size: 12px;
6262
}
63+
64+
/* Sidebar styles */
65+
.sidebar {
66+
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
67+
border-radius: 8px;
68+
border-left: 3px solid #485fc7;
69+
}
70+
.sidebar .menu-list a {
71+
border-radius: 4px;
72+
padding: 0.5em 0.75em;
73+
color: #363636;
74+
transition: all 0.15s ease;
75+
font-size: 0.9rem;
76+
}
77+
.sidebar .menu-list a:hover {
78+
background-color: #e8e8e8;
79+
color: #485fc7;
80+
}
81+
.sidebar .menu-list a.is-active {
82+
background-color: #485fc7;
83+
color: #fff;
84+
font-weight: 600;
85+
}
86+
.sidebar .menu-list li {
87+
margin-bottom: 2px;
88+
}
89+
/* Nested namespace styles */
90+
.sidebar .menu-list details {
91+
margin: 0;
92+
}
93+
.sidebar .menu-list details > summary {
94+
list-style: none;
95+
cursor: pointer;
96+
}
97+
.sidebar .menu-list details > summary::before {
98+
content: none;
99+
display: none;
100+
}
101+
.sidebar .menu-list details > summary::-webkit-details-marker,
102+
.sidebar .menu-list details > summary::marker {
103+
display: none;
104+
content: '';
105+
}
106+
.sidebar .menu-list details > summary a {
107+
display: inline-block;
108+
}
109+
.sidebar .menu-list details > summary > a::before {
110+
content: '▶';
111+
font-size: 0.5rem;
112+
display: inline-block;
113+
width: 1rem;
114+
position: static;
115+
transition: transform 0.15s ease;
116+
}
117+
.sidebar .menu-list details[open] > summary > a::before {
118+
transform: rotate(90deg);
119+
}
120+
.sidebar .menu-list details > ul {
121+
margin-left: 1rem;
122+
margin-top: 2px;
123+
padding-left: 0.5rem;
124+
border-left: 1px solid #dbdbdb;
125+
}
63126
</style>
64127

65128
<script>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<h3 class="title is-4 has-text-weight-bold">Namespaces</h3>
2+
<aside class="menu sidebar">
3+
<ul class="menu-list">
4+
<li>
5+
<%= link_to "All Tasks", tasks_path, class: class_names("is-active" => @selected_namespace.blank?) %>
6+
</li>
7+
<% build_namespace_tree(@namespaces).values.sort_by { |n| n[:name] }.each do |node| %>
8+
<%= render_namespace_node(node, @selected_namespace) %>
9+
<% end %>
10+
</ul>
11+
</aside>
12+
Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,51 @@
11
<%= tag.div(data: { refresh: (defined?(@refresh) && @refresh) || "" }) do %>
2-
<% if @available_tasks.empty? %>
3-
<div class="content is-large">
4-
<h3 class="title is-3"> The MaintenanceTasks gem has been successfully installed! </h3>
5-
<p>
6-
Any new Tasks will show up here. To start writing your first Task,
7-
run <code>bin/rails generate maintenance_tasks:task my_task</code>.
8-
</p>
9-
</div>
10-
<% else %>
11-
<% if active_tasks = @available_tasks[:active] %>
12-
<h3 class="title is-4 has-text-weight-bold">Active Tasks</h3>
13-
<%= render partial: 'task', collection: active_tasks %>
14-
<% end %>
15-
<% if new_tasks = @available_tasks[:new] %>
16-
<h3 class="title is-4 has-text-weight-bold">New Tasks</h3>
17-
<div class="grid is-col-min-20">
18-
<%= render partial: 'task', collection: new_tasks %>
19-
</div>
2+
<div class="columns">
3+
<% if @namespaces.present? %>
4+
<div class="column is-one-quarter">
5+
<%= render 'sidebar' %>
6+
</div>
207
<% end %>
21-
<% if completed_tasks = @available_tasks[:completed] %>
22-
<h3 class="title is-4 has-text-weight-bold">Completed Tasks</h3>
23-
<%= render partial: 'task', collection: completed_tasks %>
24-
<% end %>
25-
<% end %>
8+
<div class="column">
9+
<% if @selected_namespace.present? %>
10+
<div class="mb-4">
11+
<span class="tag is-medium is-info">
12+
<%= @selected_namespace %>
13+
<%= link_to "×", tasks_path, class: "delete is-small ml-2" %>
14+
</span>
15+
</div>
16+
<% end %>
17+
18+
<% if @available_tasks.empty? %>
19+
<div class="content is-large">
20+
<% if @selected_namespace.present? %>
21+
<h3 class="title is-3">No tasks found in <%= @selected_namespace %></h3>
22+
<p>
23+
<%= link_to "View all tasks", tasks_path %>
24+
</p>
25+
<% else %>
26+
<h3 class="title is-3"> The MaintenanceTasks gem has been successfully installed! </h3>
27+
<p>
28+
Any new Tasks will show up here. To start writing your first Task,
29+
run <code>bin/rails generate maintenance_tasks:task my_task</code>.
30+
</p>
31+
<% end %>
32+
</div>
33+
<% else %>
34+
<% if active_tasks = @available_tasks[:active] %>
35+
<h3 class="title is-4 has-text-weight-bold">Active Tasks</h3>
36+
<%= render partial: 'task', collection: active_tasks %>
37+
<% end %>
38+
<% if new_tasks = @available_tasks[:new] %>
39+
<h3 class="title is-4 has-text-weight-bold">New Tasks</h3>
40+
<div class="grid is-col-min-20">
41+
<%= render partial: 'task', collection: new_tasks %>
42+
</div>
43+
<% end %>
44+
<% if completed_tasks = @available_tasks[:completed] %>
45+
<h3 class="title is-4 has-text-weight-bold">Completed Tasks</h3>
46+
<%= render partial: 'task', collection: completed_tasks %>
47+
<% end %>
48+
<% end %>
49+
</div>
50+
</div>
2651
<% end %>

test/helpers/maintenance_tasks/tasks_helper_test.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,41 @@ class TasksHelperTest < ActionView::TestCase
149149
assert_not attribute_required?(task, :content)
150150
end
151151

152+
test "#extract_namespaces returns sorted unique namespaces" do
153+
task_struct = Struct.new(:name)
154+
tasks = [
155+
task_struct.new("Maintenance::Nested::NestedTask"),
156+
task_struct.new("Maintenance::UpdatePostsTask"),
157+
task_struct.new("Maintenance::Nested::NestedMore::NestedMoreTask"),
158+
task_struct.new("Maintenance::ErrorTask"),
159+
]
160+
expected = ["Maintenance", "Maintenance::Nested", "Maintenance::Nested::NestedMore"]
161+
assert_equal expected, extract_namespaces(tasks)
162+
end
163+
164+
test "#extract_namespaces excludes tasks without namespaces" do
165+
task_struct = Struct.new(:name)
166+
tasks = [
167+
task_struct.new("UpdatePostsTask"),
168+
task_struct.new("Maintenance::ErrorTask"),
169+
]
170+
assert_equal ["Maintenance"], extract_namespaces(tasks)
171+
end
172+
173+
test "#build_namespace_tree creates nested tree structure" do
174+
namespaces = ["Maintenance", "Maintenance::Nested", "Maintenance::Nested::NestedMore", "Other"]
175+
tree = build_namespace_tree(namespaces)
176+
177+
assert_equal "Maintenance", tree["Maintenance"][:name]
178+
assert_equal "Maintenance", tree["Maintenance"][:full_path]
179+
assert_equal "Nested", tree["Maintenance"][:children]["Nested"][:name]
180+
assert_equal "Maintenance::Nested", tree["Maintenance"][:children]["Nested"][:full_path]
181+
assert_equal "NestedMore", tree["Maintenance"][:children]["Nested"][:children]["NestedMore"][:name]
182+
assert_equal "Maintenance::Nested::NestedMore", tree["Maintenance"][:children]["Nested"][:children]["NestedMore"][:full_path]
183+
assert_equal "Other", tree["Other"][:name]
184+
assert_equal "Other", tree["Other"][:full_path]
185+
end
186+
152187
private
153188

154189
def with_zone_default(new_zone)

test/system/maintenance_tasks/tasks_test.rb

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class TasksTest < ApplicationSystemTestCase
4141
"Maintenance::ImportPostsTask Succeeded",
4242
]
4343

44-
assert_equal expected, page.all("h3").map(&:text)
44+
assert_equal expected, page.all(".column:not(.is-one-quarter) h3").map(&:text)
4545
end
4646

4747
test "show a Task" do
@@ -265,5 +265,44 @@ class TasksTest < ApplicationSystemTestCase
265265
assert_content "Maintenance Tasks"
266266
end
267267
end
268+
269+
test "sidebar displays namespaces and filters tasks" do
270+
visit maintenance_tasks_path
271+
272+
# Sidebar should display namespaces with short names
273+
assert_text "Namespaces"
274+
assert_link "All Tasks"
275+
assert_link "Maintenance"
276+
277+
# Nested namespaces should be inside dropdowns (details elements)
278+
within(".sidebar") do
279+
# Expand the Maintenance dropdown to see nested namespaces
280+
find("details", text: "Maintenance").click
281+
assert_link "Nested"
282+
end
283+
284+
# Click on a nested namespace to filter (within sidebar to be specific)
285+
within(".sidebar") do
286+
click_on "Nested"
287+
end
288+
289+
# Should show filtered tasks
290+
assert_link "Maintenance::Nested::NestedTask"
291+
assert_link "Maintenance::Nested::NestedMore::NestedMoreTask"
292+
293+
# Should show namespace filter tag
294+
assert_selector ".tag", text: "Maintenance::Nested"
295+
296+
# Should not show tasks from other namespaces
297+
assert_no_link "Maintenance::UpdatePostsTask"
298+
assert_no_link "Maintenance::ErrorTask"
299+
300+
# Click "All Tasks" to clear filter
301+
click_on "All Tasks"
302+
303+
# Should show all tasks again
304+
assert_link "Maintenance::UpdatePostsTask"
305+
assert_link "Maintenance::ErrorTask"
306+
end
268307
end
269308
end

0 commit comments

Comments
 (0)