recycle_bin 1.1.0 → 1.2.0
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 +57 -0
- data/README.md +228 -13
- data/app/controllers/recycle_bin/trash_controller.rb +424 -21
- data/app/helpers/recycle_bin/application_helper.rb +175 -0
- data/app/views/{layouts/recycle_bin/application.html.erb → recycle_bin/layouts/recycle_bin.html.erb} +850 -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 +75 -0
- data/app/views/recycle_bin/trash/_associations.html.erb +50 -0
- data/app/views/recycle_bin/trash/_filters.html.erb +561 -0
- data/app/views/recycle_bin/trash/_item.html.erb +267 -0
- data/app/views/recycle_bin/trash/_pagination.html.erb +75 -0
- data/app/views/recycle_bin/trash/_stats.html.erb +50 -0
- data/app/views/recycle_bin/trash/dashboard.html.erb +618 -0
- data/app/views/recycle_bin/trash/index.html.erb +247 -278
- data/app/views/recycle_bin/trash/show.html.erb +60 -215
- data/config/routes.rb +9 -2
- 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 +111 -1
- metadata +18 -8
- 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
- data/recycle_bin.gemspec +0 -47
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
<tr class="item-row" data-item-id="<%= item.id %>" data-model-type="<%= item.class.name %>">
|
|
2
|
+
<td class="checkbox-column">
|
|
3
|
+
<input type="checkbox"
|
|
4
|
+
class="item-checkbox"
|
|
5
|
+
value="<%= item.class.name %>:<%= item.id %>"
|
|
6
|
+
onchange="updateBulkCount()">
|
|
7
|
+
</td>
|
|
8
|
+
|
|
9
|
+
<td class="type-column">
|
|
10
|
+
<span class="model-badge <%= item.class.name.downcase %>">
|
|
11
|
+
<%= item.class.name %>
|
|
12
|
+
</span>
|
|
13
|
+
</td>
|
|
14
|
+
|
|
15
|
+
<td class="item-column">
|
|
16
|
+
<div class="item-content">
|
|
17
|
+
<div class="item-title">
|
|
18
|
+
<% if params[:search].present? %>
|
|
19
|
+
<%= highlight_search_terms(item.recyclable_title, params[:search]) %>
|
|
20
|
+
<% else %>
|
|
21
|
+
<%= item.recyclable_title %>
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="item-meta">
|
|
25
|
+
<span class="item-id">ID: <%= item.id %></span>
|
|
26
|
+
<% if item.respond_to?(:user_id) && item.user_id.present? %>
|
|
27
|
+
<span class="item-user">User: <%= item.user_id %></span>
|
|
28
|
+
<% end %>
|
|
29
|
+
<% if item.respond_to?(:email) && item.email.present? %>
|
|
30
|
+
<span class="item-email">
|
|
31
|
+
<% if params[:search].present? %>
|
|
32
|
+
<%= highlight_search_terms(item.email, params[:search]) %>
|
|
33
|
+
<% else %>
|
|
34
|
+
<%= item.email %>
|
|
35
|
+
<% end %>
|
|
36
|
+
</span>
|
|
37
|
+
<% end %>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</td>
|
|
41
|
+
|
|
42
|
+
<td class="timestamp-column">
|
|
43
|
+
<div class="timestamp-content">
|
|
44
|
+
<div class="timestamp-relative">
|
|
45
|
+
<%= time_ago_in_words(item.deleted_at) %> ago
|
|
46
|
+
</div>
|
|
47
|
+
<div class="timestamp-absolute">
|
|
48
|
+
<%= item.deleted_at.strftime('%B %d, %Y at %l:%M %p') %>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</td>
|
|
52
|
+
|
|
53
|
+
<td class="size-column">
|
|
54
|
+
<span class="size-badge <%= size_class(item) %>">
|
|
55
|
+
<%= human_file_size(calculate_item_memory_size(item)) %>
|
|
56
|
+
</span>
|
|
57
|
+
</td>
|
|
58
|
+
|
|
59
|
+
<td class="actions-column">
|
|
60
|
+
<div class="action-buttons">
|
|
61
|
+
<%= link_to "👁️ View", recycle_bin.trash_path(item.class.name, item),
|
|
62
|
+
class: "btn btn-sm btn-outline",
|
|
63
|
+
title: "View details" %>
|
|
64
|
+
<%= link_to "↶ Restore", recycle_bin.restore_trash_path(item.class.name, item),
|
|
65
|
+
method: :patch,
|
|
66
|
+
class: "btn btn-sm btn-success",
|
|
67
|
+
title: "Restore item",
|
|
68
|
+
data: { confirm: "Restore this #{item.class.name.downcase}?" } %>
|
|
69
|
+
<%= link_to "🗑️ Delete", recycle_bin.destroy_trash_path(item.class.name, item),
|
|
70
|
+
method: :delete,
|
|
71
|
+
class: "btn btn-sm btn-danger",
|
|
72
|
+
title: "Permanently delete",
|
|
73
|
+
data: { confirm: "Permanently delete this #{item.class.name.downcase}? This cannot be undone!" } %>
|
|
74
|
+
</div>
|
|
75
|
+
</td>
|
|
76
|
+
</tr>
|
|
77
|
+
|
|
78
|
+
<style>
|
|
79
|
+
.item-row {
|
|
80
|
+
transition: background-color 0.2s ease;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.item-row:hover {
|
|
84
|
+
background-color: #f8f9fa;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.item-content {
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
gap: 4px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.item-title {
|
|
94
|
+
font-weight: 600;
|
|
95
|
+
color: #212529;
|
|
96
|
+
line-height: 1.3;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.item-meta {
|
|
100
|
+
display: flex;
|
|
101
|
+
gap: 12px;
|
|
102
|
+
flex-wrap: wrap;
|
|
103
|
+
font-size: 0.85rem;
|
|
104
|
+
color: #6c757d;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.item-id, .item-user, .item-email {
|
|
108
|
+
background: #f8f9fa;
|
|
109
|
+
padding: 2px 6px;
|
|
110
|
+
border-radius: 4px;
|
|
111
|
+
border: 1px solid #e9ecef;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.timestamp-content {
|
|
115
|
+
display: flex;
|
|
116
|
+
flex-direction: column;
|
|
117
|
+
gap: 2px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.timestamp-relative {
|
|
121
|
+
font-weight: 600;
|
|
122
|
+
color: #495057;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.timestamp-absolute {
|
|
126
|
+
font-size: 0.8rem;
|
|
127
|
+
color: #6c757d;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.size-column {
|
|
131
|
+
text-align: center;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.size-badge {
|
|
135
|
+
padding: 4px 8px;
|
|
136
|
+
border-radius: 12px;
|
|
137
|
+
font-size: 0.8rem;
|
|
138
|
+
font-weight: 600;
|
|
139
|
+
text-align: center;
|
|
140
|
+
min-width: 60px;
|
|
141
|
+
display: inline-block;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.size-badge.small {
|
|
145
|
+
background: #d4edda;
|
|
146
|
+
color: #155724;
|
|
147
|
+
border: 1px solid #c3e6cb;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.size-badge.medium {
|
|
151
|
+
background: #fff3cd;
|
|
152
|
+
color: #856404;
|
|
153
|
+
border: 1px solid #ffeaa7;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.size-badge.large {
|
|
157
|
+
background: #f8d7da;
|
|
158
|
+
color: #721c24;
|
|
159
|
+
border: 1px solid #f5c6cb;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.action-buttons {
|
|
163
|
+
display: flex;
|
|
164
|
+
gap: 4px;
|
|
165
|
+
flex-wrap: wrap;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.action-buttons .btn {
|
|
169
|
+
font-size: 0.8rem;
|
|
170
|
+
padding: 4px 8px;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* Search highlighting */
|
|
174
|
+
mark {
|
|
175
|
+
background: #fff3cd;
|
|
176
|
+
color: #856404;
|
|
177
|
+
padding: 1px 3px;
|
|
178
|
+
border-radius: 3px;
|
|
179
|
+
font-weight: 600;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* Model type badges */
|
|
183
|
+
.model-badge {
|
|
184
|
+
padding: 4px 8px;
|
|
185
|
+
border-radius: 12px;
|
|
186
|
+
font-size: 0.75rem;
|
|
187
|
+
font-weight: 600;
|
|
188
|
+
text-transform: uppercase;
|
|
189
|
+
letter-spacing: 0.5px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.model-badge.user {
|
|
193
|
+
background: #e3f2fd;
|
|
194
|
+
color: #1976d2;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.model-badge.post {
|
|
198
|
+
background: #f3e5f5;
|
|
199
|
+
color: #7b1fa2;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.model-badge.comment {
|
|
203
|
+
background: #e8f5e8;
|
|
204
|
+
color: #388e3c;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.model-badge.order {
|
|
208
|
+
background: #fff3e0;
|
|
209
|
+
color: #f57c00;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.model-badge.product {
|
|
213
|
+
background: #fce4ec;
|
|
214
|
+
color: #c2185b;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* Responsive adjustments */
|
|
218
|
+
@media (max-width: 768px) {
|
|
219
|
+
.item-meta {
|
|
220
|
+
flex-direction: column;
|
|
221
|
+
gap: 4px;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.action-buttons {
|
|
225
|
+
flex-direction: column;
|
|
226
|
+
gap: 2px;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.action-buttons .btn {
|
|
230
|
+
width: 100%;
|
|
231
|
+
text-align: center;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
</style>
|
|
235
|
+
|
|
236
|
+
<script>
|
|
237
|
+
// Helper function to determine size class for styling
|
|
238
|
+
function sizeClass(item) {
|
|
239
|
+
// This will be handled by the helper method in the controller
|
|
240
|
+
return 'medium'; // Default fallback
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Update bulk count when checkboxes change
|
|
244
|
+
function updateBulkCount() {
|
|
245
|
+
const checkboxes = document.querySelectorAll('.item-checkbox:checked');
|
|
246
|
+
const bulkCount = document.getElementById('bulk-count');
|
|
247
|
+
const bulkActions = document.getElementById('bulk-actions');
|
|
248
|
+
|
|
249
|
+
if (bulkCount) {
|
|
250
|
+
bulkCount.textContent = `${checkboxes.length} item${checkboxes.length === 1 ? '' : 's'} selected`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (bulkActions) {
|
|
254
|
+
bulkActions.style.display = checkboxes.length > 0 ? 'flex' : 'none';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Update bulk action forms
|
|
258
|
+
const selectedItems = Array.from(checkboxes).map(cb => cb.value);
|
|
259
|
+
document.getElementById('bulk-restore-items').value = JSON.stringify(selectedItems);
|
|
260
|
+
document.getElementById('bulk-destroy-items').value = JSON.stringify(selectedItems);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Initialize bulk count on page load
|
|
264
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
265
|
+
updateBulkCount();
|
|
266
|
+
});
|
|
267
|
+
</script>
|
|
@@ -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,50 @@
|
|
|
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
|
+
<% if defined?(dashboard_view) && dashboard_view %>
|
|
7
|
+
<% today_count = filtered_items.select { |item| item.respond_to?(:deleted_at) && item.deleted_at && item.deleted_at > 1.day.ago }.count %>
|
|
8
|
+
<% else %>
|
|
9
|
+
<% today_count = filtered_items.items.select { |item| item.respond_to?(:deleted_at) && item.deleted_at && item.deleted_at > 1.day.ago }.count %>
|
|
10
|
+
<% end %>
|
|
11
|
+
<%= today_count %> deleted today
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="stat-card">
|
|
15
|
+
<div class="stat-number"><%= model_types.count %></div>
|
|
16
|
+
<div class="stat-label">Model Types</div>
|
|
17
|
+
<div class="stat-change">
|
|
18
|
+
<% if model_types.any? %>
|
|
19
|
+
<%= model_types.first(2).join(', ') %><%= model_types.count > 2 ? " +#{model_types.count - 2}" : "" %>
|
|
20
|
+
<% else %>
|
|
21
|
+
No types
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="stat-card">
|
|
26
|
+
<div class="stat-number">
|
|
27
|
+
<% if defined?(dashboard_view) && dashboard_view %>
|
|
28
|
+
<% week_count = filtered_items.select { |item| item.respond_to?(:deleted_at) && item.deleted_at && item.deleted_at > 7.days.ago }.count %>
|
|
29
|
+
<% else %>
|
|
30
|
+
<% week_count = filtered_items.items.select { |item| item.respond_to?(:deleted_at) && item.deleted_at && item.deleted_at > 7.days.ago }.count %>
|
|
31
|
+
<% end %>
|
|
32
|
+
<%= week_count %>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="stat-label">This Week</div>
|
|
35
|
+
<div class="stat-change">Recent activity</div>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="stat-card">
|
|
38
|
+
<% if defined?(dashboard_view) && dashboard_view %>
|
|
39
|
+
<div class="stat-number">🏠</div>
|
|
40
|
+
<div class="stat-label">Dashboard View</div>
|
|
41
|
+
<div class="stat-change">Overview</div>
|
|
42
|
+
<% else %>
|
|
43
|
+
<div class="stat-number"><%= current_page %> / <%= defined?(total_pages) ? total_pages : 1 %></div>
|
|
44
|
+
<div class="stat-label">Current Page</div>
|
|
45
|
+
<div class="stat-change">
|
|
46
|
+
Showing <%= (current_page - 1) * per_page + 1 %>-<%= [(current_page * per_page), total_count].min %> of <%= number_with_delimiter(total_count) %>
|
|
47
|
+
</div>
|
|
48
|
+
<% end %>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|