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.
@@ -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,6 @@
1
+ <% if notice %>
2
+ <div class="alert alert-success"><%= notice %></div>
3
+ <% end %>
4
+ <% if alert %>
5
+ <div class="alert alert-danger"><%= alert %></div>
6
+ <% end %>
@@ -0,0 +1,75 @@
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.respond_to?(:event) ? version.event.humanize : 'Unknown' %></span></td>
21
+ <td>
22
+ <% if version.respond_to?(:whodunnit) && version.whodunnit %>
23
+ <%= version.whodunnit %>
24
+ <% else %>
25
+ <span class="timestamp">System</span>
26
+ <% end %>
27
+ </td>
28
+ <td>
29
+ <span class="timestamp">
30
+ <% if version.respond_to?(:created_at) && version.created_at %>
31
+ <%= time_ago_in_words(version.created_at) %> ago •
32
+ <%= version.created_at.strftime('%B %d, %Y at %l:%M %p') %>
33
+ <% else %>
34
+ Unknown time
35
+ <% end %>
36
+ </span>
37
+ </td>
38
+ <td>
39
+ <% if version.respond_to?(:changeset) && version.changeset.any? %>
40
+ <details>
41
+ <summary class="btn btn-outline btn-sm">View Changes</summary>
42
+ <div class="changeset-details">
43
+ <% version.changeset.each do |key, changes| %>
44
+ <div class="change-item">
45
+ <strong><%= key.humanize %>:</strong>
46
+ <span class="change-from"><%= changes[0].nil? ? 'nil' : changes[0] %></span>
47
+
48
+ <span class="change-to"><%= changes[1].nil? ? 'nil' : changes[1] %></span>
49
+ </div>
50
+ <% end %>
51
+ </div>
52
+ </details>
53
+ <% else %>
54
+ <span class="timestamp">No changes recorded</span>
55
+ <% end %>
56
+ </td>
57
+ </tr>
58
+ <% end %>
59
+ </tbody>
60
+ </table>
61
+ </div>
62
+ <% else %>
63
+ <div class="empty-state">
64
+ <div class="empty-icon">📜</div>
65
+ <h4 class="empty-title">No history available</h4>
66
+ <p class="empty-subtitle">
67
+ This item has no recorded actions.
68
+ <% if Rails.env.development? %>
69
+ <br>Debug: <%= item.class.name %> ID: <%= item.id %> has <%= item.respond_to?(:versions) ? item.versions.count : 'no versions method' %> versions.
70
+ <% end %>
71
+ </p>
72
+ </div>
73
+ <% end %>
74
+ </div>
75
+ </div>
@@ -0,0 +1,50 @@
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
+ <% if item.respond_to?(:created_at) && item.created_at %>
25
+ Created <%= time_ago_in_words(item.created_at) %> ago
26
+ <% else %>
27
+ Created recently
28
+ <% end %>
29
+ <% if item.respond_to?(:deleted_at) && item.deleted_at %>
30
+ • <span class="deleted-status">Also deleted</span>
31
+ <% end %>
32
+ </div>
33
+ </div>
34
+ <% if item.respond_to?(:deleted_at) && item.deleted_at %>
35
+ <span class="model-badge deleted-badge">Deleted</span>
36
+ <% else %>
37
+ <span class="model-badge active-badge">Active</span>
38
+ <% end %>
39
+ </div>
40
+ <% end %>
41
+ </div>
42
+ <% else %>
43
+ <div class="empty-state">
44
+ <p class="timestamp">No related items found.</p>
45
+ </div>
46
+ <% end %>
47
+ </div>
48
+ <% end %>
49
+ </div>
50
+ </div>
@@ -0,0 +1,561 @@
1
+ <div class="filters-container">
2
+ <!-- Search Section -->
3
+ <div class="search-section">
4
+ <%= form_with url: recycle_bin.trash_index_path, method: :get, local: true, class: "search-form" do |form| %>
5
+ <div class="search-group">
6
+ <label class="search-label">🔍 Search Items</label>
7
+ <div class="search-input-wrapper">
8
+ <%= form.text_field :search,
9
+ value: params[:search],
10
+ placeholder: "Search across all fields...",
11
+ class: "search-input" %>
12
+ <%= form.submit "Search", class: "search-btn" %>
13
+ </div>
14
+ <% if params[:search].present? %>
15
+ <%= link_to "Clear Search", recycle_bin.trash_index_path(request.query_parameters.except(:search)),
16
+ class: "clear-search-btn" %>
17
+ <% end %>
18
+ </div>
19
+ <% end %>
20
+ </div>
21
+
22
+ <!-- Quick Filters -->
23
+ <div class="quick-filters-section">
24
+ <h4 class="filters-title">📊 Quick Filters</h4>
25
+
26
+ <%= form_with url: recycle_bin.trash_index_path, method: :get, local: true, id: "quick-filters-form", class: "quick-filters-form" do |form| %>
27
+ <!-- Preserve search term -->
28
+ <%= form.hidden_field :search, value: params[:search] if params[:search].present? %>
29
+
30
+ <div class="filters-grid">
31
+ <!-- Model Type Filter -->
32
+ <div class="filter-card">
33
+ <label class="filter-label">📁 Model Type</label>
34
+ <%= form.select :type,
35
+ options_for_select([['All Types', '']] + model_types.map { |type| [type, type] }, params[:type]),
36
+ {},
37
+ {
38
+ class: "filter-select",
39
+ onchange: "this.form.submit()"
40
+ } %>
41
+ </div>
42
+
43
+ <!-- Time Period Filter -->
44
+ <div class="filter-card">
45
+ <label class="filter-label">📅 Time Period</label>
46
+ <%= form.select :time,
47
+ options_for_select([
48
+ ['All Time', ''],
49
+ ['Today', 'today'],
50
+ ['This Week', 'week'],
51
+ ['This Month', 'month'],
52
+ ['This Year', 'year']
53
+ ], params[:time]),
54
+ {},
55
+ {
56
+ class: "filter-select",
57
+ onchange: "this.form.submit()"
58
+ } %>
59
+ </div>
60
+
61
+ <!-- Items Per Page -->
62
+ <div class="filter-card">
63
+ <label class="filter-label">📄 Items Per Page</label>
64
+ <%= form.select :per_page,
65
+ options_for_select([
66
+ ['25 items', '25'],
67
+ ['50 items', '50'],
68
+ ['100 items', '100'],
69
+ ['250 items', '250']
70
+ ], params[:per_page] || '25'),
71
+ {},
72
+ {
73
+ class: "filter-select",
74
+ onchange: "this.form.submit()"
75
+ } %>
76
+ </div>
77
+ </div>
78
+ <% end %>
79
+ </div>
80
+
81
+ <!-- Advanced Filters (Collapsible) -->
82
+ <div class="advanced-filters-section">
83
+ <div class="advanced-filters-header" onclick="toggleAdvancedFilters()">
84
+ <h4 class="filters-title">🔧 Advanced Filters</h4>
85
+ <span class="toggle-icon" id="advanced-toggle">▼</span>
86
+ </div>
87
+
88
+ <div id="advanced-filters-content" class="advanced-filters-content hidden">
89
+ <%= form_with url: recycle_bin.trash_index_path, method: :get, local: true, class: "advanced-filters-form" do |form| %>
90
+ <!-- Preserve existing filters -->
91
+ <%= form.hidden_field :search, value: params[:search] if params[:search].present? %>
92
+ <%= form.hidden_field :type, value: params[:type] if params[:type].present? %>
93
+ <%= form.hidden_field :time, value: params[:time] if params[:time].present? %>
94
+ <%= form.hidden_field :per_page, value: params[:per_page] if params[:per_page].present? %>
95
+
96
+ <div class="advanced-filters-grid">
97
+ <!-- User Filter -->
98
+ <div class="filter-card">
99
+ <label class="filter-label">👤 Deleted By User ID</label>
100
+ <%= form.text_field :deleted_by,
101
+ value: params[:deleted_by],
102
+ placeholder: "Enter user ID",
103
+ class: "filter-input" %>
104
+ </div>
105
+
106
+ <!-- Size Filter -->
107
+ <div class="filter-card">
108
+ <label class="filter-label">📊 Item Size</label>
109
+ <%= form.select :size,
110
+ options_for_select([
111
+ ['Any Size', ''],
112
+ ['Small (< 1KB)', 'small'],
113
+ ['Medium (1KB - 100KB)', 'medium'],
114
+ ['Large (> 100KB)', 'large']
115
+ ], params[:size]),
116
+ {},
117
+ { class: "filter-select" } %>
118
+ </div>
119
+
120
+ <!-- Date From -->
121
+ <div class="filter-card">
122
+ <label class="filter-label">📅 Date From</label>
123
+ <%= form.date_field :date_from,
124
+ value: params[:date_from],
125
+ class: "filter-input" %>
126
+ </div>
127
+
128
+ <!-- Date To -->
129
+ <div class="filter-card">
130
+ <label class="filter-label">📅 Date To</label>
131
+ <%= form.date_field :date_to,
132
+ value: params[:date_to],
133
+ class: "filter-input" %>
134
+ </div>
135
+ </div>
136
+
137
+ <!-- Advanced Filter Actions -->
138
+ <div class="advanced-filter-actions">
139
+ <%= form.submit "Apply Advanced Filters", class: "btn btn-primary" %>
140
+ <%= link_to "Clear All Filters", recycle_bin.trash_index_path, class: "btn btn-outline" %>
141
+ </div>
142
+ <% end %>
143
+ </div>
144
+ </div>
145
+
146
+ <!-- Active Filters Display -->
147
+ <% if any_filters_active? %>
148
+ <div class="active-filters-section">
149
+ <div class="active-filters-header">
150
+ <h5>🏷️ Active Filters</h5>
151
+ <%= link_to "Clear All", recycle_bin.trash_index_path, class: "clear-all-btn" %>
152
+ </div>
153
+
154
+ <div class="active-filters-list">
155
+ <% if params[:search].present? %>
156
+ <div class="filter-tag">
157
+ <span class="filter-type">Search:</span>
158
+ <span class="filter-value">"<%= params[:search] %>"</span>
159
+ <%= link_to "×", recycle_bin.trash_index_path(request.query_parameters.except(:search)),
160
+ class: "remove-filter-btn" %>
161
+ </div>
162
+ <% end %>
163
+
164
+ <% if params[:type].present? %>
165
+ <div class="filter-tag">
166
+ <span class="filter-type">Type:</span>
167
+ <span class="filter-value"><%= params[:type] %></span>
168
+ <%= link_to "×", recycle_bin.trash_index_path(request.query_parameters.except(:type)),
169
+ class: "remove-filter-btn" %>
170
+ </div>
171
+ <% end %>
172
+
173
+ <% if params[:time].present? %>
174
+ <div class="filter-tag">
175
+ <span class="filter-type">Time:</span>
176
+ <span class="filter-value"><%= time_filter_label(params[:time]) %></span>
177
+ <%= link_to "×", recycle_bin.trash_index_path(request.query_parameters.except(:time)),
178
+ class: "remove-filter-btn" %>
179
+ </div>
180
+ <% end %>
181
+
182
+ <% if params[:deleted_by].present? %>
183
+ <div class="filter-tag">
184
+ <span class="filter-type">User:</span>
185
+ <span class="filter-value"><%= params[:deleted_by] %></span>
186
+ <%= link_to "×", recycle_bin.trash_index_path(request.query_parameters.except(:deleted_by)),
187
+ class: "remove-filter-btn" %>
188
+ </div>
189
+ <% end %>
190
+
191
+ <% if params[:size].present? %>
192
+ <div class="filter-tag">
193
+ <span class="filter-type">Size:</span>
194
+ <span class="filter-value"><%= size_filter_label(params[:size]) %></span>
195
+ <%= link_to "×", recycle_bin.trash_index_path(request.query_parameters.except(:size)),
196
+ class: "remove-filter-btn" %>
197
+ </div>
198
+ <% end %>
199
+
200
+ <% if params[:date_from].present? || params[:date_to].present? %>
201
+ <div class="filter-tag">
202
+ <span class="filter-type">Date:</span>
203
+ <span class="filter-value"><%= [params[:date_from], params[:date_to]].compact.join(' to ') %></span>
204
+ <%= link_to "×", recycle_bin.trash_index_path(request.query_parameters.except(:date_from, :date_to)),
205
+ class: "remove-filter-btn" %>
206
+ </div>
207
+ <% end %>
208
+ </div>
209
+ </div>
210
+ <% end %>
211
+ </div>
212
+
213
+ <script>
214
+ function toggleAdvancedFilters() {
215
+ const content = document.getElementById('advanced-filters-content');
216
+ const toggle = document.getElementById('advanced-toggle');
217
+
218
+ if (content.classList.contains('hidden')) {
219
+ content.classList.remove('hidden');
220
+ toggle.textContent = '▲';
221
+ } else {
222
+ content.classList.add('hidden');
223
+ toggle.textContent = '▼';
224
+ }
225
+ }
226
+
227
+ // Show advanced filters if any advanced filter is active
228
+ document.addEventListener('DOMContentLoaded', function() {
229
+ const hasAdvancedFilters = '<%= params[:deleted_by].present? || params[:size].present? || params[:date_from].present? || params[:date_to].present? %>' === 'true';
230
+
231
+ if (hasAdvancedFilters) {
232
+ toggleAdvancedFilters();
233
+ }
234
+ });
235
+ </script>
236
+
237
+ <style>
238
+ .filters-container {
239
+ background: white;
240
+ border-radius: 12px;
241
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
242
+ margin-bottom: 24px;
243
+ overflow: hidden;
244
+ }
245
+
246
+ /* Search Section */
247
+ .search-section {
248
+ padding: 24px;
249
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
250
+ color: white;
251
+ }
252
+
253
+ .search-form {
254
+ width: 100%;
255
+ }
256
+
257
+ .search-group {
258
+ display: flex;
259
+ flex-direction: column;
260
+ gap: 12px;
261
+ }
262
+
263
+ .search-label {
264
+ font-size: 16px;
265
+ font-weight: 600;
266
+ margin: 0;
267
+ }
268
+
269
+ .search-input-wrapper {
270
+ display: flex;
271
+ gap: 8px;
272
+ align-items: stretch;
273
+ }
274
+
275
+ .search-input {
276
+ flex: 1;
277
+ padding: 12px 16px;
278
+ border: none;
279
+ border-radius: 8px;
280
+ font-size: 16px;
281
+ outline: none;
282
+ background: rgba(255, 255, 255, 0.95);
283
+ color: #333;
284
+ }
285
+
286
+ .search-input::placeholder {
287
+ color: #666;
288
+ }
289
+
290
+ .search-btn {
291
+ padding: 12px 24px;
292
+ background: rgba(255, 255, 255, 0.2);
293
+ color: white;
294
+ border: 2px solid rgba(255, 255, 255, 0.3);
295
+ border-radius: 8px;
296
+ cursor: pointer;
297
+ font-weight: 600;
298
+ transition: all 0.3s ease;
299
+ white-space: nowrap;
300
+ }
301
+
302
+ .search-btn:hover {
303
+ background: rgba(255, 255, 255, 0.3);
304
+ border-color: rgba(255, 255, 255, 0.5);
305
+ }
306
+
307
+ .clear-search-btn {
308
+ align-self: flex-start;
309
+ padding: 6px 12px;
310
+ background: rgba(255, 255, 255, 0.2);
311
+ color: white;
312
+ text-decoration: none;
313
+ border-radius: 6px;
314
+ font-size: 14px;
315
+ border: 1px solid rgba(255, 255, 255, 0.3);
316
+ transition: all 0.3s ease;
317
+ }
318
+
319
+ .clear-search-btn:hover {
320
+ background: rgba(255, 255, 255, 0.3);
321
+ text-decoration: none;
322
+ color: white;
323
+ }
324
+
325
+ /* Quick Filters Section */
326
+ .quick-filters-section {
327
+ padding: 24px;
328
+ border-bottom: 1px solid #e9ecef;
329
+ }
330
+
331
+ .filters-title {
332
+ font-size: 18px;
333
+ font-weight: 600;
334
+ color: #495057;
335
+ margin: 0 0 16px 0;
336
+ display: flex;
337
+ align-items: center;
338
+ gap: 8px;
339
+ }
340
+
341
+ .quick-filters-form {
342
+ width: 100%;
343
+ }
344
+
345
+ .filters-grid {
346
+ display: grid;
347
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
348
+ gap: 20px;
349
+ }
350
+
351
+ .filter-card {
352
+ display: flex;
353
+ flex-direction: column;
354
+ gap: 8px;
355
+ }
356
+
357
+ .filter-label {
358
+ font-size: 14px;
359
+ font-weight: 600;
360
+ color: #495057;
361
+ margin: 0;
362
+ }
363
+
364
+ .filter-select,
365
+ .filter-input {
366
+ padding: 10px 12px;
367
+ border: 2px solid #e9ecef;
368
+ border-radius: 8px;
369
+ font-size: 14px;
370
+ transition: border-color 0.3s ease, box-shadow 0.3s ease;
371
+ background: white;
372
+ }
373
+
374
+ .filter-select:focus,
375
+ .filter-input:focus {
376
+ outline: none;
377
+ border-color: #667eea;
378
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
379
+ }
380
+
381
+ /* Advanced Filters Section */
382
+ .advanced-filters-section {
383
+ border-top: 1px solid #e9ecef;
384
+ }
385
+
386
+ .advanced-filters-header {
387
+ padding: 20px 24px;
388
+ background: #f8f9fa;
389
+ cursor: pointer;
390
+ display: flex;
391
+ justify-content: space-between;
392
+ align-items: center;
393
+ transition: background-color 0.3s ease;
394
+ }
395
+
396
+ .advanced-filters-header:hover {
397
+ background: #e9ecef;
398
+ }
399
+
400
+ .toggle-icon {
401
+ font-size: 14px;
402
+ transition: transform 0.3s ease;
403
+ color: #667eea;
404
+ font-weight: bold;
405
+ }
406
+
407
+ .advanced-filters-content {
408
+ padding: 24px;
409
+ background: #f8f9fa;
410
+ border-top: 1px solid #e9ecef;
411
+ }
412
+
413
+ .advanced-filters-content.hidden {
414
+ display: none;
415
+ }
416
+
417
+ .advanced-filters-grid {
418
+ display: grid;
419
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
420
+ gap: 20px;
421
+ margin-bottom: 24px;
422
+ }
423
+
424
+ .advanced-filter-actions {
425
+ display: flex;
426
+ gap: 12px;
427
+ justify-content: center;
428
+ }
429
+
430
+ /* Active Filters Section */
431
+ .active-filters-section {
432
+ padding: 20px 24px;
433
+ background: #e3f2fd;
434
+ border-top: 1px solid #bbdefb;
435
+ }
436
+
437
+ .active-filters-header {
438
+ display: flex;
439
+ justify-content: space-between;
440
+ align-items: center;
441
+ margin-bottom: 16px;
442
+ }
443
+
444
+ .active-filters-header h5 {
445
+ font-size: 16px;
446
+ font-weight: 600;
447
+ color: #1976d2;
448
+ margin: 0;
449
+ }
450
+
451
+ .clear-all-btn {
452
+ padding: 6px 12px;
453
+ background: #1976d2;
454
+ color: white;
455
+ text-decoration: none;
456
+ border-radius: 6px;
457
+ font-size: 14px;
458
+ font-weight: 500;
459
+ transition: background-color 0.3s ease;
460
+ }
461
+
462
+ .clear-all-btn:hover {
463
+ background: #1565c0;
464
+ text-decoration: none;
465
+ color: white;
466
+ }
467
+
468
+ .active-filters-list {
469
+ display: flex;
470
+ flex-wrap: wrap;
471
+ gap: 12px;
472
+ }
473
+
474
+ .filter-tag {
475
+ background: white;
476
+ border: 1px solid #bbdefb;
477
+ border-radius: 20px;
478
+ padding: 8px 16px;
479
+ display: flex;
480
+ align-items: center;
481
+ gap: 6px;
482
+ font-size: 14px;
483
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
484
+ }
485
+
486
+ .filter-type {
487
+ font-weight: 600;
488
+ color: #1976d2;
489
+ }
490
+
491
+ .filter-value {
492
+ color: #1565c0;
493
+ }
494
+
495
+ .remove-filter-btn {
496
+ color: #d32f2f;
497
+ text-decoration: none;
498
+ font-weight: bold;
499
+ font-size: 16px;
500
+ line-height: 1;
501
+ padding: 0 4px;
502
+ border-radius: 50%;
503
+ transition: all 0.3s ease;
504
+ }
505
+
506
+ .remove-filter-btn:hover {
507
+ background: #d32f2f;
508
+ color: white;
509
+ text-decoration: none;
510
+ }
511
+
512
+ /* Responsive Design */
513
+ @media (max-width: 768px) {
514
+ .filters-container {
515
+ margin: 16px;
516
+ border-radius: 8px;
517
+ }
518
+
519
+ .search-section,
520
+ .quick-filters-section,
521
+ .advanced-filters-content,
522
+ .active-filters-section {
523
+ padding: 16px;
524
+ }
525
+
526
+ .search-input-wrapper {
527
+ flex-direction: column;
528
+ }
529
+
530
+ .filters-grid,
531
+ .advanced-filters-grid {
532
+ grid-template-columns: 1fr;
533
+ gap: 16px;
534
+ }
535
+
536
+ .advanced-filter-actions {
537
+ flex-direction: column;
538
+ }
539
+
540
+ .active-filters-list {
541
+ gap: 8px;
542
+ }
543
+
544
+ .filter-tag {
545
+ font-size: 13px;
546
+ padding: 6px 12px;
547
+ }
548
+ }
549
+
550
+ @media (max-width: 480px) {
551
+ .active-filters-header {
552
+ flex-direction: column;
553
+ gap: 12px;
554
+ align-items: flex-start;
555
+ }
556
+
557
+ .clear-all-btn {
558
+ align-self: flex-end;
559
+ }
560
+ }
561
+ </style>