updated design

This commit is contained in:
Mark Gregory
2026-08-25 14:41:15 +01:00
parent 3e6b3bfc79
commit 3c7361023c
18 changed files with 1828 additions and 76 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
return [
'route_prefix' => env('HELPDESK_ROUTE_PREFIX', 'admin/helpdesk'),
'middleware' => ['web', 'auth', 'thredary.admin'],
'middleware' => ['web', 'auth'],
'disk' => env('HELPDESK_ATTACHMENTS_DISK', 'local'),
'max_upload_kb' => (int) env('HELPDESK_MAX_UPLOAD_KB', 10240),
'allowed_extensions' => [
@@ -6,7 +6,6 @@ namespace Modules\Helpdesk\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\View\View;
use Illuminate\Support\HtmlString;
final class HelpdeskController extends Controller
{
@@ -15,9 +14,6 @@ final class HelpdeskController extends Controller
*/
public function __invoke(): View
{
return view('layouts.app', [
'title' => __('Helpdesk'),
'slot' => new HtmlString(view('helpdesk::index')->render()),
]);
return view('modules.helpdesk.index');
}
}
@@ -21,7 +21,7 @@ final class TicketCommentDeleteController extends Controller
{
$user = $request->user();
abort_unless($user !== null && method_exists($user, 'isAdmin') && $user->isAdmin(), 403);
abort_unless($user !== null && $user->isHelpdeskAdmin(), 403);
abort_unless((int) $comment->ticket_id === (int) $ticket->id, 404);
DB::transaction(function () use ($comment, $ticket, $user): void {
@@ -156,7 +156,7 @@ final class TicketCommentStoreController extends Controller
return false;
}
return (method_exists($user, 'isAdmin') && $user->isAdmin()) || (bool) ($user->helpdesk_agent ?? false);
return $user->isHelpdeskAgent();
}
/**
@@ -17,9 +17,9 @@ final class HelpdeskDashboardWidget extends Component
$user = Auth::user();
$base = Ticket::query();
if ($user->helpdesk_admin ?? false) {
if ($user->isHelpdeskAdmin()) {
// no filter
} elseif ($user->helpdesk_agent ?? false) {
} elseif ($user->isHelpdeskAgent()) {
$base->where(function ($q) use ($user) {
$q->where('agent_id', $user->id)
->orWhere('user_id', $user->id);
+1 -1
View File
@@ -691,7 +691,7 @@ final class TicketShow extends Component
{
$user = Auth::user();
return $user !== null && (method_exists($user, 'isAdmin') ? $user->isAdmin() : (bool) ($user->helpdesk_admin ?? false));
return $user !== null && $user->isHelpdeskAdmin();
}
private function buildCommentTree(Collection $grouped, ?int $parentId = null): Collection
+2 -2
View File
@@ -194,11 +194,11 @@ final class Ticket extends Model
/** @param Builder<Ticket> $query @return Builder<Ticket> */
public function scopeVisibleTo(Builder $query, User $user): Builder
{
if ($user->isAdmin() || (bool) ($user->helpdesk_admin ?? false)) {
if ($user->isHelpdeskAdmin()) {
return $query;
}
if ((bool) ($user->helpdesk_agent ?? false)) {
if ($user->isHelpdeskAgent()) {
return $query->where(static function (Builder $subQuery) use ($user): void {
$subQuery->where('agent_id', $user->id)->orWhere('user_id', $user->id);
});
+3 -11
View File
@@ -17,7 +17,7 @@ final class TicketPolicy
public function view(User $user, Ticket $ticket): bool
{
return $this->isPrivileged($user)
|| ((bool) ($user->helpdesk_agent ?? false) && ((int) $ticket->agent_id === (int) $user->id || (int) $ticket->user_id === (int) $user->id))
|| ($user->isHelpdeskAgent() && ((int) $ticket->agent_id === (int) $user->id || (int) $ticket->user_id === (int) $user->id))
|| (int) $ticket->user_id === (int) $user->id;
}
@@ -29,7 +29,7 @@ final class TicketPolicy
public function update(User $user, Ticket $ticket): bool
{
return $this->isPrivileged($user)
|| ((bool) ($user->helpdesk_agent ?? false) && (int) $ticket->agent_id === (int) $user->id);
|| ($user->isHelpdeskAgent() && (int) $ticket->agent_id === (int) $user->id);
}
public function comment(User $user, Ticket $ticket): bool
@@ -44,14 +44,6 @@ final class TicketPolicy
private function isPrivileged(User $user): bool
{
if ($user->isAdmin() || (bool) ($user->helpdesk_admin ?? false)) {
return true;
}
if (method_exists($user, 'isAdmin') && $user->isAdmin()) {
return true;
}
return false;
return $user->isHelpdeskAdmin();
}
}
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Modules\Helpdesk\Providers;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;
@@ -82,8 +83,8 @@ final class HelpdeskServiceProvider extends ServiceProvider
{
Gate::policy(Ticket::class, TicketPolicy::class);
Gate::define('helpdesk-admin', static fn ($user): bool => method_exists($user, 'isAdmin') && $user->isAdmin());
Gate::define('helpdesk-agent', static fn ($user): bool => (method_exists($user, 'isAdmin') && $user->isAdmin()) || (bool) ($user->helpdesk_agent ?? false));
Gate::define('helpdesk-admin', static fn ($user): bool => $user->isHelpdeskAdmin());
Gate::define('helpdesk-agent', static fn ($user): bool => $user->isHelpdeskAgent());
}
/**
@@ -135,19 +136,14 @@ final class HelpdeskServiceProvider extends ServiceProvider
*/
protected function registerViews(): void
{
$viewPath = resource_path('views/modules/'.ModuleInfo::nameLower());
$sourcePath = module_path(ModuleInfo::name(), 'Resources/views');
$themeRoot = rtrim((string) config('stylist.themes_path', resource_path('themes')), DIRECTORY_SEPARATOR);
$backendTheme = (string) config('stylist.default_backend_theme', 'backend');
$viewPath = $themeRoot.DIRECTORY_SEPARATOR.$backendTheme
.DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR.'modules'.DIRECTORY_SEPARATOR.ModuleInfo::nameLower();
// Module source views remain module-owned. Any application override is
// published into the selected backend theme, never resources/views.
$this->publishes([
$sourcePath => $viewPath,
], ['views', ModuleInfo::nameLower().'-module-views']);
$this->loadViewsFrom($sourcePath, ModuleInfo::nameLower());
$this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), ModuleInfo::nameLower());
Blade::componentNamespace('Modules\Helpdesk\View\Components', 'helpdesk');
Blade::anonymousComponentPath($sourcePath.'/components', 'helpdesk');
@@ -169,6 +165,24 @@ final class HelpdeskServiceProvider extends ServiceProvider
$this->loadTranslationsFrom(module_path(ModuleInfo::name(), 'Resources/lang'), ModuleInfo::nameLower());
}
/**
* Get publishable view override paths.
*
* @return array<int, string>
*/
private function getPublishableViewPaths(): array
{
$paths = [];
foreach (Config::get('view.paths') as $path) {
if (is_dir($path.'/modules/'.ModuleInfo::nameLower())) {
$paths[] = $path.'/modules/'.ModuleInfo::nameLower();
}
}
return $paths;
}
/**
* Get provided services.
*
@@ -70,6 +70,7 @@ final class StylistServiceProvider extends ServiceProvider
// replace Laravel's base view locations during their own boot phase; the booted
// callback makes Stylist the final authority for ordinary view('...') resolution.
$this->registerSelectedThemeOverrides();
$this->registerSelectedThemeFluxOverrides();
$this->app->booted(function (): void {
$this->registerSelectedThemeOverrides();
@@ -247,10 +248,12 @@ final class StylistServiceProvider extends ServiceProvider
/**
* Make Flux application overrides theme-owned.
*
* Flux normally discovers custom components from resources/views/flux. This
* application deliberately has no resources/views tree, so expose the selected
* theme's views/flux directory through Flux's existing view namespace instead.
* Package-provided Flux views remain available as the final fallback.
* Flux registers anonymous component paths using the prefix "flux". Laravel
* converts that prefix into a hashed view namespace (xxh128 on Laravel 12+).
* Registering only a normal "flux" view namespace therefore does not satisfy
* Flux::componentExists(). Register each themed views/flux directory through
* Blade exactly as Flux itself does, then prepend it to the hashed namespace so
* the selected theme wins over package stubs while retaining package fallbacks.
*/
private function registerSelectedThemeFluxOverrides(): void
{
@@ -281,7 +284,7 @@ final class StylistServiceProvider extends ServiceProvider
$fluxPath = $theme->path().DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR.'flux';
if (is_dir($fluxPath)) {
if (is_dir($fluxPath) && ! in_array($fluxPath, $themeFluxPaths, true)) {
$themeFluxPaths[] = $fluxPath;
}
}
@@ -290,27 +293,25 @@ final class StylistServiceProvider extends ServiceProvider
return;
}
// This mirrors FluxServiceProvider::bootComponentPath(). It is essential:
// Flux::componentExists() checks the hashed namespace created by
// Blade::anonymousComponentPath(..., 'flux'), not a literal flux:: namespace.
foreach ($themeFluxPaths as $fluxPath) {
Blade::anonymousComponentPath($fluxPath, 'flux');
}
$fluxNamespace = version_compare($this->app->version(), '12.0.0', '>=')
? hash('xxh128', 'flux')
: md5('flux');
// anonymousComponentPath() adds locations to the namespace. Prepend the
// selected theme paths in reverse order so the first selected theme remains
// authoritative while Flux package views continue to work as fallbacks.
foreach (array_reverse($themeFluxPaths) as $fluxPath) {
View::prependNamespace($fluxNamespace, $fluxPath);
}
$finder = View::getFinder();
if (! method_exists($finder, 'getHints')) {
return;
}
$hints = $finder->getHints();
$existingFluxPaths = array_values(array_filter(
(array) ($hints['flux'] ?? []),
static fn (string $path): bool => is_dir($path),
));
$paths = array_values(array_unique(array_merge($themeFluxPaths, $existingFluxPaths)));
if (method_exists($finder, 'replaceNamespace')) {
$finder->replaceNamespace('flux', $paths);
} else {
foreach (array_reverse($themeFluxPaths) as $fluxPath) {
View::prependNamespace('flux', $fluxPath);
}
}
if (method_exists($finder, 'flush')) {
$finder->flush();
}