recycle_bin 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +55 -2
- data/Gemfile +3 -3
- data/README.md +95 -9
- data/app/controllers/recycle_bin/trash_controller.rb +120 -24
- data/app/views/{layouts/recycle_bin/application.html.erb → recycle_bin/layouts/recycle_bin.html.erb} +849 -609
- data/app/views/recycle_bin/shared/_error_messages.html.erb +10 -0
- data/app/views/recycle_bin/shared/_flash_messages.html.erb +6 -0
- data/app/views/recycle_bin/trash/_action_history.html.erb +71 -0
- data/app/views/recycle_bin/trash/_associations.html.erb +46 -0
- data/app/views/recycle_bin/trash/_filters.html.erb +17 -0
- data/app/views/recycle_bin/trash/_item.html.erb +24 -0
- data/app/views/recycle_bin/trash/_pagination.html.erb +75 -0
- data/app/views/recycle_bin/trash/_stats.html.erb +32 -0
- data/app/views/recycle_bin/trash/index.html.erb +48 -217
- data/app/views/recycle_bin/trash/show.html.erb +60 -215
- data/docs/index.html +928 -0
- data/docs/logo.svg +71 -0
- data/lib/recycle_bin/version.rb +1 -1
- data/lib/recycle_bin.rb +69 -3
- data/recycle_bin.gemspec +4 -4
- metadata +18 -14
- data/app/views/layouts/recycle_bin/recycle_bin/application.html.erb +0 -266
- data/app/views/layouts/recycle_bin/recycle_bin/trash/index.html.erb +0 -133
- data/app/views/layouts/recycle_bin/recycle_bin/trash/show.html.erb +0 -175
@@ -0,0 +1,10 @@
|
|
1
|
+
<% if defined?(resource) && resource.errors.any? %>
|
2
|
+
<div class="alert alert-danger">
|
3
|
+
<h4><%= pluralize(resource.errors.count, "error") %> prohibited this action:</h4>
|
4
|
+
<ul>
|
5
|
+
<% resource.errors.full_messages.each do |message| %>
|
6
|
+
<li><%= message %></li>
|
7
|
+
<% end %>
|
8
|
+
</ul>
|
9
|
+
</div>
|
10
|
+
<% end %>
|
@@ -0,0 +1,71 @@
|
|
1
|
+
<div class="main-card">
|
2
|
+
<div class="card-header">
|
3
|
+
<h3 class="card-title">Action History</h3>
|
4
|
+
</div>
|
5
|
+
<div class="card-body">
|
6
|
+
<% if item.respond_to?(:versions) && item.versions.any? %>
|
7
|
+
<div class="table-container">
|
8
|
+
<table class="table">
|
9
|
+
<thead>
|
10
|
+
<tr>
|
11
|
+
<th>Event</th>
|
12
|
+
<th>User</th>
|
13
|
+
<th>Timestamp</th>
|
14
|
+
<th>Details</th>
|
15
|
+
</tr>
|
16
|
+
</thead>
|
17
|
+
<tbody>
|
18
|
+
<% item.versions.each do |version| %>
|
19
|
+
<tr>
|
20
|
+
<td><span class="event-type"><%= version.event.humanize %></span></td>
|
21
|
+
<td>
|
22
|
+
<% if version.whodunnit %>
|
23
|
+
<%= version.whodunnit %>
|
24
|
+
<% else %>
|
25
|
+
<span class="timestamp">System</span>
|
26
|
+
<% end %>
|
27
|
+
</td>
|
28
|
+
<td>
|
29
|
+
<span class="timestamp">
|
30
|
+
<%= time_ago_in_words(version.created_at) %> ago •
|
31
|
+
<%= version.created_at.strftime('%B %d, %Y at %l:%M %p') %>
|
32
|
+
</span>
|
33
|
+
</td>
|
34
|
+
<td>
|
35
|
+
<% if version.changeset.any? %>
|
36
|
+
<details>
|
37
|
+
<summary class="btn btn-outline btn-sm">View Changes</summary>
|
38
|
+
<div class="changeset-details">
|
39
|
+
<% version.changeset.each do |key, changes| %>
|
40
|
+
<div class="change-item">
|
41
|
+
<strong><%= key.humanize %>:</strong>
|
42
|
+
<span class="change-from"><%= changes[0].nil? ? 'nil' : changes[0] %></span>
|
43
|
+
→
|
44
|
+
<span class="change-to"><%= changes[1].nil? ? 'nil' : changes[1] %></span>
|
45
|
+
</div>
|
46
|
+
<% end %>
|
47
|
+
</div>
|
48
|
+
</details>
|
49
|
+
<% else %>
|
50
|
+
<span class="timestamp">No changes recorded</span>
|
51
|
+
<% end %>
|
52
|
+
</td>
|
53
|
+
</tr>
|
54
|
+
<% end %>
|
55
|
+
</tbody>
|
56
|
+
</table>
|
57
|
+
</div>
|
58
|
+
<% else %>
|
59
|
+
<div class="empty-state">
|
60
|
+
<div class="empty-icon">📜</div>
|
61
|
+
<h4 class="empty-title">No history available</h4>
|
62
|
+
<p class="empty-subtitle">
|
63
|
+
This item has no recorded actions.
|
64
|
+
<% if Rails.env.development? %>
|
65
|
+
<br>Debug: <%= item.class.name %> ID: <%= item.id %> has <%= item.respond_to?(:versions) ? item.versions.count : 'no versions method' %> versions.
|
66
|
+
<% end %>
|
67
|
+
</p>
|
68
|
+
</div>
|
69
|
+
<% end %>
|
70
|
+
</div>
|
71
|
+
</div>
|
@@ -0,0 +1,46 @@
|
|
1
|
+
<div class="main-card">
|
2
|
+
<div class="card-header">
|
3
|
+
<h3 class="card-title">Related Items</h3>
|
4
|
+
<div class="card-actions">
|
5
|
+
<span class="timestamp"><%= associations.values.flatten.count %> related items</span>
|
6
|
+
</div>
|
7
|
+
</div>
|
8
|
+
<div class="card-body">
|
9
|
+
<% associations.each do |association_name, items| %>
|
10
|
+
<div class="association-section">
|
11
|
+
<h4 class="card-title"><%= association_name.humanize %> <span class="timestamp">(<%= items.count %> items)</span></h4>
|
12
|
+
<% if items.any? %>
|
13
|
+
<div class="association-grid">
|
14
|
+
<% items.each do |item| %>
|
15
|
+
<div class="association-item">
|
16
|
+
<div class="association-content">
|
17
|
+
<div class="card-title">
|
18
|
+
<%= item.class.name %> -
|
19
|
+
<%= item.respond_to?(:title) ? item.title :
|
20
|
+
item.respond_to?(:name) ? item.name :
|
21
|
+
"ID: #{item.id}" %>
|
22
|
+
</div>
|
23
|
+
<div class="timestamp">
|
24
|
+
Created <%= time_ago_in_words(item.created_at) %> ago
|
25
|
+
<% if item.respond_to?(:deleted_at) && item.deleted_at %>
|
26
|
+
• <span class="deleted-status">Also deleted</span>
|
27
|
+
<% end %>
|
28
|
+
</div>
|
29
|
+
</div>
|
30
|
+
<% if item.respond_to?(:deleted_at) && item.deleted_at %>
|
31
|
+
<span class="model-badge deleted-badge">Deleted</span>
|
32
|
+
<% else %>
|
33
|
+
<span class="model-badge active-badge">Active</span>
|
34
|
+
<% end %>
|
35
|
+
</div>
|
36
|
+
<% end %>
|
37
|
+
</div>
|
38
|
+
<% else %>
|
39
|
+
<div class="empty-state">
|
40
|
+
<p class="timestamp">No related items found.</p>
|
41
|
+
</div>
|
42
|
+
<% end %>
|
43
|
+
</div>
|
44
|
+
<% end %>
|
45
|
+
</div>
|
46
|
+
</div>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<% if model_types.any? %>
|
2
|
+
<div class="filters">
|
3
|
+
<div class="filter-group">
|
4
|
+
<span class="filter-label">Filter by type:</span>
|
5
|
+
<%= link_to "All", recycle_bin.root_path(page: 1), class: "btn btn-sm #{params[:type].blank? ? 'btn-primary' : 'btn-outline'}" %>
|
6
|
+
<% model_types.each do |model_type| %>
|
7
|
+
<%= link_to model_type, recycle_bin.root_path(type: model_type, page: 1), class: "btn btn-sm #{params[:type] == model_type ? 'btn-primary' : 'btn-outline'}" %>
|
8
|
+
<% end %>
|
9
|
+
</div>
|
10
|
+
<div class="filter-group">
|
11
|
+
<span class="filter-label">Time:</span>
|
12
|
+
<%= link_to "Today", recycle_bin.root_path(time: 'today', page: 1), class: "btn btn-sm #{params[:time] == 'today' ? 'btn-primary' : 'btn-outline'}" %>
|
13
|
+
<%= link_to "This Week", recycle_bin.root_path(time: 'week', page: 1), class: "btn btn-sm #{params[:time] == 'week' ? 'btn-primary' : 'btn-outline'}" %>
|
14
|
+
<%= link_to "This Month", recycle_bin.root_path(time: 'month', page: 1), class: "btn btn-sm #{params[:time] == 'month' ? 'btn-primary' : 'btn-outline'}" %>
|
15
|
+
</div>
|
16
|
+
</div>
|
17
|
+
<% end %>
|
@@ -0,0 +1,24 @@
|
|
1
|
+
<tr>
|
2
|
+
<td><input type="checkbox" class="item-checkbox" value="<%= item.class.name %>:<%= item.id %>" onchange="updateBulkActions()"></td>
|
3
|
+
<td><span class="model-badge"><%= item.class.name %></span></td>
|
4
|
+
<td>
|
5
|
+
<div class="card-title">
|
6
|
+
<%= link_to truncate(item.recyclable_title, length: 60), recycle_bin.trash_path(item.class.name, item.id) %>
|
7
|
+
</div>
|
8
|
+
<div class="timestamp">ID: <%= item.id %></div>
|
9
|
+
</td>
|
10
|
+
<td>
|
11
|
+
<div class="card-title"><%= time_ago_in_words(item.deleted_at) %> ago</div>
|
12
|
+
<div class="timestamp"><%= item.deleted_at.strftime('%B %d, %Y at %l:%M %p') %></div>
|
13
|
+
</td>
|
14
|
+
<td>
|
15
|
+
<div class="card-actions">
|
16
|
+
<%= form_with url: recycle_bin.restore_trash_path(item.class.name, item.id), method: :patch, local: true do |form| %>
|
17
|
+
<%= form.submit "↶ Restore", class: "btn btn-success btn-sm", onclick: "return confirm('Restore this #{item.class.name.downcase}?')" %>
|
18
|
+
<% end %>
|
19
|
+
<%= form_with url: recycle_bin.destroy_trash_path(item.class.name, item.id), method: :delete, local: true do |form| %>
|
20
|
+
<%= form.submit "🗑️ Delete", class: "btn btn-danger btn-sm", onclick: "return confirm('Permanently delete this #{item.class.name.downcase}? This cannot be undone!')" %>
|
21
|
+
<% end %>
|
22
|
+
</div>
|
23
|
+
</td>
|
24
|
+
</tr>
|
@@ -0,0 +1,75 @@
|
|
1
|
+
<div class="pagination-wrapper">
|
2
|
+
<div class="pagination">
|
3
|
+
<% if total_pages > 1 %>
|
4
|
+
<!-- First/Previous links -->
|
5
|
+
<% if current_page > 1 %>
|
6
|
+
<%= link_to "« First", request.params.merge(page: 1) %>
|
7
|
+
<%= link_to "‹ Prev", request.params.merge(page: current_page - 1) %>
|
8
|
+
<% else %>
|
9
|
+
<span class="disabled">« First</span>
|
10
|
+
<span class="disabled">‹ Prev</span>
|
11
|
+
<% end %>
|
12
|
+
|
13
|
+
<%
|
14
|
+
# Smart pagination logic
|
15
|
+
window_size = 2 # Number of pages to show on each side of current page
|
16
|
+
start_page = [current_page - window_size, 1].max
|
17
|
+
end_page = [current_page + window_size, total_pages].min
|
18
|
+
%>
|
19
|
+
|
20
|
+
<!-- First page if not in window -->
|
21
|
+
<% if start_page > 1 %>
|
22
|
+
<%= link_to 1, request.params.merge(page: 1) %>
|
23
|
+
<% if start_page > 2 %>
|
24
|
+
<span class="disabled">…</span>
|
25
|
+
<% end %>
|
26
|
+
<% end %>
|
27
|
+
|
28
|
+
<!-- Page window around current page -->
|
29
|
+
<% (start_page..end_page).each do |page| %>
|
30
|
+
<% if page == current_page %>
|
31
|
+
<span class="current"><%= page %></span>
|
32
|
+
<% else %>
|
33
|
+
<%= link_to page, request.params.merge(page: page) %>
|
34
|
+
<% end %>
|
35
|
+
<% end %>
|
36
|
+
|
37
|
+
<!-- Last page if not in window -->
|
38
|
+
<% if end_page < total_pages %>
|
39
|
+
<% if end_page < total_pages - 1 %>
|
40
|
+
<span class="disabled">…</span>
|
41
|
+
<% end %>
|
42
|
+
<%= link_to total_pages, request.params.merge(page: total_pages) %>
|
43
|
+
<% end %>
|
44
|
+
|
45
|
+
<!-- Next/Last links -->
|
46
|
+
<% if current_page < total_pages %>
|
47
|
+
<%= link_to "Next ›", request.params.merge(page: current_page + 1) %>
|
48
|
+
<%= link_to "Last »", request.params.merge(page: total_pages) %>
|
49
|
+
<% else %>
|
50
|
+
<span class="disabled">Next ›</span>
|
51
|
+
<span class="disabled">Last »</span>
|
52
|
+
<% end %>
|
53
|
+
<% end %>
|
54
|
+
</div>
|
55
|
+
|
56
|
+
<!-- Page info -->
|
57
|
+
<div class="pagination-info">
|
58
|
+
<span>
|
59
|
+
Showing page <%= current_page %> of <%= total_pages %>
|
60
|
+
(<%= pluralize(total_count, 'item') %> total)
|
61
|
+
</span>
|
62
|
+
</div>
|
63
|
+
|
64
|
+
<!-- Per page selector -->
|
65
|
+
<div class="per-page">
|
66
|
+
<span>Items per page:</span>
|
67
|
+
<% [25, 50, 100].each do |num| %>
|
68
|
+
<% if num == per_page %>
|
69
|
+
<span class="current"><%= num %></span>
|
70
|
+
<% else %>
|
71
|
+
<%= link_to num, request.params.merge(per_page: num, page: 1), class: "per-page-link" %>
|
72
|
+
<% end %>
|
73
|
+
<% end %>
|
74
|
+
</div>
|
75
|
+
</div>
|
@@ -0,0 +1,32 @@
|
|
1
|
+
<div class="stats-grid">
|
2
|
+
<div class="stat-card">
|
3
|
+
<div class="stat-number"><%= number_with_delimiter(total_count) %></div>
|
4
|
+
<div class="stat-label">Total Items in Trash</div>
|
5
|
+
<div class="stat-change positive">
|
6
|
+
<%= filtered_items.items.select { |item| item.deleted_at > 1.day.ago }.count %> deleted today
|
7
|
+
</div>
|
8
|
+
</div>
|
9
|
+
<div class="stat-card">
|
10
|
+
<div class="stat-number"><%= model_types.count %></div>
|
11
|
+
<div class="stat-label">Model Types</div>
|
12
|
+
<div class="stat-change">
|
13
|
+
<% if model_types.any? %>
|
14
|
+
<%= model_types.join(', ') %>
|
15
|
+
<% else %>
|
16
|
+
No types
|
17
|
+
<% end %>
|
18
|
+
</div>
|
19
|
+
</div>
|
20
|
+
<div class="stat-card">
|
21
|
+
<div class="stat-number"><%= filtered_items.items.select { |item| item.deleted_at > 7.days.ago }.count %></div>
|
22
|
+
<div class="stat-label">This Week</div>
|
23
|
+
<div class="stat-change">Recent activity</div>
|
24
|
+
</div>
|
25
|
+
<div class="stat-card">
|
26
|
+
<div class="stat-number"><%= current_page %> / <%= defined?(total_pages) ? total_pages : 1 %></div>
|
27
|
+
<div class="stat-label">Current Page</div>
|
28
|
+
<div class="stat-change">
|
29
|
+
Showing <%= (current_page - 1) * per_page + 1 %>-<%= [(current_page * per_page), total_count].min %> of <%= number_with_delimiter(total_count) %>
|
30
|
+
</div>
|
31
|
+
</div>
|
32
|
+
</div>
|
@@ -1,229 +1,60 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
</div>
|
11
|
-
</div>
|
12
|
-
|
13
|
-
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); border-left: 4px solid #667eea;">
|
14
|
-
<div style="font-size: 2rem; font-weight: bold; color: #667eea; margin-bottom: 8px;"><%= @model_types.count %></div>
|
15
|
-
<div style="color: #6c757d; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.5px;">Model Types</div>
|
16
|
-
<div style="font-size: 0.8rem; margin-top: 8px;">
|
17
|
-
<% if @model_types.any? %>
|
18
|
-
<%= @model_types.join(', ') %>
|
19
|
-
<% else %>
|
20
|
-
No types
|
21
|
-
<% end %>
|
22
|
-
</div>
|
23
|
-
</div>
|
24
|
-
|
25
|
-
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); border-left: 4px solid #667eea;">
|
26
|
-
<div style="font-size: 2rem; font-weight: bold; color: #667eea; margin-bottom: 8px;">
|
27
|
-
<%= @deleted_items.select { |item| item.deleted_at > 7.days.ago }.count %>
|
28
|
-
</div>
|
29
|
-
<div style="color: #6c757d; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.5px;">This Week</div>
|
30
|
-
<div style="font-size: 0.8rem; margin-top: 8px;">
|
31
|
-
Recent activity
|
32
|
-
</div>
|
33
|
-
</div>
|
34
|
-
</div>
|
35
|
-
|
36
|
-
<!-- Filters -->
|
37
|
-
<% if @model_types.any? %>
|
38
|
-
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px;">
|
39
|
-
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
|
40
|
-
<span style="font-weight: 500; color: #495057;">Filter by type:</span>
|
41
|
-
<a href="/recycle_bin/" style="display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; border: 1px solid #dee2e6; border-radius: 6px; text-decoration: none; font-size: 14px; font-weight: 500; <%= params[:type].blank? ? 'background: #667eea; color: white;' : 'color: #495057;' %>">All</a>
|
42
|
-
<% @model_types.each do |model_type| %>
|
43
|
-
<a href="/recycle_bin/?type=<%= model_type %>" style="display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; border: 1px solid #dee2e6; border-radius: 6px; text-decoration: none; font-size: 14px; font-weight: 500; <%= params[:type] == model_type ? 'background: #667eea; color: white;' : 'color: #495057;' %>"><%= model_type %></a>
|
44
|
-
<% end %>
|
45
|
-
</div>
|
46
|
-
|
47
|
-
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-top: 15px;">
|
48
|
-
<span style="font-weight: 500; color: #495057;">Time:</span>
|
49
|
-
<a href="/recycle_bin/?time=today" style="display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; border: 1px solid #dee2e6; border-radius: 6px; text-decoration: none; font-size: 14px; font-weight: 500; <%= params[:time] == 'today' ? 'background: #667eea; color: white;' : 'color: #495057;' %>">Today</a>
|
50
|
-
<a href="/recycle_bin/?time=week" style="display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; border: 1px solid #dee2e6; border-radius: 6px; text-decoration: none; font-size: 14px; font-weight: 500; <%= params[:time] == 'week' ? 'background: #667eea; color: white;' : 'color: #495057;' %>">This Week</a>
|
51
|
-
<a href="/recycle_bin/?time=month" style="display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; border: 1px solid #dee2e6; border-radius: 6px; text-decoration: none; font-size: 14px; font-weight: 500; <%= params[:time] == 'month' ? 'background: #667eea; color: white;' : 'color: #495057;' %>">This Month</a>
|
52
|
-
</div>
|
53
|
-
</div>
|
54
|
-
<% end %>
|
55
|
-
|
1
|
+
<% content_for :title, "Deleted Items" %>
|
2
|
+
<%= render 'recycle_bin/trash/stats',
|
3
|
+
total_count: @total_count,
|
4
|
+
model_types: @model_types,
|
5
|
+
filtered_items: @filtered_items,
|
6
|
+
current_page: @current_page,
|
7
|
+
per_page: @per_page,
|
8
|
+
total_pages: @total_pages %>
|
9
|
+
<%= render 'recycle_bin/trash/filters', model_types: @model_types %>
|
56
10
|
<% if @deleted_items.any? %>
|
57
|
-
<div
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
11
|
+
<div class="main-card">
|
12
|
+
<div class="card-header">
|
13
|
+
<h3 class="card-title">Deleted Items (<%= number_with_delimiter(@total_count) %>)</h3>
|
14
|
+
<div class="card-actions">
|
15
|
+
<%# <label class="btn btn-outline btn-sm">
|
16
|
+
<input type="checkbox" id="auto-refresh"> Auto-refresh
|
17
|
+
</label> %>
|
18
|
+
</div>
|
19
|
+
</div>
|
20
|
+
<div id="bulk-actions" class="bulk-actions">
|
21
|
+
<span id="bulk-count">0 items selected</span>
|
22
|
+
<div class="card-actions">
|
23
|
+
<%= form_with url: recycle_bin.bulk_restore_trash_index_path, method: :patch, local: true do |form| %>
|
64
24
|
<input type="hidden" id="bulk-restore-items" name="selected_items" value="">
|
65
|
-
<%= form.submit "↶ Restore Selected",
|
66
|
-
style: "display: inline-flex; align-items: center; gap: 4px; padding: 8px 16px; background: #28a745; color: white; border: none; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer;",
|
67
|
-
onclick: "return handleBulkAction('restore')" %>
|
25
|
+
<%= form.submit "↶ Restore Selected", class: "btn btn-success btn-sm", onclick: "return handleBulkAction('restore')" %>
|
68
26
|
<% end %>
|
69
|
-
|
70
|
-
<%= form_with url: "/recycle_bin/trash/bulk_destroy", method: :delete, local: true, style: "display: inline;" do |form| %>
|
27
|
+
<%= form_with url: recycle_bin.bulk_destroy_trash_index_path, method: :delete, local: true do |form| %>
|
71
28
|
<input type="hidden" id="bulk-destroy-items" name="selected_items" value="">
|
72
|
-
<%= form.submit "🗑️ Delete Selected",
|
73
|
-
style: "display: inline-flex; align-items: center; gap: 4px; padding: 8px 16px; background: #dc3545; color: white; border: none; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer;",
|
74
|
-
onclick: "return handleBulkAction('destroy')" %>
|
29
|
+
<%= form.submit "🗑️ Delete Selected", class: "btn btn-danger btn-sm", onclick: "return handleBulkAction('destroy')" %>
|
75
30
|
<% end %>
|
76
31
|
</div>
|
77
32
|
</div>
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
<
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
<th style="padding: 16px; text-align: left; font-weight: 600; color: #495057;">Deleted At</th>
|
88
|
-
<th style="padding: 16px; text-align: left; font-weight: 600; color: #495057;">Actions</th>
|
89
|
-
</tr>
|
90
|
-
</thead>
|
91
|
-
<tbody>
|
92
|
-
<% @deleted_items.each do |item| %>
|
93
|
-
<tr style="border-bottom: 1px solid #dee2e6;">
|
94
|
-
<td style="padding: 16px;">
|
95
|
-
<input type="checkbox" class="item-checkbox" value="<%= item.class.name %>:<%= item.id %>" style="cursor: pointer;" onchange="updateBulkActions()">
|
96
|
-
</td>
|
97
|
-
<td style="padding: 16px;">
|
98
|
-
<span style="background: #667eea; color: white; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 500;">
|
99
|
-
<%= item.class.name %>
|
100
|
-
</span>
|
101
|
-
</td>
|
102
|
-
<td style="padding: 16px;">
|
103
|
-
<div style="font-weight: 500; color: #495057; margin-bottom: 4px;">
|
104
|
-
<%= truncate(item.recyclable_title, length: 60) %>
|
105
|
-
</div>
|
106
|
-
<small style="color: #6c757d;">ID: <%= item.id %></small>
|
107
|
-
</td>
|
108
|
-
<td style="padding: 16px;">
|
109
|
-
<div style="font-weight: 500; color: #495057; margin-bottom: 4px;">
|
110
|
-
<%= time_ago_in_words(item.deleted_at) %> ago
|
111
|
-
</div>
|
112
|
-
<small style="color: #6c757d;"><%= item.deleted_at.strftime('%B %d, %Y at %l:%M %p') %></small>
|
113
|
-
</td>
|
114
|
-
<td style="padding: 16px;">
|
115
|
-
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
116
|
-
<%= form_with url: "/recycle_bin/trash/#{item.class.name}/#{item.id}/restore", method: :patch, local: true, style: "display: inline;" do |form| %>
|
117
|
-
<%= form.submit "↶ Restore",
|
118
|
-
style: "display: inline-flex; align-items: center; gap: 4px; padding: 6px 12px; background: #28a745; color: white; border: none; border-radius: 6px; text-decoration: none; font-size: 12px; font-weight: 500; cursor: pointer;",
|
119
|
-
onclick: "return confirm('Restore this #{item.class.name.downcase}?')" %>
|
120
|
-
<% end %>
|
121
|
-
|
122
|
-
<%= form_with url: "/recycle_bin/trash/#{item.class.name}/#{item.id}", method: :delete, local: true, style: "display: inline;" do |form| %>
|
123
|
-
<%= form.submit "🗑️ Delete",
|
124
|
-
style: "display: inline-flex; align-items: center; gap: 4px; padding: 6px 12px; background: #dc3545; color: white; border: none; border-radius: 6px; text-decoration: none; font-size: 12px; font-weight: 500; cursor: pointer;",
|
125
|
-
onclick: "return confirm('Permanently delete this #{item.class.name.downcase}? This cannot be undone!')" %>
|
126
|
-
<% end %>
|
127
|
-
</div>
|
128
|
-
</td>
|
33
|
+
<div class="table-container">
|
34
|
+
<table class="table">
|
35
|
+
<thead>
|
36
|
+
<tr>
|
37
|
+
<th class="checkbox-column"><input type="checkbox" id="select-all" onchange="toggleSelectAll()"></th>
|
38
|
+
<th>Type</th>
|
39
|
+
<th>Item</th>
|
40
|
+
<th>Deleted At</th>
|
41
|
+
<th>Actions</th>
|
129
42
|
</tr>
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
Showing <%= @deleted_items.count %> items
|
43
|
+
</thead>
|
44
|
+
<tbody>
|
45
|
+
<%= render partial: 'recycle_bin/trash/item', collection: @deleted_items %>
|
46
|
+
</tbody>
|
47
|
+
</table>
|
48
|
+
</div>
|
137
49
|
</div>
|
138
|
-
|
50
|
+
<%= render 'recycle_bin/trash/pagination', current_page: @current_page, total_pages: @total_pages, total_count: @total_count, per_page: @per_page %>
|
139
51
|
<% else %>
|
140
|
-
<div class="
|
141
|
-
<div
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
<a href="/recycle_bin/" style="display: inline-flex; align-items: center; gap: 8px; padding: 12px 24px; background: #667eea; color: white; border-radius: 6px; text-decoration: none; font-weight: 500;">
|
147
|
-
🔄 Refresh
|
148
|
-
</a>
|
52
|
+
<div class="main-card">
|
53
|
+
<div class="empty-state">
|
54
|
+
<div class="empty-icon">🎉</div>
|
55
|
+
<h4 class="empty-title">No items match your filters!</h4>
|
56
|
+
<p class="empty-subtitle">Try adjusting your filters or check back later for deleted items.</p>
|
57
|
+
<%= link_to "Clear Filters", recycle_bin.root_path, class: "btn btn-primary" %>
|
149
58
|
</div>
|
150
59
|
</div>
|
151
|
-
<% end %>
|
152
|
-
|
153
|
-
<script>
|
154
|
-
// Bulk selection functionality
|
155
|
-
function toggleSelectAll() {
|
156
|
-
const selectAllCheckbox = document.getElementById('select-all');
|
157
|
-
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
158
|
-
|
159
|
-
itemCheckboxes.forEach(checkbox => {
|
160
|
-
checkbox.checked = selectAllCheckbox.checked;
|
161
|
-
});
|
162
|
-
|
163
|
-
updateBulkActions();
|
164
|
-
}
|
165
|
-
|
166
|
-
function updateBulkActions() {
|
167
|
-
const checkedBoxes = document.querySelectorAll('.item-checkbox:checked');
|
168
|
-
const bulkActions = document.getElementById('bulk-actions');
|
169
|
-
const bulkCount = document.getElementById('bulk-count');
|
170
|
-
const selectAllCheckbox = document.getElementById('select-all');
|
171
|
-
|
172
|
-
if (checkedBoxes.length > 0) {
|
173
|
-
bulkActions.style.display = 'flex';
|
174
|
-
bulkCount.textContent = checkedBoxes.length + ' item' + (checkedBoxes.length > 1 ? 's' : '') + ' selected';
|
175
|
-
} else {
|
176
|
-
bulkActions.style.display = 'none';
|
177
|
-
}
|
178
|
-
|
179
|
-
// Update select-all checkbox state
|
180
|
-
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
181
|
-
if (checkedBoxes.length === itemCheckboxes.length && itemCheckboxes.length > 0) {
|
182
|
-
selectAllCheckbox.checked = true;
|
183
|
-
selectAllCheckbox.indeterminate = false;
|
184
|
-
} else if (checkedBoxes.length > 0) {
|
185
|
-
selectAllCheckbox.checked = false;
|
186
|
-
selectAllCheckbox.indeterminate = true;
|
187
|
-
} else {
|
188
|
-
selectAllCheckbox.checked = false;
|
189
|
-
selectAllCheckbox.indeterminate = false;
|
190
|
-
}
|
191
|
-
}
|
192
|
-
|
193
|
-
function handleBulkAction(action) {
|
194
|
-
const checkedBoxes = document.querySelectorAll('.item-checkbox:checked');
|
195
|
-
|
196
|
-
if (checkedBoxes.length === 0) {
|
197
|
-
alert('Please select at least one item.');
|
198
|
-
return false;
|
199
|
-
}
|
200
|
-
|
201
|
-
const selectedItems = Array.from(checkedBoxes).map(cb => cb.value);
|
202
|
-
|
203
|
-
// Confirmation message
|
204
|
-
let message;
|
205
|
-
if (action === 'restore') {
|
206
|
-
message = 'Restore ' + checkedBoxes.length + ' selected item' + (checkedBoxes.length > 1 ? 's' : '') + '?';
|
207
|
-
} else {
|
208
|
-
message = 'Permanently delete ' + checkedBoxes.length + ' selected item' + (checkedBoxes.length > 1 ? 's' : '') + '? This cannot be undone!';
|
209
|
-
}
|
210
|
-
|
211
|
-
if (!confirm(message)) {
|
212
|
-
return false;
|
213
|
-
}
|
214
|
-
|
215
|
-
// Set the hidden input values
|
216
|
-
if (action === 'restore') {
|
217
|
-
document.getElementById('bulk-restore-items').value = JSON.stringify(selectedItems);
|
218
|
-
} else {
|
219
|
-
document.getElementById('bulk-destroy-items').value = JSON.stringify(selectedItems);
|
220
|
-
}
|
221
|
-
|
222
|
-
return true;
|
223
|
-
}
|
224
|
-
|
225
|
-
// Initialize bulk actions on page load
|
226
|
-
document.addEventListener('DOMContentLoaded', function() {
|
227
|
-
updateBulkActions();
|
228
|
-
});
|
229
|
-
</script>
|
60
|
+
<% end %>
|