mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-29 01:08:56 +00:00
Update admin area views and controllers
This commit is contained in:
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\BasePageController;
|
||||
use App\Http\Requests\Admin\AdminGroupListRequest;
|
||||
use App\Models\UsenetGroup;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -15,9 +16,9 @@ class AdminGroupController extends BasePageController
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function index(Request $request): mixed
|
||||
public function index(AdminGroupListRequest $request): mixed
|
||||
{
|
||||
$groupname = $request->input('groupname') ?? '';
|
||||
$groupname = $request->groupName();
|
||||
$grouplist = UsenetGroup::getGroupsRange($groupname);
|
||||
$title = 'Group List';
|
||||
|
||||
@@ -106,15 +107,10 @@ class AdminGroupController extends BasePageController
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function active(Request $request): mixed
|
||||
public function active(AdminGroupListRequest $request): mixed
|
||||
{
|
||||
$gname = '';
|
||||
if (! empty($request->input('groupname'))) {
|
||||
$gname = $request->input('groupname');
|
||||
}
|
||||
|
||||
$groupname = ! empty($request->input('groupname')) ? $request->input('groupname') : '';
|
||||
$grouplist = UsenetGroup::getGroupsRange($gname, true);
|
||||
$groupname = $request->groupName();
|
||||
$grouplist = UsenetGroup::getGroupsRange($groupname, true);
|
||||
$title = 'Active Groups';
|
||||
|
||||
return view('admin.groups.index', compact('title', 'groupname', 'grouplist'));
|
||||
@@ -123,15 +119,10 @@ class AdminGroupController extends BasePageController
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function inactive(Request $request): mixed
|
||||
public function inactive(AdminGroupListRequest $request): mixed
|
||||
{
|
||||
$gname = '';
|
||||
if (! empty($request->input('groupname'))) {
|
||||
$gname = $request->input('groupname');
|
||||
}
|
||||
|
||||
$groupname = ! empty($request->input('groupname')) ? $request->input('groupname') : '';
|
||||
$grouplist = UsenetGroup::getGroupsRange($gname, false);
|
||||
$groupname = $request->groupName();
|
||||
$grouplist = UsenetGroup::getGroupsRange($groupname, false);
|
||||
$title = 'Inactive Groups';
|
||||
|
||||
return view('admin.groups.index', compact('title', 'groupname', 'grouplist'));
|
||||
|
||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\BasePageController;
|
||||
use App\Http\Requests\Admin\AdminReleaseReportBulkActionRequest;
|
||||
use App\Http\Requests\Admin\AdminReleaseReportListRequest;
|
||||
use App\Models\Release;
|
||||
use App\Models\ReleaseReport;
|
||||
use App\Services\Releases\ReleaseBrowseService;
|
||||
@@ -18,11 +20,11 @@ class AdminReleaseReportController extends BasePageController
|
||||
/**
|
||||
* Display a listing of release reports.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
public function index(AdminReleaseReportListRequest $request): View
|
||||
{
|
||||
$this->setAdminPrefs();
|
||||
|
||||
$status = $request->input('status', 'pending');
|
||||
$status = $request->status();
|
||||
$reportsList = ReleaseReport::getReportsRange($status, 50);
|
||||
$statusCounts = ReleaseReport::getCountByStatus();
|
||||
|
||||
@@ -165,62 +167,13 @@ class AdminReleaseReportController extends BasePageController
|
||||
/**
|
||||
* Bulk update report statuses.
|
||||
*/
|
||||
public function bulkAction(Request $request): RedirectResponse
|
||||
public function bulkAction(AdminReleaseReportBulkActionRequest $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'action' => 'required|in:dismiss,resolve,reviewed,delete,revert',
|
||||
'report_ids' => 'required|array',
|
||||
'report_ids.*' => 'integer',
|
||||
]);
|
||||
|
||||
$action = $request->input('action');
|
||||
$reportIds = $request->input('report_ids');
|
||||
|
||||
$count = 0;
|
||||
|
||||
foreach ($reportIds as $reportId) {
|
||||
$report = ReleaseReport::with('release')->find($reportId);
|
||||
if (! $report) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($action === 'delete' && $report->release) {
|
||||
$releaseId = $report->releases_id;
|
||||
Release::where('id', $releaseId)->delete();
|
||||
ReleaseReport::where('releases_id', $releaseId)
|
||||
->update([
|
||||
'status' => 'resolved',
|
||||
'reviewed_by' => Auth::id(),
|
||||
'reviewed_at' => now(),
|
||||
]);
|
||||
} elseif ($action === 'dismiss') {
|
||||
$report->update([
|
||||
'status' => 'dismissed',
|
||||
'reviewed_by' => Auth::id(),
|
||||
'reviewed_at' => now(),
|
||||
]);
|
||||
} elseif ($action === 'resolve') {
|
||||
$report->update([
|
||||
'status' => 'resolved',
|
||||
'reviewed_by' => Auth::id(),
|
||||
'reviewed_at' => now(),
|
||||
]);
|
||||
} elseif ($action === 'reviewed') {
|
||||
$report->update([
|
||||
'status' => 'reviewed',
|
||||
'reviewed_by' => Auth::id(),
|
||||
'reviewed_at' => now(),
|
||||
]);
|
||||
} elseif ($action === 'revert' && in_array($report->status, ['resolved', 'dismissed'])) {
|
||||
$report->update([
|
||||
'status' => 'reviewed',
|
||||
'reviewed_by' => Auth::id(),
|
||||
'reviewed_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
$count++;
|
||||
}
|
||||
$action = $request->actionName();
|
||||
$reportIds = $request->reportIds();
|
||||
$count = $action === 'delete'
|
||||
? $this->bulkDeleteReportedReleases($reportIds)
|
||||
: $this->bulkUpdateReportStatuses($reportIds, $action);
|
||||
|
||||
if ($count > 0) {
|
||||
ReleaseBrowseService::bumpCacheVersion();
|
||||
@@ -237,4 +190,66 @@ class AdminReleaseReportController extends BasePageController
|
||||
|
||||
return redirect()->back()->with('success', "{$count} report(s) {$actionLabel} successfully.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $reportIds
|
||||
*/
|
||||
private function bulkUpdateReportStatuses(array $reportIds, string $action): int
|
||||
{
|
||||
$status = match ($action) {
|
||||
'dismiss' => 'dismissed',
|
||||
'resolve' => 'resolved',
|
||||
'reviewed', 'revert' => 'reviewed',
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($status === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$query = ReleaseReport::query()
|
||||
->whereIn('id', $reportIds)
|
||||
->when($action === 'revert', fn ($query) => $query->whereIn('status', ['resolved', 'dismissed']));
|
||||
|
||||
$count = (clone $query)->count();
|
||||
|
||||
$query->update([
|
||||
'status' => $status,
|
||||
'reviewed_by' => Auth::id(),
|
||||
'reviewed_at' => now(),
|
||||
]);
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $reportIds
|
||||
*/
|
||||
private function bulkDeleteReportedReleases(array $reportIds): int
|
||||
{
|
||||
$reports = ReleaseReport::query()
|
||||
->with('release:id')
|
||||
->whereIn('id', $reportIds)
|
||||
->get(['id', 'releases_id']);
|
||||
|
||||
$count = 0;
|
||||
|
||||
foreach ($reports as $report) {
|
||||
if (! $report->release) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$releaseId = $report->releases_id;
|
||||
Release::where('id', $releaseId)->delete();
|
||||
ReleaseReport::where('releases_id', $releaseId)
|
||||
->update([
|
||||
'status' => 'resolved',
|
||||
'reviewed_by' => Auth::id(),
|
||||
'reviewed_at' => now(),
|
||||
]);
|
||||
$count++;
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\BasePageController;
|
||||
use App\Http\Requests\Admin\AdminReleaseListRequest;
|
||||
use App\Models\Category;
|
||||
use App\Models\Release;
|
||||
use App\Services\Releases\ReleaseManagementService;
|
||||
@@ -27,20 +28,17 @@ class AdminReleasesController extends BasePageController
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function index(Request $request): mixed
|
||||
public function index(AdminReleaseListRequest $request): mixed
|
||||
{
|
||||
$this->setAdminPrefs();
|
||||
|
||||
$meta_title = $title = 'Release List';
|
||||
|
||||
$page = (int) $request->input('page', 1);
|
||||
$search = trim((string) $request->input('search', ''));
|
||||
$categoryId = $request->filled('category_id') ? (int) $request->input('category_id') : null;
|
||||
if ($categoryId === -1) {
|
||||
$categoryId = null;
|
||||
}
|
||||
$search = $request->searchTerm();
|
||||
$categoryId = $request->categoryId();
|
||||
|
||||
$releaseList = Release::getReleasesRange($page, $search !== '' ? $search : null, $categoryId);
|
||||
$releaseList = Release::getReleasesRange($page, $search, $categoryId);
|
||||
$releaseList->appends($request->only(['search', 'category_id']));
|
||||
|
||||
return view('admin.releases.index', [
|
||||
@@ -126,6 +124,7 @@ class AdminReleasesController extends BasePageController
|
||||
try {
|
||||
if ($id) {
|
||||
$this->releaseManagement->deleteMultiple($id);
|
||||
Release::clearAdminReleasesRangeCache();
|
||||
|
||||
// Handle AJAX requests
|
||||
if (request()->wantsJson() || request()->ajax()) {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AdminGroupListRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$groupname = $this->input('groupname');
|
||||
|
||||
$this->merge([
|
||||
'groupname' => is_string($groupname) ? trim($groupname) : $groupname,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'groupname' => ['nullable', 'string', 'max:255'],
|
||||
'page' => ['nullable', 'integer', 'min:1'],
|
||||
];
|
||||
}
|
||||
|
||||
public function groupName(): string
|
||||
{
|
||||
$value = $this->validated('groupname', '');
|
||||
|
||||
return is_string($value) ? $value : '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AdminReleaseListRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$search = $this->input('search');
|
||||
|
||||
$this->merge([
|
||||
'search' => is_string($search) ? trim($search) : $search,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'search' => ['nullable', 'string', 'max:255'],
|
||||
'category_id' => ['nullable', 'integer'],
|
||||
'page' => ['nullable', 'integer', 'min:1'],
|
||||
];
|
||||
}
|
||||
|
||||
public function searchTerm(): ?string
|
||||
{
|
||||
$search = $this->validated('search');
|
||||
|
||||
return is_string($search) && $search !== '' ? $search : null;
|
||||
}
|
||||
|
||||
public function categoryId(): ?int
|
||||
{
|
||||
$categoryId = $this->validated('category_id');
|
||||
|
||||
if ($categoryId === null || (int) $categoryId === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $categoryId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AdminReleaseReportBulkActionRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'action' => ['required', 'string', 'in:dismiss,resolve,reviewed,delete,revert'],
|
||||
'report_ids' => ['required', 'array', 'min:1'],
|
||||
'report_ids.*' => ['integer', 'exists:release_reports,id'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<int>
|
||||
*/
|
||||
public function reportIds(): array
|
||||
{
|
||||
$ids = $this->validated('report_ids', []);
|
||||
|
||||
return array_values(array_unique(array_map('intval', is_array($ids) ? $ids : [])));
|
||||
}
|
||||
|
||||
public function actionName(): string
|
||||
{
|
||||
return (string) $this->validated('action');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AdminReleaseReportListRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'status' => ['nullable', 'string', 'in:pending,reviewed,resolved,dismissed,all'],
|
||||
'page' => ['nullable', 'integer', 'min:1'],
|
||||
];
|
||||
}
|
||||
|
||||
public function status(): string
|
||||
{
|
||||
$status = $this->validated('status', 'pending');
|
||||
|
||||
return is_string($status) && $status !== '' ? $status : 'pending';
|
||||
}
|
||||
}
|
||||
+10
-6
@@ -386,15 +386,18 @@ class Release extends Model
|
||||
public static function getReleasesRange(int $page = 1, ?string $search = null, ?int $categoryId = null): mixed
|
||||
{
|
||||
$search = $search !== null && $search !== '' ? $search : null;
|
||||
$hasFilters = $search !== null || $categoryId !== null;
|
||||
|
||||
if ($search !== null && Search::isAvailable()) {
|
||||
return self::getReleasesRangeFromSearchIndex($page, $search, $categoryId);
|
||||
}
|
||||
|
||||
if (! $hasFilters) {
|
||||
$cacheable = $search === null;
|
||||
$cacheKey = null;
|
||||
|
||||
if ($cacheable) {
|
||||
$expiresAt = now()->addMinutes(config('nntmux.cache_expiry_long'));
|
||||
$cacheKey = md5('releasesRange_'.$page);
|
||||
$version = Cache::get('adminReleasesRangeVersion', 1);
|
||||
$cacheKey = md5('releasesRange_'.$version.'_'.$page.'_'.($categoryId ?? 'all'));
|
||||
$releases = Cache::get($cacheKey);
|
||||
if ($releases !== null) {
|
||||
return $releases;
|
||||
@@ -413,9 +416,8 @@ class Release extends Model
|
||||
|
||||
$releases = $query->paginate(config('nntmux.items_per_page'), ['*'], 'page', $page);
|
||||
|
||||
if (! $hasFilters) {
|
||||
$expiresAt = now()->addMinutes(config('nntmux.cache_expiry_long'));
|
||||
Cache::put(md5('releasesRange_'.$page), $releases, $expiresAt);
|
||||
if ($cacheable && $cacheKey !== null) {
|
||||
Cache::put($cacheKey, $releases, $expiresAt);
|
||||
}
|
||||
|
||||
return $releases;
|
||||
@@ -465,6 +467,8 @@ class Release extends Model
|
||||
*/
|
||||
public static function clearAdminReleasesRangeCache(int $maxPages = 25): void
|
||||
{
|
||||
Cache::forever('adminReleasesRangeVersion', ((int) Cache::get('adminReleasesRangeVersion', 1)) + 1);
|
||||
|
||||
for ($page = 1; $page <= $maxPages; $page++) {
|
||||
Cache::forget(md5('releasesRange_'.$page));
|
||||
}
|
||||
|
||||
@@ -126,8 +126,29 @@ class ReleaseReport extends Model
|
||||
int $perPage = 50
|
||||
): LengthAwarePaginator {
|
||||
$query = self::query()
|
||||
->with(['release', 'user', 'reviewer', 'responder'])
|
||||
->orderBy('created_at', 'desc');
|
||||
->select([
|
||||
'id',
|
||||
'releases_id',
|
||||
'users_id',
|
||||
'reason',
|
||||
'description',
|
||||
'response',
|
||||
'status',
|
||||
'reviewed_by',
|
||||
'reviewed_at',
|
||||
'responded_by',
|
||||
'responded_at',
|
||||
'response_is_public',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])
|
||||
->with([
|
||||
'release:id,guid,searchname,size',
|
||||
'user:id,username',
|
||||
'reviewer:id,username',
|
||||
'responder:id,username',
|
||||
])
|
||||
->orderByDesc('created_at');
|
||||
|
||||
if ($status !== null && $status !== 'all') {
|
||||
$query->where('status', $status);
|
||||
|
||||
@@ -215,7 +215,21 @@ class UsenetGroup extends Model
|
||||
|
||||
public static function getGroupsRange(string $groupname = '', mixed $active = null): LengthAwarePaginator // @phpstan-ignore missingType.generics
|
||||
{
|
||||
$groups = self::query()->groupBy('id')->orderBy('name');
|
||||
$groups = self::query()
|
||||
->select([
|
||||
'id',
|
||||
'name',
|
||||
'description',
|
||||
'first_record_postdate',
|
||||
'last_record_postdate',
|
||||
'last_updated',
|
||||
'active',
|
||||
'backfill',
|
||||
'minfilestoformrelease',
|
||||
'minsizetoformrelease',
|
||||
'backfill_target',
|
||||
])
|
||||
->orderBy('name');
|
||||
|
||||
if ($groupname !== '') {
|
||||
$groups->where('name', 'like', '%'.$groupname.'%');
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* @var array<string, list<string>>
|
||||
*/
|
||||
private array $indexes = [
|
||||
'releases' => [
|
||||
'ix_releases_categories_postdate_admin',
|
||||
'ix_releases_postdate_admin',
|
||||
],
|
||||
'usenet_groups' => [
|
||||
'ix_usenet_groups_active_name_admin',
|
||||
],
|
||||
'release_reports' => [
|
||||
'ix_release_reports_status_created_admin',
|
||||
],
|
||||
];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('releases') && ! $this->indexExists('releases', 'ix_releases_categories_postdate_admin')) {
|
||||
Schema::table('releases', function (Blueprint $table): void {
|
||||
$table->index(['categories_id', 'postdate'], 'ix_releases_categories_postdate_admin');
|
||||
});
|
||||
}
|
||||
|
||||
if (Schema::hasTable('releases') && ! $this->indexExists('releases', 'ix_releases_postdate_admin')) {
|
||||
Schema::table('releases', function (Blueprint $table): void {
|
||||
$table->index('postdate', 'ix_releases_postdate_admin');
|
||||
});
|
||||
}
|
||||
|
||||
if (Schema::hasTable('usenet_groups') && ! $this->indexExists('usenet_groups', 'ix_usenet_groups_active_name_admin')) {
|
||||
Schema::table('usenet_groups', function (Blueprint $table): void {
|
||||
$table->index(['active', 'name'], 'ix_usenet_groups_active_name_admin');
|
||||
});
|
||||
}
|
||||
|
||||
if (Schema::hasTable('release_reports') && ! $this->indexExists('release_reports', 'ix_release_reports_status_created_admin')) {
|
||||
Schema::table('release_reports', function (Blueprint $table): void {
|
||||
$table->index(['status', 'created_at'], 'ix_release_reports_status_created_admin');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
foreach ($this->indexes as $table => $indexNames) {
|
||||
if (! Schema::hasTable($table)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($indexNames as $indexName) {
|
||||
if ($this->indexExists($table, $indexName)) {
|
||||
Schema::table($table, function (Blueprint $table) use ($indexName): void {
|
||||
$table->dropIndex($indexName);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function indexExists(string $table, string $indexName): bool
|
||||
{
|
||||
try {
|
||||
return Schema::hasIndex($table, $indexName);
|
||||
} catch (Throwable) {
|
||||
if (DB::getDriverName() === 'sqlite') {
|
||||
$indexes = DB::select("PRAGMA index_list('{$table}')");
|
||||
|
||||
foreach ($indexes as $index) {
|
||||
if (($index->name ?? null) === $indexName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$indexes = DB::select("SHOW INDEX FROM `{$table}` WHERE Key_name = ?", [$indexName]);
|
||||
|
||||
return count($indexes) > 0;
|
||||
}
|
||||
};
|
||||
@@ -199,23 +199,6 @@ Alpine.data('adminUserEdit', () => ({
|
||||
}
|
||||
}));
|
||||
|
||||
// Verify User Modal
|
||||
Alpine.data('verifyUser', () => ({
|
||||
open: false,
|
||||
_form: null,
|
||||
|
||||
show(form) { this._form = form; this.open = true; },
|
||||
hide() { this.open = false; this._form = null; },
|
||||
submit() { if (this._form) this._form.submit(); this.hide(); },
|
||||
|
||||
init() {
|
||||
const self = this;
|
||||
window.showVerifyModal = function(e, form) { e.preventDefault(); self.show(form); };
|
||||
window.hideVerifyModal = function() { self.hide(); };
|
||||
window.submitVerifyForm = function() { self.submit(); };
|
||||
}
|
||||
}));
|
||||
|
||||
// Admin deleted users
|
||||
Alpine.data('adminDeletedUsers', () => ({
|
||||
allChecked: false,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Alpine.data('verifyUser') - Admin user verification confirmation modal.
|
||||
*/
|
||||
import Alpine from '@alpinejs/csp';
|
||||
|
||||
Alpine.data('verifyUser', () => ({
|
||||
open: false,
|
||||
_form: null,
|
||||
|
||||
show(form) {
|
||||
this._form = form;
|
||||
this.open = true;
|
||||
},
|
||||
|
||||
hide() {
|
||||
this.open = false;
|
||||
this._form = null;
|
||||
},
|
||||
|
||||
submit() {
|
||||
if (this._form) {
|
||||
this._form.submit();
|
||||
}
|
||||
|
||||
this.hide();
|
||||
},
|
||||
|
||||
init() {
|
||||
window.showVerifyModal = (event, form) => {
|
||||
event.preventDefault();
|
||||
this.show(form);
|
||||
};
|
||||
window.hideVerifyModal = () => this.hide();
|
||||
window.submitVerifyForm = () => this.submit();
|
||||
},
|
||||
}));
|
||||
@@ -57,7 +57,7 @@ const lazyComponentMap = {
|
||||
'adminInvitations': () => import('./components/admin/features.js'), // same file
|
||||
'adminRegexForm': () => import('./components/admin/features.js'), // same file
|
||||
'tinyMceEditor': () => import('./components/admin/features.js'), // same file
|
||||
'verifyUser': () => import('./components/admin/features.js'), // same file
|
||||
'verifyUser': () => import('./components/admin/verify-user.js'),
|
||||
'tmuxEdit': () => import('./components/admin/features.js'), // same file
|
||||
};
|
||||
|
||||
|
||||
@@ -2,29 +2,15 @@
|
||||
|
||||
@section('content')
|
||||
<div x-data="adminGroups" class="space-y-6" data-ajax-url="{{ url('/admin/ajax') }}" data-csrf-token="{{ csrf_token() }}">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm">
|
||||
<!-- Header -->
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex flex-wrap justify-between items-center gap-3">
|
||||
<h1 class="text-2xl font-semibold text-gray-800 dark:text-gray-100">
|
||||
<i class="fas fa-users mr-2"></i>{{ $title ?? 'Group List' }}
|
||||
</h1>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="{{ url('/admin/group-list-active') }}" class="px-3 py-2 bg-blue-600 dark:bg-blue-700 text-white text-sm rounded-lg hover:bg-blue-700">
|
||||
<i class="fas fa-check-circle mr-1"></i>Active Groups
|
||||
</a>
|
||||
<a href="{{ url('/admin/group-list-inactive') }}" class="px-3 py-2 bg-gray-600 text-white text-sm rounded-lg hover:bg-gray-700">
|
||||
<i class="fas fa-times-circle mr-1"></i>Inactive Groups
|
||||
</a>
|
||||
<a href="{{ url('/admin/group-list') }}" class="px-3 py-2 bg-indigo-600 text-white text-sm rounded-lg hover:bg-indigo-700">
|
||||
<i class="fas fa-list mr-1"></i>All Groups
|
||||
</a>
|
||||
<a href="{{ url('/admin/group-bulk') }}" class="px-3 py-2 bg-green-600 dark:bg-green-700 text-white text-sm rounded-lg hover:bg-green-700">
|
||||
<i class="fas fa-plus-circle mr-1"></i>Bulk Add
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<x-admin.card>
|
||||
<x-admin.page-header :title="$title ?? 'Group List'" icon="fas fa-users" subtitle="Activate, backfill, reset, and purge indexed Usenet groups.">
|
||||
<x-slot:actions>
|
||||
<x-admin.button :href="url('/admin/group-list-active')" icon="fas fa-check-circle">Active</x-admin.button>
|
||||
<x-admin.button :href="url('/admin/group-list-inactive')" tone="gray" icon="fas fa-times-circle">Inactive</x-admin.button>
|
||||
<x-admin.button :href="url('/admin/group-list')" tone="gray" icon="fas fa-list">All</x-admin.button>
|
||||
<x-admin.button :href="url('/admin/group-bulk')" tone="success" icon="fas fa-plus-circle">Bulk Add</x-admin.button>
|
||||
</x-slot:actions>
|
||||
</x-admin.page-header>
|
||||
|
||||
<!-- Info Alert -->
|
||||
<div class="px-6 py-4 bg-blue-50 dark:bg-blue-900/20 border-b border-blue-100 dark:border-blue-900">
|
||||
@@ -44,8 +30,7 @@
|
||||
@endif
|
||||
|
||||
@if($grouplist && $grouplist->count() > 0)
|
||||
<!-- Search and Actions Bar -->
|
||||
<div class="px-6 py-4 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
|
||||
<x-admin.action-bar>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<!-- Search Form -->
|
||||
<div>
|
||||
@@ -62,9 +47,7 @@
|
||||
class="pl-10 w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-400"
|
||||
placeholder="Search for group...">
|
||||
</div>
|
||||
<button type="submit" class="px-4 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded-lg hover:bg-blue-700">
|
||||
Go
|
||||
</button>
|
||||
<x-admin.button type="submit" icon="fas fa-search">Go</x-admin.button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -81,54 +64,54 @@
|
||||
<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"
|
||||
<x-admin.button type="button"
|
||||
id="reset-selected-btn"
|
||||
@click="handleAction('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="fas fa-refresh mr-1"></i> Reset Selected
|
||||
</button>
|
||||
<button type="button"
|
||||
tone="warning"
|
||||
icon="fas fa-refresh"
|
||||
class="hidden">
|
||||
Reset Selected
|
||||
</x-admin.button>
|
||||
<x-admin.button type="button"
|
||||
@click="handleAction('show-reset-modal')"
|
||||
class="px-3 py-2 bg-yellow-600 text-white text-sm rounded-lg hover:bg-yellow-700">
|
||||
<i class="fas fa-refresh mr-1"></i> Reset All
|
||||
</button>
|
||||
<button type="button"
|
||||
tone="warning"
|
||||
icon="fas fa-refresh">
|
||||
Reset All
|
||||
</x-admin.button>
|
||||
<x-admin.button type="button"
|
||||
@click="handleAction('show-purge-modal')"
|
||||
class="px-3 py-2 bg-red-600 dark:bg-red-700 text-white text-sm rounded-lg hover:bg-red-700">
|
||||
<i class="fas fa-trash mr-1"></i> Purge All
|
||||
</button>
|
||||
tone="danger"
|
||||
icon="fas fa-trash">
|
||||
Purge All
|
||||
</x-admin.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-admin.action-bar>
|
||||
|
||||
<!-- Groups Table -->
|
||||
<div class="overflow-x-auto">
|
||||
<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">
|
||||
<x-admin.data-table sticky>
|
||||
<x-slot:head>
|
||||
<x-admin.th align="center" class="w-12">
|
||||
<input type="checkbox"
|
||||
id="select-all-groups"
|
||||
x-model="allChecked"
|
||||
@change="toggleAllCheckboxes()"
|
||||
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>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Last Updated</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-32">Status</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-32">Backfill</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-24">Releases</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-24">Min Files</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-24">Min Size</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-32">Backfill Days</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-40">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
</x-admin.th>
|
||||
<x-admin.th>Group</x-admin.th>
|
||||
<x-admin.th>First Post</x-admin.th>
|
||||
<x-admin.th>Last Post</x-admin.th>
|
||||
<x-admin.th>Last Updated</x-admin.th>
|
||||
<x-admin.th align="center" class="w-32">Status</x-admin.th>
|
||||
<x-admin.th align="center" class="w-32">Backfill</x-admin.th>
|
||||
<x-admin.th align="center" class="w-24">Releases</x-admin.th>
|
||||
<x-admin.th align="center" class="w-24">Min Files</x-admin.th>
|
||||
<x-admin.th align="center" class="w-24">Min Size</x-admin.th>
|
||||
<x-admin.th align="center" class="w-32">Backfill Days</x-admin.th>
|
||||
<x-admin.th align="center" class="w-40">Actions</x-admin.th>
|
||||
</x-slot:head>
|
||||
@foreach($grouplist as $group)
|
||||
<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">
|
||||
@@ -243,9 +226,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</x-admin.data-table>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
|
||||
@@ -259,16 +240,11 @@
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="px-6 py-12 text-center">
|
||||
<i class="fas fa-exclamation-triangle text-gray-400 text-5xl mb-4"></i>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">No groups available</h3>
|
||||
<p class="text-gray-500 mb-4">No groups have been added yet.</p>
|
||||
<a href="{{ url('/admin/group-bulk') }}" class="inline-flex items-center px-4 py-2 bg-green-600 dark:bg-green-700 text-white rounded-lg hover:bg-green-700">
|
||||
<i class="fas fa-plus-circle mr-2"></i>Add Groups
|
||||
</a>
|
||||
</div>
|
||||
<x-admin.empty-state icon="fas fa-exclamation-triangle" title="No groups available" message="No groups have been added yet.">
|
||||
<x-admin.button :href="url('/admin/group-bulk')" tone="success" icon="fas fa-plus-circle" class="mt-4">Add Groups</x-admin.button>
|
||||
</x-admin.empty-state>
|
||||
@endif
|
||||
</div>
|
||||
</x-admin.card>
|
||||
|
||||
<!-- Reset All Modal -->
|
||||
<div x-show="resetAllOpen"
|
||||
@@ -385,4 +361,3 @@
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<x-admin.card>
|
||||
<x-admin.page-header :title="$title" icon="fas fa-file-lines" subtitle="Browse files from storage/logs and search inside the selected log." />
|
||||
|
||||
<x-admin.search-bar>
|
||||
<x-admin.filter-panel>
|
||||
@php
|
||||
$clearSearchParams = array_filter([
|
||||
'file' => $selectedFile !== '' ? $selectedFile : null,
|
||||
@@ -50,31 +50,19 @@
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-4 flex flex-wrap gap-2">
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">
|
||||
<i class="fas fa-search mr-2"></i>Apply
|
||||
</button>
|
||||
<x-admin.button type="submit" icon="fas fa-search">Apply</x-admin.button>
|
||||
|
||||
@if($selectedFile !== '')
|
||||
<a href="{{ route('admin.logs.index', $clearSearchParams) }}"
|
||||
class="inline-flex items-center 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 transition">
|
||||
<i class="fas fa-eraser mr-2"></i>Clear Search
|
||||
</a>
|
||||
<x-admin.button :href="route('admin.logs.index', $clearSearchParams)" tone="gray" icon="fas fa-eraser">Clear Search</x-admin.button>
|
||||
@endif
|
||||
|
||||
<a href="{{ route('admin.logs.index') }}"
|
||||
class="inline-flex items-center px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 transition">
|
||||
<i class="fas fa-rotate-left mr-2"></i>Reset
|
||||
</a>
|
||||
<x-admin.button :href="route('admin.logs.index')" tone="ghost" icon="fas fa-rotate-left">Reset</x-admin.button>
|
||||
</div>
|
||||
</form>
|
||||
</x-admin.search-bar>
|
||||
</x-admin.filter-panel>
|
||||
|
||||
@if(empty($availableLogs))
|
||||
<div class="px-6 py-12 text-center">
|
||||
<i class="fas fa-file-circle-xmark text-gray-400 dark:text-gray-600 text-5xl mb-4"></i>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">No log files found</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">The `storage/logs` directory does not currently contain any readable files.</p>
|
||||
</div>
|
||||
<x-admin.empty-state icon="fas fa-file-circle-xmark" title="No log files found" message="The storage/logs directory does not currently contain any readable files." />
|
||||
@elseif($selectedLog !== null)
|
||||
<div class="px-6 py-4 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4 text-sm">
|
||||
@@ -120,11 +108,7 @@
|
||||
|
||||
<x-admin.pagination :paginator="$searchResults" />
|
||||
@else
|
||||
<div class="px-6 py-12 text-center">
|
||||
<i class="fas fa-magnifying-glass text-gray-400 dark:text-gray-600 text-4xl mb-4"></i>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">No search results</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">Try a different search term or clear the search to view the latest lines.</p>
|
||||
</div>
|
||||
<x-admin.empty-state icon="fas fa-magnifying-glass" title="No search results" message="Try a different search term or clear the search to view the latest lines." />
|
||||
@endif
|
||||
@elseif($tailView !== null)
|
||||
<div class="px-6 py-3 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 text-sm text-gray-700 dark:text-gray-300">
|
||||
@@ -136,11 +120,7 @@
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="px-6 py-12 text-center">
|
||||
<i class="fas fa-file-lines text-gray-400 dark:text-gray-600 text-5xl mb-4"></i>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">Select a log file</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">Choose a file from the dropdown above to inspect its contents.</p>
|
||||
</div>
|
||||
<x-admin.empty-state icon="fas fa-file-lines" title="Select a log file" message="Choose a file from the dropdown above to inspect its contents." />
|
||||
@endif
|
||||
</x-admin.card>
|
||||
</div>
|
||||
|
||||
@@ -28,18 +28,9 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm">
|
||||
<!-- Header -->
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-gray-800 dark:text-gray-200">
|
||||
<i class="fas fa-flag mr-2 text-red-500"></i>{{ $title }}
|
||||
</h1>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">Manage release reports submitted by users</p>
|
||||
</div>
|
||||
|
||||
<!-- Status Stats -->
|
||||
<x-admin.card>
|
||||
<x-admin.page-header :title="$title" icon="fas fa-flag" subtitle="Manage release reports submitted by users.">
|
||||
<x-slot:actions>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="{{ route('admin.release-reports', ['status' => 'pending']) }}"
|
||||
class="px-3 py-1.5 rounded-full text-sm font-medium transition {{ $status === 'pending' ? 'bg-yellow-500 text-white' : 'bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 hover:bg-yellow-200 dark:hover:bg-yellow-800' }}">
|
||||
@@ -62,25 +53,27 @@
|
||||
<i class="fas fa-list mr-1"></i> All ({{ $statusCounts['total'] }})
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-slot:actions>
|
||||
</x-admin.page-header>
|
||||
|
||||
<!-- Reports Table -->
|
||||
@if($reportsList->count() > 0)
|
||||
<form id="bulk-action-form" method="POST" action="{{ route('admin.release-reports.bulk') }}" @submit="validateBulkAction($event)">
|
||||
@csrf
|
||||
<div class="px-6 py-3 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3">
|
||||
<x-admin.action-bar>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button type="button"
|
||||
<x-admin.button type="button"
|
||||
@click="selectAll()"
|
||||
class="px-3 py-1.5 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-lg hover:bg-blue-200 dark:hover:bg-blue-800 transition text-sm font-medium">
|
||||
<i class="fas fa-check-square mr-1"></i> Select All
|
||||
</button>
|
||||
<button type="button"
|
||||
tone="gray"
|
||||
icon="fas fa-check-square">
|
||||
Select All
|
||||
</x-admin.button>
|
||||
<x-admin.button type="button"
|
||||
@click="clearSelection()"
|
||||
class="px-3 py-1.5 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition text-sm font-medium">
|
||||
<i class="fas fa-square mr-1"></i> Clear Selection
|
||||
</button>
|
||||
tone="gray"
|
||||
icon="fas fa-square">
|
||||
Clear Selection
|
||||
</x-admin.button>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<span x-text="selectedCount"></span> selected
|
||||
</span>
|
||||
@@ -95,29 +88,23 @@
|
||||
<option value="revert">Revert to Reviewed</option>
|
||||
<option value="delete">Delete Releases & Resolve</option>
|
||||
</select>
|
||||
<button type="submit" class="px-4 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition">
|
||||
Apply
|
||||
</button>
|
||||
<x-admin.button type="submit" icon="fas fa-check">Apply</x-admin.button>
|
||||
</div>
|
||||
</div>
|
||||
</x-admin.action-bar>
|
||||
</form>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<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-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-10">Select</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">ID</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Release</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Reporter</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Reason</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Response</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Status</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Date</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<x-admin.data-table sticky>
|
||||
<x-slot:head>
|
||||
<x-admin.th class="w-10">Select</x-admin.th>
|
||||
<x-admin.th>ID</x-admin.th>
|
||||
<x-admin.th>Release</x-admin.th>
|
||||
<x-admin.th>Reporter</x-admin.th>
|
||||
<x-admin.th>Reason</x-admin.th>
|
||||
<x-admin.th>Response</x-admin.th>
|
||||
<x-admin.th>Status</x-admin.th>
|
||||
<x-admin.th>Date</x-admin.th>
|
||||
<x-admin.th>Actions</x-admin.th>
|
||||
</x-slot:head>
|
||||
@foreach($reportsList as $report)
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td class="px-4 py-4 whitespace-nowrap">
|
||||
@@ -332,9 +319,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</x-admin.data-table>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700">
|
||||
@@ -354,7 +339,7 @@
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-admin.card>
|
||||
</div>
|
||||
|
||||
<!-- Report Description Modal -->
|
||||
@@ -525,4 +510,3 @@
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="px-6 py-4 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
|
||||
<x-admin.filter-panel>
|
||||
<form method="GET" action="{{ route('admin.release-list') }}" class="flex flex-col sm:flex-row gap-2">
|
||||
<div class="relative flex-1">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
@@ -37,16 +37,16 @@
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit" class="px-4 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded-lg hover:bg-blue-700 dark:hover:bg-blue-800">
|
||||
<x-admin.button type="submit" icon="fas fa-filter">
|
||||
Apply
|
||||
</button>
|
||||
</x-admin.button>
|
||||
@if(request('search') || (request()->filled('category_id') && (int) request('category_id') !== -1))
|
||||
<a href="{{ route('admin.release-list') }}" class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 text-center">
|
||||
<x-admin.button :href="route('admin.release-list')" tone="gray" icon="fas fa-eraser">
|
||||
Clear
|
||||
</a>
|
||||
</x-admin.button>
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
</x-admin.filter-panel>
|
||||
|
||||
@if($releaselist->count() > 0)
|
||||
<form id="bulk-category-form"
|
||||
@@ -54,18 +54,20 @@
|
||||
action="{{ route('admin.release-bulk-category') }}"
|
||||
@submit="validateBulkAction($event)">
|
||||
@csrf
|
||||
<div class="px-6 py-3 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3">
|
||||
<x-admin.action-bar>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button type="button"
|
||||
<x-admin.button type="button"
|
||||
@click="selectAll()"
|
||||
class="px-3 py-1.5 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-lg hover:bg-blue-200 dark:hover:bg-blue-800 transition text-sm font-medium">
|
||||
<i class="fas fa-check-square mr-1"></i> Select All
|
||||
</button>
|
||||
<button type="button"
|
||||
tone="gray"
|
||||
icon="fas fa-check-square">
|
||||
Select All
|
||||
</x-admin.button>
|
||||
<x-admin.button type="button"
|
||||
@click="clearSelection()"
|
||||
class="px-3 py-1.5 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition text-sm font-medium">
|
||||
<i class="fas fa-square mr-1"></i> Clear Selection
|
||||
</button>
|
||||
tone="gray"
|
||||
icon="fas fa-square">
|
||||
Clear Selection
|
||||
</x-admin.button>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<span x-text="selectedCount"></span> selected
|
||||
</span>
|
||||
@@ -79,14 +81,12 @@
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit" class="px-4 py-2 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700 transition">
|
||||
<i class="fas fa-tags mr-1"></i> Change Category
|
||||
</button>
|
||||
<x-admin.button type="submit" tone="success" icon="fas fa-tags">Change Category</x-admin.button>
|
||||
</div>
|
||||
</div>
|
||||
</x-admin.action-bar>
|
||||
</form>
|
||||
|
||||
<x-admin.data-table>
|
||||
<x-admin.data-table sticky>
|
||||
<x-slot:head>
|
||||
<x-admin.th class="w-10">
|
||||
<span class="sr-only">Select</span>
|
||||
@@ -172,7 +172,7 @@
|
||||
|
||||
<x-admin.pagination :paginator="$releaselist" />
|
||||
@else
|
||||
<x-empty-state
|
||||
<x-admin.empty-state
|
||||
icon="fas fa-list"
|
||||
title="No releases found"
|
||||
message="{{ request('search') || (request()->filled('category_id') && (int) request('category_id') !== -1) ? 'No releases match your filters.' : 'No releases are currently available in the system.' }}"
|
||||
|
||||
@@ -2,17 +2,15 @@
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-6" x-data="adminUserList">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<x-admin.card>
|
||||
<x-admin.page-header :title="$title" icon="fas fa-users">
|
||||
<x-slot:actions>
|
||||
<a href="{{ url('admin/user-edit?action=add') }}" class="px-4 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded-lg hover:bg-blue-700 dark:hover:bg-blue-800">
|
||||
<i class="fas fa-plus mr-2"></i>Add New User
|
||||
</a>
|
||||
<x-admin.button :href="url('admin/user-edit?action=add')" icon="fas fa-plus">Add New User</x-admin.button>
|
||||
</x-slot:actions>
|
||||
</x-admin.page-header>
|
||||
|
||||
<!-- Search Filters -->
|
||||
<x-admin.search-bar>
|
||||
<x-admin.filter-panel>
|
||||
<form method="get" action="{{ url('admin/user-list') }}">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div>
|
||||
@@ -83,15 +81,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex gap-2">
|
||||
<button type="submit" class="px-4 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded-md hover:bg-blue-700 dark:hover:bg-blue-800">
|
||||
<i class="fas fa-search mr-2"></i>Filter
|
||||
</button>
|
||||
<a href="{{ url('admin/user-list') }}" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-300 dark:hover:bg-gray-600">
|
||||
<i class="fas fa-times mr-2"></i>Clear
|
||||
</a>
|
||||
<x-admin.button type="submit" icon="fas fa-search">Filter</x-admin.button>
|
||||
<x-admin.button :href="url('admin/user-list')" tone="gray" icon="fas fa-times">Clear</x-admin.button>
|
||||
</div>
|
||||
</form>
|
||||
</x-admin.search-bar>
|
||||
</x-admin.filter-panel>
|
||||
|
||||
<!-- Validation Messages -->
|
||||
@if($errors->any())
|
||||
@@ -120,11 +114,12 @@
|
||||
<option value="resend_verification">Resend Verification Email</option>
|
||||
<option value="delete">Soft Delete Selected</option>
|
||||
</select>
|
||||
<button type="submit"
|
||||
<x-admin.button type="submit"
|
||||
id="bulkUserActionBtn"
|
||||
class="w-full md:w-auto px-4 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded-md hover:bg-blue-700 dark:hover:bg-blue-800">
|
||||
<i class="fas fa-check mr-2"></i>Apply
|
||||
</button>
|
||||
class="w-full md:w-auto"
|
||||
icon="fas fa-check">
|
||||
Apply
|
||||
</x-admin.button>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Select non-admin active users below. Deleted users are managed from the Deleted Users page.
|
||||
</p>
|
||||
@@ -400,13 +395,9 @@
|
||||
{{ $userlist->links() }}
|
||||
</div>
|
||||
@else
|
||||
<div class="px-6 py-12 text-center">
|
||||
<i class="fas fa-users text-gray-400 text-5xl mb-4"></i>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">No users found</h3>
|
||||
<p class="text-gray-500">Try adjusting your search filters or add a new user.</p>
|
||||
</div>
|
||||
<x-admin.empty-state icon="fas fa-users" title="No users found" message="Try adjusting your search filters or add a new user." />
|
||||
@endif
|
||||
</div>
|
||||
</x-admin.card>
|
||||
</div>
|
||||
|
||||
<!-- Verify User Confirmation Modal (x-data mounts verifyUser so showVerifyModal/submitVerifyForm exist) -->
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{{-- Dense action/filter bar for admin list pages --}}
|
||||
<div {{ $attributes->merge(['class' => 'px-4 sm:px-6 py-3 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700']) }}>
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
@props([
|
||||
'tone' => 'gray',
|
||||
'icon' => null,
|
||||
])
|
||||
|
||||
@php
|
||||
$classes = [
|
||||
'blue' => 'bg-blue-100 dark:bg-blue-900/40 text-blue-800 dark:text-blue-200',
|
||||
'green' => 'bg-green-100 dark:bg-green-900/40 text-green-800 dark:text-green-200',
|
||||
'gray' => 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200',
|
||||
'orange' => 'bg-orange-100 dark:bg-orange-900/40 text-orange-800 dark:text-orange-200',
|
||||
'purple' => 'bg-purple-100 dark:bg-purple-900/40 text-purple-800 dark:text-purple-200',
|
||||
'red' => 'bg-red-100 dark:bg-red-900/40 text-red-800 dark:text-red-200',
|
||||
'yellow' => 'bg-yellow-100 dark:bg-yellow-900/40 text-yellow-800 dark:text-yellow-200',
|
||||
][$tone] ?? 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200';
|
||||
@endphp
|
||||
|
||||
<span {{ $attributes->merge(['class' => 'inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-semibold '.$classes]) }}>
|
||||
@if($icon)
|
||||
<i class="{{ $icon }}" aria-hidden="true"></i>
|
||||
@endif
|
||||
{{ $slot }}
|
||||
</span>
|
||||
@@ -0,0 +1,35 @@
|
||||
@props([
|
||||
'href' => null,
|
||||
'type' => 'button',
|
||||
'tone' => 'primary',
|
||||
'icon' => null,
|
||||
])
|
||||
|
||||
@php
|
||||
$classes = [
|
||||
'danger' => 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
|
||||
'ghost' => 'bg-transparent text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700 focus:ring-gray-400',
|
||||
'gray' => 'bg-gray-200 text-gray-800 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 focus:ring-gray-400',
|
||||
'primary' => 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
|
||||
'success' => 'bg-green-600 text-white hover:bg-green-700 focus:ring-green-500',
|
||||
'warning' => 'bg-yellow-600 text-white hover:bg-yellow-700 focus:ring-yellow-500',
|
||||
][$tone] ?? 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500';
|
||||
|
||||
$base = 'inline-flex min-h-9 items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-900 '.$classes;
|
||||
@endphp
|
||||
|
||||
@if($href)
|
||||
<a href="{{ $href }}" {{ $attributes->merge(['class' => $base]) }}>
|
||||
@if($icon)
|
||||
<i class="{{ $icon }}" aria-hidden="true"></i>
|
||||
@endif
|
||||
{{ $slot }}
|
||||
</a>
|
||||
@else
|
||||
<button type="{{ $type }}" {{ $attributes->merge(['class' => $base]) }}>
|
||||
@if($icon)
|
||||
<i class="{{ $icon }}" aria-hidden="true"></i>
|
||||
@endif
|
||||
{{ $slot }}
|
||||
</button>
|
||||
@endif
|
||||
@@ -3,7 +3,6 @@
|
||||
'noPadding' => false,
|
||||
])
|
||||
|
||||
<div {{ $attributes->merge(['class' => 'bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700']) }}>
|
||||
<div {{ $attributes->merge(['class' => 'bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700']) }}>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
{{-- Admin data table with consistent thead styling --}}
|
||||
@props([
|
||||
'dense' => true,
|
||||
'sticky' => false,
|
||||
'striped' => false,
|
||||
])
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table {{ $attributes->merge(['class' => 'min-w-full divide-y divide-gray-200 dark:divide-gray-700']) }}>
|
||||
<div class="overflow-x-auto overscroll-x-contain">
|
||||
<table {{ $attributes->merge(['class' => 'min-w-full table-auto divide-y divide-gray-200 dark:divide-gray-700']) }}>
|
||||
@if(isset($head))
|
||||
<thead class="bg-gray-50 dark:bg-gray-900">
|
||||
<thead @class([
|
||||
'bg-gray-50 dark:bg-gray-900',
|
||||
'sticky top-0 z-10' => $sticky,
|
||||
])>
|
||||
<tr>
|
||||
{{ $head }}
|
||||
</tr>
|
||||
</thead>
|
||||
@endif
|
||||
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tbody @class([
|
||||
'bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700',
|
||||
'[&_tr:nth-child(even)]:bg-gray-50/50 dark:[&_tr:nth-child(even)]:bg-gray-900/25' => $striped,
|
||||
'[&_td]:px-4 [&_td]:py-3 [&_td]:text-sm' => $dense,
|
||||
'[&_td]:px-6 [&_td]:py-4' => ! $dense,
|
||||
])>
|
||||
{{ $slot }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
@props([
|
||||
'icon' => 'fas fa-circle-info',
|
||||
'title',
|
||||
'message' => null,
|
||||
])
|
||||
|
||||
<div {{ $attributes->merge(['class' => 'px-6 py-12 text-center']) }}>
|
||||
<i class="{{ $icon }} mb-4 text-4xl text-gray-400 dark:text-gray-600" aria-hidden="true"></i>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">{{ $title }}</h3>
|
||||
@if($message)
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ $message }}</p>
|
||||
@endif
|
||||
@if(trim($slot) !== '')
|
||||
<div class="mt-4">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
{{-- Compact filter panel for admin list pages --}}
|
||||
<div {{ $attributes->merge(['class' => 'px-4 sm:px-6 py-4 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700']) }}>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
@@ -13,7 +13,6 @@
|
||||
$widthClass = $width ? "w-{$width}" : '';
|
||||
@endphp
|
||||
|
||||
<th {{ $attributes->merge(['class' => "px-6 py-3 {$alignClass} text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider {$widthClass}"]) }}>
|
||||
<th {{ $attributes->merge(['class' => "px-4 py-2.5 {$alignClass} text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide {$widthClass}"]) }}>
|
||||
{{ $slot }}
|
||||
</th>
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use PDO;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminListPerformancePlanTest extends TestCase
|
||||
{
|
||||
private string $databasePath = '';
|
||||
|
||||
/**
|
||||
* @var array<string, string|false>
|
||||
*/
|
||||
private array $originalEnvironment = [];
|
||||
|
||||
public function createApplication()
|
||||
{
|
||||
$this->databasePath = sys_get_temp_dir().'/nntmux-admin-list-performance-test.sqlite';
|
||||
|
||||
$this->originalEnvironment = [
|
||||
'APP_ENV' => getenv('APP_ENV'),
|
||||
'DB_CONNECTION' => getenv('DB_CONNECTION'),
|
||||
'DB_DATABASE' => getenv('DB_DATABASE'),
|
||||
];
|
||||
|
||||
if (file_exists($this->databasePath)) {
|
||||
unlink($this->databasePath);
|
||||
}
|
||||
|
||||
$pdo = new PDO('sqlite:'.$this->databasePath);
|
||||
$pdo->exec('CREATE TABLE settings (name VARCHAR PRIMARY KEY, value TEXT NULL)');
|
||||
$pdo->exec("INSERT INTO settings (name, value) VALUES
|
||||
('categorizeforeign', '0'),
|
||||
('catwebdl', '0'),
|
||||
('innerfileblacklist', ''),
|
||||
('title', 'NNTmux Test'),
|
||||
('home_link', '/')");
|
||||
|
||||
$this->setEnvironmentValue('APP_ENV', 'testing');
|
||||
$this->setEnvironmentValue('DB_CONNECTION', 'sqlite');
|
||||
$this->setEnvironmentValue('DB_DATABASE', $this->databasePath);
|
||||
|
||||
$app = require __DIR__.'/../../bootstrap/app.php';
|
||||
|
||||
$app->make(Kernel::class)->bootstrap();
|
||||
|
||||
return $app;
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if ($this->databasePath !== '' && file_exists($this->databasePath)) {
|
||||
unlink($this->databasePath);
|
||||
}
|
||||
|
||||
foreach ($this->originalEnvironment as $key => $value) {
|
||||
$this->setEnvironmentValue($key, $value === false ? null : $value);
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
private function setEnvironmentValue(string $key, ?string $value): void
|
||||
{
|
||||
if ($value === null) {
|
||||
putenv($key);
|
||||
unset($_ENV[$key], $_SERVER[$key]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
putenv($key.'='.$value);
|
||||
$_ENV[$key] = $value;
|
||||
$_SERVER[$key] = $value;
|
||||
}
|
||||
|
||||
public function test_release_admin_list_uses_versioned_cache_keys(): void
|
||||
{
|
||||
$modelPath = app_path('Models/Release.php');
|
||||
|
||||
$this->assertFileExists($modelPath);
|
||||
|
||||
$content = file_get_contents($modelPath);
|
||||
|
||||
$this->assertStringContainsString('adminReleasesRangeVersion', $content);
|
||||
$this->assertStringContainsString(".'_'.(\$categoryId ?? 'all')", $content);
|
||||
$this->assertStringContainsString('Cache::forever(\'adminReleasesRangeVersion\'', $content);
|
||||
}
|
||||
|
||||
public function test_group_admin_list_no_longer_groups_by_id(): void
|
||||
{
|
||||
$modelPath = app_path('Models/UsenetGroup.php');
|
||||
|
||||
$this->assertFileExists($modelPath);
|
||||
|
||||
$content = file_get_contents($modelPath);
|
||||
$methodStart = strpos($content, 'public static function getGroupsRange');
|
||||
$this->assertIsInt($methodStart);
|
||||
$methodBody = substr($content, $methodStart, 1200);
|
||||
|
||||
$this->assertStringContainsString("->select([\n 'id',", $methodBody);
|
||||
$this->assertStringContainsString("->orderBy('name')", $methodBody);
|
||||
$this->assertStringNotContainsString('groupBy', $methodBody);
|
||||
}
|
||||
|
||||
public function test_admin_list_index_migration_contains_narrow_indexes(): void
|
||||
{
|
||||
$migrationPath = database_path('migrations/2026_07_13_000000_add_admin_list_performance_indexes.php');
|
||||
|
||||
$this->assertFileExists($migrationPath);
|
||||
|
||||
$content = file_get_contents($migrationPath);
|
||||
|
||||
$this->assertStringContainsString('ix_releases_categories_postdate_admin', $content);
|
||||
$this->assertStringContainsString('ix_releases_postdate_admin', $content);
|
||||
$this->assertStringContainsString('ix_usenet_groups_active_name_admin', $content);
|
||||
$this->assertStringContainsString('ix_release_reports_status_created_admin', $content);
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,80 @@
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use PDO;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminReleaseReportControllerTest extends TestCase
|
||||
{
|
||||
private string $databasePath = '';
|
||||
|
||||
/**
|
||||
* @var array<string, string|false>
|
||||
*/
|
||||
private array $originalEnvironment = [];
|
||||
|
||||
public function createApplication()
|
||||
{
|
||||
$this->databasePath = sys_get_temp_dir().'/nntmux-admin-release-report-test.sqlite';
|
||||
|
||||
$this->originalEnvironment = [
|
||||
'APP_ENV' => getenv('APP_ENV'),
|
||||
'DB_CONNECTION' => getenv('DB_CONNECTION'),
|
||||
'DB_DATABASE' => getenv('DB_DATABASE'),
|
||||
];
|
||||
|
||||
if (file_exists($this->databasePath)) {
|
||||
unlink($this->databasePath);
|
||||
}
|
||||
|
||||
$pdo = new PDO('sqlite:'.$this->databasePath);
|
||||
$pdo->exec('CREATE TABLE settings (name VARCHAR PRIMARY KEY, value TEXT NULL)');
|
||||
$pdo->exec("INSERT INTO settings (name, value) VALUES
|
||||
('categorizeforeign', '0'),
|
||||
('catwebdl', '0'),
|
||||
('innerfileblacklist', ''),
|
||||
('title', 'NNTmux Test'),
|
||||
('home_link', '/')");
|
||||
|
||||
$this->setEnvironmentValue('APP_ENV', 'testing');
|
||||
$this->setEnvironmentValue('DB_CONNECTION', 'sqlite');
|
||||
$this->setEnvironmentValue('DB_DATABASE', $this->databasePath);
|
||||
|
||||
$app = require __DIR__.'/../../bootstrap/app.php';
|
||||
|
||||
$app->make(Kernel::class)->bootstrap();
|
||||
|
||||
return $app;
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if ($this->databasePath !== '' && file_exists($this->databasePath)) {
|
||||
unlink($this->databasePath);
|
||||
}
|
||||
|
||||
foreach ($this->originalEnvironment as $key => $value) {
|
||||
$this->setEnvironmentValue($key, $value === false ? null : $value);
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
private function setEnvironmentValue(string $key, ?string $value): void
|
||||
{
|
||||
if ($value === null) {
|
||||
putenv($key);
|
||||
unset($_ENV[$key], $_SERVER[$key]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
putenv($key.'='.$value);
|
||||
$_ENV[$key] = $value;
|
||||
$_SERVER[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the controller has the revert method.
|
||||
*/
|
||||
@@ -28,15 +98,21 @@ class AdminReleaseReportControllerTest extends TestCase
|
||||
*/
|
||||
public function test_bulk_action_supports_revert(): void
|
||||
{
|
||||
$controllerPath = app_path('Http/Controllers/Admin/AdminReleaseReportController.php');
|
||||
$requestPath = app_path('Http/Requests/Admin/AdminReleaseReportBulkActionRequest.php');
|
||||
|
||||
$this->assertFileExists($controllerPath);
|
||||
$this->assertFileExists($requestPath);
|
||||
|
||||
$content = file_get_contents($controllerPath);
|
||||
$content = file_get_contents($requestPath);
|
||||
|
||||
// Check that bulk action validation includes revert
|
||||
$this->assertStringContainsString("'action' => 'required|in:dismiss,resolve,reviewed,delete,revert'", $content);
|
||||
$this->assertStringContainsString("'revert' => 'reverted to reviewed'", $content);
|
||||
$this->assertStringContainsString("'in:dismiss,resolve,reviewed,delete,revert'", $content);
|
||||
$this->assertStringContainsString('public function reportIds', $content);
|
||||
$this->assertStringContainsString('public function actionName', $content);
|
||||
|
||||
$controllerPath = app_path('Http/Controllers/Admin/AdminReleaseReportController.php');
|
||||
$controllerContent = file_get_contents($controllerPath);
|
||||
$this->assertStringContainsString("'revert' => 'reverted to reviewed'", $controllerContent);
|
||||
$this->assertStringContainsString('bulkUpdateReportStatuses', $controllerContent);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,6 +203,38 @@ class AdminReleaseReportControllerTest extends TestCase
|
||||
$this->assertStringNotContainsString('.revert-report-btn', $content);
|
||||
}
|
||||
|
||||
public function test_verify_user_component_is_split_from_legacy_admin_features_bundle(): void
|
||||
{
|
||||
$loaderPath = resource_path('js/alpine/lazy-loader.js');
|
||||
$featuresPath = resource_path('js/alpine/components/admin/features.js');
|
||||
$verifyPath = resource_path('js/alpine/components/admin/verify-user.js');
|
||||
|
||||
$this->assertFileExists($loaderPath);
|
||||
$this->assertFileExists($featuresPath);
|
||||
$this->assertFileExists($verifyPath);
|
||||
|
||||
$loaderContent = file_get_contents($loaderPath);
|
||||
$featuresContent = file_get_contents($featuresPath);
|
||||
$verifyContent = file_get_contents($verifyPath);
|
||||
|
||||
$this->assertStringContainsString("'verifyUser': () => import('./components/admin/verify-user.js')", $loaderContent);
|
||||
$this->assertStringNotContainsString("Alpine.data('verifyUser'", $featuresContent);
|
||||
$this->assertStringContainsString("Alpine.data('verifyUser'", $verifyContent);
|
||||
}
|
||||
|
||||
public function test_release_report_list_uses_column_scoped_eager_loading(): void
|
||||
{
|
||||
$modelPath = app_path('Models/ReleaseReport.php');
|
||||
|
||||
$this->assertFileExists($modelPath);
|
||||
|
||||
$content = file_get_contents($modelPath);
|
||||
|
||||
$this->assertStringContainsString("'release:id,guid,searchname,size'", $content);
|
||||
$this->assertStringContainsString("'user:id,username'", $content);
|
||||
$this->assertStringContainsString("->orderByDesc('created_at')", $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that release report bulk-selection JS queries from the component root.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user