recycle_bin 1.0.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 +7 -0
- data/CHANGELOG.md +16 -0
- data/Gemfile +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +425 -0
- data/Rakefile +12 -0
- data/app/controllers/recycle_bin/application_controller.rb +102 -0
- data/app/controllers/recycle_bin/trash_controller.rb +213 -0
- data/app/helpers/recycle_bin/application_helper.rb +31 -0
- data/app/views/layouts/recycle_bin/application.html.erb +609 -0
- data/app/views/layouts/recycle_bin/recycle_bin/application.html.erb +266 -0
- data/app/views/layouts/recycle_bin/recycle_bin/trash/index.html.erb +133 -0
- data/app/views/layouts/recycle_bin/recycle_bin/trash/show.html.erb +175 -0
- data/app/views/recycle_bin/trash/index.html.erb +229 -0
- data/app/views/recycle_bin/trash/show.html.erb +288 -0
- data/config/routes.rb +19 -0
- data/lib/generators/recycle_bin/add_deleted_at/add_deleted_at_generator.rb +49 -0
- data/lib/generators/recycle_bin/add_deleted_at/templates/add_deleted_at_migration.rb.erb +6 -0
- data/lib/generators/recycle_bin/install/install_generator.rb +82 -0
- data/lib/generators/recycle_bin/install/templates/recycle_bin.rb +24 -0
- data/lib/recycle_bin/engine.rb +21 -0
- data/lib/recycle_bin/install/templates/recycle_bin.rb +24 -0
- data/lib/recycle_bin/soft_deletable.rb +101 -0
- data/lib/recycle_bin/version.rb +5 -0
- data/lib/recycle_bin.rb +121 -0
- data/recycle_bin.gemspec +47 -0
- data/sig/recycle_bin.rbs +4 -0
- metadata +144 -0
@@ -0,0 +1,229 @@
|
|
1
|
+
<h2>Deleted Items (<%= @deleted_items.count %>)</h2>
|
2
|
+
|
3
|
+
<!-- Statistics Dashboard -->
|
4
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px;">
|
5
|
+
<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;">
|
6
|
+
<div style="font-size: 2rem; font-weight: bold; color: #667eea; margin-bottom: 8px;"><%= @deleted_items.count %></div>
|
7
|
+
<div style="color: #6c757d; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.5px;">Items in Trash</div>
|
8
|
+
<div style="font-size: 0.8rem; margin-top: 8px; color: #28a745;">
|
9
|
+
<%= @deleted_items.select { |item| item.deleted_at > 1.day.ago }.count %> today
|
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
|
+
|
56
|
+
<% if @deleted_items.any? %>
|
57
|
+
<div style="background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); overflow: hidden;">
|
58
|
+
|
59
|
+
<!-- Bulk Actions Bar (Initially Hidden) -->
|
60
|
+
<div id="bulk-actions" style="display: none; background: #fff3cd; padding: 16px 20px; border-bottom: 1px solid #ffeaa7; justify-content: space-between; align-items: center;">
|
61
|
+
<span id="bulk-count" style="font-weight: 500; color: #856404;">0 items selected</span>
|
62
|
+
<div style="display: flex; gap: 8px;">
|
63
|
+
<%= form_with url: "/recycle_bin/trash/bulk_restore", method: :patch, local: true, style: "display: inline;" do |form| %>
|
64
|
+
<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')" %>
|
68
|
+
<% end %>
|
69
|
+
|
70
|
+
<%= form_with url: "/recycle_bin/trash/bulk_destroy", method: :delete, local: true, style: "display: inline;" do |form| %>
|
71
|
+
<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')" %>
|
75
|
+
<% end %>
|
76
|
+
</div>
|
77
|
+
</div>
|
78
|
+
|
79
|
+
<table style="width: 100%; border-collapse: collapse;">
|
80
|
+
<thead>
|
81
|
+
<tr style="background: #f8f9fa; border-bottom: 2px solid #dee2e6;">
|
82
|
+
<th style="padding: 16px; text-align: left; font-weight: 600; color: #495057; width: 40px;">
|
83
|
+
<input type="checkbox" id="select-all" style="cursor: pointer;" onchange="toggleSelectAll()">
|
84
|
+
</th>
|
85
|
+
<th style="padding: 16px; text-align: left; font-weight: 600; color: #495057;">Type</th>
|
86
|
+
<th style="padding: 16px; text-align: left; font-weight: 600; color: #495057;">Item</th>
|
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>
|
129
|
+
</tr>
|
130
|
+
<% end %>
|
131
|
+
</tbody>
|
132
|
+
</table>
|
133
|
+
</div>
|
134
|
+
|
135
|
+
<div style="text-align: center; padding: 20px; color: #6c757d;">
|
136
|
+
Showing <%= @deleted_items.count %> items
|
137
|
+
</div>
|
138
|
+
|
139
|
+
<% else %>
|
140
|
+
<div class="empty-state">
|
141
|
+
<div style="font-size: 48px; margin-bottom: 20px;">🎉</div>
|
142
|
+
<h3>Your recycle bin is empty!</h3>
|
143
|
+
<p>Deleted items will appear here and can be restored or permanently removed.</p>
|
144
|
+
|
145
|
+
<div style="margin-top: 30px;">
|
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>
|
149
|
+
</div>
|
150
|
+
</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>
|
@@ -0,0 +1,288 @@
|
|
1
|
+
<!-- Breadcrumb -->
|
2
|
+
<div style="margin-bottom: 2rem;">
|
3
|
+
<%= link_to "← Back to Dashboard", recycle_bin.root_path,
|
4
|
+
style: "color: #667eea; text-decoration: none; font-weight: 500;" %>
|
5
|
+
</div>
|
6
|
+
|
7
|
+
<!-- Item Header Card -->
|
8
|
+
<div class="main-card" style="margin-bottom: 2rem;">
|
9
|
+
<div class="card-header">
|
10
|
+
<div>
|
11
|
+
<h1 class="card-title" style="margin-bottom: 0.5rem;">
|
12
|
+
<span class="model-badge" style="margin-right: 1rem;"><%= @item.class.name %></span>
|
13
|
+
<%= @item.recyclable_title %>
|
14
|
+
</h1>
|
15
|
+
<div class="timestamp">
|
16
|
+
Deleted <%= time_ago_in_words(@item.deleted_at) %> ago •
|
17
|
+
<%= @item.deleted_at.strftime('%B %d, %Y at %l:%M %p') %>
|
18
|
+
</div>
|
19
|
+
</div>
|
20
|
+
|
21
|
+
<div class="card-actions">
|
22
|
+
<%= link_to recycle_bin.restore_trash_path(@item.class.name, @item),
|
23
|
+
method: :patch,
|
24
|
+
class: "btn btn-success",
|
25
|
+
data: { confirm: "Restore this #{@item.class.name.downcase}?" } do %>
|
26
|
+
↶ Restore Item
|
27
|
+
<% end %>
|
28
|
+
<%= link_to recycle_bin.destroy_trash_path(@item.class.name, @item),
|
29
|
+
method: :delete,
|
30
|
+
class: "btn btn-danger",
|
31
|
+
data: { confirm: "Permanently delete this #{@item.class.name.downcase}? This cannot be undone!" } do %>
|
32
|
+
🗑️ Delete Forever
|
33
|
+
<% end %>
|
34
|
+
</div>
|
35
|
+
</div>
|
36
|
+
|
37
|
+
<!-- Status Info -->
|
38
|
+
<div style="padding: 1rem 1.5rem; background: #fff3cd; border-bottom: 1px solid #ffeaa7;">
|
39
|
+
<div style="display: flex; align-items: center; gap: 1rem;">
|
40
|
+
<span style="color: #856404; font-weight: 500;">
|
41
|
+
🗑️ This item is in the recycle bin
|
42
|
+
</span>
|
43
|
+
<span class="timestamp">
|
44
|
+
Size: <%= number_to_human_size(@item_memory_size) %> •
|
45
|
+
ID: <%= @item.id %>
|
46
|
+
</span>
|
47
|
+
</div>
|
48
|
+
</div>
|
49
|
+
</div>
|
50
|
+
|
51
|
+
<!-- Original Data -->
|
52
|
+
<div class="main-card">
|
53
|
+
<div class="card-header">
|
54
|
+
<h3 class="card-title">Original Data</h3>
|
55
|
+
<div class="card-actions">
|
56
|
+
<button class="btn btn-sm btn-outline" onclick="toggleRawData()">
|
57
|
+
📋 Toggle Raw View
|
58
|
+
</button>
|
59
|
+
</div>
|
60
|
+
</div>
|
61
|
+
|
62
|
+
<div style="padding: 1.5rem;">
|
63
|
+
<% if @original_attributes.any? %>
|
64
|
+
<div class="table-container">
|
65
|
+
<table class="table">
|
66
|
+
<thead>
|
67
|
+
<tr>
|
68
|
+
<th style="width: 30%;">Field</th>
|
69
|
+
<th>Value</th>
|
70
|
+
<th style="width: 15%;">Type</th>
|
71
|
+
</tr>
|
72
|
+
</thead>
|
73
|
+
<tbody>
|
74
|
+
<% @original_attributes.each do |key, value| %>
|
75
|
+
<tr>
|
76
|
+
<td>
|
77
|
+
<strong><%= key.humanize %></strong>
|
78
|
+
</td>
|
79
|
+
<td>
|
80
|
+
<% if value.nil? %>
|
81
|
+
<em style="color: #6c757d;">nil</em>
|
82
|
+
<% elsif value.is_a?(String) && value.length > 100 %>
|
83
|
+
<div>
|
84
|
+
<div><%= truncate(value, length: 100) %></div>
|
85
|
+
<details style="margin-top: 0.5rem;">
|
86
|
+
<summary style="cursor: pointer; color: #667eea; font-size: 0.9rem;">
|
87
|
+
Show full content (<%= value.length %> characters)
|
88
|
+
</summary>
|
89
|
+
<div style="margin-top: 0.5rem; padding: 1rem; background: #f8f9fa; border-radius: 4px; white-space: pre-wrap; font-family: monospace; font-size: 0.85rem;">
|
90
|
+
<%= value %>
|
91
|
+
</div>
|
92
|
+
</details>
|
93
|
+
</div>
|
94
|
+
<% elsif value.is_a?(Time) || value.is_a?(DateTime) %>
|
95
|
+
<div>
|
96
|
+
<%= value.strftime('%B %d, %Y at %l:%M %p') %>
|
97
|
+
<div class="timestamp">(<%= time_ago_in_words(value) %> ago)</div>
|
98
|
+
</div>
|
99
|
+
<% elsif value.is_a?(Date) %>
|
100
|
+
<%= value.strftime('%B %d, %Y') %>
|
101
|
+
<% elsif value == true %>
|
102
|
+
<span style="color: #28a745; font-weight: 500;">✓ True</span>
|
103
|
+
<% elsif value == false %>
|
104
|
+
<span style="color: #dc3545; font-weight: 500;">✗ False</span>
|
105
|
+
<% elsif value.is_a?(Numeric) %>
|
106
|
+
<span style="font-family: monospace;"><%= number_with_delimiter(value) %></span>
|
107
|
+
<% else %>
|
108
|
+
<%= value %>
|
109
|
+
<% end %>
|
110
|
+
</td>
|
111
|
+
<td>
|
112
|
+
<span class="timestamp">
|
113
|
+
<%= value.class.name.downcase %>
|
114
|
+
</span>
|
115
|
+
</td>
|
116
|
+
</tr>
|
117
|
+
<% end %>
|
118
|
+
</tbody>
|
119
|
+
</table>
|
120
|
+
</div>
|
121
|
+
|
122
|
+
<!-- Raw Data View (Hidden by default) -->
|
123
|
+
<div id="raw-data" style="display: none; margin-top: 2rem;">
|
124
|
+
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; border: 1px solid #dee2e6;">
|
125
|
+
<h4 style="margin-bottom: 1rem; color: #495057;">Raw JSON Data</h4>
|
126
|
+
<pre style="background: #2d3748; color: #e2e8f0; padding: 1rem; border-radius: 4px; overflow-x: auto; font-size: 0.85rem; line-height: 1.4;"><%= JSON.pretty_generate(@original_attributes) %></pre>
|
127
|
+
</div>
|
128
|
+
</div>
|
129
|
+
|
130
|
+
<% else %>
|
131
|
+
<div class="empty-state">
|
132
|
+
<div class="empty-icon">📄</div>
|
133
|
+
<h4>No data available</h4>
|
134
|
+
<p>This item doesn't have any stored attributes.</p>
|
135
|
+
</div>
|
136
|
+
<% end %>
|
137
|
+
</div>
|
138
|
+
</div>
|
139
|
+
|
140
|
+
<!-- Associations (if any) -->
|
141
|
+
<% if @associations&.any? %>
|
142
|
+
<div class="main-card" style="margin-top: 2rem;">
|
143
|
+
<div class="card-header">
|
144
|
+
<h3 class="card-title">Related Items</h3>
|
145
|
+
<div class="card-actions">
|
146
|
+
<span class="timestamp"><%= @associations.values.flatten.count %> related items</span>
|
147
|
+
</div>
|
148
|
+
</div>
|
149
|
+
|
150
|
+
<div style="padding: 1.5rem;">
|
151
|
+
<% @associations.each do |association_name, items| %>
|
152
|
+
<div style="margin-bottom: 2rem;">
|
153
|
+
<h4 style="color: #495057; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid #dee2e6;">
|
154
|
+
<%= association_name.humanize %>
|
155
|
+
<span class="timestamp">(<%= items.count %> items)</span>
|
156
|
+
</h4>
|
157
|
+
|
158
|
+
<% if items.any? %>
|
159
|
+
<div style="display: grid; gap: 1rem;">
|
160
|
+
<% items.each do |item| %>
|
161
|
+
<div style="display: flex; align-items: center; padding: 1rem; background: #f8f9fa; border-radius: 6px; border-left: 3px solid #667eea;">
|
162
|
+
<div style="flex: 1;">
|
163
|
+
<div style="font-weight: 500; color: #495057;">
|
164
|
+
<%= item.class.name %> -
|
165
|
+
<%= item.respond_to?(:title) ? item.title :
|
166
|
+
item.respond_to?(:name) ? item.name :
|
167
|
+
"ID: #{item.id}" %>
|
168
|
+
</div>
|
169
|
+
<div class="timestamp">
|
170
|
+
Created <%= time_ago_in_words(item.created_at) %> ago
|
171
|
+
<% if item.respond_to?(:deleted_at) && item.deleted_at %>
|
172
|
+
• <span style="color: #dc3545;">Also deleted</span>
|
173
|
+
<% end %>
|
174
|
+
</div>
|
175
|
+
</div>
|
176
|
+
|
177
|
+
<% if item.respond_to?(:deleted_at) && item.deleted_at %>
|
178
|
+
<span class="model-badge" style="background: #dc3545;">Deleted</span>
|
179
|
+
<% else %>
|
180
|
+
<span class="model-badge" style="background: #28a745;">Active</span>
|
181
|
+
<% end %>
|
182
|
+
</div>
|
183
|
+
<% end %>
|
184
|
+
</div>
|
185
|
+
<% else %>
|
186
|
+
<div class="empty-state" style="padding: 2rem;">
|
187
|
+
<p style="color: #6c757d; font-style: italic;">No related items found.</p>
|
188
|
+
</div>
|
189
|
+
<% end %>
|
190
|
+
</div>
|
191
|
+
<% end %>
|
192
|
+
</div>
|
193
|
+
</div>
|
194
|
+
<% end %>
|
195
|
+
|
196
|
+
<!-- Action History -->
|
197
|
+
<div class="main-card" style="margin-top: 2rem;">
|
198
|
+
<div class="card-header">
|
199
|
+
<h3 class="card-title">Action History</h3>
|
200
|
+
</div>
|
201
|
+
|
202
|
+
<div style="padding: 1.5rem;">
|
203
|
+
<div style="display: grid; gap: 1rem;">
|
204
|
+
<!-- Deletion Event -->
|
205
|
+
<div style="display: flex; align-items: start; gap: 1rem; padding: 1rem; border-left: 3px solid #dc3545; background: #fff5f5;">
|
206
|
+
<div style="font-size: 1.5rem;">🗑️</div>
|
207
|
+
<div style="flex: 1;">
|
208
|
+
<div style="font-weight: 500; color: #495057;">Item Deleted</div>
|
209
|
+
<div class="timestamp">
|
210
|
+
<%= @item.deleted_at.strftime('%B %d, %Y at %l:%M %p') %>
|
211
|
+
(<%= time_ago_in_words(@item.deleted_at) %> ago)
|
212
|
+
</div>
|
213
|
+
<% if @item.respond_to?(:deleted_by) && @item.deleted_by %>
|
214
|
+
<div class="timestamp">by <%= @item.deleted_by %></div>
|
215
|
+
<% end %>
|
216
|
+
</div>
|
217
|
+
</div>
|
218
|
+
|
219
|
+
<!-- Creation Event -->
|
220
|
+
<div style="display: flex; align-items: start; gap: 1rem; padding: 1rem; border-left: 3px solid #28a745; background: #f0fff4;">
|
221
|
+
<div style="font-size: 1.5rem;">✨</div>
|
222
|
+
<div style="flex: 1;">
|
223
|
+
<div style="font-weight: 500; color: #495057;">Item Created</div>
|
224
|
+
<div class="timestamp">
|
225
|
+
<%= @item.created_at.strftime('%B %d, %Y at %l:%M %p') %>
|
226
|
+
(<%= time_ago_in_words(@item.created_at) %> ago)
|
227
|
+
</div>
|
228
|
+
</div>
|
229
|
+
</div>
|
230
|
+
</div>
|
231
|
+
</div>
|
232
|
+
</div>
|
233
|
+
|
234
|
+
<!-- Quick Actions -->
|
235
|
+
<div class="main-card" style="margin-top: 2rem; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);">
|
236
|
+
<div style="padding: 2rem; text-align: center;">
|
237
|
+
<h4 style="color: #495057; margin-bottom: 1.5rem;">Quick Actions</h4>
|
238
|
+
|
239
|
+
<div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;">
|
240
|
+
<%= link_to recycle_bin.restore_trash_path(@item.class.name, @item),
|
241
|
+
method: :patch,
|
242
|
+
class: "btn btn-success",
|
243
|
+
data: { confirm: "Restore this #{@item.class.name.downcase}?" } do %>
|
244
|
+
↶ Restore to Application
|
245
|
+
<% end %>
|
246
|
+
|
247
|
+
<%= link_to recycle_bin.destroy_trash_path(@item.class.name, @item),
|
248
|
+
method: :delete,
|
249
|
+
class: "btn btn-danger",
|
250
|
+
data: { confirm: "Permanently delete this #{@item.class.name.downcase}? This cannot be undone!" } do %>
|
251
|
+
🗑️ Delete Permanently
|
252
|
+
<% end %>
|
253
|
+
|
254
|
+
<%= link_to recycle_bin.root_path, class: "btn btn-outline" do %>
|
255
|
+
📋 Back to Dashboard
|
256
|
+
<% end %>
|
257
|
+
|
258
|
+
<button class="btn btn-outline" onclick="window.print()">
|
259
|
+
🖨️ Print Details
|
260
|
+
</button>
|
261
|
+
</div>
|
262
|
+
</div>
|
263
|
+
</div>
|
264
|
+
|
265
|
+
<!-- JavaScript for interactions -->
|
266
|
+
<script>
|
267
|
+
function toggleRawData() {
|
268
|
+
const rawData = document.getElementById('raw-data');
|
269
|
+
if (rawData.style.display === 'none') {
|
270
|
+
rawData.style.display = 'block';
|
271
|
+
} else {
|
272
|
+
rawData.style.display = 'none';
|
273
|
+
}
|
274
|
+
}
|
275
|
+
|
276
|
+
// Auto-copy functionality for technical details
|
277
|
+
document.addEventListener('click', function(e) {
|
278
|
+
if (e.target.matches('[data-copy]')) {
|
279
|
+
const text = e.target.dataset.copy;
|
280
|
+
navigator.clipboard.writeText(text).then(() => {
|
281
|
+
e.target.textContent = '✓ Copied!';
|
282
|
+
setTimeout(() => {
|
283
|
+
e.target.textContent = text;
|
284
|
+
}, 2000);
|
285
|
+
});
|
286
|
+
}
|
287
|
+
});
|
288
|
+
</script>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RecycleBin::Engine.routes.draw do
|
4
|
+
root 'trash#index'
|
5
|
+
|
6
|
+
# Standard resource routes
|
7
|
+
resources :trash, only: [:index] do
|
8
|
+
collection do
|
9
|
+
patch :bulk_restore
|
10
|
+
delete :bulk_destroy
|
11
|
+
delete :cleanup_large_items
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Custom routes for individual items with model type
|
16
|
+
get 'trash/:model_type/:id', to: 'trash#show', as: 'trash'
|
17
|
+
patch 'trash/:model_type/:id/restore', to: 'trash#restore', as: 'restore_trash'
|
18
|
+
delete 'trash/:model_type/:id', to: 'trash#destroy', as: 'destroy_trash'
|
19
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
require 'rails/generators/migration'
|
5
|
+
|
6
|
+
module RecycleBin
|
7
|
+
module Generators
|
8
|
+
class AddDeletedAtGenerator < ::Rails::Generators::NamedBase
|
9
|
+
include Rails::Generators::Migration
|
10
|
+
|
11
|
+
source_root File.expand_path('templates', __dir__)
|
12
|
+
desc 'Add deleted_at column to a model for soft delete functionality'
|
13
|
+
|
14
|
+
def self.next_migration_number(path)
|
15
|
+
next_migration_number = current_migration_number(path) + 1
|
16
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_migration
|
20
|
+
migration_template 'add_deleted_at_migration.rb.erb',
|
21
|
+
"db/migrate/add_deleted_at_to_#{table_name}.rb"
|
22
|
+
end
|
23
|
+
|
24
|
+
def show_instructions
|
25
|
+
say ''
|
26
|
+
say 'Next steps:', :green
|
27
|
+
say '1. Run the migration: rails db:migrate'
|
28
|
+
say "2. Include RecycleBin::SoftDeletable in your #{class_name} model:"
|
29
|
+
say ''
|
30
|
+
say " class #{class_name} < ApplicationRecord", :yellow
|
31
|
+
say ' include RecycleBin::SoftDeletable', :yellow
|
32
|
+
say ' end', :yellow
|
33
|
+
say ''
|
34
|
+
say "3. Your #{class_name.downcase} records will now be soft deleted when you call .destroy"
|
35
|
+
say '4. Visit /recycle_bin to manage deleted items'
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def table_name
|
41
|
+
@table_name ||= name.tableize
|
42
|
+
end
|
43
|
+
|
44
|
+
def class_name
|
45
|
+
@class_name ||= name.classify
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|