diff --git a/UPGRADE.md b/UPGRADE.md index bcd263b48..39d574b1c 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -52,7 +52,7 @@ If you are relying on Quill.js specifics (like css classes), use `'type' => 'qui Previously `withVideo` was true by default, if you relied on this you have to update these media fields to `'withVideo' => true`. -#### SVG's are now no longer passing thorough glide +#### SVG's are now no longer passing through glide These are now rendered directly, you can change this by updating config `twill.glide.original_media_for_extensions` to an empty array `[]` diff --git a/config/twill.php b/config/twill.php index 2ac04da68..60eb30615 100644 --- a/config/twill.php +++ b/config/twill.php @@ -25,7 +25,7 @@ /* |-------------------------------------------------------------------------- - | Application strict url handeling + | Application strict url handling |-------------------------------------------------------------------------- | | Setting this value to true will enable strict domain handling. diff --git a/docs/content/1_docs/2_getting-started/3_configuration.md b/docs/content/1_docs/2_getting-started/3_configuration.md index 5bfc125fd..33230f7cf 100644 --- a/docs/content/1_docs/2_getting-started/3_configuration.md +++ b/docs/content/1_docs/2_getting-started/3_configuration.md @@ -79,31 +79,6 @@ return [ ]; ``` -Twill registers its own exception handler in all controllers. If you need to customize it (to report errors on a 3rd party service like Sentry or Rollbar for example), you can opt-out from it in your `config/twill.php` file: - -```php - false, -]; -``` - -And then extend it from your own `app/Exceptions/Handler.php` class: - -```php -getTwillErrorView($e->getStatusCode(), !$isSubdomainAdmin && !$isSubdirectoryAdmin); - } - - /** - * Get the Twill error view used to render a specified HTTP status code. - * - * @param integer $statusCode - * @return string - */ - protected function getTwillErrorView($statusCode, $frontend = false) - { - if ($frontend) { - $view = config('twill.frontend.views_path') . ".errors.$statusCode"; - - return view()->exists($view) ? $view : "errors::{$statusCode}"; - } - - $view = "twill.errors.$statusCode"; - - return view()->exists($view) ? $view : "twill::errors.$statusCode"; - } - - protected function invalidJson($request, ValidationException $exception) - { - return response()->json($exception->errors(), $exception->status); + trigger_deprecation('area17/twill', '3.4', 'The Twill Exception handler is deprecated and will be removed in v4, go back to extending the laravel ExceptionHandler'); } } diff --git a/src/Helpers/BlockRenderer.php b/src/Helpers/BlockRenderer.php index 7527043b6..656aeafa4 100644 --- a/src/Helpers/BlockRenderer.php +++ b/src/Helpers/BlockRenderer.php @@ -12,6 +12,7 @@ use Exception; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Str; /** @@ -119,6 +120,16 @@ private static function getNestedBlocksForData( $block->setRelation('children', self::getChildren($children)); + foreach ($data['browsers'] as $browserName => $browserItems) { + $browserData = collect(); + foreach ($browserItems as $browserItem) { + $className = Relation::getMorphedModel($browserItem['endpointType']) ?? $browserItem['endpointType']; + $browserData->push((new $className())->find($browserItem['id'])); + } + + $block->setRelatedCache($browserName, $browserData); + } + $block->medias = self::getMedias($data); $class->setRenderData( diff --git a/src/Http/Controllers/Admin/Controller.php b/src/Http/Controllers/Admin/Controller.php index 894477077..247afe3ab 100644 --- a/src/Http/Controllers/Admin/Controller.php +++ b/src/Http/Controllers/Admin/Controller.php @@ -20,9 +20,6 @@ class Controller extends BaseController public function __construct() { - if (Config::get('twill.bind_exception_handler', true)) { - App::singleton(ExceptionHandler::class, TwillHandler::class); - } } /** diff --git a/src/Http/Controllers/Admin/DashboardController.php b/src/Http/Controllers/Admin/DashboardController.php index 2aab4b773..958c68cc5 100644 --- a/src/Http/Controllers/Admin/DashboardController.php +++ b/src/Http/Controllers/Admin/DashboardController.php @@ -8,6 +8,7 @@ use Illuminate\Contracts\Auth\Factory as AuthFactory; use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\View\View; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -120,7 +121,11 @@ public function search(Request $request): Collection })->map(function ($module) use ($request) { $repository = $this->getRepository($module['name'], $module['repository'] ?? null); - $found = $repository->cmsSearch($request->get('search'), $module['search_fields'] ?? ['title'])->take(10); + $found = $repository->cmsSearch( + $request->get('search'), + $module['search_fields'] ?? ['title'], + isset($module['parentRelationship']) ? fn($q) => $q->whereHas($module['parentRelationship']) : null + )->take(10); return $found->map(function ($item) use ($module) { try { @@ -136,8 +141,11 @@ public function search(Request $request): Collection $date = $item->created_at->toIso8601String(); } - $parentRelationship = $module['parentRelationship'] ?? null; - $parent = $item->$parentRelationship; + if (isset($module['parentRelationship'])) { + /** @var BelongsTo $parent */ + $parent = call_user_func([$item, $module['parentRelationship']]); + $parent_id = $parent->getParentKey(); + } return [ 'id' => $item->id, @@ -145,7 +153,7 @@ public function search(Request $request): Collection $module['name'], $module['routePrefix'] ?? null, 'edit', - array_merge($parentRelationship ? [$parent->id] : [], [$item->id]) + array_filter([$parent_id ?? null, $item->id]) ), 'thumbnail' => method_exists($item, 'defaultCmsImage') ? $item->defaultCmsImage(['w' => 100, 'h' => 100]) : null, 'published' => $item->published, @@ -251,8 +259,16 @@ private function formatActivity(Activity $activity): ?array return null; } - $parentRelationship = $dashboardModule['parentRelationship'] ?? null; - $parent = $activity->subject->$parentRelationship; + if (isset($dashboardModule['parentRelationship'])) { + /** @var BelongsTo $parent */ + $parent = call_user_func([$activity->subject, $dashboardModule['parentRelationship']]); + $parent_id = $parent->getParentKey(); + + if (empty($parent_id)) { + // Prevent module route error + return null; + } + } // @todo: Improve readability of what is happening here. return [ @@ -269,7 +285,7 @@ private function formatActivity(Activity $activity): ?array $dashboardModule['name'], $dashboardModule['routePrefix'] ?? null, 'edit', - array_merge($parentRelationship ? [$parent->id] : [], [$activity->subject_id]) + array_filter([$parent_id ?? null, $activity->subject_id]) ), ] : []) + (! is_null($activity->subject->published) ? [ 'published' => $activity->description === 'published' ? true : ($activity->description === 'unpublished' ? false : $activity->subject->published), @@ -554,10 +570,15 @@ private function getDrafts(Collection $modules): Collection if ($repository->hasBehavior('revisions')) { $query->mine(); } - $parentRelationship = $module['parentRelationship'] ?? null; - return $query->get()->map(function ($draft) use ($module, $parentRelationship) { - $parent = $draft->$parentRelationship; + if (isset($module['parentRelationship'])) { + $query->whereHas($module['parentRelationship']); + } + + return $query->get()->map(function ($draft) use ($module) { + if (isset($module['parentRelationship'])) { + $parent_id = call_user_func([$draft, $module['parentRelationship']])->getParentKey(); + } return [ 'type' => ucfirst($module['label_singular'] ?? Str::singular($module['name'])), @@ -566,7 +587,7 @@ private function getDrafts(Collection $modules): Collection $module['name'], $module['routePrefix'] ?? null, 'edit', - array_merge($parentRelationship ? [$parent->id] : [], [$draft->id]) + array_filter([$parent_id ?? null, $draft->id]) ) ]; }); diff --git a/src/Http/Controllers/Admin/ModuleController.php b/src/Http/Controllers/Admin/ModuleController.php index a89e186d9..3344ca50c 100644 --- a/src/Http/Controllers/Admin/ModuleController.php +++ b/src/Http/Controllers/Admin/ModuleController.php @@ -1200,6 +1200,7 @@ public function edit(TwillModelContract|int $id): mixed $this->setBackLink(); $controllerForm = $this->getForm($item); + $controllerForm->registerDynamicRepeaters(); if ($controllerForm->hasForm()) { $view = 'twill::layouts.form'; @@ -1221,8 +1222,11 @@ public function edit(TwillModelContract|int $id): mixed } } - return View::make($view, $this->form($id))->with( - ['formBuilder' => $controllerForm->toFrontend($this->getSideFieldsets($item))] + $sideFieldsets = $this->getSideFieldsets($item); + $sideFieldsets->registerDynamicRepeaters(); + + return View::make($view, $this->form($id, $item))->with( + ['formBuilder' => $controllerForm->toFrontend($sideFieldsets)] ); } @@ -1255,8 +1259,6 @@ public function create(int $parentModuleId = null): JsonResponse|RedirectRespons return View::exists($view); }); - View::share('form', $this->form(null)); - return View::make($view, $this->form(null))->with( ['formBuilder' => $controllerForm->toFrontend($this->getSideFieldsets($emptyModelInstance), true)] ); @@ -1381,7 +1383,7 @@ public function preview(int $id): IlluminateView /** * @param int $id - * @return \Illuminate\View\View + * @return \Illuminate\Contracts\View\View */ public function restoreRevision($id) { @@ -1417,8 +1419,6 @@ public function restoreRevision($id) ); } - View::share('form', $this->form($id, $item)); - return View::make($view, $this->form($id, $item))->with( ['formBuilder' => $controllerForm->toFrontend($this->getSideFieldsets($item))] ); diff --git a/src/Http/Controllers/Front/Controller.php b/src/Http/Controllers/Front/Controller.php index 9d72359b2..3848488c3 100644 --- a/src/Http/Controllers/Front/Controller.php +++ b/src/Http/Controllers/Front/Controller.php @@ -19,10 +19,6 @@ class Controller extends BaseController public function __construct() { - if (Config::get('twill.bind_exception_handler', true)) { - App::singleton(ExceptionHandler::class, TwillHandler::class); - } - $this->seo = new Seo(); $this->seo->title = Config::get('twill.seo.site_title'); diff --git a/src/Models/Behaviors/HasRelated.php b/src/Models/Behaviors/HasRelated.php index 29e6c1f7d..4a728ea0e 100644 --- a/src/Models/Behaviors/HasRelated.php +++ b/src/Models/Behaviors/HasRelated.php @@ -2,10 +2,9 @@ namespace A17\Twill\Models\Behaviors; -use A17\Twill\Models\Contracts\TwillModelContract; use A17\Twill\Models\RelatedItem; +use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphMany; -use Illuminate\Support\Arr; use Illuminate\Support\Collection; trait HasRelated @@ -52,6 +51,7 @@ public function loadRelated(string $browserName): Collection /** @var \A17\Twill\Models\Model $model */ if ($model = $item->related) { $model->setRelation('pivot', $item); + $item->unsetRelation('related'); return $model; } @@ -63,18 +63,26 @@ public function loadRelated(string $browserName): Collection /** * Attach items to the model for a browser field. * - * @param array $items + * @param array $items */ - public function saveRelated(array|Collection $items, string $browserName): void + public function saveRelated(array|Collection|Model $items, string $browserName): void { + $items = is_array($items) || $items instanceof Collection ? $items : [$items]; + /** @var Collection $itemsToProcess */ $itemsToProcess = $this->relatedItems()->where('browser_name', $browserName)->get(); foreach ($items as $position => $item) { + if ($item instanceof Model) { + $id = $item->getKey(); + $type = $item->getMorphClass(); + } else { + $id = $item['id']; + $type = $item['endpointType']; + } + $firstMatchKey = $itemsToProcess - ->where('related_id', $item['id']) - ->where('related_type', $item['endpointType']) - ->where('browser_name', $browserName) + ->where(fn (RelatedItem $item) => $item->related_id == $id && $item->related_type === $type) // We should only have one item always as you cannot select the same items twice. ->keys() ->first(); @@ -90,8 +98,8 @@ public function saveRelated(array|Collection $items, string $browserName): void RelatedItem::create([ 'subject_id' => $this->getKey(), 'subject_type' => $this->getMorphClass(), - 'related_id' => $item['id'], - 'related_type' => $item['endpointType'], + 'related_id' => $id, + 'related_type' => $type, 'browser_name' => $browserName, 'position' => $position + 1, ]); @@ -117,4 +125,9 @@ public function clearAllRelated(): void { $this->relatedItems()->delete(); } + + public function setRelatedCache($browser, $items): void + { + $this->relatedCache[$browser] = $items; + } } diff --git a/src/Models/Behaviors/HasSlug.php b/src/Models/Behaviors/HasSlug.php index 9160e66b5..64cb7e860 100644 --- a/src/Models/Behaviors/HasSlug.php +++ b/src/Models/Behaviors/HasSlug.php @@ -6,6 +6,7 @@ use A17\Twill\Models\Model; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Arr; use Illuminate\Support\Str; trait HasSlug @@ -13,14 +14,24 @@ trait HasSlug private int $nb_variation_slug = 3; public array $twillSlugData = []; + private bool $twill_restoring = false; + protected static function bootHasSlug(): void { - static::restoring(function ($model) { + static::restoring(function (self $model) { $model->restoreSlugs(); + $model->twill_restoring = true; }); - static::saved(function ($model) { - $model->handleSlugsOnSave(); + static::saved(function (self $model) { + if (!$model->twill_restoring) { + $model->handleSlugsOnSave(); + } + $model->twill_restoring = false; + }); + + static::deleting(function (self $model) { + $model->slugs()->delete(); }); } @@ -90,15 +101,19 @@ public function setSlugs(): void public function restoreSlugs(): void { - $activeSlugs = $this->slugs()->withTrashed()->where('active', true)->get(); + $activeSlugs = $this->slugs()->withTrashed()->get(); - $activeSlugs->each(function ($slug) { - $slug->slug = $this->suffixSlugIfExisting(['locale' => $slug->locale, 'slug' => $slug->slug]); + $hasActive = false; + $activeSlugs->each(function ($slug) use (&$hasActive) { + if ($slug->active) { + $hasActive = true; + $slug->slug = $this->suffixSlugIfExisting(['locale' => $slug->locale, 'slug' => $slug->slug]); + } $slug->deleted_at = null; $slug->save(); }); - if ($activeSlugs->isEmpty()) { + if (!$hasActive) { $this->setSlugs(); } } @@ -109,13 +124,7 @@ public function restoreSlugs(): void */ public function handleSlugsOnSave(): void { - if ($this->twillSlugData === []) { - return; - } - - foreach (getLocales() as $locale) { - $this->disableLocaleSlugs($locale); - } + $this->disableLocaleSlugs(); $slugParams = $this->twillSlugData !== [] ? $this->twillSlugData : $this->getSlugParams(); @@ -126,6 +135,9 @@ public function handleSlugsOnSave(): void $params['slug'] = Str::slug($params['slug']); } + if (empty($params['slug'])) { + continue; + } if ($this->slugs()->where('locale', $params['locale'])->where('slug', $params['slug'])->where('active', true)->doesntExist()) { $this->updateOrNewSlug($params); } @@ -208,12 +220,14 @@ protected function addOneSlug(array $slugParams): void $this->disableLocaleSlugs($slugParams['locale'], $slugModel->getKey()); } - public function disableLocaleSlugs(string $locale, int $except_slug_id = 0): void + public function disableLocaleSlugs(string|array $locale = null, int $except_slug_id = 0): void { - $this->getSlugModelClass()::where($this->getForeignKey(), $this->id) - ->where('id', '<>', $except_slug_id) - ->where('locale', $locale) - ->update(['active' => 0]); + $query = $this->getSlugModelClass()::where($this->getForeignKey(), $this->id) + ->where('id', '<>', $except_slug_id); + if ($locale !== null) { + $query->whereIn('locale', Arr::wrap($locale)); + } + $query->update(['active' => 0]); } private function suffixSlugIfExisting(array $slugParams): string @@ -345,7 +359,7 @@ public function getSlugParams(?string $locale = null): ?array } $slugParam = [ - 'active' => $translation->active, + 'active' => $translation->active ?? true, 'slug' => $translation->$slugAttribute ?? $this->$slugAttribute, 'locale' => $translation->locale, ] + $slugDependenciesAttributes; diff --git a/src/Models/Media.php b/src/Models/Media.php index 2286d0522..f82503438 100755 --- a/src/Models/Media.php +++ b/src/Models/Media.php @@ -55,12 +55,12 @@ public function altTextFrom($filename) return Str::ucfirst(preg_replace('/[-_]/', ' ', $filename)); } - public function canDeleteSafely() + public function canDeleteSafely(): bool { - return DB::table(config('twill.mediables_table', 'twill_mediables'))->where('media_id', $this->id)->count() === 0; + return !$this->isReferenced(); } - public function isReferenced() + public function isReferenced(): bool { return DB::table(config('twill.mediables_table', 'twill_mediables'))->where('media_id', $this->id)->count() > 0; } diff --git a/src/Repositories/Behaviors/HandleSlugs.php b/src/Repositories/Behaviors/HandleSlugs.php index 65df32728..6cec464bb 100644 --- a/src/Repositories/Behaviors/HandleSlugs.php +++ b/src/Repositories/Behaviors/HandleSlugs.php @@ -47,16 +47,6 @@ public function beforeSaveHandleSlugs(TwillModelContract $object, array $fields) } } - public function afterDeleteHandleSlugs(TwillModelContract $object): void - { - $object->slugs()->delete(); - } - - public function afterRestoreHandleSlugs(TwillModelContract $object): void - { - $object->slugs()->restore(); - } - public function getFormFieldsHandleSlugs(TwillModelContract $model, array $fields): array { unset($fields['slugs']); diff --git a/src/Repositories/ModuleRepository.php b/src/Repositories/ModuleRepository.php index 5ade539a3..5c8a1b375 100644 --- a/src/Repositories/ModuleRepository.php +++ b/src/Repositories/ModuleRepository.php @@ -15,6 +15,7 @@ use A17\Twill\Repositories\Behaviors\HandlePermissions; use A17\Twill\Repositories\Behaviors\HandleRelatedBrowsers; use A17\Twill\Repositories\Behaviors\HandleRepeaters; +use A17\Twill\Services\Listings\Filters\FreeTextSearch; use Exception; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Database\Eloquent\Builder; @@ -152,19 +153,16 @@ public function listAll( return $query->get()->pluck($column, $pluckBy); } - public function cmsSearch(string $search, array $fields = []): Collection + public function cmsSearch(string $search, array $fields = [], callable $query = null): Collection { - $builder = $this->model->latest(); + $searchFilter = new FreeTextSearch(); + $searchFilter->queryString($search); + $searchFilter->searchColumns($fields); + $searchFilter->searchQuery($query); - $translatedAttributes = $this->model->getTranslatedAttributes() ?? []; + $builder = $this->model->latest(); - foreach ($fields as $field) { - if (in_array($field, $translatedAttributes, true)) { - $builder->orWhereTranslationLike($field, "%$search%"); - } else { - $builder->orWhere($field, getLikeOperator(), "%$search%"); - } - } + $searchFilter->applyFilter($builder); return $builder->get(); } diff --git a/src/TwillRoutes.php b/src/TwillRoutes.php index 5881d146f..9c9624f9b 100644 --- a/src/TwillRoutes.php +++ b/src/TwillRoutes.php @@ -9,6 +9,8 @@ use Illuminate\Support\Facades\Route; use Illuminate\Support\Str; +use function request; + class TwillRoutes { /** @@ -402,4 +404,18 @@ public function getAuthRedirectPath(): string return config('twill.auth_login_redirect_path') ?? rtrim(config('twill.admin_app_url') ?: '', '/') . '/' . ltrim(config('twill.admin_app_path') ?? 'admin', '/'); } + + public function isTwillRequest(): bool + { + $adminAppUrl = str_replace(['http://', 'https://'], '', config('twill.admin_app_url', config('app.url'))); + $requestHost = request()->getHttpHost(); + + $matchesDomain = config('twill.support_subdomain_admin_routing') ? + Str::startsWith($requestHost, config('twill.admin_app_subdomain', 'admin') . '.') && Str::endsWith($requestHost, '.' . $adminAppUrl) + : !config('twill.admin_app_strict') || $requestHost === $adminAppUrl; + + $matchesPath = empty(config('twill.admin_app_path')) || request()->is(config('twill.admin_app_path', '') . '*'); + + return $matchesDomain && $matchesPath; + } } diff --git a/src/TwillServiceProvider.php b/src/TwillServiceProvider.php index 7fec36a82..4e2dbe55d 100644 --- a/src/TwillServiceProvider.php +++ b/src/TwillServiceProvider.php @@ -28,6 +28,7 @@ use A17\Twill\Commands\Update; use A17\Twill\Commands\UpdateExampleCommand; use A17\Twill\Commands\UpdateMorphMapReferences; +use A17\Twill\Facades\TwillRoutes; use A17\Twill\Http\ViewComposers\CurrentUser; use A17\Twill\Http\ViewComposers\FilesUploaderConfig; use A17\Twill\Http\ViewComposers\Localization; @@ -42,14 +43,20 @@ use Astrotomic\Translatable\TranslatableServiceProvider; use Cartalyst\Tags\TagsServiceProvider; use Exception; +use Illuminate\Contracts\Debug\ExceptionHandler; +use Illuminate\Contracts\Foundation\CachesConfiguration; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Foundation\AliasLoader; +use Illuminate\Foundation\Exceptions\Handler; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\View; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Str; +use Illuminate\Support\ViewErrorBag; +use Illuminate\Validation\ValidationException; use PragmaRX\Google2FAQRCode\Google2FA as Google2FAQRCode; use Spatie\Activitylog\ActivitylogServiceProvider; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; class TwillServiceProvider extends ServiceProvider { @@ -79,8 +86,6 @@ class TwillServiceProvider extends ServiceProvider */ public function boot(): void { - $this->requireHelpers(); - $this->publishConfigs(); $this->publishMigrations(); $this->publishAssets(); @@ -116,8 +121,13 @@ private function requireHelpers(): void */ public function register(): void { - $this->mergeConfigs(); + $this->requireHelpers(); + if (! ($this->app instanceof CachesConfiguration && $this->app->configurationIsCached())) { + $this->mergeConfigs(); + } + + $this->registerErrorHandlers(); $this->registerProviders(); $this->registerAliases(); $this->registerFacades(); @@ -150,8 +160,6 @@ public function register(): void 'blocks' => config('twill.models.block', Block::class), 'groups' => config('twill.models.group', Group::class), ]); - - config(['twill.version' => $this->version()]); } private function registerFacades(): void @@ -214,47 +222,6 @@ private function registerAliases(): void */ private function publishConfigs(): void { - if (config('twill.enabled.users-management')) { - config([ - 'auth.providers.twill_users' => [ - 'driver' => 'eloquent', - 'model' => twillModel('user'), - ], - ]); - - config([ - 'auth.guards.twill_users' => [ - 'driver' => 'session', - 'provider' => 'twill_users', - ], - ]); - - if (blank(config('auth.passwords.twill_users'))) { - config([ - 'auth.passwords.twill_users' => [ - 'provider' => 'twill_users', - 'table' => config('twill.password_resets_table', 'twill_password_resets'), - 'expire' => 60, - 'throttle' => 60, - ], - ]); - } - } - - config( - ['activitylog.enabled' => config('twill.enabled.dashboard') ? true : config('twill.enabled.activitylog')] - ); - config(['activitylog.subject_returns_soft_deleted_models' => true]); - - config( - [ - 'analytics.service_account_credentials_json' => config( - 'twill.dashboard.analytics.service_account_credentials_json', - storage_path('app/analytics/service-account-credentials.json') - ), - ] - ); - $this->publishes([__DIR__ . '/../config/twill-publish.php' => config_path('twill.php')], 'config'); $this->publishes([__DIR__ . '/../config/translatable.php' => config_path('translatable.php')], 'config'); } @@ -299,6 +266,49 @@ private function mergeConfigs(): void } $this->mergeConfigFrom(__DIR__ . '/../config/services.php', 'services'); + + if (config('twill.enabled.users-management')) { + config([ + 'auth.providers.twill_users' => [ + 'driver' => 'eloquent', + 'model' => twillModel('user'), + ], + ]); + + config([ + 'auth.guards.twill_users' => [ + 'driver' => 'session', + 'provider' => 'twill_users', + ], + ]); + + if (blank(config('auth.passwords.twill_users'))) { + config([ + 'auth.passwords.twill_users' => [ + 'provider' => 'twill_users', + 'table' => config('twill.password_resets_table', 'twill_password_resets'), + 'expire' => 60, + 'throttle' => 60, + ], + ]); + } + } + + config( + ['activitylog.enabled' => config('twill.enabled.dashboard') ? true : config('twill.enabled.activitylog')] + ); + config(['activitylog.subject_returns_soft_deleted_models' => true]); + + config( + [ + 'analytics.service_account_credentials_json' => config( + 'twill.dashboard.analytics.service_account_credentials_json', + storage_path('app/analytics/service-account-credentials.json') + ), + ] + ); + + config(['twill.version' => $this->version()]); } private function setLocalDiskUrl($type): void @@ -625,4 +635,34 @@ public function check2FA(): void ); } } + + private function registerErrorHandlers(): void + { + $handler = app(ExceptionHandler::class); + if ($handler instanceof Handler) { + $handler->renderable(function (HttpExceptionInterface $e) { + $statusCode = $e->getStatusCode(); + if (TwillRoutes::isTwillRequest()) { + $view = "twill.errors.$statusCode"; + + $view = view()->exists($view) ? $view : "twill::errors.$statusCode"; + } else { + $view = config('twill.frontend.views_path') . ".errors.$statusCode"; + + $view = view()->exists($view) ? $view : null; + } + return $view ? response()->view($view, [ + 'errors' => new ViewErrorBag(), + 'exception' => $e, + ], $e->getStatusCode(), $e->getHeaders()) : null; + }); + + $handler->renderable(function (ValidationException $exception) { + if (TwillRoutes::isTwillRequest() && request()->expectsJson()) { + return response()->json($exception->errors(), $exception->status); + } + return null; + }); + } + } } diff --git a/tests/integration/Controllers/Tables/BrowserColumnTest.php b/tests/integration/Controllers/Tables/BrowserColumnTest.php index 9046e5b20..f2b95c39b 100644 --- a/tests/integration/Controllers/Tables/BrowserColumnTest.php +++ b/tests/integration/Controllers/Tables/BrowserColumnTest.php @@ -27,12 +27,7 @@ public function testWithSingleValue(): void $category = $this->createCategory(); $this->author->saveRelated( - [ - [ - 'id' => $category->id, - 'endpointType' => Category::class, - ], - ], + $category, 'categories' ); diff --git a/tests/integration/Exceptions/ExceptionHandlerTest.php b/tests/integration/Exceptions/ExceptionHandlerTest.php new file mode 100644 index 000000000..3b4c5d2d6 --- /dev/null +++ b/tests/integration/Exceptions/ExceptionHandlerTest.php @@ -0,0 +1,39 @@ +get('/twill/foobar'); + $res->assertStatus(404); + $this->assertEquals($res->original->name(), "twill::errors.404"); + } + + public function test404InFrontend() + { + $res = $this->get('/foobar'); + $res->assertStatus(404); + $this->assertTrue(is_string($res->original)); + } + + public function testValidationException() + { + Route::post('/twill/validation-exception', function (Request $request) { + $request->validate([ + 'dummy' => 'required', + ]); + }); + + $res = $this->postJson('/twill/validation-exception'); + $res->assertStatus(422); + // Response is directly an array of exceptions and doesn't have a response key + $res->assertJsonValidationErrors('dummy', null); + } +} diff --git a/tests/integration/NplusOneRelatedTest.php b/tests/integration/NplusOneRelatedTest.php index a96fb05c7..c39691705 100644 --- a/tests/integration/NplusOneRelatedTest.php +++ b/tests/integration/NplusOneRelatedTest.php @@ -35,7 +35,7 @@ public function testSingleRelatedItem(): void 'published' => true, ]); - $storeArray[] = ['endpointType' => Writer::class, 'id' => $writer->id]; + $storeArray[] = $writer; } $letter->saveRelated($storeArray, 'dummyBrowser'); diff --git a/tests/integration/SlugTest.php b/tests/integration/SlugTest.php index 4675e33a5..e279d58d8 100644 --- a/tests/integration/SlugTest.php +++ b/tests/integration/SlugTest.php @@ -46,6 +46,27 @@ public function testBasicSlugModelDuplicate(): void } } + public function testSlugLooping(): void + { + $model = $this->module->getRepository()->create([ + 'title' => 'My title', + 'slug' => ['en' => 'my-title'], + ]); + + $this->assertEquals('my-title', $model->getSlug()); + + $this->module->getRepository()->update($model->id, ['title' => 'My title 2']); + + $this->assertEquals('my-title-2', $model->fresh()->getSlug()); + + $this->assertCount(2, $model->slugs()->get()); + + $this->module->getRepository()->update($model->id, ['title' => 'My title']); + + $this->assertEquals('my-title', $model->fresh()->getSlug()); + $this->assertCount(2, $model->slugs()->get()); + } + public function testCanReuseSoftDeletedSlug(): void { $model = $this->module->getRepository()->create([