Add separate group reset

This commit is contained in:
DariusIII
2026-01-22 15:51:37 +01:00
parent 15f8b2ef54
commit 222076d95c
5 changed files with 451 additions and 109 deletions
+82
View File
@@ -732,3 +732,85 @@ code.api-token::-webkit-scrollbar-thumb:hover {
background-color: rgba(30, 64, 175, 0.2); /* blue-900/20 */
}
/* Group Selection Styles */
.group-checkbox {
cursor: pointer;
transition: all 0.15s ease-in-out;
}
.group-row.selected {
background-color: rgba(59, 130, 246, 0.1); /* blue-500/10 */
}
.dark .group-row.selected {
background-color: rgba(59, 130, 246, 0.15); /* blue-500/15 */
}
/* Selection counter animation */
#selection-counter {
transition: opacity 0.2s ease-in-out;
}
#selection-counter.hidden {
opacity: 0;
}
/* Reset selected button animation */
#reset-selected-btn {
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
}
#reset-selected-btn:not(.hidden) {
animation: slideIn 0.2s ease-in-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Indeterminate checkbox styling */
#select-all-groups:indeterminate {
background-color: rgb(156, 163, 175); /* gray-400 */
border-color: rgb(156, 163, 175);
}
.dark #select-all-groups:indeterminate {
background-color: rgb(107, 114, 128); /* gray-500 */
border-color: rgb(107, 114, 128);
}
/* Reset selected modal list styling */
#reset-selected-list {
max-height: 150px;
overflow-y: auto;
}
#reset-selected-list::-webkit-scrollbar {
width: 6px;
}
#reset-selected-list::-webkit-scrollbar-track {
background: rgb(243, 244, 246); /* gray-100 */
border-radius: 3px;
}
.dark #reset-selected-list::-webkit-scrollbar-track {
background: rgb(55, 65, 81); /* gray-700 */
}
#reset-selected-list::-webkit-scrollbar-thumb {
background: rgb(156, 163, 175); /* gray-400 */
border-radius: 3px;
}
.dark #reset-selected-list::-webkit-scrollbar-thumb {
background: rgb(107, 114, 128); /* gray-500 */
}
+286 -102
View File
@@ -450,6 +450,15 @@ function initEventDelegation() {
case 'hide-purge-modal':
hidePurgeAllModal();
break;
case 'show-reset-selected-modal':
showResetSelectedModal();
break;
case 'hide-reset-selected-modal':
hideResetSelectedModal();
break;
case 'select-all-groups':
toggleSelectAllGroups(actionTarget);
break;
case 'toggle-group-status':
ajax_group_status(groupId, status);
break;
@@ -471,6 +480,9 @@ function initEventDelegation() {
case 'purge-all':
ajax_group_purge_all();
break;
case 'reset-selected':
ajax_group_reset_selected();
break;
}
}
@@ -3600,120 +3612,138 @@ function initAdminGroups() {
// Reset group
window.ajax_group_reset = function(id) {
if (!confirm('Are you sure you want to reset this group? This will reset the article pointers back to the current state.')) {
return;
}
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({
action: 'reset_group',
group_id: id
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (typeof showToast === 'function') {
showToast(data.message || 'Group reset successfully', 'success');
}
} else {
if (typeof showToast === 'function') {
showToast(data.message || 'Error resetting group', 'error');
}
}
})
.catch(error => {
console.error('Error:', error);
if (typeof showToast === 'function') {
showToast('Error resetting group', 'error');
showConfirm({
title: 'Reset Group',
message: 'Are you sure you want to reset this group?',
details: 'This will reset the article pointers back to the current state.',
type: 'warning',
confirmText: 'Reset',
cancelText: 'Cancel',
onConfirm: function() {
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({
action: 'reset_group',
group_id: id
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (typeof showToast === 'function') {
showToast(data.message || 'Group reset successfully', 'success');
}
} else {
if (typeof showToast === 'function') {
showToast(data.message || 'Error resetting group', 'error');
}
}
})
.catch(error => {
console.error('Error:', error);
if (typeof showToast === 'function') {
showToast('Error resetting group', 'error');
}
});
}
});
};
// Delete group
window.confirmGroupDelete = function(id) {
if (!confirm('Are you sure you want to delete this group? This action cannot be undone.')) {
return;
}
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({
action: 'delete_group',
group_id: id
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
const row = document.getElementById('grouprow-' + id);
if (row) {
row.style.transition = 'opacity 0.3s';
row.style.opacity = '0';
setTimeout(() => row.remove(), 300);
}
if (typeof showToast === 'function') {
showToast(data.message || 'Group deleted successfully', 'success');
}
} else {
if (typeof showToast === 'function') {
showToast(data.message || 'Error deleting group', 'error');
}
}
})
.catch(error => {
console.error('Error:', error);
if (typeof showToast === 'function') {
showToast('Error deleting group', 'error');
showConfirm({
title: 'Delete Group',
message: 'Are you sure you want to delete this group?',
details: 'This action cannot be undone.',
type: 'danger',
confirmText: 'Delete',
cancelText: 'Cancel',
onConfirm: function() {
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({
action: 'delete_group',
group_id: id
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
const row = document.getElementById('grouprow-' + id);
if (row) {
row.style.transition = 'opacity 0.3s';
row.style.opacity = '0';
setTimeout(() => row.remove(), 300);
}
if (typeof showToast === 'function') {
showToast(data.message || 'Group deleted successfully', 'success');
}
} else {
if (typeof showToast === 'function') {
showToast(data.message || 'Error deleting group', 'error');
}
}
})
.catch(error => {
console.error('Error:', error);
if (typeof showToast === 'function') {
showToast('Error deleting group', 'error');
}
});
}
});
};
// Purge group
window.confirmGroupPurge = function(id) {
if (!confirm('Are you sure you want to purge this group? This will delete all releases and binaries for this group. This action cannot be undone!')) {
return;
}
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({
action: 'purge_group',
group_id: id
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (typeof showToast === 'function') {
showToast(data.message || 'Group purged successfully', 'success');
}
} else {
if (typeof showToast === 'function') {
showToast(data.message || 'Error purging group', 'error');
}
}
})
.catch(error => {
console.error('Error:', error);
if (typeof showToast === 'function') {
showToast('Error purging group', 'error');
showConfirm({
title: 'Purge Group',
message: 'Are you sure you want to purge this group?',
details: 'This will delete all releases and binaries for this group. This action cannot be undone!',
type: 'danger',
confirmText: 'Purge',
cancelText: 'Cancel',
onConfirm: function() {
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({
action: 'purge_group',
group_id: id
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (typeof showToast === 'function') {
showToast(data.message || 'Group purged successfully', 'success');
}
} else {
if (typeof showToast === 'function') {
showToast(data.message || 'Error purging group', 'error');
}
}
})
.catch(error => {
console.error('Error:', error);
if (typeof showToast === 'function') {
showToast('Error purging group', 'error');
}
});
}
});
};
@@ -3816,6 +3846,160 @@ function initAdminGroups() {
modal.classList.add('hidden');
}
};
// Reset Selected Modal helpers
window.showResetSelectedModal = function() {
const modal = document.getElementById('resetSelectedModal');
const countSpan = document.getElementById('reset-selected-count');
const listDiv = document.getElementById('reset-selected-list');
if (modal) {
const selectedGroups = getSelectedGroups();
if (selectedGroups.length === 0) {
if (typeof showToast === 'function') {
showToast('No groups selected', 'warning');
}
return;
}
if (countSpan) {
countSpan.textContent = selectedGroups.length;
}
if (listDiv) {
listDiv.innerHTML = selectedGroups.map(g =>
`<div class="py-1 border-b border-gray-200 dark:border-gray-700 last:border-0">${escapeHtml(g.name)}</div>`
).join('');
}
modal.classList.remove('hidden');
}
};
window.hideResetSelectedModal = function() {
const modal = document.getElementById('resetSelectedModal');
if (modal) {
modal.classList.add('hidden');
}
};
// Get selected groups
window.getSelectedGroups = function() {
const checkboxes = document.querySelectorAll('.group-checkbox:checked');
const groups = [];
checkboxes.forEach(cb => {
groups.push({
id: cb.dataset.groupId,
name: cb.dataset.groupName
});
});
return groups;
};
// Toggle select all groups
window.toggleSelectAllGroups = function(checkbox) {
const isChecked = checkbox.checked;
const groupCheckboxes = document.querySelectorAll('.group-checkbox');
groupCheckboxes.forEach(cb => {
cb.checked = isChecked;
});
updateSelectionUI();
};
// Update selection UI (counter, button visibility)
window.updateSelectionUI = function() {
const selectedGroups = getSelectedGroups();
const count = selectedGroups.length;
const counter = document.getElementById('selection-counter');
const countSpan = document.getElementById('selected-count');
const resetSelectedBtn = document.getElementById('reset-selected-btn');
const selectAllCheckbox = document.getElementById('select-all-groups');
const allCheckboxes = document.querySelectorAll('.group-checkbox');
if (counter && countSpan) {
if (count > 0) {
counter.classList.remove('hidden');
countSpan.textContent = count;
} else {
counter.classList.add('hidden');
}
}
if (resetSelectedBtn) {
if (count > 0) {
resetSelectedBtn.classList.remove('hidden');
} else {
resetSelectedBtn.classList.add('hidden');
}
}
// Update select-all checkbox state
if (selectAllCheckbox && allCheckboxes.length > 0) {
const allChecked = Array.from(allCheckboxes).every(cb => cb.checked);
const someChecked = Array.from(allCheckboxes).some(cb => cb.checked);
selectAllCheckbox.checked = allChecked;
selectAllCheckbox.indeterminate = someChecked && !allChecked;
}
};
// Reset selected groups
window.ajax_group_reset_selected = function() {
hideResetSelectedModal();
const selectedGroups = getSelectedGroups();
if (selectedGroups.length === 0) {
if (typeof showToast === 'function') {
showToast('No groups selected', 'warning');
}
return;
}
const groupIds = selectedGroups.map(g => g.id);
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({
action: 'reset_selected_groups',
group_ids: JSON.stringify(groupIds)
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (typeof showToast === 'function') {
showToast(data.message || `${selectedGroups.length} group(s) reset successfully`, 'success');
}
// Clear selection
document.querySelectorAll('.group-checkbox:checked').forEach(cb => {
cb.checked = false;
});
document.getElementById('select-all-groups').checked = false;
updateSelectionUI();
} else {
if (typeof showToast === 'function') {
showToast(data.message || 'Error resetting selected groups', 'error');
}
}
})
.catch(error => {
console.error('Error:', error);
if (typeof showToast === 'function') {
showToast('Error resetting selected groups', 'error');
}
});
};
// Initialize checkbox change listeners
document.addEventListener('change', function(e) {
if (e.target.classList.contains('group-checkbox')) {
updateSelectionUI();
}
});
}
// TinyMCE Initialization for Admin Content Pages
+13 -5
View File
@@ -227,12 +227,20 @@
</form>
<!-- Footer -->
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900" data-ajax-url="{{ url('/admin/ajax') }}" data-csrf-token="{{ csrf_token() }}">
<div class="flex justify-between">
<a href="{{ url('/admin/group-list') }}"
class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600">
<i class="fa fa-times mr-2"></i>Cancel
</a>
<div class="flex gap-2">
<a href="{{ url('/admin/group-list') }}"
class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600">
<i class="fa fa-times mr-2"></i>Cancel
</a>
<button type="button"
data-action="reset-group"
data-group-id="{{ $group['id'] ?? '' }}"
class="px-4 py-2 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700">
<i class="fa fa-refresh mr-2"></i>Reset Group
</button>
</div>
<button type="submit"
form="groupForm"
class="px-4 py-2 bg-green-600 dark:bg-green-700 text-white rounded-lg hover:bg-green-700 dark:hover:bg-green-600">
+53 -2
View File
@@ -76,7 +76,17 @@
<!-- Bulk Actions -->
<div class="flex justify-end items-center">
<div class="flex gap-2">
<div class="flex gap-2 items-center">
<!-- Selection Counter -->
<span id="selection-counter" class="hidden text-sm text-gray-600 dark:text-gray-400 mr-2">
<span id="selected-count">0</span> selected
</span>
<button type="button"
id="reset-selected-btn"
data-action="show-reset-selected-modal"
class="hidden px-3 py-2 bg-orange-600 dark:bg-orange-700 text-white text-sm rounded-lg hover:bg-orange-700 dark:hover:bg-orange-600">
<i class="fa fa-refresh mr-1"></i> Reset Selected
</button>
<button type="button"
data-action="show-reset-modal"
class="px-3 py-2 bg-yellow-600 text-white text-sm rounded-lg hover:bg-yellow-700">
@@ -97,6 +107,13 @@
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-900">
<tr>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-12">
<input type="checkbox"
id="select-all-groups"
data-action="select-all-groups"
class="form-checkbox h-4 w-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 dark:bg-gray-700"
title="Select all groups on this page">
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Group</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">First Post</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Last Post</th>
@@ -112,7 +129,13 @@
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
@foreach($grouplist as $group)
<tr id="grouprow-{{ $group->id }}" class="hover:bg-gray-50 dark:hover:bg-gray-700">
<tr id="grouprow-{{ $group->id }}" class="hover:bg-gray-50 dark:hover:bg-gray-700 group-row">
<td class="px-4 py-4 text-center">
<input type="checkbox"
class="group-checkbox form-checkbox h-4 w-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 dark:bg-gray-700"
data-group-id="{{ $group->id }}"
data-group-name="{{ $group->name }}">
</td>
<td class="px-6 py-4">
<a href="{{ url('/admin/group-edit?id=' . $group->id) }}" class="font-semibold text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300">
{{ str_replace('alt.binaries', 'a.b', $group->name) }}
@@ -311,5 +334,33 @@
</div>
</div>
<!-- Reset Selected Modal -->
<div id="resetSelectedModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white dark:bg-gray-800">
<div class="mt-3">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">Confirm Reset Selected Groups</h3>
<p class="text-sm text-orange-600 dark:text-orange-400 mb-2">
<i class="fa fa-exclamation-triangle mr-2"></i>Are you sure you want to reset <span id="reset-selected-count">0</span> selected group(s)?
</p>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
This will reset the article pointers for the selected groups back to their current state.
</p>
<div id="reset-selected-list" class="max-h-32 overflow-y-auto mb-4 text-xs text-gray-500 dark:text-gray-400"></div>
<div class="flex justify-end gap-3">
<button type="button"
data-action="hide-reset-selected-modal"
class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300">
Cancel
</button>
<button type="button"
data-action="reset-selected"
class="px-4 py-2 bg-orange-600 dark:bg-orange-700 text-white rounded-lg hover:bg-orange-700">
Reset Selected
</button>
</div>
</div>
</div>
</div>
@endsection