diff --git a/modules/Stylist/Http/Controllers/ThemeManagementController.php b/modules/Stylist/Http/Controllers/ThemeManagementController.php index 3c4f12f..51ecb89 100644 --- a/modules/Stylist/Http/Controllers/ThemeManagementController.php +++ b/modules/Stylist/Http/Controllers/ThemeManagementController.php @@ -59,6 +59,9 @@ final class ThemeManagementController extends Controller public function uninstall(string $theme, Loader $loader, ThemePackageManager $packages) { $item = $loader->find($theme); + abort_if(in_array($item->key(), ['sonardesk', 'sonardesk-admin'], true), 403, 'Bundled themes cannot be removed.'); + $settings = app(SettingsRepository::class); + abort_if($settings->get($item->type().'_theme', null, 'site') === $item->key(), 409, 'Deactivate this theme before uninstalling.'); $packages->uninstall($item); return back()->with('status', $item->name().' uninstalled.'); diff --git a/modules/Stylist/Http/Middleware/ApplyActiveTheme.php b/modules/Stylist/Http/Middleware/ApplyActiveTheme.php index c1e67a2..0033fc6 100644 --- a/modules/Stylist/Http/Middleware/ApplyActiveTheme.php +++ b/modules/Stylist/Http/Middleware/ApplyActiveTheme.php @@ -5,6 +5,7 @@ namespace Modules\Stylist\Http\Middleware; use Closure; use Illuminate\Http\Request; use Illuminate\Support\Facades\View; +use Modules\Stylist\Theme\Loader; use Modules\Stylist\Theme\Stylist; use Throwable; @@ -12,20 +13,38 @@ final class ApplyActiveTheme { public function handle(Request $request, Closure $next) { - $context = $request->is('admin', 'admin/*', 'stylist', 'stylist/*') ? 'backend' : 'frontend'; + $context = $request->is('admin', 'admin/*', 'stylist', 'stylist/*') + || $request->routeIs('admin.*', 'stylist.*') ? 'backend' : 'frontend'; + try { + $loader = app(Loader::class); $theme = app(Stylist::class)->current($context); - if ($theme->type() === $context) { + if ($theme->type() !== $context) { + $theme = $loader->find($context === 'backend' ? 'sonardesk-admin' : 'sonardesk'); + } + + $paths = []; + $seen = []; + while ($theme && ! isset($seen[$theme->key()])) { + $seen[$theme->key()] = true; $path = $theme->path().'/views'; if (is_dir($path)) { - // Prepend so ordinary view('admin.index') and @extends('layouts.app') - // resolve theme files first, then original application files. - View::getFinder()->prependLocation($path); + $paths[] = $path; } + $theme = $theme->parent() ? $loader->find($theme->parent()) : null; + } + $baseline = $loader->find($context === 'backend' ? 'sonardesk-admin' : 'sonardesk'); + $paths[] = $baseline->path().'/views'; + + // Finder uses first match. Prepend in reverse order to preserve precedence. + foreach (array_reverse(array_unique($paths)) as $path) { + View::getFinder()->prependLocation($path); } } catch (Throwable $exception) { - report($exception); // Original application views remain available. + report($exception); + // Original Laravel views remain available for recovery. } + return $next($request); } } diff --git a/modules/Stylist/SONARDESK-INTEGRATION.md b/modules/Stylist/SONARDESK-INTEGRATION.md index c38d067..96c54cb 100644 --- a/modules/Stylist/SONARDESK-INTEGRATION.md +++ b/modules/Stylist/SONARDESK-INTEGRATION.md @@ -1,15 +1,19 @@ -# SonarDesk Stylist integration — 1.0.0 +# SonarDesk — Stylist theme integration -- `resources/themes/sonardesk/views` contains the original frontend Blade templates. -- `resources/themes/sonardesk-admin/views` contains the original backend Blade templates. -- Both are fully editable independent view trees, with application view fallback. -- Active theme is selected by existing Stylist SettingsRepository (`frontend_theme` / `backend_theme`). -- The `ApplyActiveTheme` web middleware prepends the active theme view directory for each request. -- Public pages and authenticated customer pages use frontend; `/admin` and `/stylist` use backend. -- Controllers, routes, licensing, payments, APIs and mail templates remain unchanged. -- Theme management now requires AdminOnly permissions. -- Existing CSS/JS assets remain in place. Replacement themes may provide new layout asset links. -- Run `php artisan optimize:clear` and `php artisan migrate` after deployment. -- Check `/stylist` as an administrator, then verify public, account, admin, and checkout pages. -- If database already contains old `frontend_theme`/`backend_theme` values, activate the new bundled themes from Stylist. -- Theme ZIP installation security and the completeness of third-party theme templates require separate review before production use. +Stylist is the primary resolver for ordinary frontend and backend Laravel views. The active theme, its parents, the bundled baseline and finally `resources/views` are searched in that order. + +- Frontend: `resources/themes/sonardesk/views`. +- Backend: `resources/themes/sonardesk-admin/views`. +- The bundled themes preserve existing markup, CSS, JavaScript, routes and business logic. +- `/admin` and `/stylist` use backend; other web pages (including authenticated customer pages) use frontend. +- AdminOnly middleware protects Stylist operations. +- Bundled and active themes cannot be uninstalled. +- Keep `resources/views`: email templates, fallback and compatibility views are required. +- New theme ZIPs must include all needed templates, layouts, assets and a valid theme.json. Missing views fall back to baseline. +- The middleware does not change asset URLs. Replacement themes must supply their own asset references and use the existing Stylist asset serving facilities. + +## Local validation + +Run `php artisan optimize:clear`, `php artisan migrate`, `php artisan stylist:doctor` (if registered) and `php artisan view:cache` using your configured PHP binary. Test public pages, authentication, checkout, admin, Stylist, and transactional email before deployment. + +This package has not been executed against your ServBay environment. Do not deploy to production without smoke testing. diff --git a/modules/Stylist/Services/TenantThemeResolver.php b/modules/Stylist/Services/TenantThemeResolver.php index ddd4ce2..8a84592 100644 --- a/modules/Stylist/Services/TenantThemeResolver.php +++ b/modules/Stylist/Services/TenantThemeResolver.php @@ -43,10 +43,10 @@ final class TenantThemeResolver $request = request(); - return $request->routeIs('home', 'blog.public.*', 'news.public.*', 'pages.public.*') - || ! $request->user() - ? 'frontend' - : 'backend'; + return $request->is('admin', 'admin/*', 'stylist', 'stylist/*') + || $request->routeIs('admin.*', 'stylist.*') + ? 'backend' + : 'frontend'; } public function variables(): array diff --git a/modules/Stylist/Theme/Stylist.php b/modules/Stylist/Theme/Stylist.php index 1481a22..c092a09 100644 --- a/modules/Stylist/Theme/Stylist.php +++ b/modules/Stylist/Theme/Stylist.php @@ -14,7 +14,11 @@ final class Stylist { $requested = $this->resolver->resolve($context); try { - return $this->loader->find($requested); + $theme = $this->loader->find($requested); + if ($context !== null && $theme->type() !== $context) { + throw ThemeNotFoundException::forKey($requested); + } + return $theme; } catch (ThemeNotFoundException) { $fallback = $context === 'frontend' ? (string) config('stylist.default_frontend_theme', 'sonardesk')