Skip to content

Commit 22cabfd

Browse files
authored
Categories crud (#581)
* Add Category CRUD * Fix tests * Change workshop filters text to match categories index
1 parent 1a8b659 commit 22cabfd

20 files changed

+647
-8
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
class CategoriesController < ApplicationController
2+
before_action :set_category, only: [:show, :edit, :update, :destroy]
3+
4+
def index
5+
per_page = params[:number_of_items_per_page].presence || 25
6+
@category_types = CategoryType.order(:name)
7+
8+
unpaginated = Category.joins(:category_type)
9+
filtered = unpaginated.category_type_id(params[:category_type_id])
10+
.category_name(params[:category_name])
11+
.published_search(params[:published_search])
12+
.order("metadata.name ASC, categories.name ASC")
13+
@categories = filtered.paginate(page: params[:page], per_page: per_page)
14+
15+
@count_display = if @categories.total_entries == unpaginated.count
16+
unpaginated.count
17+
else
18+
"#{@categories.total_entries}/#{unpaginated.count}"
19+
end
20+
end
21+
22+
def show
23+
end
24+
25+
def new
26+
@category = Category.new
27+
set_form_variables
28+
end
29+
30+
def edit
31+
set_form_variables
32+
end
33+
34+
def create
35+
@category = Category.new(category_params)
36+
37+
if @category.save
38+
redirect_to categories_path, notice: "Category was successfully created."
39+
else
40+
set_form_variables
41+
render :new, status: :unprocessable_content
42+
end
43+
end
44+
45+
def update
46+
if @category.update(category_params)
47+
redirect_to categories_path, notice: "Category was successfully updated.", status: :see_other
48+
else
49+
set_form_variables
50+
render :edit, status: :unprocessable_content
51+
end
52+
end
53+
54+
def destroy
55+
@category.destroy!
56+
redirect_to categories_path, notice: "Category was successfully destroyed."
57+
end
58+
59+
# Optional hooks for setting variables for forms or index
60+
def set_form_variables
61+
@category_types = CategoryType.order(:name)
62+
end
63+
64+
private
65+
66+
def set_category
67+
@category = Category.find(params[:id])
68+
end
69+
70+
# Strong parameters
71+
def category_params
72+
params.require(:category).permit(
73+
:name, :category_type_id, :metadatum_id, :published
74+
)
75+
end
76+
end

app/helpers/admin_dashboard_cards_helper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def user_content_cards
4040
# -----------------------------
4141
def reference_cards
4242
[
43-
custom_card("Categories", authenticated_root_path, icon: "🗂️", color: :lime, intensity: 100),
43+
model_card(:categories, icon: "🗂️", intensity: 100),
4444
custom_card("Service populations", authenticated_root_path, icon: "🏭", color: :lime, intensity: 100),
4545
custom_card("Project statuses", authenticated_root_path, icon: "🧮", color: :emerald, intensity: 100),
4646
custom_card("Windows types", windows_types_path, icon: "🪟"),

app/models/category.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,13 @@ class Category < ApplicationRecord
1010

1111
# Validations
1212
validates_presence_of :name, uniqueness: true
13+
14+
# Scopes
15+
scope :category_type_id, ->(category_type_id) {
16+
category_type_id.present? ? where(metadatum_id: category_type_id) : all }
17+
scope :category_name, ->(category_name) {
18+
category_name.present? ? where("categories.name LIKE ?", "%#{category_name}%") : all }
19+
scope :published, ->(published=nil) {
20+
["true", "false"].include?(published) ? where(published: published) : where(published: true) }
21+
scope :published_search, ->(published_search) { published_search.present? ? published(published_search) : all }
1322
end
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<%= simple_form_for(@category) do |f| %>
2+
<div class="space-y-6">
3+
4+
<!-- Errors -->
5+
<%= f.error_notification %>
6+
<%= render "shared/errors", resource: @category if @category.errors.any? %>
7+
8+
<!-- One-line Flex Fields -->
9+
<div class="flex flex-wrap gap-6">
10+
11+
<!-- Name -->
12+
<div class="flex-1 min-w-[220px]">
13+
<%= f.input :name,
14+
label: "Name",
15+
input_html: { class: "form-control" } %>
16+
</div>
17+
18+
<!-- Category Type -->
19+
<div class="flex-1 min-w-[220px]">
20+
<%= f.input :metadatum_id,
21+
label: "Category Type",
22+
collection: @category_types,
23+
label_method: :name,
24+
value_method: :id,
25+
include_blank: "Select a Type",
26+
input_html: { class: "form-control" } %>
27+
</div>
28+
29+
<!-- Published -->
30+
<div class="flex items-center min-w-[150px] pt-6">
31+
<%= f.input :published,
32+
as: :boolean,
33+
label: "Published?",
34+
wrapper_html: { class: "flex items-center gap-2" },
35+
input_html: { class: "form-checkbox" } %>
36+
</div>
37+
38+
</div>
39+
40+
<!-- Actions -->
41+
<div class="flex flex-wrap justify-end gap-3 pt-6">
42+
43+
<% if @category.persisted? && current_user.super_user? %>
44+
<%= link_to "Delete",
45+
@category,
46+
method: :delete,
47+
data: { confirm: "Are you sure you want to delete this category?" },
48+
class: "btn btn-danger-outline" %>
49+
<% end %>
50+
51+
<%= link_to "Cancel", categories_path,
52+
class: "btn btn-secondary-outline" %>
53+
54+
<%= f.button :submit,
55+
"Save Category",
56+
class: "bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700
57+
transition-colors duration-150" %>
58+
</div>
59+
60+
</div>
61+
<% end %>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<!-- Filters -->
2+
<div class="mb-6 p-4 bg-white border border-gray-200 rounded-lg">
3+
<%= form_with url: categories_path,
4+
method: :get,
5+
local: true,
6+
class: "grid grid-cols-1 md:grid-cols-5 gap-4 items-end" do |f| %>
7+
8+
<!-- Category Type -->
9+
<div>
10+
<%= f.label :category_type_id, "Category Type",
11+
class: "block text-sm font-medium text-gray-700" %>
12+
13+
<%= f.select :category_type_id,
14+
options_from_collection_for_select(@category_types, :id, :name, params[:category_type_id]),
15+
{ include_blank: "All types" },
16+
class: "mt-1 block w-full rounded-md border border-gray-300 p-2",
17+
onchange: "this.form.requestSubmit()" %>
18+
</div>
19+
20+
<!-- Name Search -->
21+
<div>
22+
<%= f.label :category_name, "Name Contains",
23+
class: "block text-sm font-medium text-gray-700" %>
24+
25+
<%= f.text_field :category_name,
26+
value: params[:category_name],
27+
placeholder: "e.g. Art, Music…",
28+
class: "mt-1 block w-full rounded-md border border-gray-300 p-2",
29+
oninput: "this.form.requestSubmit()" %>
30+
</div>
31+
32+
<!-- Published -->
33+
<div>
34+
<%= f.label :published_search, "Published",
35+
class: "block text-sm font-medium text-gray-700" %>
36+
37+
<%= f.select :published_search,
38+
options_for_select([["All", ""], ["Published", "true"], ["Hidden", "false"]], params[:published_search]),
39+
{},
40+
class: "mt-1 block w-full rounded-md border border-gray-300 p-2",
41+
onchange: "this.form.requestSubmit()" %>
42+
</div>
43+
44+
<!-- Clear -->
45+
<div>
46+
<%= link_to "Clear",
47+
categories_path,
48+
class: "btn btn-utility-outline whitespace-nowrap" %>
49+
</div>
50+
51+
<% end %>
52+
</div>

app/views/categories/edit.html.erb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<div class="min-h-screen py-8">
2+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
3+
<div class="max-w-5xl mx-auto bg-white border border-gray-200 rounded-xl shadow-lg hover:shadow-xl transition-shadow duration-200 p-6">
4+
<div class="flex items-center justify-between mb-6">
5+
<h1 class="text-2xl font-semibold text-gray-900">Edit <%= @category.class.model_name.human %></h1>
6+
<%= link_to "Taggings", taggings_path(category_names: @category.name),
7+
class: "btn btn-secondary-outline" %>
8+
</div>
9+
10+
<div class="space-y-6">
11+
<div class="mt-4">
12+
<%= render "form", category: @category %>
13+
</div>
14+
</div>
15+
</div>
16+
</div>
17+
</div>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<div class="min-h-screen">
2+
<div class="max-w-full mx-auto px-4 sm:px-6 lg:px-8">
3+
<div class="max-w-7xl mx-auto <%= DomainTheme.bg_class_for(:categories) %> border border-gray-200 rounded-xl shadow-lg p-6">
4+
<div class="space-y-6">
5+
6+
<!-- Header -->
7+
<div class="flex items-center justify-between mb-6">
8+
<h1 class="text-2xl font-semibold text-gray-900">
9+
<%= Category.model_name.human.pluralize %> (<%= @count_display %>)
10+
</h1>
11+
<%= link_to "New #{Category.model_name.human.downcase}",
12+
new_category_path,
13+
class: "btn btn-primary-outline" %>
14+
</div>
15+
16+
<%= render "search_boxes" %>
17+
18+
<!-- Table -->
19+
<div class="bg-white rounded">
20+
<div class="overflow-x-auto">
21+
<% if @categories.any? %>
22+
<table class="w-full table-fixed border-collapse border border-gray-200">
23+
<thead class="bg-gray-100">
24+
<tr>
25+
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700 w-1/3">
26+
Name
27+
</th>
28+
29+
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700 w-1/4">
30+
Type
31+
</th>
32+
33+
<th class="px-4 py-2 text-center text-sm font-semibold text-gray-700 w-1/6">
34+
Published
35+
</th>
36+
37+
<th class="px-4 py-2 text-center text-sm font-semibold text-gray-700 w-[80px]">
38+
Actions
39+
</th>
40+
</tr>
41+
</thead>
42+
43+
<tbody class="divide-y divide-gray-200">
44+
<% @categories.each do |category| %>
45+
<tr class=" <%= "bg-gray-200" unless category.published %>
46+
<%= category.published ? "hover:bg-gray-50" : "hover:bg-gray-100" %> transition-colors duration-150">
47+
48+
<td class="px-4 py-2 text-md text-gray-800 truncate font-bold">
49+
<%= category.name %>
50+
</td>
51+
52+
<td class="px-4 py-2 text-sm text-gray-800 truncate">
53+
<%= category.category_type&.name || "—" %>
54+
</td>
55+
56+
<td class="px-4 py-2 text-center">
57+
<% if category.published? %>
58+
<span class="inline-block px-2 py-1 text-xs font-semibold text-green-800 bg-green-100 rounded">
59+
Yes
60+
</span>
61+
<% else %>
62+
<span class="inline-block px-2 py-1 text-xs font-semibold text-gray-700 bg-gray-200 rounded">
63+
No
64+
</span>
65+
<% end %>
66+
</td>
67+
68+
<!-- Actions -->
69+
<td class="px-4 py-2 text-center">
70+
<%= link_to "Edit",
71+
edit_category_path(category),
72+
class: "btn btn-secondary-outline whitespace-nowrap" %>
73+
<%= link_to "Taggings",
74+
taggings_path(category_names: category.name),
75+
class: "btn btn-secondary-outline whitespace-nowrap" %>
76+
</td>
77+
78+
</tr>
79+
<% end %>
80+
</tbody>
81+
</table>
82+
<% else %>
83+
<!-- Empty state -->
84+
<p class="text-gray-500 text-center py-6">
85+
No <%= Category.model_name.human.pluralize %> found.
86+
</p>
87+
<% end %>
88+
</div>
89+
</div>
90+
91+
<!-- Pagination -->
92+
<div class="pagination flex justify-center mt-12">
93+
<%= tailwind_paginate @categories %>
94+
</div>
95+
96+
</div>
97+
</div>
98+
</div>
99+
</div>

app/views/categories/new.html.erb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<div class="min-h-screen py-8">
2+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
3+
<div class="max-w-5xl mx-auto bg-white border border-gray-200 rounded-xl shadow-lg hover:shadow-xl transition-shadow duration-200 p-6">
4+
<div class="flex items-center justify-between mb-3">
5+
<h1 class="text-2xl font-semibold text-gray-900">New <%= @category.class.model_name.human %></h1>
6+
</div>
7+
8+
<div class="border-b border-gray-300 mb-6"></div>
9+
10+
<div class="space-y-6">
11+
<div class="mt-4">
12+
<%= render "form", category: @category %>
13+
</div>
14+
</div>
15+
</div>
16+
</div>
17+
</div>

app/views/categories/show.html.erb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<div class="min-h-screen py-8">
2+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
3+
<div class="max-w-5xl mx-auto bg-white border border-gray-200 rounded-xl shadow-lg hover:shadow-xl transition-shadow duration-200 p-6">
4+
<div class="flex items-center justify-between mb-6">
5+
<!-- Title -->
6+
<h1 class="text-2xl font-semibold text-gray-900">
7+
<%= @category.class.model_name.human %> Details
8+
</h1>
9+
10+
<!-- Buttons Group -->
11+
<div class="flex items-center gap-2">
12+
<%= link_to("Index", categories_path, class: "btn btn-secondary-outline") %>
13+
<% if current_user.super_user? %>
14+
<%= link_to("Edit", edit_category_path(@category), class: "btn btn-primary-outline") %>
15+
<% end %>
16+
</div>
17+
</div>
18+
19+
<div class="space-y-6">
20+
<div class="mt-4">
21+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-3">
22+
23+
<div class="mb-3">
24+
<p class="font-bold text-gray-700">Name:</p>
25+
<p class="text-gray-900"><%= @category.name %></p>
26+
</div>
27+
<div class="mb-3">
28+
<p class="font-bold text-gray-700">Category type:</p>
29+
<p class="text-gray-900"><%= @category.category_type.name %></p>
30+
</div>
31+
<div class="mb-3">
32+
<p class="font-bold text-gray-700">Published:</p>
33+
<p class="text-gray-900"><%= @category.published %></p>
34+
</div>
35+
36+
</div>
37+
</div>
38+
</div>
39+
</div>
40+
</div>
41+
</div>

0 commit comments

Comments
 (0)