-
-
- {{ fieldset.title }}
-
-
- {{ value }}
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/resources/js/components/publish/Tabs.vue b/resources/js/components/publish/Tabs.vue
index d509eb3a4d..b6d01146e4 100644
--- a/resources/js/components/publish/Tabs.vue
+++ b/resources/js/components/publish/Tabs.vue
@@ -116,11 +116,14 @@
{{-- Deferred to allow Vite modules to load first --}}
diff --git a/resources/views/utilities/search.blade.php b/resources/views/utilities/search.blade.php
index ca29618c90..1c209804c7 100644
--- a/resources/views/utilities/search.blade.php
+++ b/resources/views/utilities/search.blade.php
@@ -14,7 +14,7 @@
@@ -71,7 +71,7 @@
|
diff --git a/resources/views/widgets/collection.blade.php b/resources/views/widgets/collection.blade.php
index 1518b01395..59bc7be0e0 100644
--- a/resources/views/widgets/collection.blade.php
+++ b/resources/views/widgets/collection.blade.php
@@ -5,7 +5,7 @@
@cp_svg('icons/light/content-writing')
-
{{ __($title) }}
+
{{ __($title) }}
@can('create', ['Statamic\Contracts\Entries\Entry', $collection])
diff --git a/routes/routes.php b/routes/routes.php
index 6d06db3ba6..df08b4edc4 100644
--- a/routes/routes.php
+++ b/routes/routes.php
@@ -3,13 +3,11 @@
use Illuminate\Support\Facades\Route;
use Statamic\API\Middleware\Cache;
use Statamic\Facades\Glide;
-use Statamic\Http\Middleware\API\SwapExceptionHandler as SwapAPIExceptionHandler;
use Statamic\Http\Middleware\CP\SwapExceptionHandler as SwapCpExceptionHandler;
use Statamic\Http\Middleware\RequireStatamicPro;
if (config('statamic.api.enabled')) {
Route::middleware([
- SwapApiExceptionHandler::class,
RequireStatamicPro::class,
Cache::class,
])->group(function () {
diff --git a/src/Actions/DuplicateEntry.php b/src/Actions/DuplicateEntry.php
index c7e1a35f47..b214b72b3f 100644
--- a/src/Actions/DuplicateEntry.php
+++ b/src/Actions/DuplicateEntry.php
@@ -55,7 +55,7 @@ public function run($items, $values)
->each(fn ($original) => $this->duplicateEntry($original));
}
- private function duplicateEntry(Entry $original, string $origin = null)
+ private function duplicateEntry(Entry $original, ?string $origin = null)
{
$originalParent = $this->getEntryParentFromStructure($original);
[$title, $slug] = $this->generateTitleAndSlug($original);
diff --git a/src/Assets/Asset.php b/src/Assets/Asset.php
index dc5fe026ef..79c74ef161 100644
--- a/src/Assets/Asset.php
+++ b/src/Assets/Asset.php
@@ -19,7 +19,9 @@
use Statamic\Data\HasAugmentedInstance;
use Statamic\Data\TracksQueriedColumns;
use Statamic\Data\TracksQueriedRelations;
+use Statamic\Events\AssetContainerBlueprintFound;
use Statamic\Events\AssetDeleted;
+use Statamic\Events\AssetDeleting;
use Statamic\Events\AssetReplaced;
use Statamic\Events\AssetReuploaded;
use Statamic\Events\AssetSaved;
@@ -27,6 +29,7 @@
use Statamic\Exceptions\FileExtensionMismatch;
use Statamic\Facades;
use Statamic\Facades\AssetContainer as AssetContainerAPI;
+use Statamic\Facades\Blink;
use Statamic\Facades\Image;
use Statamic\Facades\Path;
use Statamic\Facades\URL;
@@ -222,6 +225,10 @@ public function meta($key = null)
return $this->metaValue($key);
}
+ if (! $this->exists()) {
+ return $this->generateMeta();
+ }
+
if (! config('statamic.assets.cache_meta')) {
return $this->generateMeta();
}
@@ -287,7 +294,7 @@ public function metaPath()
{
$path = dirname($this->path()).'/.meta/'.$this->basename().'.yaml';
- return ltrim($path, '/');
+ return (string) Str::of($path)->replaceFirst('./', '')->ltrim('/');
}
protected function metaExists()
@@ -616,6 +623,10 @@ public function save()
*/
public function delete()
{
+ if (AssetDeleting::dispatch($this) === false) {
+ return false;
+ }
+
$this->disk()->delete($this->path());
$this->disk()->delete($this->metaPath());
@@ -884,7 +895,7 @@ public function reupload(ReplacementFile $file)
*
* @return \Symfony\Component\HttpFoundation\StreamedResponse
*/
- public function download(string $name = null, array $headers = [])
+ public function download(?string $name = null, array $headers = [])
{
return $this->disk()->filesystem()->download($this->path(), $name, $headers);
}
@@ -917,7 +928,19 @@ public function contents()
*/
public function blueprint()
{
- return $this->container()->blueprint();
+ $key = "asset-{$this->id()}-blueprint";
+
+ if (Blink::has($key)) {
+ return Blink::get($key);
+ }
+
+ $blueprint = $this->container()->blueprint($this);
+
+ Blink::put($key, $blueprint);
+
+ AssetContainerBlueprintFound::dispatch($blueprint, $this->container(), $this);
+
+ return $blueprint;
}
/**
diff --git a/src/Assets/AssetContainer.php b/src/Assets/AssetContainer.php
index 7b03d1db8f..2851326526 100644
--- a/src/Assets/AssetContainer.php
+++ b/src/Assets/AssetContainer.php
@@ -173,27 +173,39 @@ public function apiUrl()
*
* @return \Statamic\Fields\Blueprint
*/
- public function blueprint()
+ public function blueprint($asset = null)
{
- if (Blink::has($blink = 'asset-container-blueprint-'.$this->handle())) {
- return Blink::get($blink);
- }
-
- $blueprint = Blueprint::find('assets/'.$this->handle()) ?? Blueprint::makeFromFields([
- 'alt' => [
- 'type' => 'text',
- 'display' => __('Alt Text'),
- 'instructions' => __('Description of the image'),
- ],
- ])->setHandle($this->handle())->setNamespace('assets');
+ $blueprint = $this->getBaseBlueprint();
- Blink::put($blink, $blueprint);
+ $blueprint->setParent($asset ?? $this);
- AssetContainerBlueprintFound::dispatch($blueprint, $this);
+ // Only dispatch the event when there's no asset.
+ // When there is an asset, the event is dispatched from the asset.
+ if (! $asset) {
+ Blink::once(
+ 'asset-container-assetcontainerblueprintfound-'.$this->handle(),
+ fn () => AssetContainerBlueprintFound::dispatch($blueprint, $this)
+ );
+ }
return $blueprint;
}
+ private function getBaseBlueprint()
+ {
+ $blink = 'asset-container-blueprint-'.$this->handle();
+
+ return Blink::once($blink, function () {
+ return Blueprint::find('assets/'.$this->handle()) ?? Blueprint::makeFromFields([
+ 'alt' => [
+ 'type' => 'text',
+ 'display' => __('Alt Text'),
+ 'instructions' => __('Description of the image'),
+ ],
+ ])->setHandle($this->handle())->setNamespace('assets');
+ });
+ }
+
public function afterSave($callback)
{
$this->afterSaveCallbacks[] = $callback;
diff --git a/src/Assets/AugmentedAsset.php b/src/Assets/AugmentedAsset.php
index 4060810e64..fb895df50b 100644
--- a/src/Assets/AugmentedAsset.php
+++ b/src/Assets/AugmentedAsset.php
@@ -65,7 +65,7 @@ public function keys()
]);
}
- return $keys->all();
+ return $keys->merge($this->blueprintFields()->keys())->unique()->all();
}
protected function isAsset()
diff --git a/src/Assets/FileUploader.php b/src/Assets/FileUploader.php
index ac7bc71370..e36d5e40aa 100644
--- a/src/Assets/FileUploader.php
+++ b/src/Assets/FileUploader.php
@@ -15,7 +15,7 @@ public function __construct($container)
$this->container = $container ? AssetContainer::find($container) : null;
}
- public static function container(string $container = null)
+ public static function container(?string $container = null)
{
return new static($container);
}
diff --git a/src/Auth/Eloquent/RoleRepository.php b/src/Auth/Eloquent/RoleRepository.php
index 02a1543f48..64045cec54 100644
--- a/src/Auth/Eloquent/RoleRepository.php
+++ b/src/Auth/Eloquent/RoleRepository.php
@@ -7,7 +7,7 @@
class RoleRepository extends BaseRepository
{
- public function make(string $handle = null): RoleContract
+ public function make(?string $handle = null): RoleContract
{
return (new Role)->handle($handle);
}
diff --git a/src/Auth/Eloquent/User.php b/src/Auth/Eloquent/User.php
index 605c0886eb..f63ef51c0c 100644
--- a/src/Auth/Eloquent/User.php
+++ b/src/Auth/Eloquent/User.php
@@ -20,7 +20,7 @@ class User extends BaseUser
protected $roles;
protected $groups;
- public function model(Model $model = null)
+ public function model(?Model $model = null)
{
if (is_null($model)) {
return $this->model;
diff --git a/src/Auth/File/Role.php b/src/Auth/File/Role.php
index 95b786b40b..5fba2e399c 100644
--- a/src/Auth/File/Role.php
+++ b/src/Auth/File/Role.php
@@ -24,7 +24,7 @@ public function __construct()
$this->permissions = collect();
}
- public function title(string $title = null)
+ public function title(?string $title = null)
{
if (func_num_args() === 0) {
return $this->title;
@@ -40,7 +40,7 @@ public function id(): string
return $this->handle();
}
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
if (func_num_args() === 0) {
return $this->handle;
@@ -68,6 +68,8 @@ public function permissions($permissions = null)
$this->permissions = collect($permissions);
+ app(PermissionCache::class)->clear();
+
return $this;
}
@@ -89,6 +91,8 @@ public function removePermission($permission)
->diff(Arr::wrap($permission))
->values();
+ app(PermissionCache::class)->clear();
+
return $this;
}
diff --git a/src/Auth/File/RoleRepository.php b/src/Auth/File/RoleRepository.php
index 4c5e8acac5..8aab8498f1 100644
--- a/src/Auth/File/RoleRepository.php
+++ b/src/Auth/File/RoleRepository.php
@@ -7,7 +7,7 @@
class RoleRepository extends BaseRepository
{
- public function make(string $handle = null): RoleContract
+ public function make(?string $handle = null): RoleContract
{
return (new Role)->handle($handle);
}
diff --git a/src/Auth/Permission.php b/src/Auth/Permission.php
index f1adc87c4d..b06731a9f6 100644
--- a/src/Auth/Permission.php
+++ b/src/Auth/Permission.php
@@ -18,7 +18,7 @@ class Permission
protected $description;
protected $group;
- public function value(string $value = null)
+ public function value(?string $value = null)
{
if (func_num_args() > 0) {
$this->value = $value;
@@ -39,7 +39,7 @@ public function originalLabel()
return $this->label;
}
- public function label(string $label = null)
+ public function label(?string $label = null)
{
if (func_num_args() > 0) {
$this->label = $label;
@@ -52,17 +52,17 @@ public function label(string $label = null)
return __($label, [$this->placeholder => $this->placeholderLabel]);
}
- public function placeholder(string $placeholder = null)
+ public function placeholder(?string $placeholder = null)
{
return $this->fluentlyGetOrSet('placeholder')->args(func_get_args());
}
- public function placeholderLabel(string $label = null)
+ public function placeholderLabel(?string $label = null)
{
return $this->fluentlyGetOrSet('placeholderLabel')->args(func_get_args());
}
- public function placeholderValue(string $placeholderValue = null)
+ public function placeholderValue(?string $placeholderValue = null)
{
return $this->fluentlyGetOrSet('placeholderValue')->args(func_get_args());
}
@@ -103,7 +103,7 @@ public function permissions()
})->values();
}
- public function children(array $children = null)
+ public function children(?array $children = null)
{
return $this
->fluentlyGetOrSet('children')
@@ -156,7 +156,7 @@ public function toTree()
})->all();
}
- public function group(string $group = null)
+ public function group(?string $group = null)
{
return $this->fluentlyGetOrSet('group')->args(func_get_args());
}
diff --git a/src/Auth/Protect/Protectors/Authenticated.php b/src/Auth/Protect/Protectors/Authenticated.php
index a260b02eb7..3852e30cfd 100644
--- a/src/Auth/Protect/Protectors/Authenticated.php
+++ b/src/Auth/Protect/Protectors/Authenticated.php
@@ -2,6 +2,8 @@
namespace Statamic\Auth\Protect\Protectors;
+use Statamic\Exceptions\ForbiddenHttpException;
+
class Authenticated extends Protector
{
public function protect()
@@ -15,7 +17,7 @@ public function protect()
}
if (! $this->getLoginUrl()) {
- abort(403);
+ throw new ForbiddenHttpException();
}
abort(redirect($this->getLoginUrl()));
diff --git a/src/Auth/Protect/Protectors/Fallback.php b/src/Auth/Protect/Protectors/Fallback.php
index fc4caf31fb..0ea9394b1f 100644
--- a/src/Auth/Protect/Protectors/Fallback.php
+++ b/src/Auth/Protect/Protectors/Fallback.php
@@ -2,10 +2,12 @@
namespace Statamic\Auth\Protect\Protectors;
+use Statamic\Exceptions\ForbiddenHttpException;
+
class Fallback extends Protector
{
public function protect()
{
- abort(403);
+ throw new ForbiddenHttpException();
}
}
diff --git a/src/Auth/Protect/Protectors/IpAddress.php b/src/Auth/Protect/Protectors/IpAddress.php
index 52f9819e04..0f66fb7467 100644
--- a/src/Auth/Protect/Protectors/IpAddress.php
+++ b/src/Auth/Protect/Protectors/IpAddress.php
@@ -2,6 +2,8 @@
namespace Statamic\Auth\Protect\Protectors;
+use Statamic\Exceptions\ForbiddenHttpException;
+
class IpAddress extends Protector
{
public function protect()
@@ -9,7 +11,7 @@ public function protect()
$ips = array_get($this->config, 'allowed', []);
if (! in_array(request()->ip(), $ips)) {
- abort(403);
+ throw new ForbiddenHttpException();
}
}
}
diff --git a/src/Auth/Protect/Protectors/Password/PasswordProtector.php b/src/Auth/Protect/Protectors/Password/PasswordProtector.php
index 1ed0c2d2bd..42e2b09bbd 100644
--- a/src/Auth/Protect/Protectors/Password/PasswordProtector.php
+++ b/src/Auth/Protect/Protectors/Password/PasswordProtector.php
@@ -4,6 +4,7 @@
use Facades\Statamic\Auth\Protect\Protectors\Password\Token;
use Statamic\Auth\Protect\Protectors\Protector;
+use Statamic\Exceptions\ForbiddenHttpException;
class PasswordProtector extends Protector
{
@@ -15,7 +16,7 @@ class PasswordProtector extends Protector
public function protect()
{
if (empty(array_get($this->config, 'allowed', []))) {
- abort(403);
+ throw new ForbiddenHttpException();
}
if (request()->isLivePreview()) {
diff --git a/src/Auth/User.php b/src/Auth/User.php
index b368da86a5..371e5d421c 100644
--- a/src/Auth/User.php
+++ b/src/Auth/User.php
@@ -26,6 +26,7 @@
use Statamic\Events\UserCreated;
use Statamic\Events\UserCreating;
use Statamic\Events\UserDeleted;
+use Statamic\Events\UserDeleting;
use Statamic\Events\UserSaved;
use Statamic\Events\UserSaving;
use Statamic\Facades;
@@ -197,6 +198,10 @@ public function save()
public function delete()
{
+ if (UserDeleting::dispatch($this) === false) {
+ return false;
+ }
+
Facades\User::delete($this);
UserDeleted::dispatch($this);
diff --git a/src/Auth/UserGroup.php b/src/Auth/UserGroup.php
index ba1021d492..0eba8f5ae3 100644
--- a/src/Auth/UserGroup.php
+++ b/src/Auth/UserGroup.php
@@ -29,7 +29,7 @@ public function __construct()
$this->data = collect();
}
- public function title(string $title = null)
+ public function title(?string $title = null)
{
if (func_num_args() === 0) {
return $this->title;
@@ -45,7 +45,7 @@ public function id(): string
return $this->handle();
}
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
if (is_null($handle)) {
return $this->handle;
diff --git a/src/Auth/UserTags.php b/src/Auth/UserTags.php
index 12c5e765c6..bfa6a3cd2c 100644
--- a/src/Auth/UserTags.php
+++ b/src/Auth/UserTags.php
@@ -2,6 +2,8 @@
namespace Statamic\Auth;
+use Illuminate\Support\Collection;
+use Statamic\Contracts\Auth\Role;
use Statamic\Facades\URL;
use Statamic\Facades\User;
use Statamic\Fields\Field;
@@ -505,7 +507,11 @@ public function is()
return $this->parser ? null : false;
}
- $roles = Arr::wrap($this->params->explode(['role', 'roles']));
+ $roles = $this->params->get(['role', 'roles']);
+
+ if (! $roles instanceof Collection || ! $roles->every(fn ($role) => $role instanceof Role)) {
+ $roles = Arr::wrap($this->params->explode(['role', 'roles']));
+ }
foreach ($roles as $role) {
if ($user->hasRole($role)) {
@@ -529,7 +535,11 @@ public function isnt()
return $this->parser ? $this->parse() : true;
}
- $roles = Arr::wrap($this->params->explode(['roles', 'role']));
+ $roles = $this->params->get(['role', 'roles']);
+
+ if (! $roles instanceof Collection || ! $roles->every(fn ($role) => $role instanceof Role)) {
+ $roles = Arr::wrap($this->params->explode(['roles', 'role']));
+ }
$is = false;
diff --git a/src/CP/Utilities/Utility.php b/src/CP/Utilities/Utility.php
index 676e7c2c38..0519202cbb 100644
--- a/src/CP/Utilities/Utility.php
+++ b/src/CP/Utilities/Utility.php
@@ -99,7 +99,7 @@ public function url()
return cp_route('utilities.index').'/'.$this->slug();
}
- public function routes(Closure $routes = null)
+ public function routes(?Closure $routes = null)
{
return $this->fluentlyGetOrSet('routes')->args(func_get_args());
}
diff --git a/src/Console/Processes/Composer.php b/src/Console/Processes/Composer.php
index aecd448047..0157feb377 100644
--- a/src/Console/Processes/Composer.php
+++ b/src/Console/Processes/Composer.php
@@ -109,7 +109,7 @@ public function installedPath(string $package)
*
* @param mixed $extraParams
*/
- public function require(string $package, string $version = null, ...$extraParams)
+ public function require(string $package, ?string $version = null, ...$extraParams)
{
if ($version) {
$parts[] = $version;
@@ -125,7 +125,7 @@ public function require(string $package, string $version = null, ...$extraParams
/**
* Require a dev package.
*/
- public function requireDev(string $package, string $version = null, ...$extraParams)
+ public function requireDev(string $package, ?string $version = null, ...$extraParams)
{
$this->require($package, $version, '--dev', ...$extraParams);
}
diff --git a/src/Contracts/Assets/Asset.php b/src/Contracts/Assets/Asset.php
index a15721fe7b..3cdd211dd2 100644
--- a/src/Contracts/Assets/Asset.php
+++ b/src/Contracts/Assets/Asset.php
@@ -76,7 +76,7 @@ public function upload(UploadedFile $file);
*
* @return \Symfony\Component\HttpFoundation\StreamedResponse
*/
- public function download(string $name = null, array $headers = []);
+ public function download(?string $name = null, array $headers = []);
/**
* Get the asset file contents.
diff --git a/src/Contracts/Assets/AssetContainerRepository.php b/src/Contracts/Assets/AssetContainerRepository.php
index 9343323044..7f906992c0 100644
--- a/src/Contracts/Assets/AssetContainerRepository.php
+++ b/src/Contracts/Assets/AssetContainerRepository.php
@@ -12,5 +12,5 @@ public function find($id): ?AssetContainer;
public function findByHandle(string $handle): ?AssetContainer;
- public function make(string $handle = null): AssetContainer;
+ public function make(?string $handle = null): AssetContainer;
}
diff --git a/src/Contracts/Auth/Role.php b/src/Contracts/Auth/Role.php
index 14a951d57b..62e5e7691f 100644
--- a/src/Contracts/Auth/Role.php
+++ b/src/Contracts/Auth/Role.php
@@ -6,9 +6,9 @@ interface Role
{
public function id(): string;
- public function title(string $title = null);
+ public function title(?string $title = null);
- public function handle(string $handle = null);
+ public function handle(?string $handle = null);
public function permissions($permissions = null);
diff --git a/src/Contracts/Auth/RoleRepository.php b/src/Contracts/Auth/RoleRepository.php
index cd51d83c62..794accbc5f 100644
--- a/src/Contracts/Auth/RoleRepository.php
+++ b/src/Contracts/Auth/RoleRepository.php
@@ -12,5 +12,5 @@ public function find(string $id): ?Role;
public function exists(string $id): bool;
- public function make(string $handle = null): Role;
+ public function make(?string $handle = null): Role;
}
diff --git a/src/Contracts/Auth/UserGroup.php b/src/Contracts/Auth/UserGroup.php
index e8da6f1cf2..fdafe39f15 100644
--- a/src/Contracts/Auth/UserGroup.php
+++ b/src/Contracts/Auth/UserGroup.php
@@ -6,9 +6,9 @@ interface UserGroup
{
public function id(): string;
- public function title(string $title = null);
+ public function title(?string $title = null);
- public function handle(string $slug = null);
+ public function handle(?string $slug = null);
public function users();
diff --git a/src/Contracts/Entries/CollectionRepository.php b/src/Contracts/Entries/CollectionRepository.php
index 331b7c212a..34ea4209f7 100644
--- a/src/Contracts/Entries/CollectionRepository.php
+++ b/src/Contracts/Entries/CollectionRepository.php
@@ -14,7 +14,7 @@ public function findByHandle($handle): ?Collection;
public function findByMount($mount): ?Collection;
- public function make(string $handle = null): Collection;
+ public function make(?string $handle = null): Collection;
public function handles(): IlluminateCollection;
diff --git a/src/Contracts/Structures/NavigationRepository.php b/src/Contracts/Structures/NavigationRepository.php
index 8e9cb0b7bc..0a09becf3d 100644
--- a/src/Contracts/Structures/NavigationRepository.php
+++ b/src/Contracts/Structures/NavigationRepository.php
@@ -14,5 +14,5 @@ public function findByHandle($handle): ?Nav;
public function save(Nav $nav);
- public function make(string $handle = null): Nav;
+ public function make(?string $handle = null): Nav;
}
diff --git a/src/Contracts/Taxonomies/TaxonomyRepository.php b/src/Contracts/Taxonomies/TaxonomyRepository.php
index 8f5e362425..0e0b6690c1 100644
--- a/src/Contracts/Taxonomies/TaxonomyRepository.php
+++ b/src/Contracts/Taxonomies/TaxonomyRepository.php
@@ -14,7 +14,7 @@ public function findByHandle($handle): ?Taxonomy;
public function findByUri(string $uri): ?Taxonomy;
- public function make(string $handle = null): Taxonomy;
+ public function make(?string $handle = null): Taxonomy;
public function handles(): Collection;
diff --git a/src/Contracts/Taxonomies/TermRepository.php b/src/Contracts/Taxonomies/TermRepository.php
index 81ea9b0b8e..98914cec30 100644
--- a/src/Contracts/Taxonomies/TermRepository.php
+++ b/src/Contracts/Taxonomies/TermRepository.php
@@ -14,7 +14,7 @@ public function find($id);
public function findByUri(string $uri);
- public function make(string $slug = null);
+ public function make(?string $slug = null);
public function query();
diff --git a/src/Entries/AugmentedEntry.php b/src/Entries/AugmentedEntry.php
index 6996840aa2..6388f554e8 100644
--- a/src/Entries/AugmentedEntry.php
+++ b/src/Entries/AugmentedEntry.php
@@ -79,7 +79,13 @@ protected function parent()
protected function mount()
{
- return $this->data->value('mount') ?? Collection::findByMount($this->data);
+ $mount = $this->data->value('mount') ?? Collection::findByMount($this->data);
+
+ if (! $mount && ($origin = $this->data->origin())) {
+ return Collection::findByMount($origin);
+ }
+
+ return $mount;
}
public function authors()
diff --git a/src/Entries/Collection.php b/src/Entries/Collection.php
index 0d60ffd400..94351777db 100644
--- a/src/Entries/Collection.php
+++ b/src/Entries/Collection.php
@@ -13,6 +13,7 @@
use Statamic\Events\CollectionCreated;
use Statamic\Events\CollectionCreating;
use Statamic\Events\CollectionDeleted;
+use Statamic\Events\CollectionDeleting;
use Statamic\Events\CollectionSaved;
use Statamic\Events\CollectionSaving;
use Statamic\Events\EntryBlueprintFound;
@@ -678,7 +679,7 @@ public function structure($structure = null)
->args(func_get_args());
}
- public function structureContents(array $contents = null)
+ public function structureContents(?array $contents = null)
{
return $this
->fluentlyGetOrSet('structureContents')
@@ -727,7 +728,18 @@ public function hasStructure()
public function delete()
{
- $this->queryEntries()->get()->each->delete();
+ if (CollectionDeleting::dispatch($this) === false) {
+ return false;
+ }
+
+ $this->queryEntries()->get()->each(function ($entry) {
+ $entry->deleteDescendants();
+ $entry->delete();
+ });
+
+ if ($this->hasStructure()) {
+ $this->structure()->trees()->each->delete();
+ }
Facades\Collection::delete($this);
@@ -860,15 +872,9 @@ public function __toString()
public function augmentedArrayData()
{
- $data = [
+ return [
'title' => $this->title(),
'handle' => $this->handle(),
];
-
- if (! Statamic::isApiRoute() && ! Statamic::isCpRoute()) {
- $data['mount'] = $this->mount();
- }
-
- return $data;
}
}
diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php
index c330345cc4..fd76d84dd1 100644
--- a/src/Entries/Entry.php
+++ b/src/Entries/Entry.php
@@ -8,6 +8,7 @@
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Support\Carbon;
+use Illuminate\Support\Traits\Localizable;
use LogicException;
use Statamic\Contracts\Auth\Protect\Protectable;
use Statamic\Contracts\Data\Augmentable;
@@ -34,6 +35,7 @@
use Statamic\Events\EntryDeleting;
use Statamic\Events\EntrySaved;
use Statamic\Events\EntrySaving;
+use Statamic\Exceptions\BlueprintNotFoundException;
use Statamic\Facades;
use Statamic\Facades\Antlers;
use Statamic\Facades\Blink;
@@ -52,7 +54,7 @@
class Entry implements Arrayable, ArrayAccess, Augmentable, ContainsQueryableValues, Contract, Localization, Protectable, ResolvesValuesContract, Responsable, SearchableContract
{
- use ContainsComputedData, ContainsData, ExistsAsFile, FluentlyGetsAndSets, HasAugmentedInstance, Publishable, Revisable, Searchable, TracksLastModified, TracksQueriedColumns, TracksQueriedRelations;
+ use ContainsComputedData, ContainsData, ExistsAsFile, FluentlyGetsAndSets, HasAugmentedInstance, Localizable, Publishable, Revisable, Searchable, TracksLastModified, TracksQueriedColumns, TracksQueriedRelations;
use HasOrigin {
value as originValue;
@@ -143,6 +145,10 @@ public function blueprint($blueprint = null)
$blueprint = $this->collection()->entryBlueprint($blueprint, $this);
+ if (! $blueprint) {
+ throw new BlueprintNotFoundException($this->value('blueprint'), 'collections/'.$this->collection()->handle());
+ }
+
Blink::put($key, $blueprint);
EntryBlueprintFound::dispatch($blueprint, $this);
@@ -185,7 +191,7 @@ public function delete()
if (optional($parent)->isRoot()) {
$parent = null;
}
- $this->page()->pages()->all()->each(function ($child) use ($tree, $parent) {
+ $this->page()?->pages()->all()->each(function ($child) use ($tree, $parent) {
$tree->move($child->id(), optional($parent)->id());
});
$tree->remove($this);
@@ -499,7 +505,7 @@ public function date($date = null)
return null;
}
- if ($date instanceof \Carbon\Carbon) {
+ if ($date instanceof \Carbon\CarbonInterface) {
return $date;
}
@@ -921,7 +927,7 @@ public function autoGeneratedTitle()
// Since the slug is generated from the title, we'll avoid augmenting
// the slug which could result in an infinite loop in some cases.
- $title = (string) Antlers::parse($format, $this->augmented()->except('slug')->all());
+ $title = $this->withLocale($this->site()->locale(), fn () => (string) Antlers::parse($format, $this->augmented()->except('slug')->all()));
return trim($title);
}
@@ -947,6 +953,8 @@ private function resolvePreviewTargetUrl($format)
return (string) Antlers::parse($format, array_merge($this->routeData(), [
'site' => $this->site(),
+ 'uri' => $this->uri(),
+ 'url' => $this->url(),
'permalink' => $this->absoluteUrl(),
'locale' => $this->locale(),
]));
diff --git a/src/Events/AssetContainerBlueprintFound.php b/src/Events/AssetContainerBlueprintFound.php
index a11d3af535..8bb6218d2c 100644
--- a/src/Events/AssetContainerBlueprintFound.php
+++ b/src/Events/AssetContainerBlueprintFound.php
@@ -6,10 +6,12 @@ class AssetContainerBlueprintFound extends Event
{
public $blueprint;
public $container;
+ public $asset;
- public function __construct($blueprint, $container = null)
+ public function __construct($blueprint, $container = null, $asset = null)
{
$this->blueprint = $blueprint;
$this->container = $container;
+ $this->asset = $asset;
}
}
diff --git a/src/Events/AssetDeleting.php b/src/Events/AssetDeleting.php
new file mode 100644
index 0000000000..b278a86d14
--- /dev/null
+++ b/src/Events/AssetDeleting.php
@@ -0,0 +1,23 @@
+asset = $asset;
+ }
+
+ /**
+ * Dispatch the event with the given arguments, and halt on first non-null listener response.
+ *
+ * @return mixed
+ */
+ public static function dispatch()
+ {
+ return event(new static(...func_get_args()), [], true);
+ }
+}
diff --git a/src/Events/BlueprintDeleting.php b/src/Events/BlueprintDeleting.php
new file mode 100644
index 0000000000..5e9729424d
--- /dev/null
+++ b/src/Events/BlueprintDeleting.php
@@ -0,0 +1,23 @@
+blueprint = $blueprint;
+ }
+
+ /**
+ * Dispatch the event with the given arguments, and halt on first non-null listener response.
+ *
+ * @return mixed
+ */
+ public static function dispatch()
+ {
+ return event(new static(...func_get_args()), [], true);
+ }
+}
diff --git a/src/Events/CollectionDeleting.php b/src/Events/CollectionDeleting.php
new file mode 100644
index 0000000000..43df645dd1
--- /dev/null
+++ b/src/Events/CollectionDeleting.php
@@ -0,0 +1,23 @@
+collection = $collection;
+ }
+
+ /**
+ * Dispatch the event with the given arguments, and halt on first non-null listener response.
+ *
+ * @return mixed
+ */
+ public static function dispatch()
+ {
+ return event(new static(...func_get_args()), [], true);
+ }
+}
diff --git a/src/Events/Event.php b/src/Events/Event.php
index c5764d2eda..1a74725925 100644
--- a/src/Events/Event.php
+++ b/src/Events/Event.php
@@ -3,8 +3,21 @@
namespace Statamic\Events;
use Illuminate\Foundation\Events\Dispatchable;
+use Statamic\Contracts\Auth\User as UserContract;
+use Statamic\Facades\User;
abstract class Event
{
use Dispatchable;
+
+ public ?UserContract $authenticatedUser;
+
+ public static function dispatch()
+ {
+ $event = new static(...func_get_args());
+
+ $event->authenticatedUser = User::current();
+
+ return event($event);
+ }
}
diff --git a/src/Events/FieldsetDeleting.php b/src/Events/FieldsetDeleting.php
new file mode 100644
index 0000000000..86722807bd
--- /dev/null
+++ b/src/Events/FieldsetDeleting.php
@@ -0,0 +1,23 @@
+fieldset = $fieldset;
+ }
+
+ /**
+ * Dispatch the event with the given arguments, and halt on first non-null listener response.
+ *
+ * @return mixed
+ */
+ public static function dispatch()
+ {
+ return event(new static(...func_get_args()), [], true);
+ }
+}
diff --git a/src/Events/FormDeleting.php b/src/Events/FormDeleting.php
new file mode 100644
index 0000000000..9f00481367
--- /dev/null
+++ b/src/Events/FormDeleting.php
@@ -0,0 +1,23 @@
+form = $form;
+ }
+
+ /**
+ * Dispatch the event with the given arguments, and halt on first non-null listener response.
+ *
+ * @return mixed
+ */
+ public static function dispatch()
+ {
+ return event(new static(...func_get_args()), [], true);
+ }
+}
diff --git a/src/Events/GlobalSetDeleting.php b/src/Events/GlobalSetDeleting.php
new file mode 100644
index 0000000000..bbbc5549de
--- /dev/null
+++ b/src/Events/GlobalSetDeleting.php
@@ -0,0 +1,23 @@
+globals = $globals;
+ }
+
+ /**
+ * Dispatch the event with the given arguments, and halt on first non-null listener response.
+ *
+ * @return mixed
+ */
+ public static function dispatch()
+ {
+ return event(new static(...func_get_args()), [], true);
+ }
+}
diff --git a/src/Events/GlobalVariablesDeleting.php b/src/Events/GlobalVariablesDeleting.php
new file mode 100644
index 0000000000..9db8c2e8b9
--- /dev/null
+++ b/src/Events/GlobalVariablesDeleting.php
@@ -0,0 +1,23 @@
+variables = $variables;
+ }
+
+ /**
+ * Dispatch the event with the given arguments, and halt on first non-null listener response.
+ *
+ * @return mixed
+ */
+ public static function dispatch()
+ {
+ return event(new static(...func_get_args()), [], true);
+ }
+}
diff --git a/src/Events/NavDeleting.php b/src/Events/NavDeleting.php
new file mode 100644
index 0000000000..be0f8d3e25
--- /dev/null
+++ b/src/Events/NavDeleting.php
@@ -0,0 +1,23 @@
+nav = $nav;
+ }
+
+ /**
+ * Dispatch the event with the given arguments, and halt on first non-null listener response.
+ *
+ * @return mixed
+ */
+ public static function dispatch()
+ {
+ return event(new static(...func_get_args()), [], true);
+ }
+}
diff --git a/src/Events/TaxonomyDeleting.php b/src/Events/TaxonomyDeleting.php
new file mode 100644
index 0000000000..8db9dee261
--- /dev/null
+++ b/src/Events/TaxonomyDeleting.php
@@ -0,0 +1,23 @@
+taxonomy = $taxonomy;
+ }
+
+ /**
+ * Dispatch the event with the given arguments, and halt on first non-null listener response.
+ *
+ * @return mixed
+ */
+ public static function dispatch()
+ {
+ return event(new static(...func_get_args()), [], true);
+ }
+}
diff --git a/src/Events/TermDeleting.php b/src/Events/TermDeleting.php
new file mode 100644
index 0000000000..4e29862b3d
--- /dev/null
+++ b/src/Events/TermDeleting.php
@@ -0,0 +1,23 @@
+term = $term;
+ }
+
+ /**
+ * Dispatch the event with the given arguments, and halt on first non-null listener response.
+ *
+ * @return mixed
+ */
+ public static function dispatch()
+ {
+ return event(new static(...func_get_args()), [], true);
+ }
+}
diff --git a/src/Events/UserDeleting.php b/src/Events/UserDeleting.php
new file mode 100644
index 0000000000..56d823bcee
--- /dev/null
+++ b/src/Events/UserDeleting.php
@@ -0,0 +1,23 @@
+user = $user;
+ }
+
+ /**
+ * Dispatch the event with the given arguments, and halt on first non-null listener response.
+ *
+ * @return mixed
+ */
+ public static function dispatch()
+ {
+ return event(new static(...func_get_args()), [], true);
+ }
+}
diff --git a/src/Exceptions/ApiExceptionHandler.php b/src/Exceptions/ApiExceptionHandler.php
deleted file mode 100644
index 81b4611ace..0000000000
--- a/src/Exceptions/ApiExceptionHandler.php
+++ /dev/null
@@ -1,16 +0,0 @@
-renderException($request, $e);
- }
-}
diff --git a/src/Exceptions/ApiValidationException.php b/src/Exceptions/ApiValidationException.php
new file mode 100644
index 0000000000..1d13585bb6
--- /dev/null
+++ b/src/Exceptions/ApiValidationException.php
@@ -0,0 +1,13 @@
+json(['message' => $this->getMessage()], 422);
+ }
+}
diff --git a/src/Exceptions/Concerns/RendersApiExceptions.php b/src/Exceptions/Concerns/RendersApiExceptions.php
deleted file mode 100644
index 96add28bd8..0000000000
--- a/src/Exceptions/Concerns/RendersApiExceptions.php
+++ /dev/null
@@ -1,28 +0,0 @@
-json(['message' => $e->getMessage()], 422);
- }
-
- if ($e instanceof NotFoundHttpException) {
- return response()->json(['message' => $e->getMessage() ?: 'Not found.'], 404);
- }
-
- if ($e instanceof StatamicProAuthorizationException) {
- throw new StatamicProRequiredException($e->getMessage());
- }
-
- return parent::render($request, $e);
- }
-}
diff --git a/src/Exceptions/Concerns/RendersHttpExceptions.php b/src/Exceptions/Concerns/RendersHttpExceptions.php
new file mode 100644
index 0000000000..884711863f
--- /dev/null
+++ b/src/Exceptions/Concerns/RendersHttpExceptions.php
@@ -0,0 +1,56 @@
+view('statamic::errors.'.$this->getStatusCode(), [], $this->getStatusCode());
+ }
+
+ if (Statamic::isApiRoute()) {
+ return response()->json(['message' => $this->getApiMessage()], $this->getStatusCode());
+ }
+
+ if (view()->exists('errors.'.$this->getStatusCode())) {
+ return response($this->contents(), $this->getStatusCode());
+ }
+ }
+
+ protected function contents()
+ {
+ Cascade::hydrated(function ($cascade) {
+ $cascade->set('response_code', $this->getStatusCode());
+ });
+
+ return app(View::class)
+ ->template('errors.'.$this->getStatusCode())
+ ->layout($this->layout())
+ ->render();
+ }
+
+ protected function layout()
+ {
+ $layouts = collect([
+ 'errors.layout',
+ 'layouts.layout',
+ 'layout',
+ 'statamic::blank',
+ ]);
+
+ return $layouts->filter()->first(function ($layout) {
+ return view()->exists($layout);
+ });
+ }
+
+ public function getApiMessage()
+ {
+ return $this->getMessage();
+ }
+}
diff --git a/src/Exceptions/FieldsetNotFoundException.php b/src/Exceptions/FieldsetNotFoundException.php
new file mode 100644
index 0000000000..81326a4e8f
--- /dev/null
+++ b/src/Exceptions/FieldsetNotFoundException.php
@@ -0,0 +1,44 @@
+fieldsetHandle = $fieldsetHandle;
+ }
+
+ public function getSolution(): Solution
+ {
+ $description = ($suggestedFieldset = $this->getSuggestedFieldset())
+ ? "Did you mean `$suggestedFieldset`?"
+ : 'Are you sure the fieldset exists?';
+
+ return BaseSolution::create("The {$this->fieldsetHandle} fieldset was not found.")
+ ->setSolutionDescription($description)
+ ->setDocumentationLinks([
+ 'Read the fieldsets guide' => Statamic::docsUrl('/fieldsets'),
+ ]);
+ }
+
+ protected function getSuggestedFieldset()
+ {
+ return StringComparator::findClosestMatch(
+ Fieldset::all()->map->handle()->values()->all(),
+ $this->fieldsetHandle
+ );
+ }
+}
diff --git a/src/Exceptions/ForbiddenHttpException.php b/src/Exceptions/ForbiddenHttpException.php
new file mode 100644
index 0000000000..041f933236
--- /dev/null
+++ b/src/Exceptions/ForbiddenHttpException.php
@@ -0,0 +1,16 @@
+view('statamic::errors.404', [], 404);
- }
-
- if (view()->exists('errors.404')) {
- return response($this->contents(), 404);
- }
- }
-
- protected function contents()
- {
- Cascade::hydrated(function ($cascade) {
- $cascade->set('response_code', 404);
- });
-
- return app(View::class)
- ->template('errors.404')
- ->layout($this->layout())
- ->render();
- }
+ use RendersHttpExceptions;
- protected function layout()
+ public function getApiMessage()
{
- $layouts = collect([
- 'errors.layout',
- 'layouts.layout',
- 'layout',
- 'statamic::blank',
- ]);
-
- return $layouts->filter()->first(function ($layout) {
- return view()->exists($layout);
- });
+ return 'Not found.';
}
}
diff --git a/src/Exceptions/StatamicProAuthorizationException.php b/src/Exceptions/StatamicProAuthorizationException.php
index 1ef8f496c1..53f75e4c42 100644
--- a/src/Exceptions/StatamicProAuthorizationException.php
+++ b/src/Exceptions/StatamicProAuthorizationException.php
@@ -4,5 +4,8 @@
class StatamicProAuthorizationException extends AuthorizationException
{
- //
+ public function render()
+ {
+ throw new StatamicProRequiredException($this->getMessage());
+ }
}
diff --git a/src/Facades/Site.php b/src/Facades/Site.php
index 7d5c5b9c77..e21d6453ef 100644
--- a/src/Facades/Site.php
+++ b/src/Facades/Site.php
@@ -14,6 +14,7 @@
* @method static mixed findByUrl($url)
* @method static mixed current()
* @method static void setCurrent($site)
+ * @method static void resolveCurrentUrlUsing(Closure $callback)
* @method static mixed selected()
* @method static void setSelected($site)
* @method static void setConfig($key, $value = null)
diff --git a/src/Fields/Blueprint.php b/src/Fields/Blueprint.php
index 12fa154021..1d3d5dbe89 100644
--- a/src/Fields/Blueprint.php
+++ b/src/Fields/Blueprint.php
@@ -16,6 +16,7 @@
use Statamic\Events\BlueprintCreated;
use Statamic\Events\BlueprintCreating;
use Statamic\Events\BlueprintDeleted;
+use Statamic\Events\BlueprintDeleting;
use Statamic\Events\BlueprintSaved;
use Statamic\Events\BlueprintSaving;
use Statamic\Exceptions\DuplicateFieldException;
@@ -257,7 +258,7 @@ private function addEnsuredFieldToContents($contents, $ensured)
$field = $allFields->get($importKey);
$tab = $field['tab'];
$fields = collect($tabs[$tab]['sections'][$targetSectionIndex]['fields'])->keyBy(function ($field) {
- return (isset($field['import'])) ? 'import:'.$field['import'] : $field['handle'];
+ return (isset($field['import'])) ? 'import:'.($field['prefix'] ?? null).$field['import'] : $field['handle'];
});
$importedConfig = $importedField['field']->config();
$config = array_merge($config, $importedConfig);
@@ -458,6 +459,10 @@ public function save()
public function delete()
{
+ if (BlueprintDeleting::dispatch($this) === false) {
+ return false;
+ }
+
BlueprintRepository::delete($this);
BlueprintDeleted::dispatch($this);
diff --git a/src/Fields/Field.php b/src/Fields/Field.php
index bf9bee6e5d..46c6722083 100644
--- a/src/Fields/Field.php
+++ b/src/Fields/Field.php
@@ -166,7 +166,7 @@ public function validationAttributes()
{
$display = Lang::has($key = 'validation.attributes.'.$this->handle())
? Lang::get($key)
- : $this->display();
+ : __($this->display());
return array_merge(
[$this->handle() => $display],
diff --git a/src/Fields/Fields.php b/src/Fields/Fields.php
index c3f34dd7db..0fb89bd44a 100644
--- a/src/Fields/Fields.php
+++ b/src/Fields/Fields.php
@@ -5,6 +5,7 @@
use Facades\Statamic\Fields\FieldRepository;
use Facades\Statamic\Fields\Validator;
use Illuminate\Support\Collection;
+use Statamic\Exceptions\FieldsetNotFoundException;
use Statamic\Facades\Blink;
use Statamic\Facades\Fieldset as FieldsetRepository;
use Statamic\Support\Arr;
@@ -266,7 +267,7 @@ private function getImportedFields(array $config): array
return Blink::once($blink, function () use ($config) {
if (! $fieldset = FieldsetRepository::find($config['import'])) {
- throw new \Exception("Fieldset {$config['import']} not found.");
+ throw new FieldsetNotFoundException($config['import']);
}
$fields = $fieldset->fields()->all();
diff --git a/src/Fields/Fieldset.php b/src/Fields/Fieldset.php
index 6b31cf4a14..e14f7ba7a4 100644
--- a/src/Fields/Fieldset.php
+++ b/src/Fields/Fieldset.php
@@ -5,11 +5,16 @@
use Statamic\Events\FieldsetCreated;
use Statamic\Events\FieldsetCreating;
use Statamic\Events\FieldsetDeleted;
+use Statamic\Events\FieldsetDeleting;
use Statamic\Events\FieldsetSaved;
use Statamic\Events\FieldsetSaving;
use Statamic\Facades;
+use Statamic\Facades\AssetContainer;
+use Statamic\Facades\Collection;
use Statamic\Facades\Fieldset as FieldsetRepository;
+use Statamic\Facades\GlobalSet;
use Statamic\Facades\Path;
+use Statamic\Facades\Taxonomy;
use Statamic\Support\Str;
class Fieldset
@@ -111,6 +116,68 @@ public function deleteUrl()
return cp_route('fieldsets.destroy', $this->handle());
}
+ public function importedBy(): array
+ {
+ $blueprints = collect([
+ ...Collection::all()->flatMap->entryBlueprints(),
+ ...Taxonomy::all()->flatMap->termBlueprints(),
+ ...GlobalSet::all()->map->blueprint(),
+ ...AssetContainer::all()->map->blueprint(),
+ ...Blueprint::in('')->values(),
+ ])->filter()->filter(function (Blueprint $blueprint) {
+ return collect($blueprint->contents()['tabs'])
+ ->pluck('sections')
+ ->flatten(1)
+ ->pluck('fields')
+ ->flatten(1)
+ ->filter(fn ($field) => $field && $this->fieldImportsFieldset($field))
+ ->isNotEmpty();
+ })->values();
+
+ $fieldsets = \Statamic\Facades\Fieldset::all()
+ ->filter(fn (Fieldset $fieldset) => isset($fieldset->contents()['fields']))
+ ->filter(function (Fieldset $fieldset) {
+ return collect($fieldset->contents()['fields'])
+ ->filter(fn ($field) => $this->fieldImportsFieldset($field))
+ ->isNotEmpty();
+ })
+ ->values();
+
+ return ['blueprints' => $blueprints, 'fieldsets' => $fieldsets];
+ }
+
+ private function fieldImportsFieldset(array $field): bool
+ {
+ if (isset($field['import'])) {
+ return $field['import'] === $this->handle();
+ }
+
+ if (is_string($field['field'])) {
+ return Str::before($field['field'], '.') === $this->handle();
+ }
+
+ if (isset($field['field']['fields'])) {
+ return collect($field['field']['fields'])
+ ->filter(fn ($field) => $this->fieldImportsFieldset($field))
+ ->isNotEmpty();
+ }
+
+ if (isset($field['field']['sets'])) {
+ return collect($field['field']['sets'])
+ ->filter(fn ($setGroup) => isset($setGroup['sets']))
+ ->filter(function ($setGroup) {
+ return collect($setGroup['sets'])->filter(function ($set) {
+ return collect($set['fields'])
+ ->filter(fn ($field) => $this->fieldImportsFieldset($field))
+ ->isNotEmpty();
+ })->isNotEmpty();
+ })
+ ->isNotEmpty();
+ }
+
+ return false;
+ }
+
public function isDeletable()
{
return ! $this->isNamespaced();
@@ -169,6 +236,10 @@ public function save()
public function delete()
{
+ if (FieldsetDeleting::dispatch($this) === false) {
+ return false;
+ }
+
FieldsetRepository::delete($this);
FieldsetDeleted::dispatch($this);
diff --git a/src/Fields/FieldsetRepository.php b/src/Fields/FieldsetRepository.php
index 7bcd8bb20d..fabcf2e98f 100644
--- a/src/Fields/FieldsetRepository.php
+++ b/src/Fields/FieldsetRepository.php
@@ -175,7 +175,7 @@ public function addNamespace(string $namespace, string $directory): void
$this->hints[$namespace] = $directory;
}
- protected function getFieldsetsByDirectory(string $directory, string $namespace = null): Collection
+ protected function getFieldsetsByDirectory(string $directory, ?string $namespace = null): Collection
{
return File::withAbsolutePaths()
->getFilesByTypeRecursively($directory, 'yaml')
diff --git a/src/Fields/Fieldtype.php b/src/Fields/Fieldtype.php
index da9217cbc4..9c1bca046a 100644
--- a/src/Fields/Fieldtype.php
+++ b/src/Fields/Fieldtype.php
@@ -309,7 +309,7 @@ public function view()
: 'statamic::forms.fields.default';
}
- public function config(string $key = null, $fallback = null)
+ public function config(?string $key = null, $fallback = null)
{
if (! $this->field) {
return $fallback;
diff --git a/src/Fields/Validator.php b/src/Fields/Validator.php
index 51e6fb5cd5..b215b6057c 100644
--- a/src/Fields/Validator.php
+++ b/src/Fields/Validator.php
@@ -68,6 +68,10 @@ private function fieldRules()
}
return $this->fields->preProcessValidatables()->all()->reduce(function ($carry, $field) {
+ if (request()->isPrecognitive() && $field->type() == 'assets') {
+ return $carry;
+ }
+
return $carry->merge($field->setValidationContext($this->context)->rules());
}, collect());
}
diff --git a/src/Fieldtypes/Assets/Assets.php b/src/Fieldtypes/Assets/Assets.php
index f8f99d2fbb..4fc4719630 100644
--- a/src/Fieldtypes/Assets/Assets.php
+++ b/src/Fieldtypes/Assets/Assets.php
@@ -54,6 +54,7 @@ protected function configFieldItems(): array
'type' => 'asset_container',
'max_items' => 1,
'mode' => 'select',
+ 'required' => true,
'default' => AssetContainer::all()->count() == 1 ? AssetContainer::all()->first()->handle() : null,
],
'folder' => [
@@ -91,6 +92,11 @@ protected function configFieldItems(): array
'type' => 'toggle',
'default' => true,
],
+ 'query_scopes' => [
+ 'display' => __('Query Scopes'),
+ 'instructions' => __('statamic::fieldtypes.assets.config.query_scopes'),
+ 'type' => 'taggable',
+ ],
],
],
];
diff --git a/src/Fieldtypes/Bard.php b/src/Fieldtypes/Bard.php
index 9a768f3f79..3a8d24447c 100644
--- a/src/Fieldtypes/Bard.php
+++ b/src/Fieldtypes/Bard.php
@@ -242,7 +242,7 @@ public function filter()
protected function performAugmentation($value, $shallow)
{
- if ($this->shouldSaveHtml()) {
+ if ($this->shouldSaveHtml() && is_string($value)) {
return is_null($value) ? $value : $this->resolveStatamicUrls($value);
}
diff --git a/src/Fieldtypes/Date.php b/src/Fieldtypes/Date.php
index f66f1d58e8..dda18bf26f 100644
--- a/src/Fieldtypes/Date.php
+++ b/src/Fieldtypes/Date.php
@@ -182,7 +182,7 @@ private function splitDateTimeForPreProcessSingle(Carbon $carbon)
];
}
- private function splitDateTimeForPreProcessRange(array $range = null)
+ private function splitDateTimeForPreProcessRange(?array $range = null)
{
return ['date' => $range, 'time' => null];
}
diff --git a/src/Fieldtypes/Link/ArrayableLink.php b/src/Fieldtypes/Link/ArrayableLink.php
index 7ac7d04ec0..34658795f4 100644
--- a/src/Fieldtypes/Link/ArrayableLink.php
+++ b/src/Fieldtypes/Link/ArrayableLink.php
@@ -24,7 +24,7 @@ public function jsonSerialize()
return $this->url(); // Use a string for backwards compatibility in the REST API, etc.
}
- private function url()
+ public function url()
{
return is_object($this->value) ? $this->value?->url() : $this->value;
}
diff --git a/src/Fieldtypes/Relationship.php b/src/Fieldtypes/Relationship.php
index b318b55681..b489fbf897 100644
--- a/src/Fieldtypes/Relationship.php
+++ b/src/Fieldtypes/Relationship.php
@@ -6,6 +6,7 @@
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Statamic\CP\Column;
+use Statamic\Facades\Scope;
use Statamic\Fields\Fieldtype;
abstract class Relationship extends Fieldtype
@@ -327,8 +328,8 @@ public function toQueryableValue($value)
protected function applyIndexQueryScopes($query, $params)
{
collect(Arr::wrap($this->config('query_scopes')))
- ->map(fn ($handle) => app('statamic.scopes')->get($handle))
+ ->map(fn ($handle) => Scope::find($handle))
->filter()
- ->each(fn ($class) => app($class)->apply($query, $params));
+ ->each(fn ($scope) => $scope->apply($query, $params));
}
}
diff --git a/src/Fieldtypes/TemplateFolder.php b/src/Fieldtypes/TemplateFolder.php
index 1a1743a053..9268403090 100644
--- a/src/Fieldtypes/TemplateFolder.php
+++ b/src/Fieldtypes/TemplateFolder.php
@@ -2,7 +2,8 @@
namespace Statamic\Fieldtypes;
-use Statamic\Facades\Folder;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
class TemplateFolder extends Relationship
{
@@ -16,13 +17,20 @@ protected function toItemArray($id, $site = null)
public function getIndexItems($request)
{
- return Folder::disk('resources')
- ->getFoldersRecursively('views')
- ->map(function ($folder) {
- $folder = str_replace_first('views/', '', $folder);
+ return collect(config('view.paths'))
+ ->flatMap(function ($path) {
+ $directories = collect();
+ $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path), RecursiveIteratorIterator::SELF_FIRST);
- return ['id' => $folder, 'title' => $folder];
+ foreach ($iterator as $file) {
+ if ($file->isDir() && ! $iterator->isDot() && ! $iterator->isLink()) {
+ $directories->push(str_replace_first($path.DIRECTORY_SEPARATOR, '', $file->getPathname()));
+ }
+ }
+
+ return $directories->filter()->values();
})
+ ->map(fn ($folder) => ['id' => $folder, 'title' => $folder])
->values();
}
}
diff --git a/src/Forms/Form.php b/src/Forms/Form.php
index f6835235b4..520e43b78c 100644
--- a/src/Forms/Form.php
+++ b/src/Forms/Form.php
@@ -3,6 +3,7 @@
namespace Statamic\Forms;
use Illuminate\Contracts\Support\Arrayable;
+use Illuminate\Support\Facades\Log;
use Statamic\Contracts\Data\Augmentable;
use Statamic\Contracts\Data\Augmented;
use Statamic\Contracts\Forms\Form as FormContract;
@@ -13,6 +14,7 @@
use Statamic\Events\FormCreated;
use Statamic\Events\FormCreating;
use Statamic\Events\FormDeleted;
+use Statamic\Events\FormDeleting;
use Statamic\Events\FormSaved;
use Statamic\Events\FormSaving;
use Statamic\Facades\Blueprint;
@@ -25,6 +27,7 @@
use Statamic\Statamic;
use Statamic\Support\Arr;
use Statamic\Support\Traits\FluentlyGetsAndSets;
+use Statamic\Yaml\ParseException;
class Form implements Arrayable, Augmentable, FormContract
{
@@ -226,6 +229,10 @@ public function save()
*/
public function delete()
{
+ if (FormDeleting::dispatch($this) === false) {
+ return false;
+ }
+
$this->submissions()->each->delete();
File::delete($this->path());
@@ -303,9 +310,16 @@ public function submissions()
$path = config('statamic.forms.submissions').'/'.$this->handle();
return collect(Folder::getFilesByType($path, 'yaml'))->map(function ($file) {
+ try {
+ $data = YAML::parse(File::get($file));
+ } catch (ParseException $e) {
+ $data = [];
+ Log::warning('Could not parse form submission file: '.$file);
+ }
+
return $this->makeSubmission()
->id(pathinfo($file)['filename'])
- ->data(YAML::parse(File::get($file)));
+ ->data($data);
});
}
diff --git a/src/Forms/SendEmail.php b/src/Forms/SendEmail.php
index 9b192f9fa8..1721d51875 100644
--- a/src/Forms/SendEmail.php
+++ b/src/Forms/SendEmail.php
@@ -28,6 +28,7 @@ public function __construct(Submission $submission, Site $site, $config)
public function handle()
{
- Mail::send(new Email($this->submission, $this->config, $this->site));
+ Mail::mailer($this->config['mailer'] ?? null)
+ ->send(new Email($this->submission, $this->config, $this->site));
}
}
diff --git a/src/Git/CommitJob.php b/src/Git/CommitJob.php
index 379e9b9311..726443d76b 100644
--- a/src/Git/CommitJob.php
+++ b/src/Git/CommitJob.php
@@ -12,21 +12,11 @@ class CommitJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
- /**
- * Commit message.
- *
- * @var string|null
- */
- public $message;
-
/**
* Create a new job instance.
- *
- * @param string|null $message
*/
- public function __construct($message = null)
+ public function __construct(public $message = null, public $committer = null)
{
- $this->message = $message;
}
/**
@@ -34,6 +24,6 @@ public function __construct($message = null)
*/
public function handle()
{
- Git::commit($this->message);
+ Git::as($this->committer)->commit($this->message);
}
}
diff --git a/src/Git/Git.php b/src/Git/Git.php
index 3417d7017b..10215cfeaa 100644
--- a/src/Git/Git.php
+++ b/src/Git/Git.php
@@ -4,6 +4,7 @@
use Illuminate\Filesystem\Filesystem;
use Statamic\Console\Processes\Git as GitProcess;
+use Statamic\Contracts\Auth\User as UserContract;
use Statamic\Facades\Antlers;
use Statamic\Facades\Path;
use Statamic\Facades\User;
@@ -11,6 +12,8 @@
class Git
{
+ private ?UserContract $authenticatedUser;
+
/**
* Instantiate git tracked content manager.
*/
@@ -54,6 +57,18 @@ public function statuses()
return $statuses->isNotEmpty() ? $statuses : null;
}
+ /**
+ * Act as a specific user.
+ */
+ public function as(?UserContract $user): static
+ {
+ $clone = clone $this;
+
+ $clone->authenticatedUser = $user;
+
+ return $clone;
+ }
+
/**
* Git add and commit all tracked content, using configured commands.
*/
@@ -74,7 +89,7 @@ public function dispatchCommit($message = null)
$message = null;
}
- CommitJob::dispatch($message)
+ CommitJob::dispatch($message, $this->authenticatedUser())
->onConnection(config('statamic.git.queue_connection'))
->delay($delayInMinutes ?? null);
}
@@ -92,9 +107,7 @@ public function gitUserName()
return $default;
}
- $currentUser = User::current();
-
- return $currentUser ? $currentUser->name() : $default;
+ return $this->authenticatedUser()?->name() ?? $default;
}
/**
@@ -110,9 +123,12 @@ public function gitUserEmail()
return $default;
}
- $currentUser = User::current();
+ return $this->authenticatedUser()?->email() ?? $default;
+ }
- return $currentUser ? $currentUser->email() : $default;
+ private function authenticatedUser(): ?UserContract
+ {
+ return $this->authenticatedUser ?? User::current();
}
/**
diff --git a/src/Git/Subscriber.php b/src/Git/Subscriber.php
index 30177d7b5c..09764e9540 100644
--- a/src/Git/Subscriber.php
+++ b/src/Git/Subscriber.php
@@ -36,7 +36,7 @@ public function commit($event)
return;
}
- Git::dispatchCommit(
+ Git::as($event->authenticatedUser)->dispatchCommit(
$event instanceof ProvidesCommitMessage
? $event->commitMessage()
: null
diff --git a/src/Globals/GlobalSet.php b/src/Globals/GlobalSet.php
index 851a977e4e..cc811e4a4d 100644
--- a/src/Globals/GlobalSet.php
+++ b/src/Globals/GlobalSet.php
@@ -8,6 +8,7 @@
use Statamic\Events\GlobalSetCreated;
use Statamic\Events\GlobalSetCreating;
use Statamic\Events\GlobalSetDeleted;
+use Statamic\Events\GlobalSetDeleting;
use Statamic\Events\GlobalSetSaved;
use Statamic\Events\GlobalSetSaving;
use Statamic\Facades;
@@ -128,6 +129,10 @@ protected function saveOrDeleteLocalizations()
public function delete()
{
+ if (GlobalSetDeleting::dispatch($this) === false) {
+ return false;
+ }
+
$this->localizations()->each->delete();
Facades\GlobalSet::delete($this);
diff --git a/src/Globals/Variables.php b/src/Globals/Variables.php
index 546254e0c1..5c1db8806b 100644
--- a/src/Globals/Variables.php
+++ b/src/Globals/Variables.php
@@ -19,6 +19,7 @@
use Statamic\Events\GlobalVariablesCreated;
use Statamic\Events\GlobalVariablesCreating;
use Statamic\Events\GlobalVariablesDeleted;
+use Statamic\Events\GlobalVariablesDeleting;
use Statamic\Events\GlobalVariablesSaved;
use Statamic\Events\GlobalVariablesSaving;
use Statamic\Facades;
@@ -156,6 +157,10 @@ public function save()
public function delete()
{
+ if (GlobalVariablesDeleting::dispatch($this) === false) {
+ return false;
+ }
+
Facades\GlobalVariables::delete($this);
GlobalVariablesDeleted::dispatch($this);
diff --git a/src/GraphQL/Queries/Query.php b/src/GraphQL/Queries/Query.php
index 5f7cc4e4ee..0dfb1bd8f4 100644
--- a/src/GraphQL/Queries/Query.php
+++ b/src/GraphQL/Queries/Query.php
@@ -21,7 +21,7 @@ public static function auth($closure)
static::$auth = $closure;
}
- public function authorize($root, array $args, $ctx, ResolveInfo $resolveInfo = null, Closure $getSelectFields = null): bool
+ public function authorize($root, array $args, $ctx, ?ResolveInfo $resolveInfo = null, ?Closure $getSelectFields = null): bool
{
if (static::$auth) {
return call_user_func_array(static::$auth, [$root, $args, $ctx, $resolveInfo, $getSelectFields]);
diff --git a/src/GraphQL/ServiceProvider.php b/src/GraphQL/ServiceProvider.php
index d73c555664..a71c0dc935 100644
--- a/src/GraphQL/ServiceProvider.php
+++ b/src/GraphQL/ServiceProvider.php
@@ -8,7 +8,6 @@
use Statamic\Contracts\GraphQL\ResponseCache;
use Statamic\GraphQL\ResponseCache\DefaultCache;
use Statamic\GraphQL\ResponseCache\NullCache;
-use Statamic\Http\Middleware\API\SwapExceptionHandler;
use Statamic\Http\Middleware\HandleToken;
use Statamic\Http\Middleware\RequireStatamicPro;
@@ -60,7 +59,6 @@ private function addMiddleware()
collect($this->app['router']->getRoutes()->getRoutes())
->filter(fn ($route) => $route->getAction()['uses'] === GraphQLController::class.'@query')
->each(fn ($route) => $route->middleware([
- SwapExceptionHandler::class,
RequireStatamicPro::class,
HandleToken::class,
]));
diff --git a/src/GraphQL/Types/ArrayType.php b/src/GraphQL/Types/ArrayType.php
index 0da8903ec2..cdf8a13d3a 100644
--- a/src/GraphQL/Types/ArrayType.php
+++ b/src/GraphQL/Types/ArrayType.php
@@ -21,7 +21,7 @@ public function parseValue($value)
return $value;
}
- public function parseLiteral(Node $valueNode, array $variables = null)
+ public function parseLiteral(Node $valueNode, ?array $variables = null)
{
return $valueNode->value;
}
diff --git a/src/Http/Controllers/API/ApiController.php b/src/Http/Controllers/API/ApiController.php
index 23e9b854b6..56d15310d7 100644
--- a/src/Http/Controllers/API/ApiController.php
+++ b/src/Http/Controllers/API/ApiController.php
@@ -3,7 +3,7 @@
namespace Statamic\Http\Controllers\API;
use Facades\Statamic\API\ResourceAuthorizer;
-use Illuminate\Validation\ValidationException;
+use Statamic\Exceptions\ApiValidationException;
use Statamic\Exceptions\NotFoundHttpException;
use Statamic\Facades\Site;
use Statamic\Http\Controllers\Controller;
@@ -138,7 +138,7 @@ protected function getFilters()
->filter(fn ($field) => ! $allowedFilters->contains($field));
if ($forbidden->isNotEmpty()) {
- throw ValidationException::withMessages([
+ throw ApiValidationException::withMessages([
'filter' => Str::plural('Forbidden filter', $forbidden).': '.$forbidden->join(', '),
]);
}
diff --git a/src/Http/Controllers/API/TaxonomyTermEntriesController.php b/src/Http/Controllers/API/TaxonomyTermEntriesController.php
index a9047093bd..7b732c9920 100644
--- a/src/Http/Controllers/API/TaxonomyTermEntriesController.php
+++ b/src/Http/Controllers/API/TaxonomyTermEntriesController.php
@@ -4,6 +4,7 @@
use Facades\Statamic\API\FilterAuthorizer;
use Facades\Statamic\API\ResourceAuthorizer;
+use Statamic\Exceptions\NotFoundHttpException;
use Statamic\Facades\Collection;
use Statamic\Facades\Term;
use Statamic\Http\Resources\API\EntryResource;
@@ -32,6 +33,8 @@ public function index($taxonomy, $term)
$term = Term::find($taxonomy.'::'.$term);
+ throw_unless($term, new NotFoundHttpException);
+
$query = $term->queryEntries();
$this->allowedCollections = $this->allowedCollections();
diff --git a/src/Http/Controllers/CP/API/TemplatesController.php b/src/Http/Controllers/CP/API/TemplatesController.php
index 86fc73cd02..f5bf71b2bd 100644
--- a/src/Http/Controllers/CP/API/TemplatesController.php
+++ b/src/Http/Controllers/CP/API/TemplatesController.php
@@ -2,17 +2,27 @@
namespace Statamic\Http\Controllers\CP\API;
-use Statamic\Facades\Folder;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
use Statamic\Http\Controllers\CP\CpController;
class TemplatesController extends CpController
{
public function index()
{
- return collect(Folder::disk('resources')->getFilesRecursively('views'))
- ->map(function ($view) {
- return str_replace_first('views/', '', str_before($view, '.'));
+ return collect(config('view.paths'))
+ ->flatMap(function ($path) {
+ $views = collect();
+ $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path));
+
+ foreach ($iterator as $file) {
+ if ($file->isFile()) {
+ $views->push(str_before(str_replace_first($path.DIRECTORY_SEPARATOR, '', $file->getPathname()), '.'));
+ }
+ }
+
+ return $views->filter()->values();
})
- ->filter()->values();
+ ->values();
}
}
diff --git a/src/Http/Controllers/CP/Assets/BrowserController.php b/src/Http/Controllers/CP/Assets/BrowserController.php
index fbeed8b06e..1782596bda 100644
--- a/src/Http/Controllers/CP/Assets/BrowserController.php
+++ b/src/Http/Controllers/CP/Assets/BrowserController.php
@@ -6,10 +6,12 @@
use Statamic\Contracts\Assets\AssetContainer as AssetContainerContract;
use Statamic\Exceptions\AuthorizationException;
use Statamic\Facades\Asset;
+use Statamic\Facades\Scope;
use Statamic\Facades\User;
use Statamic\Http\Controllers\CP\CpController;
use Statamic\Http\Resources\CP\Assets\FolderAssetsCollection;
use Statamic\Http\Resources\CP\Assets\SearchedAssetsCollection;
+use Statamic\Support\Arr;
class BrowserController extends CpController
{
@@ -81,6 +83,8 @@ public function folder(Request $request, $container, $path = '/')
$query->orderBy($container->sortField(), $container->sortDirection());
}
+ $this->applyQueryScopes($query, $request->all());
+
$assets = $query->paginate(request('perPage'));
return (new FolderAssetsCollection($assets))->folder($folder);
@@ -98,6 +102,8 @@ public function search(Request $request, $container, $path = null)
$query->where('folder', $path);
}
+ $this->applyQueryScopes($query, $request->all());
+
$assets = $query->paginate(request('perPage'));
if ($container->hasSearchIndex()) {
@@ -106,4 +112,12 @@ public function search(Request $request, $container, $path = null)
return new SearchedAssetsCollection($assets);
}
+
+ protected function applyQueryScopes($query, $params)
+ {
+ collect(Arr::wrap($params['queryScopes'] ?? null))
+ ->map(fn ($handle) => Scope::find($handle))
+ ->filter()
+ ->each(fn ($scope) => $scope->apply($query, $params));
+ }
}
diff --git a/src/Http/Controllers/CP/Collections/CollectionsController.php b/src/Http/Controllers/CP/Collections/CollectionsController.php
index 34dd7591e0..6d76e81e23 100644
--- a/src/Http/Controllers/CP/Collections/CollectionsController.php
+++ b/src/Http/Controllers/CP/Collections/CollectionsController.php
@@ -55,6 +55,12 @@ public function show(Request $request, $collection)
{
$this->authorize('view', $collection, __('You are not authorized to view this collection.'));
+ $site = $request->site ? Site::get($request->site) : Site::selected();
+
+ if ($response = $this->ensureCollectionIsAvailableOnSite($collection, $site)) {
+ return $response;
+ }
+
$blueprints = $collection
->entryBlueprints()
->reject->hidden()
@@ -65,8 +71,6 @@ public function show(Request $request, $collection)
];
})->values();
- $site = $request->site ? Site::get($request->site) : Site::selected();
-
$blueprint = $collection->entryBlueprint();
if (! $blueprint) {
@@ -566,4 +570,11 @@ protected function getAuthorizedSitesForCollection($collection)
->values()
->all();
}
+
+ protected function ensureCollectionIsAvailableOnSite($collection, $site)
+ {
+ if (Site::hasMultiple() && ! $collection->sites()->contains($site->handle())) {
+ return redirect()->back()->with('error', __('Collection is not available on site ":handle".', ['handle' => $site->handle]));
+ }
+ }
}
diff --git a/src/Http/Controllers/CP/Collections/EntriesController.php b/src/Http/Controllers/CP/Collections/EntriesController.php
index 823f79cef5..5102b70e41 100644
--- a/src/Http/Controllers/CP/Collections/EntriesController.php
+++ b/src/Http/Controllers/CP/Collections/EntriesController.php
@@ -272,6 +272,10 @@ public function create(Request $request, $collection, $site)
{
$this->authorize('create', [EntryContract::class, $collection, $site]);
+ if ($response = $this->ensureCollectionIsAvailableOnSite($collection, $site)) {
+ return $response;
+ }
+
$blueprint = $collection->entryBlueprint($request->blueprint);
if (! $blueprint) {
@@ -403,8 +407,10 @@ public function store(Request $request, $collection, $site)
$saved = $entry->updateLastModified(User::current())->save();
}
- return (new EntryResource($entry))
- ->additional(['saved' => $saved]);
+ return [
+ 'data' => (new EntryResource($entry))->resolve()['data'],
+ 'saved' => $saved,
+ ];
}
private function resolveSlug($request)
@@ -575,4 +581,11 @@ protected function getAuthorizedSitesForCollection($collection)
->sites()
->filter(fn ($handle) => User::current()->can('view', Site::get($handle)));
}
+
+ protected function ensureCollectionIsAvailableOnSite($collection, $site)
+ {
+ if (Site::hasMultiple() && ! $collection->sites()->contains($site->handle())) {
+ return redirect()->back()->with('error', __('Collection is not available on site ":handle".', ['handle' => $site->handle]));
+ }
+ }
}
diff --git a/src/Http/Controllers/CP/Fields/FieldsController.php b/src/Http/Controllers/CP/Fields/FieldsController.php
index 2b788541ac..14e6405056 100644
--- a/src/Http/Controllers/CP/Fields/FieldsController.php
+++ b/src/Http/Controllers/CP/Fields/FieldsController.php
@@ -61,9 +61,17 @@ public function update(Request $request)
->fields()
->addValues($request->values);
- $fields->validate([], [
+ $extraRules = [];
+ $customMessages = [
'handle.not_in' => __('statamic::validation.reserved'),
- ]);
+ ];
+
+ if ($request->type === 'date' && $request->values['handle'] === 'date') {
+ $extraRules['mode'] = 'in:single';
+ $customMessages['mode.in'] = __('statamic::validation.date_fieldtype_only_single_mode_allowed');
+ }
+
+ $fields->validate($extraRules, $customMessages);
$values = array_merge($request->values, $fields->process()->values()->all());
@@ -99,7 +107,11 @@ protected function blueprint($blueprint)
'type' => 'slug',
'from' => 'display',
'separator' => '_',
- 'validate' => 'required|regex:/^[a-zA-Z][a-zA-Z0-9_]*$/|not_in:'.implode(',', $reserved),
+ 'validate' => [
+ 'required',
+ 'regex:/^[a-zA-Z]([a-zA-Z0-9_]|->)*$/',
+ 'not_in:'.implode(',', $reserved),
+ ],
'show_regenerate' => true,
],
'instructions' => [
diff --git a/src/Http/Controllers/CP/Fields/FieldsetController.php b/src/Http/Controllers/CP/Fields/FieldsetController.php
index bae8519839..9639c4bd0d 100644
--- a/src/Http/Controllers/CP/Fields/FieldsetController.php
+++ b/src/Http/Controllers/CP/Fields/FieldsetController.php
@@ -4,6 +4,7 @@
use Illuminate\Http\Request;
use Statamic\Facades;
+use Statamic\Fields\Blueprint;
use Statamic\Fields\Fieldset;
use Statamic\Fields\FieldTransformer;
use Statamic\Http\Controllers\CP\CpController;
@@ -29,6 +30,9 @@ public function index(Request $request)
'delete_url' => $fieldset->deleteUrl(),
'edit_url' => $fieldset->editUrl(),
'fields' => $fieldset->fields()->all()->count(),
+ 'imported_by' => collect($fieldset->importedBy())->flatten(1)->mapToGroups(function ($item) {
+ return [$this->group($item) => ['handle' => $item->handle(), 'title' => $item->title()]];
+ }),
'is_deletable' => $fieldset->isDeletable(),
'title' => $fieldset->title(),
],
@@ -42,6 +46,29 @@ public function index(Request $request)
return view('statamic::fieldsets.index', compact('fieldsets'));
}
+ private function group(Blueprint|Fieldset $item)
+ {
+ if ($item instanceof Fieldset) {
+ return __('Fieldsets');
+ }
+
+ if ($namespace = $item->namespace()) {
+ return match (Str::before($namespace, '.')) {
+ 'collections' => __('Collections'),
+ 'taxonomies' => __('Taxonomies'),
+ 'navigation' => __('Navigation'),
+ 'globals' => __('Globals'),
+ 'assets' => __('Asset Containers'),
+ 'forms' => __('Forms'),
+ };
+ }
+
+ return match ($item->handle()) {
+ 'user', 'user_group' => __('Users'),
+ default => __('Other'),
+ };
+ }
+
public function edit($fieldset)
{
$fieldset = Facades\Fieldset::find($fieldset);
diff --git a/src/Http/Controllers/CP/Forms/FormSubmissionsController.php b/src/Http/Controllers/CP/Forms/FormSubmissionsController.php
index 93a2c40b0e..0269c78316 100644
--- a/src/Http/Controllers/CP/Forms/FormSubmissionsController.php
+++ b/src/Http/Controllers/CP/Forms/FormSubmissionsController.php
@@ -94,7 +94,7 @@ public function show($form, $submission)
'blueprint' => $blueprint->toPublishArray(),
'values' => $fields->values(),
'meta' => $fields->meta(),
- 'title' => $submission->date()->format('M j, Y @ H:i'),
+ 'title' => $submission->formattedDate(),
]);
}
}
diff --git a/src/Http/Controllers/CP/Forms/FormsController.php b/src/Http/Controllers/CP/Forms/FormsController.php
index 1cbbf79334..1780cd6ee7 100644
--- a/src/Http/Controllers/CP/Forms/FormsController.php
+++ b/src/Http/Controllers/CP/Forms/FormsController.php
@@ -309,6 +309,15 @@ protected function editFormBlueprint($form)
'instructions' => __('statamic::messages.form_configure_email_attachments_instructions'),
],
],
+ [
+ 'handle' => 'mailer',
+ 'field' => [
+ 'type' => 'select',
+ 'instructions' => __('statamic::messages.form_configure_mailer_instructions'),
+ 'options' => array_keys(config('mail.mailers')),
+ 'clearable' => true,
+ ],
+ ],
],
],
],
diff --git a/src/Http/Controllers/CP/Navigation/NavigationController.php b/src/Http/Controllers/CP/Navigation/NavigationController.php
index 0854b2b095..11537dd73e 100644
--- a/src/Http/Controllers/CP/Navigation/NavigationController.php
+++ b/src/Http/Controllers/CP/Navigation/NavigationController.php
@@ -68,7 +68,11 @@ public function show(Request $request, $nav)
$site = $request->site ?? Site::selected()->handle();
if (! $nav->existsIn($site)) {
- return redirect($nav->trees()->first()->showUrl());
+ if ($nav->trees()->isNotEmpty()) {
+ return redirect($nav->trees()->first()->showUrl());
+ }
+
+ $nav->makeTree($site)->save();
}
$this->authorize('view', $nav->in($site), __('You are not authorized to view navs.'));
diff --git a/src/Http/Controllers/CP/Taxonomies/TaxonomiesController.php b/src/Http/Controllers/CP/Taxonomies/TaxonomiesController.php
index 03fe41b8e0..06da6e4af9 100644
--- a/src/Http/Controllers/CP/Taxonomies/TaxonomiesController.php
+++ b/src/Http/Controllers/CP/Taxonomies/TaxonomiesController.php
@@ -133,6 +133,9 @@ public function edit($taxonomy)
'collections' => $taxonomy->collections()->map->handle()->all(),
'sites' => $taxonomy->sites()->all(),
'preview_targets' => $taxonomy->basePreviewTargets(),
+ 'term_template' => $taxonomy->termTemplate(),
+ 'template' => $taxonomy->template(),
+ 'layout' => $taxonomy->layout(),
];
$fields = ($blueprint = $this->editFormBlueprint($taxonomy))
@@ -162,7 +165,10 @@ public function update(Request $request, $taxonomy)
$taxonomy
->title($values['title'])
- ->previewTargets($values['preview_targets']);
+ ->previewTargets($values['preview_targets'])
+ ->termTemplate($values['term_template'] ?? null)
+ ->template($values['template'] ?? null)
+ ->layout($values['layout'] ?? null);
if ($sites = array_get($values, 'sites')) {
$taxonomy->sites($sites);
@@ -308,6 +314,28 @@ protected function editFormBlueprint($taxonomy)
],
],
],
+ 'templates' => [
+ 'display' => __('Templates'),
+ 'fields' => [
+ 'template' => [
+ 'display' => __('Template'),
+ 'instructions' => __('statamic::messages.taxonomy_configure_template_instructions'),
+ 'type' => 'template',
+ 'placeholder' => __('System default'),
+ ],
+ 'term_template' => [
+ 'display' => __('Term Template'),
+ 'instructions' => __('statamic::messages.taxonomy_configure_term_template_instructions'),
+ 'type' => 'template',
+ 'placeholder' => __('System default'),
+ ],
+ 'layout' => [
+ 'display' => __('Layout'),
+ 'instructions' => __('statamic::messages.taxonomy_configure_layout_instructions'),
+ 'type' => 'template',
+ ],
+ ],
+ ],
]);
return Blueprint::makeFromTabs($fields);
diff --git a/src/Http/Controllers/CP/Utilities/UpdateSearchController.php b/src/Http/Controllers/CP/Utilities/UpdateSearchController.php
index 587166be51..c089a27bd3 100644
--- a/src/Http/Controllers/CP/Utilities/UpdateSearchController.php
+++ b/src/Http/Controllers/CP/Utilities/UpdateSearchController.php
@@ -5,6 +5,7 @@
use Illuminate\Http\Request;
use Statamic\Facades\Search;
use Statamic\Http\Controllers\CP\CpController;
+use Statamic\Support\Str;
class UpdateSearchController extends CpController
{
@@ -15,7 +16,11 @@ public function update(Request $request)
])['indexes']);
$indexes->each(function ($index) {
- Search::index($index)->update();
+ [$name, $locale] = explode('::', $index);
+ if ($locale) {
+ $name = Str::before($name, '_'.$locale);
+ }
+ Search::index($name, $locale ?: null)->update();
});
return back()->withSuccess(__('Update successful.'));
diff --git a/src/Http/Controllers/FormController.php b/src/Http/Controllers/FormController.php
index b7ab366f73..1304e1c3b4 100644
--- a/src/Http/Controllers/FormController.php
+++ b/src/Http/Controllers/FormController.php
@@ -28,7 +28,15 @@ public function submit(FrontendFormRequest $request, $form)
$site = Site::findByUrl(URL::previous()) ?? Site::default();
$fields = $form->blueprint()->fields();
$this->validateContentType($request, $form);
- $values = array_merge($request->all(), $assets = $request->assets());
+ $values = $request->all();
+
+ $fields->all()
+ ->filter(fn ($field) => $field->fieldtype()->handle() === 'checkboxes')
+ ->each(function ($field) use (&$values) {
+ return Arr::set($values, $field->handle(), collect(Arr::get($values, $field->handle(), []))->filter(fn ($value) => $value !== null)->values()->all());
+ });
+
+ $values = array_merge($values, $assets = $request->assets());
$params = collect($request->all())->filter(function ($value, $key) {
return Str::startsWith($key, '_');
})->all();
diff --git a/src/Http/Middleware/API/SwapExceptionHandler.php b/src/Http/Middleware/API/SwapExceptionHandler.php
deleted file mode 100644
index d44d54ffc7..0000000000
--- a/src/Http/Middleware/API/SwapExceptionHandler.php
+++ /dev/null
@@ -1,14 +0,0 @@
-getProperty('toStringFormat');
$format->setAccessible(true);
$originalToStringFormat = $format->getValue();
- Carbon::setToStringFormat(Statamic::dateFormat());
+ Date::setToStringFormat(Statamic::dateFormat());
$response = $next($request);
@@ -40,7 +41,7 @@ public function handle($request, Closure $next)
// not within the scope of the request to be the "defaults".
setlocale(LC_TIME, $originalLocale);
app()->setLocale($originalAppLocale);
- Carbon::setToStringFormat($originalToStringFormat);
+ Date::setToStringFormat($originalToStringFormat);
return $response;
}
diff --git a/src/Http/Resources/API/TermResource.php b/src/Http/Resources/API/TermResource.php
index 23f6668e22..11e339bf24 100644
--- a/src/Http/Resources/API/TermResource.php
+++ b/src/Http/Resources/API/TermResource.php
@@ -14,17 +14,17 @@ class TermResource extends JsonResource
*/
public function toArray($request)
{
- $fields = $this->resource->selectedQueryColumns() ?? $this->resource->augmented()->keys();
+ $fields = collect($this->resource->selectedQueryColumns() ?? $this->resource->augmented()->keys());
- // Don't want the 'entries' variable in API requests.
- $fields = array_diff($fields, ['entries']);
+ // Don't want these variables in API requests.
+ $fields = $fields->reject(fn ($field) => in_array($field, ['entries', 'collection']));
$with = $this->blueprint()
->fields()->all()
->filter->isRelationship()->keys()->all();
return $this->resource
- ->toAugmentedCollection($fields)
+ ->toAugmentedCollection($fields->all())
->withRelations($with)
->withShallowNesting()
->toArray();
diff --git a/src/Http/View/Composers/NavComposer.php b/src/Http/View/Composers/NavComposer.php
index e93fb5871b..d7b46b816e 100644
--- a/src/Http/View/Composers/NavComposer.php
+++ b/src/Http/View/Composers/NavComposer.php
@@ -3,6 +3,7 @@
namespace Statamic\Http\View\Composers;
use Illuminate\View\View;
+use Statamic\Facades\Blink;
use Statamic\Facades\CP\Nav;
class NavComposer
@@ -12,10 +13,8 @@ class NavComposer
'statamic::partials.nav-mobile',
];
- protected static $nav;
-
public function compose(View $view)
{
- $view->with('nav', self::$nav = self::$nav ?? Nav::build());
+ $view->with('nav', Blink::once('nav-composer-navigation', fn () => Nav::build()));
}
}
diff --git a/src/Imaging/GlideManager.php b/src/Imaging/GlideManager.php
index 4600044c2e..ad329c6eb4 100644
--- a/src/Imaging/GlideManager.php
+++ b/src/Imaging/GlideManager.php
@@ -47,6 +47,10 @@ public function server(array $config = [])
$cachePath .= '/'.Str::beforeLast($filename, '.').'/'.Str::of($path)->after('/');
+ if ($extension = ($params['fm'] ?? false)) {
+ $cachePath = Str::beforeLast($cachePath, '.').'.'.$extension;
+ }
+
return $cachePath;
});
}
diff --git a/src/Imaging/GuzzleAdapter.php b/src/Imaging/GuzzleAdapter.php
index cbdf5cdb5e..867320ac4f 100644
--- a/src/Imaging/GuzzleAdapter.php
+++ b/src/Imaging/GuzzleAdapter.php
@@ -40,7 +40,7 @@ class GuzzleAdapter implements FilesystemAdapter
* @param string $base The base URL.
* @param \GuzzleHttp\ClientInterface $client An optional Guzzle client.
*/
- public function __construct($base, ClientInterface $client = null)
+ public function __construct($base, ?ClientInterface $client = null)
{
$this->base = rtrim($base, '/').'/';
$this->client = $client ?: new Client();
diff --git a/src/Imaging/ResponseFactory.php b/src/Imaging/ResponseFactory.php
index b7d1cfa277..40e33c0271 100644
--- a/src/Imaging/ResponseFactory.php
+++ b/src/Imaging/ResponseFactory.php
@@ -20,7 +20,7 @@ class ResponseFactory implements ResponseFactoryInterface
*
* @param Request|null $request Request object to check "is not modified".
*/
- public function __construct(Request $request = null)
+ public function __construct(?Request $request = null)
{
$this->request = $request;
}
diff --git a/src/Listeners/ClearAssetGlideCache.php b/src/Listeners/ClearAssetGlideCache.php
index 1981143a83..36bdeac882 100644
--- a/src/Listeners/ClearAssetGlideCache.php
+++ b/src/Listeners/ClearAssetGlideCache.php
@@ -9,15 +9,26 @@
use Statamic\Events\AssetSaved;
use Statamic\Events\Subscriber;
use Statamic\Facades\Glide;
+use Statamic\Imaging\PresetGenerator;
class ClearAssetGlideCache extends Subscriber implements ShouldQueue
{
+ /**
+ * @var PresetGenerator
+ */
+ private $generator;
+
protected $listeners = [
AssetSaved::class => 'handleSaved',
AssetDeleted::class => 'handleDeleted',
AssetReuploaded::class => 'handleReuploaded',
];
+ public function __construct(PresetGenerator $generator)
+ {
+ $this->generator = $generator;
+ }
+
public function handleReuploaded(AssetReuploaded $event)
{
$this->clear($event->asset);
@@ -32,6 +43,7 @@ public function handleSaved($event)
{
if ($event->asset->getOriginal('data.focus') != $event->asset->get('focus')) {
$this->clear($event->asset);
+ $this->generator->generate($event->asset);
}
}
diff --git a/src/Modifiers/CoreModifiers.php b/src/Modifiers/CoreModifiers.php
index 66e5180c8b..b4910d4f8c 100644
--- a/src/Modifiers/CoreModifiers.php
+++ b/src/Modifiers/CoreModifiers.php
@@ -3,10 +3,11 @@
namespace Statamic\Modifiers;
use ArrayAccess;
-use Carbon\Carbon;
+use Carbon\CarbonInterface as Carbon;
use Countable;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Date;
use Statamic\Contracts\Assets\Asset as AssetContract;
use Statamic\Contracts\Data\Augmentable;
use Statamic\Facades\Antlers;
@@ -3008,7 +3009,7 @@ private function getMathModifierNumber($params, $context)
private function carbon($value)
{
if (! $value instanceof Carbon) {
- $value = (is_numeric($value)) ? Carbon::createFromTimestamp($value) : Carbon::parse($value);
+ $value = (is_numeric($value)) ? Date::createFromTimestamp($value) : Date::parse($value);
}
return $value;
diff --git a/src/Policies/AssetPolicy.php b/src/Policies/AssetPolicy.php
index fc5714a693..2892deed26 100644
--- a/src/Policies/AssetPolicy.php
+++ b/src/Policies/AssetPolicy.php
@@ -6,6 +6,15 @@
class AssetPolicy
{
+ public function before($user)
+ {
+ $user = User::fromUser($user);
+
+ if ($user->hasPermission('configure asset containers')) {
+ return true;
+ }
+ }
+
public function view($user, $asset)
{
return User::fromUser($user)->hasPermission("view {$asset->containerHandle()} assets");
diff --git a/src/Preferences/Preference.php b/src/Preferences/Preference.php
index 4e3574d021..a2adfbc79f 100644
--- a/src/Preferences/Preference.php
+++ b/src/Preferences/Preference.php
@@ -8,7 +8,7 @@ class Preference
protected $field;
protected $tab = 'general';
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
if (func_num_args() === 0) {
return $this->handle;
@@ -19,7 +19,7 @@ public function handle(string $handle = null)
return $this;
}
- public function field(array $field = null)
+ public function field(?array $field = null)
{
if (func_num_args() === 0) {
return $this->field;
@@ -30,7 +30,7 @@ public function field(array $field = null)
return $this;
}
- public function tab(string $tab = null)
+ public function tab(?string $tab = null)
{
if (func_num_args() === 0) {
return $this->tab;
diff --git a/src/Providers/AuthServiceProvider.php b/src/Providers/AuthServiceProvider.php
index 3aba6e4eee..c37bb1b03a 100755
--- a/src/Providers/AuthServiceProvider.php
+++ b/src/Providers/AuthServiceProvider.php
@@ -48,8 +48,8 @@ public function register()
$repository = $app[UserRepositoryManager::class]->repository();
foreach ($repository::bindings() as $abstract => $concrete) {
- if (! $this->app->bound($abstract)) {
- $this->app->bind($abstract, $concrete);
+ if (! $app->bound($abstract)) {
+ $app->bind($abstract, $concrete);
}
}
diff --git a/src/Providers/ExtensionServiceProvider.php b/src/Providers/ExtensionServiceProvider.php
index 1f7e9a2f86..15636477c4 100644
--- a/src/Providers/ExtensionServiceProvider.php
+++ b/src/Providers/ExtensionServiceProvider.php
@@ -151,6 +151,7 @@ class ExtensionServiceProvider extends ServiceProvider
Tags\Assets::class,
Tags\Cache::class,
Tags\Can::class,
+ Tags\Children::class,
Tags\Collection\Collection::class,
Tags\Cookie::class,
Tags\Dd::class,
diff --git a/src/Query/Builder.php b/src/Query/Builder.php
index bbed286cf8..16f604cbfa 100644
--- a/src/Query/Builder.php
+++ b/src/Query/Builder.php
@@ -7,6 +7,7 @@
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
+use Illuminate\Support\LazyCollection;
use InvalidArgumentException;
use Statamic\Contracts\Query\Builder as Contract;
use Statamic\Extensions\Pagination\LengthAwarePaginator;
@@ -660,4 +661,72 @@ public function with($relations)
return $this;
}
+
+ /**
+ * Chunk the results of the query.
+ *
+ * @param int $count
+ * @return bool
+ */
+ public function chunk($count, callable $callback)
+ {
+ $page = 1;
+
+ do {
+ // We'll execute the query for the given page and get the results. If there are
+ // no results we can just break and return from here. When there are results
+ // we will call the callback with the current chunk of these results here.
+ $results = $this->forPage($page, $count)->get();
+
+ $countResults = $results->count();
+
+ if ($countResults == 0) {
+ break;
+ }
+
+ // On each chunk result set, we will pass them to the callback and then let the
+ // developer take care of everything within the callback, which allows us to
+ // keep the memory low for spinning through large result sets for working.
+ if ($callback($results, $page) === false) {
+ return false;
+ }
+
+ unset($results);
+
+ $page++;
+ } while ($countResults == $count);
+
+ return true;
+ }
+
+ /**
+ * Query lazily, by chunks of the given size.
+ *
+ * @param int $chunkSize
+ * @return \Illuminate\Support\LazyCollection
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function lazy($chunkSize = 1000)
+ {
+ if ($chunkSize < 1) {
+ throw new InvalidArgumentException('The chunk size should be at least 1');
+ }
+
+ return LazyCollection::make(function () use ($chunkSize) {
+ $page = 1;
+
+ while (true) {
+ $results = $this->forPage($page++, $chunkSize)->get();
+
+ foreach ($results as $result) {
+ yield $result;
+ }
+
+ if ($results->count() < $chunkSize) {
+ return;
+ }
+ }
+ });
+ }
}
diff --git a/src/Query/EloquentQueryBuilder.php b/src/Query/EloquentQueryBuilder.php
index b878734d8d..a2c4b0ae98 100644
--- a/src/Query/EloquentQueryBuilder.php
+++ b/src/Query/EloquentQueryBuilder.php
@@ -6,6 +6,7 @@
use DateTimeInterface;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Support\Carbon;
+use Illuminate\Support\LazyCollection;
use InvalidArgumentException;
use Statamic\Contracts\Query\Builder;
use Statamic\Extensions\Pagination\LengthAwarePaginator;
@@ -430,4 +431,72 @@ protected function invalidOperatorAndValue($operator, $value)
return is_null($value) && in_array($operator, array_keys($this->operators)) &&
! in_array($operator, ['=', '<>', '!=']);
}
+
+ /**
+ * Chunk the results of the query.
+ *
+ * @param int $count
+ * @return bool
+ */
+ public function chunk($count, callable $callback)
+ {
+ $page = 1;
+
+ do {
+ // We'll execute the query for the given page and get the results. If there are
+ // no results we can just break and return from here. When there are results
+ // we will call the callback with the current chunk of these results here.
+ $results = $this->forPage($page, $count)->get();
+
+ $countResults = $results->count();
+
+ if ($countResults == 0) {
+ break;
+ }
+
+ // On each chunk result set, we will pass them to the callback and then let the
+ // developer take care of everything within the callback, which allows us to
+ // keep the memory low for spinning through large result sets for working.
+ if ($callback($results, $page) === false) {
+ return false;
+ }
+
+ unset($results);
+
+ $page++;
+ } while ($countResults == $count);
+
+ return true;
+ }
+
+ /**
+ * Query lazily, by chunks of the given size.
+ *
+ * @param int $chunkSize
+ * @return \Illuminate\Support\LazyCollection
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function lazy($chunkSize = 1000)
+ {
+ if ($chunkSize < 1) {
+ throw new InvalidArgumentException('The chunk size should be at least 1');
+ }
+
+ return LazyCollection::make(function () use ($chunkSize) {
+ $page = 1;
+
+ while (true) {
+ $results = $this->forPage($page++, $chunkSize)->get();
+
+ foreach ($results as $result) {
+ yield $result;
+ }
+
+ if ($results->count() < $chunkSize) {
+ return;
+ }
+ }
+ });
+ }
}
diff --git a/src/Query/Scopes/ScopeRepository.php b/src/Query/Scopes/ScopeRepository.php
index 16bacacc9f..fc1d84f8ee 100644
--- a/src/Query/Scopes/ScopeRepository.php
+++ b/src/Query/Scopes/ScopeRepository.php
@@ -4,19 +4,38 @@
class ScopeRepository
{
+ private $removed = [];
+
public function all()
{
return app('statamic.scopes')
->map(fn ($class) => app($class))
+ ->reject(fn ($class) => in_array($class->handle(), $this->removed))
->filter()
->values();
}
public function find($key, $context = [])
{
- if ($scope = app('statamic.scopes')->get($key)) {
- return app($scope)?->context($context);
+ if (in_array($key, $this->removed)) {
+ return;
+ }
+
+ if (! $scope = app('statamic.scopes')->get($key)) {
+ return;
}
+
+ $scope = app($scope);
+
+ if (! $scope) {
+ return;
+ }
+
+ if (! method_exists($scope, 'context')) {
+ return $scope;
+ }
+
+ return $scope->context($context);
}
public function filters($key, $context = [])
@@ -27,4 +46,11 @@ public function filters($key, $context = [])
->filter->visibleTo($key)
->values();
}
+
+ public function remove(string $handle)
+ {
+ $this->removed[] = $handle;
+
+ return $this;
+ }
}
diff --git a/src/Search/Index.php b/src/Search/Index.php
index a57bab55c3..9cf98a2d98 100644
--- a/src/Search/Index.php
+++ b/src/Search/Index.php
@@ -21,7 +21,7 @@ abstract protected function insertDocuments(Documents $documents);
abstract protected function deleteIndex();
- public function __construct($name, array $config, string $locale = null)
+ public function __construct($name, array $config, ?string $locale = null)
{
$this->name = $locale ? $name.'_'.$locale : $name;
$this->config = $config;
diff --git a/src/Search/ProvidesSearchables.php b/src/Search/ProvidesSearchables.php
index 2230480bef..f2f929091b 100644
--- a/src/Search/ProvidesSearchables.php
+++ b/src/Search/ProvidesSearchables.php
@@ -3,6 +3,7 @@
namespace Statamic\Search;
use Illuminate\Support\Collection;
+use Illuminate\Support\LazyCollection;
interface ProvidesSearchables
{
@@ -12,7 +13,7 @@ public static function referencePrefix(): string;
public function setKeys(array $keys): self;
- public function provide(): Collection;
+ public function provide(): Collection|LazyCollection;
public function contains($searchable): bool;
diff --git a/src/Search/Result.php b/src/Search/Result.php
index ee39290413..f44d8ed528 100644
--- a/src/Search/Result.php
+++ b/src/Search/Result.php
@@ -61,7 +61,7 @@ public function getReference(): string
return $this->searchable->getSearchReference();
}
- public function setScore(int $score = null): self
+ public function setScore(?int $score = null): self
{
$this->score = $score;
diff --git a/src/Search/Searchables/Entries.php b/src/Search/Searchables/Entries.php
index 7911802125..a8e8eba4c1 100644
--- a/src/Search/Searchables/Entries.php
+++ b/src/Search/Searchables/Entries.php
@@ -3,6 +3,7 @@
namespace Statamic\Search\Searchables;
use Illuminate\Support\Collection;
+use Illuminate\Support\LazyCollection;
use Statamic\Contracts\Entries\Entry as EntryContract;
use Statamic\Facades\Entry;
@@ -18,7 +19,7 @@ public static function referencePrefix(): string
return 'entry';
}
- public function provide(): Collection
+ public function provide(): Collection|LazyCollection
{
$query = Entry::query();
@@ -30,7 +31,7 @@ public function provide(): Collection
$query->where('site', $site);
}
- return $query->get()->filter($this->filter())->values();
+ return $query->lazy(100)->filter($this->filter())->values();
}
public function contains($searchable): bool
diff --git a/src/Search/Searchables/Providers.php b/src/Search/Searchables/Providers.php
index 7d1d8916b1..368726acd5 100644
--- a/src/Search/Searchables/Providers.php
+++ b/src/Search/Searchables/Providers.php
@@ -30,7 +30,7 @@ public function providers(): Collection
});
}
- public function make(string $key, Index $index = null, array $keys = null)
+ public function make(string $key, ?Index $index = null, ?array $keys = null)
{
if (! $provider = $this->providers()->get($key)) {
throw new \Exception('Unknown searchable ['.$key.']');
diff --git a/src/Search/Searchables/Terms.php b/src/Search/Searchables/Terms.php
index 240274b03d..d7d54337a9 100644
--- a/src/Search/Searchables/Terms.php
+++ b/src/Search/Searchables/Terms.php
@@ -3,6 +3,7 @@
namespace Statamic\Search\Searchables;
use Illuminate\Support\Collection;
+use Illuminate\Support\LazyCollection;
use Statamic\Contracts\Taxonomies\Term as TermContract;
use Statamic\Facades\Term;
use Statamic\Support\Str;
@@ -19,7 +20,7 @@ public static function referencePrefix(): string
return 'term';
}
- public function provide(): Collection
+ public function provide(): Collection|LazyCollection
{
$query = Term::query();
@@ -31,7 +32,7 @@ public function provide(): Collection
$query->where('site', $site);
}
- return $query->get()->filter($this->filter())->values();
+ return $query->lazy(100)->filter($this->filter())->values();
}
public function contains($searchable): bool
diff --git a/src/Sites/Sites.php b/src/Sites/Sites.php
index b67fd13f9b..76afa07f5e 100644
--- a/src/Sites/Sites.php
+++ b/src/Sites/Sites.php
@@ -2,6 +2,7 @@
namespace Statamic\Sites;
+use Closure;
use Statamic\Facades\User;
use Statamic\Support\Str;
@@ -10,6 +11,7 @@ class Sites
protected $config;
protected $sites;
protected $current;
+ protected ?Closure $currentUrlCallback = null;
public function __construct($config)
{
@@ -54,15 +56,27 @@ public function findByUrl($url)
public function current()
{
return $this->current
- ?? $this->findByUrl(request()->getUri())
+ ?? $this->findByCurrentUrl()
?? $this->default();
}
+ private function findByCurrentUrl()
+ {
+ return $this->findByUrl(
+ $this->currentUrlCallback ? call_user_func($this->currentUrlCallback) : request()->getUri()
+ );
+ }
+
public function setCurrent($site)
{
$this->current = $this->get($site);
}
+ public function resolveCurrentUrlUsing(Closure $callback)
+ {
+ $this->currentUrlCallback = $callback;
+ }
+
public function selected()
{
return $this->get(session('statamic.cp.selected-site')) ?? $this->default();
diff --git a/src/Stache/Repositories/AssetContainerRepository.php b/src/Stache/Repositories/AssetContainerRepository.php
index 3f46ba37a1..7fcb89a3e8 100644
--- a/src/Stache/Repositories/AssetContainerRepository.php
+++ b/src/Stache/Repositories/AssetContainerRepository.php
@@ -33,7 +33,7 @@ public function findByHandle(string $handle): ?AssetContainer
return $this->store->getItem($handle);
}
- public function make(string $handle = null): AssetContainer
+ public function make(?string $handle = null): AssetContainer
{
return app(AssetContainer::class)->handle($handle);
}
diff --git a/src/Stache/Repositories/CollectionRepository.php b/src/Stache/Repositories/CollectionRepository.php
index 62ad7cda24..4412d89053 100644
--- a/src/Stache/Repositories/CollectionRepository.php
+++ b/src/Stache/Repositories/CollectionRepository.php
@@ -51,7 +51,7 @@ public function findByMount($mount): ?Collection
});
}
- public function make(string $handle = null): Collection
+ public function make(?string $handle = null): Collection
{
return app(Collection::class)->handle($handle);
}
diff --git a/src/Stache/Repositories/EntryRepository.php b/src/Stache/Repositories/EntryRepository.php
index e125e500ef..f3d3d738ac 100644
--- a/src/Stache/Repositories/EntryRepository.php
+++ b/src/Stache/Repositories/EntryRepository.php
@@ -43,7 +43,7 @@ public function find($id): ?Entry
return $this->query()->where('id', $id)->first();
}
- public function findByUri(string $uri, string $site = null): ?Entry
+ public function findByUri(string $uri, ?string $site = null): ?Entry
{
$site = $site ?? $this->stache->sites()->first();
diff --git a/src/Stache/Repositories/NavigationRepository.php b/src/Stache/Repositories/NavigationRepository.php
index 7cb58ed416..1ded4311bc 100644
--- a/src/Stache/Repositories/NavigationRepository.php
+++ b/src/Stache/Repositories/NavigationRepository.php
@@ -45,7 +45,7 @@ public function delete(Nav $nav)
$this->store->delete($nav);
}
- public function make(string $handle = null): Nav
+ public function make(?string $handle = null): Nav
{
return app(Nav::class)->handle($handle);
}
diff --git a/src/Stache/Repositories/TaxonomyRepository.php b/src/Stache/Repositories/TaxonomyRepository.php
index 2ac3c7a613..0e1bd76fd3 100644
--- a/src/Stache/Repositories/TaxonomyRepository.php
+++ b/src/Stache/Repositories/TaxonomyRepository.php
@@ -54,12 +54,12 @@ public function delete(Taxonomy $taxonomy)
$this->store->delete($taxonomy);
}
- public function make(string $handle = null): Taxonomy
+ public function make(?string $handle = null): Taxonomy
{
return app(Taxonomy::class)->handle($handle);
}
- public function findByUri(string $uri, string $site = null): ?Taxonomy
+ public function findByUri(string $uri, ?string $site = null): ?Taxonomy
{
$collection = Facades\Collection::all()
->first(function ($collection) use ($uri, $site) {
@@ -67,7 +67,7 @@ public function findByUri(string $uri, string $site = null): ?Taxonomy
return true;
}
- return Str::startsWith($uri, '/'.$collection->handle());
+ return Str::startsWith($uri.'/', '/'.$collection->handle().'/');
});
if ($collection) {
diff --git a/src/Stache/Repositories/TermRepository.php b/src/Stache/Repositories/TermRepository.php
index c91c379ac1..ba75a56be7 100644
--- a/src/Stache/Repositories/TermRepository.php
+++ b/src/Stache/Repositories/TermRepository.php
@@ -45,7 +45,7 @@ public function find($id): ?Term
return $this->query()->where('id', $id)->first();
}
- public function findByUri(string $uri, string $site = null): ?Term
+ public function findByUri(string $uri, ?string $site = null): ?Term
{
$site = $site ?? $this->stache->sites()->first();
@@ -59,7 +59,7 @@ public function findByUri(string $uri, string $site = null): ?Term
return true;
}
- return Str::startsWith($uri, '/'.$collection->handle());
+ return Str::startsWith($uri.'/', '/'.$collection->handle().'/');
});
if ($collection) {
@@ -112,7 +112,7 @@ public function query()
return new TermQueryBuilder($this->store);
}
- public function make(string $slug = null): Term
+ public function make(?string $slug = null): Term
{
return app(Term::class)->slug($slug);
}
diff --git a/src/Stache/Stores/TaxonomiesStore.php b/src/Stache/Stores/TaxonomiesStore.php
index 4a79a8949b..7971ac7936 100644
--- a/src/Stache/Stores/TaxonomiesStore.php
+++ b/src/Stache/Stores/TaxonomiesStore.php
@@ -45,7 +45,10 @@ public function makeItemFromFile($path, $contents)
->searchIndex(array_get($data, 'search_index'))
->defaultPublishState($this->getDefaultPublishState($data))
->sites($sites)
- ->previewTargets($this->normalizePreviewTargets(array_get($data, 'preview_targets', [])));
+ ->previewTargets($this->normalizePreviewTargets(array_get($data, 'preview_targets', [])))
+ ->termTemplate(array_get($data, 'term_template', null))
+ ->template(array_get($data, 'template', null))
+ ->layout(array_get($data, 'layout', null));
}
protected function getDefaultPublishState($data)
diff --git a/src/Stache/Stores/TaxonomyTermsStore.php b/src/Stache/Stores/TaxonomyTermsStore.php
index 8b58771f84..bf38d8dccb 100644
--- a/src/Stache/Stores/TaxonomyTermsStore.php
+++ b/src/Stache/Stores/TaxonomyTermsStore.php
@@ -182,6 +182,19 @@ protected function getKeyFromPath($path)
public function save($term)
{
+ // Since we store terms by slug, if the slug changes it's technically
+ // a completely new term, and we'll need to delete the existing one.
+ if (($originalSlug = $term->getOriginal('slug')) && $originalSlug != $term->slug()) {
+ $existing = Term::find($term->taxonomyHandle().'::'.$originalSlug);
+ $this->delete($existing->term());
+ }
+
+ // The "old" state shouldn't be maintained within the Stache, otherwise it'll be there
+ // when the term is retrieved again. Ideally this should be done in a more generic
+ // location. We'll also use a clone to avoid modifying the original instance.
+ $term = clone $term;
+ $term->syncOriginal();
+
$this->writeItemToDisk($term);
foreach ($term->localizations() as $item) {
@@ -197,6 +210,21 @@ public function save($term)
}
}
+ public function delete($term)
+ {
+ $this->deleteItemFromDisk($term);
+
+ foreach ($term->localizations() as $item) {
+ $key = $this->getItemKey($item);
+
+ $this->forgetItem($key);
+
+ $this->forgetPath($key);
+
+ $this->resolveIndexes()->filter->isCached()->each->forgetItem($key);
+ }
+ }
+
protected function getItemFromModifiedPath($path)
{
return parent::getItemFromModifiedPath($path)->localizations()->all();
diff --git a/src/StarterKits/Installer.php b/src/StarterKits/Installer.php
index 34b419de63..eaa856f7b0 100644
--- a/src/StarterKits/Installer.php
+++ b/src/StarterKits/Installer.php
@@ -36,7 +36,7 @@ final class Installer
*
* @param mixed $console
*/
- public function __construct(string $package, $console = null, LicenseManager $licenseManager = null)
+ public function __construct(string $package, $console = null, ?LicenseManager $licenseManager = null)
{
$this->package = $package;
$this->licenseManager = $licenseManager;
@@ -51,7 +51,7 @@ public function __construct(string $package, $console = null, LicenseManager $li
* @param mixed $console
* @return static
*/
- public static function package(string $package, $console = null, LicenseManager $licenseManager = null)
+ public static function package(string $package, $console = null, ?LicenseManager $licenseManager = null)
{
return new self($package, $console, $licenseManager);
}
diff --git a/src/StaticCaching/Cachers/FileCacher.php b/src/StaticCaching/Cachers/FileCacher.php
index ea5efc0c04..11a9d3afa5 100644
--- a/src/StaticCaching/Cachers/FileCacher.php
+++ b/src/StaticCaching/Cachers/FileCacher.php
@@ -237,7 +237,7 @@ public function getNocacheJs(): string
window.livewireScriptConfig.csrf = data.csrf
}
- document.dispatchEvent(new CustomEvent('statamic:nocache.replaced'));
+ document.dispatchEvent(new CustomEvent('statamic:nocache.replaced', { detail: data }));
});
})();
EOT;
diff --git a/src/StaticCaching/Invalidate.php b/src/StaticCaching/Invalidate.php
index c40fdd0476..c22f4f6879 100644
--- a/src/StaticCaching/Invalidate.php
+++ b/src/StaticCaching/Invalidate.php
@@ -9,7 +9,7 @@
use Statamic\Events\BlueprintSaved;
use Statamic\Events\CollectionTreeDeleted;
use Statamic\Events\CollectionTreeSaved;
-use Statamic\Events\EntryDeleted;
+use Statamic\Events\EntryDeleting;
use Statamic\Events\EntrySaved;
use Statamic\Events\FormDeleted;
use Statamic\Events\FormSaved;
@@ -31,7 +31,7 @@ class Invalidate implements ShouldQueue
AssetSaved::class => 'invalidateAsset',
AssetDeleted::class => 'invalidateAsset',
EntrySaved::class => 'invalidateEntry',
- EntryDeleted::class => 'invalidateEntry',
+ EntryDeleting::class => 'invalidateEntry',
TermSaved::class => 'invalidateTerm',
TermDeleted::class => 'invalidateTerm',
GlobalSetSaved::class => 'invalidateGlobalSet',
diff --git a/src/StaticCaching/NoCache/BladeDirective.php b/src/StaticCaching/NoCache/BladeDirective.php
index 5c977d5473..d6cf8cb793 100644
--- a/src/StaticCaching/NoCache/BladeDirective.php
+++ b/src/StaticCaching/NoCache/BladeDirective.php
@@ -14,7 +14,7 @@ public function __construct(Session $nocache)
$this->nocache = $nocache;
}
- public function handle($expression, array $params, array $data = null)
+ public function handle($expression, array $params, ?array $data = null)
{
if (func_num_args() == 2) {
$data = $params;
diff --git a/src/StaticCaching/NoCache/Tags.php b/src/StaticCaching/NoCache/Tags.php
index 90c0a8c721..60a08a7387 100644
--- a/src/StaticCaching/NoCache/Tags.php
+++ b/src/StaticCaching/NoCache/Tags.php
@@ -23,8 +23,11 @@ public function index()
{
if ($this->params->has('select')) {
$fields = $this->params->explode('select');
- } elseif (config('statamic.antlers.version') === 'runtime') {
- $fields = Antlers::identifiers($this->content);
+
+ if (in_array('@auto', $fields)) {
+ $identifiers = Antlers::identifiers($this->content);
+ $fields = array_merge(array_diff($fields, ['@auto']), $identifiers);
+ }
}
return $this
diff --git a/src/Structures/CollectionStructure.php b/src/Structures/CollectionStructure.php
index 8eba9cfe1e..e2716b0335 100644
--- a/src/Structures/CollectionStructure.php
+++ b/src/Structures/CollectionStructure.php
@@ -135,13 +135,15 @@ public function trees()
public function in($site)
{
- $tree = app(CollectionTreeRepository::class)->find($this->collection()->handle(), $site);
+ return Blink::once("collection-structure-tree-{$this->handle()}-{$site}", function () use ($site) {
+ $tree = app(CollectionTreeRepository::class)->find($this->collection()->handle(), $site);
- if (! $tree && $this->existsIn($site)) {
- $tree = $this->makeTree($site);
- }
+ if (! $tree && $this->existsIn($site)) {
+ $tree = $this->makeTree($site);
+ }
- return $tree;
+ return $tree;
+ });
}
public function existsIn($site)
diff --git a/src/Structures/Nav.php b/src/Structures/Nav.php
index bb26b83793..1ee7539f75 100644
--- a/src/Structures/Nav.php
+++ b/src/Structures/Nav.php
@@ -8,6 +8,7 @@
use Statamic\Data\ExistsAsFile;
use Statamic\Events\NavBlueprintFound;
use Statamic\Events\NavDeleted;
+use Statamic\Events\NavDeleting;
use Statamic\Events\NavSaved;
use Statamic\Facades;
use Statamic\Facades\Blink;
@@ -33,6 +34,10 @@ public function save()
public function delete()
{
+ if (NavDeleting::dispatch($this) === false) {
+ return false;
+ }
+
Facades\Nav::delete($this);
NavDeleted::dispatch($this);
diff --git a/src/Structures/StructureRepository.php b/src/Structures/StructureRepository.php
index a30bb1efa9..eaa7bee1f3 100644
--- a/src/Structures/StructureRepository.php
+++ b/src/Structures/StructureRepository.php
@@ -25,7 +25,7 @@ public function find($id): ?Structure
public function findByHandle($handle): ?Structure
{
if (Str::startsWith($handle, 'collection::')) {
- return Collection::find(Str::after($handle, 'collection::'))->structure();
+ return Collection::find(Str::after($handle, 'collection::'))?->structure();
}
return Nav::find($handle);
diff --git a/src/Structures/Tree.php b/src/Structures/Tree.php
index 0f1fe63a73..fafdcd9156 100644
--- a/src/Structures/Tree.php
+++ b/src/Structures/Tree.php
@@ -160,6 +160,7 @@ public function save()
$this->cachedFlattenedPages = null;
Blink::forget('collection-structure-flattened-pages-collection*');
+ Blink::forget('collection-structure-tree*');
$this->repository()->save($this);
@@ -170,6 +171,8 @@ public function save()
public function delete()
{
+ Blink::forget('collection-structure-tree*');
+
$this->repository()->delete($this);
$this->dispatchDeletedEvent();
diff --git a/src/Tags/Children.php b/src/Tags/Children.php
new file mode 100644
index 0000000000..40d1fd6b4c
--- /dev/null
+++ b/src/Tags/Children.php
@@ -0,0 +1,27 @@
+params->put('from', Str::start(Str::after(URL::makeAbsolute(URL::getCurrent()), Site::current()->absoluteUrl()), '/'));
+ $this->params->put('max_depth', 1);
+
+ $collection = $this->params->get('collection', $this->context->value('collection')?->handle());
+
+ return $this->structure("collection::{$collection}");
+ }
+}
diff --git a/src/Tags/Concerns/QueriesScopes.php b/src/Tags/Concerns/QueriesScopes.php
index f6b5c21bb0..400981cd7a 100644
--- a/src/Tags/Concerns/QueriesScopes.php
+++ b/src/Tags/Concerns/QueriesScopes.php
@@ -2,6 +2,7 @@
namespace Statamic\Tags\Concerns;
+use Statamic\Facades\Scope;
use Statamic\Support\Arr;
trait QueriesScopes
@@ -10,11 +11,10 @@ public function queryScopes($query)
{
$this->parseQueryScopes()
->map(function ($handle) {
- return app('statamic.scopes')->get($handle);
+ return Scope::find($handle);
})
->filter()
- ->each(function ($class) use ($query) {
- $scope = app($class);
+ ->each(function ($scope) use ($query) {
$scope->apply($query, $this->params);
});
}
diff --git a/src/Tags/Glide.php b/src/Tags/Glide.php
index 7fae863889..bfd584c92a 100644
--- a/src/Tags/Glide.php
+++ b/src/Tags/Glide.php
@@ -132,20 +132,24 @@ public function generate($items = null)
$items = is_iterable($items) ? collect($items) : collect([$items]);
return $items->map(function ($item) {
- $data = ['url' => $this->generateGlideUrl($item)];
-
- if ($this->isValidExtension($item)) {
- $path = $this->generateImage($item);
- $attrs = Attributes::from(GlideManager::cacheDisk(), $path);
- $data = array_merge($data, $attrs);
- }
-
- if ($item instanceof Augmentable) {
- $data = array_merge($item->toAugmentedArray(), $data);
+ try {
+ $data = ['url' => $this->generateGlideUrl($item)];
+
+ if ($this->isValidExtension($item)) {
+ $path = $this->generateImage($item);
+ $attrs = Attributes::from(GlideManager::cacheDisk(), $path);
+ $data = array_merge($data, $attrs);
+ }
+
+ if ($item instanceof Augmentable) {
+ $data = array_merge($item->toAugmentedArray(), $data);
+ }
+
+ return $data;
+ } catch (\Exception $e) {
+ \Log::error($e->getMessage());
}
-
- return $data;
- })->all();
+ })->filter()->all();
}
/**
diff --git a/src/Tags/Nav.php b/src/Tags/Nav.php
index 7d1d22085b..ae89d1ca1c 100644
--- a/src/Tags/Nav.php
+++ b/src/Tags/Nav.php
@@ -2,6 +2,7 @@
namespace Statamic\Tags;
+use Statamic\Contracts\Taxonomies\Taxonomy;
use Statamic\Facades\Data;
use Statamic\Facades\Site;
use Statamic\Facades\URL;
@@ -45,11 +46,13 @@ public function breadcrumbs()
$this->content = trim($this->content);
}
- $crumbs = $crumbs->values()->map(function ($crumb) {
- $crumb->setSupplement('is_current', URL::getCurrent() === $crumb->urlWithoutRedirect());
+ $crumbs = $crumbs->values()
+ ->reject(fn ($crumb) => $crumb instanceof Taxonomy && ! view()->exists($crumb->template()))
+ ->map(function ($crumb) {
+ $crumb->setSupplement('is_current', URL::getCurrent() === $crumb->urlWithoutRedirect());
- return $crumb;
- });
+ return $crumb;
+ });
if (! $this->parser) {
return $crumbs;
diff --git a/src/Taxonomies/AugmentedTerm.php b/src/Taxonomies/AugmentedTerm.php
index 3003ba8046..26cfe1a457 100644
--- a/src/Taxonomies/AugmentedTerm.php
+++ b/src/Taxonomies/AugmentedTerm.php
@@ -33,6 +33,7 @@ private function commonKeys()
'taxonomy',
'edit_url',
'locale',
+ 'collection',
'updated_at',
'updated_by',
];
diff --git a/src/Taxonomies/LocalizedTerm.php b/src/Taxonomies/LocalizedTerm.php
index 6ba6e36ad7..b1ca9fd1b5 100644
--- a/src/Taxonomies/LocalizedTerm.php
+++ b/src/Taxonomies/LocalizedTerm.php
@@ -372,13 +372,17 @@ public function toResponse($request)
public function template($template = null)
{
if (func_num_args() === 0) {
- $defaultTemplate = $this->taxonomyHandle().'.show';
+ if ($template = $this->get('template')) {
+ return $template;
+ }
+
+ $template = $this->taxonomy()->termTemplate();
if ($collection = $this->collection()) {
- $defaultTemplate = $collection->handle().'.'.$defaultTemplate;
+ $template = $collection->handle().'.'.$template;
}
- return $this->get('template', $defaultTemplate);
+ return $template;
}
return $this->set('template', $template);
@@ -387,7 +391,7 @@ public function template($template = null)
public function layout($layout = null)
{
if (func_num_args() === 0) {
- return $this->get('layout', 'layout');
+ return $this->get('layout') ?? $this->taxonomy()->layout();
}
return $this->set('layout', $layout);
@@ -412,6 +416,11 @@ public function newAugmentedInstance(): Augmented
// ])->all();
// }
+ public function saveQuietly()
+ {
+ return $this->term->saveQuietly();
+ }
+
public function save()
{
return $this->term->save();
diff --git a/src/Taxonomies/Taxonomy.php b/src/Taxonomies/Taxonomy.php
index cc0be55e22..9b689730de 100644
--- a/src/Taxonomies/Taxonomy.php
+++ b/src/Taxonomies/Taxonomy.php
@@ -14,6 +14,7 @@
use Statamic\Events\TaxonomyCreated;
use Statamic\Events\TaxonomyCreating;
use Statamic\Events\TaxonomyDeleted;
+use Statamic\Events\TaxonomyDeleting;
use Statamic\Events\TaxonomySaved;
use Statamic\Events\TaxonomySaving;
use Statamic\Events\TermBlueprintFound;
@@ -43,6 +44,9 @@ class Taxonomy implements Arrayable, ArrayAccess, AugmentableContract, Contract,
protected $revisions = false;
protected $searchIndex;
protected $previewTargets = [];
+ protected $template;
+ protected $termTemplate;
+ protected $layout;
protected $afterSaveCallbacks = [];
protected $withEvents = true;
@@ -232,6 +236,10 @@ public function save()
public function delete()
{
+ if (TaxonomyDeleting::dispatch($this) === false) {
+ return false;
+ }
+
$this->queryTerms()->get()->each->delete();
Facades\Taxonomy::delete($this);
@@ -254,6 +262,9 @@ public function fileData()
'title' => $this->title,
'blueprints' => $this->blueprints,
'preview_targets' => $this->previewTargetsForFile(),
+ 'template' => $this->template,
+ 'term_template' => $this->termTemplate,
+ 'layout' => $this->layout,
];
if (Site::hasMultiple()) {
@@ -359,20 +370,50 @@ public function get($key, $fallback = null)
return $fallback;
}
- public function template()
+ public function termTemplate($termTemplate = null)
{
- $template = $this->handle().'.index';
+ return $this
+ ->fluentlyGetOrSet('termTemplate')
+ ->getter(function ($termTemplate) {
+ if ($termTemplate ?? false) {
+ return $termTemplate;
+ }
- if ($collection = $this->collection()) {
- $template = $collection->handle().'.'.$template;
- }
+ $termTemplate = $this->handle().'.show';
- return $template;
+ return $termTemplate;
+ })
+ ->args(func_get_args());
}
- public function layout()
+ public function template($template = null)
{
- return 'layout';
+ return $this
+ ->fluentlyGetOrSet('template')
+ ->getter(function ($template) {
+ if ($template ?? false) {
+ return $template;
+ }
+
+ $template = $this->handle().'.index';
+
+ if ($collection = $this->collection()) {
+ $template = $collection->handle().'.'.$template;
+ }
+
+ return $template;
+ })
+ ->args(func_get_args());
+ }
+
+ public function layout($layout = null)
+ {
+ return $this
+ ->fluentlyGetOrSet('layout')
+ ->getter(function ($layout) {
+ return $layout ?? 'layout';
+ })
+ ->args(func_get_args());
}
public function searchIndex($index = null)
diff --git a/src/Taxonomies/Term.php b/src/Taxonomies/Term.php
index 84ca6bea55..6ff5b264cf 100644
--- a/src/Taxonomies/Term.php
+++ b/src/Taxonomies/Term.php
@@ -9,6 +9,7 @@
use Statamic\Events\TermCreated;
use Statamic\Events\TermCreating;
use Statamic\Events\TermDeleted;
+use Statamic\Events\TermDeleting;
use Statamic\Events\TermSaved;
use Statamic\Events\TermSaving;
use Statamic\Facades;
@@ -242,6 +243,10 @@ public function save()
public function delete()
{
+ if (TermDeleting::dispatch($this) === false) {
+ return false;
+ }
+
Facades\Term::delete($this);
TermDeleted::dispatch($this);
diff --git a/src/View/Antlers/Language/Nodes/AntlersNode.php b/src/View/Antlers/Language/Nodes/AntlersNode.php
index 11adedd51a..86241e6901 100644
--- a/src/View/Antlers/Language/Nodes/AntlersNode.php
+++ b/src/View/Antlers/Language/Nodes/AntlersNode.php
@@ -474,7 +474,10 @@ public function getSingleParameterValue(ParameterNode $param, NodeProcessor $pro
public function reduceParameterInterpolations(ParameterNode $param, NodeProcessor $processor, $mutateVar, $data)
{
if ($param->parent != null && ! empty($param->interpolations)) {
- foreach ($param->interpolations as $interpolationVar) {
+ $parameterInterpolations = $param->interpolations;
+ rsort($parameterInterpolations);
+
+ foreach ($parameterInterpolations as $interpolationVar) {
if (array_key_exists($interpolationVar, $param->parent->processedInterpolationRegions)) {
$interpolationResult = $processor->cloneProcessor()
->setData($data)
diff --git a/src/View/Antlers/Language/Parser/AntlersNodeParser.php b/src/View/Antlers/Language/Parser/AntlersNodeParser.php
index 6709b7ba4f..b7e83abefe 100644
--- a/src/View/Antlers/Language/Parser/AntlersNodeParser.php
+++ b/src/View/Antlers/Language/Parser/AntlersNodeParser.php
@@ -243,8 +243,16 @@ public function parseNode(AntlersNode $node)
$node->isClosingTag = $this->canBeClosingTag($node);
+ $lexerContent = $node->getContent();
+
+ if ($node->name->name == 'if' || $node->name->name == 'unless' || $node->name->name == 'elseif') {
+ if (mb_strlen(trim($lexerContent)) > 0) {
+ $lexerContent = '('.$lexerContent.')';
+ }
+ }
+
// Need to run node type analysis here before the runtime node step.
- $runtimeNodes = $this->lexer->tokenize($node, $node->getContent());
+ $runtimeNodes = $this->lexer->tokenize($node, $lexerContent);
$node->runtimeNodes = $runtimeNodes;
diff --git a/src/View/Antlers/Language/Parser/DocumentParser.php b/src/View/Antlers/Language/Parser/DocumentParser.php
index 09721759e4..c001ffb364 100644
--- a/src/View/Antlers/Language/Parser/DocumentParser.php
+++ b/src/View/Antlers/Language/Parser/DocumentParser.php
@@ -914,6 +914,12 @@ private function scanToEndOfInterpolatedRegion()
$varContent = $this->interpolatedCollisions[$content];
}
+ // Forcefully rotate the initial int_ to int0 to reduce the chance of string processing collisions.
+ if ($varContent == 'int_') {
+ $varContent = 'int0';
+ $this->interpolationRegions['int_'] = -1;
+ }
+
$newLen = mb_strlen($varContent);
$origLen = mb_strlen($content);
diff --git a/src/View/Antlers/Language/Runtime/Sandbox/Environment.php b/src/View/Antlers/Language/Runtime/Sandbox/Environment.php
index 01c6d35d5d..1be0064e03 100644
--- a/src/View/Antlers/Language/Runtime/Sandbox/Environment.php
+++ b/src/View/Antlers/Language/Runtime/Sandbox/Environment.php
@@ -1353,7 +1353,7 @@ private function adjustValue($value, $originalNode)
}
}
- if (! empty($this->interpolationReplacements)) {
+ if (! empty($this->interpolationReplacements) && is_string($value)) {
if (Str::contains($value, $this->interpolationKeys)) {
$value = strtr($value, $this->interpolationReplacements);
}
diff --git a/tests/Antlers/Parser/ConditionalNodesTest.php b/tests/Antlers/Parser/ConditionalNodesTest.php
index 607c777156..2065cab5ca 100644
--- a/tests/Antlers/Parser/ConditionalNodesTest.php
+++ b/tests/Antlers/Parser/ConditionalNodesTest.php
@@ -149,7 +149,8 @@ public function test_conditions_do_not_get_parsed_as_modifiers()
/** @var AntlersNode $firstNode */
$firstNode = $nodes[0];
- $this->assertCount(7, $firstNode->runtimeNodes);
+ // The () are added automatically.
+ $this->assertCount(9, $firstNode->runtimeNodes);
// Parse the text that would produce the 7 runtime nodes above.
$runtimeNodes = $this->getParsedRuntimeNodes("{{ is_small_article || collection:handle == 'vacancies' }}");
diff --git a/tests/Antlers/Runtime/ConditionLogicTest.php b/tests/Antlers/Runtime/ConditionLogicTest.php
index e4089b94a1..805d026257 100644
--- a/tests/Antlers/Runtime/ConditionLogicTest.php
+++ b/tests/Antlers/Runtime/ConditionLogicTest.php
@@ -3,8 +3,11 @@
namespace Tests\Antlers\Runtime;
use Facades\Tests\Factories\EntryFactory;
+use Statamic\Entries\Collection;
+use Statamic\Fields\Field;
use Statamic\Fields\LabeledValue;
use Statamic\Fields\Value;
+use Statamic\Fieldtypes\Code;
use Statamic\Fieldtypes\Select;
use Statamic\Support\Arr;
use Statamic\Tags\Tags;
@@ -13,9 +16,14 @@
use Statamic\View\Cascade;
use Tests\Antlers\Fixtures\Addon\Tags\VarTest;
use Tests\Antlers\ParserTestCase;
+use Tests\FakesViews;
+use Tests\PreventSavingStacheItemsToDisk;
class ConditionLogicTest extends ParserTestCase
{
+ use FakesViews;
+ use PreventSavingStacheItemsToDisk;
+
public function test_negation_following_or_is_evaluated()
{
$template = '{{ if !first && first_row_headers || !first_row_headers }}yes{{ else }}no{{ /if }}';
@@ -769,4 +777,63 @@ public function test_uppercase_logical_keywords_in_conditions()
$this->assertSame('No', $this->renderString($template));
}
+
+ public function test_arrayable_strings_inside_conditions_used_with_modifiers()
+ {
+ $code = new Code();
+ $field = new Field('code_field', [
+ 'type' => 'code',
+ 'antlers' => false,
+ ]);
+
+ $code->setField($field);
+ $value = new Value('Oh hai, mark.', 'code_field', $code);
+
+ $template = <<<'EOT'
+{{ partial:test :code="code_field" }}
+EOT;
+
+ $this->withFakeViews();
+ $this->viewShouldReturnRaw('test', <<<'EOT'
+{{ if code | starts_with('
') }}
+Yes{{ else }}
+No{{ /if }}
+EOT
+
+ );
+
+ $this->assertSame('No', trim($this->renderString($template, ['code_field' => $value], true)));
+
+ $this->viewShouldReturnRaw('test', <<<'EOT'
+{{ if code | starts_with('Oh hai, mark.') }}
+Yes{{ else }}
+No{{ /if }}
+EOT
+
+ );
+
+ $this->assertSame('Yes', trim($this->renderString($template, ['code_field' => $value], true)));
+ }
+
+ public function test_conditions_with_objects_inside_interpolations_dont_trigger_string_errors()
+ {
+ $collection = Collection::make('blog')->routes('{slug}')->save();
+
+ $template = <<<'EOT'
+{{ if items | where('collection', {collection}) }}
+Yes
+{{ /if }}
+EOT;
+
+ $data = [
+ 'items' => [
+ [
+ 'collection' => 'blog',
+ ],
+ ],
+ 'collection' => $collection,
+ ];
+
+ $this->assertSame('Yes', trim($this->renderString($template, $data, true)));
+ }
}
diff --git a/tests/Antlers/Runtime/ParametersTest.php b/tests/Antlers/Runtime/ParametersTest.php
index f79c04832e..9e59263341 100644
--- a/tests/Antlers/Runtime/ParametersTest.php
+++ b/tests/Antlers/Runtime/ParametersTest.php
@@ -308,4 +308,30 @@ public function index()
$this->assertSame('0:123', $this->renderString($template, [], true));
}
+
+ public function test_short_tag_parameters_do_not_cause_collisions()
+ {
+ (new class extends Tags
+ {
+ protected static $handle = 'test';
+
+ public function index()
+ {
+ return $this->params->get('param').':'.$this->params->get('param_two');
+ }
+ })::register();
+
+ $template = <<<'EOT'
+{{ test param="{id}" param_two="{other:id}" }}
+EOT;
+
+ $result = $this->renderString($template, [
+ 'id' => '123',
+ 'other' => [
+ 'id' => '456',
+ ],
+ ], true);
+
+ $this->assertSame('123:456', $result);
+ }
}
diff --git a/tests/Antlers/Runtime/PhpEnabledTest.php b/tests/Antlers/Runtime/PhpEnabledTest.php
index fbac86d838..4ed5783e92 100644
--- a/tests/Antlers/Runtime/PhpEnabledTest.php
+++ b/tests/Antlers/Runtime/PhpEnabledTest.php
@@ -105,7 +105,7 @@ public function augment($value)
return $value;
}
- public function config(string $key = null, $fallback = null)
+ public function config(?string $key = null, $fallback = null)
{
return true;
}
@@ -153,7 +153,7 @@ public function augment($value)
return $value;
}
- public function config(string $key = null, $fallback = null)
+ public function config(?string $key = null, $fallback = null)
{
return true;
}
diff --git a/tests/Assets/AssetTest.php b/tests/Assets/AssetTest.php
index cefd28aa64..dfc7be8ecc 100644
--- a/tests/Assets/AssetTest.php
+++ b/tests/Assets/AssetTest.php
@@ -18,6 +18,7 @@
use Statamic\Assets\PendingMeta;
use Statamic\Assets\ReplacementFile;
use Statamic\Events\AssetDeleted;
+use Statamic\Events\AssetDeleting;
use Statamic\Events\AssetReplaced;
use Statamic\Events\AssetReuploaded;
use Statamic\Events\AssetSaved;
@@ -561,6 +562,7 @@ public function it_checks_if_an_extension_matches()
public function it_gets_the_extension_guessed_extension_and_mime_type()
{
Storage::fake('test');
+ Storage::disk('test')->put('foo.mp4a', '');
Storage::disk('test')->put('.meta/foo.mp4a.yaml', YAML::dump(['mime_type' => 'audio/mp4']));
$container = Facades\AssetContainer::make('test')->disk('test');
@@ -719,6 +721,33 @@ public function it_properly_merges_new_unsaved_data_to_meta()
$this->assertEquals($expectedAfterMerge, $asset->meta());
}
+ /** @test */
+ public function it_does_not_write_to_meta_file_when_asset_does_not_exist()
+ {
+ Storage::fake('test');
+
+ $container = Facades\AssetContainer::make('test')->disk('test');
+ $asset = (new Asset)->container($container)->path('foo/test.txt');
+
+ // No meta file should exist yet...
+ $this->assertFalse(Storage::disk('test')->exists('foo/.meta/test.txt.yaml'));
+
+ // Calling `meta` should return an empty meta array, but not write a meta file...
+ $meta = $asset->meta();
+ $this->assertEquals(['data' => []], $meta);
+ $this->assertFalse(Storage::disk('test')->exists('foo/.meta/test.txt.yaml'));
+ }
+
+ /** @test */
+ public function it_gets_meta_path()
+ {
+ $asset = (new Asset)->container($this->container)->path('test.txt');
+ $this->assertEquals('.meta/test.txt.yaml', $asset->metaPath());
+
+ $asset = (new Asset)->container($this->container)->path('foo/test.txt');
+ $this->assertEquals('foo/.meta/test.txt.yaml', $asset->metaPath());
+ }
+
/** @test */
public function it_generates_meta_on_demand_if_it_doesnt_exist()
{
@@ -1387,6 +1416,7 @@ public function it_gets_dimensions_for_svgs()
public function it_gets_no_ratio_when_height_is_zero()
{
Storage::fake('test');
+ Storage::disk('test')->put('image.jpg', '');
Storage::disk('test')->put('.meta/image.jpg.yaml', YAML::dump(['width' => '30', 'height' => '0']));
$container = Facades\AssetContainer::make('test')->disk('test');
@@ -2152,6 +2182,7 @@ public function it_syncs_original_state_with_no_data()
/** @test */
public function it_syncs_original_state_with_no_data_but_with_data_in_meta()
{
+ Storage::disk('test')->put('path/to/test.txt', '');
Storage::disk('test')->put('path/to/.meta/test.txt.yaml', "data:\n foo: bar");
$asset = (new Asset)->container($this->container)->path('path/to/test.txt');
@@ -2180,6 +2211,7 @@ public function it_syncs_original_state_with_no_data_but_with_data_in_meta()
/** @test */
public function it_syncs_original_state_with_data()
{
+ Storage::disk('test')->put('path/to/test.txt', '');
$yaml = <<<'YAML'
data:
alfa: bravo
@@ -2221,6 +2253,7 @@ public function it_syncs_original_state_with_data()
/** @test */
public function it_resolves_pending_original_meta_values_when_hydrating()
{
+ Storage::disk('test')->put('path/to/test.txt', '');
$yaml = <<<'YAML'
data:
alfa: bravo
@@ -2325,4 +2358,43 @@ private function fakeEventWithMacros()
$mock->shouldReceive('forgetListener');
Event::swap($mock);
}
+
+ /** @test */
+ public function it_fires_a_deleting_event()
+ {
+ Event::fake();
+
+ $container = Facades\AssetContainer::make('test')->disk('test');
+ Facades\AssetContainer::shouldReceive('findByHandle')->with('test')->andReturn($container);
+ Facades\AssetContainer::shouldReceive('find')->with('test')->andReturn($container);
+
+ Storage::disk('test')->put('foo/test.txt', '');
+ $asset = (new Asset)->container('test')->path('foo/test.txt');
+
+ $asset->delete();
+
+ Event::assertDispatched(AssetDeleting::class, function ($event) use ($asset) {
+ return $event->asset === $asset;
+ });
+ }
+
+ /** @test */
+ public function it_does_not_delete_when_a_deleting_event_returns_false()
+ {
+ Facades\Asset::spy();
+ Event::fake([AssetDeleted::class]);
+
+ Event::listen(AssetDeleting::class, function () {
+ return false;
+ });
+
+ Storage::disk('test')->put('foo/test.txt', '');
+ $asset = (new Asset)->container($this->container)->path('foo/test.txt');
+
+ $return = $asset->delete();
+
+ $this->assertFalse($return);
+ Facades\Asset::shouldNotHaveReceived('delete');
+ Event::assertNotDispatched(AssetDeleted::class);
+ }
}
diff --git a/tests/Auth/Eloquent/EloquentUserTest.php b/tests/Auth/Eloquent/EloquentUserTest.php
index d1fab7abb0..efb7fde007 100644
--- a/tests/Auth/Eloquent/EloquentUserTest.php
+++ b/tests/Auth/Eloquent/EloquentUserTest.php
@@ -36,28 +36,28 @@ public function it_gets_roles_already_in_the_db_without_explicitly_assigning_the
{
$roleA = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'a';
}
};
$roleB = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'b';
}
};
$roleC = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'c';
}
};
$roleD = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'd';
}
diff --git a/tests/Auth/PermissibleContractTests.php b/tests/Auth/PermissibleContractTests.php
index d0352cf13c..8dd99ae522 100644
--- a/tests/Auth/PermissibleContractTests.php
+++ b/tests/Auth/PermissibleContractTests.php
@@ -19,28 +19,28 @@ public function it_gets_and_assigns_roles()
{
$roleA = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'a';
}
};
$roleB = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'b';
}
};
$roleC = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'c';
}
};
$roleD = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'd';
}
@@ -80,28 +80,28 @@ public function it_removes_a_role_assignment()
{
$roleA = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'a';
}
};
$roleB = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'b';
}
};
$roleC = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'c';
}
};
$roleD = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'd';
}
@@ -126,14 +126,14 @@ public function it_checks_if_it_has_a_role()
{
$roleA = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'a';
}
};
$roleB = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'b';
}
@@ -229,7 +229,7 @@ public function it_checks_if_it_has_super_permissions_through_roles_and_groups()
{
$superRole = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'superrole';
}
@@ -241,7 +241,7 @@ public function permissions($permissions = null)
};
$nonSuperRole = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'nonsuperrole';
}
diff --git a/tests/Auth/UserGroupTest.php b/tests/Auth/UserGroupTest.php
index c2a7bda215..ecbf76406d 100644
--- a/tests/Auth/UserGroupTest.php
+++ b/tests/Auth/UserGroupTest.php
@@ -81,7 +81,7 @@ public function it_gets_and_sets_roles()
$role = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'test';
}
@@ -98,7 +98,7 @@ public function it_adds_a_role()
{
$role = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'test';
}
@@ -115,7 +115,7 @@ public function it_adds_a_role_using_handle()
{
$role = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'test';
}
@@ -135,21 +135,21 @@ public function it_sets_all_roles()
{
RoleAPI::shouldReceive('find')->with('one')->andReturn($roleOne = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'one';
}
});
RoleAPI::shouldReceive('find')->with('two')->andReturn($roleTwo = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'two';
}
});
RoleAPI::shouldReceive('find')->with('three')->andReturn($roleThree = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'three';
}
@@ -170,7 +170,7 @@ public function it_removes_a_role()
{
$role = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'test';
}
@@ -189,7 +189,7 @@ public function it_removes_a_role_by_handle()
{
$role = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'test';
}
@@ -209,14 +209,14 @@ public function it_checks_if_it_has_a_role()
{
$roleA = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'a';
}
};
$roleB = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'b';
}
@@ -233,14 +233,14 @@ public function it_checks_if_it_has_a_role_by_handle()
{
$roleA = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'a';
}
};
$roleB = new class extends Role
{
- public function handle(string $handle = null)
+ public function handle(?string $handle = null)
{
return 'b';
}
diff --git a/tests/Data/Entries/CollectionTest.php b/tests/Data/Entries/CollectionTest.php
index 8ea8d618b3..c068438712 100644
--- a/tests/Data/Entries/CollectionTest.php
+++ b/tests/Data/Entries/CollectionTest.php
@@ -3,7 +3,6 @@
namespace Tests\Data\Entries;
use Facades\Statamic\Fields\BlueprintRepository;
-use Facades\Tests\Factories\EntryFactory;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Facades\Event;
use Statamic\Contracts\Data\Augmentable;
@@ -11,6 +10,8 @@
use Statamic\Entries\Collection;
use Statamic\Events\CollectionCreated;
use Statamic\Events\CollectionCreating;
+use Statamic\Events\CollectionDeleted;
+use Statamic\Events\CollectionDeleting;
use Statamic\Events\CollectionSaved;
use Statamic\Events\CollectionSaving;
use Statamic\Events\EntryBlueprintFound;
@@ -723,15 +724,13 @@ public function it_gets_the_handle_when_casting_to_a_string()
/** @test */
public function it_augments()
{
- $mountedEntry = EntryFactory::collection('pages')->id('blog')->slug('blog')->data(['title' => 'Blog'])->create();
-
- $collection = (new Collection)->mount('blog')->handle('test');
+ $collection = (new Collection)->handle('test');
$this->assertInstanceof(Augmentable::class, $collection);
- $this->assertCount(3, $augmentedArray = $collection->toAugmentedArray());
- $this->assertEquals('Test', $augmentedArray['title']);
- $this->assertEquals('test', $augmentedArray['handle']);
- $this->assertEquals($mountedEntry, $augmentedArray['mount']->value());
+ $this->assertEquals([
+ 'title' => 'Test',
+ 'handle' => 'test',
+ ], $collection->toAugmentedArray());
}
/** @test */
@@ -965,4 +964,37 @@ public function it_cannot_view_collections_from_sites_that_the_user_is_not_autho
$this->assertTrue($user->can('view', $collection2));
$this->assertFalse($user->can('view', $collection3));
}
+
+ /** @test */
+ public function it_fires_a_deleting_event()
+ {
+ Event::fake();
+
+ $collection = Facades\Collection::make('test')->save();
+
+ $collection->delete();
+
+ Event::assertDispatched(CollectionDeleting::class, function ($event) use ($collection) {
+ return $event->collection === $collection;
+ });
+ }
+
+ /** @test */
+ public function it_does_not_delete_when_a_deleting_event_returns_false()
+ {
+ Facades\Collection::spy();
+ Event::fake([CollectionDeleted::class]);
+
+ Event::listen(CollectionDeleting::class, function () {
+ return false;
+ });
+
+ $collection = new Collection('test');
+
+ $return = $collection->delete();
+
+ $this->assertFalse($return);
+ Facades\Collection::shouldNotHaveReceived('delete');
+ Event::assertNotDispatched(CollectionDeleted::class);
+ }
}
diff --git a/tests/Data/Entries/EntryQueryBuilderTest.php b/tests/Data/Entries/EntryQueryBuilderTest.php
index 877804622b..ed85feb356 100644
--- a/tests/Data/Entries/EntryQueryBuilderTest.php
+++ b/tests/Data/Entries/EntryQueryBuilderTest.php
@@ -737,4 +737,26 @@ public function likeProvider()
return [$like => [$like, $expected]];
});
}
+
+ /** @test */
+ public function entries_are_found_using_chunk()
+ {
+ $this->createDummyCollectionAndEntries();
+
+ $count = 0;
+ Entry::query()->chunk(2, function ($entries) use (&$count) {
+ $this->assertCount($count++ == 0 ? 2 : 1, $entries);
+ });
+ }
+
+ /** @test */
+ public function entries_are_found_using_lazy()
+ {
+ $this->createDummyCollectionAndEntries();
+
+ $entries = Entry::query()->lazy();
+
+ $this->assertInstanceOf(\Illuminate\Support\LazyCollection::class, $entries);
+ $this->assertCount(3, $entries);
+ }
}
diff --git a/tests/Data/Entries/EntryTest.php b/tests/Data/Entries/EntryTest.php
index 2e38b0ad3c..2f00200244 100644
--- a/tests/Data/Entries/EntryTest.php
+++ b/tests/Data/Entries/EntryTest.php
@@ -2192,6 +2192,7 @@ public function it_gets_preview_targets()
'fr' => 'le-blog/{slug}',
'de' => 'das-blog/{slug}',
]);
+
$collection->save();
$entryEn = (new Entry)->collection($collection)->locale('en')->slug('foo')->date('2014-01-01');
@@ -2229,6 +2230,27 @@ public function it_gets_preview_targets()
['label' => 'Index', 'format' => 'http://preview.com/{locale}/{year}/blog?preview=true', 'url' => 'http://preview.com/de/2016/blog?preview=true'],
['label' => 'Show', 'format' => 'http://preview.com/{locale}/{year}/blog/{slug}?preview=true', 'url' => 'http://preview.com/de/2016/blog/das-foo?preview=true'],
], $entryDe->previewTargets()->all());
+
+ $collection->previewTargets([
+ ['label' => 'url', 'format' => 'http://preview.domain.com/preview?url={url}', 'refresh' => false],
+ ['label' => 'uri', 'format' => 'http://preview.domain.com/preview?uri={uri}', 'refresh' => false],
+ ]);
+ $collection->save();
+
+ $this->assertEquals([
+ ['label' => 'url', 'format' => 'http://preview.domain.com/preview?url={url}', 'url' => 'http://preview.domain.com/preview?url=/blog/foo'],
+ ['label' => 'uri', 'format' => 'http://preview.domain.com/preview?uri={uri}', 'url' => 'http://preview.domain.com/preview?uri=/blog/foo'],
+ ], $entryEn->previewTargets()->all());
+
+ $this->assertEquals([
+ ['label' => 'url', 'format' => 'http://preview.domain.com/preview?url={url}', 'url' => 'http://preview.domain.com/preview?url=/fr/le-blog/le-foo'],
+ ['label' => 'uri', 'format' => 'http://preview.domain.com/preview?uri={uri}', 'url' => 'http://preview.domain.com/preview?uri=/le-blog/le-foo'],
+ ], $entryFr->previewTargets()->all());
+
+ $this->assertEquals([
+ ['label' => 'url', 'format' => 'http://preview.domain.com/preview?url={url}', 'url' => 'http://preview.domain.com/preview?url=/das-blog/das-foo'],
+ ['label' => 'uri', 'format' => 'http://preview.domain.com/preview?uri={uri}', 'url' => 'http://preview.domain.com/preview?uri=/das-blog/das-foo'],
+ ], $entryDe->previewTargets()->all());
}
/** @test */
diff --git a/tests/Data/Globals/GlobalSetTest.php b/tests/Data/Globals/GlobalSetTest.php
index 408d4ad2b1..b4a52c212f 100644
--- a/tests/Data/Globals/GlobalSetTest.php
+++ b/tests/Data/Globals/GlobalSetTest.php
@@ -6,6 +6,8 @@
use Illuminate\Support\Facades\Event;
use Statamic\Events\GlobalSetCreated;
use Statamic\Events\GlobalSetCreating;
+use Statamic\Events\GlobalSetDeleted;
+use Statamic\Events\GlobalSetDeleting;
use Statamic\Events\GlobalSetSaved;
use Statamic\Events\GlobalSetSaving;
use Statamic\Facades\GlobalSet as GlobalSetFacade;
@@ -441,4 +443,37 @@ public function it_cannot_view_global_sets_from_sites_that_the_user_is_not_autho
$this->assertTrue($user->can('view', $set2));
$this->assertFalse($user->can('view', $set3));
}
+
+ /** @test */
+ public function it_fires_a_deleting_event()
+ {
+ Event::fake();
+
+ $set = (new GlobalSet)->title('SEO Settings');
+
+ $set->delete();
+
+ Event::assertDispatched(GlobalSetDeleting::class, function ($event) use ($set) {
+ return $event->globals === $set;
+ });
+ }
+
+ /** @test */
+ public function it_does_not_delete_when_a_deleting_event_returns_false()
+ {
+ GlobalSet::spy();
+ Event::fake([GlobalSetDeleted::class]);
+
+ Event::listen(GlobalSetDeleting::class, function () {
+ return false;
+ });
+
+ $set = (new GlobalSet)->title('SEO Settings');
+
+ $return = $set->delete();
+
+ $this->assertFalse($return);
+ GlobalSet::shouldNotHaveReceived('delete');
+ Event::assertNotDispatched(GlobalSetDeleted::class);
+ }
}
diff --git a/tests/Data/Structures/NavTest.php b/tests/Data/Structures/NavTest.php
index 069c105907..a8184da468 100644
--- a/tests/Data/Structures/NavTest.php
+++ b/tests/Data/Structures/NavTest.php
@@ -4,7 +4,10 @@
use Facades\Statamic\Stache\Repositories\NavTreeRepository;
use Illuminate\Support\Collection as LaravelCollection;
+use Illuminate\Support\Facades\Event;
use Statamic\Contracts\Entries\Collection as StatamicCollection;
+use Statamic\Events\NavDeleted;
+use Statamic\Events\NavDeleting;
use Statamic\Facades;
use Statamic\Facades\Site;
use Statamic\Facades\User;
@@ -211,4 +214,37 @@ public function it_cannot_view_navs_from_sites_that_the_user_is_not_authorized_t
$this->assertTrue($user->can('view', $nav2));
$this->assertFalse($user->can('view', $nav3));
}
+
+ /** @test */
+ public function it_fires_a_deleting_event()
+ {
+ Event::fake();
+
+ $nav = tap(Facades\Nav::make()->handle('test'))->save();
+
+ $nav->delete();
+
+ Event::assertDispatched(NavDeleting::class, function ($event) use ($nav) {
+ return $event->nav === $nav;
+ });
+ }
+
+ /** @test */
+ public function it_does_not_delete_when_a_deleting_event_returns_false()
+ {
+ Facades\Nav::spy();
+ Event::fake([NavDeleted::class]);
+
+ Event::listen(NavDeleting::class, function () {
+ return false;
+ });
+
+ $nav = (new Nav)->handle('test');
+
+ $return = $nav->delete();
+
+ $this->assertFalse($return);
+ Facades\Nav::shouldNotHaveReceived('delete');
+ Event::assertNotDispatched(NavDeleted::class);
+ }
}
diff --git a/tests/Data/Taxonomies/AugmentedTermTest.php b/tests/Data/Taxonomies/AugmentedTermTest.php
index a719cb3ee4..577b0a432f 100644
--- a/tests/Data/Taxonomies/AugmentedTermTest.php
+++ b/tests/Data/Taxonomies/AugmentedTermTest.php
@@ -4,9 +4,11 @@
use Carbon\Carbon;
use Statamic\Contracts\Auth\User as UserContract;
+use Statamic\Contracts\Entries\Collection as CollectionContract;
use Statamic\Contracts\Query\Builder as BuilderContract;
use Statamic\Contracts\Taxonomies\Taxonomy as TaxonomyContract;
use Statamic\Facades\Blueprint;
+use Statamic\Facades\Collection;
use Statamic\Facades\Taxonomy;
use Statamic\Facades\Term;
use Statamic\Facades\User;
@@ -66,6 +68,7 @@ public function it_gets_values()
'locale' => ['type' => 'string', 'value' => 'en'],
'updated_at' => ['type' => Carbon::class, 'value' => '2017-02-03 14:10'],
'updated_by' => ['type' => UserContract::class, 'value' => 'test-user'],
+ 'collection' => ['type' => 'null', 'value' => null],
];
$this->assertAugmentedCorrectly($expectations, $augmented);
@@ -90,4 +93,27 @@ public function supplemented_title_is_used()
$this->assertInstanceOf(Value::class, $title);
$this->assertEquals('Supplemented Title', $title->value());
}
+
+ /** @test */
+ public function collection_is_present_when_set()
+ {
+ $collection = tap(Collection::make('test'))->save();
+ tap(Taxonomy::make('test'))->save();
+
+ $term = Term::make()
+ ->taxonomy('test')
+ ->blueprint('test')
+ ->in('en')
+ ->slug('term-slug')
+ ->data(['title' => 'Actual Title']);
+
+ $augmented = new AugmentedTerm($term);
+
+ $this->assertNull($augmented->get('collection')->value());
+
+ $term->collection($collection);
+
+ $this->assertInstanceOf(CollectionContract::class, $value = $augmented->get('collection')->value());
+ $this->assertEquals($collection->handle(), $value->handle());
+ }
}
diff --git a/tests/Data/Taxonomies/TaxonomyTest.php b/tests/Data/Taxonomies/TaxonomyTest.php
index 6e58b63b70..d7f1b026f5 100644
--- a/tests/Data/Taxonomies/TaxonomyTest.php
+++ b/tests/Data/Taxonomies/TaxonomyTest.php
@@ -8,6 +8,8 @@
use Statamic\Contracts\Entries\Entry as EntryContract;
use Statamic\Events\TaxonomyCreated;
use Statamic\Events\TaxonomyCreating;
+use Statamic\Events\TaxonomyDeleted;
+use Statamic\Events\TaxonomyDeleting;
use Statamic\Events\TaxonomySaved;
use Statamic\Events\TaxonomySaving;
use Statamic\Events\TermBlueprintFound;
@@ -391,6 +393,45 @@ public function if_saving_event_returns_false_the_taxonomy_doesnt_save()
Event::assertNotDispatched(TaxonomySaved::class);
}
+ /** @test */
+ public function it_gets_and_sets_the_layout()
+ {
+ $taxonomy = (new Taxonomy)->handle('tags');
+
+ // defaults to layout
+ $this->assertEquals('layout', $taxonomy->layout());
+
+ // taxonomy level overrides the default
+ $taxonomy->layout('foo');
+ $this->assertEquals('foo', $taxonomy->layout());
+ }
+
+ /** @test */
+ public function it_gets_and_sets_the_template()
+ {
+ $taxonomy = (new Taxonomy)->handle('tags');
+
+ // defaults to taxonomy.index
+ $this->assertEquals('tags.index', $taxonomy->template());
+
+ // taxonomy level overrides the default
+ $taxonomy->template('foo');
+ $this->assertEquals('foo', $taxonomy->template());
+ }
+
+ /** @test */
+ public function it_gets_and_sets_the_term_template()
+ {
+ $taxonomy = (new Taxonomy)->handle('tags');
+
+ // defaults to taxonomy.show
+ $this->assertEquals('tags.show', $taxonomy->termTemplate());
+
+ // taxonomy level overrides the default
+ $taxonomy->termTemplate('foo');
+ $this->assertEquals('foo', $taxonomy->termTemplate());
+ }
+
/** @test */
public function it_cannot_view_taxonomies_from_sites_that_the_user_is_not_authorized_to_see()
{
@@ -430,4 +471,37 @@ public function additionalPreviewTargetProvider()
'through facade' => [true],
];
}
+
+ /** @test */
+ public function it_fires_a_deleting_event()
+ {
+ Event::fake();
+
+ $taxonomy = tap(Facades\Taxonomy::make('test'))->save();
+
+ $taxonomy->delete();
+
+ Event::assertDispatched(TaxonomyDeleting::class, function ($event) use ($taxonomy) {
+ return $event->taxonomy === $taxonomy;
+ });
+ }
+
+ /** @test */
+ public function it_does_not_delete_when_a_deleting_event_returns_false()
+ {
+ Facades\Taxonomy::spy();
+ Event::fake([TaxonomyDeleted::class]);
+
+ Event::listen(TaxonomyDeleting::class, function () {
+ return false;
+ });
+
+ $taxonomy = new Taxonomy('test');
+
+ $return = $taxonomy->delete();
+
+ $this->assertFalse($return);
+ Facades\Taxonomy::shouldNotHaveReceived('delete');
+ Event::assertNotDispatched(TaxonomyDeleted::class);
+ }
}
diff --git a/tests/Data/Taxonomies/TermTest.php b/tests/Data/Taxonomies/TermTest.php
index 26e98b068f..31c6de4e3c 100644
--- a/tests/Data/Taxonomies/TermTest.php
+++ b/tests/Data/Taxonomies/TermTest.php
@@ -8,6 +8,8 @@
use Statamic\Events\TermBlueprintFound;
use Statamic\Events\TermCreated;
use Statamic\Events\TermCreating;
+use Statamic\Events\TermDeleted;
+use Statamic\Events\TermDeleting;
use Statamic\Events\TermSaved;
use Statamic\Events\TermSaving;
use Statamic\Facades;
@@ -313,4 +315,77 @@ public function it_gets_preview_targets()
['label' => 'Show', 'format' => 'http://preview.com/{locale}/tags/{slug}?preview=true', 'url' => 'http://preview.com/de/tags/das-foo?preview=true'],
], $termDe->previewTargets()->all());
}
+
+ /** @test */
+ public function it_gets_and_sets_the_layout()
+ {
+ $taxonomy = tap(Taxonomy::make('tags'))->save();
+ $term = (new Term)->taxonomy('tags');
+
+ // defaults to layout
+ $this->assertEquals('layout', $term->layout());
+
+ // taxonomy level overrides the default
+ $taxonomy->layout('foo');
+ $this->assertEquals('foo', $term->layout());
+
+ // term level overrides the origin
+ $return = $term->layout('baz');
+ $this->assertEquals($term, $return);
+ $this->assertEquals('baz', $term->layout());
+ }
+
+ /** @test */
+ public function it_gets_and_sets_the_template()
+ {
+ $taxonomy = tap(Taxonomy::make('tags'))->save();
+ $term = (new Term)->taxonomy('tags');
+
+ // defaults to taxonomy.show
+ $this->assertEquals('tags.show', $term->template());
+
+ // taxonomy level overrides the default
+ $taxonomy->termTemplate('foo');
+ $this->assertEquals('foo', $term->template());
+
+ // term level overrides the origin
+ $return = $term->template('baz');
+ $this->assertEquals($term, $return);
+ $this->assertEquals('baz', $term->template());
+ }
+
+ /** @test */
+ public function it_fires_a_deleting_event()
+ {
+ Event::fake();
+
+ $taxonomy = tap(Taxonomy::make('tags'))->save();
+ $term = (new Term)->taxonomy('tags');
+
+ $term->delete();
+
+ Event::assertDispatched(TermDeleting::class, function ($event) use ($term) {
+ return $event->term === $term;
+ });
+ }
+
+ /** @test */
+ public function it_does_not_delete_when_a_deleting_event_returns_false()
+ {
+ Facades\Term::spy();
+ Event::fake([TermDeleted::class]);
+
+ Event::listen(TermDeleting::class, function () {
+ return false;
+ });
+
+ $taxonomy = tap(Taxonomy::make('tags'))->save();
+ $term = (new Term)->taxonomy('tags');
+
+ $return = $term->delete();
+
+ $this->assertFalse($return);
+ Facades\Term::shouldNotHaveReceived('delete');
+ Event::assertNotDispatched(TermDeleted::class);
+ }
}
diff --git a/tests/Feature/Assets/ClearAssetGlideCacheTest.php b/tests/Feature/Assets/ClearAssetGlideCacheTest.php
index d8417f2005..b3ab068bf1 100644
--- a/tests/Feature/Assets/ClearAssetGlideCacheTest.php
+++ b/tests/Feature/Assets/ClearAssetGlideCacheTest.php
@@ -9,6 +9,7 @@
use Statamic\Events\AssetReuploaded;
use Statamic\Events\AssetSaved;
use Statamic\Facades\Glide;
+use Statamic\Imaging\PresetGenerator;
use Statamic\Listeners\ClearAssetGlideCache;
use Tests\TestCase;
@@ -22,7 +23,7 @@ public function it_subscribes()
$events->shouldReceive('listen')->with(AssetDeleted::class, [ClearAssetGlideCache::class, 'handleDeleted'])->once();
$events->shouldReceive('listen')->with(AssetSaved::class, [ClearAssetGlideCache::class, 'handleSaved'])->once();
- (new ClearAssetGlideCache)->subscribe($events);
+ app(ClearAssetGlideCache::class)->subscribe($events);
}
/** @test */
@@ -31,7 +32,7 @@ public function it_clears_when_deleting()
$asset = Mockery::mock(Asset::class);
Glide::shouldReceive('clearAsset')->with($asset)->once();
- (new ClearAssetGlideCache)->handleDeleted(new AssetDeleted($asset));
+ app(ClearAssetGlideCache::class)->handleDeleted(new AssetDeleted($asset));
}
/** @test */
@@ -40,7 +41,7 @@ public function it_clears_when_reuploading()
$asset = Mockery::mock(Asset::class);
Glide::shouldReceive('clearAsset')->with($asset)->once();
- (new ClearAssetGlideCache)->handleReuploaded(new AssetReuploaded($asset));
+ app(ClearAssetGlideCache::class)->handleReuploaded(new AssetReuploaded($asset));
}
/** @test */
@@ -49,10 +50,12 @@ public function it_clears_when_focus_is_added()
$asset = Mockery::mock(Asset::class);
$asset->shouldReceive('getOriginal')->with('data.focus')->once()->andReturnNull();
$asset->shouldReceive('get')->with('focus')->once()->andReturn('50-50-1');
+ $asset->shouldReceive('id')->twice()->andReturn('123');
- Glide::shouldReceive('clearAsset')->with($asset)->once();
+ Glide::shouldReceive('clearAsset')->with($asset)->once()->globally()->ordered();
+ $this->mock(PresetGenerator::class)->shouldReceive('generate')->withArgs(fn ($arg1) => $arg1->id() === $asset->id())->once()->globally()->ordered();
- (new ClearAssetGlideCache)->handleSaved(new AssetSaved($asset));
+ app(ClearAssetGlideCache::class)->handleSaved(new AssetSaved($asset));
}
/** @test */
@@ -61,10 +64,12 @@ public function it_clears_when_focus_changes()
$asset = Mockery::mock(Asset::class);
$asset->shouldReceive('getOriginal')->with('data.focus')->once()->andReturn('75-25-1');
$asset->shouldReceive('get')->with('focus')->once()->andReturn('50-50-1');
+ $asset->shouldReceive('id')->twice()->andReturn('123');
- Glide::shouldReceive('clearAsset')->with($asset)->once();
+ Glide::shouldReceive('clearAsset')->with($asset)->once()->globally()->ordered();
+ $this->mock(PresetGenerator::class)->shouldReceive('generate')->withArgs(fn ($arg1) => $arg1->id() === $asset->id())->once()->globally()->ordered();
- (new ClearAssetGlideCache)->handleSaved(new AssetSaved($asset));
+ app(ClearAssetGlideCache::class)->handleSaved(new AssetSaved($asset));
}
/** @test */
@@ -74,9 +79,10 @@ public function it_doesnt_clear_focus_stays_the_same()
$asset->shouldReceive('getOriginal')->with('data.focus')->once()->andReturn('75-25-1');
$asset->shouldReceive('get')->with('focus')->once()->andReturn('75-25-1');
- Glide::shouldReceive('clearAsset')->with($asset)->never();
+ Glide::shouldReceive('clearAsset')->with($asset)->never()->globally()->ordered();
+ $this->mock(PresetGenerator::class)->shouldNotHaveReceived('generate');
- (new ClearAssetGlideCache)->handleSaved(new AssetSaved($asset));
+ app(ClearAssetGlideCache::class)->handleSaved(new AssetSaved($asset));
}
/** @test */
@@ -85,9 +91,11 @@ public function it_clears_when_focus_is_removed()
$asset = Mockery::mock(Asset::class);
$asset->shouldReceive('getOriginal')->with('data.focus')->once()->andReturn('75-25-1');
$asset->shouldReceive('get')->with('focus')->once()->andReturnNull();
+ $asset->shouldReceive('id')->twice()->andReturn('123');
- Glide::shouldReceive('clearAsset')->with($asset)->once();
+ Glide::shouldReceive('clearAsset')->with($asset)->once()->globally()->ordered();
+ $this->mock(PresetGenerator::class)->shouldReceive('generate')->withArgs(fn ($arg1) => $arg1->id() === $asset->id())->once()->globally()->ordered();
- (new ClearAssetGlideCache)->handleSaved(new AssetSaved($asset));
+ app(ClearAssetGlideCache::class)->handleSaved(new AssetSaved($asset));
}
}
diff --git a/tests/Feature/Collections/DeleteCollectionTest.php b/tests/Feature/Collections/DeleteCollectionTest.php
index 6570796940..6d2443feb2 100644
--- a/tests/Feature/Collections/DeleteCollectionTest.php
+++ b/tests/Feature/Collections/DeleteCollectionTest.php
@@ -3,6 +3,9 @@
namespace Tests\Feature\Collections;
use Statamic\Facades\Collection;
+use Statamic\Facades\Entry;
+use Statamic\Facades\Site;
+use Statamic\Facades\Structure;
use Statamic\Facades\User;
use Tests\FakesRoles;
use Tests\PreventSavingStacheItemsToDisk;
@@ -48,4 +51,83 @@ public function it_deletes_the_collection()
$this->assertCount(0, Collection::all());
}
+
+ /** @test */
+ public function it_deletes_the_collection_with_localized_entries()
+ {
+ $this->withoutExceptionHandling();
+
+ Site::setConfig(['sites' => [
+ 'en' => ['url' => '/', 'locale' => 'en_US'],
+ 'fr' => ['url' => '/fr', 'locale' => 'fr_FR'],
+ ]]);
+
+ $this->setTestRoles(['test' => ['access cp', 'configure collections']]);
+ $user = tap(User::make()->assignRole('test'))->save();
+
+ $collection = Collection::make('test')->sites(['en', 'fr'])->save();
+ $this->assertCount(1, Collection::all());
+
+ $entry = tap(Entry::make()->locale('en')->slug('test')->collection('test')->data(['title' => 'Test']))->save();
+ $entry->makeLocalization('fr')->slug('test-fr')->data(['title' => 'Test FR'])->save();
+
+ $this
+ ->actingAs($user)
+ ->delete(cp_route('collections.destroy', $collection->handle()))
+ ->assertOk();
+
+ $this->assertCount(0, Collection::all());
+ }
+
+ /** @test */
+ public function it_deletes_tree_files()
+ {
+ $this->setTestRoles(['test' => ['access cp', 'configure collections']]);
+ $user = tap(User::make()->assignRole('test'))->save();
+
+ $collection = tap(Collection::make('test')->structureContents(['root' => true]))->save();
+
+ Entry::make()->id('a')->slug('a')->collection('test')->data(['title' => 'A'])->save();
+ Entry::make()->id('b')->slug('b')->collection('test')->data(['title' => 'B'])->save();
+ Entry::make()->id('c')->slug('c')->collection('test')->data(['title' => 'C'])->save();
+
+ $collection->structure()->in('en')->tree([['entry' => 'a'], ['entry' => 'b'], ['entry' => 'c']]);
+
+ $this
+ ->actingAs($user)
+ ->delete(cp_route('collections.destroy', $collection->handle()))
+ ->assertOk();
+
+ $this->assertCount(0, Collection::all());
+ $this->assertNull(Structure::find('collection::test'));
+ }
+
+ /** @test */
+ public function it_deletes_tree_files_in_a_multisite()
+ {
+ Site::setConfig(['sites' => [
+ 'en' => ['url' => '/', 'locale' => 'en_US'],
+ 'fr' => ['url' => '/fr', 'locale' => 'fr_FR'],
+ ]]);
+
+ $this->setTestRoles(['test' => ['access cp', 'configure collections']]);
+ $user = tap(User::make()->assignRole('test'))->save();
+
+ $collection = tap(Collection::make('test')->sites(['en', 'fr'])->structureContents(['root' => true]))->save();
+
+ Entry::make()->id('a')->slug('a')->locale('en')->collection('test')->data(['title' => 'A'])->save();
+ Entry::make()->id('b')->slug('b')->locale('en')->collection('test')->data(['title' => 'B'])->save();
+ Entry::make()->id('c')->slug('c')->locale('fr')->collection('test')->data(['title' => 'C'])->save();
+
+ $collection->structure()->in('en')->tree([['entry' => 'a'], ['entry' => 'b']]);
+ $collection->structure()->in('fr')->tree([['entry' => 'c']]);
+
+ $this
+ ->actingAs($user)
+ ->delete(cp_route('collections.destroy', $collection->handle()))
+ ->assertOk();
+
+ $this->assertCount(0, Collection::all());
+ $this->assertNull(Structure::find('collection::test'));
+ }
}
diff --git a/tests/Feature/Fieldsets/ViewFieldsetListingTest.php b/tests/Feature/Fieldsets/ViewFieldsetListingTest.php
index e995677338..7e8401e062 100644
--- a/tests/Feature/Fieldsets/ViewFieldsetListingTest.php
+++ b/tests/Feature/Fieldsets/ViewFieldsetListingTest.php
@@ -54,6 +54,7 @@ public function before($user, $ability, $fieldset)
'edit_url' => 'http://localhost/cp/fields/fieldsets/foo/edit',
'delete_url' => 'http://localhost/cp/fields/fieldsets/foo',
'is_deletable' => true,
+ 'imported_by' => collect(),
],
[
'id' => 'bar',
@@ -63,6 +64,7 @@ public function before($user, $ability, $fieldset)
'edit_url' => 'http://localhost/cp/fields/fieldsets/bar/edit',
'delete_url' => 'http://localhost/cp/fields/fieldsets/bar',
'is_deletable' => true,
+ 'imported_by' => collect(),
],
]),
'Baz' => collect([
@@ -74,6 +76,7 @@ public function before($user, $ability, $fieldset)
'edit_url' => 'http://localhost/cp/fields/fieldsets/baz::foo/edit',
'delete_url' => 'http://localhost/cp/fields/fieldsets/baz::foo',
'is_deletable' => false,
+ 'imported_by' => collect(),
],
[
'id' => 'baz::bar',
@@ -83,6 +86,7 @@ public function before($user, $ability, $fieldset)
'edit_url' => 'http://localhost/cp/fields/fieldsets/baz::bar/edit',
'delete_url' => 'http://localhost/cp/fields/fieldsets/baz::bar',
'is_deletable' => false,
+ 'imported_by' => collect(),
],
]),
]))
diff --git a/tests/Feature/Fieldtypes/FilesTest.php b/tests/Feature/Fieldtypes/FilesTest.php
index 8f3996daea..9b4eee5ea5 100644
--- a/tests/Feature/Fieldtypes/FilesTest.php
+++ b/tests/Feature/Fieldtypes/FilesTest.php
@@ -2,8 +2,8 @@
namespace Tests\Feature\Fieldtypes;
-use Carbon\Carbon;
use Illuminate\Http\UploadedFile;
+use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Storage;
use Statamic\Assets\AssetContainer;
use Statamic\Facades\User;
@@ -45,7 +45,7 @@ public function it_uploads_a_file($container, $isImage, $expectedPath, $expected
? UploadedFile::fake()->image('test.jpg', 50, 75)
: UploadedFile::fake()->create('test.txt');
- Carbon::setTestNow(Carbon::createFromTimestamp(1671484636));
+ Date::setTestNow(Date::createFromTimestamp(1671484636));
$disk = Storage::fake('local');
diff --git a/tests/Fields/BlueprintTest.php b/tests/Fields/BlueprintTest.php
index d9433de4a5..f755b60cff 100644
--- a/tests/Fields/BlueprintTest.php
+++ b/tests/Fields/BlueprintTest.php
@@ -13,6 +13,8 @@
use Statamic\CP\Columns;
use Statamic\Events\BlueprintCreated;
use Statamic\Events\BlueprintCreating;
+use Statamic\Events\BlueprintDeleted;
+use Statamic\Events\BlueprintDeleting;
use Statamic\Events\BlueprintSaved;
use Statamic\Events\BlueprintSaving;
use Statamic\Facades;
@@ -1420,4 +1422,36 @@ public function it_resolves_itself_to_a_queryable_value()
$this->assertInstanceOf(QueryableValue::class, $blueprint);
$this->assertEquals('test', $blueprint->toQueryableValue());
}
+
+ /** @test */
+ public function it_fires_a_deleting_event()
+ {
+ Event::fake();
+
+ $blueprint = (new Blueprint)->setHandle('test');
+
+ $blueprint->delete();
+
+ Event::assertDispatched(BlueprintDeleting::class, function ($event) use ($blueprint) {
+ return $event->blueprint === $blueprint;
+ });
+ }
+
+ /** @test */
+ public function it_does_not_delete_when_a_deleting_event_returns_false()
+ {
+ Facades\Blueprint::spy();
+ Event::fake([BlueprintDeleted::class]);
+
+ Event::listen(BlueprintDeleting::class, function () {
+ return false;
+ });
+
+ $blueprint = (new Blueprint)->setHandle('test');
+ $return = $blueprint->delete();
+
+ $this->assertFalse($return);
+ Facades\Blueprint::shouldNotHaveReceived('delete');
+ Event::assertNotDispatched(BlueprintDeleted::class);
+ }
}
diff --git a/tests/Fields/FieldsTest.php b/tests/Fields/FieldsTest.php
index b7bd128522..995c7a8818 100644
--- a/tests/Fields/FieldsTest.php
+++ b/tests/Fields/FieldsTest.php
@@ -265,7 +265,7 @@ public function it_prefixes_the_handles_of_nested_imported_fieldsets()
public function it_throws_exception_when_trying_to_import_a_non_existent_fieldset()
{
$this->expectException('Exception');
- $this->expectExceptionMessage('Fieldset test_partial not found.');
+ $this->expectExceptionMessage('Fieldset [test_partial] not found');
FieldsetRepository::shouldReceive('find')->with('test_partial')->once()->andReturnNull();
(new Fields)->createFields([
diff --git a/tests/Fields/FieldsetTest.php b/tests/Fields/FieldsetTest.php
index b36f30ab64..f2476aeeb5 100644
--- a/tests/Fields/FieldsetTest.php
+++ b/tests/Fields/FieldsetTest.php
@@ -5,16 +5,39 @@
use Illuminate\Support\Facades\Event;
use Statamic\Events\FieldsetCreated;
use Statamic\Events\FieldsetCreating;
+use Statamic\Events\FieldsetDeleted;
+use Statamic\Events\FieldsetDeleting;
use Statamic\Events\FieldsetSaved;
use Statamic\Events\FieldsetSaving;
+use Statamic\Facades\Blueprint;
+use Statamic\Facades\Collection;
use Statamic\Facades\Fieldset as FieldsetRepository;
use Statamic\Fields\Field;
use Statamic\Fields\Fields;
use Statamic\Fields\Fieldset;
+use Tests\PreventSavingStacheItemsToDisk;
use Tests\TestCase;
class FieldsetTest extends TestCase
{
+ use PreventSavingStacheItemsToDisk;
+
+ private $fieldsets;
+
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->fieldsets = FieldsetRepository::getFacadeRoot();
+ }
+
+ public function tearDown(): void
+ {
+ $this->fieldsets->all()->each->delete();
+
+ parent::tearDown();
+ }
+
/** @test */
public function it_gets_the_handle()
{
@@ -145,6 +168,258 @@ public function gets_a_single_field()
$this->assertNull($fieldset->field('unknown'));
}
+ /** @test */
+ public function gets_blueprints_importing_fieldset()
+ {
+ $fieldset = Fieldset::make('seo')->setContents(['fields' => [
+ ['handle' => 'meta_title', 'field' => ['type' => 'text']],
+ ]])->save();
+
+ tap(Collection::make('one'))->save();
+ $blueprintA = Blueprint::make('one')->setNamespace('collections.one')->setContents([
+ 'tabs' => [
+ 'main' => [
+ 'sections' => [
+ [
+ 'fields' => [
+ ['handle' => 'title', 'field' => ['type' => 'text']],
+ ['handle' => 'slug', 'field' => ['type' => 'slug']],
+ ['import' => 'seo'],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ])->save();
+
+ $importedBy = $fieldset->importedBy();
+
+ $this->assertCount(1, $importedBy['blueprints']);
+ $this->assertEquals($blueprintA->handle(), $importedBy['blueprints']->first()->handle());
+ }
+
+ /** @test */
+ public function gets_blueprints_importing_fieldset_inside_grid()
+ {
+ $fieldset = Fieldset::make('seo')->setContents(['fields' => [
+ ['handle' => 'meta_title', 'field' => ['type' => 'text']],
+ ]])->save();
+
+ tap(Collection::make('one'))->save();
+ $blueprintA = Blueprint::make('one')->setNamespace('collections.one')->setContents([
+ 'tabs' => [
+ 'main' => [
+ 'sections' => [
+ [
+ 'fields' => [
+ ['handle' => 'title', 'field' => ['type' => 'text']],
+ ['handle' => 'slug', 'field' => ['type' => 'slug']],
+ [
+ 'handle' => 'grid',
+ 'field' => [
+ 'type' => 'grid',
+ 'fields' => [
+ ['import' => 'seo'],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ])->save();
+
+ $importedBy = $fieldset->importedBy();
+
+ $this->assertCount(1, $importedBy['blueprints']);
+ $this->assertEquals($blueprintA->handle(), $importedBy['blueprints']->first()->handle());
+ }
+
+ /** @test */
+ public function gets_blueprints_importing_fieldset_inside_replicator()
+ {
+ $fieldset = Fieldset::make('seo')->setContents(['fields' => [
+ ['handle' => 'meta_title', 'field' => ['type' => 'text']],
+ ]])->save();
+
+ tap(Collection::make('one'))->save();
+ $blueprintA = Blueprint::make('one')->setNamespace('collections.one')->setContents([
+ 'tabs' => [
+ 'main' => [
+ 'sections' => [
+ [
+ 'fields' => [
+ ['handle' => 'title', 'field' => ['type' => 'text']],
+ ['handle' => 'slug', 'field' => ['type' => 'slug']],
+ [
+ 'handle' => 'replicator',
+ 'field' => [
+ 'type' => 'replicator',
+ 'sets' => [
+ 'set_group' => [
+ 'sets' => [
+ 'set' => [
+ 'fields' => [
+ ['import' => 'seo'],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ])->save();
+
+ $importedBy = $fieldset->importedBy();
+
+ $this->assertCount(1, $importedBy['blueprints']);
+ $this->assertEquals($blueprintA->handle(), $importedBy['blueprints']->first()->handle());
+ }
+
+ /** @test */
+ public function gets_blueprints_importing_single_field_from_fieldset()
+ {
+ $fieldset = Fieldset::make('seo')->setContents(['fields' => [
+ ['handle' => 'meta_title', 'field' => ['type' => 'text']],
+ ]])->save();
+
+ tap(Collection::make('one'))->save();
+ $blueprintA = Blueprint::make('one')->setNamespace('collections.one')->setContents([
+ 'tabs' => [
+ 'main' => [
+ 'sections' => [
+ [
+ 'fields' => [
+ ['handle' => 'title', 'field' => ['type' => 'text']],
+ ['handle' => 'slug', 'field' => ['type' => 'slug']],
+ ['handle' => 'meta_title', 'field' => 'seo.meta_title'],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ])->save();
+
+ $importedBy = $fieldset->importedBy();
+
+ $this->assertCount(1, $importedBy['blueprints']);
+ $this->assertEquals($blueprintA->handle(), $importedBy['blueprints']->first()->handle());
+ }
+
+ /** @test */
+ public function gets_fieldsets_importing_fieldset()
+ {
+ $fieldset = Fieldset::make('seo')->setContents(['fields' => [
+ ['handle' => 'meta_title', 'field' => ['type' => 'text']],
+ ]])->save();
+
+ $fieldsetA = Fieldset::make('one')
+ ->setContents([
+ 'fields' => [
+ ['import' => 'seo'],
+ ],
+ ])
+ ->save();
+
+ $importedBy = $fieldset->importedBy();
+
+ $this->assertCount(1, $importedBy['fieldsets']);
+ $this->assertEquals($fieldsetA->handle(), $importedBy['fieldsets']->first()->handle());
+ }
+
+ /** @test */
+ public function gets_fieldsets_importing_fieldset_inside_grid()
+ {
+ $fieldset = Fieldset::make('seo')->setContents(['fields' => [
+ ['handle' => 'meta_title', 'field' => ['type' => 'text']],
+ ]])->save();
+
+ $fieldsetA = Fieldset::make('one')
+ ->setContents([
+ 'fields' => [
+ [
+ 'handle' => 'grid',
+ 'field' => [
+ 'type' => 'grid',
+ 'fields' => [
+ ['import' => 'seo'],
+ ],
+ ],
+ ],
+ ],
+ ])
+ ->save();
+
+ $importedBy = $fieldset->importedBy();
+
+ $this->assertCount(1, $importedBy['fieldsets']);
+ $this->assertEquals($fieldsetA->handle(), $importedBy['fieldsets']->first()->handle());
+ }
+
+ /** @test */
+ public function gets_fieldsets_importing_fieldset_inside_replicator()
+ {
+ $fieldset = Fieldset::make('seo')->setContents(['fields' => [
+ ['handle' => 'meta_title', 'field' => ['type' => 'text']],
+ ]])->save();
+
+ $fieldsetA = Fieldset::make('one')
+ ->setContents([
+ 'fields' => [
+ [
+ 'handle' => 'replicator',
+ 'field' => [
+ 'type' => 'replicator',
+ 'sets' => [
+ 'set_group' => [
+ 'sets' => [
+ 'set' => [
+ 'fields' => [
+ ['import' => 'seo'],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ])
+ ->save();
+
+ $importedBy = $fieldset->importedBy();
+
+ $this->assertCount(1, $importedBy['fieldsets']);
+ $this->assertEquals($fieldsetA->handle(), $importedBy['fieldsets']->first()->handle());
+ }
+
+ /** @test */
+ public function gets_fieldsets_importing_single_field_from_fieldset()
+ {
+ $fieldset = Fieldset::make('seo')->setContents(['fields' => [
+ ['handle' => 'meta_title', 'field' => ['type' => 'text']],
+ ]])->save();
+
+ $fieldsetA = Fieldset::make('one')
+ ->setContents([
+ 'fields' => [
+ ['handle' => 'meta_title', 'field' => 'seo.meta_title'],
+ ],
+ ])
+ ->save();
+
+ $importedBy = $fieldset->importedBy();
+
+ $this->assertCount(1, $importedBy['fieldsets']);
+ $this->assertEquals($fieldsetA->handle(), $importedBy['fieldsets']->first()->handle());
+ }
+
/** @test */
public function it_saves_through_the_repository()
{
@@ -251,4 +526,36 @@ public function if_saving_event_returns_false_the_fieldset_doesnt_save()
Event::assertNotDispatched(FieldsetSaved::class);
}
+
+ /** @test */
+ public function it_fires_a_deleting_event()
+ {
+ Event::fake();
+
+ $fieldset = (new Fieldset)->setHandle('test');
+
+ $fieldset->delete();
+
+ Event::assertDispatched(FieldsetDeleting::class, function ($event) use ($fieldset) {
+ return $event->fieldset === $fieldset;
+ });
+ }
+
+ /** @test */
+ public function it_does_not_delete_when_a_deleting_event_returns_false()
+ {
+ FieldsetRepository::spy();
+ Event::fake([FieldsetDeleted::class]);
+
+ Event::listen(FieldsetDeleting::class, function () {
+ return false;
+ });
+
+ $fieldset = (new Fieldset)->setHandle('test');
+ $return = $fieldset->delete();
+
+ $this->assertFalse($return);
+ FieldsetRepository::shouldNotHaveReceived('delete');
+ Event::assertNotDispatched(FieldsetDeleted::class);
+ }
}
diff --git a/tests/Forms/FormTest.php b/tests/Forms/FormTest.php
index e7fae763f1..3796f3a709 100644
--- a/tests/Forms/FormTest.php
+++ b/tests/Forms/FormTest.php
@@ -6,6 +6,8 @@
use Illuminate\Support\Facades\Event;
use Statamic\Events\FormCreated;
use Statamic\Events\FormCreating;
+use Statamic\Events\FormDeleted;
+use Statamic\Events\FormDeleting;
use Statamic\Events\FormSaved;
use Statamic\Events\FormSaving;
use Statamic\Facades\Form;
@@ -199,4 +201,37 @@ public function it_can_get_action_url()
$this->assertEquals($route, $form->actionUrl());
}
+
+ /** @test */
+ public function it_fires_a_deleting_event()
+ {
+ Event::fake();
+
+ $form = Form::make('contact_us');
+
+ $form->delete();
+
+ Event::assertDispatched(FormDeleting::class, function ($event) use ($form) {
+ return $event->form === $form;
+ });
+ }
+
+ /** @test */
+ public function it_does_not_delete_when_a_deleting_event_returns_false()
+ {
+ Form::spy();
+ Event::fake([FormDeleted::class]);
+
+ Event::listen(FormDeleting::class, function () {
+ return false;
+ });
+
+ $form = new \Statamic\Forms\Form('test');
+
+ $return = $form->delete();
+
+ $this->assertFalse($return);
+ Form::shouldNotHaveReceived('delete');
+ Event::assertNotDispatched(FormDeleted::class);
+ }
}
diff --git a/tests/FrontendTest.php b/tests/FrontendTest.php
index 3378c4e5d6..f227211f5b 100644
--- a/tests/FrontendTest.php
+++ b/tests/FrontendTest.php
@@ -7,6 +7,7 @@
use Facades\Tests\Factories\EntryFactory;
use Illuminate\Http\Response;
use Illuminate\Support\Carbon;
+use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Event;
use Statamic\Events\ResponseCreated;
use Statamic\Facades\Blueprint;
@@ -664,7 +665,7 @@ public function it_sets_the_translation_locale_based_on_site()
public function it_sets_the_carbon_to_string_format()
{
config(['statamic.system.date_format' => 'd/m/Y']);
- Carbon::setTestNow('October 21st, 2022');
+ Date::setTestNow('October 21st, 2022');
$this->viewShouldReturnRaw('layout', '{{ template_content }}');
$this->viewShouldReturnRaw('some_template', '
{{ now }}
');
$this->makeCollection()->save();
@@ -737,8 +738,8 @@ public function index()
private function assertDefaultCarbonFormat()
{
$this->assertEquals(
- Carbon::now()->format(Carbon::DEFAULT_TO_STRING_FORMAT),
- (string) Carbon::now(),
+ Date::now()->format(Carbon::DEFAULT_TO_STRING_FORMAT),
+ (string) Date::now(),
'Carbon was not formatted using the default format.'
);
}
diff --git a/tests/Git/GitEventTest.php b/tests/Git/GitEventTest.php
index 974bd2ef9a..22568caec8 100644
--- a/tests/Git/GitEventTest.php
+++ b/tests/Git/GitEventTest.php
@@ -32,7 +32,7 @@ public function setUp(): void
Config::set('statamic.git.enabled', true);
$this->actingAs(
- User::make()
+ $user = User::make()
->id('chewbacca')
->email('chew@bacca.com')
->data(['name' => 'Chewbacca'])
@@ -47,11 +47,13 @@ public function setUp(): void
Storage::fake('test');
Git::shouldReceive('statuses');
+ Git::shouldReceive('as')->with($user)->andReturnSelf();
}
/** @test */
public function it_doesnt_commit_when_git_is_disabled()
{
+ Git::shouldReceive('as')->never();
Git::shouldReceive('dispatchCommit')->with('Collection saved')->never();
Git::shouldReceive('dispatchCommit')->with('Collection deleted')->never();
@@ -66,6 +68,7 @@ public function it_doesnt_commit_when_git_is_disabled()
/** @test */
public function it_doesnt_commit_when_automatic_is_disabled()
{
+ Git::shouldReceive('as')->never();
Git::shouldReceive('dispatchCommit')->with('Collection saved')->never();
Git::shouldReceive('dispatchCommit')->with('Collection deleted')->never();
@@ -80,6 +83,7 @@ public function it_doesnt_commit_when_automatic_is_disabled()
/** @test */
public function it_doesnt_commit_ignored_events()
{
+ Git::shouldReceive('as')->never();
Git::shouldReceive('dispatchCommit')->with('Collection saved')->never();
Git::shouldReceive('dispatchCommit')->with('Collection deleted')->once();
@@ -96,6 +100,7 @@ public function it_doesnt_commit_ignored_events()
/** @test */
public function it_doesnt_commit_when_event_subscriber_is_disabled()
{
+ Git::shouldReceive('as')->never();
Git::shouldReceive('dispatchCommit')->with('Collection saved')->never();
Git::shouldReceive('dispatchCommit')->with('Collection deleted')->once();
diff --git a/tests/Git/GitTest.php b/tests/Git/GitTest.php
index d74fddd040..59eeaabaeb 100644
--- a/tests/Git/GitTest.php
+++ b/tests/Git/GitTest.php
@@ -181,6 +181,16 @@ public function it_gets_git_user_info()
$this->assertEquals('Chewy', Git::gitUserName());
$this->assertEquals('chew@bacca.com', Git::gitUserEmail());
+ $han = User::make()
+ ->email('han@solo.com')
+ ->data(['name' => 'Han Solo'])
+ ->makeSuper();
+
+ $this->assertEquals('Han Solo', Git::as($han)->gitUserName());
+ $this->assertEquals('han@solo.com', Git::as($han)->gitUserEmail());
+ $this->assertEquals('Chewy', Git::gitUserName());
+ $this->assertEquals('chew@bacca.com', Git::gitUserEmail());
+
Config::set('statamic.git.use_authenticated', false);
$this->assertEquals('Spock', Git::gitUserName());
diff --git a/tests/Modifiers/AddQueryParamTest.php b/tests/Modifiers/AddQueryParamTest.php
index 07eb5fd5b1..1f95968224 100644
--- a/tests/Modifiers/AddQueryParamTest.php
+++ b/tests/Modifiers/AddQueryParamTest.php
@@ -26,7 +26,7 @@ public function it_does_nothing_if_no_parameters_are_passed()
$this->assertSame("{$this->baseUrl}#test", $this->modify("{$this->baseUrl}#test"));
}
- private function modify(string $url, array $queryParam = null)
+ private function modify(string $url, ?array $queryParam = null)
{
if (is_null($queryParam)) {
return Modify::value($url)->addQueryParam()->fetch();
diff --git a/tests/Modifiers/RemoveQueryParamTest.php b/tests/Modifiers/RemoveQueryParamTest.php
index 575fb34119..83174366ab 100644
--- a/tests/Modifiers/RemoveQueryParamTest.php
+++ b/tests/Modifiers/RemoveQueryParamTest.php
@@ -33,7 +33,7 @@ public function it_does_nothing_if_no_parameters_are_passed()
$this->assertSame($this->baseUrl, $this->modify($this->baseUrl));
}
- private function modify(string $url, string $queryParamKey = null)
+ private function modify(string $url, ?string $queryParamKey = null)
{
if (is_null($queryParamKey)) {
return Modify::value($url)->removeQueryParam()->fetch();
diff --git a/tests/Modifiers/SetQueryParamTest.php b/tests/Modifiers/SetQueryParamTest.php
index 7b7307ace3..4e6d05fc45 100644
--- a/tests/Modifiers/SetQueryParamTest.php
+++ b/tests/Modifiers/SetQueryParamTest.php
@@ -63,7 +63,7 @@ public function it_does_nothing_if_no_parameters_are_passed()
$this->assertSame($this->baseUrl, $this->modify($this->baseUrl));
}
- private function modify(string $url, array $queryParam = null)
+ private function modify(string $url, ?array $queryParam = null)
{
if (is_null($queryParam)) {
return Modify::value($url)->setQueryParam()->fetch();
diff --git a/tests/Routing/RouteBindingTest.php b/tests/Routing/RouteBindingTest.php
index bdc4d721e9..fd5ea972a0 100644
--- a/tests/Routing/RouteBindingTest.php
+++ b/tests/Routing/RouteBindingTest.php
@@ -169,8 +169,8 @@ public function binds_route_parameters_in_statamic_routes_with_bindings_disabled
*/
public function binds_route_parameters_in_frontend_routes(
$uri,
- Closure $enabledCallback = null,
- Closure $disabledCallback = null,
+ ?Closure $enabledCallback = null,
+ ?Closure $disabledCallback = null,
) {
$this->setupContent();
@@ -193,8 +193,8 @@ public function binds_route_parameters_in_frontend_routes(
*/
public function binds_route_parameters_in_frontend_routes_with_bindings_disabled(
$uri,
- Closure $enabledCallback = null,
- Closure $disabledCallback = null,
+ ?Closure $enabledCallback = null,
+ ?Closure $disabledCallback = null,
) {
$this->setupContent();
diff --git a/tests/Stache/Stores/AssetContainersStoreTest.php b/tests/Stache/Stores/AssetContainersStoreTest.php
index c822c415fd..b3ea2404d9 100644
--- a/tests/Stache/Stores/AssetContainersStoreTest.php
+++ b/tests/Stache/Stores/AssetContainersStoreTest.php
@@ -11,6 +11,7 @@
use Statamic\Facades\Path;
use Statamic\Stache\Stache;
use Statamic\Stache\Stores\AssetContainersStore;
+use Statamic\Stache\Stores\UsersStore;
use Tests\TestCase;
class AssetContainersStoreTest extends TestCase
@@ -111,6 +112,7 @@ public function it_saves_to_disk()
// irrelevant for this test but gets called during saving
Facades\Stache::shouldReceive('shouldUpdateIndexes')->andReturnTrue();
Facades\Stache::shouldReceive('duplicates')->andReturn(optional());
+ Facades\Stache::shouldReceive('store')->with('users')->andReturn((new UsersStore((new Stache)->sites(['en']), app('files')))->directory($this->tempDir));
$container = Facades\AssetContainer::make('new')
->title('New Container');
diff --git a/tests/StaticCaching/NocacheTagsTest.php b/tests/StaticCaching/NocacheTagsTest.php
index f33f0314e8..5929cb2882 100644
--- a/tests/StaticCaching/NocacheTagsTest.php
+++ b/tests/StaticCaching/NocacheTagsTest.php
@@ -106,10 +106,13 @@ public function it_can_keep_nested_nocache_tags_dynamic_inside_cache_tags()
/** @test */
public function it_only_adds_appropriate_fields_of_context_to_session()
{
- // We will not add `baz` to the session because it is not used in the template.
- // We will not add `nope` to the session because it is not in the context.
- $expectedFields = ['foo', 'bar'];
- $template = '{{ nocache }}{{ foo }}{{ bar }}{{ nope }}{{ /nocache }}';
+ $expectedFields = [
+ 'foo', // By adding @auto it will be picked up from the template.
+ 'baz', // Explicitly selected
+ // 'bar' // Not explicitly selected
+ // 'nope' // Not in the context
+ ];
+ $template = '{{ nocache select="@auto|baz" }}{{ foo }}{{ nope }}{{ /nocache }}';
$context = [
'foo' => 'alfa',
'bar' => 'bravo',
diff --git a/tests/Tags/ChildrenTest.php b/tests/Tags/ChildrenTest.php
new file mode 100644
index 0000000000..4a0bb85b05
--- /dev/null
+++ b/tests/Tags/ChildrenTest.php
@@ -0,0 +1,101 @@
+ [
+ 'en' => ['url' => '/', 'locale' => 'en_US'],
+ 'fr' => ['url' => '/fr/', 'locale' => 'fr_FR'],
+ ]]);
+ }
+
+ private function tag($tag, $data = [])
+ {
+ return (string) Parse::template($tag, $data);
+ }
+
+ private function setUpEntries()
+ {
+ $this->collection = tap(Collection::make('pages')->sites(['en', 'fr'])->routes('{slug}')->structureContents(['root' => false]))->save();
+
+ EntryFactory::collection('pages')->id('foo')->data([
+ 'title' => 'the foo entry',
+ ])->create();
+
+ EntryFactory::collection('pages')->id('bar')->data([
+ 'title' => 'the bar entry',
+ ])->create();
+
+ EntryFactory::collection('pages')->id('fr-foo')->origin('foo')->locale('fr')->data([
+ 'title' => 'the french foo entry',
+ ])->create();
+
+ EntryFactory::collection('pages')->id('fr-bar')->origin('foo')->locale('fr')->data([
+ 'title' => 'the french bar entry',
+ ])->create();
+
+ $this->collection->structure()->in('en')->tree([
+ ['entry' => 'foo', 'url' => '/foo', 'children' => [
+ ['entry' => 'bar', 'url' => '/foo/bar'],
+ ]],
+ ])->save();
+
+ $this->collection->structure()->in('fr')->tree([
+ ['entry' => 'fr-foo', 'url' => '/fr-foo', 'children' => [
+ ['entry' => 'fr-bar', 'url' => '/fr-foo/fr-bar'],
+ ]],
+ ])->save();
+ }
+
+ /** @test */
+ public function it_gets_children_data()
+ {
+ $this->setUpEntries();
+ $this->get('/foo');
+
+ $this->assertEquals('the bar entry', $this->tag('{{ children }}{{ title }}{{ /children }}', ['collection' => $this->collection]));
+ }
+
+ /** @test */
+ public function it_gets_children_data_when_in_another_site()
+ {
+ $this->setUpEntries();
+
+ $this->get('/fr/fr-foo');
+
+ $this->assertEquals('the french bar entry', $this->tag('{{ children }}{{ title }}{{ /children }}', ['collection' => $this->collection]));
+ }
+
+ /** @test */
+ public function it_doesnt_affect_children_in_nav()
+ {
+ $this->setUpEntries();
+
+ $mock = Mockery::mock(Children::class);
+ $mock->shouldNotReceive('index');
+
+ $this->app['statamic.tags']['children'] = $mock;
+
+ $this->get('/foo');
+
+ $this->assertEquals('the bar entry', $this->tag('{{ nav }}{{ children }}{{ title }}{{ /children }}{{ /nav }}'));
+ }
+}
diff --git a/tests/Tags/User/UserTagsTest.php b/tests/Tags/User/UserTagsTest.php
index 5cfe425223..73d12cba64 100644
--- a/tests/Tags/User/UserTagsTest.php
+++ b/tests/Tags/User/UserTagsTest.php
@@ -4,6 +4,7 @@
use Illuminate\Http\Exceptions\HttpResponseException;
use Statamic\Facades\Parse;
+use Statamic\Facades\Role;
use Statamic\Facades\User;
use Tests\FakesRoles;
use Tests\FakesUserGroups;
@@ -16,9 +17,9 @@ class UserTagsTest extends TestCase
FakesUserGroups,
PreventSavingStacheItemsToDisk;
- private function tag($tag)
+ private function tag($tag, $params = [])
{
- return Parse::template($tag, []);
+ return Parse::template($tag, $params);
}
/** @test */
@@ -68,6 +69,10 @@ public function it_renders_user_is_tag_content()
// Test if user is assigned any of these roles
$this->assertEquals('yes', $this->tag('{{ user:is role="webmaster|admin" }}yes{{ /user:is }}'));
$this->assertEquals('', $this->tag('{{ user:isnt role="webmaster|admin" }}yes{{ /user:isnt }}'));
+
+ // test if it handles the value of a user_roles tag
+ $this->assertEquals('yes', $this->tag('{{ user:is :roles="roles" }}yes{{ /user:is }}', ['roles' => Role::all()]));
+ $this->assertEquals('', $this->tag('{{ user:isnt :roles="roles" }}yes{{ /user:isnt }}', ['roles' => Role::all()]));
}
/** @test */
diff --git a/tests/TestCase.php b/tests/TestCase.php
index db295ac12a..6c3064b1af 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -74,7 +74,7 @@ protected function resolveApplicationConfiguration($app)
];
foreach ($configs as $config) {
- $app['config']->set("statamic.$config", require(__DIR__."/../config/{$config}.php"));
+ $app['config']->set("statamic.$config", require (__DIR__."/../config/{$config}.php"));
}
}
@@ -177,7 +177,7 @@ public static function assertArraySubset($subset, $array, bool $checkForObjectId
}
// This method is unavailable on earlier versions of Laravel.
- public function partialMock($abstract, \Closure $mock = null)
+ public function partialMock($abstract, ?\Closure $mock = null)
{
$mock = \Mockery::mock(...array_filter(func_get_args()))->makePartial();
$this->app->instance($abstract, $mock);
diff --git a/tests/View/Antlers/ParserTests.php b/tests/View/Antlers/ParserTests.php
index 08029ec3a5..ccd7a6c897 100644
--- a/tests/View/Antlers/ParserTests.php
+++ b/tests/View/Antlers/ParserTests.php
@@ -1144,7 +1144,7 @@ public function augment($value)
return 'augmented '.$value;
}
- public function config(string $key = null, $fallback = null)
+ public function config(?string $key = null, $fallback = null)
{
return true;
}
@@ -1158,7 +1158,7 @@ public function augment($value)
return 'augmented '.$value;
}
- public function config(string $key = null, $fallback = null)
+ public function config(?string $key = null, $fallback = null)
{
return false;
}
diff --git a/translator b/translator
index 2a77af13c6..94c7d69e38 100644
--- a/translator
+++ b/translator
@@ -76,6 +76,7 @@ $additionalKeys = [
'float.title',
'form.title',
'grid.title',
+ 'group.title',
'hidden.title',
'html.title',
'icon.title',