recycle_bin 1.1.1 → 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.
@@ -27,5 +27,180 @@ module RecycleBin
27
27
  time.strftime('%B %d, %Y')
28
28
  end
29
29
  end
30
+
31
+ # Check if any filters are currently active
32
+ def any_filters_active?
33
+ %w[search type time deleted_by size date_from date_to].any? { |param| params[param].present? }
34
+ end
35
+
36
+ # Generate human-readable label for time filter
37
+ def time_filter_label(time_value)
38
+ case time_value
39
+ when 'today'
40
+ 'Today'
41
+ when 'week'
42
+ 'This Week'
43
+ when 'month'
44
+ 'This Month'
45
+ when 'year'
46
+ 'This Year'
47
+ else
48
+ time_value.to_s.humanize
49
+ end
50
+ end
51
+
52
+ # Generate human-readable label for size filter
53
+ def size_filter_label(size_value)
54
+ case size_value
55
+ when 'small'
56
+ 'Small (< 1KB)'
57
+ when 'medium'
58
+ 'Medium (1KB - 100KB)'
59
+ when 'large'
60
+ 'Large (> 100KB)'
61
+ else
62
+ size_value.to_s.humanize
63
+ end
64
+ end
65
+
66
+ # Classify item size for CSS styling
67
+ def size_class(item)
68
+ size_bytes = calculate_item_memory_size(item)
69
+
70
+ if size_bytes < 1.kilobyte
71
+ 'small'
72
+ elsif size_bytes < 100.kilobytes
73
+ 'medium'
74
+ else
75
+ 'large'
76
+ end
77
+ end
78
+
79
+ # Calculate item memory size (helper method for views)
80
+ def calculate_item_memory_size(item)
81
+ return 0 unless item.respond_to?(:attributes)
82
+
83
+ # Simple calculation of item memory footprint
84
+ item.attributes.to_s.bytesize
85
+ rescue StandardError => e
86
+ Rails.logger.debug("Error calculating memory size: #{e.message}")
87
+ 0
88
+ end
89
+
90
+ # Format file size in human-readable format
91
+ def human_file_size(bytes)
92
+ return '0 B' if bytes.nil? || bytes.zero?
93
+
94
+ units = %w[B KB MB GB TB]
95
+ size = bytes.to_f
96
+ unit_index = 0
97
+
98
+ while size >= 1024 && unit_index < units.length - 1
99
+ size /= 1024
100
+ unit_index += 1
101
+ end
102
+
103
+ "#{size.round(2)} #{units[unit_index]}"
104
+ end
105
+
106
+ # Highlight search terms in text
107
+ def highlight_search_terms(text, search_term)
108
+ return text if search_term.blank? || text.blank?
109
+
110
+ text.to_s.gsub(/(#{Regexp.escape(search_term)})/i, '<mark>\1</mark>').html_safe
111
+ end
112
+
113
+ # Generate search result summary
114
+ def search_result_summary(total_count, search_term)
115
+ if search_term.present?
116
+ "Found #{total_count} result#{total_count == 1 ? '' : 's'} for \"#{search_term}\""
117
+ else
118
+ "#{total_count} item#{total_count == 1 ? '' : 's'} in trash"
119
+ end
120
+ end
121
+
122
+ # Check if current page has search results
123
+ def search_results?
124
+ @deleted_items&.any?
125
+ end
126
+
127
+ # Generate export filename with current filters
128
+ def export_filename_with_filters(base_name, format)
129
+ filters = []
130
+ filters << "search_#{params[:search]}" if params[:search].present?
131
+ filters << "type_#{params[:type]}" if params[:type].present?
132
+ filters << "time_#{params[:time]}" if params[:time].present?
133
+ filters << "user_#{params[:deleted_by]}" if params[:deleted_by].present?
134
+ filters << "size_#{params[:size]}" if params[:size].present?
135
+
136
+ if params[:date_from].present? || params[:date_to].present?
137
+ date_range = [params[:date_from], params[:date_to]].compact.join('_to_')
138
+ filters << "date_#{date_range}" if date_range.present?
139
+ end
140
+
141
+ timestamp = Time.current.strftime('%Y%m%d_%H%M%S')
142
+ filter_suffix = filters.any? ? "_#{filters.join('_')}" : ''
143
+
144
+ "#{base_name}#{filter_suffix}_#{timestamp}.#{format}"
145
+ end
146
+
147
+ # Generate filter breadcrumb
148
+ def filter_breadcrumb
149
+ return unless any_filters_active?
150
+
151
+ breadcrumbs = []
152
+
153
+ breadcrumbs << "Search: \"#{params[:search]}\"" if params[:search].present?
154
+
155
+ breadcrumbs << "Type: #{params[:type]}" if params[:type].present?
156
+
157
+ breadcrumbs << "Time: #{time_filter_label(params[:time])}" if params[:time].present?
158
+
159
+ breadcrumbs << "User: #{params[:deleted_by]}" if params[:deleted_by].present?
160
+
161
+ breadcrumbs << "Size: #{size_filter_label(params[:size])}" if params[:size].present?
162
+
163
+ if params[:date_from].present? || params[:date_to].present?
164
+ date_range = [params[:date_from], params[:date_to]].compact.join(' to ')
165
+ breadcrumbs << "Date: #{date_range}"
166
+ end
167
+
168
+ breadcrumbs.join(' • ')
169
+ end
170
+
171
+ # Check if advanced filters are visible
172
+ def advanced_filters_visible?
173
+ %w[deleted_by size date_from date_to].any? { |param| params[param].present? }
174
+ end
175
+
176
+ # Generate filter count badge
177
+ def active_filter_count
178
+ count = 0
179
+ count += 1 if params[:search].present?
180
+ count += 1 if params[:type].present?
181
+ count += 1 if params[:time].present?
182
+ count += 1 if params[:deleted_by].present?
183
+ count += 1 if params[:size].present?
184
+ count += 1 if params[:date_from].present? || params[:date_to].present?
185
+ count
186
+ end
187
+
188
+ # Check if item matches current search
189
+ def item_matches_search?(item, search_term)
190
+ return true if search_term.blank?
191
+
192
+ search_term = search_term.downcase
193
+
194
+ # Check common fields
195
+ %w[title name email content body description].any? do |field|
196
+ item.send(field).to_s.downcase.include?(search_term) if item.respond_to?(field) && item.send(field).present?
197
+ end ||
198
+ # Check ID
199
+ item.id.to_s.include?(search_term) ||
200
+ # Check model class name
201
+ item.class.name.downcase.include?(search_term) ||
202
+ # Check all attributes
203
+ item.attributes.values.any? { |val| val.to_s.downcase.include?(search_term) }
204
+ end
30
205
  end
31
206
  end
@@ -461,37 +461,37 @@
461
461
  flex-direction: column;
462
462
  gap: 1rem;
463
463
  }
464
-
464
+
465
465
  .nav {
466
466
  width: 100%;
467
467
  justify-content: center;
468
468
  }
469
-
469
+
470
470
  .container {
471
471
  padding: 1rem;
472
472
  }
473
-
473
+
474
474
  .filters {
475
475
  flex-direction: column;
476
476
  align-items: flex-start;
477
477
  gap: 1rem;
478
478
  }
479
-
479
+
480
480
  .table th,
481
481
  .table td {
482
482
  padding: 0.5rem;
483
483
  }
484
-
484
+
485
485
  .stats-grid {
486
486
  grid-template-columns: repeat(2, 1fr);
487
487
  }
488
-
488
+
489
489
  .card-header {
490
490
  flex-direction: column;
491
491
  gap: 1rem;
492
492
  align-items: flex-start;
493
493
  }
494
-
494
+
495
495
  .card-actions {
496
496
  width: 100%;
497
497
  justify-content: flex-start;
@@ -502,7 +502,7 @@
502
502
  .table thead {
503
503
  display: none;
504
504
  }
505
-
505
+
506
506
  .table tr {
507
507
  display: block;
508
508
  margin-bottom: 1rem;
@@ -510,14 +510,14 @@
510
510
  border-radius: 8px;
511
511
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
512
512
  }
513
-
513
+
514
514
  .table td {
515
515
  display: block;
516
516
  padding: 0.5rem 1rem;
517
517
  border: none;
518
518
  border-bottom: 1px solid #f0f0f0;
519
519
  }
520
-
520
+
521
521
  .table td:before {
522
522
  content: attr(data-label) ": ";
523
523
  font-weight: bold;
@@ -681,18 +681,18 @@
681
681
  <div class="header">
682
682
  <div class="header-content">
683
683
  <div class="logo">
684
- <img src="https://raw.githubusercontent.com/R95-del/recycle_bin/main/docs/logo.svg"
685
- alt="RecycleBin Logo"
684
+ <img src="https://raw.githubusercontent.com/R95-del/recycle_bin/main/docs/logo.svg"
685
+ alt="RecycleBin Logo"
686
686
  class="logo-icon"
687
687
  onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';">
688
688
  <span class="icon-fallback" style="display:none;">🗑️</span>
689
689
  <span>RecycleBin</span>
690
690
  </div>
691
-
691
+
692
692
  <nav class="nav">
693
- <%= link_to "Dashboard", recycle_bin.root_path,
694
- class: ("active" if current_page?(recycle_bin.root_path)) %>
695
- <%= link_to "All Items", recycle_bin.trash_index_path,
693
+ <%= link_to "Dashboard", recycle_bin.root_path,
694
+ class: ("active" if current_page?(recycle_bin.root_path) || current_page?(recycle_bin.dashboard_path)) %>
695
+ <%= link_to "All Items", recycle_bin.trash_index_path,
696
696
  class: ("active" if current_page?(recycle_bin.trash_index_path)) %>
697
697
  </nav>
698
698
  </div>
@@ -717,26 +717,26 @@
717
717
  // Handle custom method (e.g., PATCH, DELETE)
718
718
  if (this.dataset.method) {
719
719
  e.preventDefault();
720
-
720
+
721
721
  const method = this.dataset.method.toUpperCase();
722
722
  const url = this.href;
723
723
  const form = document.createElement('form');
724
724
  form.method = 'POST';
725
725
  form.action = url;
726
-
726
+
727
727
  const methodInput = document.createElement('input');
728
728
  methodInput.type = 'hidden';
729
729
  methodInput.name = '_method';
730
730
  methodInput.value = method;
731
731
  form.appendChild(methodInput);
732
-
732
+
733
733
  const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
734
734
  const csrfInput = document.createElement('input');
735
735
  csrfInput.type = 'hidden';
736
736
  csrfInput.name = 'authenticity_token';
737
737
  csrfInput.value = csrfToken;
738
738
  form.appendChild(csrfInput);
739
-
739
+
740
740
  document.body.appendChild(form);
741
741
  form.submit();
742
742
  }
@@ -745,7 +745,7 @@
745
745
 
746
746
  // Bulk actions and other interactivity
747
747
  updateBulkActions();
748
-
748
+
749
749
  document.addEventListener('click', function(e) {
750
750
  if (e.target.matches('[data-copy]')) {
751
751
  const text = e.target.dataset.copy;
@@ -760,7 +760,7 @@
760
760
 
761
761
  const autoRefreshCheckbox = document.getElementById('auto-refresh');
762
762
  let autoRefreshInterval;
763
-
763
+
764
764
  if (autoRefreshCheckbox) {
765
765
  autoRefreshCheckbox.addEventListener('change', function() {
766
766
  if (this.checked) {
@@ -777,11 +777,11 @@
777
777
  function toggleSelectAll() {
778
778
  const selectAllCheckbox = document.getElementById('select-all');
779
779
  const itemCheckboxes = document.querySelectorAll('.item-checkbox');
780
-
780
+
781
781
  itemCheckboxes.forEach(checkbox => {
782
782
  checkbox.checked = selectAllCheckbox.checked;
783
783
  });
784
-
784
+
785
785
  updateBulkActions();
786
786
  }
787
787
 
@@ -790,14 +790,15 @@
790
790
  const bulkActions = document.getElementById('bulk-actions');
791
791
  const bulkCount = document.getElementById('bulk-count');
792
792
  const selectAllCheckbox = document.getElementById('select-all');
793
-
793
+ const itemCheckboxes = document.querySelectorAll('.item-checkbox');
794
+
794
795
  if (checkedBoxes.length > 0) {
795
796
  bulkActions.classList.add('show');
796
797
  bulkCount.textContent = checkedBoxes.length + ' item' + (checkedBoxes.length > 1 ? 's' : '') + ' selected';
797
798
  } else {
798
799
  bulkActions.classList.remove('show');
799
800
  }
800
-
801
+
801
802
  if (checkedBoxes.length === itemCheckboxes.length && itemCheckboxes.length > 0) {
802
803
  selectAllCheckbox.checked = true;
803
804
  selectAllCheckbox.indeterminate = false;
@@ -812,31 +813,31 @@
812
813
 
813
814
  function handleBulkAction(action) {
814
815
  const checkedBoxes = document.querySelectorAll('.item-checkbox:checked');
815
-
816
+
816
817
  if (checkedBoxes.length === 0) {
817
818
  alert('Please select at least one item.');
818
819
  return false;
819
820
  }
820
-
821
+
821
822
  const selectedItems = Array.from(checkedBoxes).map(cb => cb.value);
822
-
823
+
823
824
  let message;
824
825
  if (action === 'restore') {
825
826
  message = 'Restore ' + checkedBoxes.length + ' selected item' + (checkedBoxes.length > 1 ? 's' : '') + '?';
826
827
  } else {
827
828
  message = 'Permanently delete ' + checkedBoxes.length + ' selected item' + (checkedBoxes.length > 1 ? 's' : '') + '? This cannot be undone!';
828
829
  }
829
-
830
+
830
831
  if (!confirm(message)) {
831
832
  return false;
832
833
  }
833
-
834
+
834
835
  if (action === 'restore') {
835
836
  document.getElementById('bulk-restore-items').value = JSON.stringify(selectedItems);
836
837
  } else {
837
838
  document.getElementById('bulk-destroy-items').value = JSON.stringify(selectedItems);
838
839
  }
839
-
840
+
840
841
  return true;
841
842
  }
842
843
 
@@ -17,9 +17,9 @@
17
17
  <tbody>
18
18
  <% item.versions.each do |version| %>
19
19
  <tr>
20
- <td><span class="event-type"><%= version.event.humanize %></span></td>
20
+ <td><span class="event-type"><%= version.respond_to?(:event) ? version.event.humanize : 'Unknown' %></span></td>
21
21
  <td>
22
- <% if version.whodunnit %>
22
+ <% if version.respond_to?(:whodunnit) && version.whodunnit %>
23
23
  <%= version.whodunnit %>
24
24
  <% else %>
25
25
  <span class="timestamp">System</span>
@@ -27,20 +27,24 @@
27
27
  </td>
28
28
  <td>
29
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') %>
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 %>
32
36
  </span>
33
37
  </td>
34
38
  <td>
35
- <% if version.changeset.any? %>
39
+ <% if version.respond_to?(:changeset) && version.changeset.any? %>
36
40
  <details>
37
41
  <summary class="btn btn-outline btn-sm">View Changes</summary>
38
42
  <div class="changeset-details">
39
43
  <% version.changeset.each do |key, changes| %>
40
44
  <div class="change-item">
41
- <strong><%= key.humanize %>:</strong>
42
- <span class="change-from"><%= changes[0].nil? ? 'nil' : changes[0] %></span>
43
-
45
+ <strong><%= key.humanize %>:</strong>
46
+ <span class="change-from"><%= changes[0].nil? ? 'nil' : changes[0] %></span>
47
+
44
48
  <span class="change-to"><%= changes[1].nil? ? 'nil' : changes[1] %></span>
45
49
  </div>
46
50
  <% end %>
@@ -68,4 +72,4 @@
68
72
  </div>
69
73
  <% end %>
70
74
  </div>
71
- </div>
75
+ </div>
@@ -15,13 +15,17 @@
15
15
  <div class="association-item">
16
16
  <div class="association-content">
17
17
  <div class="card-title">
18
- <%= item.class.name %> -
19
- <%= item.respond_to?(:title) ? item.title :
20
- item.respond_to?(:name) ? item.name :
18
+ <%= item.class.name %> -
19
+ <%= item.respond_to?(:title) ? item.title :
20
+ item.respond_to?(:name) ? item.name :
21
21
  "ID: #{item.id}" %>
22
22
  </div>
23
23
  <div class="timestamp">
24
- Created <%= time_ago_in_words(item.created_at) %> ago
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 %>
25
29
  <% if item.respond_to?(:deleted_at) && item.deleted_at %>
26
30
  • <span class="deleted-status">Also deleted</span>
27
31
  <% end %>
@@ -43,4 +47,4 @@
43
47
  </div>
44
48
  <% end %>
45
49
  </div>
46
- </div>
50
+ </div>